1、Linux下进程的结构
一个进程,主要包含三个元素: 一个可以执行的程序;和该进程相关联的全部数据(包括变量,内存空间,缓冲区等等); 程序的执行上下文(execution context)。
Linux下一个进程在内存里有三部份的数据,就是“数据段”,“堆栈段”和“代码段。“代码段”,顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一个代码段。堆栈段存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(比如用malloc之类的函数取得的空间)。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。
2、fork()的使用
fork() creates a child process that differs from the parent process only in its PID and PPID, and in the fact that resource utilizations are set to 0. File locks and pending signals are not inherited.
一个程序一调用fork函数,系统就为一个新的进程准备了前述三个段,首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。而如果两个进程要共享什么数据的话,就要使用另一套函数(shmget,shmat,shmdt等)来操作。现在,已经是两个进程了,对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零,这样,对于程序,只要判断fork函数的返回值,就知道自己是处于父进程还是子进程中。
典型的代码如下:
void main(){
if ( fork() == 0 ) {
/* 子进程程序 */
printf("This is child process\n");
}
else {
/* 父进程程序*/
printf("This is process process\n");
}
}
3、关于fork()性能的疑问
如果一个大程序在运行中,它的数据段和堆栈都很大,如果fork只是简单地复制所有的数据,那系统开销会变得很大。在Linux系统里面,无论是数据段还是堆栈段都是由许多页构成的,fork函数复制这两个段,只是“逻辑”上的,并非“物理”上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的“页”从物理上也分开。这就是 Linux系统的COW(Copy-On-Write)机制,这样系统在空间上的开销就可以达到最小。
在fork()的man page里有关于此的说明:
Under Linux, fork() is implemented using copy-on-write pages, so the only penalty that it incurs is the time and memory required to duplicate the parent’s page tables, and to create a unique task structure for the child.
4、fork()后父进程与子进程的调度
依赖于具体系统的调度算法。如果需要父子进程协同,需要通过原语来解决。
fork在英文中是叉子,分叉的意思,在函数fork中,取后面的意思。很形象的表示程序从这里分叉,fork函数创建了子进程,子进程和父进程同时(其实是cpu分时处理)开始运行分叉之后的程序。
如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,getpid()返回的才是各自真是的进程号。
先看这段范例代码:
#include < sys / types.h > ;
main ()
{
pid_t pid;
pid=fork();
if (pid < 0)
printf("error in fork!");
else if (pid == 0)
printf("i am the child process, my process id is %dn",getpid());
else
printf("i am the parent process, my process id is %dn",getpid());
}
这段代码写了一个使用fork函数创建子进程,父子进程同时运行而产生交错的,不一样的运行结果。
运行结果如下:
[root@localhost c]# ./a.out
i am the child process, my process id is 4286
i am the parent process, my process id is 4285
fork在英文中是叉子,分叉的意思,在函数fork中,取后面的意思。很形象的表示程序从这里分叉,fork函数创建了子进程,子进程和父进程同时(其实是cpu分时处理)开始运行分叉之后的程序。
我把程序改写了一下:
#include < sys / types.h >
main()
{
pid_t pid;
printf("\n[%d]not fork pid=%d\n",getpid(),pid);
pid=fork();
printf("\n[%d]forked pid=%d\n",getpid(),pid);
if(pid<0)
{
printf("error in fork!\n");
getchar();
exit(1);
}
else if(pid==0)
printf("\n[%d]in child process,p_id=%d\n",getpid(),getpid());
else
{
printf("\n[%d]in parent process,my pid=%d\n",getpid(),pid);
printf("\n[%d]in parent process,my getpid=%d\n",getpid(),getpid());
}
}
程序运行结果如下:
[hardy@localhost fork]$ ./fork
[3819]not fork
[3820]forked pid=0
[3820]in child process,p_id=3820
[3819]forked pid=3820
[3819]in parent process,my pid=3820
[3819]in parent process,my getpid=3819
可以清楚的看到 not fork只打印了一次,其中[3819]是父进程的进程号,创建fork以后,fork函数返回给父进程的值pid是子进程的进程号[3820],而在子进程中,pid值为零。也就是说子进程中,pid被置零。
在linux中,只有一个函数可以创建子进程:fork。
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
由f o r k创建的新进程被称为子进程( child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程I D。将子进程I D返回给父进程的理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以获得其所有子进程的进程I D。f o r k使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用g e t p p i d以获得其父进程的进程I D (进程ID 0总是由交换进程使用,所以一个子进程的进程I D不可能为0 )。
子进程和父进程共享很多资源,除了打开文件之外,很多父进程的其他性质也由子进程继承:
• 实际用户I D、实际组I D、有效用户I D、有效组I D。
• 添加组I D。
• 进程组I D。
• 对话期I D。
• 控制终端。
• 设置-用户- I D标志和设置-组- I D标志。
• 当前工作目录。
• 根目录。
• 文件方式创建屏蔽字。
• 信号屏蔽和排列。
• 对任一打开文件描述符的在执行时关闭标志。
• 环境。
• 连接的共享存储段。
• 资源限制。
父、子进程之间的区别是:
• fork的返回值。
• 进程I D。
• 不同的父进程I D。
• 子进程的t m s _ u t i m e , t m s _ s t i m e , t m s _ c u t i m e以及t m s _ u s t i m e设置为0。
• 父进程设置的锁,子进程不继承。
• 子进程的未决告警被清除。
• 子进程的未决信号集设置为空集。
使f o r k失败的两个主要原因是:( a )系统中已经有了太多的进程(通常意味着某个方面出了问题),或者( b )该实际用户I D的进程总数超过了系统限制。回忆表2 - 7,其中C H I L D _ M A X规定了每个实际用户I D在任一时刻可具有的最大进程数。
f o r k有两种用法:
(1) 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待委托者的服务请求。当这种请求到达时,父进程调用f o r k,使子进程处理此请求。父进程则继续等待下一个服务请求。
(2) 一个进程要执行一个不同的程序。这对s h e l l是常见的情况。在这种情况下,子进程在从f o r k返回后立即调用e x e c。
我们从一个例子程序中可以看到fork函数的作用,子进程与父进程之间的资源共享。
#include <sys/types.h> #include <stdio.h> #include <stdlib.h> int glob = 6; char buf[] = "a write to stdout\n"; int main() { int var; pid_t pid; var = 88; fprintf(stderr, "%s", buf); printf("before fork\n"); if(( pid = fork() ) < 0 ) { fprintf(stderr, "fork error\n"); } else if(pid == 0) { glob++; var++; printf("child process\n"); printf("pid = %d, father pid = %d, glob = %d, var = %d\n", getpid(), getppid(), glob, var); exit(0); } else { sleep(2); printf("father process\n"); printf("pid = %d, father pid = %d, glob = %d, var = %d\n", getpid(), getppid(), glob, var); } return 0; } |
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
static int n = 0;
printf(”fork!\n”); /*printf(”fork!”)*/
switch (pid = fork())
{
case -1:
{
/* 这里pid为-1,fork函数失败 */
/* 一些可能的原因是 */
/* 进程数或虚拟内存用尽 */
perror(”The fork failed!”);
break;
}
case 0:
{
/* pid为0,子进程 */
printf(”[child]i am child!\n”);
printf(”[child]getpid=[%d]\n”, getpid() );
printf(”[child]pid=[%d]\n”, pid );
break;
}
default:
{
/* pid大于0,父进程 */
printf(”[parent]i am parent!\n” );
printf(”[parent]getpid=[%d]\n”,getpid() );
printf(”[parent]pid=[%d]\n”,pid );
break;
}
}
printf(”n=[%d]\n”, n++);
return 0;
}
输出结果1
fork!
[child]i am child!
[child]getpid=[4807]
[child]pid=[0]
n=[0]
[parent]i am parent!
[parent]getpid=[4806]
[parent]pid=[4807]
n=[0]
输出结果2
fork![child]i am child!
[child]getpid=[6163]
[child]pid=[0]
n=[0]
fork![parent]i am parent!
[parent]getpid=[6162]
[parent]pid=[6163]
n=[0]
如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,getpid()返回的才是各自真实的进程号。
printf(”fork!”);//print 一次; 这里会print 2次
如果将 printf(”fork!”) 换成 printf(”fork!\n”) 那么就是只打印一次了.
主要的区别是因为有了一个 \n 回车符号
这就跟Printf的缓冲机制有关了,printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里了,并没有实际的写到屏幕上
但是,只要看到有 \n 则会立即刷新stdout,因此就马上能够打印了.
运行了printf(”fork!”) 后,fork!仅仅被放到了缓冲里,再运行到fork时,缓冲里面的fork!被子进程继承了
因此在子进程度stdout缓冲里面就也有了fork!.
所以,你最终看到的会是fork!被printf了2次!!!!
而运行 printf(”fork!\n”)后,fork!被立即打印到了屏幕上,之后fork到的子进程里的stdout缓冲里不会有 AAAAAA 内容
因此你看到的结果会是 AAAAAA 被printf了1次!!!!
一段关于fork的小程序的启示
前几天,论坛上有人问了这样一个问题:
#include <sys/types.h>
#include <unistd.h>
int main ()
{
for ( int i = 0 ; i < 3 ; i ++)
{
int pid = fork ();
if (pid == 0 )
{
printf ( “child\n” );
}
else
{
printf ( “father\n” );
}
}
return 0 ;
}
请问输出结果是什么?
初看,想当然认为结果是 3对child -father,只是顺序不确定,而且按照Unix环境高级编程中的说法,极端的情况下可能还会出现两个输出的内容相互夹杂的情况。
但是,在Unix测试了一下发现输出竟然有 7对child -father。为什么会这样呢?看了半天程序终于明白了这个简单的问题。其实,这个问题在写 /懂汇编的人看来是再清楚不过了,问题就出在这个 for循环。
1.i = 0时,父进程进入 for循环,此时由于fork的作用,产生父子两个进程 (分别记为F0 /S0 ),分别输出father和child,然后,二者分别执行后续的代码,那后续的代码是什么呢 ? return 0 ?当然不是,由于 for循环的存在,后续的代码是add指令和一条jump指令,因此,父子进程都将进入i = 1的情况;
2.i = 1时,父进程继续分成父子两个进程 (分别记为F1 /S1 ),而i = 0时fork出的子进程也将分成两个进程 (分别记为FS01 /SS01 ),然后所有这些进程进入i = 2;
3. ….过程于上面类似,已经不用多说了,相信一切都已经明了了,依照上面的标记方法,i = 2时将产生F2 /S2 ,FS12 /SS12 ,FFS012 /SFS012 ,FSS012 /SSS012 .
因此,最终的结果是输出 7对child /father。其对应的数学公式为:
1 + 2 + 4 + … + 2 ^(n - 1 ) = 2 ^n - 1
将程序进行改写如下:
输出结果如下(可以画出树图进行分析)