原文地址:http://blog.youkuaiyun.com/colzer/article/details/8146138
Linux下的进程通信基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩 充,形成了“system V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来,如图示:

其中,最初Unix IPC包括:管道、FIFO、信号;System V IPC包括:System V消息队列、System V信号灯、System V共享内存区;Posix IPC包括: Posix消息队列、Posix信号灯、Posix共享内存区。有两点需要简单说明一下:1)由于Unix版本的多样性,电子电气工程协会(IEEE)开 发了一个独立的Unix标准,这个新的ANSI Unix标准被称为计算机环境的可移植性操作系统界面(PSOIX)。现有大部分Unix和流行版本都是遵循POSIX标准的,而Linux从一开始就遵 循POSIX标准;2)BSD并不是没有涉足单机内的进程间通信(socket本身就可以用于单机内的进程间通信)。事实上,很多Unix版本的单机 IPC留有BSD的痕迹,如4.4BSD支持的匿名内存映射、4.3+BSD对可靠信号语义的实现等等。
在文章《linux基础编程:进程通信之信号》和《linux基础编程:进程通信之管道》两篇文章中介绍了最初的Unix IPC通信机制。通过对这两种方式的理解,我们知道管道和信号都是随着进程持续而存在(IPC一直存在到打开IPC对象的最后一个进程关闭该对象为止),如果进程结束了,管道和信号都会关闭或者丢失。下面将会分别介绍基于System V IPC的通信机制:消息队列,信号灯,共享内存区。基于System V IPC的通信机制的特点是:它是随着内核的持续而存在(IPC一直持续到内核重新启动或者显示删除该对象为止)。本文将介绍System V IPC 在内核中实现的原理和以及相应的API,应用。
System V IPC原理
首先基于System V IPC的通信是基于内核来实现。首先我们来分析整个System V IPC的结构。在linux 3.6.5内核源码中我们可以在/include/linux/ipc_namespace.h文件中找到struct ipc_namespace这个结构体,该结构体是基于System V IPC 三种通信机制的命名空间或者说全局入口,在该结构体中定义了一个struct ipc_ids ids[3]结构体数组,关键的结构体代码如下:
- struct ipc_namespace {
- atomic_t count;
- struct ipc_ids ids[3];
- ...
- };
- struct ipc_ids {
- int in_use;
- unsigned short seq;
- unsigned short seq_max;
- struct rw_semaphore rw_mutex;
- struct idr ipcs_idr;
- };
- #define IPC_SEM_IDS 0
- #define IPC_MSG_IDS 1
- #define IPC_SHM_IDS 2
- #define msg_ids(namespace) ((namespace)->ids[IPC_MSG_IDS])
- #define sem_ids(namespace) ((namespace)->ids[IPC_SEM_IDS])
- #define shm_ids(namespace) ((namespace)->ids[IPC_SHM_IDS])
- struct kern_ipc_perm
- {
- spinlock_t lock;
- int deleted;
- int id;
- key_t key;
- uid_t uid;
- gid_t gid;
- uid_t cuid;
- gid_t cgid;
- umode_t mode;
- unsigned long seq;
- void *security;
- };
对于每一种IPC具体的条目中,struct kern_ipc_perm为相应条目的第一个元素,得到struct kern_ipc_perm的指针的头指针,就相当于得到相应条目的头指针,以消息队列为例子代码如下。struct kern_ipc_perm结构体中的key_t key为该条目的唯一的key标识符。struct kern_ipc_perm结构体中还定义对应的ipc的特征信息(uid用户ID等)。
- struct msg_queue {
- struct kern_ipc_perm q_perm;
- ....
- };
通过前面描述的内容,我们可以得到到每一个IPC条目的索引,下面我们将介绍具体的IPC条目的存储内容。
消息队列
- /* one msq_queue structure for each present queue on the system */
- struct msg_queue {
- struct kern_ipc_perm q_perm;
- time_t q_stime; /* last msgsnd time */
- time_t q_rtime; /* last msgrcv time */
- time_t q_ctime; /* last change time */
- unsigned long q_cbytes; /* current number of bytes on queue */
- unsigned long q_qnum; /* number of messages in queue */
- unsigned long q_qbytes; /* max number of bytes on queue */
- pid_t q_lspid; /* pid of last msgsnd */
- pid_t q_lrpid; /* last receive pid */
- struct list_head q_messages;
- struct list_head q_receivers;
- struct list_head q_senders;
- };
- /* one msg_msg structure for each message */
- struct msg_msg {
- struct list_head m_list;
- long m_type;
- int m_ts; /* message text size */
- struct msg_msgseg* next;
- void *security;
- /* the actual message follows immediately */
- };
- struct msgbuf {
- long mtype; /* type of message */
- char mtext[1]; /* message text */
- };
上面基本上介绍了整个消息队列的内存模型,对消息的操作也比较少,在include/linux/ipc.h文件中定义:
- #define MSGSND 11//发送消息到队列
- #define MSGRCV 12//从队列中接受消息
- #define MSGGET 13//打开或创建消息队列
- #define MSGCTL 14//控制消息队列
信号量
- 二值信号灯:最简单的信号灯形式,信号灯的值只能取0或1,类似于互斥锁。 注:二值信号灯能够实现互斥锁的功能,但两者的关注内容不同。信号灯强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
- 计算信号灯:信号灯的值可以取任意非负值(当然受内核本身的约束)。
- struct sem_array {
- struct kern_ipc_perm ____cacheline_aligned_in_smp
- sem_perm; /* permissions .. see ipc.h */
- time_t sem_otime; /* last semop time */
- time_t sem_ctime; /* last change time */
- struct sem *sem_base; /* ptr to first semaphore in array */
- struct list_head sem_pending; /* pending operations to be processed */
- struct list_head list_id; /* undo requests on this array */
- int sem_nsems; /* no. of semaphores in array */
- int complex_count; /* pending complex operations */
- };
- struct sem {
- int semval; /* current value */
- int sempid; /* pid of last operation */
- struct list_head sem_pending; /* pending single-sop operations */
- };
- /* semop system calls takes an array of these. */
- struct sembuf {
- unsigned short sem_num; /* semaphore index in array */
- short sem_op; /* semaphore operation */
- short sem_flg; /* operation flags */
- };
- /* arg for semctl system calls. */
- union semun {
- int val; /* value for SETVAL */
- struct semid_ds __user *buf; /* buffer for IPC_STAT & IPC_SET */
- unsigned short __user *array; /* array for GETALL & SETALL */
- struct seminfo __user *__buf; /* buffer for IPC_INFO */
- void __user *__pad;
- };
- #define SEMOP 1//改变信号量的值
- #define SEMGET 2//打开或者创建一个信号量
- #define SEMCTL 3//消息量控制
- #define SEMTIMEDOP 4//好像是内部使用吧,没有仔细去看
共享内存
采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]: 一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建 立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回 文件的。因此,采用共享内存的通信方式效率是非常高的。
- #define SHMAT 21//空间映射:把上面打开的内存区域连接到用户的进程空间中
- #define SHMDT 22//解除映射:将共享内存从当前进程中分离
- #define SHMGET 23//创建打开一个内存区域
- #define SHMCTL 24//内存区域的控制:包括初始化和删除内存区域。
API以及应用
上面从实现原理上对三种System V IPC进行介绍,我们发现其实三种通信机制和原理差不多,对其进行操作也不多,并且比较相似。下面我将介绍在用户空间通过相应的API函数来操作相应的IPC。
消息队列API
- #include <sys/types.h>
- #include <sys/ipc.h>
- #include <sys/msg.h>
- int msgget(key_t key, int msgflg);
- int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- <pre name="code" class="cpp">struct msgbuf {
- long mtype; /* type of message */
- char *mtext;
- };</pre>
- <pre></pre>
- <span></span>
- <div><span>在具体的应用中,可以自定义该结构体,只要第一个字段为一个long类型的消息类型,比如,如下的结构体:</span></div>
- <div><span></span><pre name="code" class="cpp">struct msgbuf {
- long mtype; /* type of message */
- int fromPID;
- int cmdID
- };</pre></div>
- msgsz为发送消息的内容的长度,注意:该长度不包括类型字段的一个long类型的大小,比如上面例子msgsz=sizeof(msgbuf)-sizeof(long)。
- <pre></pre>
- <pre></pre>
- <pre></pre>
- ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
- int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- IPC_STAT:检索当期当前消息队列的属性,返回的值储存在一个struct msqid_ds结构体中,该结构见下面。
- IPC_SET:如果进程有足够权限,可以利用buf来设置队列属性。
- IPC_RMID:用于删除队列。
- struct msqid_ds {
- struct ipc_perm
- {
- __kernel_key_t key;
- __kernel_uid_t uid;
- __kernel_gid_t gid;
- __kernel_uid_t cuid;
- __kernel_gid_t cgid;
- __kernel_mode_t mode;
- unsigned short seq;
- };
- struct msg *msg_first; /* first message on queue,unused */
- struct msg *msg_last; /* last message in queue,unused */
- __kernel_time_t msg_stime; /* last msgsnd time */
- __kernel_time_t msg_rtime; /* last msgrcv time */
- __kernel_time_t msg_ctime; /* last change time */
- unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
- unsigned long msg_lqbytes; /* ditto */
- unsigned short msg_cbytes; /* current number of bytes on queue */
- unsigned short msg_qnum; /* number of messages in queue */
- unsigned short msg_qbytes; /* max number of bytes on queue */
- __kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
- __kernel_ipc_pid_t msg_lrpid; /* last receive pid */
- };
信号量API
- P(sv):如果sv值是大于0,就给它减去1;如果它的值为0,就挂起进程的执行。
- V(sv):如果有其他进程因为等待sv而挂起,就让它恢复运行;如果没有因sv等待而挂起的进程,就对该信号量进行加1操作。
- #include <sys/types.h>
- #include <sys/ipc.h>
- #include <sys/sem.h>
- int semget(key_t key, int nsems, int semflg);
打开或者创建信号量:参数key是一个键值,唯一标识一个信号灯集,用法与msgget()中的key相同;参数nsems指定打开或者新创建的信号灯集中将包含信号灯的数目,一般情况下,都是取值为1;semflg参数是一些标志位。参数key和semflg的取值,以及何时打开已有信号灯集或者创建一个新的信号灯集与msgget()中的对应部分相同,不再祥述。该调用返回与健值key相对应的信号灯集描述字。调用返回:成功返回信号灯集描述字,否则返回-1。
- int semop(int semid, struct sembuf *sops, unsigned nsops);
- /* semop system calls takes an array of these. */
- struct sembuf {
- unsigned short sem_num; /* semaphore index in array */
- short sem_op; /* semaphore operation */
- short sem_flg; /* operation flags */
- };
- int semctl(int semid, int semnum, int cmd, ...);
- /* arg for semctl system calls. */
- union semun {
- int val; /* value for SETVAL */
- struct semid_ds __user *buf; /* buffer for IPC_STAT & IPC_SET */
- unsigned short __user *array; /* array for GETALL & SETALL */
- struct seminfo __user *__buf; /* buffer for IPC_INFO */
- void __user *__pad;
- };
共享内存API
- #include <sys/types.h>
- #include <sys/shm.h>
- int shmget(key_t key, size_t size, int shmflg);//创建共享内存
- void *shmat(int shmid, const void *shmaddr, int shmflg);//映射到自己的内存空间
- int shmdt(const void *shmaddr);//解除映射
- int shmctl(int shmid, int cmd, struct shmid_ds *buf);//控制共享内存
sheget为创建或者打开一个共享内存,成功就返回相应的共享内存标识符,否则就返回-1。shmflg低端9位为权限标志,利用共享内存进行通信时候,可以利用该标志对共享内存进行只读,只写等权限控制。
shmat为空间映射。通过创建的共享内存,在它能被进程访问之前,需要把该段内存映射到用户进程空间。shmaddr是用来指定共享内存映射到当前进程中的地址位置,要想该设置有用,shmflg必须设置为SHM_RND标志。大部分情况下,应该设置为为空指针(void *)0。让系统自动选择地址,从而减小程序对硬件的依赖性。shmflg除了上面的设置以外,还可以设置为SHM_RDONLY,使得映射过来的地址只读。如果函数调用成功,返回映射的地址的第一个字节,否则返回-1。
shmdt用于解除上面的映射。
shmctl用于控制共享内存,相比上面几个控制函数,这里的比较简单,明确的三个参数。struct shmid_ds定义在include/linux/shm.h,如下。cmd有IPC_STAT,IPC_SET,IPC_RMID含义和消息队列一样的。好了。好像很简单一样。。。。
- struct shmid_ds {
- struct ipc_perm
- {
- __kernel_key_t key;
- __kernel_uid_t uid;
- __kernel_gid_t gid;
- __kernel_uid_t cuid;
- __kernel_gid_t cgid;
- __kernel_mode_t mode;
- unsigned short seq;
- };
- int shm_segsz; /* size of segment (bytes) */
- __kernel_time_t shm_atime; /* last attach time */
- __kernel_time_t shm_dtime; /* last detach time */
- __kernel_time_t shm_ctime; /* last change time */
- __kernel_ipc_pid_t shm_cpid; /* pid of creator */
- __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
- unsigned short shm_nattch; /* no. of current attaches */
- unsigned short shm_unused; /* compatibility */
- void *shm_unused2; /* ditto - used by DIPC */
- void *shm_unused3; /* unused */
- };