进程间通信
进程间通信
复习定位
每个进程拥有独立的地址空间(进程A不能直接读取进程B的内存)——因此进程间需要IPC(Inter-Process Communication)机制来协作。管道、共享内存、消息队列、信号量、套接字是Linux支持的主要IPC手段。管道最简单——共享内存最快——套接字可跨网络。选择哪种IPC取决于所要交换的数据量、速度和是否跨网络。
管道(pipe)
管道是最古老的IPC形式——在父进程和子进程之间单向传输数据。
匿名管道——通过pipe()系统调用创建——返回一对文件描述符(fd[0]读端,fd[1]写端)。数据从写端流入——从读端读出——内核缓冲区(通常16个内存页,64KB)。如果写端写入速率超过读端读出速率——管道缓冲区满——进程阻塞;如果管道的写端关闭后继续读——读返回0(EOF)。
管道只能在有亲缘关系的进程间使用(父子或兄弟进程之间在fork前打开管道传递fd)。无名管道默认是半双工(只能一个方读另一个方写)。
命名管道(FIFO)——通过mkfifo创建——可以在不相关进程之间通信——只要两个进程能访问同一个文件系统路径。FIFO以文件形式存在于文件系统中——打开时需要同时有读者和写者才能打开成功。
共享内存
共享内存是最快的IPC——两个或多个进程将同一块物理内存映射到自己的地址空间——进程可以直接读写这片共享区域——不需要内核参与数据拷贝——因此速度最快。共享内存本身不提供同步机制——进程间对共享数据的访问需要信号量或互斥锁保证同步——防止竞态条件。
// 创建共享内存段
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
// 附加到进程地址空间
int *shared = (int*)shmat(shmid, NULL, 0);
// 读写共享内存
shared[0] = 42;
// 分离
shmdt(shared);消息队列
消息队列是内核维持的消息链表——进程向队列发送消息(有类型、长度和数据)——另一进程按类型接收消息。每个消息队列在系统范围有唯一ID——不相关进程也可通过该ID通信。消息队列无需同步——因为读操作会自动阻塞等待消息到来——比共享内存更容易编程——但速度比共享内存慢(因为消息需要在内核和用户之间拷贝)。
信号量
信号量不是传递数据——而是实现进程间同步和互斥。信号量是一个整数——通过P/V操作来管理对共享资源的访问——通常和共享内存配合使用。
套接字
套接字(socket)最初的目的是用于网络通信——但Unix域套接字可用于同一台机器上不同进程之间的IPC——其性能与管道相近。套接字可以跨网络通信——这是其他IPC机制不具备的能力。
复习检查
匿名管道——为什么只能在父进程和子进程(或兄弟进程)之间使用——因为在fork之前——父进程已经创建了pipe——子进程继承了fd——二者共享了同一管道——不相关的进程无法获得共享的pipe的fd。
共享内存为什么是最快的IPC——共享内存不通过内核传输数据——进程直接读写同一物理内存——数据传输的零拷贝——远超需要数据从用户态拷贝到内核态再拷贝到用户态的管道和消息队列。
共享内存的同步问题——共享内存没有任何内嵌的同步机制——两个进程可能同时写一个地址产生栈条件——必须使用信号量互斥访问共享区域。
管道是半双工的——如果两个进程需要互相通信——需要两个管道——或者使用socketpair(AF_UNIX,SOCK_STREAM)实现全双工通信。
消息队列与共享内存+信号量相比的好处——消息队列按类型分拣消息——收方可以只接收指定类型的消息——不需要自己处理同步——编程简单。但共享内存+信号量的速度更快、能处理更大的数据量(而且适合大数据块交换)。