在linux系统中,第一个进程是系统固有的,与生俱来的或者说是由内核的设计者安排好了的,内核在引导并完成了基本的初始化以后,就有了系统第一进程(实际上是内核线程)。除此之外,所有其他的进程和内核线程都有这个原始进程或其子孙进程所创建,都是这个原始进程的后代。
linux将进程的创建和执行分成两步。
第一步是从已存在的“父进程”中像细胞分裂一样地复制出一个“子进程”。复制出来的子进程有自己的task_struct结构和系统空间堆栈,单与父进程共享其他所有的资源。例如要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方,所以这一步所做的是“复制”。为此,linux系统提供了两个系统调用。
1)fork():此调用是全部复制,父进程所有的资源全部通过数据结构的复制“遗传给”子进程,并且fork()调用为无参调用。
2)clone():此调用可以将资源有选择地复制给子进程,而没有复制的数据结构则可以通过指针的复制让子进程共享。极端情况下,一个进程可以通过clone()创建出一个线程。既然是有选择复制,clone()是有参调用。
有一个和fork()相似的调用vfork(),同样不带参数,但是除了task_struct和系统空间堆栈以外的资源全部通过数据结构指针的复制“遗传”。所以和clone()的极端情况一样,vfork出来的是线程而不是进程(注意分清在linux系统中进程和线程的区别:对于各个用户线程来说,有独自的task_struct和系统空间堆栈,但是都共享3G用户空间;对于各个用户进程来说,有独自的task_struct、系统空间堆栈以及3G用户空间)。
第二步是目标程序的执行,一般来说 ,一个进程的创建,是因为有不同的目标程序要让新的程序去执行()。所以创建出的子进程要与父进程“分道扬镳”,走自己的路,linux为此提供了一个系统调用execve(),使得一个进程可以执行以文件形式存在的可执行程序的映像。
创建了子进程以后,父进程有三个选择。第一是继续走自己的路,与子进程分道扬镳。但是如果子进程先于父进程“去世”,则由内核给父进程发送一个报丧的信号。第二是停下来,进入睡眠状态,等待子进程完成其使命至最终去世,然后再由父进程继续执行。系统提供了两个系统调用,wait4()和wait3()。两个系统调用基本相同,wait4()等待某个特定的子进程去世,wait3()则等待任何一个子进程去世。第三种选择是“自行退出历史舞台”,结束自己的生命。linux为此设置了一个系统调用exit()。第三种选择是第一种选择的特例。
下面程序演示进程的“生命周期”:
1 #include<stdio.h> 2 3 int main() 4 { 5 int child; 6 char *args[] = {"/bin/echo", "hello", "world!", NULL}; 7 8 if(!(child = fork())) 9 { 10 /*child*/ 11 printf("pid %d: %d is my father\n", getpid(), getppid()); 12 execve("/bin/echo", args, NULL); 13 printf("pid %d: I am back, something is wrong!\n", getpid()); 14 } 15 else 16 { 17 int myself = getpid(); 18 printf("pid %d: %d is my son\n", myself, child); 19 wait4(child, NULL, 0, NULL); 20 printf("pid %d: done\n", myself); 21 } 22 return 0; 23 }
这里,进入main()的进程为父进程,它在第8行执行了系统调用fork()创建了一个进程,也就是复制了一个子进程。子进程复制出来以后,就像其父进程一样地接受内核的调度,而且具有相同的返回地址。所以当父进程和子进程受调度继续运行而从内和空间返回时都返回到同一点上。一千多的大娘们只有一个进程执行,而从这一点却有两个进程在执行了。复制出来的进程全面的继承了父进程的所有资源和特性,但是还有一些区别。
第一,子进程有不同于父进程的进程号pid,而且子进程的task_struct中有几个字段说明谁是它的父亲。
第二,二者从fork()返回时所具有的返回值不一样。当子进程从fork()返回时,其返回值为0;而父进程从fork()返回时的返回值确是子进程的pid,这是不可能为0的。
在这程序程中,if语句吧父子进程区分开来。然后第10-12(没有13行,原因接下来说)行属于子进程,第16-19行属于父进程,各自执行各自的路线。在此进程中,我们选择让父进程停下来等待,所以父进程调用wait4();而子进程通过execve()执行“/bin/echo”。子进程在执行echo以后不会再执行第十三行,而是“壮士一去兮不复返”。这是因为在echo中必定有一个exit()调用,使子进程结束它的生命。对exit()的调用是每个可执行程序映像必有的,虽然我们在这个程序中并没有调用它,而是以return语句从main()返回,但是gcc在编译和连接时会自动加上,所以每个程序都逃不过这一关。
参考:
毛德操,胡希明《linux内核源代码情景分析》(上册)