本章主要讨论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. 虚拟机上CentOS7网络配置

    如果图片损坏:点击链接:https://www.toutiao.com/i6493449418249863693/ 设置网络 首先打开虚拟网络编辑器 权限打开 选择NAT模式,设置IP 应用确定之后, ...

  2. HDU-2010.水仙花数(C语言描述)

    Problem Description     春天是鲜花的季节,水仙花就是其中最迷人的代表,数学上有个水仙花数,他是这样定义的: "水仙花数"是指一个三位数,它的各位数字的立方和 ...

  3. Spring学习:简单实现一个依赖注入和循环依赖的解决

    依赖注入 什么是依赖注入 使用一个会创建和查找依赖对象的容器,让它负责供给对象. 当a对象需要b对象时,不再是使用new创建,而是从容器中获取,对象与对象之间是松散耦合的关系,有利于功能复用. 依赖: ...

  4. 论文翻译:2020_FLGCNN: A novel fully convolutional neural network for end-to-end monaural speech enhancement with utterance-based objective functions

    论文地址:FLGCNN:一种新颖的全卷积神经网络,用于基于话语的目标函数的端到端单耳语音增强 论文代码:https://github.com/LXP-Never/FLGCCRN(非官方复现) 引用格式 ...

  5. python2.7发送邮件失败之——SMTPAuthenticationError问题

    使用python2.7发送邮件,代码如下: from email.header import Headerfrom email.mime.text import MIMETextimport smtp ...

  6. Android官方文档翻译 十三 3.1Supporting Different Languages

    Supporting Different Languages 支持不同语言 This class teaches you to 这节课教给你 Create Locale Directories and ...

  7. [MRCTF2020]Ezaudit

    [MRCTF2020]Ezaudit 知识点 1.源码泄露 2.伪随机数 3.sql注入? 题解 打开题目是个漂亮的前端,扫一下发现www.zip文件泄露,下载审计 <?php header(' ...

  8. BarTender调用示例

    安装BarTender 软件后,会注册一个COM 然后在项目中添加BarTender COM 引用 BarTender模板中的条码右键属性-数据源类型-嵌入的数据-名称(比如设置为 barcode p ...

  9. 【刷题-LeetCode】236. Lowest Common Ancestor of a Binary Tree

    Lowest Common Ancestor of a Binary Tree Given a binary tree, find the lowest common ancestor (LCA) o ...

  10. Prometheus-operator 介绍和配置解析

    随着云原生概念盛行,对于容器.服务.节点以及集群的监控变得越来越重要.Prometheus 作为 Kubernetes 监控的事实标准,有着强大的功能和良好的生态.但是它不支持分布式,不支持数据导入. ...