课程摘要:本课程是一份面向嵌入式 Linux 初学者的实践导向速成指南。上一篇我们学习了文件 I/O 操作的基础,这一篇我们继续深入进程相关的核心知识。课程摒弃繁杂的理论,以动手实操为核心,带你掌握进程的创建、终止、监控与替换等关键技能,为后续多进程并发编程打下坚实基础。

学习目标:

进程管理:
理解什么是进程,学会 fork 创建进程、exit 终止进程、wait/waitpid 监控进程状态、exec 族函数替换进程执行新程序


学习内容:

1. 进程的基本概念:

我们先搞懂两个问题:

什么是进程?

为什么需要进程?

大家写代码编译出来的可执行文件,那叫程序,是躺在硬盘上的静态文件。而当你运行这个程序,它被加载到内存中跑起来之后,就变成了进程

进程就是正在运行的程序实例,每个进程都有自己独立的一套资源:代码段、数据段、堆栈、文件描述符等等,相当于一个独立的小环境。

每个进程都有一个唯一的身份证号,叫 进程 ID(PID),由操作系统分配,用来区分和管理不同的进程。

那为什么要有进程这个东西呢?主要有三个好处:

  1. 安全性:每个进程跑在独立的地址空间里,一个进程崩了不会连累其他进程,系统更稳定。
  2. 提高效率:多个进程可以同时跑(并发执行),充分利用 CPU 资源,响应更快。
  3. 简化编程:可以把一个复杂的大项目拆成多个小进程,每个进程负责一块功能,模块之间高内聚低耦合,代码更好维护。

比如做一个车载系统,UI 界面是一个进程,行车记录仪是一个进程,OTA 升级又是一个进程,各干各的,互不干扰。


2. 父子进程

Linux 系统启动的时候,内核会创建第一个 PID 为 1 的进程(叫 init 进程),后面所有的进程都是由它创建出来的。所以 Linux 的所有进程其实是一棵进程树,像家谱一样,有父有子。

父进程:创建子进程的那个进程,可以给子进程发信号,也可以等子进程结束。

子进程:父进程创建出来的,相当于父进程的一个副本,可以继承父进程的资源,也可以给父进程发信号。

这里有两个常考的概念,大家一定要搞清楚:

进程类型 产生原因 后果
孤儿进程 父进程先挂了,子进程还在跑 被 init 进程(PID=1)收养,有人管,没问题
僵尸进程 子进程先结束了,但父进程没调用 wait/waitpid 收尸 子进程占着少量资源不放,不消耗 CPU,但积累多了占 PID 资源

简单说:孤儿进程有人管(init 收养),僵尸进程没人收(占着茅坑不拉屎)。


3. fork 创建进程

在 Linux 中,创建一个新进程用的是 fork() 函数。这是一个非常神奇的函数,调用一次,返回两次!

fork 的函数原型:
#include <unistd.h>
pid_t fork(void);

参数说明

参数 数据类型 详细说明 使用规则与补充
(无参数) - 不需要传参 调用后当前进程会被复制一份,产生子进程
返回值 含义
= 0 这是在子进程中返回的,固定返回 0,用来标识"我是子进程"
> 0 这是在父进程中返回的,返回值是子进程的 PID
-1 创建失败,错误码存入 errno,一般是系统资源不足或进程数达到上限

⚠️ 注意:fork 的返回值不是当前进程的 PID!

  • 父进程中返回的是子进程的 PID
  • 子进程中固定返回 0(作为身份标识)
