0
点赞
收藏
分享

微信扫一扫

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口


【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_#include

 💭 写在前面:CSAPP 是计算机科学经典教材《Computer Systems: A Programmer's Perspective》的缩写,该教材由Randal E. Bryant和David R. O'Hallaron 合著。本文以程序员的视角来看,我们不会深入研究(或编写)实际管理进程的内核代码。 我们将学习当我们的程序想要创建、终止或等待进程时,如何向内核发出请求(即系统调用)。在我们开始之前,让我们简要讨论系统调用包装器。

📜 本章目录:

0x00 进程控制(Process Control)

0x01 系统调用错误处理(System Call Error Handling)

0x02 错误报告函数(Error-reporting functions)

0x03 错误处理封装接口(Error-handling Wrappers)

0x04 获取 PID

0x05 进程状态(Process State)

0x06 终止进程(Terminating Processes)

0x07 进程创建(Creating Processes)

0x08 使用进程图建模 fork

0x09 回收子进程(Reaping Child Processes)

0x0A 与子进程同步(Synchronizing with Children)

0x0B execve: 加载一个运行的程序

0x00 进程控制(Process Control)

每当一个程序想要在其自身进程之外产生影响时,它必须请求内核的帮助。比如文件读写:

  • fopen 内部调用 open,这时会调用系统调用
  • 我们的程序也可以直接调用 open,包含 <fcntl.h>
  • 我们也将称此封装函数 (open) 为系统调用

0x01 系统调用错误处理(System Call Error Handling)

几乎所有系统级操作都有可能失败(除了一些返回void的函数之外),因此我们必须明确地检查失败情况。

在错误情况下,Linux 系统级函数通常返回 -1,并设置全局变量 errno 以指示原因。

if (some_syscall() < 0) {
    fprintf(stderr, "Syscall error: %s\n", strerror(errno));
    exit(1);
}

if (some_syscall() < 0) {
perror("Syscall error");   // 更简洁的版本
exit(1);
}

0x02 错误报告函数(Error-reporting functions)

可以使用错误报告函数来简化。但请注意,退出可能并不总是正确的操作。

#include <stdlib.h> // exit()
#include <unistd.h> // fork()
#include <errno.h> // errno()
#include <string.h> // strerror()

void unix_error(char *msg) /* Unix-style error */
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(1);
}

if ((pid = fork()) < 0)
unix_error("fork error");

0x03 错误处理封装接口(Error-handling Wrappers)

我们使用以下封装函数进一步简化了先前的代码,这并不是在实际应用程序中通常想要做的事情。

pid_t Fork(void)
{
    pid_t pid;
    if ((pid = fork()) < 0)
        unix_error("Fork error");
    return pid;
}

pid = Fork(); // 只在成功时返回

0x04 获取 PID

每一个进程在系统中,都会存在一个惟一的标识符!

这就如同每个人都有身份证号一样,进程也需要标号的,所以每个进程都存在有一个 

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_#include_02


Process ID: a unique integer ID assigned to each process

pid_t getpid(void)    // 返回当前进程pid
pid_t getppid(void)   // 放回父进程pid

* 每个进程都有一个父进程。

 

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_父进程_03

 下面我们隆重介绍下获取 【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_父进程_04 的函数 —— getpid() 

想要查看进程 【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_父进程_04,一定是这个进程得运行起来。

我们不妨先问问 Linux 手册中的那个男人,getpid 的下落:

$ man 2 getpid

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_子进程_06

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
    while (1) {
        printf("I am m a process! , pid: %d\n",getpid());
        sleep(1);
    }
}

🚩 运行结果如下:

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_linux_07

启动后,我们发现我们的 mytest 可执行程序的 

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_#include_02

 为 

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_#include_09


是否果真如此?我们还是用 ps aux 验证一下看看:

ps aux | head -1 && ps aux | grep 'mytest' | grep -v grep

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_父进程_10

0x05 进程状态(Process State)

从程序员的角度来看,我们可以将一个进程看作处于三种状态之一。

但是从操作系统的角度来看,可能会有更多的状态,详情可以看下面这篇文章:

