【Linux】fork()函数总结,以及fork()引出的一些知识点,linuxfork


1、 进程相关概念

  当一个进程的时间片用完后,系统把相关的寄存器的值保存到该进程表相应的表项里。同时把代替该进程即将执行的进程的上下文从进程表中读出,并更新相应的寄存器值,这个过程称为上下文交换。上下文交换其实就是读出新的进程的PC(程序计数器),指示当前进程的下一条将要执行的指令。一个进程主要包含以下三个元素:(1)一个正在执行的程序;(2)与该进程相关联的全部数据(变量,内存,缓冲区)(3)程序上下文(程序计数器pc)

2、头文件:

  #include <unistd.h>

  #include <types.h>

3、函数原型:

  pid_t fork(void);

  pid_t是一个宏,其实质是int类型,被定义在#include <sys/types.h>中;

  返回值:1)成功:调用一次则返回两个值:子进程返回0,父进程返回子进程的PID

         2)失败:返回-1

4、获取进程id

  getpid()getppid()

5、Fork过程

  当前进程(调用者,父进程)调用fork()创建一个新的进程(子进程)。子进程是父进程的副本,它将获得父进程数据空间、堆、栈(上下文)等资源的副本(注意:(1)子进程copy父进程的变量,内存与缓冲区,即整个的数据空间的内容,但数据空间是独立的,即父子进程并不共享这些存储空间。(2)父子进程对打开文件的共享: fork之后,子进程会继承父进程所打开的文件表,即父子进程共享文件表,该文件表是由内核维护的,两个进程共享文件状态,偏移量等。这一点很重要,当在父进程中关闭文件时,子进程的文件描述符仍然有用,相应的文件表也不会被释放。(3为了提高效率,fork后并不立即复制父进程空间,采用了COWCopy-On-Write);当父子进程任意之一,要修改数据段、堆、栈时,进行复制操作,但仅复制修改区域;)。父子进程间共享的存储空间只有代码段(只读的,且仅共享fork()后面的代码段)。子进程和父进程继续执行fork调用之后的指令。(4)fork之后,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

6看一个程序:

#include<unistd.h>
#include<stdio.h>
int glob = 6;
char buf[] = "a write to stdout\n";
int  main(void)
{
        int var = 88;
        pid_t   pid;
        if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
        {        
                printf("write error”);
        		return 0;
   		 }
        printf("before fork\n"); //注意这一行
        if ( (pid = fork()) < 0)
  	    {
                printf("fork error");
  		     	return 0;
   		 }
        else if (pid == 0) 
   		 {
                glob++;
                var++;
        } 
  		 else
   		 {
                sleep(2);
   		 }
        printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);  
  		return 0;
}//fork_test.c

选择输出的方式不同,会有不同的输出结果。

直接输出到控制台:
a write to stdout
before fork
pid = 1867, glob = 7, var = 89
pid = 1866, glob = 6, var = 88
使用重定向:./fork_test >fork_test.txt,fork_test.txt内容如下:

a write to stdout
before fork
pid = 1939, glob = 7, var = 89
before fork
pid = 1938, glob = 6, var = 88

  为什么会有这么大的不同?我们先来看看stdoutSTDOUT_FILENO的区别(OS和C++的区别)。

  stdin类型为 FILE*;STDIN_FILENO类型为 int。使用stdin的函数主要有:fread、fwrite、fclose等,基本上都以f开头;使用STDIN_FILENO的函数有:read、write、close等。操作系统一级提供的文件API都是以文件描述符来表示文件,STDIN_FILENO就是标准输入设备(一般是键盘)的文件描述符。标准C++一级提供的文件操作函数库都是用FILE*来表示文件,stdin就是指向标准输入设备文件的FILE*。

  通过man stdin可以查看到:stdin, stdout, stderr - standard I/O streams(具体自行man)。

  stdin / stdout / stderr 分别是指向stream的FILE型的指针变量。当程序启动时,与其结合的整型文件描述符(fd)分别是0,1,2。STDIN_FILENO / STDOUT_FILENO / STDERR_FILENO 是在<unistd.h>文件中定义的预编译macro。其值是0,1,2。(通过freopen(3),可以改变与这3个文件描述符(fd)结合的stream值)。也就是说,程序启动时:FILE * stdin / stdout / stderr 对应的 文件描述符(fd)分别是 STDIN_FILENO(0) / STDOUT_FILENO(1) / STDERR_FILENO(2) 。但是,可以通过FILE *freopen(const char *path, const char *mode, FILE *stream); 来改变,使文件描述符(fd)对应到其他的stream上。(结合到哪个文件stream上,那个文件stream就是变成 标准输入输出)。(标准输入stdin/标准输出stdout/error输出stderr分别改变。)

