💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 Linux。
💡个 人 主 页:@笑口常开xpr 的 个 人 主 页
📚系 列 专 栏:Linux 探 索 之 旅:从 命 令 行 到 系 统 内 核
✨代 码 趣 语:当 waitpid 候 着 子 进 程 的 归 途,当 Z 态 的 “残 影” 待 清 扫,代 码 里 的 等 待 犹 如 持 灯 的 守 夜人 - - - 每 一 次 status 的 拆 解,都 是 为 回 收 “迷 路 的 碎 片”。
💪代 码 千 行,始 于 坚 持,每 日 敲 码,进 阶 编 程 之 路。
📦gitee 链 接:gitee

在这里插入图片描述

         对 Linux 初 学 者 而 言,“进 程” 是 理 解 系 统 底 层 的 关 键,却 常 让 人 困 在 “只 会 用 命 令,不 懂 原 理” 的 阶 段。本 文 以 实 操 为 核 心,从 fork 函 数 切 入,拆 解 进 程 创 建(写 时 拷 贝)、终 止(3 种 场 景)、等 待(wait/waitpid 解 决 僵 尸 进 程),搭 配 代 码 与 截 图,帮 你 快 速 掌 握 进 程 管 理 核 心 逻 辑。


一、进 程 的 创 建

1、fork 函 数 初 识

         在 linux 中 fork 函 数 是 非 常 重 要 的 函 数,它 从 已 存 在 进 程 中 创 建 一 个 新 进 程。新 进 程 为 子 进 程,而 原 进 程 为 父 进 程。
进 程 调 用 fork,当 控 制 转 移 到 内 核 中 的 fork 代 码 后,内 核 做:

  1. 分 配 新 的 内 存 块 和 内 核 数 据 结 构 给 子 进 程
  2. 将 父 进 程 部 分 数 据 结 构 内 容 拷 贝 至 子 进 程,页 表 代 码 和 数 据 可 以 是 完 全 一 样 的。
  3. 添 加 子 进 程 到 系 统 进 程 列 表 当 中
  4. fork 返 回,开 始 调 度 器 调 度
    在这里插入图片描述
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    printf("pid:%d Before!\n",getpid());
    fork();
    printf("pid:%d After!\n",getpid());
    return 0;
}

在这里插入图片描述
         fork 之 前 父 进 程 独 立 执 行,fork 之 后,父 子 两 个 执 行 流 分 别 执 行。

注 意
fork 之 后,谁 先 执 行 完 全 由 调 度 器 决 定。


2、fork 函 数 返 回 值

         子 进 程 返 回 0,父 进 程 返 回 的 是 子 进 程 的 pid。


3、写 时 拷 贝

         父 子 代 码 共 享,父 子 再 不 写 入 时,数 据 也 是 共 享 的,当 任 意 一 方 试 图 写 入,便 以 写 时 拷 贝 的 方 式 各 自 一 份 副 本。

         写 时 拷 贝 本 质 是 写 的 时 候 再 用,是 一 种 延 时 申 请,按 需 申 请。

具 体 见 下 图:
在这里插入图片描述
         无 论 是 父 进 程 还 是 子 进 程,如 果 想 要 写 入,会 将 父 进 程 中 可 写 的 部 分 改 成 只 读,子 进 程 继 承 时 也 是 只 读 状 态,暂 时 是 只 读 状 态。针 对 这 种 情 况,操 作 系 统 不 做 异 常 处 理,如 果 想 要 写 入 数 据,会 将 页 表 对 应 的 区 域 重 新 映 射,然 后 进 行 写 时 拷 贝,这 样 就 能 访 问 原 来 可 写 的 区 域。


4、创 建 多 个 进 程

