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

  • 相关阅读:
    获取农历日期
    图片上传代码(C#)
    ASP.net使用技术总结(1)GridView控件的单击处理
    JavaScript使用小技巧:IE8的关闭处理
    FrameSet左右收缩编码
    哈哈,开心!今天注册开通了 弟弟Kernel 的网志
    设计模式简介
    Delphi字符串、PChar与字符数组之间的转换
    C++中数组参数详解
    1、简单工厂模式
  • 原文地址:https://www.cnblogs.com/littleorange/p/12734642.html
Copyright © 2011-2022 走看看