Linux进程间通信(IPC)详解:信号、管道、共享内存与信号量

1. 信号(Signal)

信号是Linux系统中用于通知进程某个事件已发生的异步通信机制。信号可以由内核、其他进程或进程自身发送。

1.1 常见信号

  • SIGINT:终端中断信号,通常由Ctrl+C产生,默认行为是终止进程。
  • SIGKILL:强制终止信号,不能被捕获、忽略或阻塞。
  • SIGTERM:终止信号,请求进程正常退出。
  • SIGSTOP:停止信号,暂停进程执行。
  • SIGCONT:继续信号,恢复被暂停的进程。

1.2 信号处理函数

进程可以通过以下方式处理信号:

  • SIG_IGN:忽略该信号。
  • SIG_DFL:恢复信号的默认行为。
  • 自定义处理函数:使用signal()sigaction()注册自定义信号处理函数。
#include <signal.h> #include <stdio.h> void signal_handler(int sig) { printf("Received signal: %d\n", sig); } int main() { // 注册SIGINT信号处理函数 signal(SIGINT, signal_handler); while(1) { // 主循环 } return 0; }

2. 管道(Pipe)

管道是Linux进程间通信的一种基本方式,数据在管道中单向流动。

2.1 管道特性

  • 管道是单向的,数据只能从左到右流动
  • 管道会创建子进程来执行命令
  • 管道中的命令是并行执行的
  • 管道可以串联形成管道链

2.2 有名管道(Named Pipe/FIFO)

有名管道通过mkfifo命令或mkfifo()系统调用创建。

特性:
  • 有可见的文件名(存在于文件系统中)
  • 可以被任意进程(需要有权限)访问
  • 数据传输遵循先进先出原则
  • 本质上是内存中的特殊文件,不占用磁盘空间
  • 关闭所有引用它的进程后,管道文件依然存在(需要手动删除)
