进程通信(2)

在Unix的System V版本中,增加了三种新的进程通信机制:消息队列、信号量、共享内存。

2、Linux系统也支持这三种通信机制,并将它们统称为IPC对象。每一个IPC对象都有一个唯一的标识号(描述符),系统内核通过使用该标识号来存取各个IPC对象。Linux系统中所有IPC对象的数据结构中都包含一个ipc_perm结构,它包含IPC对象的关键字、进程拥有者和创建者的用户标志及组标志符、存取模式以及序列号,在include/linux/ipc.h中定义具体如下:

struct ipc_perm{
    key_t key;    //关键字
    ushort uid;    //拥有者
    ushort gid;
    ushort cuid;    //创建者
    ushort cgid;
    ushort mode;    //存取模式
    ushort seq;     //序列号
}

IPC对象关键字不仅是获取标志号的依据,也是客户进程和服务器进程联系的纽带。IPC关键字与标志号是两个不同的概念,关键字有两种:公有和私有。如果是公有的,所有进程通过权限检查后,均可以找到IPC对象的标识号。需要强调的是,系统是通过标识号来访问IPC对象的。

1.消息队列:系统内核地址空间中的一个内部链表,它允许一个活多个进程将消息发送到消息队列中,也允许一个或多个进程从消息队列中读出消息。每个消息队列都有一个唯一的标识号表示。

在Linux系统中,每一个消息队列都用一个msgid_ds结构进行描述,系统为所有的消息队列维护一个msgque链表,该表的第一个元素是指向msqid_ds结构的指针。消息队列的组织结构如下:

image

消息队列的操作:
1)创建消息队列:int msgget(key_t kty, int msgflag),第一个是关键值,系统将它与内核中的已有的每一个消息队列的关键值做比较。消息队列的打开和存取方式取决于第二个参数msgflag。返回消息队列的标识号。
2)发送消息:int msgsnd(int msgid,struct msgbuf *msgp,int msgsz,int msgflag),第一个参数是消息队列的标识号,指定消息发往何处;第二个参数是指向消息缓冲结构的指针;第三个参数是消息的大小;第四个参数可以设置为0,此时该参数将被忽略或设置为IPC_NOWAIT,分别对应于消息队列满时采取阻塞或不作处理继续执行两种情况。如果权限和资源允许,系统则将消息复制到msg结构中,并挂入到消息队列的尾部。
3)接受消息:int msgrcv(int msgid,struct msgbuf *msgp,int msgsz,long msgtyp,int msgflg)
如果成功,返回实际读出的字节数,否则返回-1以及errno(表示失败的原因),消息缓冲结构包括消息类型和消息正文两部分,第四个参数表示请求读取的消息类型;第五个参数表示读消息标志。
4)消息队列的操纵int msgctl(int msgid,int cmd,struct msgid_ds *buf),成功返回0。cmd有三种操作:IPC_STAT(获取消息队列信息,保存在buf指向的msgid结构中)、IPC_SET(用buf指向的msgid设置消息队列的属性)、IPC_RMID(删除消息队列)。
2.信号量,是一种对资源进行访问控制的机制,它对应于一个整数值,该值可以被一个或多个进程所检测和设置,这种检测具有原子性,执行期间不可中断。作为一种锁定机制,信号量可以用于多个进程对共享资源的存取。

为完整描述IPC信号量对象,Linux为每个信号量对象建立了一个数据结构,即semid_ds结构。为了管理各个信号量对象,系统设置了一个名为semary的指针数组,保存了系统中所有semid_ds结构的地址。每个信号量对象管理着一组信号量构成的集合sem_base、该集合上的操作序列sem_pending和撤销操作序列undo。
信号量对象也可称为信号量集,它管理着由多个信号量构成的一个数组,每个元素都是一个sem结构,在linux/sem.h中定义如下:

struct sem{
    ushort semval;    //信号量的当前值
    short sempid;    //在信号量上最后一次操作的进程
    ushort semncnt;    //等待信号量增加的进程数
    ushort semzcnt;    //等待信号量值为0的进程数
}

信号量对象由semid_ds结构描述,结构如下:

struct semid_ds{
    struct ipc_perm sem_perm;    //各个进程对信号量对象的操作权限,IPC对象都有
    time_t sem_otime;    //记录最后一次对信号量操作的时间
    time_t sem_ctime;    //最后一次对信号量对象的修改时间
    struct sem *sem_base;    //指向信号量集中的第一个元素
    struct sem_queue *sem_pending;    //信号量队列,指向被挂起的操作
    struct sem_queue *sem_pending_last;    //上述队列的队尾
    struct sem_undo *undo;    //指向信号量集上待撤销操作的一个队列
    ushort sem_nsems;    //信号量的个数
}

