Linux/UNIX之进程间的通信(2)
进程间的通信(2)
有三种IPC我们称为XSI IPC,即消息队列、信号量以及共享存储器,它们之间有很多相似之处。
标识符和键
每个内核的IPC结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符加以引用。例如,为了对一个消息队列发送或取消息,只需要知道其队列标识符。与文件描述符不同,IPC标识符不是小的整数。当一个IPC结构被创建,以后被删除时,与这种结果相关的标识符连续加1,知道达到一个整型数的最大值,然后又回到0。
标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上会和,需要提供一个外部名方案。为此使用了键,每个IPC对象都与一个键相关联,于是键就用作该对象的外部名,无论何时创建IPC结构,都应该指定一个键,键的数据类型是基本系统数据类型key_t,通常在头文件<sys/types.h>中被定义为长整型。键由内核转换成标识符。
消息队列
消息队列是消息的连接表,存放在内核中并由消息队列标识符标识。我们把消息队列成为队列,其标识符为队列ID。
消息队列提供了一种子两个不相关的进程之间传递数据相当简单而且有效的方法。与命名管道相比,消息队列的优势在于,它独立于发送和接受进程而存在,这消除了在同步命名管的打开和关闭时可能产生的一些困难。
消息队列函数的定义如下:
#include <sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgctl(intmsqid, int cmd, struct msqid_ds *buf);
int msgget(key_tkey, int msgflg);
int msgsnd(intmsqid, const void *msgp, size_t msgsz, int msgflg);
ssize_tmsgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgget用于创建一个新的队列或打开一个现存的队列,程序提供一个兼职来命名某个特定的消息队列,第二个参数msgflg由九个标志组成,返回队列标识符。
msgsnd将消息添加到队列尾端。每个消息包含一个正长整型类型字段,一个非负长度以及实际数据字节,所有这些都在将消息添加到队列时,传送给msgsnd。msgp参数指向一个长整型,它包含了整型消息类型,再其后紧跟数据。若发送的最长消息是512字节,可定义下列结构,于是msgp就是指向msgbuf结构的指针。msgflag可以设置为IPC_NOWAIT,类似于I/O的非阻塞I/O标志。
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[512]; /* message data */
};
msgrcv用于从队列中取消息。我们并不一定要以先进先出次序取消息,也可以按消息的类型字段取消息。第二个参数是指向准备接收消息的指针;第四个参数指定接收优先级,若值为0,就获取队列中第一个消息,若大于0获取具有相同消息类型的第一个消息,若小于0获取消息类型小于或等于msgtyp的绝对值的第一个消息。
msgctl函数可以对队列执行多种操作。
信号量
信号量是一个计数器,用于多进程对共享数据对象的访问。为了获得共享资源,进程需要执行下列操作:
1. 测试控制该资源的信号量
2. 若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示它使用了一个资源单位
3. 若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0。进程被唤醒后,它返回第1步。
当进程不在使用由一个信号量控制的共享资源时,该信号量值增1。如果有进程正在休眠等待此信号量,则唤醒它们。
为了正确实现信号量,信号量值的测试和减1操作应当是原子操作。为此,信号量通常在内核中实现。
最简单的信号量是只能取值为0和1的变量,即二进制信号量(二元信号量)。
PV操作的定义非常简单。假设有一个信号量变量sv,则这两个操作定义如下:
P(sv):如果sv的值大于零,就给它减去1;如果它的值等于0,就挂起该进程的运行。
V(sv):如果有其他进程因等待sv而被挂起,就让他恢复运行;如果没有进程因等待sv而被挂起,就给它加1。
但信号量集与普通信号量相比要复杂得多:
1. 信号量集并非单个非负值,而必须将其定义为含有一个或多个信号量的集合。当创建一个信号量集时,要指定该集合中信号量的数目
2. 创建信号量集(semget)与对其赋初值(semctl)分开。这是一个致命弱点,因为不能原子地创建一个信号集,并对该集合中的各个信号量赋初值
3. 即使没有使用信号量集,他们仍然存在。有些程序在终止时并没有回访已经分配给他们的信号量集,所以我们不得不为此次担心。
#include <sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semget(key_t key, int nsems, intsemflg);
semget函数的作用是创建一个新信号量或取得一个已有信号量的健。
#include <sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semop(int semid, struct sembuf*sops, unsigned nsops);
semop函数用于改变信号量的值。它的第一个参数sem_id是由semget返回的信号量标识符。第二个参数是一个指向饥饿数组的指针,每个素组成员至少包含以下成员:
struct sembuf{
unsigned short sem_num; /*semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
第一个成员sem_num是信号量编号,除非你需要使用一组信号量,否则它的取值一般为0。第二个成员sem_op成员的值是信号量在一次操作中需要改变的数值。通常只会用到两个值,一个是-1,也就是P操作,它等待信号量变为可用;一个是+1,也就是V操作,它发送信号表示信号量现在可用。最后一个成员sem_flag通常被设置为SEM_UNDO。它将使得操作系统跟踪当前进程对这个信号量的修改情况,如果该进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量。
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semctl(int semid, int semnum, intcmd, ...);
semctl函数允许我们直接控制信号量信息,sem_num参数是信号量编号,一般为0。cmd参数是即将采取的动作。
共享内存
共享内存允许两个不相关的进程访问同一个逻辑内存。共享内存为在多个进程之间共享和传递数据提供了一种有效的方式。由于它并未提供同步机制,所以我们通常需要其他的机制来同步对共享内存的访问。共享内存是由IPC为进程创建的一个特殊的地址范围,它将出现在该进程的地址空间中。其他进程可以同一段共享内存连接到它们自己的地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是由malloc分配的一样。如果一个进程向共享内存写入数据,所做的改动将立刻可以访问同一段共享内存的任何其他进程看到。
共享内存使用的函数如下:
#include<sys/types.h>
#include<sys/shm.h>
void *shmat(intshmid, const void *shmaddr, int shmflg);
int shmdt(constvoid *shmaddr);
#include<sys/ipc.h>
#include<sys/shm.h>
int shmctl(intshmid, int cmd, struct shmid_ds *buf);
int shmget(key_tkey, size_t size, int shmflg);
我们需要用shmget函数创建共享内存:第一个参数作为键值;第二个参数是以字节为单位指定需要共享的内存容量;第三个参数shmflag包含九个比特的权限标志,权限标志允许一个进程创建的共享内存可以被共享内存的创建者拥有的进程写入,同时其他用户创建的进程只能读取该共享内存。
第一次创建共享内存时,它不能被任何进程访问,要想启用对该共享内存的访问,必须将其连接到一个进程的地址空间中。这项工作由shmat函数完成。第一个参数是共享内存标识符;第二个参数shmaddr指定的是共享内存连接到当前进程中的地址位置。通常是一个空指针,表示让系统选择共享内存出现的地址。第三个参数shmflg是一组位标志。它的两个取值可能是SHM_RND(这个标志与shm_addr联合使用,用来控制共享内存联结的地址)和SHM_RDONLY(它使得连接的内存只读)。
shmdt函数作用是将共享内存从当前进程中分离,参数是shmat返回的指针。
shmctl的第二个参数是要采取的动作;第三个参数是一个指针,指向共享内存模式和访问权限的结构。