总结一句话:STDOUT_FILENOOS提供的,定义在头文件<unistd.h>,没有buffer(直接的系统调用);FILE *stdout是标准C++提供的,定义在头文件<stdio.h>,有buffer

使用stdin / stdout / stderr的函数主要有:freadfwritefclose等,基本上都以f开头。

使用STDIN_FILENO / STDOUT_FILENO / STDERR_FILENO的函数有:readwriteclose等。

上程序:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#define CHANGE_BY_FREOPEN /* 需要注释掉,进行切换 */
int main(int argc,char**argv)
{
         char buf[]="hello,world\n";
	#ifdef CHANGE_BY_FREOPEN
	 freopen("stdout_text.txt","w",stdout);
  	//freopen("stderr_text.txt","w",stderr);/* stderr */
	#endif
  	printf("%s",buf);
 	fwrite(buf,strlen(buf), 1,stdout);
 	write(STDOUT_FILENO,&buf,strlen(buf));
  	perror("error out");/* stderr */
  	return 0;
}/* stdouttest.c */    

 

编译命令: gcc -o stdouttest stdouttest.c
①当 #define CHANGE_BY_FREOPEN 被注释掉的时候:终端输出的结果是:

hello,world

hello,world

hello,world

error out: Sucess

②当 #define CHANGE_BY_FREOPEN 有效的时候:终端输出的结果是:

error out: Sucess

同时,会创建一个名为stdout_text.txt的文件,该文件中的内容是:

hello,world

hello,world

hello,world
//freopen("stderr_text.txt","w",stderr);/* stderr */如果有效的话():①shell中,什么也不输出。②stdout_text.txt 中,输出3个(hello,world + 换行符)。③stderr_text.txt 中,输出1个(error out: Success)。 

 

接着stdoutSTDOUT_FILENO的区别后,下面看下关于"printf"/"write"和缓冲的说明:

printf是在stdio.h中声明的函数,而标准IO都是带缓冲的,所以printf是带缓冲的。而write则是不带缓冲的。

标准IO在输入或输出到终端设备时,它们是行缓冲的,否则(文件)它们是全缓冲的。而标准错误流stderr是不使用缓冲的。更为准确的描述是:当且仅当标准输入和标准输出并不涉及交互式设备使,他们才是全缓冲的。标准出错流不使用缓冲。下列情况会引发缓冲区的刷新(清空缓冲区)

1、缓冲区满时;
2、执行flush语句;
3、执行endl语句(printf"\n")
4关闭文件。

综上所述:write的内容在父进程直接输出到了设备,“before fork”在主线程输出到终端后因为换行符而清空了缓冲区,所以也只输出了一次。而重定向到"a.txt"时,printf使用的是全缓冲,所以“before fork”并未输出到设备,而是随着fork()而被复制了一份到子进程的空间中,所以输出了两次。

注意:在重定向父进程输出时,子进程也被重定向了。

另外:为什么父进程要等待2秒?在fork时,父进程所有打开的文件描述符都被复制一份到子进程中,然而他们共享文件描述符所指向的文件对象(FILE结构:描述文件读写指针,偏移量,标志等)。如果不等待,文件偏移量被交替修改,很可能产生混乱的输出。(实际上,这么小的程序,进程执行是很快的,经过实际测试,这部分可以不用,不会产生混乱输出。)