【看表情包学Linux】进程状态解析 | 运行态 | 终止态 | 进程挂起与阻塞 | 运行态R | 阻塞态S/D | 死亡态X | 僵尸态Z | 暂停态T/t | 僵尸进程 | 孤儿进程

我们现在以程序员的角度来看,三种状态分别是:

Running: 进程正在执行,或者它正在等待被内核选择并运行。

Stoped (阻塞): 进程的执行已被暂停,直到收到进一步通知才会被调度 (信号)

Terminated and not reaped (僵尸): 进程已完成,但父进程尚未被通知。

0x06 终止进程(Terminating Processes)

进程会因为以下 ”三种之一的情况” 而变成终止状态:

① 接收到一个默认操作为终止进程的信号(下一讲)

② 调用 exit 函数 ▪ 即使主函数返回,也会隐式调用 exit 函数

int fork(void)

③ 以状态值 status 终止进程。约定:正常返回 0,错误返回非零值。另一种显式设置退出状态的方法是从主函数中返回一个整数值(例如在主函数的末尾写上 return 0)

注意:exit 函数只会被调用一次,且不会返回!

0x07 进程创建(Creating Processes)

父进程通过调用 fork 函数创建一个新的正在运行的子进程。

int fork(void);

对于子进程返回 0,对于父进程返回子进程的 pid。

子进程几乎与父进程相同:

  • 子进程获得父进程虚拟地址空间的一个相同但独立的副本。
  • 子进程获得父进程打开文件描述符的相同副本
  • 子进程的PID与父进程不同

fork 是个有趣的函数(也经常令人困惑),因为它只被调用一次,但返回两次。

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_#include_11

创建执行状态的完整副本:

  • 将一个标识为父进程,将另一个标识为子进程。
  • 恢复父进程或子进程的执行。

fork 例子:

int main(void)
{
	pid_t pid;
	int x = 1;
	pid = Fork();
	if (pid == 0) { /* Child */
		printf("child : x=%d\n", ++x);
		return 0;
	}
	/* Parent */
	printf("parent: x=%d\n", --x);
	return 0;
}

调用一次,返回两次。如何区分我们程序的进程和子进程? 通过检查返回值!父进程返回子进程 pid,子进程返回 0,所以我们可以通过 if 语句来抓子进程! 

Duplicate but separate address space(复制了一份独立地地址空间):这是因为进程具有独立性!在 fork 返回后:x 的值为1 ,对 x 的更改是独立的!

并发执行:无法预测父进程和子进程的执行顺序

共享打开的文件:stdout 在父进程和子进程中是相同的。

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_#include_12

0x08 使用进程图建模 fork

进程图是捕捉并发程序中语句的部分排序的有用工具:

  • 每个顶点是语句的执行 a -> b 表示 a 发生在 b 之前
  • 边缘可以标记为变量的当前值
  • printf 顶点可以标记为输出
  • 每个图形以没有入边的顶点开始。

图的任何拓扑排序对应于可行的总排序。

  • 顶点的总排序,其中所有边缘都指向从左到右

int main(int argc, char** argv)
{
	pid_t pid;
	int x = 1;
	pid = Fork();
	if (pid == 0) { /* Child */
		printf("child: x=%d\n", ++x);
		return 0;
	}
	/* Parent */
	printf("parent: x=%d\n", --x);
	return 0;
}

进程图画法:(分叉)

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_子进程_13

解释过程图:

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_#include_14

例子:连续的两个 fork 函数

void fork2()
{
	printf("L0\n");
	fork();
	printf("L1\n");
	fork();
	printf("Bye\n");
}

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_服务器_15

例子:父进程中嵌套 fork

void fork4()
{
	printf("L0\n");
	if (fork() != 0) {
		printf("L1\n");
		if (fork() != 0) {
			printf("L2\n");
		}
	}
	printf("Bye\n");
}

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_服务器_16

例子:子进程中嵌套 fork

void fork5()
{
	printf("L0\n");
	if (fork() == 0) {
		printf("L1\n");
		if (fork() == 0) {
			printf("L2\n");
		}
	}
	printf("Bye\n");
}

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_linux_17

0x09 回收子进程(Reaping Child Processes)

进程终止后,仍会占用系统资源!例如:退出状态、操作系统中的各种结构体。

我们称之为 "僵尸进程" ,是一种半死不活的玩意。

