Linux 系统编程:进程管理——从 fork 到迷你 Shell

代码仓库:github.com/ZhangXiaolin-persist/linux-sysprog-course

1. 为什么需要进程管理?

之前学文件 I/O,我学会了操作文件。但怎么让多个程序同时运行?怎么让它们之间传输数据?这就是进程管理要解决的问题。

2. 核心概念

2.1 进程创建 —— fork()

fork() 将程序一分为二,变成父子两个进程。一次调用,两次返回:

  • 子进程返回 0
  • 父进程返回子进程的 PID(大于 0)
  • 失败返回 -1

fork 刚完成时,父子进程内存内容完全相同,之后各自独立运行。

pid_t pid = fork();
if (pid == -1) {
    perror("fork");
    exit(1);
}
if (pid == 0) {
    printf("我是子进程\n");
} else {
    printf("我是父进程,子进程 PID = %d\n", pid);
}

2.2 程序替换 —— execlp() / execvp()

这两个函数把当前进程完全替换成另一个可执行程序。成功不返回,失败才返回。

函数 传参方式 PATH 搜索
execlp 一个个列,NULL 结尾 自动搜索
execvp 放数组里,传首位地址 自动搜索
// execlp 方式
execlp("ls", "ls", "-l", NULL);

// execvp 方式
char *args[] = {"ls", "-l", NULL};
execvp(args[0], args);

exec 成功后原程序被覆盖,perror + exit(1) 只是失败时的兜底。

2.3 子进程回收 —— wait()

父进程调用 wait(NULL) 会阻塞等待,直到任意一个子进程退出才继续。

pid_t pid = fork();
if (pid == 0) {
    execlp("ls", "ls", "-l", NULL);
    perror("execlp");
    exit(1);
}
wait(NULL);
printf("子进程已退出\n");

2.4 管道通信 —— pipe()

pipe(fd) 创建一根数据水管,fd[0] 是读端,fd[1] 是写端。

子进程(stdout) → dup2 → fd[1] ──水流──→ fd[0] → dup2 → 父进程(stdin)
int fd[2];
pipe(fd);

pid_t pid = fork();
if (pid == 0) {
    close(fd[0]);
    dup2(fd[1], 1);
    close(fd[1]);
    execlp("cat", "cat", "/etc/hostname", NULL);
    perror("cat");
    exit(1);
}
close(fd[1]);
dup2(fd[0], 0);
close(fd[0]);
execlp("wc", "wc", "-c", NULL);
perror("wc");
exit(1);

运行输出 14,和 cat /etc/hostname | wc -c 结果一致。

2.5 重定向 —— dup2()

// > 输出重定向:stdout 写进文件
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, 1);
close(fd);

// < 输入重定向:stdin 从文件读
int fd = open("input.txt", O_RDONLY);
dup2(fd, 0);
close(fd);

3. 踩到的坑

坑1:父进程忘了关管道 fd

  • 现象:管道程序永远卡住,不退出
  • 原因:fork 后父进程还抓着写端 fd[1],读端永远等不到 EOF
  • 修复:父进程不用管道时,必须 close(fd[0]); close(fd[1]);

坑2:strcmp 和 == 搞混

  • 现象:检测 < 符号永远不生效
  • 原因args[i] == "<" 比较的是指针地址,不是字符串内容
  • 修复:改成 strcmp(args[i], "<") == 0

4. 迷你 Shell

我把这些知识串起来做了一个迷你 Shell,演进路线:

版本 新增能力
v1 while + fgets + fork + execlp 骨架
v2 strtok 参数切割 + execvp + exit 内置
v3 管道 `
v4 重定向 > < 支持

核心思路:

  1. while(1) + fgets 循环读取用户命令
  2. strtok 按空格拆成命令数组
  3. 检测到 | 时,pipe + 双 fork 连接左右命令
  4. 检测到 > / < 时,open + dup2 重定向
  5. 普通命令走 fork + execvp + wait
// 简化版:用 pipe 实现 ls -l | grep .c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    int fd[2];
    pipe(fd);

    pid_t pid = fork();
    if (pid == 0) {
        dup2(fd[1], 1);
        close(fd[0]);
        close(fd[1]);
        execlp("ls", "ls", "-l", NULL);
        perror("execlp ls");
        exit(1);
    }

    dup2(fd[0], 0);
    close(fd[1]);
    close(fd[0]);
    execlp("grep", "grep", ".c", NULL);
    perror("execlp grep");
    exit(1);
}
$ gcc -Wall -Wextra -g pipe_shell_demo.c -o pipe_shell_demo
$ ./pipe_shell_demo

总结

从 fork/exec/wait 出发,到 pipe/dup2 管道重定向,最后拼出一个带管道功能的迷你 Shell。学完这套,对"Shell 到底怎么执行命令"有了实实在在的理解。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