首页 > 技术文章 > system v进程间通信整理

leijiangtao 2014-10-30 11:54 原文

key_t键和ftok函数
三种类型的system v IPC使用key_t值作为他们的名字。头文件<sys/types.h>
把key_t这个数据类型定义为一个整数,它通常是一个至少32位的整数,这些整数通常是由ftok函数赋予的。
函数ftok()把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键。
#include<sys/ipc.h>
key_t ftok(const char *pathname,int id);
返回:若成功则为IPC键,若出错则返回-1;
如果pathname不存在,或者对于调用进程不可访问,ftok就返回-1;
********************************************************************************************
创建与打开IPC通道
创建或者打开一个IPC对象的三个getxxx函数的第一个参数key是类型为key_t的IPC键,返回值identifier
是一个整数标识符。该标识符不同于ftok函数的id参数,我们不久就会看到的。对于key值,应用程序有两种
选择。
(1)调用ftok,给他传递pathname和id.
(2)指定key为IPC_PRIVATE,这将保证会创建一个新的唯一的IPC对象!
所有三个getxxx函数都有一个oflag的参数,它指定IPC对象的读写权限位,并选择是创建一个新的IPC对象还是
访问一个已存在的IPC对象。
选择规则如下:
1>指定key为IPC_PRIVATE能保证创建一个唯一的IPC对象。没有一对id和pathname的组合会
导致ftok产生IPC_PRIVATE这个键值。
2>设置oflag参数的IPC_CREAT位但不设置IPC_EXCL位时,如果指定的IPC键值对象不存在,那就创建一个新的对象,否则返回该对象!
3>同时设置oflag的IPC_CREAT和IPC_EXCL位时,如果所指定的IPC对象不存在,那就创建一个新的对象,否则返回EEXIST
错误,因为该对象已存在。对IPC对象来说,IPC_CREAT和IPC_EXCL的组合和open函数的O_CREAT和O_EXCL的组合类似。
4>oflag无特殊标志(我的理解为0),若key(ftok的返回值)已存在,成功,引用已存在对象!
oflag无特殊标志(我的理解为0),若key(ftok的返回值)不存在,出错,errno=ENOENT!
注意若果只有IPC_CREAT而没有IPC_EXCL标志的那一行,我们的不到一个指示以判断是创建了一个新的对象,还是引用了一个已存在的对象。
大多数应用程序中,由服务器创建IPC对象并指定IPC_CREAT标志(如果他不关心该对象是否存在,)或者IPC_CREAT|IPC_EXCL标志(如果他需要检查
该对象是否已经存在)。客户则不指定其中任何一个标志(他们假定服务器已经创建了该对象)。
**************************************************************************************
system v消息队列的函数
msgget函数用于创建一个新的消息队列或者访问一个已存在的消息队列。
#include<sys/msg.h>
int msgget(key_t key,int oflag)
返回:成功则为非负的标识符,若出错则为-1;
返回值是一个整数标识符,其他三个msg函数就用来指代该队列。他是基于指定的key产生的,而key既可以是
ftok的返回值,也可以是常值IPC_PRIVATE,我认为还有一种可以是任意一个正整数;
msgsnd函数打开一个消息队列后,使用msgsnd往其上放置一个消息。
#include<sys/msg.h>
int msgsnd(int msqid,const void *ptr,size_t length,int flag);
返回:若成功则为0,若出错则为-1
其中msqid是由msgget返回的标识符。ptr是一个结构体指针,该结构具有如下模板,它定义在<sys/msg.h>中。
struct msgbuf{
long mtype;/*message type,must be>0*/
char mtext[1];message data;
};
消息类型必须大于0,因为对于msgrcv函数来说,非正的消息类型用作特殊的指示器。
msgbuf结构定义的名字mtext不大确切,消息的类型并不限于文本。任何形式的数据都是允许的。ptr指向的只是一个含有
消息类型的长整数,消息本身跟在他的后面。
举例说明如某个应用需要交换由一个16整数后跟一个8字节的字符数组组成的消息,那他定义自己的结构如下:
#define MY_DATA 8
typedef struct my_msgbuf{
long mtype;//message type
int16_t mshort;//start of message data;
char mchar[MY_DATA];
}message;
msgsnd的length参数以字节为单位指定待发送消息的长度。这是位于长整数消息类型之后的用户
自定义数据的长度。长度可以为0。在刚给出的例子中,长度可以递减成sizeof(message)-sizeof(long).
flag参数既可以为为0,也可以是IPC_NOWAIT.IPC_NOWAIT标志使得msgsnd调用非阻塞(nonblocking):如果没有
存放新消息的可用空间,该函数立马返回。这个条件可能发生的情况包括:
1>在指定的队列中已有太多的字节
2>在系统范围存在太多的消息。
如果这两种条件中有一个存在,而且IPC_NOWAIT标志已指定,msgsnd就返回一个EAGIAN错误。
如果这两种条件中有一个存在,但是IPC_NOWAIT标志未指定,那么调用线程被投入睡眠,直到:
1>具备存放新消息的空间;
2>由msqid标识的消息队列从系统中删除;
3>调用线程被某个捕获的信号所中断;
msgrcv函数从某个消息队列中读取一个消息。
#include<sys/msg.h>
ssize_t msgrcv(int msqid,void *ptr,size_t length,long type,int flag);
返回:若成功则为读入缓冲区中数据的字节数,如出错则为-1;
其中ptr参数指定所接收消息的存放位置。跟msgsnd一样,该指针指向紧挨在真正消息数据之前返回的长整数类型字段。
length指定了由ptr指向的缓冲区中数据部分的大小。这是该函数能返回的最大数据量。该长度不包括长整形类型字段。
type指定希望从所给定的队列中读取什么样的消息。
1>如果type为0,那么就返回该队列中的第一个消息。既然每个消息队列都是作为一个FIFO(先进先出)链表维护的,
因此type为0指定返回该队列最早的消息。
2>type如果大于0,那么就返回其类型值为type的第一个消息。
3>如果type小于0,那么就返回类型值小于或者等于type参数的绝对值的消息类型中最小的第一个消息。
msgrcv的flag参数指定了所请求类型的消息不在所指定的消息队列中该如何处理。在没有消息可得的情况下,如果设置了
flag中的IPC_NOWAIT位,msgrcv函数会立即返回一个错误,否则,调用者被阻塞知道下列事件发生为止:
1>有一个所请求类型的消息可获取;
2>由msqid标识的消息队列从系统中删除;
3>调用线程被某个捕获的信号所中断;
flag参数中另外有一位可以指定:MSG_NOERROR。当所接收消息的真正数据部分大于length参数时,如果设设置了该位,msgrcv函数就只是截取数据部分,而不返回错误!
成功返回时,msgrcv返回的是所接收消息队列中数据的字节数。它不包括也通过ptr参数返回的长整数消息类型所需的几个字节!
msgctl函数提供一个消息队列上的各种控制操作。
#include<sys/msg.h>
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
返回:若成功则为0,若出错则为-1;
msgctl函数提供了3个命令。
IPC_RMID 从系统中删除由msqid指定的消息队列。当前在队列上的任何消息都将被丢弃。对于该命令而言,msgctl函数的第三个参数被忽略!
*****************************************************************************************
system v共享内存的函数
#inlucde<sys/shm.h>
int shmget(key_t,size_t size,int oflag);
返回:若成功则为共享内存对象区对象,若出错则为-1;
返回值是一个称为共享内存区标识符(shared memory indentifier)的整数,其他三个shmXXX函数
就用来指代这个内存区;
key可以是ftok的返回值,也可以是IPC_PRIVATE;
size为以字节为单位指定内存区的大小。当实际操作为创建一个新的共享内存区时,
必须制定一个不为0的size值。如果实际操作为访问一个已存在的共享内存区,那么size应为0;
oflag是读写权限的组合。他还可以与IPC_CREAT或与IPC_CREAT |IPC_EXCL按位或;
当实际操作为创建一个新的共享内存区时,该内存区被初始化为size字节的0.
注意:shmget创建或者打开一个共享内存区,但并没有给调用进程提供访问内存区的手段。这是
shmat函数的目的。
由shmet创建或者打开一个共享内存区后,通过调用shmat把它附接到调用进程的地址空间。
void *shmat(int shmid,const void *shmaddr,int flag);
返回:若成功则为映射区的起始地址,若出错则为-1 ;
其中shmid是由shmget返回的标识符。shmat的返回值是所指定的共享内存区在调用进程内的起始地址。
确定这个地址的规则如下。
如果shmaddr是一个空指针,那么系统替调用者选择地址。这是推荐的方法,有利于移植;
如果shmaddr是一个非空指针,那么返回返回地址取决于是否给flag参数指定了SHM_RND值:等等。。。
默认情况下,只要调用进程具有某个共享内存区的读写权限,它附接该内存区后就能够同时读写该内存区。
flag参数中也可以指定SHM_RDONLY值,他限于只读访问!
int shmdt(const void *shmaddr);
//返回:若成功则为0,若出错则为-1
当一个进程终止时,他附接着的所有内存共享区都自动断接掉。
注意本函数调用并不删除所指定的共享内存区。这个删除工作通以IPC_RMID命令调用shmctl完成;
int shmctl(int shmid,int cmd,struct shmid_da *buf);
返回:若成功则为0,若出错则为-1;该函数提供了3个命令
IPC_RMID 从系统中删除由shmid标识的共享内存区并拆除它!等等。。。其他命令既不说了。
***********************************************************************************
system v 信号量
1>二值信号量(binary semaphore):其值或为0或为1的信号量。这与互斥锁类似,若资源被锁住则信号量值为0,若资源可用
则信号量值为1;
接着把这种信号扩展为
2>计数信号量(counting semaphore):其值在0和某个限制值(对于posix信号量,该值必须至少为32767)之间的信号量。
这两种类型的信号量中,等待(wait)操作都等待信号量的值变为大于0,然后将它减1.挂出(post)操作则只是将信号量
值加1,从而唤醒正在等待该信号量值变为大于0的任意线程。
system v信号量通过定义如下概念给信号量增加了另一级复杂度。
计数信号量集(set of counting semaphores):一个或者多个信号量(构成一个集合),其中每个都是计数信号量。每个集合
的信号量数存在一个限制,一般在25个的数量级上。当我们谈及system v信号量时,所指的是计数信号量集。当我们谈及
posix信号量时,所指的是单个计数信号量。
semget 函数创建信号量集或访问一个已存在的信号量集。
#include<sys/sem.h>
int semget (key_t key,int nsems,int oflag)
返回:若成功则为非负标识符,若出错则为-1;
返回值是一个称为信号量标识符(semaphore identifiler)的整数,semop和semctl函数将使用他。
nsems参数指定集合中信号量的数。如果我们不创建一个新的信号量集,而是访问一个已存在的集合
,那就可以那参数指定为0.
oflag值是SEM_R,SEM_A常值的组合。其中R代表读,A代表改(ALTER),它们还可以与IPC_CREAT
或者IPC_CREAT|IPC_EXCL按位或。
与该集合中每个信号量关联的每个sem结构并不初始化,这些结构是在以SET_VAL或者SETALL命令
调用semctl时初始化的。
信号量值得初始化问题
unix98明确的陈述semget并不初始化每个信号量的值,这个初始化必须通过SET_VAL命令
(设置集合中的一个值)或者SETVAL命令(设置集合中所有值)调用semctl来完成!
semop函数打开一个信号量集合后,对其中一个或者多个信号量的操作就是用semop函数来执行。
#include<sys/sem.h>
int semop(int semid,struct sembuf *pstr,size_t nops);
返回:若成功则为0,出错则为-1;
其中opstr指向一个如下结构体数组:
struct sembuf{
short sem_num;//semaphore number:0,1,,,,,,,nsems-1
short sem_op;//semaphore operation:<0,>0,=0
short sem_flag;//operation flags :0,IPC_WAIT,SEM_UNDO
nops参数指出由opstr指向的sembuf结构数组中元素的数目。该数组中每个元素给目标
信号量集合内某个特定的信号量指定一个操作。这个特定的信号量由sem_num指定,0代表第一个元素
,1代表第二个元素,以此类推,直到nsems-1,其中nsems是目标信号量集合内成员信号量的数目(也就是
创建该集合时传递给semget的第二个参数)。
注意:我们仅仅保证sembuf结构体中含有所给出的三个成员,它还可能含有其他成员,而且各成员
并不保证以我们给出的顺序排序。这意味着我们决不能静态的初始化这种结构!例如:
struct sembuf ops[2]={
0,0,0, //wait for [0]to be 0
0,1,SEM_UNDO //then increment [0] by 1
};
而是必须使用运行时初始化方法,例如!
struct sembuf ops[2];
ops[0].sem_num=0;
ops[0].sem_op=0;//wait for [0]to be 0
ops[0].sem_flg=0;
ops[0].sem_num=0;
ops[0].sem_op=1;//then increment [0] by 1
ops[0].sem_flg=SEM_UNDO;
由内核保证传递给semop函数的操作数组(opstr)被原子的执行。内核保证或者完成所有指定的操作
,或者什么操作都不做。
每个特定的操作由sem_op的值确定,他可以是负数,0,或者正数。
我们将应如下术语。
semval:信号量的当前值
semncnt:等待semval变为大于其当前值的线程
semzcnt:等待semval变为0的线程数
semadj:所指定信号量针对调用进程的调整值。
sem_flag成员中指定了SEM_UNDO 标志后,semadj才会更新。
使得一个给定信号量操作非阻塞的方法是,在对应的sembuf结构的sem_flag成员中指定IPC_NOWAIT
标志。
现在我们基于每个具体指定的sem_op操作的三类可能值---正数,0,负数---来描述semop的操作。
1>如果sem_op是正数,其值就加到semval上,这对应于释放某个信号量控制的资源。如果指定了SEM_UNDO
标志,那么就从相应信号量的semadj值中减去sem_op的值。
2>如果sem_op是0,那么调用者希望等到semval变为0.如果semval已经是0,那就立即返回。
如果semval不为0,相应的信号量的senzcnt值就加1,调用线程则被阻塞到semval变为0
(到那时semzcnt值在减一)前面已经提到,如果指定了
IPC_NOWAIT标志,调用线程将不会投入睡眠。如果某个被铺获得信号中断了引起睡眠的semop函数,或者相应的信号量被删除了
,那么函数将过早的返回一个错误。
3>如果sem_op是负数,那么调用者希望等到semval变为大于或者等于semop的绝对值,
这对应于分配资源。
如果semval大于或者等于sem_op的绝对值,那就从semval中减去sem_op的绝对值。如果指定了SEM_UNDO
标志,那么sem_op的绝对值就加到相应信号量的semadj值上。
如果semval小于semop相应的semzcnt值就加1,调用线程则被阻塞到semval变为大于或者等于sem_op的
绝对值。到那时该线程将被解阻塞,还将从semval中减去sem_op的绝对值,相应信号量的semzcnt值
减1.如果指定了SEM_UNDO标志,那么sem_op的绝对值将加到相应信号量的semadj值上。前面已经提到,如果指定了
IPC_NOWAIT标志,调用线程将不会投入睡眠。另外如果某个被铺获得信号中断了引起睡眠的sem_op函数,或者相应的信号量被删除了
,那么函数将过早的返回一个错误。
semctl函数对一个信号量执行各种控制操作。
#include<sys/sem.h>
int semctl(int semid,int semnum,int cmd,..../*union semun arg*/);
第一个参数semid标识其操作待控制的信号量集,第二个参数semnum标识该信号量集内的某个成员(0,1,等等直到nsems-1)。semnum值
仅应用于GETVAL,SETVAL,GETNCNT,GETZCNT和GETPID命令。不理解神魔意思。。。
第四个参数是可选的,取决于第三个参数cmd(参见下面给出的联合中的注释)
union semun{
int val;//used for SETVAL only
struct semid_ds *buf;//used for IPC_SET and IPC_STAT
unshort *array;//used for GETVAL and SETALL
};
这个联合并没有出现在系统头文件中,因此必须由应用程序声明。(但是有些系统在<sys/sem.h>头文件中定义了这个联合)
但是我们必须由应用程序显示的声明!
system v中支持下列cmd值
GETVAL 把semval的当前作为函数返回值返回。既然信号量绝不会是负数(semval 被声明成一个unsigned short整数),那么成功的返回值总是非负数。
SETVAL 把semval设置为arg.val。如果操作成功,那么相应信号量在所有进程中的信号量调整至(semadj)将被置为0;
GETPID 把sempid的当前值作为函数返回值返回。
GETNCNT 把semncnt的当前值作为函数返回值返回。
GETZCNT 把semzcnt的当前值作为函数返回值返回。
GETALL 返回指定集合内的每个成员的semval值,这些值通过arg.arry指针返回,函数本身的返回值则为0.注意调用者必须分配一个
unsigned short 整数数组,该数组要容纳所指定集合内的所有成员的semval值得,然后把arg.array设置成指向这个数组。
SETALL 设置所指定信号量集合中每个成员的semval值。这些值是通过arg.arrry指针指定的。
IPC_RMID 把由semid指定的信号量集从系统中删除掉。
******************************************************************************
system v内存映射函数
mmap函数把一个文件或者一个posix共享内存区对象映射到调用进程地址空间,使用该函数有三个目的:
1>使用普通文件以提供内存映射I/O
2>使用特殊文件已提供匿名的内存映射
3>使用shm_open以提供无亲缘关系进程间的posix共享内存区
#include<sys/mman.h>
void *mmap(void *addr,size_t len,int prot,int flags,int fd,off_t offset);
返回:若成功则为被映射区的起始地址,若出错则为MAP_FAILED;
其中 addr可以指定描述符应被映射到进程内空间的起始地址。他通常被指定为一个空指针,
这样告诉内核自己去选择起始地址。无论在哪种情况下,该函数的返回值都是描述符fd映射到的内存区的起始地址。
length是被影射到调用进程地址空间中的字节数,它从被影射文件开头的第offset个字节开始算。offset通常设置为0.
内存映射区的保护由prot参数指定,该参数常见值是代表读写访问的PROT_READ(数据可读)|PROT_WRITE(数据可写)。
参数还可以为下面的值:PROT_EXCL(数据可执行) PROT_NONE(数据不可访问)
flag使用这些常值指定:MAP_SHARED(变动是共享的),MAP_PRIVATE(变动是私自的),MAP_FIXED(准确的解释addr参数)
MAP_SHARED(变动是共享的)和MAP_PRIVATE(变动是私自的)这两个标志必须选择一个,一般来说这个MAP_FIXED标志不用,
如果指定了MAP_PRIVATE,那么调用进程对被影射数据所做的修改质对该进程可见,而不改变其底层支撑对象
(或者一个文件对象,或者是一个共享内存区对象),如果指定了MAP_SHARED,那么调用进程对被影射数据所做的修改对于
共享该对象的所有进程都可见,并且确实改变了底层支撑对象。mmap成功返回后,fd参数可以关闭。该操作对于由mmap建立的
映射关系没有影响。
为了从某个进程的地址空间删除一个映射关系,我们调用munmap
#include<sys/mman.h>
int munmap(void *addr,size_t len);
返回:若成功则为0,若出错则为-1;
其中参数addr参数是由mmap返回的地址,len是映射区的大小。再次访问这些地址将会导致向调用进程产生一个SIGSEGV信号
(当然这里假设以后mmap调用并不重用这部分空间)。如果被影射区是使用MAP_PRIVATE标志映射的,那么调用进程对他所做的
变动都会被丢弃掉。(不理解什么意思)呵呵。。。。。
内核的虚拟内存算法保持内存映射文件(一般在硬盘上)与内存映射区(在内存中)的同步,前提是他是一个MAP_SHARED内存区。
这就是说如果我们修改了处于内存映射到文件的内存区中某个位置的内容,那么内核将在稍后某个时刻相应的更新文件。然而有时候
我们希望确信硬盘上的文件内容与映射区中的内容一致,于是调用msync来执行这种同步。
#include<sys/mman.h>
int msync(void *addr,size_t len,int flags);
返回:若成功则为0,若出错则为-1
其中addr和len参数通常指代内存中的整个内存映射区,不过也可以指定该内存区的一个子集。flags参数可以为以下各常值的组合。
MS_ASYNC(执行异步写),MS_SYNC(执行同步写),MS_INVALIDATE(使高速缓冲的数据失效)。
MS_ASYNC和MS_SYNC这两个常值中必须指定一个,但是不能都指定。他们的差别是,一旦写操作已有内核排入队列,MS_ASYNC即返回。
而MS_SYNC则要等到写操作完成后才返回。若果还指定了MS_INVALIDATE,那么与最终副本不一致的文件数据的所有内存中副本都失效。后续
的引用将从文件中取得数据。
为何要用mmap
到此为止就mmap的描述间接说明了内存映射文件:我们open它之后调用mmap把他映射到调用进程地址空间的某个文件。
使用内存映射文件所得到的奇妙特性是,所有的I/O都在内核的掩饰下完成,我们只需编写存取内存映射区中各个值的代码。我们绝不调用
read/write或lseek.
然而需要我们了解以防误解的说明是,不是所有文件都能进行内存映射。例如,试图把一个访问终端或者套接字的描述符映射到内存将导致
mmap返回一个错误。这些类型的描述符必须使用read或者write(或者他们的变体)来访问。
mmap的另一个用途是在无亲缘关系的进程间提供共享内存的内存区。这种情况下,所映射文件的实际内容成了被共享内存区
的初始内容,而且这些进程对共享内存区所做的任何变动都复制回所映射的文件(以及提供随文件系统的持续性)。这里假设
指定了MAP_SHARED标志,他是进程间共享内存所需求的。

推荐阅读