XSI机制的进程间通信
1、XSI介绍:
什么是XSI:
X/Open国际联盟有限公司是一个欧洲基金会,它的建立是为了向UNIX环境提供标准,XSI是X/Open System Interface的缩写,也就是X/Open设计的系统接口。
X/Open的主要的目标是促进对UNIX系统、接口、网络和应用的开放式系统协议的制定。它还促进在不同的UNIX环境之间的应用程序的互操作性,以及支持对电气电子工程师协会对UNIX的可移植操作系统接口规范。
IPC对象:
-
它用于进程间通信的XSI-IPC内核对象,类似于匿名管道、文件内核对象一样,通过使用XSI方式进行进程间通信时,系统会在内核中创建出一个XSI-IPC内核对象,让通信的进程能够共同访问该内核对象
-
XSI-IPC对象只存在于内核空间,不会在文件系统中显示,该对象需要通过IPC键值来创建和获取
IPC键值:
-
用于创建\获取 IPC对象的凭证,是一个无符号的整数,相当于IPC对象的名字,类似于文件名
-
在创建IPC对象时,需要创建者提供一个IPC键值,有点类似给文件取名字,但是所有已经存在的IPC对象都在同一个作用域下,因此所有进程都可以访问到,所有有很大重名的风险,所以不建议创建者自己瞎想一个IPC键值去创建IPC对象,而应该使用操作系统提供的一个自动生成IPC键值的API
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id); 功能:根据项目路劲和项目编号 自动生成一个ipc键值 pathname:建议提供当前项目的路径 proj_id:项目编号 返回值:会根据项目路径和编号来计算出一个IPC键值,但是只是根据字符串内容来计算,不会检查路径是否为假 注意:使用时,建议提供当前的正确的工作路径,以及不同的项目编号,就可以获得不同的IPC键值,通过不同的IPC键值来创建不同的IPC对象 但是获取时,需要拿到相同的IPC对象,因此需要拿相同的IPC键值,因此提供相同路径和编号即可获取
IPC描述符:
-
类似于文件描述符,是一个非负整数,它是IPC内核对象的给用户空间来访问的凭证
显示IPC对象命令:
ipcs -m #查看共享内存IPC对象 memory ipcs -q #查看消息队列IPC对象 queue ipcs -s #查看信号量IPC对象 sem ipcs -a #查看所有的IPC对象
删除IPC对象命令:
ipcrm -m id #删除共享内存IPC对象 ipcrm -q id #删除消息队列IPC对象 ipcrm -s id #删除信号量IPC对象
2、共享内存:
基本原理:
-
在内核中开辟一块内存,可以让其它进程的虚拟地址与这块内存进行映射,从而达到让多个进程共享一块内存的目的,当一个进程向这块内存写数据时,其它进程就都可以读取到,这就达到了通信的目的
-
优点:这种通信方式不存在数据的复制,是最快的进程间通信方式
-
缺点:需要考虑同步访问的问题(用信号)
使用方式:
当一个进程向共享内存写入数据时,内核不会通知其他进程,进程从共享内存读取数据时,也无法分辨是否是通信的对方新写入的数据,为解决该问题,有以下方式:
-
读写:一个进程只负责写,另一个只负责读,这要负责读的进程使用的是最新即可,是一种单向通信
-
轮询:配合定时器或者闹钟,每隔一段时间就读取一次
-
中断:配合信号,进程只要往共享内存中写入数据,就给对方发送一个约定好的信号,其它进程如果已经读取完共享内存也可以发送一个约定好的信号
相关API:
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg); 功能:创建\获取一个共享内存的IPC对象 key:IPC键值,类似于文件名 size:共享内存的字节数,建议取内存页字节数的整数倍,默认1页=4096字节 如果是想要创建,则必须指定size的大小 如果是获取已有的共享内存,这size可取0即可 shmflg: 0 -表示该参数无效,获取共享内存,size也无效了 IPC_CREAT -创建共享内存 IPC_EXCL -如果已经存在同一个IPC对象,返回失败 mode -共享内存的权限,当创建共享内存时必须加上 例如:IPC_CREAT|0644 返回值:IPC描述符,失败-1 void *shmat(int shmid, const void *shmaddr, int shmflg); 功能:加载共享内存(把进程中的虚拟地址与内核中的共享内存建立映射关系) shmid:IPC描述符,要映射哪条共享内存 shmaddr:想要映射的虚拟地址,可以给NULL由操作系统自动分配 shmflg: 0 - 以读写方式映射共享内存 SHM_RDONLY - 以只读方式映射共享内存 返回值:映射成功后的虚拟内存首地址,失败返回-1 int shmdt(const void *shmaddr); 功能:取消虚拟内存与共享内存的映射,也叫卸载共享内存 shmaddr:要取消映射的虚拟内存地址 int shmctl(int shmid, int cmd, struct shmid_ds *buf); 功能:销毁共享内存、获取共享内存属性、设置共享内存的属性 shmid:IPC描述符 cmd: IPC_SET -设置共享内存的属性 buf输入型参数,只有uid、gid、mode可设置 IPC_STAT -获取共享内存属性 buf输出型参数 IPC_RMID -删除共享内存 buf给NULL即可, -删除并非真正直接删除,而是对共享内存的使用计数-1,如果该计数被-1为0后,意味着系统中没有任何的进程映射这块共享内存,才会从内核中真正销毁它 struct shmid_ds { struct ipc_perm shm_perm; // 属主和权限信息 size_t shm_segsz; // 共享内存的大小 time_t shm_atime; // 最后映射时间(加载) time_t shm_dtime; // 最后取消映射的时间(卸载) time_t shm_ctime; // 最后内存内容修改时间 pid_t shm_cpid; // 创建者pid pid_t shm_lpid; // 最后加载、卸载者pid shmatt_t shm_nattch; // 当前映射的进程数的计数器 ... }; struct ipc_perm { key_t __key; // IPC键值 uid_t uid; // 属主id gid_t gid; // 属主组id uid_t cuid; // 创建者id gid_t cgid; // 创建者组id unsigned short mode; // 权限 unsigned short __seq; // 序列号 };
编程模型:
进程A 进程B 创建共享内存 获取共享内存 映射共享内存 映射共享内存 写数据并通知对方 接收到通知后读取数据 接收到通知后读取数据 写数据并通知对方 取消映射 取消映射 删除共享内存
// shmA.c #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h> static int shmid; static char* shm; // 接收到对方发送信号 才读取数据 void sigread(int num) { printf("\nread:%s\n",shm); if(0 == strncmp(shm,"quit",4)) { printf("对方不想鸟你了\n"); // 取消映射 if(shmdt(shm)) { perror("shmdt"); exit(EXIT_FAILURE); } // 删除共享内存 if(shmctl(shmid,IPC_RMID,NULL)) { perror("shmctl"); exit(EXIT_FAILURE); } // 必须结束进程 return没有用 exit(EXIT_SUCCESS); } } int main(int argc,const char* argv[]) { signal(SIGRTMIN,sigread); printf("我是进程%u\n",getpid()); // 创建共享内存 shmid = shmget(ftok(".",110),4096,IPC_CREAT|0644); if(0 > shmid) { perror("shmget"); return EXIT_FAILURE; } // 映射共享内存 shm = shmat(shmid,NULL,0); if((void*)-1 == shm) { perror("shmat"); return EXIT_FAILURE; } // 获取对方的进程id pid_t pid = 0; printf("请输入与我通信的进程ID:"); scanf("%u",&pid); // 写入数据并通知对方 for(;;) { printf(">>>"); scanf("%s",shm); // 发送信号 kill(pid,SIGRTMIN); if(0 == strncmp(shm,"quit",4)) { printf("你终止通信了\n"); break; } } // 取消映射 if(shmdt(shm)) { perror("shmdt"); return EXIT_FAILURE; } // 删除共享内存 if(shmctl(shmid,IPC_RMID,NULL)) { perror("shmctl"); return EXIT_FAILURE; } return EXIT_SUCCESS; }
3、消息队列:
基本原理:
-
由系统内核维护的一个链式队列,每个节点称为一条消息,每条消息由消息类型、数据、长度信息组成
-
和管道类似,可以双向通信,并且从中读取一个消息后,会自动出队,而且不是按照顺序读取,是按照消息类型读取
相关API:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflg); 功能:创建\获取消息队列 key:IPC键值 msgflg: 0 -获取,不存在则失败 IPC_CREAT -创建,不存在则创建,存在会获取,除非 IPC_EXCL -排斥,已存在则创建失败 mode -消息队列权限,创建时必给 返回值:IPC描述符,失败-1 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); 功能:向消息队列发送消息包 msqid:IPC描述符 msgp:提供一个包含有消息类型和消息内容的消息包内存块,参考格式如下: struct msgbuf { long mtype; // 消息类型 char mtext[1]; // 消息内容,也可以使用柔性数组 }; msgsz:提供消息包中消息内容的字节数,不包含消息类型的字节数 msgflg: 0 -如果当消息队列中没有空闲空间,该函数会一直阻塞 IPC_NOWAIT -当消息队列中没有空闲空间,不会阻塞,返回-1 返回值:成功0 失败-1 ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg); 功能:从消息读取消息包 msqid:IPC描述符 msgp:接收消息包的结构体首地址,也是由消息类型+消息内容组成,输出型参数 msgsz:消息包的字节数,如果实际消息包的大小>msgsz,会立即返回-1 msgtyp:要接收的消息类型 0 -获取消息队列中的第一条消息 >0 -获取指定消息类型的消息 <0 -获取消息队列中,消息类型小于或等于abs(msgtyp)的消息,如果有多个,则接收最小值的 msgflg: 0 阻塞等待 IPC_NOWAIT 如果消息队列中没有该消息类型的消息,直接返回 MSG_EXCEPT 获取消息队列中第一个不等于msgtyp的消息,msgtyp>0 MSG_NOERROR 如果包含它,则当消息包的大小>msgsz时,不会报错,并接收msgsz字节 返回值:成功接收到的消息的字节数 int msgctl(int msqid, int cmd, struct msqid_ds *buf); 功能:销毁消息队列、获取消息队列属性、设置消息队列属性 shmid:IPC描述符 cmd: IPC_SET -设置消息队列的属性 buf输入型参数,只有uid、gid、mode可设置 IPC_STAT -获取消息队列属性 buf输出型参数 IPC_RMID -删除消息队列 buf给NULL即可 struct msqid_ds { struct ipc_perm msg_perm; // 与shmctl的一致 time_t msg_stime; /* Time of last msgsnd(2) */ time_t msg_rtime; /* Time of last msgrcv(2) */ time_t msg_ctime; // 最后属性修改时间 unsigned long __msg_cbytes; // 消息队列中的字节数 msgqnum_t msg_qnum; // 消息队列中的消息数 msglen_t msg_qbytes; // 消息队列运行的最大字节数 pid_t msg_lspid; /* PID of last msgsnd(2) */ pid_t msg_lrpid; /* PID of last msgrcv(2) */ };
编程模型:
进程A 进程B 创建消息队列 获取消息队列 发送消息(type:a) 接收消息(type:a) 接收消息(type:b) 发送消息(type:b) 销毁消息队列
4、信号量
基本原理:
-
所谓信号量就是内核中维护的一个全局变量,当做计数器,用于计数多进程工作中的共享资源数,也是为了限制多进程对共享资源的使用
-
信号量是一种数据操作锁,本身是不具备数据交换通信功能的,而是通过控制其他进程的通信资源来协助实现进程间通信
-
假设操作系统中有n个共享资源,需要把信号量的值设置为n
-
当有m个进程需要以独占的形式使用k个共享资源,并且m*k>n,需要使用信号量
-
每个进程在使用共享资源之前先尝试执行信号量-1操作
-
如果信号量>0时,说明剩余共享资源够用,则-1后拿到共享资源去执行,当执行完毕后,需要把共享资源归还,信号量+1操作
-
如果信号量<=0时,说明共享资源不足,则进程阻塞等待,直到信号量的值>0,才重新唤醒后,继续尝试-1
-
-
注意:进程之间资源几乎都是相互独立的,共享资源很少,所以很少用到信号量
相关API:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg); 功能:创建或获取信号量集合 key:IPC键值 nsems:表示信号量集合中信号量的数量,一般写1即可 semflg: 0 -获取,不存在则失败 IPC_CREAT -创建,不存在则创建,存在会获取,除非 IPC_EXCL -排斥,已存在则创建失败 mode -消息队列权限,创建时必给 返回值:IPC描述符,失败-1 int semop(int semid, struct sembuf *sops, size_t nsops); 功能:对信号量集中的信号量进行加减操作 semid:IPC描述符 sops:结构体数组首地址 struct sembuf{ unsigned short sem_num; // 信号量在信号集中的下标 short sem_op; // 操作数 // 如果sem_op大于0,则将其加到下标sem_num号信号量中,相当于资源释放 // 如果sem_op小于0,则将其对下标sem_num号信号量中的值相减,相当于获取资源 short sem_flg; // 操作标记位 // 0 如果下标sem_num号信号量不够减,进程会阻塞等待,直到资源数够减为止 // IPC_NOWAIT 不阻塞等待 }; nsops:表示sops结构体数组的指针中指向了多少个结构体,就是要操作的信号量的数量,一般写1 int semctl(int semid, int semnum, int cmd, ...); 功能:删除信号量、获取信号量属性、设置信号量属性 semnum:表示对信号量集合中semnum下标的信号量进行操作 cmd: IPC_SET -设置信号量的属性 buf输入型参数,只有uid、gid、mode可设置 IPC_STAT -获取信号量属性 buf输出型参数 IPC_RMID -删除信号量 buf给NULL即可 GETALL -获取信号量集合中所有信号量的值,放入semun.array SETALL -设置信号量集合中所有信号量的值,通过semun.array修改 SETVAL -设置信号量集合中某个信号量的值,通过semun.val修改 GETVAL -获取信号量集合中某个信号量的值,通过返回值获取 根据cmd的选择,使用第四个参数semun的值 union semun { int val; /* Value for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */ };