当前位置: 首页 > news >正文

C/C++ Linux系统编程:进程通讯完全指南,管道通讯、共享内存以及消息队列

引言

本文是笔者在学习 Linux 系统编程进程间通讯部分时的一份学习笔记总结与记录。内容涵盖了进程间通讯的基本概念、进程通讯的两大标准,并以POSIX标准为主深度讲解管道通讯(有名/无名)、共享内存和消息队列。希望通过清晰的逻辑和丰富的示例,帮助自己和大家更好地理解和掌握这一关键知识点。

一、什么是进程间通信(IPC)?

进程间通信 (Inter-Process Communication, IPC) 是指在不同进程之间传播或交换信息的一系列技术和方法。

每个进程都拥有自己独立的虚拟地址空间,一个进程无法直接访问另一个进程的数据或者进行通讯,为了实现协同工作,操作系统必须提供专门的机制来促成这种通信,这就是IPC的意义所在。

常见的 IPC 方式如下:

  • 管道 (Pipe) / 命名管道 (FIFO)

  • 信号 (Signal)

  • 消息队列 (Message Queue)

  • 共享内存 (Shared Memory)

  • 信号量 (Semaphore)

  • 套接字 (Socket)

本文将重点讨论管道、共享内存和消息队列。

二、两种通信标准:POSIX 和 System V

2.1 System V IPC

System V(读作 System Five )是一种基于UNIX的操作系统版本,最初由 AT&T(American TelePhone and Telegraph Company,美国电话电报公司,由Bell TelePhone Company发展而来)开发。它在1983年首次发布,对UNIX操作系统的发展产生了深远的影响。SystemV引入了许多新的特性和标准,后来被许多 UNIX 系统和类 UNIX 系统(如 Linux )采纳。

System V IPC是UNIX和类UNIX系统中常用的IPC方法之一,它通过关键字(key)来标识和访问IPC资源。

2.2 POSIX IPC

由 IEEE 的 POSIX 标准定义,旨在提高可移植性和一致性。POSIX IPC是POSIX标准中的一部分,提供了一种更现代和标准化的进程间通信方式。

POSIX IPC 使用名字(name)作为唯一标识。这些名字通常是以正斜杠(/)开头的字符串,用于唯一地识别资源如消息队列、信号量或共享内存对象。

💡 建议:现代开发推荐使用 POSIX IPC,更直观、易管理;但System V仍广泛存在于遗留系统中,需掌握。

本文后续将专注于POSIX标准下的IPC机制。

三、管道(Pipe)通信

管道是Unix中最古老的IPC形式,它提供了一种单向字节流的通信通道。

3.1 无名管道(Pipe)

无名管道没有实体文件与之关联,只能用于具有亲缘关系的进程间(如父子进程、兄弟进程)通信。

核心API原型:pipe()
#include <unistd.h>int pipe(int pipefd[2]);

功能:创建一个无名管道

参数:pipefd[2]:一个整型数组,用于返回两个文件描述符。

  • pipefd[0]:用于从管道读取(read)数据。

  • pipefd[1]:用于向管道写入(write)数据。

返回值:

  • 成功:返回 0

  • 失败:返回 -1,并设置 errno

工作流程:
  1. 父进程调用 pipe() 创建管道,获取两个文件描述符。

  2. 父进程调用 fork() 创建子进程。子进程会继承父进程的文件描述符表,因此也拥有这两个fd。

  3. 通信双方需要预先约定好数据流向。通常,父进程会关闭一端,子进程关闭另一端,形成一个单向通道。

示例代码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>int main() {int pipefd[2];pid_t pid;char buf[1024];// 创建管道if (pipe(pipefd) == -1) {perror("pipe");return 1;}pid = fork();if (pid == -1) {perror("fork");return 1;}if (pid == 0) {  // 子进程:写入数据close(pipefd[0]); // 关闭读端const char *msg = "子进程消息内容";write(pipefd[1], msg, strlen(msg));close(pipefd[1]);printf("子进程发送消息\n");} else {  // 父进程:读取数据close(pipefd[1]); // 关闭写端ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);if (n > 0) {buf[n] = '\0';printf("父进程接受数据: %s\n", buf);}close(pipefd[0]);wait(NULL); // 等待子进程结束}return 0;
}

3.2 有名管道(FIFO)

无名管道的最大限制是只能用于亲缘进程。有名管道(FIFO) 通过提供一个文件系统路径名来解决这个问题,任何进程(只要有适当权限)都可以通过打开这个“文件”来进行通信。

