Linux学习笔记(十四)--进程间通信

张开发
2026/5/4 20:33:25 15 分钟阅读
Linux学习笔记(十四)--进程间通信
进程间通信目的数据传输一个进程需要将它的数据发送给另一个进程资源共享多个进程之间共享同样的资源。通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止 时要通知父进程。进程控制有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望能够拦截另 一个进程的所有陷入和异常并能够及时知道它的状态改变。本质让不同进程看到同一份资源。“资源”是一种特定形式的内存空间。这个资源一般是由操作系统提供而不是两个进程的其中一个因为假设由一个进程提供那么这个资源属于谁是这个进程独有那么就会破坏进程的独立性。我们进程访问这个空间进行通信本质就是访问操作系统进程代表的就是用户资源从创建到使用(一般)再到释放系统会提供一个系统调用接口来实现(从底层设计从接口设计都要由操作系统独立设计)所以一般操作系统会有一个独立的通信模块这个模块隶属于文件系统这个模块叫做IPC通信模块。IPC的作用提供一种受控的机制允许数据跨越进程边界流动同时不破坏操作系统的隔离保护。进程间通信发展管道基于文件级别的通信--System V进程间通信(本机内部通信--POSIX进程间通信网络通信进程间通信分类管道概念管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”本质具有“血缘关系”的进程通常是父子进程或兄弟进程间的单向字节流。接口pipe基本语法#include unistd.h int pipe(int pipefd[2]);参数pipefd[2]一个包含2个整数的数组pipefd[0]用于读取管道的文件描述符pipefd[1]用于写入管道的文件描述符若成功则返回0不成功则返回-1创建pipe#include unistd.h #include stdio.h int main() { int pipefd[2]; if (pipe(pipefd) -1) { perror(pipe failed); return 1; } printf(Pipe created successfully!\n); printf(Read end: fd %d\n, pipefd[0]); printf(Write end: fd %d\n, pipefd[1]); // 使用管道... // 记得关闭文件描述符 close(pipefd[0]); close(pipefd[1]); return 0; }特点匿名管道概念通过pipe()系统调用创建存在于内核中没有文件系统入口。只能用于有亲缘关系的进程。关键特性单向性数据只能从一端写入从另一端读取。这形成了经典的“生产者-消费者”模型。亲缘关系限制通常由父进程创建然后通过fork()将管道的文件描述符复制给子进程从而实现通信。字节流导向不维护消息边界。写入端多次写入的Hello和World在读取端可能被一次读取为HelloWorld。应用层需要自己定义消息分隔协议。生命周期随进程当所有引用该管道的进程都终止后管道资源会被内核自动回收。工作原理1创建管道父进程调用 int pipe(int fd[2])系统调用。内核会创建一个管道并返回两个文件描述符fd[0]用于读取管道。fd[1]用于写入管道。2创建子进程父进程调用 fork()。此时子进程继承了父进程打开的文件描述符表因此它也拥有指向同一个内核管道的fd[0]和fd[1]。3关闭不需要的端口由于管道是单向的为了让数据从父流向子父进程关闭它的读端 close(fd[0])。子进程关闭它的写端 close(fd[1])。反之如果想让数据从子流向父则关闭相反的描述符。4进行通信父进程用 write(fd[1], buf, size)向管道写数据。子进程用 read(fd[0], buf, size)从管道读数据。5通信结束进程关闭所有描述符当没有进程再持有管道的写端描述符时读端会收到EOF。站在文件描述符角度理解管道站在内核角度理解管道内核与底层缓冲区管道在内核中有一个固定大小的缓冲区通常为4KB或64KB可通过fcntl设置。写操作将数据复制到内核缓冲区读操作从缓冲区复制数据到用户空间。阻塞与非阻塞读空管道如果管道为空读操作默认阻塞直到有数据写入。写满管道如果管道已满写操作默认阻塞直到有数据被读出腾出空间。可以使用fcntl设置文件描述符为O_NONBLOCK来改为非阻塞模式。同步与互斥内核保证了管道读写的原子性。小于管道缓冲区大小PIPE_BUF通常是512字节或4KB的写操作是原子的即不会被其他写入操作的数据穿插。优点1简单高效是系统调用不涉及磁盘I/O数据在内核和用户空间间复制一次。2无需同步代码内核自动处理读写同步阻塞/唤醒。3资源自动管理。缺点1只能用于亲缘进程。2单向通信。要实现双向通信需要创建两个管道。3传输的是字节流无消息边界对结构化数据不友好。4生命周期短随进程结束。匿名管道代码1父进程向子进程发送消息#include stdio.h #include stdlib.h #include unistd.h #include string.h #include sys/wait.h int main() { int pipefd[2]; pid_t pid; char buffer[100]; if (pipe(pipefd) -1) { perror(pipe创建失败); exit(EXIT_FAILURE); } printf(管道创建成功: fd[0]%d, fd[1]%d\n, pipefd[0], pipefd[1]); pid fork(); if (pid 0) { perror(fork失败); exit(EXIT_FAILURE); } if (pid 0) { printf( 父进程 (PID%d) \n, getpid()); close(pipefd[0]); char *message Hello from parent process!; printf(父进程准备发送消息: %s\n, message); write(pipefd[1], message, strlen(message) 1); // 1包含\0 printf(父进程已发送消息\n); close(pipefd[1]); printf(父进程关闭了写端 fd[1]\n); wait(NULL); printf(子进程已结束父进程退出\n); } else { // 子进程 (pid 0) printf( 子进程 (PID%d) \n, getpid()); close(pipefd[1]); ssize_t bytes_read read(pipefd[0], buffer, sizeof(buffer)); printf(子进程接收到 %ld 字节数据\n, bytes_read); if (bytes_read 0) { printf(子进程收到的消息: %s\n, buffer); } close(pipefd[0]); printf(子进程关闭了读端 fd[0]\n); printf(子进程退出\n); } return 0; }2父子进程互相发送消息#include stdio.h #include stdlib.h #include unistd.h #include string.h #include sys/wait.h int main() { int pipe1[2]; int pipe2[2]; pid_t pid; char buffer[100]; if (pipe(pipe1) -1 || pipe(pipe2) -1) { perror(管道创建失败); exit(EXIT_FAILURE); } printf(pipe1: 父[%d]-子[%d]\n, pipe1[1], pipe1[0]); printf(pipe2: 子[%d]-父[%d]\n, pipe2[1], pipe2[0]); pid fork(); if (pid 0) { perror(fork失败); exit(EXIT_FAILURE); } if (pid 0) { printf( 父进程 (PID%d) \n, getpid()); close(pipe1[0]); close(pipe2[1]); char *msg_to_child Hello child! This is your parent.; printf(父进程发送消息: %s\n, msg_to_child); write(pipe1[1], msg_to_child, strlen(msg_to_child) 1); ssize_t bytes read(pipe2[0], buffer, sizeof(buffer)); if (bytes 0) { printf(父进程收到子进程消息: %s\n, buffer); } bytes read(pipe2[0], buffer, sizeof(buffer)); if (bytes 0) { printf(父进程收到子进程回复: %s\n, buffer); } char *end_msg Goodbye child!; write(pipe1[1], end_msg, strlen(end_msg) 1); close(pipe1[1]); close(pipe2[0]); wait(NULL); printf(父进程退出\n); } else { printf( 子进程 (PID%d) \n, getpid()); close(pipe1[1]); close(pipe2[0]); ssize_t bytes read(pipe1[0], buffer, sizeof(buffer)); if (bytes 0) { printf(子进程收到父进程消息: %s\n, buffer); } // 回复父进程 char *reply1 Hi parent! I got your message.; printf(子进程回复父进程: %s\n, reply1); write(pipe2[1], reply1, strlen(reply1) 1); char *reply2 How are you today?; write(pipe2[1], reply2, strlen(reply2) 1); bytes read(pipe1[0], buffer, sizeof(buffer)); if (bytes 0) { printf(子进程收到父进程消息: %s\n, buffer); } close(pipe1[0]); close(pipe2[1]); printf(子进程退出\n); } return 0; }命名管道 (FIFO)概念通过mkfifo()创建在文件系统中有一个路径名如/tmp/myfifo。任何知道该名字的进程都可以打开它进行通信突破了亲缘关系限制。关键特性可以用于任意进程间通信不限于亲缘关系遵循先进先出FIFO原则数据在内核中缓冲不实际写入磁盘创建命名管道1命令行创建使用mkfifo命令$ mkfifo mypipe2使用C语言创建#include sys/types.h #include sys/stat.h // 方法1使用 mkfifo 函数 int mkfifo(const char *pathname, mode_t mode); // 方法2使用 mknod 函数更通用 int mknod(const char *pathname, mode_t mode, dev_t dev);两个独立进程完整通信1写入者#include stdio.h #include stdlib.h #include fcntl.h #include sys/stat.h #include unistd.h #include string.h #define FIFO_PATH /tmp/myfifo int main() { int fd; char message[100]; printf(Writer Process (PID%d)\n, getpid()); if (mkfifo(FIFO_PATH, 0666) -1) { if (errno ! EEXIST) { perror(mkfifo); exit(EXIT_FAILURE); } } printf(Opening FIFO for writing...\n); fd open(FIFO_PATH, O_WRONLY); if (fd -1) { perror(open); exit(EXIT_FAILURE); } printf(FIFO opened successfully!\n); for (int i 1; i 5; i) { snprintf(message, sizeof(message), Message %d from writer (PID%d), i, getpid()); printf(Writing: %s\n, message); ssize_t bytes write(fd, message, strlen(message) 1); if (bytes -1) { perror(write); break; } sleep(1); } write(fd, END, 4); close(fd); printf(Writer finished.\n); return 0; }2读取者#include stdio.h #include stdlib.h #include fcntl.h #include sys/stat.h #include unistd.h #include string.h #define FIFO_PATH /tmp/myfifo int main() { int fd; char buffer[256]; printf(Reader Process (PID%d)\n, getpid()); printf(Opening FIFO for reading...\n); fd open(FIFO_PATH, O_RDONLY); if (fd -1) { perror(open); exit(EXIT_FAILURE); } printf(FIFO opened successfully!\n); while (1) { memset(buffer, 0, sizeof(buffer)); ssize_t bytes read(fd, buffer, sizeof(buffer) - 1); if (bytes 0) { printf(EOF reached or error\n); break; } printf(Received: %s\n, buffer); if (strcmp(buffer, END) 0) { printf(Received END signal, exiting...\n); break; } } close(fd); unlink(FIFO_PATH); printf(Reader finished.\n); return 0; }命名管道与匿名管道区别特性匿名管道命名管道文件系统可见否是进程关系必须有亲缘关系任意进程创建方式pipe()系统调用mkfifo()函数或 mknod()生命周期随进程结束持久存在直至被删除打开方式通过继承的文件描述符通过路径名打开

更多文章