原文地址:http://blog.chinaunix.net/u/12783/showart_722257.html
前面讲到了进程的上下文以及如何管理/操作进程的地址空间,其中,里面提到了一些调用。这些调用与下文有一定的关联,在此列出:
fork():调用dupreg(),attachreg()
exec():调用detachreg(),allocreg(),attachreg(),growreg(),loadreg(),mapreg()
brk():调用growreg()
eixt():调用detachreg()
当然还有其它的一些调用与进程控制有关,如signal(),wait(),kill()等,以后会讲到。
创建进程
一个进程的生命起点是fork(),由其父进程调用以产生该进程。fork()是UNIX系统上唯一一个能够创建新进程的调用,当然,除了init进程,这个进程是由内核在系统boot时创建的,其PID是0。系统启动完成后,init进程将推出,取而代之的是PID为1的swap进程(有时候又叫做idle进程)。
在执行fork()时,kernel将会做一些列的操作:
1、在进程表中为新进程申请一个表项;
2、为新进程分配一个唯一的PID;
3、将父进程的上下文及地址空间做一个逻辑拷贝——[color=red]一般来说,只需要拷贝数据段和堆栈段等,文本段无需拷贝,因为父子进程共享该段[/color]。若子进程立即调用exec()家族的函数,则更不需要拷贝文本段,数据段和堆栈段也无需拷贝,因为将会有一个全新的image加载到子进程并运行;
4、因为父子进程共享文件,所以增加文件和inode的引用计数;
5、将子进程的状态设置为Read to Run;
6、返回两次:将子进程PID返回给父进程(可用于跟踪子进程状态),将0返回给子进程;
关于fork()的实现,请求分页和交换系统有所不同,本文将讨论交换系统上的实现,并假定有足够的内存来创建子进程。以后讨论分页系统的实现。下面是伪代码:
fork()的实现是相当复杂的,但是,抓住三点就好理解了:上下文、调度、共享资源。在将子进程放入调度队列之前,先设置子进程的上下文信息(包含那个假系统级上下文层)。当子进程被调度,它将执行从“if (IsParent())”开始的那段代码。同样,父进程在设置子进程的上下文信息后也执行这段代码,看起来好像父子进程都是在该点“被调度”,从运行态转为其他状态。
在交换系统上,创建进程时需要两份空间来存放子进程:一份在内存,一份在磁盘——因为子进程的状态为READ_TO_RUN,如果此时系统没用可用内存,而它又处于低优先级,它就有可能被交换到磁盘上。另外需要注意的是,在获取空闲的进程表项时需要有一定的锁机制来避免竞态条件。
获取了进程表项之后,子进程便处于“被创建”(或“正被创建”)状态。此时,内核将会让这个新生儿“继承”其父进程的进程表项,并将父进程的pid设置到子进程,以便子进程识别父进程。同时,子进程将被放到进程的树结构中——同时还会设置其优先级、初始化调度参数等等。
之后便是调整一些引用计数。关于打开的文件的引用计数,大家可以用经典的“协同进程”方法做实验。
还记得u area吗?只有通过u area,内核才能识别一个运行中的进程。它也是进程上下文中静态系统级上下文的一员。然而,在fork()时,除了其中进程表项的指针,父子进程的u area完全一样。当然,在fork()执行完之后,这两个u area就可能不一样了。
好了,进程所需的所有的静态上下文都已经创建好了,内核需要做的就是为子进程populate一个假系统上下文层。大家要注意了!内核将父进程的第一层上下文拷贝给子进程,该层包括用户保存的寄存器上下文和调用fork()的内核栈楨,然后创建一个假的系统上下文层,该层保存了上一层以及其他的寄存器信息,设置正确的PC(“if (IsParent())”),以保证子进程能够被正确的“恢复执行”——虽然子进程从来没有执行过。
前面讲到了进程的上下文以及如何管理/操作进程的地址空间,其中,里面提到了一些调用。这些调用与下文有一定的关联,在此列出:
fork():调用dupreg(),attachreg()
exec():调用detachreg(),allocreg(),attachreg(),growreg(),loadreg(),mapreg()
brk():调用growreg()
eixt():调用detachreg()
当然还有其它的一些调用与进程控制有关,如signal(),wait(),kill()等,以后会讲到。
创建进程
一个进程的生命起点是fork(),由其父进程调用以产生该进程。fork()是UNIX系统上唯一一个能够创建新进程的调用,当然,除了init进程,这个进程是由内核在系统boot时创建的,其PID是0。系统启动完成后,init进程将推出,取而代之的是PID为1的swap进程(有时候又叫做idle进程)。
在执行fork()时,kernel将会做一些列的操作:
1、在进程表中为新进程申请一个表项;
2、为新进程分配一个唯一的PID;
3、将父进程的上下文及地址空间做一个逻辑拷贝——[color=red]一般来说,只需要拷贝数据段和堆栈段等,文本段无需拷贝,因为父子进程共享该段[/color]。若子进程立即调用exec()家族的函数,则更不需要拷贝文本段,数据段和堆栈段也无需拷贝,因为将会有一个全新的image加载到子进程并运行;
4、因为父子进程共享文件,所以增加文件和inode的引用计数;
5、将子进程的状态设置为Read to Run;
6、返回两次:将子进程PID返回给父进程(可用于跟踪子进程状态),将0返回给子进程;
关于fork()的实现,请求分页和交换系统有所不同,本文将讨论交换系统上的实现,并假定有足够的内存来创建子进程。以后讨论分页系统的实现。下面是伪代码:
PID fork()
{
Assert(EnoughMemory && EnoughResources);
PID aPid = GetFreeProcTableEntry(); // 同时得到进程表项。
Assert(NumOfProcs(GetCurrentUser()) < MAX_PROC_NUM_PER_USER); // 保证一个用户不会创建太多进程。
procTable[aPid]->State = CREATED;
CopyData(procTable[aPid], procTable[getppid()]); // 从父进程的表项中拷贝数据,如有效uid,当前目录等等。
/* 增加当前目录及改变的根的inode引用计数;
增加文件表中打开文件的引用计数;
拷贝一份父进程的上下文(包括u area,文本段、堆栈段、数据段);
将一个假系统级上下文层压到子进程的系统上下文栈,该上下文层包括能够让子进程识别自己的一些信息,并且当子进程获得处理器时从此处开始执行; // 就像进程被调度时保存的上下文。*/
if(IsParent()) // 一旦被调度,将从假系统级上下文层开始执行,父子进程将共享下面的代码。
{
procTable[aPid]->State = READY_TO_RUN; // 此时子进程才可能被调度。
return aPid;
}
else
{
初始化u area的与分时相关的一些字段;
return 0;
}
}
fork()的实现是相当复杂的,但是,抓住三点就好理解了:上下文、调度、共享资源。在将子进程放入调度队列之前,先设置子进程的上下文信息(包含那个假系统级上下文层)。当子进程被调度,它将执行从“if (IsParent())”开始的那段代码。同样,父进程在设置子进程的上下文信息后也执行这段代码,看起来好像父子进程都是在该点“被调度”,从运行态转为其他状态。
在交换系统上,创建进程时需要两份空间来存放子进程:一份在内存,一份在磁盘——因为子进程的状态为READ_TO_RUN,如果此时系统没用可用内存,而它又处于低优先级,它就有可能被交换到磁盘上。另外需要注意的是,在获取空闲的进程表项时需要有一定的锁机制来避免竞态条件。
获取了进程表项之后,子进程便处于“被创建”(或“正被创建”)状态。此时,内核将会让这个新生儿“继承”其父进程的进程表项,并将父进程的pid设置到子进程,以便子进程识别父进程。同时,子进程将被放到进程的树结构中——同时还会设置其优先级、初始化调度参数等等。
之后便是调整一些引用计数。关于打开的文件的引用计数,大家可以用经典的“协同进程”方法做实验。
还记得u area吗?只有通过u area,内核才能识别一个运行中的进程。它也是进程上下文中静态系统级上下文的一员。然而,在fork()时,除了其中进程表项的指针,父子进程的u area完全一样。当然,在fork()执行完之后,这两个u area就可能不一样了。
好了,进程所需的所有的静态上下文都已经创建好了,内核需要做的就是为子进程populate一个假系统上下文层。大家要注意了!内核将父进程的第一层上下文拷贝给子进程,该层包括用户保存的寄存器上下文和调用fork()的内核栈楨,然后创建一个假的系统上下文层,该层保存了上一层以及其他的寄存器信息,设置正确的PC(“if (IsParent())”),以保证子进程能够被正确的“恢复执行”——虽然子进程从来没有执行过。