Unix下进程间通信的方法主要有以下几种:
随进程持续IPC:IPC对象一直存在直到拥有进程结束
pipe:无名管道,一般半双工通信,只能用于继承关系的进程(fork)之间的通信,可以用于零拷贝函数splice
FIFO:有名管道,可以在独立不相关进程间全双工通信
随内核持续IPC: IPC对象一直存在直到内核重启,或者IPC对象被显示关闭。System V IPC结构就是属于这种IPC方式,他是不使用Unix文件系统命名空间的一套进程间通信方式
System V 消息队列:发送者(msgsnd)将新消息添加到队列的尾端,接收者(msgrecv)可以以非先进先出的顺序接收消息System V信号量:为多进程同步共享数据对象访问的计数器集合System V共享内存:不同进程将自己的虚拟内存空间映射到同一片物理内存下随文件系统持续IPC: 即使内核重启,IPC对象也会一直保存。一般IPC方法没有随文件系统持续的,因为效率很低,不过POSIX IPC如果使用内存映射文件的方法,可以归于这一类
这一篇总结System V信号量,共享内存。后续会总结:System V消息队列,pipe,FIFO。POSIX IPC手头参考书上介绍的不全,先不总结。
System V IPC一套使用了自己的命名空间,所以先做通用的总结。System V IPC对象的创建和获取主要是通过:标识符,key实现的,同时类似于UNIX的文件系统,System V IPC也定义了自己的权限结构struct ipc_perm,保存在每种System V IPC自己的标识结构内。
标识符:在内核中,每一个System V IPC对象都使用非整数标识符唯一标识。对IPC结构使用的接口msgctl/msgsnd/msgrcv, semclt/semop, shmctl/shmat/shmdt都是使用标识符对IPC结构进行引用。
key:标识符是IPC结构的内部名,key则是IPC对应的外部名,通过合作进程都可以获得到的key,使用msgget/semget/shmget再获得对应IPC结构的标识符,从而合作进程汇聚到同一个IPC结构上。
使用get方法获取IPC结构的标识符有下面几种方法:
get方法中flag设置IPC_CREAT标志,直接创建一个新的IPC结构,如果是父子进程,可以不使用key转换,相应key参数可传入IPC_PRIVATE。
除了IPC_CREAT,flag参数还可以设置IPC_EXCL标志,如果IPC结构已存在就会出错,errno设置为EEXIST,和IPC_CREAT搭配效果更佳。
合作进程引用一个公有头文件,定义一个客户端/服务端都认可的key,服务端创建IPC
使用ftok,使用客户进程/服务进程都认同的路径名(必须已存在且可访问)+ID(0-255),转换出一个key
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);ftok组成key的原理:
按给定路径获得stat结构中st_dev和st_ino字段st_dev, st_ino, proj_id组合获得key值基本每个IPC结构都有一个xxxid_ds的标识符结构,里面一定会包含一个struct ipc_perm结构,规定权限和所有者:
struct ipc_perm { uid_t cuid; /* 创建用户ID */ gid_t cgid; /* 创建组ID */ uid_t uid; /* 所有用户ID */ gid_t gid; /* 所有组ID */ unsigned short mode; /* 读写权限 */ };uid, gid, mode可以使用msgctl/semctl/shmctl修改,类似chown/shmod
mode只记录读(十进制4),写(十进制2)权限,同样区分用户(百位),组(十位),其他(个位)的权限
信号量本质上就是计数器,记录当前共享资源可以被访问的连接数。
当使用一个信号量时,信号量值semval为正,说明进程可使用该资源,此时信号量值-1(P操作),如信号量值为0(小于0),进程休眠
当进程不再使用一个信号量控制共享资源时,信号量+1(V操作),如有进程在休眠将其唤醒
System V的信号量不是单独一个信号量,而是定义了一个信号量集合。内部每个信号量索引范围0~n-1
System V信号量常用的3个接口:
semget:创建/获取目标信号量IPC结构的标识符semctl:获取/设置信号量IPC的控制信息semop:操作信号量,PV操作就是使用这个函数通过key获取/创建相应的信号量标识符:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg);如果是创建新的集合,nsems必须指定,如果是关联一个已有的集合,nsems可以指定为0
一个信号量集合IPC创建后,内核中会初始化和维护一个关联semid_ds结构,会保存信号量集合的控制信息,可以使用后面介绍的semctl获取其内容,或者进行修改:
struct semid_ds { struct ipc_perm sem_perm; /* 信号量所有权限 */ time_t sem_otime; /* semop最后执行的时间戳 */ time_t sem_ctime; /* 最后一次修改(semctl)的时间戳 */ unsigned short sem_nsems; /* 集合中信号量的数量 */ };除了上面提到的semid_ds结构,集合中每一个信号量都对应一个sem结构,保存信号量的内容(信号值,挂起进程数等),在semid_ds中也有指向该结构的数组指针,semop主要就是对对应信号量sem结构的内容进行修改,也可以通过semop获取sem的信息:
#include <sys/sem.h> /* 系统中一个sem对应一个信号量 */ struct sem { short sempid; /* 最后一次操作的进程pid */ ushort semval; /* 当前信号量值 */ ushort semncnt; /* 等待semval增加的进程数 */ ushort semzcnt; /* 等待semval=0的进程数 */ };信号量集合控制信息的操纵:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd, .../* union semun arg */); 最后一个可选参数union semun arg是一个联合体,由cmd标志确定是否可以省略,或者使用联合体中哪一个变量:union semun { int val; /* SETVAL的返回值 */ struct semid_ds *buf; /* IPC_STAT, IPC_SET的缓冲 */ unsigned short *array; /* GETALL, SETALL返回数组 */ struct seminfo *__buf; /* IPC_INFO缓冲 */ }; cmd具体标志的分类: IPC_STAT/IPC_SET:获取/设置信号量集合的semid_ds。IPC_SET只能由信号量的当前的有效用户进程或者sudo进程设置IPC_RMID:OS中删除该信号量集,立刻发生,删除后仍在使用(semop)该信号量集的进程将会返回EIDRM。同样只能由信号量的当前的有效用户进程或者sudo进程设置GETVAL/SETVAL/GETPID/GETNCNT/GETZCNT:sem结构中对应成员getter和setter标志,SETVAL中semval在arg.val中指定,其余getter方法直接从返回值返回GETALL/SETALL:获取/设置所有的信号量值,保存在arg.array中对于信号量集合中具体信号进行PV操作的函数接口,信号量操作的核心:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *sops, unsigned nsops);sops指定了操作信号量的struct sembuf数组,nsops指定了该数组的长度,具体结构的定义如下:
struct sembuf { unsigned short sem_num; /* 信号量在集合中索引:0~nsems */ short sem_op; /* 修改semval的修改值 */ short sem_flg; /* 操作标志:IPC_NOWAIT, IPC_UNDO */ }首先根据sembuf中·sem_op的正负进行讨论,也就是常说的PV操作:
sem_op > 0:就是常说的V操作,释放信号量的资源,semval会增加sem_op的数量sem_op < 0:就是常说的P操作,尝试获取控件的信号量资源,semval会相应减少sem_op,针对semval是否足够的情况就可以划分讨论: |sem_op| <= semval:此时信号量资源数足够,直接从semval减去sem_op完事|sem_op| > semval:此时资源数不够,需要看sem_flg中IPC_NOWAIT,下面会说 sem_op == 0:表示调用进程希望等待到semval为0的时候,根据当前semval是否为0,又要划分讨论: semval == 0:直接满足条件,semop函数立刻返回semval != 0:不满足条件,又要看sem_flg中IPC_NOWAIT是否设置当操作的sem_op修改值不满足当前semval要求时,IPC_NOWAIT会决定进程是直接出错返回,还是挂起等待。而如果选择挂起等待的话,由于信号量删除/中断等问题会有不同的错误代码返回
针对sem_op|为负,sem_op| > semval的情况:
设置IPC_NOWAIT:semop不会等待,出错直接返回,errno设置EAGAIN未设置IPC_NOWAIT:sem结构中semncnt+=1,进程休眠直到下面事件发生: |semop| <= semval,此时sem结构中semncnt-=1(等待结束),然后semval再减去|semop|,semop正常返回信号被系统删除(比如其他进程调用了semctl并且cmd=IPC_RMID),函数出错返回EIDRM进程捕捉到中断信号,并从信号处理程序中返回,此时semncnt -= 1(调用进程不再等待),函数出错返回,errno设置为EINTR针对sem_op == 0且semval != 0的情况其实和上面V操作阻塞是类似的,只不过semzcnt会做+1-1的操作。
如果这个标志被设置,OS将跟踪该进程对该信号量的修改情况。假如进程在没有释放信号量的情况下就终止了,OS将自动释放进程持有的信号量,防止其他进程因得不到信号量而出现死锁问题。因此在使用semop函数时候,建议IPC_UNDO是要设置的。
具体实现的话,SEM_UNDO与进程变量semadj(每个信号量跟踪计数)相关联,具体的增减主要根据sem_op修改值的正负来确定:
sem_op为正:semadj减去sem_op相应的数值,内核相当于放弃对这部分资源的跟踪sem_op为负,且semval修改成功:semadj增加相应的数值,内核相当于记录对等价数量资源的跟踪exit被调用:semadj记住了当前进程占用信号量多少资源,只要semadj不为0都会修改回去创建/获取一个共享内存IPC结构的标识符:
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg); size指出了共享内存的大小。对于Linux来说,一个共享内存段最大字节长度32768Byte,最多4096个段。类似的,一个共享内存IPC创建后,内核会创建并维护一个关联shmid_ds结构:struct shmid_ds { struct ipc_perm shm_perm; /* 权限结构 */ size_t shm_segsz; /* 内存段大小,字节为单位 */ time_t shm_atime; /* 最后关联本段的时间戳 */ time_t shm_dtime; /* 最后取消关联本段的时间戳 */ time_t shm_ctime; /* 最后修改(shmctl)时间戳 */ pid_t shm_cpid; /* 创建共享内存段进程的pid */ pid_t shm_lpid; /* 最后shmat/shmdt进程的pid */ shmatt_t shm_nattch; /* 当前链接本段的进程数 */ ... }; 需要注意shmid_ds中shm_getsz ,表示了实际存储段的字节大小(会向上取整到页的整数倍,多余碎片不可用)对共享内存IPC的控制信息进行操作:
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);相关的cmd标志也比较少:
IPC_STAT/IPC_SET:获取/修改shmid_ds内容,修改只能由共享内存的有效用户id进程,以及sudo进程IPC_RMID:删除共享内存段,只有到shm_nattach=0(没有进程链接该段)物理内存才会被实际删除。但shm的标识符会立即删除,之后不能使用shmat链接该段。同样只能由有效用户id进程,sudo进程来做SHM_LOCK/SHM_UNLOCK:共享存储段加解锁,只能由sudo用户来做使用shmget获取共享内存IPC的标识符后,还需要调用shmat将当前进程的虚拟地址空间,和共享内存的实际物理地址进行关联,如果不想使用共享内存,还需要取消当前进程的虚拟内存空间和共享内存的实际物理地址的关联:
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg); int shmdt(const void *shmaddr); 链接成功后,shmid_ds::shm_attach += 1,表示有新的进程和共享内存链接shmat中的addr不要管,一般都是设置成0,表示由内核选择第一可用地址shmdt中shmaddr是shmat中返回的关联地址,shmdt不会删除共享内存IPC的标识符和shmid_ds,只会对shmid_ds中shm_nattach -= 1,只有某个进程调用shmctl使用了SHM_RMID标志,共享内存才会被物理删除