本文约5700字,今天来根据《一份靠谱的Linux终端产品应用层软件架构学习计划》这篇帖子中的计划,来梳理Linux 进程间通信IPC的知识体系。
关注公众号, 即可获得与Linux相关的电子书籍以及常用开发工具,文末有文档清单。
管道是最古老的 IPC 形式,本质是内核维护的一块环形缓冲区,提供单向、字节流的数据传输。它通过 pipe() 系统调用创建,返回两个文件描述符:fd[0] 用于读,fd[1] 用于写。数据从写端流入,从读端流出,遵循先进先出顺序。
特点:
#include<unistd.h>
intpipe(int pipefd[2]); // 成功返回0,失败-1
读写使用常规 read() / write() 系统调用,关闭用 close()。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
intmain(){
int fd[2];
pid_t pid;
char buf[256];
if (pipe(fd) == -1) {
perror("pipe");
return1;
}
pid = fork();
if (pid == -1) {
perror("fork");
return1;
}
if (pid == 0) { // 子进程:写入
close(fd[0]); // 关闭读端
char *msg = "Hello world!";
write(fd[1], msg, strlen(msg) + 1);
close(fd[1]);
} else { // 父进程:读取
close(fd[1]); // 关闭写端
read(fd[0], buf, sizeof(buf));
printf("Parent received: %s\n", buf);
close(fd[0]);
wait(NULL);
}
return0;
}
cmd1 | cmd2)。命名管道是对管道的扩展,通过文件系统路径名标识,允许无亲缘关系的进程访问。FIFO 在文件系统中是一个特殊文件(类型为 p),通过 mkfifo 创建。其通信行为和管道一致:单向字节流,阻塞读写。
#include<sys/stat.h>
intmkfifo(constchar *pathname, mode_t mode); // 创建FIFO文件
打开 FIFO 使用 open()(需指定 O_RDONLY 或 O_WRONLY),之后用 read/write 通信。注意:打开 FIFO 时若对方未打开,默认阻塞(除非指定 O_NONBLOCK)。
写端(writer.c):
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
intmain(){
int fd = open("/tmp/myfifo", O_WRONLY);
if (fd == -1) { perror("open"); return1; }
char *msg = "Hello FIFO!";
write(fd, msg, strlen(msg)+1);
close(fd);
return0;
}
读端(reader.c):
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
intmain(){
int fd = open("/tmp/myfifo", O_RDONLY);
if (fd == -1) { perror("open"); return1; }
char buf[256];
read(fd, buf, sizeof(buf));
printf("Received: %s\n", buf);
close(fd);
return0;
}
需先创建 FIFO:mkfifo /tmp/myfifo 0666(或使用 mkfifo 函数)。
共享内存是最快的 IPC 方式,因为它允许多个进程直接映射同一块物理内存区域,数据无需在内核和用户空间之间拷贝。进程可像访问普通内存一样读写该区域,但需要自行同步(常与信号量配合)。
Linux 下有两种实现:System V 共享内存(shmget、shmat 等)和 POSIX 共享内存(shm_open、mmap)。POSIX 更新、更简洁,本文侧重 POSIX。
#include<sys/mman.h>
#include<sys/stat.h>
#include<fcntl.h>
intshm_open(constchar *name, int oflag, mode_t mode); // 创建/打开共享内存对象,返回fd
intftruncate(int fd, off_t length); // 设置大小
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); // 映射
intmunmap(void *addr, size_t len); // 解除映射
intshm_unlink(constchar *name); // 删除共享内存对象
写进程(writer):
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<unistd.h>
#define SHM_NAME "/myshm"
#define SHM_SIZE 1024
intmain(){
int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (fd == -1) { perror("shm_open"); exit(1); }
ftruncate(fd, SHM_SIZE); // 设置大小
char *addr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
close(fd); // 映射后fd可关闭
strcpy(addr, "Hello from POSIX SHM!");
munmap(addr, SHM_SIZE);
return0;
}
读进程(reader):
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<unistd.h>
#define SHM_NAME "/myshm"
#define SHM_SIZE 1024
intmain(){
int fd = shm_open(SHM_NAME, O_RDWR, 0666);
if (fd == -1) { perror("shm_open"); exit(1); }
char *addr = mmap(NULL, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
close(fd);
printf("Read: %s\n", addr);
munmap(addr, SHM_SIZE);
// 可选择 shm_unlink(SHM_NAME) 删除对象
return0;
}
共享内存自身不提供同步,若多进程同时读写需配合信号量(见第五节)或互斥锁(如基于共享内存的 pthread_mutex_t 且设置 PTHREAD_PROCESS_SHARED)。
消息队列是内核维护的消息链表,每个消息具有类型和正文。进程通过消息类型选择性地读取,支持多对多通信,消息有边界,按优先级或类型检索。Linux 支持 System V 消息队列(msgget、msgsnd、msgrcv)和 POSIX 消息队列(mq_open、mq_send、mq_receive)。POSIX 更现代,接口更简洁,支持异步通知。
#include<mqueue.h>
mqd_tmq_open(constchar *name, int oflag, mode_t mode, struct mq_attr *attr);
intmq_send(mqd_t mqdes, constchar *msg_ptr, size_t msg_len, unsignedint msg_prio);
ssize_tmq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsignedint *msg_prio);
intmq_close(mqd_t mqdes);
intmq_unlink(constchar *name);
发送端:
#include<stdio.h>
#include<stdlib.h>
#include<mqueue.h>
#include<string.h>
#define QUEUE_NAME "/myqueue"
intmain(){
mqd_t mq = mq_open(QUEUE_NAME, O_CREAT | O_WRONLY, 0666, NULL);
if (mq == (mqd_t)-1) { perror("mq_open"); exit(1); }
char *msg = "Hello Message Queue!";
if (mq_send(mq, msg, strlen(msg)+1, 0) == -1) { perror("mq_send"); }
mq_close(mq);
return0;
}
接收端:
#include<stdio.h>
#include<stdlib.h>
#include<mqueue.h>
#define QUEUE_NAME "/myqueue"
#define MAX_MSG_SIZE 256
intmain(){
mqd_t mq = mq_open(QUEUE_NAME, O_RDONLY, 0666, NULL);
if (mq == (mqd_t)-1) { perror("mq_open"); exit(1); }
char buf[MAX_MSG_SIZE];
unsignedint prio;
ssize_t len = mq_receive(mq, buf, MAX_MSG_SIZE, &prio);
if (len == -1) { perror("mq_receive"); }
else {
buf[len] = '\0';
printf("Received (prio=%u): %s\n", prio, buf);
}
mq_close(mq);
mq_unlink(QUEUE_NAME); // 可删除
return0;
}
mq_attr 设置。mq_timedsend/mq_timedreceive)。信号量是一种用于同步和互斥的计数器,支持 P(等待/减)和 V(释放/加)操作。Linux 同样有两种:System V 信号量(semget、semop)和 POSIX 命名/匿名信号量(sem_open、sem_wait、sem_post)。POSIX 更简洁,本文重点。
信号量分为:
#include<semaphore.h>
// 命名信号量(用于无亲缘进程)
sem_t *sem_open(constchar *name, int oflag, mode_t mode, unsignedint value);
intsem_wait(sem_t *sem); // P操作,阻塞
intsem_trywait(sem_t *sem); // 非阻塞
intsem_post(sem_t *sem); // V操作
intsem_close(sem_t *sem);
intsem_unlink(constchar *name);
// 匿名信号量(用于共享内存中的同步,或线程同步)
intsem_init(sem_t *sem, int pshared, unsignedint value); // pshared=1表示进程共享
intsem_destroy(sem_t *sem);
(结合共享内存示例,信号量保证写者先写,读者后读)写者:
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<semaphore.h>
#include<unistd.h>
#include<string.h>
#define SHM_NAME "/myshm"
#define SEM_NAME "/mysem"
#define SHM_SIZE 1024
intmain(){
// 创建信号量,初值为0
sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0666, 0);
if (sem == SEM_FAILED) { perror("sem_open"); exit(1); }
int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
ftruncate(fd, SHM_SIZE);
char *addr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
strcpy(addr, "Data from writer");
sem_post(sem); // 通知读者数据已就绪
munmap(addr, SHM_SIZE);
// 不关闭信号量,留给读者
return0;
}
读者:
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<semaphore.h>
#include<unistd.h>
#define SHM_NAME "/myshm"
#define SEM_NAME "/mysem"
#define SHM_SIZE 1024
intmain(){
sem_t *sem = sem_open(SEM_NAME, 0); // 打开已存在的信号量
if (sem == SEM_FAILED) { perror("sem_open"); exit(1); }
int fd = shm_open(SHM_NAME, O_RDWR, 0666);
char *addr = mmap(NULL, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0);
close(fd);
sem_wait(sem); // 等待写者完成
printf("Read: %s\n", addr);
munmap(addr, SHM_SIZE);
sem_close(sem);
sem_unlink(SEM_NAME);
shm_unlink(SHM_NAME);
return0;
}
/dev/shm/ 或 /run/shm/(虚拟文件系统)。UNIX 域套接字(AF_UNIX)是同一主机上进程间全双工、可靠的通信方式,语法与网络套接字类似,但地址是文件系统路径(或抽象命名空间)。它支持流式(SOCK_STREAM,类似 TCP) 和数据报(SOCK_DGRAM,类似 UDP,但可靠且不丢包)。UNIX 域套接字在内核中直接交换数据,无需网络协议栈,性能优异,且能传递文件描述符(通过 SCM_RIGHTS)和凭证(SCM_CREDENTIALS)。
#include<sys/socket.h>
#include<sys/un.h>
intsocket(int domain, int type, int protocol); // domain = AF_UNIX
intbind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
intlisten(int sockfd, int backlog);
intaccept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
intconnect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
ssize_t send/recv 或 write/read
地址结构:
structsockaddr_un {
sa_family_t sun_family; // AF_UNIX
char sun_path[108]; // 路径名
};
服务端(server.c):
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<unistd.h>
#include<string.h>
#define SOCK_PATH "/tmp/uds_socket"
intmain(){
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock == -1) { perror("socket"); exit(1); }
structsockaddr_unaddr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path)-1);
unlink(SOCK_PATH); // 确保旧文件不存在
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind"); exit(1);
}
listen(sock, 5);
int conn = accept(sock, NULL, NULL);
if (conn == -1) { perror("accept"); exit(1); }
char buf[256];
ssize_t n = read(conn, buf, sizeof(buf)-1);
if (n > 0) {
buf[n] = '\0';
printf("Received: %s\n", buf);
write(conn, buf, n); // echo back
}
close(conn);
close(sock);
unlink(SOCK_PATH);
return0;
}
客户端(client.c):
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<unistd.h>
#include<string.h>
#define SOCK_PATH "/tmp/uds_socket"
intmain(){
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock == -1) { perror("socket"); exit(1); }
structsockaddr_unaddr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path)-1);
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("connect"); exit(1);
}
char *msg = "Hello UDS!";
write(sock, msg, strlen(msg)+1);
char buf[256];
ssize_t n = read(sock, buf, sizeof(buf)-1);
if (n > 0) {
buf[n] = '\0';
printf("Echo: %s\n", buf);
}
close(sock);
return0;
}
sendmsg/recvmsg 和 cmsg 结构,可将一个进程打开的文件描述符传递给另一个进程,实现文件共享。选型建议:
ipcrm 或 shm_unlink 等),否则可能泄漏。POSIX 对象位于 /dev/shm/ 或 /run/user/,系统重启会自动清除,但最好显式 unlink。O_NONBLOCK),信号量有 sem_trywait。write 到管道),谨慎使用复杂 IPC。Linux 提供了丰富的 IPC 机制,每种都有其设计哲学和适用舞台。理解它们的底层原理和 API 特性,能帮助我们根据具体需求做出最优选择。无论是轻量级的管道,还是高性能的共享内存,亦或是灵活强大的 UNIX 域套接字,掌握它们都是后端开发、系统编程和架构设计的重要基础。
希望本文能为你构建起完整的 IPC 知识体系,欢迎留言讨论!
“谢谢你看到这里”嵌入式Linux设备内部跨模块通信方案对比与统一消息架构设计
嵌入式Linux设备内部跨模块通信方案对比与统一消息架构设计
分享读书心得、工作经验,自我成长和生活方式。
希望我的文字能对你有所帮助