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

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

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



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

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

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

Process ID: a unique integer ID assigned to each process

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

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

下面我们隆重介绍下获取 的函数 —— getpid()

想要查看进程 ,一定是这个进程得运行起来。

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

$ man 2 getpid

#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);
    }
}

🚩 运行结果如下:

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

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

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

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 是个有趣的函数(也经常令人困惑),因为它只被调用一次,但返回两次。

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

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

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 在父进程和子进程中是相同的。

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;
}

进程图画法:(分叉)

解释过程图:

例子:连续的两个 fork 函数

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

例子:父进程中嵌套 fork

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

例子:子进程中嵌套 fork

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

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

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

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

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

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

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

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

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

pid_t wait(int *status)

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

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

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

wait 的更灵活版本:

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

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

等待子进程的例子

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

  • … 除非发生错误

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

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

 

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   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/.

相关文章
|
1月前
|
监控 安全 Unix
进程回收的实现方式与注意事项:Linux C/C中的回收机制
进程回收的实现方式与注意事项:Linux C/C中的回收机制
35 1
|
1月前
|
Linux
【Linux】—— 进程等待 wait&&waitpid
【Linux】—— 进程等待 wait&&waitpid
【Linux】—— 进程等待 wait&&waitpid
|
1月前
|
Linux
进程等待(wait和wait函数)【Linux】
进程等待(wait和wait函数)【Linux】
|
2月前
|
Java
操作系统基础:进程同步【下】
操作系统基础:进程同步【下】
|
3天前
|
算法 Linux Shell
【linux进程(二)】如何创建子进程?--fork函数深度剖析
【linux进程(二)】如何创建子进程?--fork函数深度剖析
|
30天前
|
Linux Shell 调度
【Linux】进程排队的理解&&进程状态的表述&&僵尸进程和孤儿进程的理解
【Linux】进程排队的理解&&进程状态的表述&&僵尸进程和孤儿进程的理解
|
1月前
|
自然语言处理 Linux Shell
【Linux】—— 详解进程PCB和进程状态
【Linux】—— 详解进程PCB和进程状态
|
2月前
|
算法 Java
史上最全!操作系统:【进程同步】
史上最全!操作系统:【进程同步】
160 0
|
2月前
操作系统基础:进程同步【中】
操作系统基础:进程同步【中】
|
2月前
|
算法
操作系统基础:进程同步【上】
操作系统基础:进程同步【上】

相关实验场景

更多