再看一个例子:

#include <stdio.h> 
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
	pid_t childpid;
	char buf[100]={0};
	int status;//供wait使用的参数
	int f;//定义文件标识符
	f=open("text",O_CREAT|O_RDWR,0664);
	if(f==-1)//文件标识符
{
		perror("create file failed!");
		return 1;
	}
	strcpy(buf,"father process’s data\n");
	printf("%d\n",15);
	//此时创建子进程
	childpid=fork();
	printf("childpid=%d",childpid);
	if(childpid==0)
{	strcpy(buf,"child process’s data\n");
		printf("%s",buf);
		puts("child process is working");
		printf("child process’s PID is: %d\n",getpid());
		printf("father process’s PID is: %d\n",getppid());
		int n1= write(f,buf,strlen(buf));//把buf的数据输出重定向到文件中去
		close(f);
		exit(0);//子进程调用exit函数进入僵死状态,参数0表示正常退出
	}
	else {wait(&status);//wait函数是一个等待子进程退出的函数,其参数是一个int类型的指针,保存子进程退出的一些信息
		printf("%s/",buf);
puts("father process is working");
		printf("father process’s PID is: %d\n",getpid());
		printf("child process’s PID is: %d\n",childpid);
		int n=write(f,buf,strlen(buf));
		close(f);
	}
	return 0;
}

程序运行结果如下:

15//父进程打印出来的

Child process’s data

Child process is working

Child process’s PID is:4850

Father process’s PID is: 4849

Father process’s data

Father process is working

Father process’s PID is:4849

Child process’s PID is:4850
如果把 printf("%d/n",15); 改为printf("%d",15); ,那么运行的结果为:

15Child process’s data//15是子进程打印出来的

Child process is working

Child process’s PID is:4850

Father process’s PID is: 4849

15Father process’s data//15是父进程打印出来的

Father process is working

Father process’s PID is:4849

Child process’s PID is:4850

分析如下:

1.wait()函数阻塞父进程,直到子进程返回。因此,子进程先执行,直到退出为止。(当然,这里也可以用sleep(10)来完成同样的功能)

函数说明:wait()会暂时停止目前进程的执行直到有信号来到或子进程结束如果在调用wait()时子进程已经结束wait()会立即返回子进程结束状态值子进程的结束状态值会由参数status 返回而子进程的进程识别码也会一快返回如果不在意结束状态值则参数 status 可以设成NULL. 子进程的结束状态值请参考waitpid().

2.为什么printf("%d/n",15); 与 printf("%d",15);打印的结果不相同?

printf("%d/n",15); 与 printf("%d",15); 区别:前者将数据已经输出到终端上了,后者的数据还在缓冲区内。当创建子进程时,子进程要copy父进程的数据,包括copy缓冲区,所以,第一个程序只打印出一个15,而第二个程序打印出两个15.还要注意一点,第一个结果的15是由父进程打印出来的,而第二个结果由于子进程先执行,复制缓冲区,所以子进程先打印出15,而后父进程才打印出15.

3. close(f),当子进程已经关闭了文件,父进程怎么还能将数据写入?

在前面的分析中得知,父子进程共享同一个文件表,共享文件表的状态,偏移位置等信息。所以在子进程关闭文件描述符后,在父进程中仍然是有效的,而父进程写数据也从文件的当前位置开始写。

linux系统文件流,缓冲及文件描述符与进程之间的关系,可参考http://topic.csdn.net/u/20090309/18/3aba9e11-c8a8-492b-9fe7-29043974a102.html

 

再来几个fork的小题目:

 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main()
{
    pid_t   pid1;
    pid_t   pid2;
    pid1 = fork();
    pid2 = fork();
    printf("pid1=%d,pid2=%d\n",pid1,pid2);
    exit(0);
}