代码示例
/*
头文件说明:
<stdio.h>  printf标准输入输出函数
<unistd.h> fork、getpid、getppid、sleep 等系统调用声明
*/
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    /*
     变量 pid:存储 fork 的返回值,pid_t 类型(本质是 int)
     fork() 调用一次会返回两次:父进程返回子PID,子进程返回0
     */
    pid_t pid = fork();

    /* 判断 fork 是否创建失败 */
    if (pid == -1)
    {
        /*
         函数:perror()
         功能:打印自定义前缀 + 系统标准错误描述
         参数:"fork error" 自定义错误提示前缀
         */
        perror("fork error");
        return -1; // 程序异常退出
    }

    if (pid == 0)
    /* 子进程执行的代码分支 */
    {
        while (1)
        {
            /*
             getpid():获取当前进程自己的 PID
             getppid():获取当前进程的父进程 PID
             sleep(1):休眠1秒,避免刷屏太快
             */
            printf("[CHILD ] 我是子进程, pid=%d, 我的父进程pid=%d\n", 
                   getpid(), getppid());
            sleep(1);
        }
    }
    else
    /* 父进程执行的代码分支 */
    {
        while (1)
        {
            printf("[PARENT] 我是父进程, pid=%d, 我的子进程pid=%d\n", 
                   getpid(), pid);
            sleep(1);
        }
    }

    return 0;
}
现在来讲讲代码的实现:
1. fork 之后发生了什么?

调用 fork() 之后,操作系统会把当前进程(父进程)完完整整复制一份,产生一个子进程。子进程几乎继承了父进程的所有东西:代码、数据、堆栈、打开的文件描述符等等。

然后两个进程从 fork() 这一行之后各自独立运行,就像细胞分裂一样,一个变两个。

2. 怎么区分父进程和子进程?

fork() 的返回值来判断:

  • 返回 0 → 你现在在子进程里
  • 返回 > 0 → 你现在在父进程里,这个值就是子进程的 PID

这就是为什么代码里要用 if-else 分支来分别写父子进程的逻辑。

3. 父子进程谁先执行?

不一定!这由操作系统的调度器决定,可能父进程先跑,也可能子进程先跑,甚至交替着跑。如果你想控制顺序,后面会学到用 wait 来让父进程等子进程。


4. 终止进程

进程的终止分两种:正常终止异常终止

正常终止的两种方式:
方式 说明 适用场景
return 0 main 函数里 return main 函数结束,进程正常退出
exit(status) 调用 exit 函数退出 任何地方都能调用,直接终止整个进程
exit 的函数原型:
#include <stdlib.h>
void exit(int status);

参数说明

参数 数据类型 详细说明 使用规则与补充
status int 进程退出状态码,范围 0~255 父进程可以通过 wait/waitpid 获取这个值

退出状态码的约定:

调用 退出码 含义 典型用途
exit(0) 0 成功(EXIT_SUCCESS) 程序正常结束
exit(1) 1 失败(EXIT_FAILURE) 一般错误
exit(n) n 自定义错误码 脚本里用 $? 可以捕获
异常终止:

进程收到某些信号而被迫终止,比如:

  • kill -9 pid 发 SIGKILL 信号强杀
  • 段错误(访问非法内存)
  • 除以零等等

5. wait / waitpid 监控进程状态

前面提到僵尸进程的问题——子进程结束了,父进程不收尸就会变僵尸。怎么收尸?用 wait()waitpid()

wait 的函数原型:
#include <sys/wait.h>
pid_t wait(int *status);

参数说明

参数 数据类型 详细说明 使用规则与补充
status int * 传出参数,用来存子进程的退出状态 传 NULL 表示不关心退出状态
返回值 含义
> 0 成功,返回结束的那个子进程的 PID
-1 出错,比如没有子进程可等
waitpid 的函数原型:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

参数说明

参数 数据类型 详细说明 使用规则与补充
pid pid_t 指定等待哪个子进程 > 0:等指定 PID 的子进程
-1:等任意子进程(等同于 wait)
status int * 传出参数,存子进程退出状态 同 wait
options int 控制选项 0:阻塞等待
WNOHANG:非阻塞,没子进程退出就立即返回0
返回值 含义
> 0 成功,返回结束的子进程 PID
= 0 WNOHANG 模式下,没有子进程退出
-1 出错

💡 小技巧:用 WEXITSTATUS(status) 这个宏,可以从 status 里提取出子进程 exit 的退出码。

