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后并不立即复制父进程空间,采用了COW(Copy-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
为什么会有这么大的不同?我们先来看看stdout与STDOUT_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_FILENO是OS提供的,定义在头文件<unistd.h>中,没有buffer(直接的系统调用);FILE *stdout是标准C++提供的,定义在头文件<stdio.h>中,有buffer。
使用stdin / stdout / stderr的函数主要有:fread、fwrite、fclose等,基本上都以f开头。
使用STDIN_FILENO / STDOUT_FILENO / STDERR_FILENO的函数有:read、write、close等。
上程序:
#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)。
接着stdout与STDOUT_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 第一行 (P0当i=0时执行fork()后进入else产生的)
1 father 1958 4982 4984 第二行 (P0当i=1时执行fork()后进入else产生的)
0 child 4982 4983 0 第三行 (P0当i=0时执行fork()后进入if产生的)
1 child 4982 4984 0 第四行 (P0当i=1时执行fork()后进入if产生的)
1 father 4982 4983 4985 第五行 (P1当i=1时执行fork()后进入else产生的)
1 child 1 4985 0 第六行 (P1当i=1时执行fork()后进入if产生的)
分析如下:
假设主程序原来的进程为P0。
当i=0时,程序进入循环,P0调用fork(),随后产生出P1,在P0中fork()返回的fpid是子进程的PID,所以fpid不为0,此时判断语句进入else,所以会有第一行;在P1中fork()返回的fpid是0,此时判断语句进入if,所以会有第三行。
当i=1时,(以下P0与P1没有先后顺序,主要看OS的策略)P0再次调用fork(),随后产生出P2,在P0中fork()返回fpid!=0,进入else,所以会有第二行;在P2中fork()返回fpid=0,进入if,所以会有第四行。另外,i=0时产生的P1也会调用fork(),随后产生出P3,在P1中fork()返回fpid!=0,进入else,所以会有第五行;在P2中fork()返回fpid=0,进入if,所以会有第六行。
上面值得注意的是第六行中的getppid()值为1。按照上面的分析,按说4985的父进程应该4983吗?怎么会是1呢?这里得讲到进程的创建和死亡的过程,进程4983执行完第二个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。4983死亡后,4985就没有父进程了,这在操作系统是不被允许的,所以4985的父进程就被置为init了,init是永远不会死亡的。看下面的linux父子进程终止的先后顺序不同产生不同的结果:
1)父进程先于子进程终止:
此种情况就是孤儿进程。当父进程先退出时,系统会让init进程接管子进程 。
2)子进程先于父进程终止,而父进程又没有调用wait或waitpid函数
此种情况子进程进入僵死状态,且会一直保持下去直到系统重启。子进程处于僵死状态时,内核只保存进程的一些必要信息以备父进程所需。此时子进程始终占有着资源,同时也减少了系统可以创建的最大进程数。
僵死状态:一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占有的资源)的进程被称为僵死进程(zombie)。ps命令将僵死进程的状态打印为Z 。
3)子进程先于父进程终止,而父进程调用了wait或waitpid函数
此时父进程会等待子进程结束。
那么有人可能会问: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