1.1节 进程和内存
这一节涉及的系统调用有:
fork
exit
wait
一个xv6进程由用户空间内存(指令、数据、栈)和内核专有的进程状态组成。
xv6分时共享进程,即xv6透明地在一组等待执行的进程间切换可用的CPU。
当一个进程不执行时,xv6保存该进程的CPU寄存器,当下次运行该进程时,恢复先前保存的CPU寄存器值。
内核给每个进程关联一个进程标识符或者PID。
理解系统调用fork
一个进程可使用fork
系统调用来创建新的进程。
fork
创建一个新的进程,即子进程:子进程跟父进程有着同样的内存内容。
fork
在父进程和子进程中都会返回。
- 对父进程来说,
fork
的返回值是子进程的PID; - 对子进程来说,
fork
的返回值是0;
示例代码1
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");
}
输出:
parent: child=1234
child: exiting
parent: child 1234 is done
系统调用exit
- 会导致调用进程停止执行,释放诸如内存和打开文件等资源。
- 参数是整数类型的状态参数,约定0表示成功,1表示失败;
系统调用wait
- 返回值是当前进程的已退出的子进程的PID,并拷贝子进程的退出状态给传递给
wait
的地址; - 如果调用者的子进程没有一个退出,则
wait
就等待着其中一个退出; - 如果调用者没有子进程,则wait立即返回-1;
- 如果父进程不关心子进程的退出状态,则父进程可传递一个0地址给
wait
。
注意:
- 虽然最初子进程和父进程有着相同的内存内容,但是父进程和子进程是使用的是不同的内存和寄存器,即修改一个进程中的变量不会影响另一个进程中的变量的值。比如,在父进程中,wait函数的返回值保存在变量pid中,这并没有修改子进程中的pid的值,子进程中的pid的值仍是0。
xv6系统使用的ELF格式。
系统调用exec
- 有两个参数,一个是可执行文件的名字,一个是字符串数组参数;
- 使用从存储在文件系统里的文件中加载的新的内存映像来替换调用进程的内存。
- 前述的文件必须有一个特定的格式,描述了文件的哪个部分持有指令,哪个部分持有数据,从哪个指令开始执行等。
- 当exec执行成功后,它并不返回给调用程序,而开始执行在ELF头里声明的的入口处的指令。
代码示例2
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程序的主体结构很简单:
int
main(void)
{
static char buf[100];
int fd;
// Ensure that three file descriptors are open.
while((fd = open("console", O_RDWR)) >= 0){
if(fd >= 3){
close(fd);
break;
}
}
// Read and run input commands.
while(getcmd(buf, sizeof(buf)) >= 0){
if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
// Chdir must be called by the parent, not the child.
buf[strlen(buf)-1] = 0; // chop \n
if(chdir(buf+3) < 0)
fprintf(2, "cannot cd %s\n", buf+3);
continue;
}
if(fork1() == 0)
runcmd(parsecmd(buf));
wait(0);
}
exit(0);
}
解释:
主循环
首先,父进程使用getcmd
从用户读取一行输入;
然后,父进程调用fork
创建了shell进程的拷贝,即子进程;
最后,父进程调用wait
,子进程运行命令。
比如,用户在shell上键入了"echo hello",则runcmd
被调用使的参数是"echo hello"。
runcmd会运行实际的命令。对于"echo hello",runcmd
会调用exec
。
如果exec成功了,则子进程将执行来自echo
的指令而不是runcmd
。
在某个点处,echo
会调用exit
,会导致父进程从wait
中返回。
为什么不将fork
和exec
整个成一个调用?
shell使用fork
和exec
的这种隔离来实现I/O重定向。
为了避免创建一个拷贝进程带来的浪费,然后立即用exec
来替换子进程,内核通过使用诸如copy-on-write等的虚拟内存技术来优化fork
的实现。
xv6都是隐式地分配大部分用户空间内存:
-
fork
分配了父进程内存的子进程拷贝所需的内存; -
exec
分配了足够的内存来持有可执行文件;
如果一个进程在运行时需要更多的内存,则该进程可调用sbrk(n)
来使其数据内存增长n个字节。
注意:
sbrk
的返回值是新的内存的地址。