代码示例
/*
头文件说明:
<stdio.h>    printf、perror 标准输入输出
<stdlib.h>   exit 函数声明
<unistd.h>   fork、getpid、sleep 等系统调用
<sys/wait.h> wait、waitpid、WEXITSTATUS 等宏声明
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    /* 创建子进程 */
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("fork error");
        return 1;
    }
    else if (pid == 0)
    {
        // ===== 子进程 =====
        printf("子进程:我是子进程,PID = %d\n", getpid());
        printf("子进程:我要休眠2秒模拟干活...\n");
        sleep(2);
        printf("子进程:我干完了,准备退出,退出码是 88\n");
        exit(88); // 子进程以状态码88退出
    }
    else
    {
        // ===== 父进程 =====
        printf("父进程:我是父进程,PID = %d,子进程 PID = %d\n", getpid(), pid);
        printf("父进程:我等着子进程结束...\n");

        int status;
        /*
         waitpid:等待指定子进程结束
         参数1 pid:等待的子进程PID
         参数2 &status:传出子进程的退出状态
         参数3 0:阻塞模式,子进程不结束就一直等
         */
        pid_t ret = waitpid(pid, &status, 0);
        if (ret == -1)
        {
            perror("waitpid error");
            return 1;
        }

        printf("父进程:子进程 %d 结束了\n", ret);

        /*
         WIFEXITED(status):判断子进程是不是正常退出的
         WEXITSTATUS(status):提取正常退出的退出码
         */
        if (WIFEXITED(status))
        {
            printf("父进程:子进程是正常退出的,退出码是 %d\n", 
                   WEXITSTATUS(status));
        }
        else
        {
            printf("父进程:子进程是异常终止的\n");
        }
    }

    return 0;
}

6. exec 族函数替换进程

你可能发现了,fork 出来的子进程和父进程长得一模一样,干的也是一样的活。如果我想让子进程去执行一个完全不同的新程序怎么办?这就需要 exec 族函数了。

exec 的作用是:在当前进程里加载并运行一个新的程序,把原来的代码、数据全部替换掉。注意,exec 不创建新进程,只是替换当前进程的内容!

execl 的函数原型(最常用的一个):
#include <unistd.h>
int execl(const char *path, const char *arg, ...);

参数说明

参数 数据类型 详细说明 使用规则与补充
path const char * 可执行文件的绝对路径 比如 “/bin/ls”
arg const char * 参数列表,第一个参数通常是程序名(argv[0]) 最后必须以 (char *)NULL 结尾
返回值 含义
无返回值 成功的话直接去跑新程序了,不会返回
-1 失败才会返回,设置 errno

典型用法:execl("/bin/ls", "ls", "-l", "/home", NULL);

exec 族六个函数的区别:

exec 族一共有 6 个函数,功能都一样,就是传参方式和查找路径不同。可以通过后缀字母来记忆:

后缀字母 含义 对应函数
l (list) 参数用列表形式,一个个传 execl, execlp, execle
v (vector) 参数用数组形式传 execv, execvp, execve
p (path) 会自动在 PATH 环境变量里找程序,可以写相对路径 execlp, execvp
e (environment) 可以传自定义环境变量 execle, execve

简单对比:

函数 路径方式 参数形式 环境变量
execl 绝对路径 列表 继承父进程
execlp PATH 自动查找 列表 继承父进程
execv 绝对路径 数组 继承父进程
execvp PATH 自动查找 数组 继承父进程
execle 绝对路径 列表 自定义
execve 绝对路径 数组 自定义
代码示例(fork + execl 经典组合)
/*
头文件说明:
<stdio.h>    printf、perror 标准输入输出
<stdlib.h>   exit 函数声明
<unistd.h>   fork、execl、getpid 等系统调用
<sys/wait.h> waitpid、WEXITSTATUS 等宏
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    printf("主进程:PID = %d,准备创建子进程执行 ls -l\n", getpid());

    pid_t pid = fork();
    if (pid == -1)
    {
        perror("fork error");
        return 1;
    }
    else if (pid == 0)
    {
        // ===== 子进程:用 execl 替换成 ls 命令 =====
        printf("子进程:PID = %d,准备执行 ls -l /home\n", getpid());

        /*
         execl:加载并执行新程序
         参数1 "/bin/ls":ls 命令的绝对路径
         参数2 "ls":程序名(对应 argv[0])
         参数3 "-l":第一个命令行参数
         参数4 "/home":第二个命令行参数
         参数5 NULL:参数列表结束标志,必须有!
         */
        execl("/bin/ls", "ls", "-l", "/home", NULL);

        // 如果执行到这里,说明 execl 失败了(成功的话不会返回)
        perror("execl error");
        exit(1);
    }
    else
    {
        // ===== 父进程:等子进程执行完 =====
        int status;
        waitpid(pid, &status, 0);

        if (WIFEXITED(status))
        {
            printf("父进程:子进程执行完毕,退出码 = %d\n", 
                   WEXITSTATUS(status));
        }
    }

    return 0;
}

