文章目录
一、非阻塞式等待
1.1 首先介绍status值的简单获取
printf("success:%d, sig number:%d, child exit code:%d\n",
ret,
(status & 0x7F), // &上信号位低七位数据,来查看是否是正常终止
(status >> 8) & 0xFF); // 在正常终止的情况下,再去查看退出状态
我想说的是这里的>>并不会改变status,并没有赋值
右移获取次低八位数据
- 查看进程是否正常退出
(status & 0x7F)—>WIFEXITED(status) - 查看正常退出情况下进程退出码
(status >> 8) & 0xFF)—>WEXITSTATUS(status)
1.2 非阻塞式
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t id = fork();
if (id == 0) {
// 子进程
int cnt = 5;
while (cnt--) {
printf("我是子进程: %d, 父进程: %d, 计数: %d\n", getpid(), getppid(), cnt + 1);
sleep(1);
}
exit(10); // 子进程退出码
}
// 父进程:非阻塞等待子进程
int status = 0;
while (1) {
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret == 0) {
// 子进程还在运行
printf("父进程轮询中...\n");
sleep(1);
} else if (ret > 0) {
// 子进程退出
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程被信号终止,信号编号:%d\n", WTERMSIG(status));
}
break;
} else {
perror("waitpid 出错");
break;
}
}
return 0;
}
这里对于前面的阻塞式等待做了哪些修改呢?
- 阻塞式
waitpid(id, &status, 0); //其中 0 表示父进程会阻塞等待子进程结束。
- 非阻塞式
waitpid(id, &status, WNOHANG); // 使用WNOHANG这个宏,这样父进程就会立即返回,不会等待子进程结束
1.3 非阻塞等待+函数指针调度
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define Num 10
// 定义函数指针类型
typedef void (*func_t)();
// 函数指针数组
func_t handlerTask[Num];
// 示例任务函数
void task1() {
printf("执行任务1\n");
}
void task2() {
printf("执行任务2\n");
}
// 加载任务到函数指针数组
void loadTask() {
memset(handlerTask, 0, sizeof(handlerTask));
handlerTask[0] = task1;
handlerTask[1] = task2;
}
int main() {
loadTask(); // 加载任务
pid_t id = fork();
if (id == 0) {
// 子进程
int cnt = 5;
while (cnt--) {
printf("我是子进程: %d, 父进程: %d, 计数: %d\n", getpid(), getppid(), cnt + 1);
sleep(1);
}
exit(10); // 子进程退出码
}
// 父进程:非阻塞等待子进程
int status = 0;
while (1) {
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret == 0) {
// 子进程还在运行
printf("父进程轮询中...\n");
sleep(1);
} else if (ret > 0) {
// 子进程退出
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程被信号终止,信号编号:%d\n", WTERMSIG(status));
}
break;
} else {
perror("waitpid 出错");
break;
}
}
// 父进程执行任务
printf("父进程开始执行任务...\n");
for (int i = 0; i < Num; ++i) {
if (handlerTask[i]) {
handlerTask[i](); // 执行任务函数
}
}
return 0;
}
1.4 非阻塞等待的意义
1. 保持父进程活跃
父进程不会因为等待子进程而停滞,可以继续执行其他任务,比如:
- 处理用户输入
- 执行后台逻辑
- 维护网络连接
2. 支持并发任务调度
可以同时管理多个子进程,轮询它们的状态,而不是一个个阻塞等待。
for (int i = 0; i < N; ++i) {
waitpid(child[i], &status[i], WNOHANG);
}
3. 提高程序响应性
在图形界面、服务器、嵌入式系统中,阻塞会导致界面卡顿或服务中断。非阻塞等待能让程序持续响应外部事件。
4. 更灵活的控制流程
你可以根据子进程状态决定是否继续等待、重启任务、或执行清理操作,而不是被动等待。
二、进程程序替换
2.1 程序替换如何执行
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("process is running\n");
// 使用 execlp 自动查找路径并执行 ls 命令
execlp("ls", "ls", "-a", "-l", NULL);
// 如果 execlp 执行失败,打印错误信息
perror("execlp");
// 这行代码不会执行,除非 execlp 失败
printf("process done\n");
return 0;
}
程序输出结果

