嵌入式 Linux 快速入门(二)
课程摘要:本课程是一份面向嵌入式 Linux 初学者的实践导向速成指南。上一篇我们学习了文件 I/O 操作的基础,这一篇我们继续深入进程相关的核心知识。课程摒弃繁杂的理论,以动手实操为核心,带你掌握进程的创建、终止、监控与替换等关键技能,为后续多进程并发编程打下坚实基础。
学习目标:
进程管理:
理解什么是进程,学会 fork 创建进程、exit 终止进程、wait/waitpid 监控进程状态、exec 族函数替换进程执行新程序
学习内容:
1. 进程的基本概念:
我们先搞懂两个问题:
什么是进程?
为什么需要进程?
大家写代码编译出来的可执行文件,那叫程序,是躺在硬盘上的静态文件。而当你运行这个程序,它被加载到内存中跑起来之后,就变成了进程。
进程就是正在运行的程序实例,每个进程都有自己独立的一套资源:代码段、数据段、堆栈、文件描述符等等,相当于一个独立的小环境。
每个进程都有一个唯一的身份证号,叫 进程 ID(PID),由操作系统分配,用来区分和管理不同的进程。
那为什么要有进程这个东西呢?主要有三个好处:
- 安全性:每个进程跑在独立的地址空间里,一个进程崩了不会连累其他进程,系统更稳定。
- 提高效率:多个进程可以同时跑(并发执行),充分利用 CPU 资源,响应更快。
- 简化编程:可以把一个复杂的大项目拆成多个小进程,每个进程负责一块功能,模块之间高内聚低耦合,代码更好维护。
比如做一个车载系统,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 组合拳
- 先 fork 创建子进程
- 子进程里调用 exec 替换成新程序
- 父进程该干嘛干嘛,或者 wait 等子进程
这样既能创建新进程干新活,又不影响父进程原来的逻辑,这就是 Linux 下启动新程序的标准方式!
以上就是 Linux 进程管理的基础入门了,觉得有帮助的友友们可以点赞收藏支持一下小弟!!!
学完了可以自己尝试动手完成一个小练习,步骤如下:
写一个程序,父进程创建两个子进程:
- 第一个子进程执行
ls -l /命令 - 第二个子进程执行
echo "hello world"命令 - 父进程等待两个子进程都结束后,打印各自的退出状态,然后退出
提示: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;
}
代码说明
-
进程创建逻辑:
- 父进程先调用
fork()创建第一个子进程(执行ls -l /) - 父进程再调用
fork()创建第二个子进程(执行echo "hello world") - 两个子进程创建后,父子进程并行执行
- 父进程先调用
-
命令执行:
- 使用
execlp()函数,它会自动在 PATH 环境变量中查找命令 - 第一个子进程:
execlp("ls", "ls", "-l", "/", NULL) - 第二个子进程:
execlp("echo", "echo", "hello world", NULL)
- 使用
-
进程等待:
- 父进程使用
waitpid()分别等待两个子进程 - 使用
WIFEXITED()判断子进程是否正常退出 - 使用
WEXITSTATUS()获取子进程的退出状态码
- 父进程使用
-
错误处理:
- 检查
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
========================================
父进程结束
结果说明:
ls -l /命令会列出根目录的详细内容(权限、所有者、大小、时间等)echo "hello world"命令会输出字符串 “hello world”- 两个命令的退出码都是 0,表示执行成功
- 由于进程调度顺序不确定,两个子进程的输出顺序可能会交替出现,但父进程的等待和状态打印是顺序的
注意事项:
- 实际运行时的 PID 数字会不同
ls -l /的输出内容取决于你的系统根目录- 如果系统没有
/bin/ls或/bin/echo命令,程序会执行失败
编译和运行步骤
# 1. 编译程序
gcc -o process_exercise process_exercise.c
# 2. 运行程序
./process_exercise
# 3. 查看退出状态(可选)
echo "第一个子进程退出码:$?"
你可以尝试修改代码,比如改变命令参数、添加更多子进程,或者尝试不同的 exec 族函数(如 execv、execvp 等)来加深理解!
更多推荐




所有评论(0)