Linux C并发编程基础(进程间通信)
1.进程间通信的核心分类首先明确 Linux 支持的主流 IPC 方式按使用场景和特性分类管道匿名管道 / 命名管道简单易用、单向 / 双向、基于文件描述符适合简单数据传递的场景匿名管道适用于父子进程 / 有亲缘关系进程间的通信命名管道适用于无亲缘关系进程间的通信。消息队列System V/Posix按消息类型传递、异步通信、可持久化适合无亲缘关系进程、需要分类传递数据的场景。共享内存System V/Posix最高效直接访问内存无数据拷贝、需同步机制配合适合高并发、大数据量传递场景如视频、大数据处理。信号量System V/Posix实现进程间同步、互斥保护共享资源配合共享内存、多个进程竞争同一资源的场景如临界区保护。信号Linux 标准信号 / 实时信号异步通知、简单高效、携带信息少适合进程异常处理、简单通知场景如终止进程、定时器通知。SocketAF_UNIX/AF_INET支持本地进程AF_UNIX、跨主机进程AF_INET、通用性强适合本地无亲缘关系进程、跨服务器分布式进程通信。2.管道Pipe—— 最简单的字节流通信管道是 Linux 最基础的 IPC 机制本质是「内核维护的一块缓存」对外表现为一对文件描述符进程通过读写文件描述符实现数据传递。分为匿名管道和命名管道两种。2.1.匿名管道Anonymous Pipe核心特性「匿名」无实际文件路径仅存在于内核中进程退出后管道消失。单向通信数据只能从一端写入另一端读出读端、写端固定。亲缘关系限制仅支持父子进程、兄弟进程等有亲缘关系的进程通信由 fork() 继承文件描述符。基于字节流无消息边界读取数据时按字节流读取需自行处理数据格式。阻塞特性读端无数据时阻塞写端管道满时阻塞写端关闭后读端读取到 EOF0读端关闭后写端会收到 SIGPIPE 信号终止。函数原型// 成功返回 0失败返回 -1设置 errno int pipe(int pipefd[2]);参数pipefd[2]输出参数返回两个文件描述符pipefd[0]管道读端用于读取数据。pipefd[1]管道写端用于写入数据。注意创建管道后通常需要配合 fork() 创建子进程子进程继承管道文件描述符再关闭无用的端如父进程关闭读端子进程关闭写端实现单向通信。实操示例父子进程通过匿名管道通信#include stdio.h #include stdlib.h #include unistd.h #include string.h int main() { int pipefd[2]; char buf[1024]; const char *msg Hello, 子进程这是父进程通过匿名管道发送的消息\n; // 第一步创建匿名管道 if (pipe(pipefd) -1) { perror(pipe failed); exit(EXIT_FAILURE); } // 第二步创建子进程 pid_t pid fork(); if (pid -1) { perror(fork failed); exit(EXIT_FAILURE); } if (pid 0) { // 子进程关闭写端读取管道数据 close(pipefd[1]); // 子进程仅读取关闭无用的写端 // 阻塞读取管道数据 ssize_t read_len read(pipefd[0], buf, sizeof(buf) - 1); if (read_len 0) { buf[read_len] \0; // 字符串结束符 printf(【子进程】读取到管道数据%s, buf); } else if (read_len 0) { printf(【子进程】管道写端已关闭无更多数据\n); } else { perror(read failed); } close(pipefd[0]); // 读取完成关闭读端 exit(EXIT_SUCCESS); } else { // 父进程关闭读端写入管道数据 close(pipefd[0]); // 父进程仅写入关闭无用的读端 // 向管道写入数据 ssize_t write_len write(pipefd[1], msg, strlen(msg)); if (write_len -1) { perror(write failed); } else { printf(【父进程】成功向管道写入 %zd 字节数据\n, write_len); } close(pipefd[1]); // 写入完成关闭写端触发子进程 EOF // 等待子进程执行完毕 wait(NULL); exit(EXIT_SUCCESS); } }编译运行结果gcc anonymous_pipe_demo.c -o anonymous_pipe_demo ./anonymous_pipe_demo # 输出 【父进程】成功向管道写入 66 字节数据 【子进程】读取到管道数据Hello, 子进程这是父进程通过匿名管道发送的消息2.2.命名管道Named Pipe/FIFO核心特性「命名」有实际的文件路径如 /tmp/my_fifo存在于文件系统中进程退出后管道文件仍存在需手动删除。双向 / 单向通信默认单向可通过打开方式配置为双向。无亲缘关系限制支持任意两个无亲缘关系的进程通信只要能访问该管道文件。其他特性与匿名管道一致基于字节流、阻塞特性、依赖文件描述符。函数原型// 成功返回 0失败返回 -1设置 errno int mkfifo(const char *pathname, mode_t mode);参数pathname管道文件的路径如 /tmp/my_fifo。mode管道文件的权限如 0664与 chmod 权限一致受 umask 影响。注意命名管道创建后需要通过 open() 打开读端用 O_RDONLY写端用 O_WRONLY之后通过 read()/write() 读写数据与普通文件操作一致。实操示例两个无亲缘关系进程通过命名管道通信第一步写进程fifo_writer.c#include stdio.h #include stdlib.h #include unistd.h #include string.h #include sys/stat.h #include fcntl.h #define FIFO_PATH /tmp/my_fifo // 命名管道路径 #define MODE 0664 // 管道文件权限 int main() { const char *msg Hello, 读进程这是写进程通过命名管道发送的消息\n; // 第一步创建命名管道若已存在忽略 EEXIST 错误 if (mkfifo(FIFO_PATH, MODE) -1) { if (errno ! EEXIST) { // 排除文件已存在的错误 perror(mkfifo failed); exit(EXIT_FAILURE); } printf(【写进程】命名管道已存在无需重复创建\n); } else { printf(【写进程】成功创建命名管道%s\n, FIFO_PATH); } // 第二步以只写模式打开命名管道 int fd open(FIFO_PATH, O_WRONLY); if (fd -1) { perror(open failed); exit(EXIT_FAILURE); } // 第三步向管道写入数据 ssize_t write_len write(fd, msg, strlen(msg)); if (write_len -1) { perror(write failed); } else { printf(【写进程】成功向管道写入 %zd 字节数据\n, write_len); } // 第四步关闭文件描述符删除管道文件可选 close(fd); unlink(FIFO_PATH); // 删除管道文件 printf(【写进程】操作完成已删除命名管道\n); exit(EXIT_SUCCESS); }第二步读进程fifo_reader.c#include stdio.h #include stdlib.h #include unistd.h #include string.h #include sys/stat.h #include fcntl.h #define FIFO_PATH /tmp/my_fifo // 与写进程一致的管道路径 int main() { char buf[1024]; // 第一步以只读模式打开命名管道若不存在会阻塞等待管道被创建 printf(【读进程】等待命名管道创建...\n); int fd open(FIFO_PATH, O_RDONLY); if (fd -1) { perror(open failed); exit(EXIT_FAILURE); } printf(【读进程】成功打开命名管道%s\n, FIFO_PATH); // 第二步阻塞读取管道数据 ssize_t read_len read(fd, buf, sizeof(buf) - 1); if (read_len 0) { buf[read_len] \0; printf(【读进程】读取到管道数据%s, buf); } else if (read_len 0) { printf(【读进程】管道写端已关闭无更多数据\n); } else { perror(read failed); } // 第三步关闭文件描述符 close(fd); printf(【读进程】操作完成\n); exit(EXIT_SUCCESS); }第三步编译运行# 1. 编译两个进程 gcc fifo_writer.c -o fifo_writer gcc fifo_reader.c -o fifo_reader # 2. 先启动读进程后台运行 ./fifo_reader # 3. 再启动写进程 ./fifo_writer # 输出 【读进程】等待命名管道创建... 【写进程】成功创建命名管道/tmp/my_fifo 【写进程】成功向管道写入 66 字节数据 【写进程】操作完成已删除命名管道 【读进程】成功打开命名管道/tmp/my_fifo 【读进程】读取到管道数据Hello, 读进程这是写进程通过命名管道发送的消息 【读进程】操作完成3.消息队列Message Queue—— 按类型传递的异步通信消息队列是 System V IPC 三大组件之一另外两个是共享内存、信号量本质是「内核维护的一个消息链表」进程可以向队列中写入不同类型的消息也可以从队列中读取指定类型的消息实现异步通信。核心特性异步通信发送方写入消息后无需等待接收方读取消息持久化在内核中接收方按需读取。按类型传递每个消息都有一个类型标识接收方可按类型筛选读取无需按写入顺序读取。无亲缘关系限制支持任意进程通信只要拥有相同的键值 key。局限性消息大小和队列总长度有内核限制进程退出后消息队列仍存在需手动删除避免资源泄漏。函数原型获取一个当前未用的IPC的key获取消息队列ID发送和接受消息设置或者获取消息队列的相关属性// 1. 创建/获取消息队列成功返回消息队列 ID失败返回 -1 int msgget(key_t key, int msgflg); // 2. 向消息队列发送消息成功返回 0失败返回 -1 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); // 3. 从消息队列接收消息成功返回读取的消息长度失败返回 -1 ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); // 4. 控制消息队列删除、获取状态成功返回 0失败返回 -1 int msgctl(int msqid, int cmd, struct msqid_ds *buf);关键说明key_t key用于标识消息队列的唯一键值通常通过 ftok() 生成。消息结构需自定义必须以 long msg_type 开头消息类型后续为消息数据。msgctl() 的 cmd 参数传入 IPC_RMID 可删除消息队列。实操示例两个进程通过消息队列传递消息自定义消息结构struct msg_buf { long msg_type; // 消息类型必须 1 char msg_data[1024]; // 消息数据 };发送进程用 ftok() 生成 key。用 msgget() 创建 / 获取消息队列。填充消息结构设置类型和数据用 msgsnd() 发送消息。接收进程用相同的 key 获取消息队列。用 msgrcv() 按指定类型读取消息。最后用 msgctl() 删除消息队列。示例#include stdio.h #include sys/wait.h #include sys/types.h #include sys/stat.h #include unistd.h #include stdlib.h #include fcntl.h #include string.h #include strings.h #include sys/ipc.h #include sys/msg.h //定义消息结构体 typedef struct _msg{ long mtype; //接收者类型 char mtext[100];//消息内容可以有多个成员 }MSG; long msg_type10; int main() { //获取KEY值 key_t keyftok(./,1); //创建消息队列 int msg_idmsgget(key,IPC_CREAT | 0777); pid_t sonfork(); if(son0){ printf(子进程,ID为%d\n,getpid()); while(1){ MSG new_msg; new_msg.mtypemsg_type; scanf(%s,new_msg.mtext); msgsnd(msg_id,new_msg,sizeof(MSG)-sizeof(long),0); if(strcmp(new_msg.mtext,exit)0){ break; } } } else if(son0){ printf(父进程,ID为%d\n,getpid()); while(1){ MSG new_msg; msgrcv(msg_id,new_msg,sizeof(MSG)-sizeof(long),msg_type,0); printf(from son:%s\n,new_msg.mtext); if(strcmp(new_msg.mtext,exit)0){ break; } } wait(NULL); } else if(son0){ perror(fork failed\n); } return 0; }4.共享内存Shared Memory—— 最高效的大数据通信共享内存是所有 IPC 方式中效率最高的一种本质是「内核分配一块物理内存映射到多个进程的虚拟地址空间」进程直接访问该内存区域无需数据拷贝其他 IPC 方式均需内核中转存在数据拷贝。核心特性高效性无数据拷贝进程直接读写内存适合大数据量、高并发场景。无同步机制共享内存本身不提供同步和互斥多个进程同时读写会导致数据错乱需配合信号量或互斥锁使用。无亲缘关系限制支持任意进程通信通过相同 key 关联。局限性进程退出后共享内存仍存在需手动删除内存大小受系统限制。函数原型获取共享内存ID对共享内存进行映射或者解除映射获取或者设置共享内存的相关属性// 1. 创建/获取共享内存成功返回共享内存 ID失败返回 -1 int shmget(key_t key, size_t size, int shmflg); // 2. 将共享内存映射到进程虚拟地址空间成功返回映射地址失败返回 (void*)-1 void *shmat(int shmid, const void *shmaddr, int shmflg); // 3. 解除共享内存与进程的映射成功返回 0失败返回 -1 int shmdt(const void *shmaddr); // 4. 控制共享内存删除、获取状态成功返回 0失败返回 -1 int shmctl(int shmid, int cmd, struct shmid_ds *buf);核心流程进程 A 用 shmget() 创建共享内存指定大小和权限。进程 A 用 shmat() 将共享内存映射到自身虚拟地址空间。进程 B 用相同 key 获取共享内存并用 shmat() 映射到自身地址空间。进程 A/B 直接读写映射地址共享内存配合信号量实现同步。通信完成后用 shmdt() 解除映射用 shmctl() 删除共享内存示例#include stdio.h #include sys/wait.h #include sys/types.h #include sys/stat.h #include unistd.h #include stdlib.h #include fcntl.h #include string.h #include strings.h #include sys/ipc.h #include sys/msg.h #include sys/shm.h #define SCHEME_SIZE 1024 int main() { //获取KEY值 key_t keyftok(./,1); //创建共享内存 int shm_idshmget(key,SCHEME_SIZE,IPC_CREAT | 0777); pid_t sonfork(); if(son0){ printf(子进程,ID为%d\n,getpid()); char *buf(char *)shmat(shm_id,NULL,0); while(1){ scanf(%s,buf); if(strcmp(buf,exit)0){ break; } } shmdt(buf); shmctl(shm_id,IPC_RMID,NULL); } else if(son0){ printf(父进程,ID为%d\n,getpid()); char *buf(char *)shmat(shm_id,NULL,0); while(1){ if(strlen(buf)0){ printf(from son:%s\n,buf); if(strcmp(buf,exit)0){ break; } } sleep(1); } shmdt(buf); shmctl(shm_id,IPC_RMID,NULL); wait(NULL); } else if(son0){ perror(fork failed\n); shmctl(shm_id,IPC_RMID,NULL); } return 0; }5.信号Signal—— 最简单的异步通知通信信号是 Linux 进程间最基础的异步通知机制用于「通知进程发生了某个事件」进程可以对信号做出三种响应默认处理、忽略、自定义处理。核心特性异步性信号可随时发送给进程进程无需主动轮询收到信号后中断当前执行流程处理信号函数。携带信息少仅传递信号编号无法传递复杂数据部分实时信号支持携带少量数据。通用性强支持所有进程通信还可用于内核向进程发送通知如段错误 SIGSEGV。常用标准信号共 31 个无实时性函数原型向指定进程发送信号捕捉一个特定信号自己给自己发送一个指定信号将本进程挂起直至收到一个信号信号集操作函数阻塞或者解除阻塞一个或多个信号给某进程发送一个指定的信号同时携带一些数据捕捉一个指定信号且可以通过扩展响应函数来获取信号携带的额外数据// 1. 注册信号处理函数设置信号的响应方式 void (*signal(int sig, void (*handler)(int)))(int); // 2. 向指定进程发送信号 int kill(pid_t pid, int sig); // 3. 自定义信号处理函数用于捕获信号后执行自定义逻辑 void handler(int sig) { printf(收到信号%d\n, sig); }实操示例父进程向子进程发送信号#include stdio.h #include stdlib.h #include unistd.h #include signal.h #include sys/wait.h // 子进程的信号处理函数 void sig_handler(int sig) { if (sig SIGUSR1) { // 自定义用户信号 1 printf(【子进程】收到父进程发送的 SIGUSR1 信号%d即将退出\n, sig); exit(EXIT_SUCCESS); } } int main() { pid_t pid fork(); if (pid -1) { perror(fork failed); exit(EXIT_FAILURE); } if (pid 0) { // 子进程注册 SIGUSR1 信号的处理函数 signal(SIGUSR1, sig_handler); printf(【子进程】PID%d等待父进程发送信号...\n, getpid()); // 无限循环等待信号 while (1) { sleep(1); } } else { // 父进程睡眠 3 秒后向子进程发送 SIGUSR1 信号 printf(【父进程】PID%d睡眠 3 秒后发送信号...\n, getpid()); sleep(3); // 向子进程发送 SIGUSR1 信号 if (kill(pid, SIGUSR1) -1) { perror(kill failed); } else { printf(【父进程】成功向子进程 %d 发送 SIGUSR1 信号\n, pid); } // 等待子进程退出 wait(NULL); printf(【父进程】子进程已退出\n); } return 0; }6.本地 SocketAF_UNIX—— 最通用的本地进程通信Socket 不仅支持跨主机的网络通信AF_INET还支持本地进程间通信AF_UNIX也叫 AF_LOCAL本质是「基于文件系统的套接字文件」具有通用性强、支持双向通信、无数据大小限制等优点是本地无亲缘关系进程通信的首选之一。核心特性通用性强API 与网络 Socket 一致便于扩展为跨主机通信。双向通信支持全双工通信进程可同时读写。无亲缘关系限制支持任意本地进程通信通过套接字文件关联。基于字节流 / 数据报支持两种通信模式灵活适配不同场景。核心流程AF_UNIX 字节流模式服务端创建套接字socket(AF_UNIX, SOCK_STREAM, 0)。绑定套接字文件bind()指定套接字文件路径。监听连接listen()。接受连接accept()。读写数据read()/write()。客户端创建套接字socket(AF_UNIX, SOCK_STREAM, 0)。连接服务端connect()指定相同的套接字文件路径。读写数据read()/write()。通信完成后关闭套接字删除套接字文件。示例服务端void init_server() { struct sockaddr_un addr_server, addr_client; int addrlen sizeof(struct sockaddr_un); server_fd socket( AF_UNIX, SOCK_STREAM, 0); if(server_fd0) return; unlink(/var/run/unixsock); memset( addr_server, 0, sizeof( addr_server ) ); addr_server.sun_family AF_UNIX; strncpy( addr_server.sun_path, /var/run/unixsock, sizeof( addr_server.sun_path ) - 1 ); if(bind( server_fd, ( struct sockaddr * )addr_server, addrlen ) 0) return; if(listen( server_fd, 10 ) 0) return; } void recv_data() { int clientfd; struct sockaddr_un addrclient; int addrlen sizeof(struct sockaddr_un); char buf[1024]{0}; int readlen; bool flag; while(1) { clientfdaccept( server_fd, ( struct sockaddr * )addrclient,(socklen_t *)addrlen ); if(clientfd0) return; flagtrue; while(flag) { readlenread(clientfd,buf,sizeof(buf)); if(readlen0) break; qDebug().noquote()QString(buf); memset(buf,0,sizeof (buf)); } close(clientfd); } }客户端void init_client(void) { struct stat buf; if(access(FILE_CLIENT,F_OK) 0){ printf(%s is not access\n,FILE_CLIENT); return; } if(stat(FILE_CLIENT,buf) ! 0){ return; } if(!S_ISSOCK(buf.st_mode)){ printf(%s is not a socket file\n,FILE_CLIENT); return; } client_fd socket(AF_UNIX,SOCK_STREAM,0); if(client_fd 0){ printf(create socket failed\n); return; } struct sockaddr_un addr_server; int addrlen sizeof(struct sockaddr_un); memset( addr_server, 0, sizeof( addr_server ) ); addr_server.sun_family AF_UNIX; strncpy( addr_server.sun_path, FILE_CLIENT, sizeof( addr_server.sun_path ) - 1 ); if(connect( client_fd, ( struct sockaddr * )addr_server, addrlen ) 0){ printf(connect failed\n); return; } client_connect_flag 1; }7.信号量System V/Posix1获取信号量ID2对信号量进行P/V操作或者等零操作信号量操作结构体的定义如下struct sembuf { unsigned short sem_num; /*信号量元素序号(数组下标)*/ short sem_op;/*操作参数*/ short sem_flg;/*操作选项*/ };请注意信号量元素的序号从0开始实际上就是数组下标。根据sem_op的数值信号量操作分成3种情况1.当sem_op大于0时进行V操作即信号量元素的值semval将会被加上sem_op 的值。如果SEM_UNDO被设置了那么该V操作将会被系统记录。V操作永远不会导致进程阻塞。2.当sem_op等于0时进行等零操作如果此时semval恰好为0则semop()立即成功返回否则如果IPC_NOWAIT被设置则立即出错返回并将errno设置为EAGAIN否则将使得进程进入睡眠直到以下情况发生semval变为0。信号量被删除。将导致semop()出错退出错误码为EIDRM收到信号。将导致semop()出错退出错误码为EINTR3.当sem_op小于0时进行P操作即信号量元素的值semval将会被减去sem_op 的绝对值。如果semval大于或等于sem_op的绝对值则semop()立即成功返回semval的值将减去sem_op的绝对值并且如果SEM_UNDO被设置了那么该P操作将会被系统记录。如果semval小于sem_ op的绝对值并且设置了IPC_NOWAIT那么semop()将会出错返回且将错误码置为EAGAIN否则将使得进程进入睡眠,直到以下情况发生semval的值变得大于或者等于sem_op的绝对值。信号量被删除。将导致semop()出错退出,错误码为EIDRM收到信号。将导致semop()出错退出,错误码为EINTR3获取或者设置信号量的相关属性8.各类 IPC 方式对比与选型建议8.1.核心对比总结8.2.选型建议简单父子进程通信优先匿名管道最简单。无亲缘关系进程简单数据传递优先命名管道无需复杂 API。无亲缘关系进程分类传递数据优先消息队列异步按类型传递。高并发、大数据量传递如视频、大数据优先共享内存 信号量最高效。简单异步通知如进程终止、异常通知优先信号最轻量。本地进程复杂通信、需兼容跨主机扩展优先本地 Socket通用性最强。9.核心总结Linux 进程间通信的核心是依赖内核中转或共享内核资源突破进程地址空间的独立性限制。主流 IPC 分为管道类、消息类、共享内存类、信号类、Socket 类各有优劣需按场景选型。效率优先级共享内存 信号 管道 / 命名管道 消息队列 本地 Socket。易用性优先级匿名管道 信号 命名管道 消息队列 本地 Socket 共享内存。生产环境中共享内存必须配合信号量实现同步避免数据竞争所有 System V IPC 资源消息队列、共享内存、信号量使用后必须手动删除避免资源泄漏。