测试示例:
# 创建有名管道 mkfifo mypipe 读端:持续监听管道 cat < mypipe 或 while true; do cat < mypipe; done 写端:多次写入内容 echo "Hello" > mypipe echo "World" > mypipe
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { // 创建有名管道 mkfifo("mypipe", 0666); // 写端进程 int fd = open("mypipe", O_WRONLY); char buffer[] = "Hello from named pipe"; write(fd, buffer, strlen(buffer)); close(fd); return 0; }

2.3 无名管道(Anonymous Pipe)

无名管道通过pipe()系统调用创建。

特性:
  • 无名称,仅通过文件描述符访问
  • 仅支持亲缘进程:父子、兄弟进程
  • 创建方式:pipe()系统调用
  • 生命周期:随进程结束而销毁
创建示例:
#include <stdio.h> #include <unistd.h> #include <string.h> int main() { int pipefd[2]; char buffer[100]; // 创建管道 if (pipe(pipefd) == -1) { perror("pipe"); return 1; } pid_t pid = fork(); if (pid == 0) { // 子进程:写端 close(pipefd[0]); // 关闭读端 char *msg = "Hello from child"; write(pipefd[1], msg, strlen(msg)); close(pipefd[1]); } else { // 父进程:读端 close(pipefd[1]); // 关闭写端 read(pipefd[0], buffer, sizeof(buffer)); printf("Parent received: %s\n", buffer); close(pipefd[0]); } return 0; }

2.4 有名管道 vs 无名管道

特性有名管道无名管道
名称有名(存在文件系统中)不在文件系统中
进程关系任意进程(有权限)父子、兄弟进程
生命周期随文件系统的销毁而销毁随进程的销毁而销毁
创建方式mkfifopipe()

2.5 管道注意事项

  1. 避免管道破裂:读端已关闭的情况下,写端还在输出数据,会触发SIGPIPE信号,终止写端进程
  2. 数据无边界:管道是字节流数据,若按照信息的格式一条一条发送,需要自定义信息格式(长度+数据),避免多读少读
  3. 缓冲区阻塞问题:读空、写满会阻塞进程,需要使用fcntl()设置非阻塞属性进行处理
  4. 资源泄露问题:进程退出前要关闭管道描述符,否则可能导致缓冲区中的数据丢失

2.6 通信方式

  • 单工:单向性,确定通信方式后固定,不能相互传输数据
  • 全双工:双向性,同一时刻,双方可以相互传输数据
  • 半双工:双向性,同一时刻,只能一方给另一方传输数据

管道的通信方式为半双工

3. 共享内存(Shared Memory)

共享内存允许多个进程访问同一块内存区域,是最高效的IPC方式。

3.1 共享内存函数

#include <sys/ipc.h> #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);

3.2 shmget函数详解

int shmget(key_t key, size_t size, int shmflg);

  • 返回值:-1表示共享内存创建失败;正数表示该段的标识符
  • key:共享内存段的唯一标识
  • size:创建的共享内存空间的大小
    • 创建新共享空间时,该参数填正整数
    • 获取已有共享空间时,该参数可以填0
  • shmflg:标志位,用来控制函数行为
    • 创建标志:
      • IPC_CREAT:如果key不存在,则创建
      • IPC_EXCL:与IPC_CREAT配合使用,如果key已经创建则报错并设置错误原因给errno
    • 权限控制:需要指定权限位(0666、0777、0600)

3.3 shmat函数详解

void *shmat(int shmid, const void *shmaddr, int shmflg);

作用:将一个已存在的共享内存段映射到进程的地址空间中

  • shmid:共享内存段的标识符,通常是shmget函数的返回值
  • shmaddr:指定共享内存段要附加到进程地址空间的哪个地址
    • NULL:让系统内核自动选择一块合适的、未使用的地址来附加共享内存段
    • 非NULL(填具体地址):系统会将共享内存段附加到指定位置,如果指定位置不能使用,则报错,附加失败
  • shmflg:控制附加操作控制位
    • 0:默认权限,根据shmget创建时的权限进行读写
    • SHM_RDONLY:以只读的方式附加共享内存段,如果该进程尝试写入,则引发段错误
    • SHM_EXEC:允许在共享内存段上执行代码

3.4 共享内存示例

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> #define SHM_SIZE 1024 int main() { key_t key = ftok(".", 'R'); int shmid; char *data; // 创建共享内存 if ((shmid = shmget(key, SHM_SIZE, 0644 | IPC_CREAT)) == -1) { perror("shmget"); exit(1); } // 附加到进程地址空间 data = shmat(shmid, NULL, 0); if (data == (char *)(-1)) { perror("shmat"); exit(1); } pid_t pid = fork(); if (pid == 0) { // 子进程:写入数据 printf("Child writing to shared memory...\n"); strcpy(data, "Hello from child process"); printf("Child wrote: %s\n", data); } else { // 父进程:读取数据 sleep(1); // 等待子进程写入 printf("Parent reading from shared memory...\n"); printf("Parent read: %s\n", data); // 分离共享内存 shmdt(data); // 删除共享内存 shmctl(shmid, IPC_RMID, NULL); } return 0; }

练习:设置一个进程为读取共享内存上的数值,持续读取;另外一个进程可以持续写入,直到输入"end"后两个进程都结束。共享内存的工作方式为全双工。

4. 信号量(Semaphore)

信号量是操作系统用于进程间同步与互斥的关键机制,解决多个进程对共享资源的竞争问题,避免数据冲突或不一致。

4.1 信号量基本概念

  • 本质:一个受保护的整数变量,其值反映了共享资源的可用数量
  • 原子操作:对信号量的修改(加减)是不可被中断的操作,避免多进程并发修改导致数据不一致
  • 阻塞机制:当信号量的值为0时,试图获取该信号量的进程会被阻塞,直到有其他进程释放资源

4.2 信号量类型

  1. 二值信号量:值只能是0和1,用于实现互斥(类似锁,一次只允许一个进程运行)
  2. 计数信号量:值可以是任意非负数,用于控制对多个相同资源的并发访问。假设信号量值为3,最大可支持3个进程同时运行

4.3 P、V原语

信号量的行为由两个基本操作定义(称为"原语",确保原子性):

  • P操作(获取资源):将信号量减一
    • 若结果≥0,进程可继续执行
    • 若结果<0,进程被阻塞,放入信号量的等待队列
  • V操作(释放资源):将信号量加一
    • 不会阻塞,永远释放资源
    • 若结果≤0,从等待队列中唤醒一个进程,使其继续执行

4.4 相关术语

  • 临界资源:同一时刻,只允许一个进程访问的资源
  • 临界区:访问临界资源的代码段

4.5 信号量函数

#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> // 创建或获取信号量集 int semget(key_t key, int nsems, int semflg); // 控制信号量集 int semctl(int semid, int semnum, int cmd, ...); // 执行信号量操作 int semop(int semid, struct sembuf *sops, size_t nsops);
semget函数详解

int semget(key_t key, int nsems, int semflg);

  • key:用于信号量集的唯一性,确保不同进程能通过相同的key访问同一信号量集
  • nsems:信号量的数量(要创建的信号量集中包含的信号量的个数)
    • 若是创建新信号量集,则nsems必须大于0
    • 若是获取已存在的信号量集,则nsems可设置为0
  • semflg(标志位):控制信号量的创建/获取行为
    • IPC_CREAT:若对应的信号量集不存在,则创建新集;若已存在,则返回其ID
    • IPC_EXCL:与IPC_CREAT组合使用,若信号量集已存在,则创建失败(确保获得新资源)
    • 权限:例如0600
semctl函数详解

int semctl(int semid, int semnum, int cmd, ...);

  • semid:信号量集的标识符(由semget()返回),指定要操作的信号量集
  • semnum:信号量在集中的索引(从0开始),指定要操作的单个信号量
    • 若对整个信号量集进行操作(如IPC_RMID),则此参数忽略
  • cmd:要执行的控制命令,决定semctl的行为
    • SETVAL:设置semnum指定的信号量的初始值(需配合第四个参数传递)
    • GETVAL:获取semnum指定的信号量的当前值(返回值为信号量的值)
    • IPC_RMID:删除整个信号量集(所有进程都无法访问)
    • IPC_STAT:获取信号量集的状态信息,存入semid_ds结构体(创建时间,权限)
    • IPC_SET:设置信号量集的属性,需要通过semid_

5. 消息队列(Message Queue)

消息队列是Linux系统中另一种进程间通信(IPC)机制,它允许进程通过发送和接收消息来交换数据。消息队列由内核维护,具有以下特点:

5.1 消息队列基本概念

  • 内核维护:消息队列由操作系统内核创建和管理
  • 消息结构:每条消息通常包含类型字段和实际数据
  • 先进先出:消息按照发送顺序排队,但可以根据类型选择性接收
  • 异步通信:发送者和接收者不需要同时存在

5.2 消息队列数据结构

消息队列中的消息通常采用以下结构:

struct mess { long type; // 消息类型,用于标识消息类别(如1,2,3...) char data[1024]; // 消息数据,可根据需要自定义 };

5.3 消息队列相关函数

#include <sys/msg.h> // 创建或获取消息队列 int msgget(key_t key, int msgflg); // 发送消息到消息队列 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); // 从消息队列接收消息 ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); // 控制消息队列 int msgctl(int msqid, int cmd, struct msqid_ds *buf);

5.4 消息队列示例

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ipc.h> #include <sys/msg.h> struct message { long msg_type; char msg_text[100]; }; int main() { key_t key; int msgid; struct message msg; // 生成key key = ftok("progfile", 65); // 创建消息队列 msgid = msgget(key, 0666 | IPC_CREAT); // 发送消息 msg.msg_type = 1; strcpy(msg.msg_text, "Hello from message queue"); msgsnd(msgid, &msg, sizeof(msg), 0); printf("Message sent: %s\n", msg.msg_text); // 接收消息 msgrcv(msgid, &msg, sizeof(msg), 1, 0); printf("Message received: %s\n", msg.msg_text); // 删除消息队列 msgctl(msgid, IPC_RMID, NULL); return 0; }

6. 线程示例:pthread基本使用

以下是一个使用pthread库创建线程的完整示例,演示了线程的创建、执行和回收:

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> void* fun(void* arg) { for(int i = 0; i < 5; i++) { printf("fun run\n"); sleep(1); } pthread_exit("fun over\n"); } int main() { pthread_t id; pthread_create(&id, NULL, fun, NULL); for(int i = 0; i < 2; i++) { printf("main run\n"); sleep(1); } char* s = NULL; pthread_join(id, (void**)&s); // 阻塞等待线程结束 printf("s=%s\n", s); exit(0); }

6.1 代码解析

  • pthread_create:创建新线程,参数包括线程ID、属性、线程函数和参数
  • pthread_exit:线程退出,可以返回一个值给主线程
  • pthread_join:主线程等待子线程结束,并获取线程的返回值
  • 编译命令gcc test.c -o test -lpthread
  • 运行命令./test

6.2 运行结果分析

程序运行时会交替输出"main run"和"fun run",因为主线程和子线程并发执行:

main run fun run main run fun run fun run fun run fun run s=fun over

主线程执行2次循环后,通过pthread_join等待子线程结束,最后打印子线程返回的字符串。

7. IPC方式总结

Linux系统提供了多种进程间通信(IPC)方式,每种方式都有其适用场景:

IPC方式特点适用场景
管道半双工,亲缘进程间通信父子进程间简单数据传递
有名管道有文件名,任意进程可访问无亲缘关系进程间通信
共享内存最高效,直接内存访问需要高速数据交换的场景
信号量进程同步与互斥控制对共享资源的访问
消息队列异步通信,支持消息类型需要可靠消息传递的系统
套接字网络通信,跨主机网络应用程序

7.1 关键概念回顾

  • P,V操作:信号量的基本原子操作,用于进程同步
  • 临界资源:同一时刻,只允许一个进程访问的资源
  • 临界区:访问临界资源的代码段
  • 全双工/半双工:通信方向性,共享内存为全双工,管道为半双工