1 / 18

1 fork: 创建新进程

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

Download Presentation

1 fork: 创建新进程

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


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

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

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

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

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

  6. $ ./fork1 [1] ./fork1: BEGIN [2] a+b=30 [3] a+b=230 [4] ./fork1: END [3] a+b=230 [4] ./fork1: END

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

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

  9. 【例7-7】 fork创建子进程,父子进程执行不同的程序段。  $ cat fork2.c int 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]); }

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

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

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

  13. 管道操作(1) • 创建管道 int pipe(int pfd[2]) • 创建一个管道,pfd[0]和pfd[1]分别为管道两端的文件描述字,pfd[0]用于读,pfd[1]用于写 • 管道写 ret = write(pfd[1], buf, n) • 若管道已满,则被阻塞,直到管道另一端read将已进入管道的数据取走为止 • 管道容量:某一有限值,如8192字节

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

  15. 父子进程管道通信 • 父进程关闭读端,子进程关闭写端 • 父进程 • fork后

  16. 管道通信:写端 /* 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; } }

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

More Related