fork那点事
fork 总结
fork()通过复制调用进程来创建一个新进程。在Linux下,fork()是通过使用写时复制页面实现的,所以它唯一的缺点是复制父页表的时间和内存,并为子进程创建独特的任务结构。
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。 fork后这两个进程的变量都是独立的,存在不同的地址中。
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。
我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
父子进程不同点
1)子进程有自己独特的进程ID,并且此ID与任何现进程组里的ID不一样。
2)子进程不会继承父进程的记忆锁(mlock(2),mlockall(2) )。
3)子进程资源利用率(getrusage(2))和CPU时间计数器(次数(2) )重置为零。
4)子进程的待决信号集最初是空的(sigpending(2))。
5)子项不会从父进程项继承信号量调整(semop(2))。
6)子进程不从父进程继承记录锁(fcntl(2))。
7)子进程不从其父进程(setitimer(2),alarm(2),timer_create(2))继承定时器。
8)子进程不从父级继承未完成的异步I/O操作(aio_read(3),aio_write(3)),也不从父级继承任何异步I/O上下文.
9)子进程不会从其父进程继承目录更改通知(dnotify)(请参阅fcntl(2)中的F_NOTIFY说明)
10)子进程终止后会发出SIGCHLD信号。
11)由ioperm(2)设置的端口访问权限位不被子进程继承; 子进程必须使用ioperm打开它需要的任何位
12)如果进程的一个线程调用fork()函数,父进程的整个虚拟地址空间在子级中复制,包括状态互斥量,条件变量和其他线程对象(pthreads);(可以使用pthread_atfork(3)对复制范围进行限制)。
子进程从父进程可继承的资源
1)子进程继承父母的一组打开文件描述符的副本(参考open(2))
2)子进程继承父母的一组开放消息队列描述符的副本(参考mq_overview(7))
3)子进程继承父母打开的目录流集的副本(opendir(3))
fork和vfork的异同
1)vfork()就像fork(2)一样为调用进程创建子进程。
2)vfork()是clone(2)的特例。 它用于创建新进程而不复制父进程的页表。 它可能在子进程创建后对性能敏感的应用程序中很有用,然后立即运行一个execve()。
3)vfork()与fork(2)的不同之处在于,调用线程被挂起直到子进程终止(通常通过调用_exit(2),或者在传送致命信号后异常),或者调用execve2)。在那之前,孩子与父母共享所有内存,包括堆栈。孩子不得从当前函数返回或调用exit(3),但可以调用_exit(2)。
4)与fork(2)一样,由vfork()创建的子进程继承了各种调用者的进程属性(例如,文件描述符,信号处置和当前工作目录)的副本。vfork()调用仅在处理虚拟地址空间方面有所不同。
测试代码
这里我就先分析下面几个例子,后续的例子在进程通信、线程池、进程同步中会附带的举例说明。本次我们就先深入了解fork()函数。
fork()进程的变量都是独立的,存在不同的地址中,不是共用的
#include<unistd.h> #include<stdio.h> intmain(void) { int i=0; pid_t rpid; int count=0; rpid=fork(); if(rpid<0){ return -1; } if(rpid==0){ printf(" child %d %d %d\n",getpid(),getppid(),rpid); count++; //child process add count } else{ printf(" parent %d %d%d\n",getpid(),getppid(),rpid); count=100; //parent process assign 100 to count } printf("pid%d,%d\n",getpid(),count); //print process id and count value; return 0; }
运行结果:
parent1417 1095 1418
pid1417,100
child 1418 1 0
pid 1418,1
从运行结果上可以看出父进程先运行,子进程如果没有了父进程那么此时它的父进程ID就会是1。并且,count在父进程和子进程的值不一样。父进程的count为100,子进程的count为1。
fork()函数的调用次数与数量分析
两次fork()
#include<unistd.h> #include<stdio.h> intmain(void) { int i=0; pid_t rpid; rpid=fork(); if(rpid<0){ return -1; } if(rpid==0) printf(" child %d %d %d\n",getpid(),getppid(),rpid); else printf(" parent %d %d%d\n",getpid(),getppid(),rpid); rpid=fork(); if(rpid<0){ return -1; } if(rpid==0) printf(" child %d %d %d\n",getpid(),getppid(),rpid); else printf(" parent %d %d%d\n",getpid(),getppid(),rpid); return 0; }
测试结果
parent1689 1095 1690
child 1690 1689 0
parent 1689 1095 1691
parent 1690 1 1692
child 1691 1 0
child 1692 1 0
从测试结果可以看出来,进程1689为调用fork的进程,经过第一次调用生成1690子进程。再调用第二次fork的时候1689又生成一个子进程为1691,由于系统先运行父进程,所以子进程的父进程ID重新设置为1 。第一次生成的子进程也会调用第二个fork函数来生成子进程1692。同理,父进程先运行,子进程1692的父进程也设置为1。如下图所示:
三次fork()
#include<unistd.h> #include<stdio.h> intmain(void) { int i=0; pid_t rpid; rpid=fork(); if(rpid<0){ return -1; } if(rpid==0) printf(" child %d %d %d\n",getpid(),getppid(),rpid); else printf(" parent %d %d%d\n",getpid(),getppid(),rpid); rpid=fork(); if(rpid<0){ return -1; } if(rpid==0) printf(" child %d %d %d\n",getpid(),getppid(),rpid); else printf(" parent %d %d%d\n",getpid(),getppid(),rpid); rpid=fork(); if(rpid<0){ return -1; } if(rpid==0) printf(" child %d %d %d\n",getpid(),getppid(),rpid); else printf(" parent %d %d%d\n",getpid(),getppid(),rpid); return 0; }
运行结果:
parent1842 1095 1843
parent 1842 1095 1844
parent 1842 1095 1845
child 1843 1842 0
child 1845 1 0
parent 1843 1 1846
child 1844 1 0
child 1846 1843 0
parent 1843 1 1847
parent 1844 1 1848
child 1847 1 0
child 1848 1 0
parent 1846 1 1849
child 1849 1 0
从上面的运行结果来看,主进程1842总是优先执行。在执行的过程中调用了3次fork。第一次fork生成1843进程,第二次fork生成1844进程,第三次fork生成1845进程。接下来1843运行第二个fork和第三个fork分别生成进程1846和进程1847。1846进程又运行了最后一个fork生成1849。总进程第二个fork生成的1844进程运行第三个fork生成子进程1848。进程1845是进程在调用第三个fork生成的子进程,由于程序没有fork了,所以它就没有子进程。进程的关系如下图所示:
进程的总数与调用fork的关系
从数学推导中可以推出进程的总数与调用fork的关系为f(N) = 2^N。
参考资料
1.man fork