在这之前我们再来介绍一下创建子进程的两个目的
- 子进程执行父进程代码的一部分,也就是执行对应的磁盘代码的一部分
- 想让子进程执行一个全新的程序,子进程加载磁盘上指定的程序,执行新的程序代码和数据,这也就是今天这一节的主题也就是进程程序替换
这里我们观察输出结果,发现在进程程序替换之后,
printf("process done");这一句代码并没有执行,也就是说明,进程程序替换,是做了一个覆盖。
替换原理: 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
2.2 exec 系列函数一览(共 6 个)
| 函数名 | 参数形式 | 是否查找 PATH | 说明 |
|---|---|---|---|
execl | 列表 | ❌ 不查找 PATH | 参数一个个列出来 |
execlp | 列表 | ✅ 查找 PATH | 自动查找命令路径 |
execle | 列表 + 环境 | ❌ 不查找 PATH | 可指定环境变量 |
execv | 数组 | ❌ 不查找 PATH | 参数用数组传递 |
execvp | 数组 | ✅ 查找 PATH | 自动查找命令路径 |
execve | 数组 + 环境 | ❌ 不查找 PATH | 最底层,需指定环境变量 |
不过相比于上面的表格,下面这张我认为更加方便大家记忆
| 字母 | 含义 | 说明 |
|---|---|---|
l | list(列表) | 参数一个个列出来,如 "ls", "-l", NULL |
v | vector(数组) | 参数用数组传递,如 char* args[] = {"ls", "-l", NULL} |
p | path(自动查找路径) | 会根据环境变量 PATH 自动查找程序位置 |
e | env(自定义环境变量) | 允许你传入自己的环境变量数组 envp[] |
2.3 示例:执行 ls -a -l 命令
1. execl
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
2. execlp
execlp("ls", "ls", "-a", "-l", NULL);
3. execle
/* 对于自己构建的环境变量 */
char*const envp_[] = {
(char*)"MYENV=111111", NULL // 这里不进行强转的话他又不认
};
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, envp_);
/* 对于系统环境变量的传入 */
extern char** environ; // 声明这个二级指针,否则头文件又不知道了
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);
/* 那如果我这两种都想要呢?怎么办? 这个时候就要用到 putenv */
// putenv:向当前进程添加或修改环境变量
putenv("MYENV=22222");
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);
// 这个时候咱们自己的环境变量就被添加到这张系统环境变量表当中了
4. execv
char* args_[] = { "ls", "-a", "-l", NULL };
execv("/usr/bin/ls", args_);
5. execvp
char* args[] = { "ls", "-a", "-l", NULL };
execvp("ls", args);
6. execve
char* args[] = { "ls", "-a", "-l", NULL };
char* envp[] = { "PATH=/bin", NULL };
execve("/usr/bin/ls", args, envp);
2.4 程序替换调用别的语言(C 调用 Python 脚本)
示例代码:C 调用 Python 脚本 hello.py
🔹 Python 脚本(hello.py)
# hello.py
print("Hello from Python!")
🔹 C 程序(call_python.c)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
printf("Calling Python script...\n");
// 使用 execlp 调用 python3 并执行 hello.py
execlp("python3", "python3", "hello.py", NULL);
// 如果 exec 调用失败
perror("execlp failed");
return 1;
}
🧪 编译并运行
gcc call_python.c -o call_python
./call_python
输出:
Calling Python script...
Hello from Python!
2.5 execve真正的系统调用
使用man指令查看手册会发现这六个函数

我们继续深入理解 execve ——真正的系统调用。
为什么说 execve 是“真正的系统调用”?
因为它是 Linux 内核提供的原始接口,其他所有 exec 系列函数(如 execl, execvp, execle 等)都是对它的封装和简化。它直接与内核交互,完成进程替换的核心操作。
示例:使用 execve 执行 ls -l
#include <unistd.h>
#include <stdio.h>
int main() {
char *args[] = { "ls", "-l", NULL };
char *env[] = { "PATH=/bin", NULL };
execve("/bin/ls", args, env);
// 如果 execve 失败,打印错误信息
perror("execve failed");
return 1;
}
exec 系列函数调用关系图(简化)
execl ┐
execle ├───► 封装参数 + 环境变量 → 调用 execve
execlp ┘
execv ┐
execvp ├───► 封装参数数组 → 调用 execve
execve ◄─── 最终调用的系统接口
这个底层调用在2号手册之中,而这一章就是系统调用的手册页!

2.5 关于内建命令
就拿cd命令进行举例
当然可以!下面我用一个简单的例子来说明如何在你自己的 shell 程序中处理内建命令 cd(切换目录)。
用户输入命令
假设用户在你的 shell 中输入:
cd /home/user/Documents
你会读取这一行并解析成一个字符串数组:
myargv[0] = "cd";
myargv[1] = "/home/user/Documents";
在 do_exec() 中处理 cd
void do_exec(char** myargv)
{
if (myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
{
if (myargv[1] != NULL)
{
if (chdir(myargv[1]) != 0)
{
perror("cd failed");
}
}
else
{
fprintf(stderr, "cd: missing argument\n");
}
return; // 处理完内建命令后直接返回
}
// 非内建命令,使用 fork 和 execvp 执行
pid_t id = fork();
assert(id >= 0);
if (id == 0)
{
execvp(myargv[0], myargv);
perror("execvp failed");
exit(1);
}
int status;
waitpid(id, &status, 0);
}
所以我们发现,有些程序是不需要调用子进程来完成的,或者说是必须要使用父进程来进行操作,这样的命令我们就称之为内建命令。
三、Makefile
.PHONY: all # 声明伪目标,避免与文件名冲突
all: mybin processsub # 默认目标,构建 mybin 和 processsub 两个可执行文件
mybin: mybin.c # 构建 mybin,依赖 mybin.c
gcc -o $@ $^ -std=c99 # 编译命令:$@ 表示目标名,$^ 表示所有依赖
processsub: processsub.c # 构建 processsub,依赖 processsub.c
gcc -o $@ $^ -std=c99 # 编译命令,同上
.PHONY: clean # 声明 clean 为伪目标
clean: # 清理目标
rm -f processsub mybin # 删除生成的可执行文件

被折叠的 条评论
为什么被折叠?



