最好的参考资料:
1.师从互联网。
2.UNP v2 Posix IPC的相关章节3、6、11、14。
3.Linux man 命令。
缅怀Stevens 大师~~~在《深入理解Linux内核架构》中作者说过:POSIX标准已经用更为现代的方式,引入了类似的结构(即Posix IPC)。我不讨论Posix的相关机制了,因为大多数应用程序仍然在使用System V IPC 。
IPC键: 一个进程内可能同时使用多个同一种类 System V IPC,比方说信号量。System V IPC使用key_t类型的IPC 键值(就是XXXget函数的第一个参数)来区分同种但不相同的IPC 资源。Linux 2.6.35在sys/types.h中把它定义为int类型。有两种获得IPC键值的方法,通过ftok函数可生成这样的键值,但可能重复,不唯一,但重复概率极小。另外一个是直接使用IPC_PREVATE宏——唯一的键值。IPC关键字本质上类似与文件系统的文件路径名;
IPC标志符:通过XXXget函数将IPC键转换成IPC标志符,System V IPC使用IPC标志符作为他们的名字。凡知道这个标志符(即一个魔数)的程序,都能访问IPC 标志符对应的IPC资源。IPC 标志符又类似于文件描述符。
#include <sys/ipc.h>
key_t ftok ( const char * pathname, int proj_id);//在Linux中ftok函数使用proj_id的低8位、st_dev的低8位以及st_ino的低16位生成这个键值。
这里重点说一下shmget、semget、msgget函数的最后一个flag参数,他的值是IPC_CREAT、IPC_EXCL、S_IRUGO(无此宏,它代表用户、组和其他)、S_IWUGO(同前)的或运算组合。当只指定S_IRUSR ,ipcs命令显示的perms是502。测试777也可以达到。
注:S_IXUGO不需要使用。
内核给为3种 System V IPC分别维护一个(only one)类型如下结构:
struct ipc_ids{
int in_use;//分配的某种(3种之一)IPC资源数
int max_id;//使用的最大索引位置
unsigned short seq;//下一个分配位置的序号
unsigned short seq_max;//序号的最大值溢出归0
struct semaphore sem;//保护ipc_ids自身
struct ipc_id_ary nullentry;//如果IPC资源无法初始话,则entries指针指向位数据结构 (一般不使用)
struct ipc_id_ary * entries;//所有同一类型(比如信号量)的IPC资源的入口哦。
...
}
entries有两个成员:p和size。p指向类型为struct kern_ipc_perm的结构(对应一个IPC资源)的数组。size数组的大小。
struct kern_ipc_perm {
key_t key; /* Key supplied to msgget *///这个key就是用来区分同一类型IPC资源。
unsigned int uid; /* Effective UID of owner */
unsigned int gid; /* Effective GID of owner */
unsigned int cuid; /* Effective UID of creator */
unsigned int cgid; /* Effective GID of creator */
unsigned short mode; //权限位
...
};
这样内核就能不断缩小目标范围,找到每个IPC资源了。其实,p指向的这个struct kern_ipc_perm 实际上是对应3种不同System IPC 的一个描述结构的一个成员而已。这3个描述结构分别是:semid_ds,shmid_ds,msgid_ds.可以在头文件中找到他们,他们对应的内核中的结构为sem_array,msg_queue,shmid_kernel,这才是某个IPC资源的真正描述体。
另外,在ULK上有这样的描述:所有的System V IPC 函数都必须通过适当的Linux系统调用实现的。实际上,在80X86架构下,只有一个名为ipc()的IPC系统调用。当进程调用一个IPC函数,比如说msgget(),该函数实际上调用C库中的一个封装函数,该封装函数又通过以msgget()的所有参数加上一个适当的子命令代码(本例是MSGGET)作为参数来调用ipc()系统调用,sys_ipc()服务例程检查子命令代码,并调用内核函数实现所请求的服务。
若还不能明白,那就多看看《深入LINUX 内核架构》和《ULK》相关章节^_^。
System V 的信号量现在指的是信号量的集合,当然可以指定含有一个信号量的信号量集合,就像Posix 信号量。
对于每个信号量集——IPC资源,内核维护一个如下的信息结构,它定义在bits/sem.h中
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned short sem_nsems; /* No. of semaphores in set */
};
对于每个信号量集中的某个信号量,内核维护一个大概形式如下的结构,这个结构是不透明的内部数据结构,无法在头文件中找到:
struct sem{
unsigned short semval; /* semaphore value *///该信号量的当前值
unsigned short semzcnt; /* # waiting for zero *///等待该信号量值变为0的线程数
unsigned short semncnt; /* # waiting for increase *///等待该信号量变为大于当前值的某个值的线程数
pid_t sempid; /* process that did last op */
unsigned short semadj;//在UNPv2中指出:semadj不必存在!他用于SEM_UNDO标志。
};
#include<sys/sem.h>
int semget (key_t key, int nsems, int semflg) ;
semget函数的基本原理:进程甲创建一个和key相关信号量集,参数nsems指定相关信号量集含有的信号量个数,一旦信号量集创建了所含有的信号量的个数就更改不了了,进程甲、乙、丙、丁第二次通过相同的key调用semget时nsems可随意指定,那就设为0吧。kernel2.6.35验证内核会初始化这个信号量集的每个信号量为0,man手册和Setevns大师 警告:这不可移植!!
union semun{ /* The user should define a union like the following to use it for argumentsfor `semctl'. */
int val; <= value for SETVAL
struct semid_ds *buf; <= buffer for IPC_STAT & IPC_SET
unsigned short int *array; <= array for GETALL & SETALL
struct seminfo *__buf; <= buffer for IPC_INFO
};/*Previous versions of this file used to define this union but this is incorrect. One can test the macro _SEM_SEMUN_UNDEFINED to see whether one must define the union or not. */
int semctl (int semid, int semnum, int cmd, .../*union senum arg */);
semctl函数的基本原理:linux并未提供union semun结构的定义,这是一个模板。该结构是做为semctl的最后一个参数,配合cmd使用。该函数的每个命令的具体使用参看UNPv2(2010 人邮新版)的第十一章:231页以及man手册。
struct sembuf{//成员的顺序再不同实现上不同,所以不能静态初始化该结构。
unsigned short int sem_num; /* semaphore number */
short int sem_op; /* semaphore operation */
short int sem_flg; /* operation flag */
};
int semop (int semid, struct sembuf * sops, size_t nsops) ;
semop函数的基本原理:参数sops指向一个sembuf结构的数组,参数nsops是这个数组的元素的个数。其中sembuf.sem_num成员用来标记操作信号量集中的第几个信号量(从0开始)。重点说下sembuf.sem_op,其值和上文中的 struct sem结构个相关的。他的值分为三种情况:
1.大于0:sem_op值直接加到sem.seval上。进程返回。
2.等于零:如果sem.seval等于0.进程返回。如果sem.seval不等于0进程被阻塞直到:第一种情况:sem.seval变为0,进程返回。第二种情况:被中断进程返回,errno置为EINTR。
3.小于0:若sem.seval大于等于sem_op的绝对值,则sem.seval减掉sem_op的绝对值。若sem.seval小于sem_op的绝对值,则sem.seval减掉sem_op的绝对值。进程被阻塞直到:第一种情况:sem.seval大于等于sem_op的绝对值,进程返回并且sem.seval减掉sem_op的绝对值。第二种情况:被中断进程返回,errno置为EINTR。
最后sembuf.sem_flg:可设为0、IPC_NOWAIT、SEM_UNDO的或运算操作。当指定IPC_NOWAIT时,以上所有阻塞都不会发生,取而代之的是,errno被置为EAGAIN,进程返回。当指定SEM_UNDO时sembuf.sem_op操作的影响只对本次进程操作有影响,当调用进程结束设置了SEM_UNDO的那几个信号量值将会回复到进程开始之前。正如杨继张老师所言:该信号量的值就像变得根本没有运行过该进程一样,这句是复旧(undo)的本意。
呼~~~这复杂性绝非Posix信号量之辈,所能比拟的~~~^_^
关于新创建的信号量集的初始化:System V信号量在设计中,创建和初始化信号量集需要两次函数调用是个致命的缺陷——Stevens.
在man semget命令中有如下声明:
The values of the semaphores in a newly created set are indeterminate. (POSIX.1-2001 is explicit on this point.) Although Linux, like many other implementations, initializes the semaphore values to 0, a porta‐ble application cannot rely on this: it should explicitly initialize the semaphores to the desired values.
新创建的信号量集中的信号们的值是不确定的。(Posix明确的指出过)虽然Linux和其他的实现上初始化他们为0,为了可移植性我们应该明确的初始化他们为想要的值。
The semaphores in a set are not initialized by semget(). In order to initialize the semaphores, semctl(2) must be used to perform a SETVAL or a SETALL operation on the semaphore set. (Where multiple peers do not know who will be the first to initialize the set, checking for a nonzero sem_otime in the associated data structure retrieved by a semctl(2) IPC_STAT operation can be used to avoid races.)
semget没有初始化信号集中的信号的话,应该用semctl的SETVAL或SETALL的操作初始化信号集。(多个进程可以通过检测sem_otime(semctl的IPC_STAT操作可得到)的值(在semget创建新的信号量集时,sem_ids中sem_otime成员被置为0,只有在semop操作后被置为当前值),来避免竞争发生)。这意味着创建信号量的那个进程必须初始化他的值,而且必须在任何其他进程可以使用该信号量之前调用semop。
内核对每个消息队列,维护一个定义在<bits/msq.h>中的结构
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages in queue */
msglen_t msg_qbytes; /* Maximum number of bytes allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
我第一次看的时候很疑惑:怎么没有消息有关的数据结构呢?注意到他的备注/* Structure of record for one message inside the kernel.
The type `struct msg' is opaque. */哦,原来是不透明的。
#include <sys/msg.h>
int msgget (key_t key, int msgflg);
#ifdef __USE_GNU//编译时加上-D_GNU_SOURCE即可
struct msgbuf {/* Template for struct to be used as argument for `msgsnd' and `msgrcv'. */
long int mtype; /* type of received/sent message */
char mtext[1]; /* text of the message */
};
#endif
ssize_t msgrcv (int msqid, void * msgp, size_t msgsz,long int msgtyp, int msgflg);//msgflag可为0或者IPC_NOWAIT或者MSG_NOERROR
int msgsnd (int msqid, const void * msgp, size_t msgsz, int msgflg);//msgflag可为0或者IPC_NOWAIT
int msgctl (int msqid, int cmd, struct msqid_ds *buf) ;
这里说下msgrcv和msgsnd函数:msgp参数他的类型是struct msgbuf 。linux提供了一个如上的模板,我们可以自行添加其他数据信息,但long int的mtype成员必须要放在最前面。这个成员,非常重要,他是标注这个消息的类型的。消息的收发基本上是围绕这个类型在转。
另外,msgsz参数有点特别:他的值指定的是msgbuf结构数据部分的大小即其值应为sizeof(*msgp)-sizeof(long int)。
关于msgrcv的type参数:
1.当type为0:返回消息队列的第一个消息。消息队列是FIFO链表。
2.当type大于0:返回type类型最早的消息。没有的话将被阻塞,若指定IPC_NOWAIT标志,errno置为ENOMSG。特别的若指定MSG_EXCEPT标志,无论type存在与否,只返回第一个消息。
3.当type小于0:返回小于等于type绝对值的最小消息。
4.相关细节参看man msgrcv。
P.S.关于eclipse的宏提示:我刚才测试sys/msg.h文件下struct msgbuf 结构宏的是__USE_GNU没有定义(eclipse 显示 灰色的)于是我加上了-D_GNU_SOURCE还是灰色,敲代码时也没有结构体成员提示,百思不得其解。但是编译了一下,通过!程序正常运行!你们懂的~~:-)我想是因为-D_GNU_SOURCE是在我们编译时才起作用的。平时,eclipse不能感知到他已经定义了~~~~故,__USE_GNU处于未定义状态下。
对于每个共享内存区,内核都会维护这样的结构,在bits/shm.h中可以找到他:
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time of this structure*/
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
最有用的IPC机制就是共享内存,这种机制允许两个或多个进程通过把公共数据结构放入一个共享内存区 来访问。如果进程要访问这种存放在共享内存区的数据结构,就必须在自己的地址空间中增加一个新的内存区,它将映射与这个共享内存区相关的也框。这样的也框可以很容易地由内核通过请求调页进行处理——ULK做了这样的描述。
#include <sys/shm.h>
int shmget (key_t key, size_t size, int shmflg);
int shmctl (int shmid, int cmd, struct shmid_ds *buf);
void *shmat (int shmid, const void * shmaddr, int shmflg);
int shmdt (const void *shmaddr);
这几个函数的原理是:shmget创建或打开一个共享内存空间对象,shmat函数把这个共享内存空间映射到自己的进程空间。shmdt断开这种映射。但共享内存区是随内核持续的,他上面的数据不会随shmdt而删除。彻底删除该共享内存区要到他的引用计数器变为0时。shmdt发现指定的共享内存区的引用计数为0,就顺便删除他。shmctl的IPC_RMID用于减少链接计数器即删除共享内存空间对象(原理同文件操作的unlink)。之后通过直接读写shmat返回的地址读写就可以了~~~和Posix 共享内存空间原理一样!!!
UNIX网络编程第二卷:进程间通信,作者:W.Richard Stevens,译者:杨继张。
深入理解Linux 内核 第三版 作者:Daniel P.Bovet &Marco Cesati, 译者:陈莉君&张琼声&张宏伟。
深入linux内核架构 作者:WolfgangIMauerer ,译者:郭旭。
本文引用了互联网上众大神的bolg和帖子,非常感谢。
特别感谢广大的开源社区。
若有侵害到您的利益,及时相告,我将在一个工作日内删除。