要求如下:已知从这个程序执行到这个程序的所有进程结束这个时间段内,没有其它进程执行。
 1、请说出执行这个程序后,一共运行了几个进程。
 2、如果其中一个进程的输出结果是“pid1=1001, pid2=1002”,写出其他进程的输出结果(不考虑进程执行顺序)。

答案:

1、一共执行了四个进程。(P0, P1, P2, P3

2、另外几个进程的输出分别为:

 pid1:1001, pid2:0

 pid1:0, pid2:1003

 pid1:0, pid2:0

#include <unistd.h>
#include <stdio.h> 
int main () 
{ 
	pid_t fpid; //fpid表示fork函数返回的值
	int count=0;
	fpid=fork(); 
	if (fpid < 0) 
		printf("error in fork!"); 
	else if (fpid == 0) {
		printf("i am the child process, my process id is %d\n",getpid()); 
		printf("我是爹的儿子\n");//对某些人来说中文看着更直白。
		count++;
	}
	else {
		printf("i am the parent process, my process id is %d\n",getpid()); 
		printf("我是孩子他爹\n");
		count++;
	}
	printf("统计结果是: %d\n",count);
	return 0;
}

运行结果是:
    i am the child process, my process id is 5574
    我是爹的儿子
    统计结果是: 1
    i am the parent process, my process id is 5573
    我是孩子他爹
    统计结果是: 1

#include <unistd.h>
#include <stdio.h>
int main(void)
{
   int i=0;
   for(i=0;i<2;i++){
       pid_t fpid=fork();
       if(fpid==0)
    	   printf("%d child  %4d %4d %4d/n",i,getppid(),getpid(),fpid);
       else
    	   printf("%d father%4d %4d %4d/n",i,getppid(),getpid(),fpid);
   }
   return 0;
}

程序运行结果:

0 father 1958 4982 4983 第一行 (P0i=0时执行fork()后进入else产生的)

1 father 1958 4982 4984 第二行 (P0i=1时执行fork()后进入else产生的)

0 child 4982 4983 0 第三行 (P0i=0时执行fork()后进入if产生的)

1 child 4982 4984 0 第四行 (P0i=1时执行fork()后进入if产生的)

1 father 4982 4983 4985 第五行 (P1i=1时执行fork()后进入else产生的)

1 child 1 4985 0 第六行 (P1i=1时执行fork()后进入if产生的)

分析如下:

假设主程序原来的进程为P0

i=0时,程序进入循环,P0调用fork(),随后产生出P1,在P0fork()返回的fpid是子进程的PID,所以fpid不为0,此时判断语句进入else,所以会有第一行;在P1fork()返回的fpid0,此时判断语句进入if,所以会有第三行。

i=1时,(以下P0P1没有先后顺序,主要看OS的策略)P0再次调用fork(),随后产生出P2,在P0fork()返回fpid!=0,进入else,所以会有第二行;在P2fork()返回fpid=0,进入if,所以会有第四行。另外,i=0时产生的P1也会调用fork(),随后产生出P3,在P1fork()返回fpid!=0,进入else,所以会有第五行;在P2fork()返回fpid=0,进入if,所以会有第六行。

上面值得注意的是第六行中的getppid()值为1。按照上面的分析,按说4985的父进程应该4983吗?怎么会是1呢?这里得讲到进程的创建和死亡的过程,进程4983执行完第二个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。4983死亡后,4985就没有父进程了,这在操作系统是不被允许的,所以4985的父进程就被置为init了,init是永远不会死亡的。看下面的linux父子进程终止的先后顺序不同产生不同的结果:

1)父进程先于子进程终止:

此种情况就是孤儿进程。当父进程先退出时,系统会让init进程接管子进程 。
2)子进程先于父进程终止,而父进程又没有调用waitwaitpid函数

此种情况子进程进入僵死状态,且会一直保持下去直到系统重启。子进程处于僵死状态时,内核只保存进程的一些必要信息以备父进程所需。此时子进程始终占有着资源,同时也减少了系统可以创建的最大进程数。

僵死状态:一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占有的资源)的进程被称为僵死进程(zombie)ps命令将僵死进程的状态打印为
3)子进程先于父进程终止,而父进程调用了waitwaitpid函数 
此时父进程会等待子进程结束。

