Linux 进程间通信(IPC):管道与信号量完全指南

引言

在 Linux 系统编程中,进程间通信(IPC,Inter-Process Communication) 是一个核心课题。进程是独立运行的单位,默认情况下彼此隔离。但很多时候,我们需要让进程之间交换数据或同步执行顺序------这就是进程间通信要解决的问题。

Linux 提供了多种 IPC 机制:

  • 管道(Pipe):最简单的 IPC 机制,分为有名管道和无名管道

  • 信号量(Semaphore):用于进程同步,协调资源访问顺序

  • 共享内存(Shared Memory):最高效的数据交换方式

  • 消息队列(Message Queue):结构化数据传递

  • 套接字(Socket):网络通信

今天,我将重点讲解管道信号量这两种 IPC 机制,涵盖它们的工作原理、使用方法和典型应用场景。


第一部分:管道(Pipe)

一、管道的基本概念

管道是 Linux 中最早的进程间通信方式,它的本质是内核内存中的一个环形缓冲区 ,不占用磁盘空间。

管道的特点:

  • 数据存储在内存中(读写速度快)

  • 半双工通信(同一时刻只能单向传输)

  • 自带同步机制(读写自动阻塞/唤醒)

  • 无持久性(进程结束,管道消失)

二、管道的分类

类型 通信范围 创建方式 文件标识
无名管道 仅限父子进程间通信 pipe() 系统调用 无文件路径
有名管道 任意两个进程间通信 mkfifo 命令或 mkfifo() 函数 管道文件(类型为 p)

三、管道的工作原理

管道在内核中通过环形缓冲区实现,包含两个关键指针:

指针 功能 移动规则
头指针 指向下一个待写入的位置 写入数据后向后移动
尾指针 指向下一个待读取的位置 读取数据后向后移动

读写规则:

  • 写入数据:头指针后移,若缓冲区满则阻塞

  • 读取数据:尾指针后移,若缓冲区空则阻塞

  • 循环覆盖:指针到达缓冲区末尾后重置到起始位置

四、有名管道(FIFO)

1. 创建有名管道

命令行创建

mkfifo myfifo

查看文件类型(p 表示管道文件)

ls -l myfifo

prw-r--r-- 1 user user 0 Apr 27 10:00 myfifo

cpp 复制代码
// 程序中创建
#include <sys/types.h>
#include <sys/stat.h>

int main() {
    mkfifo("myfifo", 0666);
    return 0;
}
2. 有名管道通信示例

写端程序(write.c):

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main() {
    int fd = open("myfifo", O_WRONLY);
    if (fd == -1) {
        perror("open error");
        exit(1);
    }
    
    char buffer[128];
    while (1) {
        printf("请输入消息: ");
        fgets(buffer, sizeof(buffer), stdin);
        buffer[strlen(buffer) - 1] = '\0';
        
        write(fd, buffer, strlen(buffer) + 1);
        
        if (strcmp(buffer, "exit") == 0) break;
    }
    
    close(fd);
    return 0;
}

读端程序(read.c):

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("myfifo", O_RDONLY);
    if (fd == -1) {
        perror("open error");
        exit(1);
    }
    
    char buffer[128];
    while (1) {
        int n = read(fd, buffer, sizeof(buffer));
        if (n == 0) break;  // 写端关闭
        
        printf("收到消息: %s\n", buffer);
        
        if (strcmp(buffer, "exit") == 0) break;
    }
    
    close(fd);
    return 0;
}

五、管道的阻塞特性

场景 阻塞情况 说明
open 调用 阻塞直到另一端以对应模式打开 只读打开时等待写端,只写打开时等待读端
read 调用 管道为空且写端未关闭时阻塞 写端关闭后 read 立即返回 0
write 调用 管道满时阻塞 读端读取后解除阻塞
cpp 复制代码
// 演示管道的阻塞特性
// 单独运行写端会阻塞在 open
int main() {
    int fd = open("myfifo", O_WRONLY);  // 阻塞!等待读端
    // 只有读端也打开后,才继续执行
    write(fd, "hello", 5);
    close(fd);
    return 0;
}

六、管道关闭的特殊情况

情况 结果 说明
读端关闭,写端写入 进程收到 SIGPIPE 信号(13),默认终止进程 内核检测到无效操作
写端关闭,读端读取 read 立即返回 0(EOF) 类似文件末尾
两端都关闭 管道被内核销毁 所有文件描述符关闭后释放
cpp 复制代码
// 忽略 SIGPIPE 信号
#include <signal.h>

