进程与管道
进程创建与进程间通信
概述
进程是操作系统资源分配的基本单位,每个进程拥有独立的地址空间。在Unix/Linux系统中,通过 fork 创建子进程,通过 exec 族函数执行新程序,通过 wait 等待子进程结束。管道(pipe)是最简单的进程间通信机制,允许具有亲缘关系的进程之间传递数据。
基础概念
进程的生命周期
- 创建:通过
fork从父进程创建子进程 - 执行:通过
exec族函数加载新程序 - 等待:父进程通过
wait等待子进程结束 - 终止:进程通过
exit或从main返回终止
管道的分类
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 匿名管道 | pipe() 创建,单向 | 父子进程间通信 |
| 命名管道 | mkfifo() 创建,文件系统可见 | 无亲缘关系进程间通信 |
进程相关头文件
| 头文件 | 说明 |
|---|---|
<unistd.h> | fork, exec, pipe, getpid 等 |
<sys/wait.h> | wait, waitpid |
<sys/types.h> | pid_t 等类型定义 |
<fcntl.h> | 文件控制选项 |
快速上手
创建子进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) {
// 创建失败
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);
wait(NULL); // 等待子进程结束
printf("子进程已结束\n");
}
return 0;
}
使用管道通信
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(void) {
int pipefd[2]; // pipefd[0] 读端, pipefd[1] 写端
// 创建管道
if (pipe(pipefd) == -1) {
perror("pipe 失败");
return 1;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程:写入数据
close(pipefd[0]); // 关闭读端
const char *msg = "来自子进程的消息";
write(pipefd[1], msg, strlen(msg) + 1);
close(pipefd[1]); // 关闭写端
} else {
// 父进程:读取数据
close(pipefd[1]); // 关闭写端
char buf[256];
ssize_t n = read(pipefd[0], buf, sizeof(buf));
printf("父进程收到: %s (%zd 字节)\n", buf, n);
close(pipefd[0]); // 关闭读端
wait(NULL);
}
return 0;
}
详细用法
exec 族函数
#include <stdio.h>
#include <unistd.h>
// exec 族函数对比
// l: 参数以列表形式传递(以 NULL 结尾)
// v: 参数以数组形式传递
// p: 在 PATH 中搜索程序
// e: 自定义环境变量
int main(void) {
pid_t pid = fork();
if (pid == 0) {
// 方式一:execl - 列表参数
execl("/bin/ls", "ls", "-l", "/tmp", NULL);
// 方式二:execlp - 搜索 PATH
// execlp("ls", "ls", "-l", NULL);
// 方式三:execv - 数组参数
// char *args[] = {"ls", "-l", "/tmp", NULL};
// execv("/bin/ls", args);
// 如果 exec 成功,以下代码不会执行
perror("exec 失败");
return 1;
}
wait(NULL);
printf("子进程执行完毕\n");
return 0;
}
wait 和 waitpid
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
printf("子进程运行中...\n");
sleep(2);
return 42; // 子进程退出码
}
// 等待子进程结束
int status;
pid_t ret = waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程被信号终止,信号: %d\n", WTERMSIG(status));
}
return 0;
}
命名管道(FIFO)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#define FIFO_PATH "/tmp/my_fifo"
// 写入端
int writer(void) {
mkfifo(FIFO_PATH, 0666); // 创建命名管道
int fd = open(FIFO_PATH, O_WRONLY);
if (fd == -1) { perror("open 失败"); return 1; }
const char *msg = "通过命名管道发送的消息";
write(fd, msg, strlen(msg) + 1);
close(fd);
return 0;
}
// 读取端
int reader(void) {
int fd = open(FIFO_PATH, O_RDONLY);
if (fd == -1) { perror("open 失败"); return 1; }
char buf[256];
read(fd, buf, sizeof(buf));
printf("收到: %s\n", buf);
close(fd);
unlink(FIFO_PATH); // 删除命名管道
return 0;
}
常见场景
场景一:实现简单的 shell
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_ARGS 64
int main(void) {
char line[256];
while (1) {
printf("mysh> ");
if (!fgets(line, sizeof(line), stdin)) break;
// 去除换行符
line[strcspn(line, "\n")] = '\0';
if (strcmp(line, "exit") == 0) break;
if (line[0] == '\0') continue;
// 解析命令和参数
char *args[MAX_ARGS];
int argc = 0;
char *token = strtok(line, " ");
while (token && argc < MAX_ARGS - 1) {
args[argc++] = token;
token = strtok(NULL, " ");
}
args[argc] = NULL;
// 创建子进程执行命令
pid_t pid = fork();
if (pid == 0) {
execvp(args[0], args);
perror("命令执行失败");
exit(1);
} else {
wait(NULL);
}
}
return 0;
}
场景二:管道连接两个命令
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
// 实现 ls | wc -l 的效果
int main(void) {
int pipefd[2];
pipe(pipefd);
pid_t pid1 = fork();
if (pid1 == 0) {
// 子进程1:执行 ls,输出到管道
close(pipefd[0]); // 关闭读端
dup2(pipefd[1], STDOUT_FILENO); // 标准输出重定向到管道写端
close(pipefd[1]);
execlp("ls", "ls", NULL);
perror("exec ls 失败");
return 1;
}
pid_t pid2 = fork();
if (pid2 == 0) {
// 子进程2:执行 wc -l,从管道读取
close(pipefd[1]); // 关闭写端
dup2(pipefd[0], STDIN_FILENO); // 标准输入重定向到管道读端
close(pipefd[0]);
execlp("wc", "wc", "-l", NULL);
perror("exec wc 失败");
return 1;
}
// 父进程:关闭管道两端,等待子进程
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
return 0;
}
场景三:进程池
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#define WORKER_COUNT 4
// 工作进程函数
void worker(int id) {
printf("工作进程 %d (PID=%d) 启动\n", id, getpid());
sleep(1 + id % 3); // 模拟不同耗时的工作
printf("工作进程 %d 完成\n", id);
}
int main(void) {
pid_t workers[WORKER_COUNT];
// 创建工作进程
for (int i = 0; i < WORKER_COUNT; i++) {
workers[i] = fork();
if (workers[i] == 0) {
worker(i);
exit(0);
}
}
// 等待所有工作进程完成
for (int i = 0; i < WORKER_COUNT; i++) {
int status;
pid_t pid = wait(&status);
printf("进程 %d 已结束\n", pid);
}
printf("所有工作进程已完成\n");
return 0;
}
注意事项
僵尸进程
子进程结束后,如果父进程没有调用 wait,子进程会变成僵尸进程(Z状态),占用系统资源:
// 避免僵尸进程的方式一:父进程调用 wait
wait(NULL);
// 方式二:忽略 SIGCHLD 信号
signal(SIGCHLD, SIG_IGN);
// 方式三:双重 fork
pid_t pid = fork();
if (pid == 0) {
if (fork() == 0) {
// 孙子进程执行工作
worker();
exit(0);
}
exit(0); // 子进程立即退出
}
wait(NULL); // 等待子进程(不等待孙子进程)
管道是单向的
管道只能单向传输数据。如果需要双向通信,需要创建两个管道:
int pipe1[2]; // 父 -> 子
int pipe2[2]; // 子 -> 父
pipe(pipe1);
pipe(pipe2);
管道的阻塞特性
当管道读端关闭后,写端继续写入会收到 SIGPIPE 信号。当管道写端关闭后,read 返回0表示文件结束。
进阶用法
使用 popen 执行命令
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// 执行命令并读取输出
FILE *fp = popen("ls -l /tmp", "r");
if (!fp) {
perror("popen 失败");
return 1;
}
char line[256];
while (fgets(line, sizeof(line), fp)) {
printf(">> %s", line);
}
int status = pclose(fp);
printf("命令退出状态: %d\n", WEXITSTATUS(status));
return 0;
}
守护进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
// 创建守护进程
void daemonize(void) {
// 步骤一:创建子进程,父进程退出
pid_t pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0);
// 步骤二:创建新会话
if (setsid() < 0) exit(1);
// 步骤三:再次 fork,防止获取终端
pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0);
// 步骤四:设置文件权限掩码
umask(0);
// 步骤五:更改工作目录
chdir("/");
// 步骤六:关闭标准文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 将标准输入输出重定向到 /dev/null
open("/dev/null", O_RDWR);
dup(0);
dup(0);
}
int main(void) {
daemonize();
// 守护进程主循环
while (1) {
// 执行守护任务
sleep(60);
}
return 0;
}
多进程并发服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#define PORT 8080
void handle_client(int client_fd) {
char buf[1024];
ssize_t n = read(client_fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("收到请求: %s\n", buf);
const char *response = "HTTP/1.1 200 OK\r\n\r\nHello, World!";
write(client_fd, response, strlen(response));
}
close(client_fd);
}
int main(void) {
signal(SIGCHLD, SIG_IGN); // 避免僵尸进程
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = INADDR_ANY
};
bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, 10);
printf("服务器监听端口 %d\n", PORT);
while (1) {
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) continue;
pid_t pid = fork();
if (pid == 0) {
close(server_fd); // 子进程不需要监听套接字
handle_client(client_fd);
exit(0);
}
close(client_fd); // 父进程不需要客户端套接字
}
return 0;
}