💡 经典套路:fork + exec 组合拳

  1. 先 fork 创建子进程
  2. 子进程里调用 exec 替换成新程序
  3. 父进程该干嘛干嘛,或者 wait 等子进程

这样既能创建新进程干新活,又不影响父进程原来的逻辑,这就是 Linux 下启动新程序的标准方式!


以上就是 Linux 进程管理的基础入门了,觉得有帮助的友友们可以点赞收藏支持一下小弟!!!

学完了可以自己尝试动手完成一个小练习,步骤如下:

写一个程序,父进程创建两个子进程:

  1. 第一个子进程执行 ls -l / 命令
  2. 第二个子进程执行 echo "hello world" 命令
  3. 父进程等待两个子进程都结束后,打印各自的退出状态,然后退出

提示:fork 两次,分别在两个子进程里调用 execlp,父进程用 waitpid 分别等。

下面是一个完整的 C 语言代码示例,实现了上述小练习的要求:

/*
 * 小练习完整代码示例:父进程创建两个子进程分别执行不同命令
 * 编译:gcc -o process_exercise process_exercise.c
 * 运行:./process_exercise
 */

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

int main(void)
{
    pid_t pid1, pid2;  // 存储两个子进程的PID
    int status1, status2;  // 存储两个子进程的退出状态

    printf("父进程启动,PID = %d\n", getpid());
    printf("========================================\n");

    // ========== 创建第一个子进程 ==========
    pid1 = fork();
    if (pid1 == -1)
    {
        perror("第一次 fork 失败");
        exit(EXIT_FAILURE);
    }
    else if (pid1 == 0)
    {
        // ===== 第一个子进程:执行 ls -l / 命令 =====
        printf("第一个子进程创建成功,PID = %d\n", getpid());
        printf("执行命令:ls -l /\n");
        printf("----------------------------------------\n");
        
        /*
         * execlp 参数说明:
         * 1. "ls" - 命令名,execlp会在PATH环境变量中查找
         * 2. "ls" - argv[0],通常是程序名
         * 3. "-l" - 命令行参数,表示长格式显示
         * 4. "/"  - 命令行参数,表示根目录
         * 5. NULL - 参数列表结束标志
         */
        execlp("ls", "ls", "-l", "/", NULL);
        
        // 如果执行到这里,说明 execlp 失败了
        perror("第一个子进程 execlp 失败");
        exit(EXIT_FAILURE);
    }

    // ========== 创建第二个子进程 ==========
    pid2 = fork();
    if (pid2 == -1)
    {
        perror("第二次 fork 失败");
        exit(EXIT_FAILURE);
    }
    else if (pid2 == 0)
    {
        // ===== 第二个子进程:执行 echo "hello world" 命令 =====
        printf("第二个子进程创建成功,PID = %d\n", getpid());
        printf("执行命令:echo \"hello world\"\n");
        printf("----------------------------------------\n");
        
        /*
         * execlp 参数说明:
         * 1. "echo" - 命令名
         * 2. "echo" - argv[0]
         * 3. "hello world" - 要输出的字符串
         * 4. NULL - 参数列表结束
         */
        execlp("echo", "echo", "hello world", NULL);
        
        // 如果执行到这里,说明 execlp 失败了
        perror("第二个子进程 execlp 失败");
        exit(EXIT_FAILURE);
    }

    // ========== 父进程等待两个子进程结束 ==========
    printf("父进程等待子进程结束...\n");
    printf("========================================\n");
    
    // 等待第一个子进程
    if (waitpid(pid1, &status1, 0) == -1)
    {
        perror("等待第一个子进程失败");
    }
    else
    {
        if (WIFEXITED(status1))
        {
            printf("第一个子进程 (PID=%d) 正常退出,退出码 = %d\n", 
                   pid1, WEXITSTATUS(status1));
        }
        else if (WIFSIGNALED(status1))
        {
            printf("第一个子进程 (PID=%d) 被信号终止,信号编号 = %d\n", 
                   pid1, WTERMSIG(status1));
        }
    }
    
    // 等待第二个子进程
    if (waitpid(pid2, &status2, 0) == -1)
    {
        perror("等待第二个子进程失败");
    }
    else
    {
        if (WIFEXITED(status2))
        {
            printf("第二个子进程 (PID=%d) 正常退出,退出码 = %d\n", 
                   pid2, WEXITSTATUS(status2));
        }
        else if (WIFSIGNALED(status2))
        {
            printf("第二个子进程 (PID=%d) 被信号终止,信号编号 = %d\n", 
                   pid2, WTERMSIG(status2));
        }
    }
    
    printf("========================================\n");
    printf("父进程结束\n");
    
    return 0;
}

