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
#include
#include
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;
};
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
#include
#include
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
#include
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 */
};