1 fork: 创建新进程
-
Upload
davis-hebert -
Category
Documents
-
view
164 -
download
5
description
Transcript of 1 fork: 创建新进程
系统调用 fork 创建一个新进程,但新进程的指令段,用户数据段,用户堆栈段都是旧进程一模一样的复制。系统数据段也几乎全是旧进程的复制。 原先的进程被称作“父进程” , 新创建的进程被称作“子进程”。在UNIX 中, fork 系统调用是创建新进程的惟一方式。 fork 调用惟一能出错的原因是系统中的资源耗尽。
1 fork: 创建新进程
进程执行 fork() 系统调用,创建出一个新的进程。新的进程叫做子进程,原先的进程叫父进程。组成进程的四个要素中,子进程的指令段,数据段,用户堆栈段,统统都是父进程一模一样的复制。子进程的系统数据段也几乎都是父进程的复制, PCB 中只有很少的几个成员父子不同,这至少包括进程标识号 PID 。这种子进程的初始状态复制父进程的方法,叫“继承”。从此以后,父子进程各自都有一套自己独立的数据和指令,以后的发展,父子进程可以有不同的行为,父子进程对数据的修改也互不影响。
【例 7-6 】 使用 fork 创建子进程。$ cat fork1.c
int a;
int main(int argc, char **argv)
{
int b;
printf("[1] %s: BEGIN\n", argv[0]);
a = 10;
b = 20;
printf("[2] a+b=%d\n", a + b);
fork();
a += 100;
b += 100;
printf("[3] a+b=%d\n", a + b);
printf("[4] %s: END\n", argv[0]);
}
上述程序在执行时,进程执行到 fork() 系统调用,创建出一个新的进程。从执行 a += 100 开始,父子进程各有自己一套执行流在独立的执行。这条语句操作的是用户数据区中的变量 a ,父子进程操作的都是各自私有的数据,互不影响。b += 100; 访问用户堆栈区的数据 b ,同样的,父子进程互不影响。printf 需要在当前终端上打印,该库函数最终引用文件描述符 1 ,文件描述符是进程系统数据段里的内容,在创建新进程的时候,子进程抄写的跟父进程一样。因此,子进程也是在当前的终端上打印,而不是输出到别的终端上。
最后一个 printf 引用的 argv 是 main 函数的实参,在堆栈区内, argv[0] 指针指向的存储空间是堆栈栈底存放的一些数据,尽管父子进程输出的结果完全相同,但却是父子进程各自引用自己私有的堆栈段中数据的结果。因为子进程堆栈段抄写的跟父进程一样,而且新进程创建后父子进程都未修改它们,所以输出结果会相同。 下面是上述程序的执行结果:
$ ./fork1
[1] ./fork1: BEGIN
[2] a+b=30
[3] a+b=230
[4] ./fork1: END
[3] a+b=230
[4] ./fork1: END
这就是 fork() 创建新进程的方法。从概念上讲,子进程指令段,数据段,堆栈段都是父进程的复制。从逻辑上看,可以理解子进程和父进程有各自独立的私有存储空间,并且子进程的初始状态是父进程的复制。了解了内核处理的这些技术,就不用担心这样做会占用很多内存。必须有一种方法在程序中区分开父进程和子进程,让父子进程执行不同的程序代码,这样才有意义。区分父子进程的方法就是依靠 fork() 系统调用的返回值。 fork 执行返回后,有两个执行流,两个进程(父和子)都收到返回值,是一个整数,但这两个值不相同。返回值很关键,它用于区分父进程和子进程。例如:
int p;
p = fork();
那么,这条语句可以分解为两个动作:① 执行 fork() 系统调用;② 给变量 p 赋值。按照 fork() 系统调用的功能,执行完①后,新产生一个子进程,子进程是父进程的复制。接下来的操作②就会有两个进程分别在自己独立的地址空间内执行,各有自己独立的 p 变量。但是,操作系统对系统调用的处理导致函数调用 fork() 在父子进程中有不同的返回值。在对 p 赋值时,父进程的 p 得到一个正整数,而子进程的 p 得到 0 。程序据此区分父子进程,父进程得到的正整数是子进程的 PID 号。
【例 7-7 】 fork 创建子进程,父子进程执行不同的程序段。 $ cat fork2.cint main(int argc, char *argv[]){ int p; printf("[1] %s: BEGIN\n", argv[0]); p = fork(); if (p > 0) { printf("[2] Parent, p=%d\n", p); } else if (p == 0) { printf("[3] Child, p=%d\n", p); } else { perror("Create new process"); } printf("[4] My PID %d, Parent's PID %d\n", getpid(), getppid()); printf("[5] %s: END\n", argv[0]);}
系统调用 fork() 返回值 -1 ,那么,创建新进程失败。程序中的 getpid() 和 getppid() 是两个系统调用,分别用于获取进程在内核中PCB 结构里记录的当前进程的 PID 号和当前进程的父进程的 PID 号。用户程序无法直接访问内核数据,必须使用系统调用。类似的系统调用还有一些,例如: getuid() 获得当前进程的用户标识号等。
上述程序的执行输出如下。这个例子中子进程先于父进程运行,也有可能父进程先于子进程运行。 $ ./fork2
[1] ./fork2: BEGIN
[3] Child, p=0
[4] My PID 20796, Parent's PID 23950
[5] ./fork2: END
[2] Parent, p=20796
[4] My PID 23950, Parent's PID 30320
[5] ./fork2: END
fork 返回值大于 0 ,是子进程的 PID ;子进程的 fork 返回值为 0 。内核在实现 fork 调用时 ,首先初始化创建一个新的 proc 结构,然后复制父进程环境(包括 user 结构和内存资源)给子进程。
exec 系统调用,从一个指定的程序文件重新初始化一个进程。 exec 系统调用并没有创建新进程,进程还是旧的进程,只是 exec 调用将它重新初始化了指令段,用户数据段和堆栈段 , 系统数据段也几乎没有变化。应当说, exec 系统调用是在旧进程中运行新程序。在组成进程的四个要素中,系统数据段 PCB基本不变。调用 exec 时需要提供一个程序文件的名字,从磁盘上读入这一程序文件的内容,用这个程序文件中的 CPU 指令 ,重新初始化当前进程的指令段,当前进程的原指令段被丢弃;使用程序文件中的数据段说明,重新初始化当前进程的数据段,并从程序文件中获取数据段初值,
exec: 重新初始化进程
管道操作 (1)
• 创建管道 int pipe(int pfd[2])– 创建一个管道, pfd[0] 和 pfd[1] 分别为管道两端
的文件描述字, pfd[0] 用于读, pfd[1] 用于写• 管道写
ret = write(pfd[1], buf, n)– 若管道已满,则被阻塞,直到管道另一端 read
将已进入管道的数据取走为止– 管道容量:某一有限值,如 8192 字节
管道操作 (2)• 管道读
ret = read(pfd[0], buf, n)– 若管道写端已关闭,则返回 0– 若管道为空,且写端文件描述字未关闭,则被阻塞– 若管道不为空 ( 设管道中实际有 m 个字节 )
• n≥m ,则读 m 个;• 如果 n < m 则读取 n 个
– 实际读取的数目作为 read 的返回值。– 注意:管道是无记录边界的字节流通信
• 关闭管道 close– 关闭写端则读端 read 调用返回 0 。– 关闭读端则写端 write 导致进程收到 SIGPIPE 信号
( 默认处理是终止进程,该信号可以被捕捉 )• 写端 write 调用返回 -1 , errno 被设为 EPIPE
父子进程管道通信• 父进程pfd[0]pfd[1]
pfd[0]pfd[1]
pfd[0]pfd[1]
pfd[0]pfd[1]
pfd[0]pfd[1]
• fork 后
• 父进程关闭读端,子进程关闭写端
管道通信:写端/* File name : pwrite.c */#define syserr(str) { perror(str); exit(1); }main(){ int pfd[2]; char fdstr[10], *message = "Send/Receive data using pipe"; if (pipe(pfd) == -1) syserr("Create pipe"); switch(fork()) { case -1: syserr("fork"); case 0: /* child */ close(pfd[1]); sprintf(fdstr, "%d", pfd[0]); execlp("./pread", "pread", fdstr, 0); syserr("Execute pread file"); break; default: /* parent */ close(pfd[0]); if(write(pfd[1], message, strlen(message) + 1) == -1) perror("Write pipe"); break; }}
管道通信:读端/* File name : pread.c */#define syserr(str) { perror(str); exit(1); }main(int argc, char **argv){ int fd, nbyte, i; char buf[1024]; fd = strtol(argv[1], 0, 0); nbyte = read(fd, buf, sizeof(buf)); switch (nbyte) { case -1: syserr("Read pipe"); break; case 0: printf("Pipe closed\n"); break; default: printf("%d bytes [%s]\n", nbyte, buf); break; } exit(0);}