#include<unistd.h>
#include<stdlib.h>
#define N 5
void RunChild()
{
    int cnt = 5;
    while(cnt)
    {
        printf("I am child:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
        cnt--;
    }
}
int main()
{
    for(int i = 0;i<N;i++)
    {
        pid_t id = fork();
        if(id==0)
        {
            //子进程
            RunChild();
            exit(0);//终止子进程
        }
    }
    sleep(1000);
    return 0;
}

在这里插入图片描述
         5 次 循 环 结 束 后,父 进 程 没 有 结 束,子 进 程 终 止 成 为 僵 尸 进 程。父 子 进 程 谁 先 运 行 由 调 度 器 决 定。


二、进 程 终 止

1、进 程 退 出 场 景

  1. 代 码 运 行 完 毕,结 果 正 确
  2. 代 码 运 行 完 毕,结 果 不 正 确
  3. 代 码 异 常 终 止

结 果 是 否 正 确 采 用 进 程 的 退 出 码 来 进 行 判 定。


2、进 程 常 见 退 出 方 法

         成 功 只 有 1 种 可 能,但 失 败 有 多 个 理 由。

(1)正 常 终 止(可 以 通 过 echo $? 查 看 进 程 退 出 码)

echo $?
         表 示 最 近 一 次 进 程 退 出 时 的 退 出 码。可 以 通 过 观 察 退 出 码 来 判 断 进 程 是 否 正 常 结 束。


  1. 从 main 返 回

         在 c 语 言 中,程 序 返 回 0 中 的 0 表 示 进 程 的 退 出 码,表 征 进 程 的 运 行 结 果 是 否 正 确,0->success。main 函 数 的 返 回 值 的 本 质 表 示 进 程 运 行 完 成 时 是 否 是 正 确 的 结 果,如 果 不 是,可 以 使 用 不 同 的 数 字 表 示 不 同 的 出 错 原 因。

在这里插入图片描述
         进 程 中 父 进 程 会 关 心 程 序 的 运 行 情 况,用 户 可 以 根 据 错 误 码 来 找 出 程 序 中 的 错 误。


可 以 改 变 return 的 返 回 值
在这里插入图片描述
         如 上 图 所 示,第 二 次 调 用 echo $? 返 回 值 成 为 0。当 第 2 次 输 出 时,程 序 变 成 了 echo 命 令,echo 上 次 执 行 是 正 确 的,所 以 退 出 码 为 0。


strerror
         将 错 误 码 转 换 成 错 误 码 描 述。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


示 例 1
在这里插入图片描述

         系 统 提 供 的 错 误 码 和 错 误 码 描 述 是 有 对 应 关 系 的。错 误 码 用 来 表 征 错 误 原 因,错 误 码 描 述 展 现 更 详 细 的 错 误 信 息。


示 例 2
可 以 自 己 定 义 错 误 码。
在这里插入图片描述


errno
最 近 一 次 的 错 误 码。
在这里插入图片描述
在这里插入图片描述
示 例
在这里插入图片描述


3、代 码 异 常

         本 质 可 能 就 是 代 码 没 有 跑 完。进 程 的 退 出 码 无 意 义,不 关 心 退 出 码 了。进 程 出 现 了 异 常,本 质 是 进 程 收 到 了 对 应 的 信 号。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    int* p = NULL;
    *p=100;
    return 0;
}

在这里插入图片描述
         访 问 野 指 针,进 程 抛 出 异 常 显 示 段 错 误,对 应 第 11 号。