核心AP原型I:mkfifo()
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);

功能:在文件系统中创建一个FIFO特殊文件(命名管道)。

参数:

  • pathname:FIFO文件的路径名。

  • mode:指定文件的权限(如 0666

工作流程:
  1. 一个进程(通常是服务器或生产者)使用 mkfifo() 创建一个FIFO文件。

  2. 通信双方都使用标准的文件I/O函数(openreadwriteclose)来操作这个FIFO。

  3. open 的阻塞行为

    • 只读方式 open FIFO会阻塞,直到另一个进程以只写方式打开它。

    • 只写方式 open FIFO也会阻塞,直到另一个进程以只读方式打开它。

    • 使用 O_NONBLOCK 标志可以改变这种阻塞行为。

示例代码:

有名管道通讯可以适用于任何进程之间的通讯,所以使用两个独立进程实现

写进程(fifo_write_test.c)

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <errno.h>int main(int argc, char const *argv[])
{int fd;char *pipe_path = "/tmp/myfifo";if (mkfifo(pipe_path,0664) != 0){perror("mkfifo");if (errno != 17){exit(EXIT_FAILURE);}}// 对有名管道的特殊文件创建fdfd = open(pipe_path,O_WRONLY);if (fd == -1){perror("open");exit(EXIT_FAILURE);}char buf[100];ssize_t read_num;while ((read_num = read(STDIN_FILENO,buf,sizeof(buf))) > 0){write(fd,buf,read_num);}if(read_num < 0){perror("read");close(fd);exit(EXIT_FAILURE);}printf("发送数据到管道完成\n");close(fd);// 释放管道// 清除对应的特殊文件if (unlink(pipe_path) == -1){perror("unlink");}return 0;
}

读进程(fifo_read_test.c)

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <errno.h>int main(int argc, char const *argv[])
{int fd;char *pipe_path = "/tmp/myfifo";// 对有名管道的特殊文件创建fdfd = open(pipe_path,O_RDONLY);if (fd == -1){perror("open");exit(EXIT_FAILURE);}char buf[100];ssize_t read_num;// 读取管道信息写入到控制台while ((read_num = read(fd,buf,sizeof(buf))) > 0){write(STDOUT_FILENO,buf,read_num);}if(read_num < 0){perror("read");close(fd);exit(EXIT_FAILURE);}printf("接收数据到管道完成\n");close(fd);return 0;
}

编译运行

gcc fifo_write_test.c -o writer
gcc fifo_read_test.c -o reader
# 终端1运行写进程(它会阻塞等待读进程)
./writer
# 终端2运行读进程
./reader

注意!!!

调用 open() 打开有名管道时,flags设置为 O_WRONLY 则当前进程用于向有名管道写入数据,设置为 O_RDONLY 则当前进程用于从有名管道读取数据。设置为 O_RDWR 技术上是可行的,但正如上文提到的,此时管道既读又写很可能导致一个进程读取到自己发送的数据,通信出现混乱。因此,打开有名管道时,flags 只应为 O_WRONLY 或 O_RDONLY。 

四、共享内存(Shared Memory)通信

共享内存是最快的IPC方式。它允许多个进程将同一块物理内存映射到它们各自的地址空间。一个进程写入共享内存的数据,立即可被其他所有映射了该内存区域的进程看到。

4.1关键步骤

  1. 创建/获取共享内存对象。

  2. 映射共享内存到进程地址空间。

  3. 进行读写操作。

  4. 解除映射

  5. (可选)删除共享内存对象

4.2 POSIX共享内存核心API

4.2.1 shm_open()

函数原型

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>int shm_open(const char *name, int oflag, mode_t mode);

功能:创建或打开一个共享内存对象。它返回一个文件描述符,类似于 open()

参数:

  • name:共享内存对象的名字,必须以 / 开头(例如 /my_shm)。它会在 /dev/shm 目录下创建对应的文件。

  • oflag:标志位。可以是 O_RDONLYO_RDWR。与 O_CREAT(不存在则创建)、O_EXCL(与O_CREAT连用,确保创建的是新对象,如果存在则返回错误,避免覆盖原有对象)、O_TRUNC(如果对象已存在,将其截断长度为0,只有包含 O_RDWR 有效)进行按位或。

  • mode:权限位(如 0666),仅在创建时(使用了 O_CREAT)有效。

4.2.2 shm_unlink()

函数原型

#include <sys/mman.h>int shm_unlink(const char *name);

功能:删除一个先前由 shm_open 创建的命名共享内存对象。

参数name,与 shm_open 使用的名字相同。

注意!尽管这个函数被称为 unlink ,但是它并没有真正删除共享内存段本身,而是移除了与共享内存对象相关联的名称,使得无法通过该名称再次打开该内存段对象。当所有进程都解除映射后,系统会销毁该对象并释放资源。

4.2.3 ftruncate() 和 truncate()

函数原型

#include <unistd.h>
#include <sys/types.h>int ftruncate(int fd, off_t length);
int truncate(const char *path,off_t length)

这两个函数的功能完全相同:将普通文件或共享内存对象的大小设置为参数 length 指定的值

  • 如果文件原先的大小大于参数 length,则超出部分的数据将被丢弃

  • 如果文件原先的大小小于参数 length,则文件将被扩展,扩展的部分用零字节('\0')填充(这被称为“创建一个空洞”)。

对于 ftruncate()通过已经打开的文件描述符来操作文件。这是在调整共享内存大小时最常用的方法,因为你已经通过 shm_open() 拿到了一个文件描述符 shm_fd

  • 参数:fd:一个已经打开的可写文件描述符(例如,由 openshm_open 返回的描述符)。

而对于 truncate()则直接通过文件的路径名来操作,无需先打开文件。

  • 参数 path:需要调整大小的文件的路径名

4.2.4 mmap()和 munmap()

对于 mmap()

函数原型

#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

功能:将文件映射到内存区域,进程可以直接对内存区域进行读写操作,就像操作普通内存一样,但实际上是对文件或设备进行读写,从而实现高效的 I/O 操作。

参数:

  • addr:指向期望映射的内存地址的指针,通常设为 NULL,由系统内核自动选择合适地址。

  • length:映射区域的长度,以字节为单位。通常与 ftruncate 设置的大小一致。

  • prot:映射区域的保护方式。

    • PROT_READ:可读

    • PROT_WRITE:可写

    • PROT_EXEC:执行

  • flags:控制映射区域的特性。

    • MAP_SHARED映射区域是共享的,对映射区域的修改会影响文件和其他映射到同一区域的进程,其他进程可见必须使用此标志

    • MAP_PRIVATE映射区域是私有的,对映射区域的修改不会影响原始文件,对文件的修改会被暂时保存在一个私有副本中

    • MAP_ANONYMOUS:创建一个匿名映射,不与任何文件关联

  • fdshm_open() 返回的文件描述符。

  • offset:从文件/共享内存起始处的偏移量,通常为 0

返回值:成功返回映射区域的起始地址,失败返回 MAP_FAILED(即 (void *)-1)。

对于 munmap()

函数原型

#include <sys/mman.h>int munmap(void *addr, size_t length);

功能用于取消之前通过 mmap() 函数建立的内存映射关系

参数:

  • addrmmap() 返回的地址。

  • length这是要解除映射的内存区域的大小(以字节为单位),它必须与之前通过 mmap() 映射的大小一致

示例代码

下面给出一个综合的案例介绍各个API的使用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>int main(int argc, char const *argv[])
{// 创建一个共享内存对象char shm_name[100] = {0};sprintf(shm_name,"/letter%d",getpid());int fd = shm_open(shm_name,O_RDWR | O_CREAT,0644);if (fd < 0){perror("shem_open");exit(EXIT_FAILURE);}// 设置共享内存大小ftruncate(fd,1024);// 内存映射char *share = mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);if (share == MAP_FAILED){perror("mmap");exit(EXIT_FAILURE);}// 映射完成之后,关闭fd连接,不是释放close(fd);// 使用内存映射实现进程之间的通讯// 创建父子进程pid_t pid = fork();if (pid < 0) {// 创建子进程失败perror("fork");}else if (pid == 0){// 子进程strcpy(share,"子进程共享内存通讯内容\n");printf("子进程%d完成通讯\n",getpid());}else{// 父进程waitpid(pid,NULL,0);printf("父进程%d收到子进程%d的消息内容:%s",getpid(),pid,share);// 释放映射int re = munmap(share,1024);if (re == -1){perror("munmap");exit(EXIT_FAILURE);}}// 释放共享内存对象shm_unlink(shm_name);return 0;
}
4.3 内存共享对象在临时文件系统中的表示

内存共享对象在临时文件系统中的表示位于/dev/shm目录下。

为看到共享对象在临时文件系统中的表示,我们修改程序,在创建后不销毁。

五、消息队列(Message Queue)通信

消息队列提供了一种在进程间发送格式化消息数据块的机制。每个消息都是一个有类型的数据包,读进程可以按类型读取,提供了比FIFO更强的灵活性。   

特点

  • 消息被赋予一个优先级(类型),可以按优先级读取,而非严格FIFO。

  • 消息队列有内核持久性,即使没有进程连接着它,队列及其消息也会一直存在,直到被显式删除或系统重启。

  • 通信可以是双向的。

5. 1 POSIX消息队列核心数据结构

5.1.1 mqd_t
#include <mqueue.h>
typedef int mqd_t;

该数据类型定义在mqueue.h中,是用来记录消息队列描述符的。实质上是int类型的别名。

5.1.2 struct mq_attr
struct mq_attr {
long mq_flags;   /* Flags (ignored for mq_open()) */
long mq_maxmsg;  /* Max. # of messages on queue */
long mq_msgsize; /* Max. message size (bytes) */
long mq_curmsgs; /* # of messages currently in queue(ignored for mq_open()) */
};

mq_flags 标记,对于mq_open,忽略它,因为这个标记是通过前者的调用传递的

mq_maxmgs 队列可以容纳的消息的最大数量

mq_msgsize 单条消息的最大允许大小,以字节为单位

mq_curmsgs 当前队列中的消息数量,对于mq_open,忽略它

5.1.3 timespec
struct timespec {time_t   tv_sec;  // 秒 (Seconds)long     tv_nsec; // 纳秒 (Nanoseconds) [0, 999,999,999]
};

时间结构体,提供了纳秒级的UNIX时间戳,前一个数据是秒,后一个是纳秒

5. 2 POSIX消息队列核心API

5.2.1 mq_open()
#include <mqueue.h>mqd_t mq_open(const char *name, int oflag, ... /* mode_t mode, struct mq_attr *attr */);

功能:打开一个或创建一个消息队列

参数

  • name:消息队列的名字,必须以 / 开头(例如 /my_mq)。

  • oflag:标志位,与 open 和 shm_open 类似(O_RDONLYO_WRONLYO_RDWRO_CREATO_EXCLO_NONBLOCK)。

  • mode:(可变参数,仅在 O_CREAT 时需要)权限位。

  • attr:(可变参数,仅在 O_CREAT 时可以指定)指向 mq_attr 结构的指针,用于设置队列属性。如果为 NULL,则使用默认属性。

5.2.2 mq_timedsend() 和 mq_timedreceive()
#include <mqueue.h>
#include <time.h>int mq_timedsend(mqd_t mqdes, const char *msg_ptr,size_t msg_len, unsigned int msg_prio,const struct timespec *abs_timeout);ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr,size_t msg_len, unsigned int *msg_prio,const struct timespec *abs_timeout);

参数:

  • mqdesmq_open() 返回的消息队列描述符。

  • msg_ptr:指向要发送的消息缓冲区的指针。

  • msg_len:消息的长度(字节数),必须小于等于创建队列时设置的 mq_msgsize

  • msg_prio:消息的优先级(一个非负整数)。数值越大,优先级越高。如果不需要优先级,设为 0

  • abs_timeout这是最关键的新参数。它是一个指向 struct timespec 结构的指针,指定了一个绝对时间(Absolute Time),表示操作等待的最终截止时间。

返回值:

成功返回发送字节数

失败时返回 -1,并设置 errno如果失败是因为超时,errno 会被设置为 ETIMEDOUT

5.2.3 clock_gettime() - 获取精确时间

为了正确地设置 abs_timeout,我们需要一个高精度的函数来获取当前时间,并在此基础上进行计算。

#include <time.h>int clock_gettime(clockid_t clk_id, struct timespec *tp);

功能:获取指定时钟的当前时间。

参数

  • clk_id:时钟的标识符,指定要使用哪种时钟。

    • CLOCK_REALTIME系统实时时间,即我们通常理解的时间(年月日时分秒)。这个时间可以被系统管理员或 ntp 服务调整,可能会导致时间跳跃这是最常用的,也是 mq_timed* 函数默认使用的时钟

    • CLOCK_MONOTONIC单调时钟。从某个未指定的起点开始(通常是系统启动时间)开始计时,不受系统时间调整的影响。非常适合用于测量时间间隔。如果你的超时逻辑要求绝不因为系统时间被调快或调慢而受影响,就应该使用这个时钟

  • tp:指向 struct timespec 结构的指针,用于存储获取到的时间。

示例代码

#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>int main(int argc, char const *argv[])
{// 创建消息队列struct mq_attr attr;// 有用的参数 表示消息队列的容量attr.mq_maxmsg = 10;attr.mq_msgsize = 100;// 被忽略的消息 在创建爱你消息队列的时候用不到attr.mq_flags = 0;attr.mq_curmsgs = 0;char *mq_name = "/father_son_mq";mqd_t mqdes = mq_open(mq_name,O_RDWR | O_CREAT,0664,&attr);if (mqdes == (mqd_t)-1){perror("mq_open");exit(EXIT_FAILURE);}// 创建父子进程pid_t pid = fork();if (pid < 0){perror("fork");exit(EXIT_FAILURE);}else if (pid == 0){// 子进程 等待接受消息队列中的信息char read_buf[100];struct timespec time_info;// 清空缓冲区memset(read_buf,0,100);// 设置接收数据的等待时间clock_gettime(0,&time_info);time_info.tv_sec += 15;// 接受消息队列的数据同时打印到控制台终端for (size_t i = 0; i < 10; i++){// 清空缓冲区memset(read_buf,0,100);// 设置接收数据的等待时间clock_gettime(0,&time_info);time_info.tv_sec += 15;// 接受消息队列的数据同时打印到控制台终端if (mq_timedreceive(mqdes,read_buf,100,NULL,&time_info) == -1){perror("mq_timedreceive");}printf("子进程接收到数据:%s\n",read_buf);}}else{// 父进程 发送消息到消息对列char send_buf[100];struct timespec time_info;for (int i = 0; i < 10; i++){// 清空处理bufmemset(send_buf,0,100);sprintf(send_buf,"父进程的第%d次的消息发送\n",(i+1));// 获取当前的具体时间clock_gettime(0,&time_info);time_info.tv_sec += 5;if (mq_timedsend(mqdes,send_buf,strlen(send_buf),0,&time_info) == -1){perror("mq_timedesend");}printf("父进程发送一条消息,休息1s\n");sleep(1);}}// 最终:不管是父进程还是子进程都需要释放对消息队列的引用close(mqdes);// 清除消息队列,只需要执行一次if (pid > 0){mq_unlink(mq_name);}return 0;
}

结语

进程间通信是Linux系统编程的核心技能之一。理解每种IPC机制的原理、适用场景和API细节,有助于你编写高效、可靠的多进程程序。

🙋‍♂️ 如有疑问,欢迎留言讨论!

👍 如果本文对你有帮助,别忘了点赞 + 收藏!

http://www.dtcms.com/a/361787.html

相关文章:

  • 零基础从头教学Linux(Day 25)
  • vue3使用Eslint
  • B样条曲线在节点u处添加节点的操作方法
  • 心率监测系统优化方案全解析
  • 火语言 RPA:轻松生成界面应用,让开发触手可及​
  • 求欧拉回路:Hierholzer算法图解模拟
  • 计算机网络技术(四)完结
  • 算法题-02
  • 大型语言模型监督微调(SFT)
  • GitLab 18.3 正式发布,更新多项 DevOps、CI/CD 功能【二】
  • MiniCPM-V-4.5:重新定义边缘设备多模态AI的下一代视觉语言模型
  • 前端测试深度实践:从单元测试到E2E测试的完整测试解决方案
  • Axios与Ajax:现代Web请求大比拼
  • 新手向:前端开发中的常见问题
  • Laser Lorentzian Lineshape
  • 进程控制之进程创建与终止
  • Vue3+TS 流星夜景
  • TensorFlow 2.10 是最后一个支持在原生Windows上使用GPU的TensorFlow版本
  • Redisson和Redis实现分布式锁的对比
  • 【免费数据】2019年我国36个主要城市的高分辨率城市空地分布矢量数据
  • 【2025ICCV】
  • FOUPK3云服务平台旗下产品
  • Python 实战:内网渗透中的信息收集自动化脚本(7)
  • GD32入门到实战24--RTC实时时钟
  • 恶意软件概念学习
  • 【游戏开发】Houdini相较于Blender在游戏开发上有什么优劣势?我该怎么选择开发工具?
  • 【Java】Redis(中间件)
  • 订单后台管理系统-day07菜品模块
  • 域名备案后不解析可以吗
  • 五、导入现有模型