回收:父进程在终止子进程时执行(使用 wait 或 waitpid)

父进程会获得子进程退出状态信息,内核然后会删除僵尸子进程。

如果父进程不进行回收,如果任何一个父进程在不回收子进程的情况下终止,那么这个进程就是孤儿进程了,这个孤儿进程将被 init 进程领养,随后回收。因此,只需要在长时间运行的进程中显式地进行回收(例如:shell 和服务器)

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_子进程_18

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_子进程_19

0x0A 与子进程同步(Synchronizing with Children)

父进程通过以下系统调用之一来回收子进程:

pid_t wait(int *status)

暂停当前进程,直到其任一子进程终止。

返回子进程的 PID,并在 status 中记录退出状态。

pid_t waitpid(pid_t pid, int *status, int options)

wait 的更灵活版本:

可以等待特定的子进程或一组子进程。

如果没有子进程可回收,可以被告知立即返回。

等待子进程的例子 

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_子进程_20

wait 状态码:(Status codes)

wait 的返回值是终止的子进程的 pid。

如果 status != NULL,则指向的整数将被设置为指示退出状态的值

比传递给 exit 的值提供更多的信息。

必须使用在 sys/wait.h 中定义的宏进行解码:

WIFEXITED,WEXITSTATUS,WIFSIGNALED,
WTERMSIG,WIFSTOPPED,WSTOPSIG,WIFCONTINUED

另一个wait示例:如果有多个子进程完成,将以任意顺序进行处理。可以使用宏WIFEXITED和WEXITSTATUS来获取有关退出状态的信息

void fork10() {
	int i, child_status;
	for (i = 0; i < N; i++) {
		if ((fork()) == 0)
			exit(100 + i); /* Child */
	}
	for (i = 0; i < N; i++) { /* Parent */
		pid_t wpid = wait(&child_status);
		if (WIFEXITED(child_status))
			printf("Child %d terminated with exit status %d\n",
				wpid, WEXITSTATUS(child_status));
		else
			printf("Child %d terminate abnormally\n", wpid);
	}
}

waitpid:等待特定进程

pid_t waitpid(pid_t pid, int &status, int options)

暂停当前进程,直到特定进程终止,支持各种选项:

void fork11() {
	pid_t pid[N];
	int i, child_status;
	for (i = 0; i < N; i++) {
		if ((pid[i] = fork()) == 0)
			exit(100 + i); /* Child */
	}
	for (i = N - 1; i >= 0; i--) {
		pid_t wpid = waitpid(pid[i], &child_status, 0);
		if (WIFEXITED(child_status))
			printf("Child %d terminated with exit status %d\n",
				wpid, WEXITSTATUS(child_status));
		else
			printf("Child %d terminate abnormally\n", wpid);
	}
}

0x0B execve: 加载一个运行的程序

int execve(char *filename, char *argv[], char *envp[])

在当前进程中加载并运行:

  • 可执行文件文件名
  • 可以是目标文件或以 #!interpreter 开头的脚本文件(例如,#!/bin/bash,#!/usr/bin/python)
  • … 带有参数列表argv,按照惯例,argv[0] 设置为文件名
  • … 和环境变量列表 envp ,“name=value” 字符串(例如,USER=droh) ,参见 getenv、putenv、printenv

覆盖代码、数据和栈

  • 保留 PID、打开的文件和信号上下文

仅被调用一次,不会返回

Called once and never returns

  • … 除非发生错误

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_服务器_21

栈布局示例:在子进程中使用当前环境执行“/bin/ls -lt /usr/include”

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_子进程_22

if ((pid = Fork()) == 0) { /* Child runs program */
	if (execve(myargv[0], myargv, environ) < 0) {
		printf("%s: %s\n", myargv[0], strerror(errno));
		exit(1);
	}
}

 

【CSAPP】进程控制 | 系统调用错误处理 | 进程状态 | 终止进程 | 进程创建 | 回收子进程 | 与子进程同步(wait/waitpid) | execve 接口_linux_23

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2023.3.21
❌ [ 勘误 ]   /* 暂无 */
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

Bryant and O’Hallaron, Computer Systems: A Programmer’s Perspective, Third Edition

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.


举报

相关推荐

0 条评论