所谓进程间通信,就是让不同进程在内核的帮助下看到同一份系统资源,以达到数据交互的目的。
Linux提供了六种进程间通信的机制:管道、共享内存、信号量、消息队列、信号、套接字。
管道基于管道文件,本质上是一个存在于内核缓冲区的环形队列,满足先进先出(FIFO)原则,允许两个进程以**“生产者/消费者模型”**进行通信。
管道分为匿名管道和命名管道,他们拥有几乎相同的底层原理,区别就是创建和使用方式不同。
匿名管道只能由**“有血缘关系”**的进程使用,即拥有共同祖先的进程,常见的就是父子进程。
进程通过系统调用pipe()完成匿名管道的创建:
int pipe(int pipefd[2])
成功则返回0,失败则返回-1。
其中pipefd是一个输出型参数,表示匿名管道文件的两个文件描述符:
pipefd[0]具有==“只读”==属性,进程可通过该文件描述符进行读操作pipefd[1]具有==“只写”==属性,进程可通过该文件描述符进行写操作匿名管道使用文件描述符继承的方式保证进程通信:
在父进程使用pipe创建匿名管道时,文件描述符表中就会维护两个文件描述符pipefd[0]和pipefd[1],子进程会继承父进程的文件描述符表。
但是,匿名管道是一种单向通信方式,只能有一个进程读,另一个进程写。因此,负责读的进程最好将写文件描述符关闭,负责写的进程最好将读文件描述符关闭,避免用户的误操作!

不同于匿名管道仅使用文件描述符进行操作,命名管道是一个具有文件名的真正的文件,拥有独立的inode,因此允许任意进程打开该文件以实现进程间通信。
注:与匿名管道相同,命名管道文件只在使用时将数据存在内核缓冲区中。
mkfifo命令创建命名管道:mkfifo pipe_file_name
mkfifo函数创建命名管道:int mkfifo(const char *pathname, mode_t mode)
其中pathname是管道文件的存储路径(包括文件名);mode是该文件的权限(八进制);成功则函数返回0,失败返回-1。
在确定好哪个进程读,哪个进程写后,就可以通过对应的方式(O_RDONLY/O_WRONLY)打开管道文件,利用read/write进行读写,完成进程间通信。
SIGPIPE信号而退出;EOF(即read返回值为0);ulimit -a命令查看得到的pipe size为4KB而测试的最大容量为64KB的原因。管道文件能够保证实时通信,而普通文件存在于磁盘,内核并不会立即将缓冲区内容刷新到磁盘,因此无法完成实时通信。
管道文件的读写是并发安全的,相当于内核为用户提供了一种同步通信机制,使用起来比较方便。
共享内存是一段由内核维护的物理内存空间。
由于不同进程拥有独立的虚拟地址空间和页表,因此可以将共享内存通过进程页表映射到不同进程地址空间的共享区。如此,不同进程就可以通过自己的虚拟地址访问相同的物理地址,从而达到通信的目的。
int shmget(key_t key, size_t size, int shmflg)
key:用来标识共享内存的键值,不同进程可以通过相同的key获取到同一块共享内存。key可以由用户随意指定,也可以通过ftok函数获取。size:向系统申请的共享内存大小。建议申请PAGE_SIZE(虚拟页大小)的整数倍,因为系统是以页大小的整数倍开辟共享内存的,如果size不是页的整数倍,那么系统为了对齐而额外开辟的空间会导致内存浪费;shmflg:标志位。如果以key为标识的共享内存不存在,则可以通过IPC_CREAT创建该内存;如果以key为标识的共享内存已存在,且使用IPC_CREAT | IPC_EXCL时,则shmget函数返回-1;该标志位还可以用来设置共享内存的读写权限(按位或八进制权限)。shmid,之后通过该id使用共享内存;失败则返回-1。注:尽管申请的空间会被对齐至PAGE_SIZE的整数倍,但是用户能够使用的大小依然是size。
key_t ftok(const char *pathname, int proj_id)
pathname是一个路径名,proj_id是一个项目id,它们都可以随意填写。对于相同的pathname和proj_id,ftok()会返回相同的key值。
key_t key = ftok(PATH, PROJ_ID);
int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0644);
void *shmat(int shmid, const void *shmaddr, int shmflg)
shmid:shmget函数成功执行返回的id值;shmaddr:可以由用户指定一个共享区的地址作为共享内存映射到本进程地址空间的起始地址,一般设为NULL,表示由系统选取合适的地址;shmflg:指明对共享内存的权限,如SHM_RDONLY表示只读,不过该参数一般设为0,表示使用该内存的创建进程设置的权限。shmid标识的共享内存映射到共享区的起始地址,用户可以通过该地址读写共享内存,进行通信。注:该函数用来实现对共享内存的attach(挂接)。只有挂载到同一个共享内存的进程才能通过这段内存通信。
// 写进程wrproc.c每秒追加一个字符x
void* addr = shmat(id, NULL, 0);
for (int i = 0; i < shm_size; ++i)
{
addr[i] = 'x';
sleep(1);
}
// 读进程rdproc.c每秒打印一次共享内存的内容
void* addr = shmat(id, NULL, 0);
while (int i = 0; i < shm_size; ++i)
{
sleep(1);
printf("%s\n", addr);
}
int shmdt(const void *shmaddr)
shmaddr是shmat函数成功执行返回的起始地址,进程调用该函数表示不再使用这段共享内存,即de-attach(分离,取消挂接)。
如果成功分离则返回0,失败返回-1。
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
shmid:shmget函数成功执行返回的id值;cmd:控制选项。IPC_RMID,表示销毁这段共享内存;另外还有IPC_STAT和IPC_SET等选项,这里不关注;buf:指向一个保存着共享内存的模式状态和访问权限的数据结构,当使用IPC_RMID销毁共享内存时,该参数设为NULL即可。使用ipcs可以查看进程间通信的相关信息,其中ipcs -m仅查看共享内存的相关信息。
ipcrm -m id即可删除指定id的共享内存。
shmctl函数或 ipcrm -m指令销毁它。信号量(semaphore)是为了弥补"多进程竞争共享内存导致数据错乱"而引入的同步和互斥机制。
信号量本质就是一个计数器,对信号量的操作主要是PV操作。
PV操作具有原子性,因此又被称为PV原语。
进程同步,即让并发的进程按要求有序地执行。
以两个进程之间的同步为例:
如此,就保证了A进程在B进程之前运行。
进程互斥,即保证不同进程不能同时进入一个临界区。
实现步骤:
消息队列,即MQ(Message Queue),本质是保存在内核中的消息链表。
信号是进程间通信机制中唯一的一个异步通信机制。
当A进程向B进程发送信号时,B进程不一定立即处理该信号,而是在CPU切换到用户态之前检查是否有信号,然后进行对应处理。
信号详解戳这里
套接字可用于相同主机上的两个进程或是网络上不同主机上的两个进程进行通信,详见网络编程socket部分。
套接字详解戳这里