zoukankan      html  css  js  c++  java
  • 《操作系统导论》第5章 | 进程API

    本章主要讨论UNIX系统中的进程创建。UNIX系统采用了一种非常有趣的创建新进程的方式,即通过一对系统调用:fork()exec()。进程还可以通过第三个系统调用wait(),来等待其创建的子进程执行完成。

    fork()系统调用

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[]) {
      printf("hello world (pid:%d)
    ", (int)getpid());
      int rc = fork();
      if (rc < 0) {  // fork failed; exit
        fprintf(stderr, "fork failed
    ");
        exit(1);
      } else if (rc == 0) {  // child (new process)
        printf("hello, I am child (pid:%d)
    ", (int)getpid());
      } else {  // parent goes down this path (main)
        printf("hello, I am parent of %d (pid:%d)
    ", rc, (int)getpid());
      }
      return 0;
    }
    

    运行这段程序,得到如下输出:

    hello world (pid:62775)
    hello, I am parent of 62779 (pid:62775)
    hello, I am child (pid:62779)
    

    当它刚开始运行时,进程输出一条hello world信息,以及自己的进程描述符(process identifier,PID)。该进程的PID是62775。在UNIX系统中,如果要操作某个进程(如终止进程),就要通过PID来指明。紧接着进程调用了fork()系统调用,这是操作系统提供的创建新进程的方法。新创建的进程几乎与调用进程完全一样,对操作系统来说,这时看起来有两个完全一样的p1程序在运行,并都从fork()系统调用中返回。新创建的进程称为子进程,原来的进程称为父进程。子进程不会从main()函数开始执行(因此hello world信息只输出了一次),而是直接从fork()系统调用返回,就好像是它自己调用了fork()

    子进程并不是完全拷贝了父进程。虽然它拥有自己的地址空间(即拥有自己的私有内存)、寄存器、程序计数器等,但是它从fork()返回的值是不同的。父进程获得的返回值是新创建子进程的PID,而子进程获得的返回值是0。这个差别非常重要,因为这样就很容易编写代码处理两种不同的情况。这段程序的输出不是确定的,因为CPU的调度程序决定了某个时刻哪个进程被执行,而调度程序又比较复杂,所以我们不能假设哪个进程先运行。

    wait()系统调用

    有时候,父进程需要等待子进程执行完毕。这项任务由wait()系统调用(或者更完整的接口waitpid())来完成:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[]) {
      printf("hello world (pid:%d)
    ", (int)getpid());
      int rc = fork();
      if (rc < 0) {  // fork failed; exit
        fprintf(stderr, "fork failed
    ");
        exit(1);
      } else if (rc == 0) {  // child (new process)
        printf("hello, I am child (pid:%d)
    ", (int)getpid());
      } else {  // parent goes down this path (main)
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)
    ", rc, wc,
               (int)getpid());
      }
      return 0;
    }
    

    上面的代码增加了wait()调用,因此输出结果也变得确定了。因为子进程可能先运行,先于父进程输出结果。但是,如果父进程碰巧先运行,它会马上调用wait()。该系统调用会在子进程运行结束后才返回。因此,即使父进程先运行,它也会等待子进程运行完毕,然后wait()返回,接着父进程才输出自己的信息。

    hello world (pid:10532)
    hello, I am child (pid:10533)
    hello, I am parent of 10533 (wc:10533) (pid:10532)
    

    exec()系统调用

    exec()系统调用可以让子进程执行与父进程不同的程序。如下所示,子进程调用execvp()来运行字符计数程序wc。实际上,它针对源代码文件p3.c运行wc,从而告诉我们该文件有多少行、多少单词,以及多少字节。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[]) {
      printf("hello world (pid:%d)
    ", (int)getpid());
      int rc = fork();
      if (rc < 0) {  // fork failed; exit
        fprintf(stderr, "fork failed
    ");
        exit(1);
      } else if (rc == 0) {  // child (new process)
        printf("hello, I am child (pid:%d)
    ", (int)getpid());
        char *myargs[3];
        myargs[0] = strdup("wc");    // program: "wc" (word count)
        myargs[1] = strdup("p3.c");  // argument: file to count
        myargs[2] = NULL;            // marks end of array
        execvp(myargs[0], myargs);   // runs word count
        printf("this shouldn't print out");
      } else {  // parent goes down this path (main)
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)
    ", rc, wc,
               (int)getpid());
      }
      return 0;
    }
    

    上述代码的输出结果:

    hello world (pid:25414)
    hello, I am child (pid:25415)
     27 119 880 p3.c
    hello, I am parent of 25415 (wc:25415) (pid:25414)
    

    给定可执行程序的名称(如wc)及需要的参数(如p3.c)后,exec()会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段(以及静态数据),堆、栈及其他内存空间也会被重新初始化。然后操作系统就执行该程序,将参数通过argv传递给该进程。因此,它并没有创建新进程,而是直接将当前运行的程序(以前的p3)替换为不同的运行程序(wc)。子进程执行exec()之后,几乎就像p3.c从未运行过一样。对exec()的成功调用永远不会返回。

    为什么这样设计进程API

    分离fork()exec()的做法在构建UNIX shell时非常有用。shell是一个用户程序,它首先显示一个提示符,然后等待用户输入。你可以向它输入一个命令(一个可执行程序的名称及需要的参数),大多数情况下,shell可以在文件系统中找到这个可执行程序,调用fork()创建新进程,并调用exec()的某个变体来执行这个可执行程序,调用wait()等待该命令完成。子进程执行结束后,shell从wait()返回并再次输出一个提示符,等待用户输入下一条命令。

    $ wc p3.c > newfile.txt
    

    在上面的例子中,wc的输出结果被重定向到文件newfile.txt中。shell实现结果重定向的方式也很简单,当完成子进程的创建后,shell在调用exec()之前先关闭了标准输出,打开了文件newfile.txt。这样,即将运行的程序wc的输出结果就被发送到该文件,而不是打印在屏幕上。

    下面的程序展示了重定向的工作原理。具体来说,UNIX系统从0开始寻找可以使用的文件描述符。在这个例子中,STDOUT_FILENO将成为第一个可用的文件描述符,因此在open()被调用时,得到赋值。然后子进程向标准输出文件描述符的写入,都会被透明地转向新打开的文件而非屏幕。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <fcntl.h>
    #include <assert.h>
    #include <sys/wait.h>
    #include <sys/stat.h>
    
    int main(int argc, char *argv[]) {
      int rc = fork();
      if (rc < 0) {  // fork failed; exit
        fprintf(stderr, "fork failed
    ");
        exit(1);
      } else if (rc == 0) {  // child: redirect standard output to a file
    
        close(STDOUT_FILENO);
        open("./p4.output", O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU);
    
        char *myargs[3];             // now exec "wc"...
        myargs[0] = strdup("wc");    // program: "wc" (word count)
        myargs[1] = strdup("p4.c");  // argument: file to count
        myargs[2] = NULL;            // marks end of array
        execvp(myargs[0], myargs);   // runs word count
      } else {  // parent goes down this path (original process)
    
        int wc = wait(NULL);
        assert(wc >= 0);
      }
      return 0;
    }
    

    上述程序的运行结果:

    $ ./p4
    $ cat p4.output
     31 120 877 p4.c
    

    首先,当运行p4程序后,好像什么也没有发生。shell只是打印了命令提示符,等待用户的下一个命令。但事实并非如此,p4确实调用了fork()来创建新的子进程,之后调用execvp()来执行wc。屏幕上没有看到输出,是由于结果被重定向到文件p4.output。其次,当用cat命令打印输出文件时,能看到运行wc的所有预期输出。

    UNIX管道也是用类似的方式实现的,但用的是pipe()系统调用。在这种情况下,一个进程的输出被链接到了一个内核管道上(队列),另一个进程的输入也被连接到了同一个管道上。因此,前一个进程的输出无缝地作为后一个进程的输入,许多命令可以用这种方式串联在一起,共同完成某项任务。比如通过将grep、wc命令用管道连接可以完成从一个文件中查找某个词,并统计其出现次数的功能:grep -o foo file | wc -l

  • 相关阅读:
    Atitit.ati orm的设计and架构总结 适用于java c# php版
    Atitit.ati dwr的原理and设计 attilax 总结 java php 版本
    Atitit.ati dwr的原理and设计 attilax 总结 java php 版本
    Atitit. 软件设计 模式 变量 方法 命名最佳实践 vp820 attilax总结命名表大全
    Atitit. 软件设计 模式 变量 方法 命名最佳实践 vp820 attilax总结命名表大全
    Atitit 插件机制原理与设计微内核 c# java 的实现attilax总结
    Atitit 插件机制原理与设计微内核 c# java 的实现attilax总结
    atitit.基于  Commons CLI 的命令行原理与 开发
    atitit.基于  Commons CLI 的命令行原理与 开发
    atitit.js 与c# java交互html5化的原理与总结.doc
  • 原文地址:https://www.cnblogs.com/littleorange/p/12734642.html
Copyright © 2011-2022 走看看