int main() {
    signal(SIGPIPE, SIG_IGN);  // 忽略 SIGPIPE
    
    int fd = open("myfifo", O_WRONLY);
    // 即使读端关闭,write 也不会导致进程终止
    write(fd, "hello", 5);
    return 0;
}

七、无名管道(pipe)

1. pipe 函数
cpp 复制代码
#include <unistd.h>

int pipe(int fd[2]);
// 成功返回 0,失败返回 -1
// fd[0]:读端文件描述符
// fd[1]:写端文件描述符
2. 单进程使用无名管道
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd[2];
    pipe(fd);
    
    write(fd[1], "hello", 5);
    sleep(3);
    
    char buffer[128];
    read(fd[0], buffer, 127);
    printf("buffer=%s\n", buffer);  // 输出:buffer=hello
    
    close(fd[0]);
    close(fd[1]);
    return 0;
}
3. 父子进程通过无名管道通信
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];
    pipe(fd);
    
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程:读取数据
        close(fd[1]);  // 关闭写端
        
        char buffer[128];
        read(fd[0], buffer, 127);
        printf("子进程收到: %s\n", buffer);
        
        close(fd[0]);
        exit(0);
    } else {
        // 父进程:写入数据
        close(fd[0]);  // 关闭读端
        
        char buffer[128];
        printf("请输入消息: ");
        fgets(buffer, sizeof(buffer), stdin);
        buffer[strlen(buffer) - 1] = '\0';
        
        write(fd[1], buffer, strlen(buffer) + 1);
        close(fd[1]);
        
        wait(NULL);
    }
    
    return 0;
}

无名管道的核心特点:

  • 仅限父子进程间通信(通过 fork 继承文件描述符)

  • 半双工通信,需关闭不需要的描述符

  • 自带同步机制,读写自动阻塞/唤醒

八、管道总结

特性 无名管道 有名管道
通信范围 父子进程间 任意进程间
创建方式 pipe() mkfifo
文件路径
持久性 进程结束消失 文件存在,数据随进程
数据存储 内存 内存

第二部分:信号量(Semaphore)

一、什么是信号量?

信号量是操作系统提供的进程同步机制,用于协调多个进程对共享资源的访问顺序,避免竞态条件。

典型应用场景:

  • 打印机等共享设备的互斥访问

  • 生产者-消费者问题

  • 读者-写者问题

  • 多个进程协调执行顺序

二、核心概念

概念 定义 示例
临界资源 同一时刻仅允许单个进程访问的资源 打印机、试衣间
临界区 访问临界资源的代码段 打印数据的代码
P操作 申请资源(原子减一) 进入试衣间前
V操作 释放资源(原子加一) 离开试衣间后
互斥 防止多个进程同时访问临界资源 信号量初值设为 1
同步 协调进程执行顺序 生产后才能消费

三、信号量的工作原理

四、信号量接口

使用信号量需要包含头文件 <sys/sem.h>

1. semget------创建/获取信号量
cpp 复制代码
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
// 参数:
//   key: 键值,多个进程通过相同的 key 获取同一个信号量
//   nsems: 信号量个数
//   semflg: 标志位(IPC_CREAT 创建,IPC_EXCL 排他)
// 返回值:成功返回信号量 ID,失败返回 -1
2. semctl------控制信号量(初始化/删除)
cpp 复制代码
// 自定义联合体(某些系统需要定义)
union semun {
    int val;              // 用于 SETVAL
    struct semid_ds *buf;
    unsigned short *array;
};

int semctl(int semid, int semnum, int cmd, ...);
// 参数:
//   semid: 信号量 ID
//   semnum: 信号量编号(多个信号量时使用)
//   cmd: 命令(SETVAL 初始化,IPC_RMID 删除)
3. semop------执行 P/V 操作
cpp 复制代码
struct sembuf {
    unsigned short sem_num;   // 信号量编号
    short sem_op;             // -1(P操作)或 +1(V操作)
    short sem_flg;            // 标志位(通常为 0)
};

int semop(int semid, struct sembuf *sops, size_t nsops);

五、信号量封装示例

sem.h(头文件):

cpp 复制代码
#ifndef SEM_H
#define SEM_H

int sem_init(int key);
void sem_p(int semid);
void sem_v(int semid);
void sem_destroy(int semid);

#endif

sem.c(实现文件):

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

