本章主要讨论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)\n", (int)getpid());
int rc = fork();
if (rc < 0) { // fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("hello, I am child (pid:%d)\n", (int)getpid());
} else { // parent goes down this path (main)
printf("hello, I am parent of %d (pid:%d)\n", 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)\n", (int)getpid());
int rc = fork();
if (rc < 0) { // fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("hello, I am child (pid:%d)\n", (int)getpid());
} else { // parent goes down this path (main)
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", 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)\n", (int)getpid());
int rc = fork();
if (rc < 0) { // fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("hello, I am child (pid:%d)\n", (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)\n", 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\n");
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

《操作系统导论》第5章 | 进程API的更多相关文章

  1. UNIX环境高级编程 第8章 进程控制

    本章是UNIX系统中进程控制原语,包括进程创建.执行新程序.进程终止,另外还会对进程的属性加以说明,包括进程ID.实际/有效用户ID. 进程标识 每个进程某一时刻在系统中都是独一无二的,它们之间是用一 ...

  2. OS OSTEP (Operating Systems Three Easy pieces 操作系统导论 )

    读<OSTEP>的一点重点记录与感悟 (未完) Chapter-2 第二章 1. 操作系统的设计目标:  抽象.高性能.保护.不间断运行. 抽象:建立一些“抽象”,让操作系统方便和易于使用 ...

  3. 《Linux内核设计与实现》读书笔记 第三章 进程管理

    第三章进程管理 进程是Unix操作系统抽象概念中最基本的一种.我们拥有操作系统就是为了运行用户程序,因此,进程管理就是所有操作系统的心脏所在. 3.1进程 概念: 进程:处于执行期的程序.但不仅局限于 ...

  4. windows核心编程---第四章 进程

    上一章介绍了内核对象,这一节开始就要不断接触各种内核对象了.首先要给大家介绍的是进程内核对象.进程大家都不陌生,它是资源和分配的基本单位,而进程内核对象就是与进程相关联的一个数据结构.操作系统内核通过 ...

  5. Linux内核分析——第三章 进程管理

    第三章 进程管理 3.1 进程 1.进程就是处于执行期的程序:进程就是正在执行的程序代码的实时结果:进程是处于执行期的程序以及相关的资源的总称:进程包括代码段和其他资源. 线程:是在进程中活动的对象. ...

  6. 《微服务架构设计模式》读书笔记 | 第8章 外部API模式

    目录 前言 1. 外部API的设计难题 1.1 FTGO应用程序的服务及客户端 1.2 FTGO移动客户端API的设计难题 1.3 其他类型客户端API的设计难题与特点 2. API Gateway模 ...

  7. java JDK8 学习笔记——第15章 通用API

    第十五章 通用API 15.1 日志 15.1.1 日志API简介 1.java.util.logging包提供了日志功能相关类与接口,不必额外配置日志组件,就可在标准Java平台使用是其好处.使用日 ...

  8. <自动化测试方案_6>第六章、API自动化测试

    第六章.API自动化测试 (一)工具实现 目前大众接口测试的工具有:Postman.SoupUI.jmeter他们的特点介绍有人做个宏观的研究,这里进行引用:https://blog.csdn.net ...

  9. Windows核心编程:第4章 进程

    Github https://github.com/gongluck/Windows-Core-Program.git //第4章 进程.cpp: 定义应用程序的入口点. // #include &q ...

随机推荐

  1. Source Insight 4安装图文教程(附链接)

    Source Insight 4安装图文教程,附激活文件直链 下载链接:http://naturalporters.vicp.io/uploads/si4_kgen_unis.zip 直链没了,就用下 ...

  2. USB Tethering always displays grey when USB tethering type is Linux(EEM)

    USB Tethering always displays grey when USB tethering type is Linux(EEM) 1.Problem DESCRIPTION USB T ...

  3. 热词cloud-EChart安装

    1.安装npm install echarts npm install echarts-wordcloud注意版本:echarts版本5只能和wordcloud版本2的一起使用 :echarts版本4 ...

  4. unity3d发布安卓出错plese set the package name

    发布时报错 参考https://forum.unity.com/threads/where-is-package-name-setting.318839/ 参考https://answers.unit ...

  5. 一个BPMN流程示例带你认识项目中流程的生命周期

    摘要:本文详细说明了在工作流Activiti框架中的BPMN流程定义整个运行的生命周期. 本文分享自华为云社区<本文详细说明了在工作流Activiti框架中的BPMN流程定义整个运行的生命周期& ...

  6. 集合框架-工具类-JDK5.0特性-函数可变参数

    1 package cn.itcast.p4.news.demo; 2 3 public class ParamterDemo { 4 5 public static void main(String ...

  7. Python如何把八进制转换成ASCII码

    做题途中拿到一串八进制字符串 0126 062 0126 0163 0142 0103 0102 0153 0142 062 065 0154 0111 0121 0157 0113 0111 010 ...

  8. IDEA出现Cannot resolve symbol “xxx“(无法解析符号)的解决办法

    1,File->Invalidate Caches/Restart 清除缓存并重启 idea 2,检查pom文件中的依赖关系是否正确 3,maven -> Reimport 4,打开pro ...

  9. 如何保存并复制python虚拟环境

    关于虚拟环境的一些基础概念学习了本期视频 保存 以我的一个虚拟环境示例: 在要保存的虚拟环境下使用: pip freeze > requirements.txt 复制 pip install - ...

  10. jsp 4-15