信号量集的操作
1)创建新的信号量集或获取已存在的信号量集:int semget(key_t key,int nsems,int semflg),返回IPC标志号。第二个参数说明信号量的数量;第三个参数确定如何打开或存取信号量集。
2)信号量的操作执行:int semop(int semid,struct sembuf *sops,unsigned nsops),第二个参数指向信号量集上要执行的操作数组;第三个参数是数组中操作的个数。
sops数组中,元素均为sembuf结构,在linux/sem.h中定义如下:

struct sembuf{
    ushort sem_num;    //信号量集合中的信号量编号,0代表第1个信号量
    short sem_op;    //信号量操作值,P/V
    short sem_flg;    //操作标志
}

3)信号量的控制操作:int semctl(int semid,int semnum,int cmd,union semun arg),第三个参数表示信号量集上要执行的命令,取值包括:GETVAL、GETALL、SETVAL、SETALL、GETPID、GETNCNT、GETZCNT、IPC_STAT、IPC_SET、IPC_RMID;第四个参数是个联合体,根据cmd决定选择它的哪个成员,用于存放操作得到的结果或提供操作所需的参数。
4)死锁:信号量的使用可能会引起死锁。不仅信号量操作的顺序不当会产生死锁,有时进程的执行的夭折也会引起死锁。当某个进程修改了信号量并成功进入临界区之后,该进程因为执行失败或被终止,无法退出临界区,导致信号量永远无法释放,这就形成了死锁。为此,Linux系统设置了undo结构来解决此问题。具体做法是:首先将信号量进行操作前的状态保存到信号量集semid_ds的sem_undo和进程结构task_struct的sem_undo结构中;当对信号量进行操作时,将信号量的变化值的相反数保存到sem_undo结构中;进程结束时,系统遍历进程sem_undo集合中的所有信号量数据,使用调整值进行还原。

3.共享内存,是系统开辟的一个内存区域,系统将该区域映射到多个进程的虚拟地址空间中,使得这些进程都可以共享该存储区域,通过对共享存储区的读/写,实现彼此之间的信息交换。共享内存机制是目前速度最快的通信机制。需要注意的是:共享内存机制只是提供了进程相互通信所需的共享存储区域以及该区域进行操作的手段,但未提供对该区域进行互斥访问和进程同步的设置,因此,需要用户自行实施,以保证通信正常进行。

共享内存涉及到进程管理和存储管理两方面的知识,涉及多种数据结构。这里只介绍共享内存段对象的数据结构shmid_ds,在linux/shm.h中如下:

struct shmid_ds{
    struct ipc_perm;    //各进程对共享内存段的权限
    intshm_segsz;    //共享内存段的字节数
    time_t shm_atime;    //最后一个进程附加到该共享内存段的时间
    time_t shm_dtime;    //最后一个进程离开该共享内存段的时间
    time_t shm_ctime;    //最后一次修改本结构的时间
    unsigned short shm_cpid;    //创建该共享内存段的进程的标识符
    unsigned short shm_lpid;    //最后一次使用该共享内存段的进程的标识符
    short shm_nattach;    //使用该共享内存区域的进程个数
    unsigned short shm+npages;    //该共享内存段的页面数
    unsigned long *shm_pages;    //共享页面的页表项的地址指针
    struct vm_area_struct *attaches;    //对各个共享段的描述
}

共享内存段的操作:
1)共享内存段的建立:int shmget(key_t key,int size,int shmflag),返回共享内存段对象的标志号。
2)共享内存段的附接:char *shmat(int shmid,char *shmaddr,int shmflg),成功返回附接到进程虚地址空间的那个共享内存段的地址。第二个参数表示附接成功后,在进程虚拟地址空间中的起始(虚拟)地址;第三个参数表示进程对附接的共享内存段的操作方式,如只读、读写等。
3)共享内存段与进程虚地址空间的断接:int shmdt(char *shmaddr),参数是指共享内存的起始地址,是由shmat()函数返回的。
4)共享内存段的操纵:int shmctl(int shmid,int cmd,struct shmid *buf),第二个参数包括:IPC_STAT(将共享内存段的shmid_ds结构的数据复制到buf所指的地址中)、IPC_SET(从buf所指结构读数据,修改shmid_ds中的ipc_term域的值)、IPC_RMID(移除)。

posted @ 2014-01-12 14:24  侯凯  阅读(437)  评论(0编辑  收藏  举报