验 证

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    while(1)
    {
        printf("Hello world,pid:%d\n",getpid());
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
         第 11 个 信 号 是 进 程 出 现 了 段 错 误。

4、echo

         终 止 程 序 并 返 回 程 序 的 退 出 码 和 return 的 效 果 类 似。

在这里插入图片描述

5、exit 和 return 的 区 别

return
在这里插入图片描述
exit
在这里插入图片描述

         exit 在 任 意 地 方 被 调 用,都 表 示 调 用 进 程 直 接 退 出,return 只 表 示 当 前 函 数 返 回,没 有 退 出 进 程。

6、_exit

终 止 进 程。
在这里插入图片描述

7、exit 和 _exit 的 区 别

exit
在这里插入图片描述

_exit

在这里插入图片描述
         exit 在 结 束 之 后 还 做 了 以 下 工 作:

  1. 执 行 用 户 通 过 atexit 或 on_exit 定 义 的 清 理 函 数。
  2. 关 闭 所 有 打 开 的 流,所 有 的 缓 存 数 据 均 被 写 入。
  3. 调 用 _exit。

在这里插入图片描述

         exit 是 库 函 数,_exit 是 系 统 调 用。
         printf 函 数 先 把 数 据 写 入 缓 冲 区 中,合 适 的 时 候 进 行 刷 新。这 个 缓 冲 区 绝 对 不 在 内 核 中。如 上 图,如 果 缓 冲 区 在 内 核 中,exit 和 _exit 都 会 刷 新,但 实 际 _exit 没 有 刷 新 缓 冲 区。

三、进 程 等 待

1、必 要 性

  1. 子 进 程 退 出,父 进 程 如 果 不 管 不 顾,就 可 能 造 成 ‘僵 尸 进 程’ 的 问 题,进 而 造 成 内 存 泄 漏。
  2. 僵 尸 进 程 无 法 被 杀 死,需 要 通 过 进 程 等 待 来 杀 掉 它,进 而 解 决 内 存 泄 漏 问 题。
  3. 需 要 得 到 子 进 程 的 退 出 情 况,即 知 道 布 置 给 子 进 程 的 任 务 子 进 程 的 任 务 完 成 的 怎 么 样,是 可 以 选 择 的。

2、定 义

         通 过 系 统 调 用 wait/waitpid,来 进 行 对 子 进 程 进 行 状 态 检 测 与 回 收 的 功 能。

3、回 收

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    pid_t id = fork();
    if(id<0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0) 
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(0);
    }
    else 
    {
        //父进程 
        while(1)
        {
            printf("I am father,pid:%d,ppid:%d\n",getpid(),getppid());
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述
         子 进 程 退 出 后 一 直 成 为 僵 尸 状 态。父 进 程 通 过 调 用 wait/waitpid 来 进 行 僵 尸 进 程 的 回 收 问 题。


4、wait

         wait 是 系 统 调 用 接 口,等 待 进 程 直 到 进 程 的 状 态 发 生 改 变。

在这里插入图片描述


1 个 进 程

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
    pid_t id = fork();
    if(id<0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0) 
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(0);
    }
    else 
    {
        //父进程
        int cnt = 10;
        while(cnt)
        {
            printf("I am father,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        pid_t ret = wait(NULL);
        if(ret == id)
        {
           printf("wait success,ret:%d\n",ret); 
        }
    }
    return 0;
}

在这里插入图片描述
         当 父 进 程 的 循 环 结 束 之 后,子 进 程 被 回 收。
         在 子 进 程 成 为 僵 尸 状 态 以 后,父 进 程 等 待 是 必 须 的。wait 等 待 任 意 一 个 子 进 程 退 出。


多 个 进 程

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
#define N 10
void RunChild()
{
    int cnt = 5;
    while(cnt)
    {
        printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
        cnt--;
    }
}
int main()
{
    for(int i = 0;i<N;i++)
    {
        pid_t id = fork();
        if(id==0)
        {
            RunChild();
            exit(0);
        }
        printf("create child process:%d success\n",id);//只有父进程才会执行
    }
    sleep(10);
    //等待
    for(int i=0;i<N;i++)
    {
        pid_t id = wait(NULL);
        if(id > 0)
        {
            printf("wait %d success\n",id);
        }
    }
    sleep(5);
    return 0;
}

在这里插入图片描述
         wait 当 任 意 一 个 子 进 程 退 出 的 时 候,wait 回 收 子 进 程。

         如 果 任 意 一 个 子 进 程 不 退 出,父 进 程 默 认 在 wait 的 时 候,调 用 这 个 系 统 调 用 的 时 候,也 就 不 返 回,默 认 叫 做 阻 塞 状 态。

5、waitpid

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
    pid_t id = fork();
    if(id<0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0) 
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(0);
    }
    else 
    {
        //父进程
        int cnt = 10;
        while(cnt)
        {
            printf("I am father,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        pid_t ret = waitpid(-1,NULL,0);
        if(ret == id)
        {
           printf("wait success,ret:%d\n",ret); 
        }
    }
    return 0;
}

在这里插入图片描述
当 waitpid 回 收 子 进 程 时 返 回 的 是 子 进 程 的 pid。


status
在这里插入图片描述

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
    pid_t id = fork();
    if(id<0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0) 
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(1);
    }
    else 
    {
        //父进程
        int cnt = 10;
        while(cnt)
        {
            printf("I am father,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        int status = 0;
        pid_t ret = waitpid(id,&status,0);
        if(ret == id)
        {
           printf("wait success,ret:%d,status:%d\n",ret,status); 
        }
    }
    return 0;
}

在这里插入图片描述
         父 进 程 等 待,期 望 获 得 子 进 程 的 代 码 是 否 异 常,如 果 没 有 异 常 ,结 果 对 吗,不 对 是 因 为 什 么。

         int 类 型 总 共 有 32 个 比 特 位,目 前 只 考 虑 低 16 位。
在这里插入图片描述
         上 面 代 码 中,status 是 256 的 原 因 是 因 为 子 进 程 的 exit 为 1 即 退 出 码 为 1,00000000 00000000 00000001 00000000,化 为 十 进 制 就 是 2^8 为 256。

         如 果 低 7 位 是 否 为 0,如 果 为 0 则 进 程 没 有 收 到 信 号,则 代 码 没 有 异 常。

         上 面 的 代 码 中 如 果 status 为 全 局 变 量,因 为 父 子 进 程 具 有 独 立 性,父 进 程 无 法 得 到 子 进 程 的 数 据。父 进 程 要 拿 子 进 程 的 状 态 数 据,只 能 通 过 wait 等 系 统 调 用 来 得 到 子 进 程 的 代 码 和 数 据。


验 证

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
    pid_t id = fork();
    if(id<0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0) 
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(11);
    }
    else 
    {
        //父进程
        int cnt = 10;
        while(cnt)
        {
            printf("I am father,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        int status = 0;
        pid_t ret = waitpid(id,&status,0);
        if(ret == id)
        {
            //0x7F 0111 1111
            printf("wait success,ret:%d,exit sig:%d,exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF); 
        }
    }
    return 0;
}

在这里插入图片描述

6、原 理

         waitpid 是 操 作 系 统 提 供 的 接 口,子 进 程 退 出 时 会 将 接 收 的 信 号 以 及 main 函 数 的 返 回 值 返 回 到 status 中,父 进 程 通 过 waitpid 得 到 子 进 程 的 相 关 信 息 来 回 收 子 进 程。
         父 进 程 在 等 待 时 只 能 等 待 自 己 的 子 进 程,不 能 等 待 其 余 进 程 ,否 则 会 等 待 失 败。
在这里插入图片描述

7、WIFEXITED 和 WEXITSTATUS

         可 以 使 用 WIFEXITED 和 WEXITSTATUS 来 检 测 进 程 是 否 正 常 退 出。
在这里插入图片描述
1 个 进 程

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
    pid_t id = fork();
    if(id<0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0) 
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(11);
    }
    else 
    {
        //父进程
        int cnt = 10;
        while(cnt)
        {
            printf("I am father,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        int status = 0;
        pid_t ret = waitpid(id,&status,0);
        if(ret == id)
        {
            //0x7F 0111 1111
            //printf("wait success,ret:%d,exit sig:%d,exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
            if(WIFEXITED(status))
            {
                printf("process success,code exit:%d\n",WEXITSTATUS(status));
            }
            else 
            {
                printf("process fail\n");
            }
        }
    }
    return 0;
}

在这里插入图片描述


多 个 进 程

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
#define N 10
void RunChild()
{
    int cnt = 5;
    while(cnt)
    {
        printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
        cnt--;
    }
}
int main()
{
    for(int i = 0;i<N;i++)
    {
        pid_t id = fork();
        if(id==0)
        {
            RunChild();
            exit(i);
        }
        printf("create child process:%d success\n",id);//只有父进程才会执行
    }
    sleep(10);
    //等待
    for(int i=0;i<N;i++)
    {
        //pid_t id = wait(NULL);
        int status = 0;
        pid_t id = waitpid(-1,&status,0);
        if(id > 0)
        {
            printf("wait %d success,exit code:%d\n",id,WEXITSTATUS(status));
        }
    }
    sleep(5);
    return 0;
}

在这里插入图片描述
         多 个 子 进 程 被 父 进 程 回 收。

         Linux 的 进 程 也 是 一 棵 多 叉 树 结 构,父 进 程 只 对 直 系 的 子 进 程 直 接 负 责。

8、options

         阻 塞 方 式,当 options 为 0 的 时 候 为 阻 塞 方 式。waitpid 会 导 致 父 进 程 进 入 阻 塞 状 态。

(1)WNOHANG

         在 等 待 过 程 中 采 用 非 阻 塞 等 待。

(2)非 阻 塞 轮 询

         非 阻 塞 轮 询 是 一 种 在 程 序 中 定 期 检 查 某 个 状 态 或 资 源 是 否 就 绪 的 机 制,其 核 心 特 点 是 不 会 阻 塞 当 前 程 序 的 执 行 流 程。
         每 次 检 查 操 作 不 会 “卡 住” 程 序,如 果 目 标 未 就 绪,检 查 会 立 即 返 回,允 许 程 序 继 续 执 行 其 他 任 务,而 不 是 一 直 等 待 到 目 标 就绪。

(3)代 码 演 示

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
    pid_t id = fork();
    if(id<0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0) 
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(11);
    }
    else 
    {
        int status = 0;
        while(1)//轮询
        {
            pid_t ret = waitpid(id,&status,WNOHANG);//非阻塞
            if(ret > 0)
            {
                //0x7F 0111 1111
                //printf("wait success,ret:%d,exit sig:%d,exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
                if(WIFEXITED(status))
                {
                    printf("process success,code exit:%d\n",WEXITSTATUS(status));
                }
                else 
                {
                    printf("process fail\n");
                }
                break;
            }
            else if(ret<0)
            {
                printf("wait fail\n");
                break;
            }
            else 
            {
                printf("子进程还没有退出,再等等...\n");
            }
            sleep(1);
        }
    }    
    sleep(3);
    return 0;
}

在这里插入图片描述
注 意
         在 while 循 环 中 必 须 添 加 sleep(1) 这 一 语 句。原 因 是,若 缺 少 这 个 睡 眠 操 作,程 序 会 进 入 无 间 断 的 轮 询 状 态,持 续 不 断 地 查 询 子 进 程 是 否 退 出。这 种 高 频 次 的 查 询 会 导 致 CPU 资 源 被 大 量 占 用,进 而 可 能 造 成 程 序 运 行 出 现 卡 顿 现 象。

         通 过 进 程 等 待 可 以 保 证 父 进 程 是 多 进 程 当 中 最 后 一 个 退 出 的 进 程。

         父 进 程 可 以 在 等 待 子 进 程 返 回 时 做 一 些 简 单 级 的 任 务。但 是 父 进 程 的 核 心 是 等 待 子 进 程 返 回,即 延 迟 回 收 子 进 程,统 一 回 收 子 进 程。


在这里插入图片描述


四、总 结

         本 文 带 你 掌 握 了 fork 原 理、写 时 拷 贝、进 程 终 止 方 式,以 及 wait/waitpid 回 收 僵 尸 进 程 的 方 法 - - - 这 些 是 Linux 系 统 编 程 的 基 础,为 后 续 进 程 通 信、线 程 管 理 铺 路。建 议 多 实 操 修 改 代 码 加 深 理 解,关 注 专 栏,后 续 将 解 锁 更 多 Linux 底 层 内 容。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