代码说明

  1. 进程创建逻辑

    • 父进程先调用 fork() 创建第一个子进程(执行 ls -l /
    • 父进程再调用 fork() 创建第二个子进程(执行 echo "hello world"
    • 两个子进程创建后,父子进程并行执行
  2. 命令执行

    • 使用 execlp() 函数,它会自动在 PATH 环境变量中查找命令
    • 第一个子进程:execlp("ls", "ls", "-l", "/", NULL)
    • 第二个子进程:execlp("echo", "echo", "hello world", NULL)
  3. 进程等待

    • 父进程使用 waitpid() 分别等待两个子进程
    • 使用 WIFEXITED() 判断子进程是否正常退出
    • 使用 WEXITSTATUS() 获取子进程的退出状态码
  4. 错误处理

    • 检查 fork() 返回值是否为 -1(创建失败)
    • 检查 execlp() 是否返回(返回表示执行失败)
    • 检查 waitpid() 是否返回 -1(等待失败)

运行结果

父进程启动,PID = 12345
========================================
第一个子进程创建成功,PID = 12346
执行命令:ls -l /
----------------------------------------
第二个子进程创建成功,PID = 12347
执行命令:echo "hello world"
----------------------------------------
父进程等待子进程结束...
========================================
total 64
lrwxrwxrwx   1 root root     7 Apr 15  2023 bin -> usr/bin
drwxr-xr-x   4 root root  4096 Jun 24 20:30 boot
drwxr-xr-x  20 root root  4180 Jun 24 20:30 dev
drwxr-xr-x 146 root root 12288 Jun 24 20:30 etc
...(更多 ls -l / 的输出)...
hello world
第一个子进程 (PID=12346) 正常退出,退出码 = 0
第二个子进程 (PID=12347) 正常退出,退出码 = 0
========================================
父进程结束

结果说明

  1. ls -l / 命令会列出根目录的详细内容(权限、所有者、大小、时间等)
  2. echo "hello world" 命令会输出字符串 “hello world”
  3. 两个命令的退出码都是 0,表示执行成功
  4. 由于进程调度顺序不确定,两个子进程的输出顺序可能会交替出现,但父进程的等待和状态打印是顺序的

注意事项

  • 实际运行时的 PID 数字会不同
  • ls -l / 的输出内容取决于你的系统根目录
  • 如果系统没有 /bin/ls/bin/echo 命令,程序会执行失败

编译和运行步骤

# 1. 编译程序
gcc -o process_exercise process_exercise.c

# 2. 运行程序
./process_exercise

# 3. 查看退出状态(可选)
echo "第一个子进程退出码:$?"

你可以尝试修改代码,比如改变命令参数、添加更多子进程,或者尝试不同的 exec 族函数(如 execv、execvp 等)来加深理解!


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