一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
原文链接 xv6-riscv文档
1.1 进程与内存
xv6的进程由用户空间的内存(指令、数据和栈)以及只对内核可见的进程状态组成。xv6的进程采取分时复用的方式:它在可使用的CPU之间切换,运行等待运行中的程序。当一个程序不执行时,xv6保存它的CPU寄存器直到下次运行这个程序。内核通过**进程描述符(process identifier)**或者说PID来识别进程。
进程也许会通过fork系统调用创建新的进程。fork给新的进程和创建它的进程一样的内存内容(指令与数据)。fork会在原进程和新的进程中都返回(译者注:也即所谓的一次运行,两次返回)。在原进程,fork返回新进程的PID;而对于新进程,fork返回0。我们可以把原进程认为是父进程,而新进程认为是子进程。
例如,我们看下面的C代码:
int pid = fork();//通过返回值判断父进程还是子进程
if(pid > 0){
//父进程
printf("parent: child=%d\n", pid);
pid = wait((int *) 0);
printf("child %d is done\n", pid);
} else if(pid == 0){
//子进程
printf("child: exiting\n");
exit(0);
} else {
printf("fork error\n");
}
exit系统调用会让调用它的进程停止运行并且释放像内存和打开的文件这样的资源。exit携带着一个整数状态,通常0表示成功而1表示失败。而wait系统调用返回它调用了exit的子进程的PID,并且从传递给wait的地址复制exit携带者的退出信息;而如果没有子进程调用exit,则父进程就会等待直到有个子进程exit;如果该父进程没有子进程,则wait会立即返回-1,表示失败。如果父进程并不需要子进程的退出信息,则可以传递一个0地址给wait。
在上述的例子中,输出是:
parent: child=1234
child: exiting
输出可能会以其他的顺序或者错乱的顺序输出,这取决于父进程和子进程的printf语句谁先被调用。在子进程退出后,父进程的wait返回,从而输出
parent: child 1234 is done
尽管父进程和子进程有相同的内存内容,但是父进程和子进程执行在不同的内存地址和寄存器之上,修改其中一个的变量并不会影响另一个。例如,如果wait的返回值被存储在父进程的pid变量之中,它也不会改变子进程中pid变量的值,pid变量的值仍然为0。
exec系统调用用从文件中加载新的内存镜像替换调用进程内存。这个文件必须有一个特别的形式,必须严格要求哪部分存放指令哪部分放数据以及从哪条指令开始执行。xv6使用ELF格式的文件,在第三章中我们将会详细介绍。当exec调用成功时,他不会返回给调用进程;与之相反的,被加载进来的指令将从ELF表示的地方开始被运行。exec携带两个参数,包含着可执行镜像的地址以及一系列字符变量。例如:
char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");
这段代码用"/bin/echo"程序替换了该进程内存,同时传入了"echo hello"的参数。大部分的程序忽略参数列表的第一个元素,因为其通常是这个程序的名字。
xv6的shell使用以上的系统调用来给用户调用程序。shell的主要结构是简单的,可以看main。主循环通过getcmd从用户这读入一行输入,之后其调用fork,来创建一个新的shell进程。在这之后,父进程调用wait等待子进程运行命令。例如,当用户输入"echo hello"到shell的时候,runcmd以这句话为参数调用。runcmd会实际运行这句指令。对于"echo hello",他将会调用exec。如果exec成功,那么子进程会执行echo的指令而不是runcmd。有些时候echo会调用wait,这使得父进程从wait返回,参见main。
你也许会好奇为什么fork和exec不能组合成为一个单独的系统调用,我们后面会在IO的实现中看到shell是如何利用这种分开的。为了避免无意义的创建进程并且能够理解替换,操作系统内核通过使用虚存技术比如写时复制优化了fork的实现。
xv6通常隐式的分配用户空间:fork在复制时分配需要的空间,而exec分配可执行文件需要的空间。进程在运行时需要更多的空间可以调用sbrk来增加n字节的空间,sbrk通常返回新的空间的地址。
今天的内容主要集中于fork、exec、wait、exit等系统调用,如果曾经使用过多进程的同学就会理解到自己的多进程是如何实现的了。当然,只做这些阅读是很难理解具体如何实现的,后续我们将会结合具体的代码进行讲解。
感谢阅读。