第20章 进程与进程间通信

章节摘要

进程是操作系统进行资源分配和调度的基本单位。理解进程的概念、生命周期和进程间通信机制,是进行系统编程的基础。本章将深入讲解进程的创建、管理和进程间通信的各种方式。

20.1 进程基础

20.1.1 什么是进程

定义: 进程(Process)是程序的一次执行实例,是操作系统进行资源分配和调度的基本单位。

进程 vs 程序:

特性 程序 进程
本质 静态的代码和数据 动态的执行实例
生命周期 永久存在 创建、运行、终止
资源 不占用系统资源 占用CPU、内存等资源
数量 一个程序文件 可以有多个进程实例

进程的组成:

  1. 代码段(Text Segment):存放程序的机器指令
  2. 数据段(Data Segment):存放全局变量和静态变量
  3. 堆(Heap):动态分配的内存
  4. 栈(Stack):存放局部变量和函数调用信息
  5. 进程控制块(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()的工作原理:

  1. 创建一个新进程(子进程)
  2. 子进程是父进程的副本
  3. 子进程拥有父进程的代码、数据、堆、栈的副本
  4. 子进程和父进程并发执行

示例:第一个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 结束

重要特性:

  1. fork后父子进程的执行顺序是不确定的
  2. 子进程复制父进程的内存空间(写时复制)
  3. 子进程继承父进程的文件描述符
  4. 子进程有独立的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 进程的终止

正常终止的方式:

  1. 从main返回
  2. 调用exit()
  3. 调用_exit()或_Exit()

异常终止的方式:

  1. 调用abort()
  2. 接收到信号
  3. 最后一个线程响应取消请求

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:非阻塞,如果没有子进程终止立即返回0
    • WUNTRACED:报告停止的子进程

示例:使用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!

重要规则:

  1. 关闭不使用的端:读进程关闭写端,写进程关闭读端
  2. 写端全部关闭时:读端read返回0(EOF)
  3. 读端全部关闭时:写端write产生SIGPIPE信号
  4. 管道为空时:read阻塞
  5. 管道满时: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
正在清理资源...
程序已退出

信号处理的注意事项:

  1. 信号处理函数要简短:只设置标志,不做复杂操作
  2. 使用sig_atomic_t类型:保证原子性
  3. 避免调用不可重入函数:如printf、malloc等
  4. 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 项目需求

实现一个多进程任务管理器,支持以下功能:

  1. 创建进程池处理任务
  2. 父进程分配任务给子进程
  3. 子进程执行任务并返回结果
  4. 使用管道进行进程间通信
  5. 优雅处理信号和进程退出

功能需求:

  • 创建固定数量的工作进程
  • 父进程从标准输入读取任务
  • 任务分配给空闲的工作进程
  • 工作进程执行任务并返回结果
  • 支持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 已关闭

任务管理器已退出
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