那么有人可能会问:4983执行完后结束了,从而导致4985没有了父进程;同样,4982在第二次循环执行完后也结束了,不是也应该导致4984没有了父进程了吗?为什么第四行中的getppid()不是1?这其实在上面提到过,就是因为操作系统调度的问题。理论上来说,第四行和第六行的getppid()“都有可能为1”。

#include <unistd.h>
#include <stdio.h>
int main(void)
{
   int i=0;
   for(i=0;i<N;i++){
       pid_t fpid=fork();
       if(fpid==0)
    	   printf("son/n");
       else
    	   printf("father/n");
   }
   return 0;
}

上面程序的循环中有个N,总结一下规律:对于这种N次循环的情况,执行printf函数的次数为2*(2^N-1)次,创建的子进程数为2^N-1个。

 

最后再上一份代码:

#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
   fork();
   fork() && fork() || fork();
   fork();
   return 0;
   //printf(“+ \n”);
}

问题:不算main这个进程自身,程序到底创建了多少个进程?

答案:答案是总共20个进程,除去main进程,还有19个进程。

 

参考文章:

http://hi.baidu.com/passerryan/item/bbe792245816c61209750821

http://www.cnblogs.com/lq0729/archive/2011/10/24/2222536.html

http://blog.csdn.net/xiaoxi2xin/archive/2010/04/24/5524769.aspx

http://blog.csdn.net/chenjin_zhong/article/details/6099228

http://blog.csdn.net/jason314/article/details/5640969


linux fork 函数问题2

函数定义:
int fork( void );
返回值:
子进程中返回0,父进程中返回子进程ID,出错返回-1
两次fork 便成了四个进程,进程是从fork函数开始分叉的,而由于fork返回值不是自己的id,所以只有爷爷进程里p0才等于getpid,而你说sleep(3)注释掉的现象我没发现。
fork fork
______|______|____________ (爷爷)
| |
| |___________ (二叔)
|
|
|__________________ (大叔)
|
|
|____________ 孙子)
 

linux下的fork函数

不止三个,子进程仍然在循环体内,所以子进程也会根据它的i值来做循环,然后fork子进程的子进程,但是你的程序也有问题,父进程打印后没有等待子进程运行结束就return了,这样有些子进程还来不仅运行就退出了。所以你的程序的输出结果是不可靠的,也是可能是随机的(其结果会根据运行时操作系统的具体调度的不同而不同)。所以我给你改了一下,这样就能让所有被fork的子进程(包括这些子进程再次fork出来的子进程)都有机会运行。

int main()
{
int i;
int my_pid;
int chld_pid;
for( i= 0; i< 3; i++)
{
chld_pid= fork();
if(chld_pid== 0) {
my_pid = getpid();
printf("i = %d, child pid %d\n", i, my_pid);
}
else {
my_pid = getpid();
printf("i = %d, parent pid %d, child pid %d\n",i, my_pid, chld_pid);
wait(0); /*waiting for the completion of child */
}
}
return 0;
}

运行后的结果为:
i = 0, parent pid 5294, child pid 5295
i = 0, child pid 5295
i = 1, parent pid 5295, child pid 5296
i = 1, child pid 5296
i = 2, parent pid 5296, child pid 5297
i = 2, child pid 5297
i = 2, parent pid 5295, child pid 5298
i = 2, child pid 5298
i = 1, parent pid 5294, child pid 5299
i = 1, child pid 5299
i = 2, parent pid 5299, child pid 5300
i = 2, child pid 5300
i = 2, parent pid 5294, child pid 5301
i = 2, child pid 5301

这样你就可以看到,实际上有7个子进程被fork出来,其中有3个是被真正的父进程(5294)fork出来的,而其余的则是被5294的子进程(甚至子进程的子进程)所fork出来的。其中的逻辑关系从打印出来的进程号就可以一目了然了,我就不赘述了。
 

相关内容