第20章 进程与进程间通信
第20章 进程与进程间通信
章节摘要
进程是操作系统进行资源分配和调度的基本单位。理解进程的概念、生命周期和进程间通信机制,是进行系统编程的基础。本章将深入讲解进程的创建、管理和进程间通信的各种方式。
20.1 进程基础
20.1.1 什么是进程
定义: 进程(Process)是程序的一次执行实例,是操作系统进行资源分配和调度的基本单位。
进程 vs 程序:
| 特性 | 程序 | 进程 |
|---|---|---|
| 本质 | 静态的代码和数据 | 动态的执行实例 |
| 生命周期 | 永久存在 | 创建、运行、终止 |
| 资源 | 不占用系统资源 | 占用CPU、内存等资源 |
| 数量 | 一个程序文件 | 可以有多个进程实例 |
进程的组成:
- 代码段(Text Segment):存放程序的机器指令
- 数据段(Data Segment):存放全局变量和静态变量
- 堆(Heap):动态分配的内存
- 栈(Stack):存放局部变量和函数调用信息
- 进程控制块(PCB):存放进程的状态信息
进程的状态:
新建(New) → 就绪(Ready) → 运行(Running) → 终止(Terminated)
↑ ↓
└── 阻塞(Blocked)
- 新建:进程正在被创建
- 就绪:进程已准备好运行,等待CPU
- 运行:进程正在CPU上执行
- 阻塞:进程等待某个事件(如I/O)
- 终止:进程执行完毕
20.1.2 查看进程信息
使用ps命令:
# 查看当前用户的进程
ps
# 查看所有进程
ps aux
# 查看进程树
ps auxf
# 查看特定进程
ps -p <PID>
使用top命令:
# 实时查看进程状态
top
# 按内存使用排序
top -o %MEM
# 按CPU使用排序
top -o %CPU
在C程序中获取进程信息:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("进程ID (PID): %d\n", getpid());
printf("父进程ID (PPID): %d\n", getppid());
printf("用户ID (UID): %d\n", getuid());
printf("有效用户ID (EUID): %d\n", geteuid());
printf("组ID (GID): %d\n", getgid());
return 0;
}
输出示例:
进程ID (PID): 12345
父进程ID (PPID): 12344
用户ID (UID): 1000
有效用户ID (EUID): 1000
组ID (GID): 1000
20.1.3 进程的创建 - fork()
fork()函数:
#include <unistd.h>
pid_t fork(void);
返回值:
- 父进程:返回子进程的PID(大于0)
- 子进程:返回0
- 失败:返回-1
fork()的工作原理:
- 创建一个新进程(子进程)
- 子进程是父进程的副本
- 子进程拥有父进程的代码、数据、堆、栈的副本
- 子进程和父进程并发执行
示例:第一个fork程序
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("程序开始,PID = %d\n", getpid());
pid_t pid = fork();
if (pid < 0) {
// fork失败
perror("fork失败");
return 1;
} else if (pid == 0) {
// 子进程
printf("我是子进程,PID = %d,父进程PID = %d\n",
getpid(), getppid());
} else {
// 父进程
printf("我是父进程,PID = %d,子进程PID = %d\n",
getpid(), pid);
}
printf("进程 %d 结束\n", getpid());
return 0;
}
输出:
程序开始,PID = 12345
我是父进程,PID = 12345,子进程PID = 12346
进程 12345 结束
我是子进程,PID = 12346,父进程PID = 12345
进程 12346 结束
重要特性:
- fork后父子进程的执行顺序是不确定的
- 子进程复制父进程的内存空间(写时复制)
- 子进程继承父进程的文件描述符
- 子进程有独立的PID
20.1.4 fork()的常见陷阱
陷阱1:不检查fork返回值
// ❌ 错误:没有检查fork是否成功
pid_t pid = fork();
if (pid == 0) {
// 子进程
} else {
// 父进程
}
// 如果fork失败,pid=-1,会被当作父进程处理
// ✅ 正确:检查所有三种情况
pid_t pid = fork();
if (pid < 0) {
perror("fork失败");
return 1;
} else if (pid == 0) {
// 子进程
} else {
// 父进程
}
陷阱2:误解fork的返回值
// ❌ 错误理解:认为fork返回两次
pid_t pid = fork();
printf("pid = %d\n", pid);
// 实际上:父进程打印子进程PID,子进程打印0
// ✅ 正确理解:fork创建了两个进程,各自执行后续代码
pid_t pid = fork();
if (pid == 0) {
printf("子进程:fork返回 %d\n", pid);
} else {
printf("父进程:fork返回 %d(子进程PID)\n", pid);
}
陷阱3:多次fork导致进程数量爆炸
// ❌ 危险:创建2^n个进程
for (int i = 0; i < 10; i++) {
fork(); // 每次fork都会使进程数量翻倍
}
// 10次fork后会有2^10 = 1024个进程!
// ✅ 正确:只在父进程中fork
for (int i = 0; i < 10; i++) {
pid_t pid = fork();
if (pid == 0) {
// 子进程:执行任务后退出
do_work(i);
exit(0);
}
}
// 只创建10个子进程
陷阱4:忘记子进程会复制父进程的变量
#include <stdio.h>
#include <unistd.h>
int main() {
int x = 10;
pid_t pid = fork();
if (pid == 0) {
// 子进程
x = 20;
printf("子进程:x = %d\n", x);
} else {
// 父进程
sleep(1); // 等待子进程先执行
printf("父进程:x = %d\n", x);
}
return 0;
}
输出:
子进程:x = 20
父进程:x = 10
说明: 子进程修改x不会影响父进程,因为它们有独立的内存空间。
20.1.5 进程的终止
正常终止的方式:
- 从main返回
- 调用exit()
- 调用_exit()或_Exit()
异常终止的方式:
- 调用abort()
- 接收到信号
- 最后一个线程响应取消请求
exit() vs _exit():
| 函数 | 说明 |
|---|---|
exit(status) |
标准库函数,会执行清理工作(刷新缓冲区、调用atexit注册的函数) |
_exit(status) |
系统调用,立即终止,不执行清理工作 |
示例:exit的清理工作
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void cleanup(void) {
printf("清理函数被调用\n");
}
int main() {
atexit(cleanup);
printf("使用exit终止"); // 注意:没有\n
exit(0);
// 或者使用_exit
// printf("使用_exit终止");
// _exit(0);
}
使用exit()输出:
使用exit终止清理函数被调用
使用_exit()输出:
说明: exit()会刷新stdout缓冲区,所以"使用exit终止"被打印出来;_exit()不会刷新缓冲区,所以什么都不打印。
退出状态码:
#include <stdlib.h>
// 成功
exit(EXIT_SUCCESS); // 或 exit(0)
// 失败
exit(EXIT_FAILURE); // 或 exit(1)
// 自定义状态码
exit(2); // 表示特定的错误类型
在shell中查看退出状态:
./program
echo $? # 打印上一个程序的退出状态
20.2 等待子进程
20.2.1 wait()函数
wait()函数:
#include <sys/wait.h>
pid_t wait(int *status);
功能: 等待任意一个子进程终止。
返回值:
- 成功:返回终止的子进程PID
- 失败:返回-1
status参数: 用于获取子进程的退出状态。
示例:使用wait等待子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork失败");
return 1;
} else if (pid == 0) {
// 子进程
printf("子进程开始,PID = %d\n", getpid());
sleep(2);
printf("子进程结束\n");
exit(42); // 退出状态码42
} else {
// 父进程
printf("父进程等待子进程...\n");
int status;
pid_t child_pid = wait(&status);
printf("子进程 %d 已终止\n", child_pid);
if (WIFEXITED(status)) {
printf("子进程正常退出,退出状态码 = %d\n",
WEXITSTATUS(status));
}
}
return 0;
}
输出:
父进程等待子进程...
子进程开始,PID = 12346
子进程结束
子进程 12346 已终止
子进程正常退出,退出状态码 = 42
status宏:
| 宏 | 说明 |
|---|---|
WIFEXITED(status) |
子进程正常终止返回true |
WEXITSTATUS(status) |
获取子进程的退出状态码 |
WIFSIGNALED(status) |
子进程被信号终止返回true |
WTERMSIG(status) |
获取终止子进程的信号编号 |
WIFSTOPPED(status) |
子进程被停止返回true |
WSTOPSIG(status) |
获取停止子进程的信号编号 |
20.2.2 waitpid()函数
waitpid()函数:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
参数:
-
pid:
> 0:等待指定PID的子进程= 0:等待同一进程组的任意子进程= -1:等待任意子进程(同wait)< -1:等待进程组ID等于|pid|的任意子进程
-
options:
0:阻塞等待WNOHANG:非阻塞,如果没有子进程终止立即返回0WUNTRACED:报告停止的子进程
示例:使用waitpid等待特定子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid1 = fork();
if (pid1 == 0) {
printf("子进程1,PID = %d\n", getpid());
sleep(3);
exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
printf("子进程2,PID = %d\n", getpid());
sleep(1);
exit(2);
}
// 父进程:先等待pid2
printf("父进程等待子进程2...\n");
int status;
waitpid(pid2, &status, 0);
printf("子进程2已终止,退出状态 = %d\n", WEXITSTATUS(status));
// 再等待pid1
printf("父进程等待子进程1...\n");
waitpid(pid1, &status, 0);
printf("子进程1已终止,退出状态 = %d\n", WEXITSTATUS(status));
return 0;
}
输出:
子进程1,PID = 12346
子进程2,PID = 12347
父进程等待子进程2...
子进程2已终止,退出状态 = 2
父进程等待子进程1...
子进程1已终止,退出状态 = 1
示例:非阻塞等待
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
printf("子进程睡眠3秒...\n");
sleep(3);
exit(0);
}
// 父进程:非阻塞等待
printf("父进程开始非阻塞等待\n");
int status;
pid_t result;
while (1) {
result = waitpid(pid, &status, WNOHANG);
if (result == 0) {
// 子进程还在运行
printf("子进程还在运行...\n");
sleep(1);
} else if (result == pid) {
// 子进程已终止
printf("子进程已终止\n");
break;
} else {
perror("waitpid失败");
break;
}
}
return 0;
}
输出:
父进程开始非阻塞等待
子进程睡眠3秒...
子进程还在运行...
子进程还在运行...
子进程还在运行...
子进程已终止
20.2.3 僵尸进程(Zombie Process)
什么是僵尸进程?
当子进程终止后,如果父进程没有调用wait/waitpid回收子进程,子进程会变成僵尸进程。
僵尸进程的特点:
- 进程已终止,但进程表项仍然存在
- 占用PID和少量内存(保存退出状态)
- 无法被kill命令杀死
- 大量僵尸进程会耗尽PID资源
示例:创建僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("子进程 %d 即将退出\n", getpid());
exit(0);
} else {
// 父进程:不调用wait,让子进程变成僵尸
printf("父进程 %d,子进程 %d\n", getpid(), pid);
printf("使用 'ps aux | grep Z' 查看僵尸进程\n");
sleep(30); // 保持父进程运行
}
return 0;
}
查看僵尸进程:
ps aux | grep Z
# 或
ps aux | grep defunct
输出示例:
user 12346 0.0 0.0 0 0 ? Z 10:30 0:00 [program] <defunct>
避免僵尸进程的方法:
方法1:父进程调用wait/waitpid
// ✅ 正确:及时回收子进程
pid_t pid = fork();
if (pid == 0) {
// 子进程
exit(0);
} else {
// 父进程
wait(NULL); // 回收子进程
}
方法2:忽略SIGCHLD信号
#include <signal.h>
// 设置为忽略SIGCHLD信号,子进程终止时自动回收
signal(SIGCHLD, SIG_IGN);
pid_t pid = fork();
if (pid == 0) {
exit(0);
}
// 父进程不需要调用wait
方法3:使用信号处理函数
#include <signal.h>
#include <sys/wait.h>
void sigchld_handler(int sig) {
// 回收所有已终止的子进程
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main() {
signal(SIGCHLD, sigchld_handler);
// 创建子进程...
return 0;
}
20.2.4 孤儿进程(Orphan Process)
什么是孤儿进程?
当父进程先于子进程终止时,子进程会变成孤儿进程。
孤儿进程的处理:
- 孤儿进程会被init进程(PID=1)收养
- init进程会自动回收孤儿进程
- 不会造成资源泄漏
示例:创建孤儿进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("子进程 %d,父进程 %d\n", getpid(), getppid());
sleep(5);
printf("5秒后,子进程 %d,父进程 %d\n", getpid(), getppid());
// 父进程已终止,PPID变为1(init进程)
} else {
// 父进程
printf("父进程 %d 即将退出\n", getpid());
sleep(1);
// 父进程退出,子进程变成孤儿
}
return 0;
}
输出:
父进程 12345 即将退出
子进程 12346,父进程 12345
5秒后,子进程 12346,父进程 1
僵尸进程 vs 孤儿进程:
| 特性 | 僵尸进程 | 孤儿进程 |
|---|---|---|
| 产生原因 | 子进程终止,父进程未回收 | 父进程先终止 |
| 父进程 | 存在但不回收 | 不存在 |
| 危害 | 占用PID,可能耗尽资源 | 无危害 |
| 处理 | 父进程调用wait回收 | init自动收养并回收 |
| 状态 | Z(zombie) | 正常运行 |
20.3 exec函数族
20.3.1 什么是exec
exec函数族: 用于在当前进程中执行新的程序,替换当前进程的代码段、数据段、堆和栈。
exec的特点:
- 不创建新进程,PID不变
- 替换当前进程的映像
- 成功后不返回,失败返回-1
- 通常与fork配合使用
exec函数族:
| 函数 | 说明 |
|---|---|
execl(path, arg0, arg1, ..., NULL) |
列表方式传参,需要路径 |
execv(path, argv[]) |
数组方式传参,需要路径 |
execle(path, arg0, ..., NULL, envp[]) |
列表传参,指定环境变量 |
execve(path, argv[], envp[]) |
数组传参,指定环境变量 |
execlp(file, arg0, ..., NULL) |
列表传参,在PATH中搜索 |
execvp(file, argv[]) |
数组传参,在PATH中搜索 |
命名规则:
- l:list,参数以列表形式传递
- v:vector,参数以数组形式传递
- p:path,在PATH环境变量中搜索程序
- e:environment,可以指定环境变量
20.3.2 execl和execv
示例:使用execl执行ls命令
#include <stdio.h>
#include <unistd.h>
int main() {
printf("执行ls命令:\n");
// execl需要完整路径
execl("/bin/ls", "ls", "-l", "-h", NULL);
// 如果execl成功,下面的代码不会执行
perror("execl失败");
return 1;
}
示例:使用execv执行程序
#include <stdio.h>
#include <unistd.h>
int main() {
char *args[] = {
"ls",
"-l",
"-h",
NULL // 必须以NULL结尾
};
printf("执行ls命令:\n");
execv("/bin/ls", args);
perror("execv失败");
return 1;
}
示例:fork + exec模式
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
printf("父进程 PID = %d\n", getpid());
pid_t pid = fork();
if (pid < 0) {
perror("fork失败");
return 1;
} else if (pid == 0) {
// 子进程:执行新程序
printf("子进程 PID = %d,执行ls命令\n", getpid());
execl("/bin/ls", "ls", "-l", NULL);
// 如果exec失败才会执行到这里
perror("execl失败");
exit(1);
} else {
// 父进程:等待子进程
printf("父进程等待子进程...\n");
int status;
wait(&status);
printf("子进程已终止\n");
printf("父进程继续执行\n");
}
return 0;
}
输出:
父进程 PID = 12345
父进程等待子进程...
子进程 PID = 12346,执行ls命令
total 24
-rwxr-xr-x 1 user user 8960 May 5 10:30 program
-rw-r--r-- 1 user user 456 May 5 10:29 program.c
子进程已终止
父进程继续执行
20.3.3 execlp和execvp
示例:使用execlp(在PATH中搜索)
#include <stdio.h>
#include <unistd.h>
int main() {
// execlp会在PATH环境变量中搜索程序
// 不需要指定完整路径
execlp("ls", "ls", "-l", NULL);
perror("execlp失败");
return 1;
}
示例:执行自定义程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "用法: %s <命令> [参数...]\n", argv[0]);
return 1;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程:执行用户指定的命令
execvp(argv[1], &argv[1]);
perror("execvp失败");
exit(1);
} else {
// 父进程:等待子进程
int status;
wait(&status);
if (WIFEXITED(status)) {
printf("命令退出状态: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
使用示例:
./program ls -l
./program echo "Hello World"
./program gcc --version
20.3.4 exec的常见陷阱
陷阱1:忘记NULL结尾
// ❌ 错误:参数列表没有NULL结尾
execl("/bin/ls", "ls", "-l");
// ✅ 正确:必须以NULL结尾
execl("/bin/ls", "ls", "-l", NULL);
陷阱2:第一个参数的含义
// ⚠️ 注意:第一个参数是程序路径,第二个参数是argv[0]
execl("/bin/ls", "ls", "-l", NULL);
// ^^^^^^^^ ^^^^
// 程序路径 argv[0]
// 通常argv[0]设置为程序名,但可以设置为其他值
execl("/bin/ls", "myls", "-l", NULL);
// 程序内部看到的argv[0]是"myls"
陷阱3:exec后的代码不会执行
// ❌ 错误理解:认为exec后会返回
execl("/bin/ls", "ls", NULL);
printf("这行不会执行\n"); // exec成功后不会执行
// ✅ 正确:只有exec失败才会继续执行
if (execl("/bin/ls", "ls", NULL) == -1) {
perror("exec失败");
exit(1);
}
陷阱4:路径错误
// ❌ 错误:使用相对路径但程序不在当前目录
execl("ls", "ls", NULL); // 失败
// ✅ 正确:使用完整路径
execl("/bin/ls", "ls", NULL);
// ✅ 或使用execlp在PATH中搜索
execlp("ls", "ls", NULL);
20.4 进程间通信(IPC)
20.4.1 IPC概述
什么是IPC?
进程间通信(Inter-Process Communication, IPC)是指在不同进程之间传递数据或信号的机制。
为什么需要IPC?
- 进程之间的内存空间是独立的
- 需要共享数据或协调工作
- 实现进程间的同步和互斥
常见的IPC方式:
| 方式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 管道(Pipe) | 半双工通信 | 简单易用 | 只能用于有亲缘关系的进程 |
| 命名管道(FIFO) | 半双工通信 | 可用于无亲缘关系的进程 | 仍是半双工 |
| 消息队列 | 消息传递 | 可以有多个读写者 | 有大小限制 |
| 共享内存 | 直接访问内存 | 速度最快 | 需要同步机制 |
| 信号量 | 同步机制 | 可以实现互斥和同步 | 不能传递数据 |
| 信号 | 异步通知 | 简单 | 信息量少 |
| 套接字(Socket) | 网络通信 | 可跨网络 | 相对复杂 |
20.4.2 管道(Pipe)
什么是管道?
管道是一种半双工的通信方式,数据只能单向流动。
管道的特点:
- 数据先进先出(FIFO)
- 只能用于有亲缘关系的进程
- 管道是特殊的文件,只存在于内存中
- 一端写入,另一端读取
pipe()函数:
#include <unistd.h>
int pipe(int pipefd[2]);
参数:
pipefd[0]:读端pipefd[1]:写端
返回值:
- 成功:0
- 失败:-1
示例:父子进程通过管道通信
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
int pipefd[2];
char buffer[100];
// 创建管道
if (pipe(pipefd) == -1) {
perror("pipe失败");
return 1;
}
pid_t pid = fork();
if (pid < 0) {
perror("fork失败");
return 1;
} else if (pid == 0) {
// 子进程:从管道读取
close(pipefd[1]); // 关闭写端
ssize_t n = read(pipefd[0], buffer, sizeof(buffer));
if (n > 0) {
buffer[n] = '\0';
printf("子进程收到: %s\n", buffer);
}
close(pipefd[0]);
exit(0);
} else {
// 父进程:向管道写入
close(pipefd[0]); // 关闭读端
const char *msg = "Hello from parent!";
write(pipefd[1], msg, strlen(msg));
close(pipefd[1]);
wait(NULL);
}
return 0;
}
输出:
子进程收到: Hello from parent!
重要规则:
- 关闭不使用的端:读进程关闭写端,写进程关闭读端
- 写端全部关闭时:读端read返回0(EOF)
- 读端全部关闭时:写端write产生SIGPIPE信号
- 管道为空时:read阻塞
- 管道满时:write阻塞
示例:双向通信需要两个管道
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
int pipe1[2]; // 父→子
int pipe2[2]; // 子→父
char buffer[100];
if (pipe(pipe1) == -1 || pipe(pipe2) == -1) {
perror("pipe失败");
return 1;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程
close(pipe1[1]); // 关闭pipe1写端
close(pipe2[0]); // 关闭pipe2读端
// 从pipe1读取
read(pipe1[0], buffer, sizeof(buffer));
printf("子进程收到: %s\n", buffer);
// 向pipe2写入
const char *reply = "Hello from child!";
write(pipe2[1], reply, strlen(reply));
close(pipe1[0]);
close(pipe2[1]);
exit(0);
} else {
// 父进程
close(pipe1[0]); // 关闭pipe1读端
close(pipe2[1]); // 关闭pipe2写端
// 向pipe1写入
const char *msg = "Hello from parent!";
write(pipe1[1], msg, strlen(msg));
// 从pipe2读取
ssize_t n = read(pipe2[0], buffer, sizeof(buffer));
buffer[n] = '\0';
printf("父进程收到: %s\n", buffer);
close(pipe1[1]);
close(pipe2[0]);
wait(NULL);
}
return 0;
}
输出:
子进程收到: Hello from parent!
父进程收到: Hello from child!
20.4.3 管道的常见陷阱
陷阱1:忘记关闭不使用的端
// ❌ 错误:没有关闭写端
int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
// 子进程只读,但没有关闭写端
read(pipefd[0], buffer, sizeof(buffer));
// 如果所有写端都没关闭,read会一直阻塞
}
// ✅ 正确:关闭不使用的端
if (pid == 0) {
close(pipefd[1]); // 关闭写端
read(pipefd[0], buffer, sizeof(buffer));
close(pipefd[0]);
}
陷阱2:管道容量限制
// ⚠️ 注意:管道有容量限制(通常64KB)
// 写入大量数据可能阻塞
// 解决方案:分块写入或使用非阻塞I/O
const int CHUNK_SIZE = 4096;
for (int i = 0; i < total_size; i += CHUNK_SIZE) {
write(pipefd[1], data + i, CHUNK_SIZE);
}
陷阱3:死锁
// ❌ 危险:可能导致死锁
// 父进程写满管道后阻塞,等待子进程读取
// 但子进程也在写,等待父进程读取
// 双方都在等待,形成死锁
// ✅ 解决方案:使用非阻塞I/O或多线程
20.4.4 命名管道(FIFO)
什么是FIFO?
命名管道(FIFO)是一种特殊的文件,可以用于无亲缘关系的进程间通信。
FIFO的特点:
- 有文件名,存在于文件系统中
- 可以用于任意进程间通信
- 仍然是半双工通信
- 数据先进先出
创建FIFO:
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
示例:写进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
const char *fifo_path = "/tmp/myfifo";
// 创建FIFO
if (mkfifo(fifo_path, 0666) == -1) {
perror("mkfifo失败");
// 如果FIFO已存在,继续执行
}
printf("打开FIFO进行写入...\n");
int fd = open(fifo_path, O_WRONLY);
if (fd == -1) {
perror("open失败");
return 1;
}
const char *msg = "Hello through FIFO!";
write(fd, msg, strlen(msg));
printf("已写入: %s\n", msg);
close(fd);
return 0;
}
示例:读进程
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
const char *fifo_path = "/tmp/myfifo";
char buffer[100];
printf("打开FIFO进行读取...\n");
int fd = open(fifo_path, O_RDONLY);
if (fd == -1) {
perror("open失败");
return 1;
}
ssize_t n = read(fd, buffer, sizeof(buffer));
if (n > 0) {
buffer[n] = '\0';
printf("读取到: %s\n", buffer);
}
close(fd);
unlink(fifo_path); // 删除FIFO文件
return 0;
}
使用方法:
# 终端1:运行读进程
./reader
# 终端2:运行写进程
./writer
20.4.5 信号(Signal)
什么是信号?
信号(Signal)是一种软件中断,用于通知进程发生了某个事件。
常见信号:
| 信号 | 值 | 说明 | 默认动作 |
|---|---|---|---|
| SIGINT | 2 | 中断(Ctrl+C) | 终止 |
| SIGQUIT | 3 | 退出(Ctrl+\) | 终止+core dump |
| SIGKILL | 9 | 强制终止 | 终止(不可捕获) |
| SIGSEGV | 11 | 段错误 | 终止+core dump |
| SIGTERM | 15 | 终止信号 | 终止 |
| SIGCHLD | 17 | 子进程状态改变 | 忽略 |
| SIGSTOP | 19 | 停止进程 | 停止(不可捕获) |
| SIGCONT | 18 | 继续运行 | 继续 |
发送信号:
#include <signal.h>
int kill(pid_t pid, int sig);
示例:发送信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
pid_t pid;
printf("输入要发送信号的进程PID: ");
if (scanf("%d", &pid) != 1) {
printf("输入错误\n");
return 1;
}
printf("发送SIGTERM信号到进程 %d\n", pid);
if (kill(pid, SIGTERM) == 0) {
printf("信号发送成功\n");
} else {
perror("kill失败");
}
return 0;
}
捕获信号:
#include <signal.h>
void (*signal(int sig, void (*handler)(int)))(int);
示例:捕获SIGINT信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("\n捕获到SIGINT信号 (Ctrl+C)\n");
printf("程序不会退出,再按一次Ctrl+C\n");
}
int main() {
// 注册信号处理函数
signal(SIGINT, sigint_handler);
printf("程序运行中,按Ctrl+C测试信号处理\n");
printf("PID = %d\n", getpid());
while (1) {
printf("工作中...\n");
sleep(2);
}
return 0;
}
输出:
程序运行中,按Ctrl+C测试信号处理
PID = 12345
工作中...
工作中...
^C
捕获到SIGINT信号 (Ctrl+C)
程序不会退出,再按一次Ctrl+C
工作中...
工作中...
^C
捕获到SIGINT信号 (Ctrl+C)
程序不会退出,再按一次Ctrl+C
示例:优雅退出
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <stdbool.h>
volatile sig_atomic_t keep_running = 1;
void sigint_handler(int sig) {
keep_running = 0;
}
int main() {
signal(SIGINT, sigint_handler);
printf("程序运行中,按Ctrl+C优雅退出\n");
while (keep_running) {
printf("工作中...\n");
sleep(1);
}
printf("\n正在清理资源...\n");
// 执行清理工作
sleep(1);
printf("程序已退出\n");
return 0;
}
输出:
程序运行中,按Ctrl+C优雅退出
工作中...
工作中...
^C
正在清理资源...
程序已退出
信号处理的注意事项:
- 信号处理函数要简短:只设置标志,不做复杂操作
- 使用sig_atomic_t类型:保证原子性
- 避免调用不可重入函数:如printf、malloc等
- SIGKILL和SIGSTOP不能被捕获
20.4.6 共享内存(Shared Memory)
什么是共享内存?
共享内存是最快的IPC方式,允许多个进程访问同一块物理内存。
共享内存的特点:
- 速度最快(直接访问内存)
- 需要同步机制(信号量)
- 数据不会自动消失
共享内存函数:
#include <sys/shm.h>
// 创建或获取共享内存
int shmget(key_t key, size_t size, int shmflg);
// 连接共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 断开共享内存
int shmdt(const void *shmaddr);
// 控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
示例:写进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main() {
key_t key = 1234;
// 创建共享内存
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget失败");
return 1;
}
// 连接共享内存
char *shm_ptr = (char *)shmat(shmid, NULL, 0);
if (shm_ptr == (char *)-1) {
perror("shmat失败");
return 1;
}
// 写入数据
const char *msg = "Hello from shared memory!";
strcpy(shm_ptr, msg);
printf("已写入共享内存: %s\n", msg);
// 断开共享内存
shmdt(shm_ptr);
return 0;
}
示例:读进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main() {
key_t key = 1234;
// 获取共享内存
int shmid = shmget(key, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget失败");
return 1;
}
// 连接共享内存
char *shm_ptr = (char *)shmat(shmid, NULL, 0);
if (shm_ptr == (char *)-1) {
perror("shmat失败");
return 1;
}
// 读取数据
printf("从共享内存读取: %s\n", shm_ptr);
// 断开共享内存
shmdt(shm_ptr);
// 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
使用方法:
# 运行写进程
./writer
# 运行读进程
./reader
查看共享内存:
ipcs -m
删除共享内存:
ipcrm -m <shmid>
20.5 实战项目:多进程任务管理器
20.5.1 项目需求
实现一个多进程任务管理器,支持以下功能:
- 创建进程池处理任务
- 父进程分配任务给子进程
- 子进程执行任务并返回结果
- 使用管道进行进程间通信
- 优雅处理信号和进程退出
功能需求:
- 创建固定数量的工作进程
- 父进程从标准输入读取任务
- 任务分配给空闲的工作进程
- 工作进程执行任务并返回结果
- 支持Ctrl+C优雅退出
20.5.2 完整实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdbool.h>
#define MAX_WORKERS 4
#define BUFFER_SIZE 256
typedef struct {
pid_t pid;
int pipe_to_worker[2]; // 父进程 -> 子进程
int pipe_from_worker[2]; // 子进程 -> 父进程
bool busy;
} Worker;
Worker workers[MAX_WORKERS];
volatile sig_atomic_t keep_running = 1;
void sigint_handler(int sig) {
keep_running = 0;
}
// 工作进程函数
void worker_process(int read_fd, int write_fd) {
char buffer[BUFFER_SIZE];
while (1) {
// 从父进程读取任务
ssize_t n = read(read_fd, buffer, sizeof(buffer));
if (n <= 0) {
break;
}
buffer[n] = '\0';
// 模拟任务处理
printf("[Worker %d] 收到任务: %s", getpid(), buffer);
sleep(2); // 模拟耗时操作
// 返回结果
char result[BUFFER_SIZE];
snprintf(result, sizeof(result), "完成: %s", buffer);
write(write_fd, result, strlen(result));
}
close(read_fd);
close(write_fd);
exit(0);
}
// 创建工作进程
bool create_worker(int index) {
// 创建管道
if (pipe(workers[index].pipe_to_worker) == -1 ||
pipe(workers[index].pipe_from_worker) == -1) {
perror("pipe失败");
return false;
}
pid_t pid = fork();
if (pid < 0) {
perror("fork失败");
return false;
} else if (pid == 0) {
// 子进程
close(workers[index].pipe_to_worker[1]);
close(workers[index].pipe_from_worker[0]);
worker_process(workers[index].pipe_to_worker[0],
workers[index].pipe_from_worker[1]);
exit(0);
} else {
// 父进程
workers[index].pid = pid;
workers[index].busy = false;
close(workers[index].pipe_to_worker[0]);
close(workers[index].pipe_from_worker[1]);
printf("创建工作进程 %d (PID: %d)\n", index, pid);
return true;
}
}
// 查找空闲工作进程
int find_idle_worker(void) {
for (int i = 0; i < MAX_WORKERS; i++) {
if (!workers[i].busy) {
return i;
}
}
return -1;
}
// 分配任务给工作进程
bool assign_task(int worker_index, const char *task) {
workers[worker_index].busy = true;
ssize_t written = write(workers[worker_index].pipe_to_worker[1],
task, strlen(task));
if (written < 0) {
perror("写入任务失败");
workers[worker_index].busy = false;
return false;
}
printf("任务已分配给工作进程 %d\n", worker_index);
return true;
}
// 检查工作进程结果
void check_worker_results(void) {
char buffer[BUFFER_SIZE];
for (int i = 0; i < MAX_WORKERS; i++) {
if (!workers[i].busy) {
continue;
}
// 非阻塞读取
fd_set readfds;
struct timeval tv = {0, 0};
FD_ZERO(&readfds);
FD_SET(workers[i].pipe_from_worker[0], &readfds);
int ret = select(workers[i].pipe_from_worker[0] + 1,
&readfds, NULL, NULL, &tv);
if (ret > 0) {
ssize_t n = read(workers[i].pipe_from_worker[0],
buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
printf("[结果] 工作进程 %d: %s\n", i, buffer);
workers[i].busy = false;
}
}
}
}
// 清理所有工作进程
void cleanup_workers(void) {
printf("\n正在关闭所有工作进程...\n");
for (int i = 0; i < MAX_WORKERS; i++) {
if (workers[i].pid > 0) {
close(workers[i].pipe_to_worker[1]);
close(workers[i].pipe_from_worker[0]);
kill(workers[i].pid, SIGTERM);
waitpid(workers[i].pid, NULL, 0);
printf("工作进程 %d 已关闭\n", i);
}
}
}
int main() {
signal(SIGINT, sigint_handler);
printf("=== 多进程任务管理器 ===\n");
printf("创建 %d 个工作进程...\n\n", MAX_WORKERS);
// 创建工作进程池
for (int i = 0; i < MAX_WORKERS; i++) {
if (!create_worker(i)) {
cleanup_workers();
return 1;
}
}
printf("\n任务管理器已启动\n");
printf("输入任务内容(每行一个任务),Ctrl+C退出\n\n");
char task[BUFFER_SIZE];
while (keep_running) {
// 检查工作进程结果
check_worker_results();
// 非阻塞读取用户输入
fd_set readfds;
struct timeval tv = {0, 100000}; // 100ms超时
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);
if (ret > 0) {
if (fgets(task, sizeof(task), stdin) == NULL) {
break;
}
// 查找空闲工作进程
int worker_index = find_idle_worker();
if (worker_index >= 0) {
assign_task(worker_index, task);
} else {
printf("所有工作进程都在忙,请稍后...\n");
sleep(1);
}
}
}
cleanup_workers();
printf("\n任务管理器已退出\n");
return 0;
}
20.5.3 使用示例
编译:
gcc task_manager.c -o task_manager
运行:
./task_manager
输出示例:
=== 多进程任务管理器 ===
创建 4 个工作进程...
创建工作进程 0 (PID: 12346)
创建工作进程 1 (PID: 12347)
创建工作进程 2 (PID: 12348)
创建工作进程 3 (PID: 12349)
任务管理器已启动
输入任务内容(每行一个任务),Ctrl+C退出
处理数据1
任务已分配给工作进程 0
[Worker 12346] 收到任务: 处理数据1
处理数据2
任务已分配给工作进程 1
[Worker 12347] 收到任务: 处理数据2
[结果] 工作进程 0: 完成: 处理数据1
[结果] 工作进程 1: 完成: 处理数据2
^C
正在关闭所有工作进程...
工作进程 0 已关闭
工作进程 1 已关闭
工作进程 2 已关闭
工作进程 3 已关闭
任务管理器已退出
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)