xv6源码剖析 009
我们继续昨天的内容。proc.h
和proc.c
growproc(int n)
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}
当我们的进程使用sbrk()
系统调用申请或者释放内存时,内核会在底层调用这个函数。
fork(void)
我们重点看看这个函数,我们在写代码的时候也会常常用到这个函数,有些时候我们会和另一个系统调用exec
一起调用。
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// 将父进程的页表拷贝到子进程中,
// 所以子进程是父进程的一个副本
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
// 设置进程栈
np->sz = p->sz;
// 标识父进程
np->parent = p;
// 复制父进程trapframe
// trapframe保存着父进程的寄存器的状态
*(np->trapframe) = *(p->trapframe);
// Cause fork to return 0 in the child.
// 设置返回值
np->trapframe->a0 = 0;
// increment reference counts on open file descriptors.
// 增加父进程打开的文件描述符的引用计数
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
// 复制父进程的名字
safestrcpy(np->name, p->name, sizeof(p->name));
// 标识子进程
pid = np->pid;
// 设置子进程的状态,能够被调度器调度
np->state = RUNNABLE;
// 释放子进程的进程锁
release(&np->lock);
// 从父进程中返回子进程的进程id
return pid;
}
有几个点值得我们注意的:
-
一般情况下,我们是这样使用fork的
int main() { do_something(); int pid; if ((pid = fork()) == 0) { do_something_child(); } else if (pid > 0) { do_something_parent(); } else { exit(0); } }
从上面的fork的代码我们可以很好地理解为什么要这样使用,而不是像线程一样直接使用就好了。
子进程和父进程是相互独立的,它们并不共享相同的资源,这是由操作系统的隔离机制决定的,但是线程不一样,线程是运行在进程中的,而且一个进程中的不同进程是共享一部分进程的内存的;但是不同进程之间的线程就像进程一样,它们的交互是通过进程间通信(ipc)的。
从代码中,我们知道,当调用fork的时候,内核会完全复制父进程的页表,进程地址空间,trapframe page和相应的文件描述符,等等,唯一不同的就是,内核会直接将父进程的返回值返回(在代码中),而对于子进程则是将返回值保存在一个寄存器中;其实本质上,它们都是相同的,只是在代码中的体现有所不同,因为这个调用是由父进程引起的。
-
增减描述符的引用计数
这个小小的细节也值得我们注意。看下面的ipc通信的小例子。
void test() { int pipe[2]; // 初始化一个匿名管道 ::pipe(pipe); char buff[512]; int pid; if ((pid = fork()) == 0) { memset(buff, 0, sizeof(buff)); read(pipe[0], buff, sizeof(buff)); do_something(buff); } else if (pid > 0) { write(pipe[1], buff, sizeof(buff)); do_something(); } else { exit(1); } }
上面是一个常规匿名管道的应用。我们可以思考一下,为什么可以通过一个pipe来进行进程间的通信,明明一个进程在fork的时候,会将父进程的所有内容都进行拷贝?
文件描述符是文件系统(file system)的一个高级抽象,它的底层是不同类型的数据存储对象,我们在后面文件系统的时候会讲到。现在我们只需要将文件描述符看成一个c语言中的指针,虽然进行了拷贝,但是父进程和子进程指向的内容是一样的,并且操作系统还会在内部维护一个指向该文件的引用计数(reference count),每当用户进程调用close()的时候,就会将这个引用计数减一,当计数为零并且连接数(link)为零时就会关闭这个文件。
-
copy-on write
这个机制也属于xv6实验的范畴,所以我只简单讲述一下,
在内核拷贝父进程的页表时,函数uvmcopy()需要递归地遍历整个页表,以复制整个父进程的进程地址空间。当父进程的空间过大的,扫描就非常耗时,所以,我们一般是直接拷贝父进程页表中的映射(mapping),相当于拷贝一个指针到子进程中,并将每一个pte都设置为只读(read only),当父进程或子进程尝试写这个页面的时候,就会引发缺页中断(page fault)从而陷入(trap)内核,内核将一部分的内存进行复制并重新建立映射关系。然后返回用户空间,重新执行代码。
今晚时间比较少,写的可能少了点,sorry!!!