int sem_init(int key) {
    int semid;
    
    // 尝试创建信号量
    semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0600);
    
    if (semid == -1) {
        // 已存在,直接获取
        semid = semget(key, 1, 0600);
    } else {
        // 创建成功,初始化值为 1
        union semun a;
        a.val = 1;
        semctl(semid, 0, SETVAL, a);
    }
    
    return semid;
}

void sem_p(int semid) {
    struct sembuf buf = {0, -1, 0};
    semop(semid, &buf, 1);
}

void sem_v(int semid) {
    struct sembuf buf = {0, 1, 0};
    semop(semid, &buf, 1);
}

void sem_destroy(int semid) {
    semctl(semid, 0, IPC_RMID);
}

六、使用信号量实现进程互斥

进程A(a.c):

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include "sem.h"

#define KEY 0x1234

int main() {
    int semid = sem_init(KEY);
    
    for (int i = 0; i < 5; i++) {
        sem_p(semid);          // 进入临界区
        printf("A");           // 临界区
        fflush(stdout);
        sleep(1);
        printf("A\n");         // 临界区
        fflush(stdout);
        sem_v(semid);          // 退出临界区
        sleep(1);
    }
    
    // 注意:应由最后一个进程销毁信号量
    // 此处简化,实际需要协作
    return 0;
}

进程B(b.c):

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include "sem.h"

#define KEY 0x1234

int main() {
    int semid = sem_init(KEY);
    
    for (int i = 0; i < 5; i++) {
        sem_p(semid);          // 进入临界区
        printf("B");           // 临界区
        fflush(stdout);
        sleep(1);
        printf("B\n");         // 临界区
        fflush(stdout);
        sem_v(semid);          // 退出临界区
        sleep(1);
    }
    
    return 0;
}

编译与运行:

# 编译
gcc -c sem.c
gcc a.c sem.o -o a
gcc b.c sem.o -o b

# 运行(在两个终端分别执行)
./a
./b

**# 输出结果(严格交替):

A

A

B

B

A

A

...**

七、信号量的注意事项

要点 说明
初始化时机 由第一个进程创建并初始化,其他进程只获取
销毁时机 由最后一个进程销毁(需进程间协调)
SEM_UNDO 标志 进程异常终止时自动恢复信号量状态,避免死锁
原子性 P/V 操作是原子的,不会被中断

总结

一、管道核心要点

特性 说明
存储位置 内存(环形缓冲区)
通信模式 半双工
同步机制 读写自动阻塞/唤醒
无名管道 仅父子进程
有名管道 任意进程

二、信号量核心要点

概念 说明
P操作 申请资源(值-1,为0时阻塞)
V操作 释放资源(值+1,唤醒等待进程)
互斥信号量 初值为 1,保证互斥访问
同步信号量 初值为 0,协调执行顺序
临界区 被 P/V 包围的代码段

三、对比总结

IPC 机制 数据存储 通信模式 同步机制 适用场景
管道 内存 单向 自动阻塞 简单数据流
信号量 计数器 信号 P/V 操作 资源同步
共享内存 内存 双向 需配合信号量 大数据量
消息队列 内核 双向 自动排队 结构化消息

管道和信号量是 Linux 进程间通信的基础机制。管道解决了数据传输问题,信号量解决了同步问题。理解它们的原理和使用方法,是掌握多进程编程的关键。

学习建议:

  1. 理解管道的阻塞特性,注意读写端的正确管理

  2. 区分无名管道和有名管道的使用场景

  3. 掌握信号量的 P/V 操作逻辑

  4. 区分互斥(保护临界资源)和同步(协调执行顺序)

相关推荐
mjhcsp2 小时前
雨云服务器使用方法(入门1)
服务器
W.W.H.2 小时前
远程连接协议(SSH\Telnet\FTP\Serial等)
运维·arm开发·经验分享·ssh
张青贤2 小时前
linux离线部署docker和docker-compose
linux·docker·docker-compose
oioihoii2 小时前
OpenClaw桌面 UI 自动化中的 Token 消耗问题几种可能的优化方向
运维·ui·自动化
b***25112 小时前
18650与21700电芯在锂电池自动化生产线中的协同发展
运维·自动化
Johnstons2 小时前
网络抓包留存平台怎么选:全量留存、按需抓包与传统镜像方案的边界、场景与判断标准
运维·服务器·网络·网络运维
晨晖22 小时前
linux命令7(systemctl服务进行管理)
linux·运维·服务器
Nice__J2 小时前
ISO26262功能安全——SafeOS
java·linux·安全
不仙5203 小时前
Hermes 接入飞书(Feishu/Lark)部署文档
linux·服务器·ai