linux下C 编程学习之多进程编程(一)(转)
一、进程概念
进程是操作系统中资源分配的最小单位,而线程是调度的最小单位。
一个进程,主要包含三个元素:
a) 一个可以执行的程序;
b) 和该进程相关联的全部数据(包括变量,内存空间,缓冲区等等);
c) 程序的执行上下文(execution context)。
不妨简单理解为,一个进程表示的,就是一个可执行程序的一次执行过程中的一个状态。操作系统对进程的管理,典型的情况,是通过进程表完成的。进程表中的每一个表项,记录的是当前操作系统中一个进程的情况。对于单 CPU的情况而言,每一特定时刻只有一个进程占用CPU,但是系统中可能同时存在多个活动的(等待执行或继续执行的)进程。
二、进程标识符
每一个进程都有一个非负整形表示的唯一进程ID。虽然进程ID总是唯一的,但是可以重用。当一个进程终止,其之前被分配的进程ID就可以再次被使用。
三、创建进程 fork() 函数
fork()是进程的核心函数,由fork创建的新进程被成为子进程。
eg:
有一个现有的进程可以调用fork函数创建一个新的进程:
#include <unistd.h>
pid_t fork(void);
fork函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。
将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,但是没有一个函数能使一个进程可以获得其所有子进程的进程ID。
fork使子进程获得返回值为0的原因:一个进程只会有一个父进程,所以子进程总是可以调用getppid用来获得其父进程的进程ID。
子进程和父进程继续执行fork调用之后的指令(原因在文章的后面部分会解释)。子进程是父进程的副本。子进程可以获得父进程的数据空间、堆和栈的副本,但是父子进程并不共享这些存储空间,父子进程共享这些正文段。
fork函数简单示例:
#include "apue.h"
int glob = 6;
char buf[] = "a write to stdout\n";
int main(void)
{
int var;
pid_t pid;
var = 88;
if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)//write函数将buf中的内容写到标准输出流中,此处制作输出使用。
err_sys("write error");
printf("before fork\n");
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
glob++;
var++;
} else {
sleep(2); //学过Linux的都知道,在fork()之后,是父进程先执行还是子进程先执行取决于内核的调度算法。所以在这里为了让子进程先执行,我们
先让父进程sleep 2秒,但是2秒钟不一定够,所以不一定能够保证子进程先执行。
}
编译、运行 上述代码段之后可得如下结果:
wangye@wangye:~$ gcc -g test1.c -o test1 libapue.a //编译
wangye@wangye:~$ ./test1 //运行
a write to stdout
before fork
pid = 20266, glob = 7, var = 89
pid = 20265, glob = 6, var = 88
代码分析:
a)、当你看到fork的时候,你可以把fork理解成“分叉”,在分叉的同时,生成的一个子进程复制了父进程的基本所有的东西,包括代码、数据和分配给进程的资源。也就是子进程几乎是和父进程是一模一样的。但是子进程可能会根据不同情况调用其他函数。比如exec函数。
b)、在语句pid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(pid<0)……
为什么两个进程的pid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。解释一下pid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的pid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其pid为0.
c)、fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。
d)、创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
e)、fork调用执行完毕后,出现两个进程,
或许有人问两个进程的内容完全一样,为什么打印的结果不一样啊,那是因为判断条件的原因,上面列举的只是进程的代码和指令,还有变量。
执行完fork后,进程1的变量为var=88,pid!=0(父进程)。进程2的变量为var=88,pid=0(子进程),这两个进程的变量都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过pid来识别和操作父子进程的。
f)、还有人可能问为什么不是从#include处开始复制代码的
这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了int var=88;fork只拷贝下一个要执行的代码到新的进程。(因为FORK是复制产生一个新的进程,因此新的进程与旧的进程之间的上下文,如寄存器上下文等是一致的,也就是说两个进程的变量值,PC指针值也是一样的在这里:PC是指寄存器PC,它里边的值总是指向当前程序的运行点的地址,因此两个进程都是在同一个位置开始运行)。
四、fork难度++
a)、首先看一段代码:
#include "apue.h" int main(void) { int i=0; pid_t pid; for(i=0;i<2;i++) { if((pid=fork())<0){ printf("forkerror\n"); }else if(pid==0){ printf("%d,childself's pid=%d,parent's pid=%d,returnid=%d\n",i,getpid(),getppid(),pid); }else{ printf("%d,parentself's pid=%d,parent's father's pid=%d,returnid=%d\n",i,getpid(),getppid(),pid); sleep(2);//为了确保(1)、在i=0时子进程先于父进程执行fork调用; (2)、父进程在子进程之后退出,这样可以保证子进程不会变成孤儿进程而过继给init进程; } } exit(0); }
wangye@wangye:~$ gcc -g test32.c -o test32 libapue.a
wangye@wangye:~$ ./test32
0,parentself's pid=21352,parent's father's pid=31055,returnid=21353
0,childself's pid=21353,parent's pid=21352,returnid=0
1,parentself's pid=21353,parent's father's pid=21352,returnid=21354
1,childself's pid=21354,parent's pid=21353,returnid=0
1,parentself's pid=21352,parent's father's pid=31055,returnid=21355
1,childself's pid=21355,parent's pid=21352,returnid=0
分析:
第一步:在父进程中,当指令执行for循环时,i=0,接着执行fork,fork执行完后,系统中出现两个进程,分别是21352和21353。可以看到父进程21352的父进程是31055,子进程21353的父进程正好是21352。我们用一个链表来表示这个关系:
31055->21352->21353
第一次fork后,21352(父进程)的返回值 returnid=21353,而子进程21353的返回值rturnid=0。原因是:fork函数被调用一次产生两个返回值,父进程得到的返回值是它所产生的子进程的进程ID,而子进程得到的返回值是0。
所以会有如下打印结果:
0,parentself's pid=21352,parent's father's pid=31055,returnid=21353
0,childself's pid=21353,parent's pid=21352,returnid=0
第二步:有上面的结果可知,当i=1时,子进程21353先执行,接着执行fork,系统中又新增一个新进程21354,对于此时进程链为:21352(当前进程的父进程)->21353(当前进程)->21354(被创建的子进程)。从输出可以看到21353原来是21352的子进程,现在变成21354的父进程。父子进程是相对的,这个大家应该容易理解。只要当前进程执行了fork,该进程就变成了父进程了,就打印出了parent。
对于进程21352,当i=1时,接着执行fork,该进程创建一个进程号为21355的子进程,对于此进程进程链为:31055-(当前进程的父进程)>21352(当前进程)->21355(被创建的子进程)。
第三步:第二步的时候创建了两个进程21354和21355,,这两个进程执行完printf函数后就结束了,所以这两个进程无法进入第三次循环,无法执行fork,故returnid= 0;其他进程也是如此。
以下是进程21354和21355打印出的结果:
1,childself's pid=21354,parent's pid=21353,returnid=0
1,childself's pid=21355,parent's pid=21352,returnid=0
总结一下,这个程序执行的流程如下:
这个程序最终产生了3个子进程,执行过6次printf()函数。
b)、接下来再看一段代码:
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int i=0;
printf("i son/pa ppid pid fpid\n");//ppid指当前进程的父进程pid,pid指当前进程的pid,fpid指fork返回给当前进程的值
for(i=0;i<2;i++){
pid_t fpid=fork();
if(fpid<0)
printf("error\n");
if(fpid==0)
printf("%d child %4d %4d %4d\n",i,getppid(),getpid(),fpid);
else
{ printf("%d parent %4d %4d %4d\n",i,getppid(),getpid(),fpid);
}
}
return 0;
}
编译运行结果如下:
wangye@wangye:~$ gcc -g fork1.c -o fork1
wangye@wangye:~$ ./fork1
i son/pa ppid pid fpid
0 parent 31055 29375 29376
0 child 29375 29376 0
1 parent 31055 29375 29377
1 parent 29375 29376 29378
1 child 1 29378 0
1 child 1 29377 0
该程序和上面的程序类似但是细心的读者会发现, 进程29378和进程29377的父进程难道不该是29376和29375吗,怎么会是1呢?在这里得涉及到进程的创建和死亡过程,在29376和29375执行完第二个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。29376和29375死亡后,29378和29377就没有父进程了就变成了孤儿进程,这在操作系统中是不被允许的,所以进程29378和进程29377的父进程就被置为1了,相当于把这两个孤儿进程过继给PID=1的init进程(init进程是系统专用进程),是永远不会死亡的。
我们在来看一份代码:
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int i=0;
for(i=0;i<3;i++){
pid_t fpid=fork();
if(fpid==0)
printf("son/n");
else
printf("father/n");
}
return 0;
}
编译运行结果:
wangye@wangye:~$ gcc -g fork2.c -o fork2
wangye@wangye:~$ ./fork2
father
son
father
father
father
father
son
son
son
father
son
father
son
son
针对上述结果进行一下详细分析,如图所示:
总结一下规律,对于这种N次循环的情况,执行printf函数的次数为2*(1+2+4+……+2N-1)次,创建的子进程数为1+2+4+……+2N-1个。 网上有人说N次循环产生2*(1+2+4+……+2N)个进程,这个说法是不对的,希望大家需要注意。
如果想测试一个程序中创建的子进程数,最好的方法就是调用printf函数打印该进程的pid,也即调用printf("%d \n",getpid());或者通过printf("+ \n");来判断产生了几个进程。有人想直接通过调用printf("+");来统计创建了几个进程,这是不妥当的。具体原因如下:
我们重新来看一下本篇文章的第一段代码:
1):我们编译完之后执行结果如下:
wangye@wangye:~$ ./test1
a write to stdout
before fork
pid = 31332, glob = 7, var = 89
pid = 31331, glob = 6, var = 88
2):接下来我们用第二种方式运行该程序,执行命令./test1 > tmp.out,然后cat tmp.out(./test1 > temp.out是将执行结果输出到文件tmp.out中;cat tmp.out是获取该文件的内容)得到结果如下:
<span style="font-size:12px">w</span><span style="font-size:12px">angye@wangye:~$ ./test1 > tmp.out
wangye@wangye:~$ cat tmp.out
a write to stdout
before fork
pid = 31335, glob = 7, var = 89
before fork
pid = 31334, glob = 6, var = 88</span>
在第一种情况下:由于write函数是不带缓冲的,因为在fork()之前调用的write,所以其数据写到标准输出一次。但是标准I/O库是带缓冲的,当我们用以上这种方式运行的时候,before fork只会输出一次,其原因是我们是用的交互式方式运行的该程序,且标准输出连到终端设备,它是行缓冲,标准缓冲区由换行符冲洗掉了,所以只得到printf一次。
在第二种情况下我们得到两次before fork:这是因为我们把标准输出重定向到了tmp.out,它是全缓冲的,那么该行数据不会被换行符冲洗掉,然后在将父进程的数据空间复制到子进程中时,该缓冲区也被复制到了子进程中。于是父子进程都拥有该行内容的标准I/O缓冲区。
进一步的解释请看如下代码是:
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t fpid;//fpid表示fork函数返回的值
//printf("fork!");
//printf("fork!\n");
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());
else
printf("I am the parent process, my process id is %d\n", getpid());
return 0;
}
注释第一个printf输出,保留第二个printf输出的输出结果:
wangye@wangye:~$ gcc -g fork3.c -o fork3
wangye@wangye:~$ ./fork3
fork!
I am the parent process, my process id is 31459
I am the child process, my process id is 31460
注释第二个printf输出,保留第一个printf输出的输出结果:
wangye@wangye:~$ gcc -g fork3.c -o fork3
wangye@wangye:~$ ./fork3
fork!I am the parent process, my process id is 31506
fork!I am the child process, my process id is 31507
分析:之所以会有 上面的情况是跟printf的缓冲机制有关,即:printf某些内容时,操作系统仅仅是把该内容放到了stdout(标准输出流)的缓冲队列里了,并没有实际的写到屏幕上。但是,只要看到有/n 则会立即刷新stdout,因此就马上能够打印了。 运行了printf("fork!")后,“fork!”仅仅被放到了缓冲里,程序运行到fork时,缓冲里面的“fork!” 被子进程复制过去了。因此在子进程度stdout缓冲里面就也有了fork! 。所以,你最终看到的会是fork! 被printf了2次!!!!
而运行printf("fork! /n")后,“fork!”被立即打印到了屏幕上,之后fork到的子进程里的stdout缓冲里不会有fork! 内容(前面说过了子进程的执行不是从#include开始执行的而是从代码段的fork处开始往下执行)。因此你看到的结果会是fork! 被printf了1次!!!!这就是为什么说printf("+");不能正确地反应进程的数量的原因。
为了测试子进程是否复制了父进程缓冲区里的内容,对本次文章第一次讲解的代码作如下修改:
#include "apue.h"
int glob=6;
char buf[]="a write to stdout\n";
int main(void)
{
int var;
pid_t pid;
var=88;
if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1)
err_sys("write error");
printf("before fork %d\n",getpid());
if((pid=fork())<0)
err_sys("fork error");
else if(pid==0)
{
glob++;
var++;
}
else sleep(2);
printf("pid= %d,glod=%d, var=%d \n",getpid(),glob,var);
exit(0);
}
编译连接产生如下结果:
wangye@wangye:~$ gcc -g fork4.c -o fork4 libapue.a
wangye@wangye:~$ ./fork4 >tmp.out
wangye@wangye:~$ cat tmp.out
a write to stdout
before fork 1976
pid= 1977,glod=7, var=89
before fork 1976
pid= 1976,glod=6, var=88
两次输出的都是before fork 1976,而1976是父进程的pid.所以可知子进程确实复制了父进程缓冲区中的内容。