1 IPC对象
多个IPC -> 一个IPC对象 -> 一个IPC对象标识符 -> 一个键(整数)(即IPC对象的外部名称)
共享内存、消息队列和信号量,这三种IPC一般被合称为XSI IPC,它们有很多相似之处。
为了实现进程间的数据交换,系统内核会为参与通信的诸方维护一个内核对象(类似一个结构体变量),记录和通信有关的各种配置参数和运行时信息,称为IPC对象。
系统中的每个IPC对象都有唯一的、非负整数形式的标识符,所有与IPC相关的操作,都需要提供IPC对象标识符。
与文件描述符不同,IPC对象标识符不是最小整数。当一个IPC对象被创建,以后又被销毁时,与该类型对象相关的标识符会持续加1,直到达到一个整数型的最大值,然后又回转到0。
标识符是IPC对象的内部名。为了使多个合作进程能够在同一个IPC对象上会合,需要提供一个外部名方案。为此使用了键。每个IPC对象都与一个键相关联,于是键就被作为该对象的外部名称。
无论何时,创建或者获取一个IPC对象都必须指定一个键。键的数据类型为key_t,在<sys/ type.h>头文件中被定义为int。系统内核负责维护键与标识符的对应关系。
1.1 ftok()
#include <sys/ipc.h>
key_t ftok ( const char* pathname, int proj_id);
功能:用于合成一个键
pathname:一个真实存在的路径名
proj_id:项目ID,仅低8位有效,取0~255之间的整数
返回值:成功返回可用来创建或获取IPC对象的键,失败返回-1
ftok()函数用pathname参数调用stat函数,将其输出的stat结构体中的st_dev(设备ID)成员、st_ino(i节点号)成员,与proj_id参数组合来生成键。
-参与生成键的是设备ID和i节点号,而不是pathname参数字符串本身。假设当前路径为/home/t/uc,则ftok("/home/t/uc",123); 和ftok(".",123); 所返回的键是完全一样的。
设备ID和i节点号都至少是整型字长的数据,而键也是整型字长,再加上一个字节项目ID,在合成键的过程中难免会丢失一部分信息(低8位)。因此有时候明明提供的是不同的路径,该函数返回的键却是一样的。
1.2 IPC相关命令
查看系统中的IPC对象:
ipcs (-a) 查看所有
ipcs -m 查看memory共享内存段
ipcs -q 查看message queue,消息队列
ipcs -s 查看semphore,信号量集
删除系统中的IPC对象:
进程中打开IPC对象,不关闭的话,进程结束后仍在。故需敲命令来结束:
ipcrm -m id 移除用id标识的共享内存段
ipcrm -q id 移除用id标识的消息队列
ipcrm -s id 移除用id标识的信号量集
2 共享内存
系统内核负责维护的内存区域,其他地址空间通常被映射到堆和栈之间。
多个进程通过共享内存通信,所传输的数据通过各个进程的虚拟内存被直接反映到同一块物理内存中,这就避免了在不同进程和系统内核之间来回复制数据的开销。因此基于共享内存的进程间通信,是速度最快的进程间通信方式。
共享内存本身缺乏足够的同步机制,这就需要程序员编写额外的代码来实现。
2.1 shmget() 创建/获取
#include <sys/shm.h>
int shmget( key_t key, size_t size, int shmflg);
功能:创建新的或获取已有的共享内存
key:键
size:字节数,自动按页取整
shmflg:创建标志,可取以下值
0 获取,不存在即失败
IPC_CREAT 创建,不存在即创建,已存在即获取
IPC_CREAT | IPC_EXCL 排他,不存在即创建,已存在即失败
0664 通过位或来组合共享内存权限
返回值:成功返回共享内存的ID(即IPC对象的内部名称),失败返回-1
2.2 shmat() 加载
#include <sys/shm.h>
void* shmat (int shmid, void const* shmaddr, int shmflg);
功能:加载共享内存,将物理内存中的共享区域映射到进程用户空间的虚拟内存中。
同时将系统内核中共享内存对象的加载技
shmid:共享内存的id
shmaddr:映射到共享内存的虚拟内存起始地址,取NULL,由系统自动选择
shmflg:加载标志,可取以下值
0 以读写方式使用共享
SHM_RDONLY 以只读方式使用共享内存
返回值:成功返回共享内存的起始地址,失败返回-1
2.3 shmdt() 卸载
#include <sys/shm.h>
int shmdt ( void const* shmaddr );
功能:卸载共享内存
shmaddr:共享内存的起始地址
返回值:成0败-1
sgndt()负责从调用进程的虚拟内存中结束shmaddr所指向的映射区到共享内存的映射,同时将系统内核中共享内存的加载计数减1。
2.4 shmctl() 销毁
#include <sys/shm.h>
int shmctl ( int shmid, IPC_RMID, NULL);
功能:(标记)销毁共享内存
shmid:共享内存对象ID
返回值:成0败-1
销毁共享内存,并非真的销毁,而是做一个销毁标记,禁止任何进程对该共享内存形成新的加载,但已有的加载依然保留。只有当使用者们纷纷卸载,直至其加载计数降为0时,共享内存才会真的被销毁。
共享内存一旦被创建,需要执行代码或执行命令销毁。如程序执行过程中Ctrl + C中断,使用ipcs命令可查共享内存仍在,需ipcs -m id号 销毁。
//rshm.c 读取共享内存
#include<stdio.h>
#include<unistd.h>
#include<sys/shm.h>int main(void){//合成键printf("%d进程:合成键\n",getpid());key_t key = ftok(".",123); //key,键,IPC对象--共享内存的外部名称if(key == -1){perror("ftok");return -1;}//获取共享内存printf("%d进程:获取共享内存\n",getpid());getchar();int shmid = shmget(key,0,0); //shmid,共享内存ID,IPC对象--共享内存的内部名称if(shmid == -1){perror("shmget");return -1;}//加载共享内存printf("%d进程:加载共享内存\n",getpid());getchar();char* shmaddr = shmat(shmid,NULL,0); //共享内存用来存串,就char*if(shmaddr == (void*)-1){perror("shmat");return -1;}//读取共享内存printf("%s\n",shmaddr);//卸载共享内存printf("%d进程:卸载共享内存\n",getpid());getchar();if(shmdt(shmaddr) == -1){perror("shmdt");return -1;}printf("%d进程:大功告成\n",getpid());return 0;
}//搭配wshm.c执行,再开第三个窗口,ipcs命令
//wshm.c 创建、写入、销毁共享内存
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/shm.h>// shmget shmat shmdt shmctlint main(void){//合成键printf("%d进程:合成键\n",getpid());key_t key = ftok(".",123);if(key == -1){perror("ftok");return -1;}//创建共享内存printf("%d进程:创建共享内存\n",getpid());int shmid = shmget(key,4096,IPC_CREAT | IPC_EXCL | 0664);if(shmid == -1){perror("shmget");return -1;}//加载共享内存printf("%d进程:加载共享内存\n",getpid());getchar();char* shmaddr = shmat(shmid,NULL,0);if(shmaddr == (void*)-1){perror("shmat");return -1;}//写入数据 strcpy sprintf memcpygetchar();strcpy(shmaddr,"hello world!");//卸载共享内存printf("%d进程:卸载共享内存\n",getpid());getchar();if(shmdt(shmaddr) == -1){perror("shmdt");return -1;}//销毁共享内存printf("%d进程:销毁共享内存\n",getpid());getchar();if(shmctl(shmid,IPC_RMID,NULL) == -1){perror("shmctl");return -1;}printf("%d进程:大功告成\n",getpid());return 0;
}//搭配rshm.c 执行,再开第三个窗口,ipcs命令
3 消息队列 推荐!
消息队列是一个有系统内核负责存储和管理,并通过消息队列标识符引用的消息链表队列。链式结构,先进先出(可按消息类型提取)。
可以通过msgget()函数创建一个新的或获取一个已有的消息队列。
可以通过msgsnd()向消息队列的尾端追加消息,所追加的消息除了包含消息数据以外,还包含消息类型和数据长度(以字节为单位)。
可以通过msgrcv()从消息队列中提取消息,但不一定非按先进先出的顺序提取,也可以按消息的类型提取。
相较于其他几种IPC机制,消息队列具有明显的优势:
1)流量控制。如果系统资源(内存)短缺或者接受消息的进程来不及处理更多的消息,则发送消息的进程会在系统内核的控制下进入睡眠状态,待条件满足后再被内核唤醒,继续之前的发送过程。
2)面向记录。每个消息都是完整的信息单元,发送端是一个消息一个消息地发,接收端也是一个消息一个消息地收。而不像管道那样手法两端所面对的都是字节流,彼此之间没有结构上的一致性。
3)类型过滤。先进先出是队列的固有特征,但消息队列还支持按类型提取消息的做法,这就比严格先进先出的管道具有更大的灵活性。
4)天然同步。消息队列本身就具备同步机制,空队列不可读,满队列不可写,不发则不收,无需像共享内存那样编写额外的同步代码。
系统限制(与时俱进):
1)可发送的消息字节数上限:8192
2)单条队列消息总字节数上限:16384(16K)
3)全系统总消息队列数上限:16
4)全系统消息总字节数上限:262144(256K == 16×16K)
3.1 msgget() 创建/获取
#include <sys/msg.h>
int msgget ( key_t key, int msgflg);
功能:创建新的或获取已有的消息队列
key:键
msgflg:创建标志,可取以下值
0 获取,不存在即失败
IPC_CREAT 创建,不存在即创建,已存在即获取
IPC_CREAT | IPC_EXCL 排他,不存在即创建,已存在即失败
0664 通过位或来组合读写权限
返回值:成功返回消息队列的ID,失败返回-1
3.2 msgsnd() 发送
#include <sys/msg.h>
int msgsnd ( int msgid, void const* msgp, size_t msgsz, int msgflg );
功能:发送消息
msgid:消息队列的ID
mdgp:指向一个包含long消息类型和消息数据的内存块。是结构体的地址:
内存块的前4/8字节必须是大于0的整数,代表消息类型,其后紧跟消息数据
msgsz:期望发送消息数据(不含消息类型)的字节数
msgflg:发送标志,一般取0即可
返回值:成0败-1
注意,msgsnd()的msgp参数所指向的内存块中包含消息类型,其值必须大于0,但该函数的msgsz参数所表示的期望发送字节数中却不包含消息类型long所占的4个字节(64位系统是8)消息:
消息队列缺省为阻塞模式,如果调用msgsnd()发送消息时,超过了系统内核有关消息的上限,该函数会阻塞,直到系统内核允许加入新消息为止。比如有消息因被接收而离开消息队列。
如果msgflg参数中包含IPC_NOWAIT,则msgsnd()在系统内核中的消息已达上限的情况下不会阻塞,而是返回-1,同时置errno为EAGAIN。
3.3 msgrcv() 接收
#include <sys/msg.h>
int msgrcv ( int msgid, void* msgp, size_t msgsz, long msgtye, int msgflg );
功能:接收消息
msgid:消息队列的ID
msgp:指向一块包含消息类型(long)和消息数据(char[]或其他)的内存(结构体)
msgsz:期望接收消息数据(不含消息类型)的字节数,一般取 消息数据的容器量
msgflg:接收标志,一般取0即可
msgtyp:消息类型,可取以下值:
0 提取消息队列的第一条信息
>0 若msgflg参数不包含MSG_EXCEPT位,则提取消息队列中第一条类型
为msgtyp的消息;
若msgflg参数包含MSG_EXCEPT位,则提取消息队列中第一条类型
不为msgtyp的消息。
<0 提取消息队列中类型小于等于msgtype绝对值的消息,
类型越小的消息越被优先录取
返回值:成功返回实际接收到的消息数据字节数,失败返回-1
注意,msgrcv()的msgp参数所指向的内存块中包含的消息类型,其值由该函数输出,但该函数的msgsz参数所表示的期望接收字节数以及该函数所返回的实际接收字节数中都不包含消息类型所占的4个字节。
若存在于msgtyp参数匹配的消息,但其数据长度大于msgsz参数,且msgflg参数包含MSG_NOERROR位,则只截取该消息数据的前msgsz字节返回,剩余部分直接丢弃;但如果msgflg参数不包含MSG_NOERROR位,则不处理该消息,直接返回-1,并置errno为E2BIG。
若消息队列中有可接收消息,则msgrcv()会将消息移除消息队列,并立即返回所接收到的消息数据的字节数,表示接收成功,否则此函数会阻塞,直到消息队列中有可接收消息位置。若msgflg参数中包含IPC_NOWAIT位,则msgrcv()在消息队列中没有可接收消息的情况下不会阻塞,而是返回-1,同时置errno为ENOMSG。
3.1 msgctl() 销毁
#include <sys/msg.h>
int msgctl ( int msgid, IPC_RMID, NULL );
功能:销毁消息队列
msgid:消息队列的ID
返回值:成0败-1
//rmsg.c 读取消息队列
#include<stdio.h>
#include<unistd.h>
#include<sys/msg.h>
#include<errno.h>
int main(void){//合成键printf("%d进程:合成键\n",getpid());key_t key = ftok(".",123);if(key == -1){perror("ftok");return -1;}//获取消息队列printf("%d进程:获取消息队列\n",getpid());int msgid = msgget(key,0);if(msgid == -1){perror("msgget");return -1;}//接受数据printf("%d进程:接受数据\n",getpid());for(;;){//接受数据struct{long type;//消息类型char data[64];//消息内容}buf = {};if(msgrcv(msgid,&buf,sizeof(buf.data)-1,1234,0) == -1){if(errno == EIDRM){printf("%d进程:消息队列别销毁\n",getpid());break;}else{perror("msgrcv");return -1;}}//显示消息printf("%ld.%s",buf.type,buf.data);}return 0;
} //结合wmsg.c ,同时开第3个终端ipcs命令
//wmsg.c 写入消息队列
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/msg.h>int main(void){//合成键printf("%d进程:合成键\n",getpid());key_t key = ftok(".",123);if(key == -1){perror("ftok");return -1;}//创建消息队列printf("%d进程:创建消息队列\n",getpid());int msgid = msgget(key,IPC_CREAT | IPC_EXCL | 0664);if(msgid == -1){perror("msgget");return -1;}//发消息printf("%d进程:发送数据\n",getpid());for(;;){//通过键盘获取要发送的数据struct{ //结构体名可以不写(区分结构体和结构体变量)long type;//消息类型char data[64];//消息内容}buf = {1234,""}; //buf是结构体变!量!名 有typedef是结构体别名!fgets(buf.data,sizeof(buf.data),stdin); //是stdin,则敲个回车,就是1条消息的结束。// ! 退出if(strcmp(buf.data,"!\n") == 0){break;}//发送数据if(msgsnd(msgid,&buf,strlen(buf.data),0) == -1){perror("msgsnd");return -1;}}//销毁消息队列printf("%d进程:销毁消息队列\n",getpid());if(msgctl(msgid,IPC_RMID,NULL) == -1){perror("msgctl");return -1;}return 0;
}//开个终端ipcs命令,执行本代码,输几个消息,看ipcs,再执行rmsg