参考:郑谦益. GNU/Linux编程[M]. 北京:人民邮电出版社, 2012 :187-211.
进程通信概述
Linux中,各进程的用户地址空间彼此独立,一个进程不能直接访问另一个进程的用户地址空间,而不同进程之间拥有相同的内核地址空间,Linux内核对于各进程来说是共享的,因此进程之间可以通过内核实现通信。
在没有进程间通信(Inter Process Communication, IPC)之前,可以使用文件、信号或者网络来实现数据交换。两个进程通过文件交换数据,但这种方式操作起来不够方便,且效率较低;使用信号的方式也无法在两个进程间传递大量的信息。后来,引入了管道、IPC和网络套接字的概念和技术用于实现多进程间的同步与通信。
管道(pipe)
管道是在同一台计算机上的两个进程之间进行数据交互的一种机制,具有单向、先进先出、无结构的字节流的特点。管道有两个端点,一端用于写入数据,另一端用于读出数据,当数据从管道中被读出后,这些数据将被移走。
当进程从空管道中读取或写入已满的管道时,进程将被挂起,直到有进程向管道中写入数据或从管道中读取数据。
根据管道提供应用接口的不同,管道可以分为命名管道和无名管道。
无名管道
无名管道是在内核中建立一条管道,管道有两个端,一端用于写,另一端用于读,从管道写入端写入的数据可以从管道的读出端读出。
无名管道的创建
pipe()函数用于创建一条管道,其函数原型如下:
#include <unistd.h>
int pipe(int fd[2]);
- fd[2]:两个文件描述符,其中fd[0]为读出端,fd[1]为写入端。
返回值:成功返回0,否则返回-1。
应注意管道是单向的,只能从fd[1]写入,从fd[0]读出。两个文件描述符可以像普通文件一样,调用read()、write()和close()等函数操作。
此外,进程使用pipe()函数创建的无名管道的两端所对应的两个文件描述符是属于用户地址空间的,如果其他进程要操作管道的另一端,就必须继承这个文件描述符的资源,因此可以采用父进程创建子进程的方式(fork()函数),分别对管道的一端进行读写。
读写某个进程
popen()函数用于读取某个进程的结果,或写入某个进程的输入,其函数原型如下:
#include <stdio.h>
FILE* popen(const char* command, const char* type);
- command:要执行的Shell命令;
- type:执行命令的输入输出类型,"r"表示打开命令执行的标准输出,"w"表示打开命令执行的标准输入。
返回值:成功返回文件I/O流,否则返回NULL。
关闭由popen()函数打开的管道
pclose()函数用于关闭由popen()函数打开的管道,其函数原型如下:
#include <stdio.h>
int pclose(FILE* stream);
- stream:由popen()打开的管道;
返回值:成功返回exit status,失败返回-1。
命名管道
与无名管道不同,命名管道作为一个特殊的文件保存在文件系统中,任意一个进程都可以对其进行读写,只要该进程有相应的读写权限。
mkfifo()函数用于创建一个管道文件,其函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);
- pathname:文件路径名;
- mode:存取权限。
返回值:成功返回0,失败返回-1,错误信息位于errno中。
消息队列(Message Queue)
消息队列是存在于内核中的消息列表。一个进程可以将消息发送至消息队列,另一个进程则可从消息队列中获取消息。消息队列的操作方式为先进先出。
创建消息队列
msgget()函数用于获取或建立消息队列,其函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
- key:用于区分不同消息队列的参数(非0整数);
- msgflg:存取权限或创建条件。
返回值:成功返回消息队列标识,失败返回-1。
如果key为IPC_PRIVATE,则内核创建一块新的共享内存;否则,如果key所对应的共享内存已存在,则返回该共享内存。msgflg用二进制来标识,其最小9位用于说明共享内存的存取权限,如果msgflg包含IPC_CREAT,则表示创建共享内存。
发送消息
msgsnd()函数用于向已创建的消息队列中放置消息,其函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, struct msgbuf* msgp, size_t msgsz, int msgflg);
- msqid:消息队列标识;
- msgp:发送给队列的消息,可以是任意类型的结构体,但第一个字段必须是long类型,表明消息的类型,msgrcv()函数将据此接收消息;
- msgsz:发送消息的长度,不含msgp指示消息类型的前4个字节;
- msgflg:指定发送消息的行为。
返回值:成功写入返回0,否则返回-1,错误信息位于errno中。
msgflg的取值可以是下面三者其一:
- 0:当消息队列满时阻塞函数,直到消息能写入队列;
- IPC_NOWAIT:消息队列满时,msgsnd()函数立即返回;
- IPC_NOERROR:如果发送的消息长度大于msgsz指示的长度,则做截断处理,截断部分将丢弃,且不通知发送消息的进程。
接收消息
msgrcv()函数用于从消息队列中接收消息,其函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv(int msqid, struct msgbuf* msgp, size_t msgsz, long msgtyp, int msgflg);
- msqid:消息队列标识;
- msgp:从消息队列接收消息的类型,须与msgsnd()函数发送消息的结构体类型一致;
- msgsz:接收消息的长度,不含msgp指示消息类型的前4个字节;
- msgtyp:指示接收消息的行为;
- msgflg:指示消息类型。
返回值:成功收到消息返回实际收到的消息长度,否则返回-1,错误信息位于errno中。
msgtyp的取值可以是下面三者其一:
- 0:接收队列中读端的第一条消息;
- >0:接收类型等于msgtyp的第一条消息;
- <0:接收类型等于msgtyp绝对值的第一条消息。
msgflg的取值可以是下面四者其一:
- 0:当消息队列满时阻塞函数,直到消息能写入队列;
- IPC_NOWAIT:消息队列满时,msgsnd()函数立即返回;
- IPC_EXCEPT:与msgtyp配合使用时,返回消息队列中第一条类型不为msgtyp的消息;
- IPC_NOERROR:如果发送的消息长度大于msgsz指示的长度,则做截断处理,截断部分将丢弃,且不通知发送消息的进程。
设置消息队列属性
msgctl()函数用于设置或获取共享内存的属性,其函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds* buf);
- msqid:消息队列标识,即msgget()函数的返回值;
- cmd:对消息队列的操作方式;
- buf:存放对消息队列操作的信息。
返回值:成功返回0,失败返回-1。
cmd可以是如下取值:
- IPC_RMID:删除消息队列;
- IPC_STAT:获取消息队列的状态;
- IPC_SET:改变消息队列的存取权限。
struct msqid_ds结构体定义如下:
struct msqid_ds {
struct ipc_perm msg_perm; // 存取权限
struct msg* msg_first; // 指向消息队列头的指针
struct msg* msg_last; // 指向消息队列尾的指针
__kernel_time_t msg_stime; // 消息队列最后发送时间
__kernel_time_t msg_rtime; // 消息队列最后接收时间
__kernel_time_t msg_ctime; // 消息队列最后修改时间
unsigned short msg_cbytes; // 当前消息队列的字节数
unsigned short msg_qnum; // 消息队列中的消息数
unsigned short msg_qbytes; // 消息队列的最大字节数
__kernel_ipc_pid_t msg_lspid; // 最后发送消息的进程ID
__kernel_ipc_pid_t msg_lrpid; // 最近接收消息的进程ID
};
其中struct ipc_perm结构体用于存放IPC对象标识和创建者的信息,定义如下:
struct ipc_perm {
ket_t key; // IPC对象标识
uid_t uid; // 用户ID
gid_t gid; // 用户组ID
uid_t cuid; // 创建用户ID
gid_t cgid; // 创建用户组ID
unsigned short mode; // 存取权限
unsigned short seq; // 序列号
};
共享内存(Shared Memory)
共享内存是内核中的一块存储空间,这块内存被映射至多个进程的虚拟地址空间。共享内存在不同进程虚拟地址空间中的映射地址未必相同。通过这块共享内存,不同进程之间可以交换数据,并且对共享内存内容的改变,也会立即在进程的虚拟地址空间中得到反映。
创建共享内存
shmget()函数用于创建一块共享内存,其函数原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int shmflg);
- key:用于区分不同共享内存段的参数(非0整数);
- size:共享内存大小;
- sgmflg:存取权限或创建条件。
返回值:成功返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数;失败返回-1。
如果key为IPC_PRIVATE,则内核创建一块新的共享内存;否则,如果key所对应的共享内存已存在,则返回该共享内存。shmflg用二进制来标识,其最小9位用于说明共享内存的存取权限,如果shmflg包含IPC_CREAT,则表示创建共享内存。
实际应用中,key可以通过ftok()函数来生成,其函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- pathname:文件或目录的路径,如果是文件,此文件必须存在且可存取;
- proj_id:根据自己约定可随意设置的一个序号,取值范围为[1,255]之间的整数。
返回值:成功返回key值(非0整数),否则返回-1。
ftok()函数的实质是把从pathname导出的信息与proj_id的低序8位组合成一个key,由于每个文件在Linux系统中的inode具有唯一性,所以可以使用pathname来生成key值。
共享内存映射的建立
shmat()函数用于将创建的共享内存映射至虚拟地址空间中的某个区间,其函数原型如下:
#include <sys/types.h>
#include <sys/shm.h>
void* shmat(int shmid, const void* shmaddr, int shmflg);
- shmid:共享内存标识,即shmget()函数的返回值;
- shmaddr:进程虚拟地址,通常为NULL,表示由系统决定对应的地址;
- shmflg:读写标志,如果指定了SHM_RDONLY位,则以只读的方式使用该共享内存区域,否则以读写的方式使用。
返回值:成功返回共享内存所映射的虚拟地址空间中的地址,否则返回-1。
共享内存映射的解除
shmdt()函数用于解除已有共享内存的映射关系,其函数原型如下:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void* shmaddr);
- shmaddr:共享内存首地址。
返回值:成功返回0,否则返回-1。
设置共享内存属性
shmctl()函数用于设置或获取共享内存的属性,其函数原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
- shmid:共享内存标识,即shmget()函数的返回值;
- cmd:对共享内存的操作方式;
- buf:存放对共享内存操作的信息。
返回值:成功返回0,失败返回-1。
cmd可以是如下取值:
- IPC_STAT:获取共享内存的状态;
- IPC_SET:设置共享内存的权限;
- IPC_RMID:删除共享内存;
- IPC_LOCK:锁定共享内存,使之不被置换;
- IPC_UNLOCK:解锁共享内存。
struct shmid_ds结构体定义如下:
struct shmid_ds {
struct ipc_perm shm_perm; // 存取权限
int shm_segsz; // 共享内存大小
__kernel_time_t shm_atime; // 共享内存最后映射时间
__kernel_time_t shm_dtime; // 共享内存最后解除映射时间
__kernel_time_t shm_ctime; // 共享内存最后修改时间
__kernel_ipc_pid_t shm_cpid; // 创建进程ID
__kernel_ipc_pid_t shm_lpid; // 最近操作进程ID
unsigned short shm_nattch; // 建立映射的进程数
};