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的结构。在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; };每一个struct ipc_ids结构体对应System V IPC 每一种通信机制,struct ipc_ids ids[3]就对应了三种IPC(msg_ids消息队列,sem_ids信号量,shm_ids共享内存区)。通过下面宏定义可以分别得到三种IPC结构体:
#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 ipc_ids结构体对应System V IPC 每一种通信机制,struct ipc_ids结构体中struct idr结构体记录了该IPC所有条目(比如:如果是消息队列,此时idr中记录了系统中当前所有消息队列的信息)。在文件/include/linux/idr.h中定义struct idr结构体,它是一种类似数组的内存区域。在IPC通信中,我们把该数组的每一项条目存储内容为struct kern_ipc_perm的结构体的指针。通过/ipc/util.c文件中的int ipc_addid(struct ipc_ids* ids, struct kern_ipc_perm* new, int size)函数,可以把struct kern_ipc_perm结构体指针添加到相对应的struct ipc_ids的struct idr中,此时struct kern_ipc_perm*就指向相应的IPC的一个条目,其结构体定义如下:
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; };每一个消息队列包括了该队列的基本信息,struct list_head 类型的消息队列,以及当前出于阻塞状态的消息接受者和发送者。对于q_messages队列来说,每一个元素都为struct msg_msg类型:
/* 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 */ };该处采用了文章 《 通过看linux环境相关源码学习编程》中提到的第四条的方法来存储具体的消息内容,该结构体用于内核部分存储消息。在用户空间的代码发送和接受到消息为一个如下的简单的结构体:
struct msgbuf { long mtype; /* type of message */ char mtext[1]; /* message text */ };mtype成员代表消息类别,从消息队列中读取消息的一个重要依据就是消息的类型;mtext是消息内容,当然长度不一定为1。因此,在用户空间里,对于发送消息来说,首先预置一个msgbuf缓冲区并写入消息类型和内容,调用相应的发送函数即可;对读取消息来说,首先分配这样一个msgbuf缓冲区,然后把消息读入该缓冲区即可。
#define MSGSND 11//发送消息到队列 #define MSGRCV 12//从队列中接受消息 #define MSGGET 13//打开或创建消息队列 #define MSGCTL 14//控制消息队列第二节将详细介绍消息队列的操作。
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 *sem_base为信号量列表的头指针。struct sem是一个简单的数据结构:
struct sem { int semval; /* current value */ int sempid; /* pid of last operation */ struct list_head sem_pending; /* pending single-sop operations */ };它维持一个当前值,最后操作的进程ID以及一个阻塞队列。在用户空间可以通过struct sembuf对sem中的信号量的值进行改变(SETVAL操作)或者通过通过联合体union semun对整个信号量进行改变(IPC_STAT SETVAL等操作),两个结构分别如下:
/* 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; };和消息队列一样,在在include/linux/ipc.h文件中定义相应的操作:
#define SEMOP 1//改变信号量的值 #define SEMGET 2//打开或者创建一个信号量 #define SEMCTL 3//消息量控制 #define SEMTIMEDOP 4//好像是内部使用吧,没有仔细去看
#define SHMAT 21//空间映射:把上面打开的内存区域连接到用户的进程空间中 #define SHMDT 22//解除映射:将共享内存从当前进程中分离 #define SHMGET 23//创建打开一个内存区域 #define SHMCTL 24//内存区域的控制:包括初始化和删除内存区域。一般对内存区域的操作是先打开-》映射-》(操作)-》(控制)-》解除映射。
上面从实现原理上对三种System V IPC进行介绍,我们发现其实三种通信机制和原理差不多,对其进行操作也不多,并且比较相似。下面我将介绍在用户空间通过相应的API函数来操作相应的IPC。
#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);发送消息到消息队列:msgid为msgget函数返回队列标识符,已经不是队列Key了。msgp为一个具体的消息内容,它指向一个struct msgbuf类型的结构体:
struct msgbuf { long mtype; /* type of message */ char *mtext; };在具体的应用中,可以自定义该结构体,只要第一个字段为一个long类型的消息类型,比如,如下的结构体:msgsz为发送消息的内容的长度,注意:该长度不包括类型字段的一个long类型的大小,比如上面例子msgsz=sizeof(msgbuf)-sizeof(long)。struct msgbuf { long mtype; /* type of message */ int fromPID; int cmdID };
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);从消息队列中读取消息:前三个参数和msgsnd一样,这里就不描述了。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
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 */ };由于该结构体比较复杂,在实际开发过程中,特别在进行IPC_SET操作时候,正确的办法是先调用IPC_STAT得到这样一个结构体,然后再修改该结构体,最后再调用IPC_SET操作。
#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);对信号量进行PV操作:semid为semget返回的信号量描述符。我们知道在打开或者创建的时候,如果nsems参数不为1,此时semid指向的是一个信号量集,而不是单独的一个信号量。因此每次对该信号集进行操作时候必须指定需要操作的信号量数目,即nsops大小。struct sembuf *sops指向的是一个struct sembuf结构体数组,数组大小即为nsops。如果我们的信号量集只有一个信号量,此时,nsops=1,我们的sops就直接指向一个struct sembuf类型的指针。下面主要介绍一下struct sembuf数据结构,在上面原理部分已经给出该结构体的定义,为了描述,重复给一次了:
/* 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 */ };该结构比较简单,semnum是当前需要操作的信号量在信号集中编号,从0开始。
int semctl(int semid, int semnum, int cmd, ...);信号量控制函数:semnum为需要控制的信号量在信号集中的编号,如果信号集只有一个元素,该值为0。cmd为控制类型,对于有些操作,需要第四个参数,即为一个union semun联合体,根据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; };cmd=SETVAL:用于把信号量初始化为一个已知的值,用于第一次使用该信号量时,完成信号量值的初始化。此时使用的是union semun 中val字段。
#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 */ };