1 fork: 创建新进程

18

description

1 fork: 创建新进程. 系统调用 fork 创建一个新进程,但新进程的指令段,用户数据段,用户堆栈段都是旧进程一模一样的复制。系统数据段也几乎全是旧进程的复制。 原先的进程被称作 “ 父进程 ” , 新创建的进程被称作 “ 子进程 ” 。在 UNIX 中, fork 系统调用是创建新进程的惟一方式。 fork 调用惟一能出错的原因是系统中的资源耗尽 。. - PowerPoint PPT Presentation

Transcript of 1 fork: 创建新进程

Page 1: 1  fork: 创建新进程
Page 2: 1  fork: 创建新进程

系统调用 fork 创建一个新进程,但新进程的指令段,用户数据段,用户堆栈段都是旧进程一模一样的复制。系统数据段也几乎全是旧进程的复制。 原先的进程被称作“父进程” , 新创建的进程被称作“子进程”。在UNIX 中, fork 系统调用是创建新进程的惟一方式。 fork 调用惟一能出错的原因是系统中的资源耗尽。

1 fork: 创建新进程

Page 3: 1  fork: 创建新进程

进程执行 fork() 系统调用,创建出一个新的进程。新的进程叫做子进程,原先的进程叫父进程。组成进程的四个要素中,子进程的指令段,数据段,用户堆栈段,统统都是父进程一模一样的复制。子进程的系统数据段也几乎都是父进程的复制, PCB 中只有很少的几个成员父子不同,这至少包括进程标识号 PID 。这种子进程的初始状态复制父进程的方法,叫“继承”。从此以后,父子进程各自都有一套自己独立的数据和指令,以后的发展,父子进程可以有不同的行为,父子进程对数据的修改也互不影响。

Page 4: 1  fork: 创建新进程

【例 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]);

}

Page 5: 1  fork: 创建新进程

上述程序在执行时,进程执行到 fork() 系统调用,创建出一个新的进程。从执行 a += 100 开始,父子进程各有自己一套执行流在独立的执行。这条语句操作的是用户数据区中的变量 a ,父子进程操作的都是各自私有的数据,互不影响。b += 100; 访问用户堆栈区的数据 b ,同样的,父子进程互不影响。printf 需要在当前终端上打印,该库函数最终引用文件描述符 1 ,文件描述符是进程系统数据段里的内容,在创建新进程的时候,子进程抄写的跟父进程一样。因此,子进程也是在当前的终端上打印,而不是输出到别的终端上。

Page 6: 1  fork: 创建新进程

最后一个 printf 引用的 argv 是 main 函数的实参,在堆栈区内, argv[0] 指针指向的存储空间是堆栈栈底存放的一些数据,尽管父子进程输出的结果完全相同,但却是父子进程各自引用自己私有的堆栈段中数据的结果。因为子进程堆栈段抄写的跟父进程一样,而且新进程创建后父子进程都未修改它们,所以输出结果会相同。 下面是上述程序的执行结果:

Page 7: 1  fork: 创建新进程

$ ./fork1

[1] ./fork1: BEGIN

[2] a+b=30

[3] a+b=230

[4] ./fork1: END

[3] a+b=230

[4] ./fork1: END

Page 8: 1  fork: 创建新进程

这就是 fork() 创建新进程的方法。从概念上讲,子进程指令段,数据段,堆栈段都是父进程的复制。从逻辑上看,可以理解子进程和父进程有各自独立的私有存储空间,并且子进程的初始状态是父进程的复制。了解了内核处理的这些技术,就不用担心这样做会占用很多内存。必须有一种方法在程序中区分开父进程和子进程,让父子进程执行不同的程序代码,这样才有意义。区分父子进程的方法就是依靠 fork() 系统调用的返回值。 fork 执行返回后,有两个执行流,两个进程(父和子)都收到返回值,是一个整数,但这两个值不相同。返回值很关键,它用于区分父进程和子进程。例如:

Page 9: 1  fork: 创建新进程

int p;

p = fork(); 

那么,这条语句可以分解为两个动作:① 执行 fork() 系统调用;② 给变量 p 赋值。按照 fork() 系统调用的功能,执行完①后,新产生一个子进程,子进程是父进程的复制。接下来的操作②就会有两个进程分别在自己独立的地址空间内执行,各有自己独立的 p 变量。但是,操作系统对系统调用的处理导致函数调用 fork() 在父子进程中有不同的返回值。在对 p 赋值时,父进程的 p 得到一个正整数,而子进程的 p 得到 0 。程序据此区分父子进程,父进程得到的正整数是子进程的 PID 号。

Page 10: 1  fork: 创建新进程

【例 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]);}

Page 11: 1  fork: 创建新进程

系统调用 fork() 返回值 -1 ,那么,创建新进程失败。程序中的 getpid() 和 getppid() 是两个系统调用,分别用于获取进程在内核中PCB 结构里记录的当前进程的 PID 号和当前进程的父进程的 PID 号。用户程序无法直接访问内核数据,必须使用系统调用。类似的系统调用还有一些,例如: getuid() 获得当前进程的用户标识号等。

Page 12: 1  fork: 创建新进程

上述程序的执行输出如下。这个例子中子进程先于父进程运行,也有可能父进程先于子进程运行。 $ ./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 结构和内存资源)给子进程。

Page 13: 1  fork: 创建新进程

exec 系统调用,从一个指定的程序文件重新初始化一个进程。 exec 系统调用并没有创建新进程,进程还是旧的进程,只是 exec 调用将它重新初始化了指令段,用户数据段和堆栈段 , 系统数据段也几乎没有变化。应当说, exec 系统调用是在旧进程中运行新程序。在组成进程的四个要素中,系统数据段 PCB基本不变。调用 exec 时需要提供一个程序文件的名字,从磁盘上读入这一程序文件的内容,用这个程序文件中的 CPU 指令 ,重新初始化当前进程的指令段,当前进程的原指令段被丢弃;使用程序文件中的数据段说明,重新初始化当前进程的数据段,并从程序文件中获取数据段初值,

exec: 重新初始化进程

Page 14: 1  fork: 创建新进程

管道操作 (1)

• 创建管道 int pipe(int pfd[2])– 创建一个管道, pfd[0] 和 pfd[1] 分别为管道两端

的文件描述字, pfd[0] 用于读, pfd[1] 用于写• 管道写

ret = write(pfd[1], buf, n)– 若管道已满,则被阻塞,直到管道另一端 read

将已进入管道的数据取走为止– 管道容量:某一有限值,如 8192 字节

Page 15: 1  fork: 创建新进程

管道操作 (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

Page 16: 1  fork: 创建新进程

父子进程管道通信• 父进程pfd[0]pfd[1]

pfd[0]pfd[1]

pfd[0]pfd[1]

pfd[0]pfd[1]

pfd[0]pfd[1]

• fork 后

• 父进程关闭读端,子进程关闭写端

Page 17: 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; }}

Page 18: 1  fork: 创建新进程

管道通信:读端/* 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);}