一、System V IPC (了解)
1、System V IPC 机制概念
- System V IPC (系统5的IPC 官方版本中引入的一大类进程间通信机制)
- IPC 对象包含: 共享内存 、 消息队列和信号灯集;
- 每个IPC对象有唯一的ID;(在创建是由系统分配的)
- IPC对象创建后一直存在,直到被显示地删除(或者系统关闭,IPC也会自动释放);
- 每个IPC对象有一个关联的KEY;一种属性;作用:通过KEY可以使得不同进程可以打开/找到 同一个IPC对象;KEY值为0 表示 私有对象;
- ipcs/ipcrm 两个命令
- ipcs : 显示当前系统5的所有IPC对象;
- ipcrm :删除一个对像; 可以根据key 、id 等删除;
- System V IPC —— key
2、ftok(熟练)
- 为了避免系统中的key值与新创建的key冲突 ,使用ftok;降低概率
- 为了创建一个Key值;
#include <sys/types.h>
#include <sys/ipc.h>
- key_t ftol(const char *path,int proj_id);
- 成功时返回合法的key值,失败时返回EOF;
- path 存在且 可访问的文件的路径; ---- 路经存在且存在
- key值的由来:由第一个参数系统分配的i结点的编号与第二个参数的低八位进行组合移位,形成key值;
- prij_id 用于生成key 的数字,不能为0;
3、System V IPC -- ftok 示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
int main(int argc, const char *argv[])
{
key_t key;
if((key = ftok(".",'a')) == -1)
{
perror("ftok");
exit(-1);
}
printf("%d \n",(int)key);
return 0;
}
— 每个进程必须生成相同的key,因为通过相同的Key,才能找到同一个IPC对象;
二、共享内存
1、共享内存特点(了解)
- 共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝;
- 共享内存 在 内存空间 创建,,不能被用户直接访问,但可被进程映射到用户空间访问,使用灵活;
- 由于多个进程可同时访问共享内存,因此需要同步和互斥机制配合使用;
2、共享内存创建(熟练)
- 共享内存使用步骤:
- 创建/打开共享内存;
- 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问;
- 读写共享内存;
- 撤销共享内存映射;
- 删除共享内存对象;
- 共享内存的创建 — shmget (可用于创建/打开一个共享内存)
#include <sys/ipc.h>
#include <sys/shm.h>
- int shmget(key_t key , int size , int shmflg);
- 成功时返回共享内存的id,失败时返回EOF;
- key 和共享内存关联的key , IPC_PRIVATE或 ftok生成;
- shmflg 共享内存 标志位 IPC_CREAT|0666;
3、示例
- 要求:创建一个私有的共享内存,大小为512字节,权限为0666;
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
int main(int argc, const char *argv[])
{
char* addr;
int msgid;
if((shmid = shmget(IPC_PRIVATE,512,0666)) < 0)
{
perror("shmget");
exit(-1);
}
printf("%d \n",shmid);
return 0;
}
- 要求:创建/打开一个和Key 关联的共享内存,大小为1024字节,权限为0666;
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
int main(int argc, const char *argv[])
{
char* addr;
int msgid;
key_t key;
if((key = ftok(".",'a')) == -1)
{
perror("ftok");
exit(-1);
}
if((shmid = shmget(key,1024,IPC_CREAT|0666)) < 0)
{
perror("shmget");
exit(-1);
}
printf("%d \n",shmid);
return 0;
}
- IPC_CREAT|0666 :系统去判断该key与对象关联 的共享内存存在了,就打开;不存在则取创建;返回ID;
4、小结
ftok:生成key值;
共享内存:不需要内存拷贝,效率最高;
shmget() :创建打开/创建;
5、共享内存映射(熟练)
— 首先要获得共享内存的ID后,对共享内存进行以下各种操作
— 每一个IPC对象都有一个唯一的ID,使用该ID可以对该对象做相应的操作;
- 共享内存映射 ---- shmat
作用:根据共享内存的ID,把一个共享内存映射到进程的地址空间,映射完以后,我们就可以像访问一个自己分配的内存空间一样取访问共享空间;
#include <sys/ipc.h>
#include <sys.shm.h>
void *shmat(int shmid , const void *shmaddr, int shmflg);
- 成功时返回映射后的地址,失败时返回(void*)-1;
- shmid : 映射空间返回的id;
- *shmaddr : 指定映射后的地址,为NULL时,由操作系统来自动分配地址;(推荐使用NULL,认为分配 会占取不能使用的地址空间,与malloc相似)
- shmflg : 标志位 0表示可读写;SHM_RDONLY 表示只读;
6、共享内存读写(熟练)
- 通过指针访问共享内存,指针类型取决于共享内存中存放的数据类型;
- 存放的是int ,定义int指针去读写;
例如: 在共享内存中存放键盘输入的字符串:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(int argc, const char *argv[])
{
char* addr;
int shmid;
key_t key;
if((key = ftok(".",'a')) == -1) ///获取 key值
{
perror("ftok");
exit(-1);
}
if((shmid = shmget(key,1024,IPC_CREAT|0666)) < 0) //打开/创建一个 共享文件
{
perror("shmget");
exit(-1);
}
printf("%d \n",shmid);
if((addr = (char*)shmat(shmid,NULL,0)) == (char*)-1 ) //映射一个共享文件
{
perror("shmat");
exit(-1);
}
fgets(addr,32,stdin);
return 0;
}
7、共享内存撤销映射 ---- shmdt
#include <sys/ipc.h>
#include <sys.shm.h>
int shmdt (void *shmaddr);
- 成功是返回0,失败返回EOF;
- shmaddr : 映射空间的首地址;
- 不使用共享内存时应撤销映射;
- 进程结束时自动撤销共享内存的映射;
8、共享内存控制(熟练)
- 共享内存控制 ----- shmctl
#include <sys/ipc.h>
#include <sys.shm.h>
int shmctl(int shmid,int cmd , struct shmid_ds *buf);
- 可能会用来显式的删除一个共享内存;
- 成功返回0,失败时返回EOF;
- shmid : 要操作的共享内存的id;
- cmd : 要执行的操作 IPC_STAT(获取当前共享内存的属性) IPC_SET(设置)IPC_RMID(删除一个共享内存的ID:使用该操作时不需要第三个参数,直接使用NULL即可)
- buf : 保存保存或设置共享内存属性的地址;
- 共享内存 ---- 注意事项
- 每块共享内存大小有限制
- ipcs -l 查看共享内存的属性
- cat /proc/sys/kernel/shmmax 修改共享内存的大小
- 共享内存删除的时间点:第一个进程创建共享内存,最后一个进程删除共享内存;
- shmctl(shmid,IPC_RMID,NULL);标记这个共享内存将要删除
- nattach 变成0时真正删除;将所有进程中都删除了该共享内存时,才会真正的删除(所有进程都撤销该共享内存);
二、消息队列(熟练)
1、消息队列 的概念
- System V IPC的一种,进程间通信的机制 之一;
- 消息队列由消息队列ID来唯一标识;
- 消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等;
- 消息队列可以按照类型来发送/接收消息;
- 不同的进程 通过 消息队列进行通信,可在一个消息队列中定义不同的类型来辨别;
— 消息队列使用步骤:
- 打开/创建消息队列 msgget
- 向消息队列发送消息 msgsnd
- 从消息队列接收消息 msgrcv
- 控制消息队列 msgctl
2、打开/创建消息队列(熟练)
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key ,int msgflg);
- 成功时返回消息队列的ID,失败时返回EOF;
- key和消息队列关联的 key ; 如果创建进程私有消息队列使用IPC_PRIVATE , 如果多个进程使用一个消息队列,每个进程使用ftok得到key值;
- msgflg : 标志IPC_CREAT | 0666;加上这个后,系统会检查如果消息队列不存在则创建;
— 打开消息队列的示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
int main(int argc, const char *argv[])
{
char* addr;
int msgid;
key_t key;
if((key = ftok(".",'a')) == -1)
{
perror("ftok");
exit(-1);
}
if((msgid = msgget(key,IPC_CREAT|0666))<0)
{
perror("msgid");
exit(-1);
}
return 0;
}
- 多个进程使用ftok获得key时,“.”标识在当前目录,如果多进程都是使用该参数的话,要保证进程在同一个目录中执行;
3、发送消息(熟练)
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msgif, const void* msgp , size_t size);
- 成功时返回0,失败时返回-1
- msgid 消息队列id
- msgp 消息缓冲区地址;
- size 消息正文长度;
- msgflg 标志位 0 或 IPC_NOWAIT;
- 标志位中: 0 的含义是,进程成功收到消息后返回(当消息队列满时,进程被阻塞,直到成功收到消息后返回0);IPC_NOWAIT 的含义是,消息发送后,成功返回0,消息队列满了也不阻塞,直接返回-1后其他信号;
- 消息格式
— 通信双方首先定义好统一的消息格式;
— 用户根据应用需求定义结构体类型;
— 首成员类型为long ,代表消息类型(正整数);
— 其他成员都属于消息正文;
typedef struct
{
long mtype; //
char mtext[64];
}MSG;
- 通信的双方:通信格式必须是一致的;
4、小结
- 打开/创建 消息队列 ---- msgget
- 定义消息格式 --- 用户自定义结构体;
- 消息发送 ----- msgsnd 向用户发送指定的数据;
5、接收消息队列(熟练)
#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv (int msgid , void *msgp , size_t size , long msgtype , );
- 成功时返回收到的消息长度,失败时返回-1;
- msgid 消息队列 id;
- msgp 消息缓冲区地址;
- size 指定接收的消息长度;
- msgtype 指定接收的消息类型;
- msgflg 标志位 0 或 IPC_NOWAIT;
- 如果指定长度 比传过来的消息的长度要 小,则只会接收指定的长度的消息;
- msgtype 可指定为0;接收最早的 消息;也可指定为负数,按照优先级接收消息;
- msgflg : 0 意味则 如果没有指定的消息,阻塞等待,直到有消息为止;IPC_NOWAIT:有消息则接收,没有消息则返回失败;
6、控制消息队列(熟练)
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl (int msgid , int cmd , struct msqid_ds *buf );
- 成功时返回0,失败时返回-1;
- msgid 消息队列 id;
- cmd 要执行的操作 IPC_STAT/IPC_SET/IPC_RMID;
- buf 存放消息队列属性的地址;
- 使用方法与shmctl大体相同,不同为共享内存的删除需要所有进程都删除了才会彻底删除,而消息队列一旦使用该函数,将会彻底删除,如果有进程正在发送或者接收消息,也将立马出错;
7、消息队列示例(掌握)
- 消息队列 示例 :
第一个进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
typedef struct //消息队列的格式
{
long mtype; //消息类型
char mtext[64]; //消息正文
}MSG;
#define LEN (sizeof(MSG) - sizeof(long)) //正文长度
#define TypeA 100
#define TypeB 200
int main(int argc, const char *argv[])
{
key_t key;
int msgid;
MSG buf;
if((key = ftok(".",'q')) == -1) //得到key值
{
perror("ftok");
exit(-1);
}
if((msgid = msgget(key , IPC_CREAT|0666)) < 0) //打开/创建一个消息队列
{
perror("msgget");
exit(-1);
}
while(1)
{
buf.mtype = TypeB;
printf("intput : \n");
fgets(buf.mtext,64,stdin); //从键盘输入字符到消息队列中
msgsnd(msgid,&buf,LEN,0); //发送消息
if(strcmp(buf.mtext,"quit\n") == 0)break;
if(msgrcv(msgid,&buf,LEN,TypeA,0) < 0) //接收消息
{
perror("msgrcv");
exit(-1);
}
printf("recv from clientB : %s \n",buf.mtext);
if(strcmp(buf.mtext,"quit\n") == 0)
{
msgctl(msgid,IPC_RMID,0);//删除消息队列
exit(-1);
}
}
return 0;
}
第二个进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
typedef struct
{
long mtype;
char mtext[64];
}MSG;
#define LEN (sizeof(MSG) - sizeof(long))
#define TypeA 100
#define TypeB 200
int main(int argc, const char *argv[])
{
key_t key;
int msgid;
MSG buf;
if((key = ftok(".",'q')) == -1)
{
perror("ftok");
exit(-1);
}
if((msgid = msgget(key , IPC_CREAT|0666)) < 0)
{
perror("msgget");
exit(-1);
}
while(1)
{
if(msgrcv(msgid,&buf,LEN,TypeB,0) < 0)
{
perror("msgrcv");
exit(-1);
}
if(strcmp(buf.mtext,"quit\n") == 0)
{
msgctl(msgid,IPC_RMID,0);//删除消息队列
exit(-1);
}
printf("recb from cleintA : %s \n",buf.mtext);
buf.mtype = TypeA;
printf("intput : \n");
fgets(buf.mtext,64,stdin);
msgsnd(msgid,&buf,LEN,0);
if(strcmp(buf.mtext,"quit\n") == 0)break;
}
return 0;
}
- 一个进程中使用 多进程、多线程 实现信息说法;
- 使用消息队列跟方便的搭建一个 CS架构,实现多客户端之间的通信;
三、信号灯(集)机制
1、System V 信号灯
- 信号灯的含义: 计数信号灯;
- Posix无名信号灯在线程专题中有所提及;
- 本讲主要讲解 System V 信号灯;
2、System V 信号灯特点
- Posix信号灯 不论是P 还是V操作,只能操作一个信号灯;
- 而在System V 信号灯 可以操作集合中的多个信号灯;
— 两个进程 P1 P2分别申请资源A B 锁死的情况:
— 两个进程 P1 P2分别申请资源A B 使用System V 信号灯避免 锁死的情况:
- 将P(A) P(B)申请资源 整合成一个集合set,只有同时满足时 ,另一个才能使用资源;
— 使用System V 信号扽 使用步骤:
- 打开/创建 信号灯 semget
- 信号灯 初始化 semctl
- P/V 操作 semop
- 删除信号灯 semctl
3、打开/创建信号灯(熟练)
#include <sys/ipc .h>
#include <sys/sem.h>
int semget (key_t key , int nsems , int semflg);
- 成功时返回信号灯 的id , 失败返回-1;
- key 和消息队列光联的key IPC_PRIVATE 或 ftok;
- nsems 集合 中包含的计数信号灯个数;
- semflg 标志位 IPC_CREAT|0666 IPC_EXCL;
- nsems : 初始化时要明确 程序中有几类资源,每类资源需要多少个信号灯;
- semflg : IPC_CREAT|0666 信号灯不存在则创建;如果加上IPC_EXCL,起检测的作用,如果不存在则创建,如果存在则出错;一般在第一个进程创建信号灯时使用,判断某个信号灯是否创建过;
4、信号灯初始化(熟练)
1、信号灯初始化
#include <sys/ipc .h>
#include <sys/sem.h>
int semctl (int semid , int semnum , int cmd , .......);
- 成功时返回0 , 失败返回-1;
- semif 要操作的信号灯集 ID;
- semnum 要操作的集合中的信号灯编号;
- cmd 执行的操作 SETVAL IPC_RMID;
- union semun 取决于cmd;
- 信号灯集合中,信号灯的编号从0开始;
- SETVAL : 设置 信号灯; IPC_RMID: 删除信号灯集合;
- 第四个参数:union semun 取决于cmd ; SETVAL时需要,IPC_RMID 时不需要;
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) */
};
5、信号灯集初始化 ---- 示例
- 一般程序中有多少个信号灯 就需要初始化几次;
- 信号灯不能被多次初始化; 在初始化前最好 判断一下是否存在;
6、信号灯 — P/V操作(熟练)
— 信号灯P/V操作 ---- semop
#include <sys/ipc .h>
#include <sys/sem.h>
int semop (int semid , struct sembuf* sops , unsigned nsops);
- 成功时返回0 , 失败返回-1;
- semif 要操作的信号灯集 ID;
- sops 描述对信号灯操作的结构体(数组);
- nsops 要操作的信号灯的个数;
- sops : 一个 struct sembuf* 类型 的数组,要操作的信号灯操作一一sembuf 对应;
strct sembuf
{
short semnum;
short sem_op;
short sem_flg;
};
- semnum 信号灯编号;
- sem_op -1 :p操作 1 : v 操作
- sem_flg 0 / IPC_NOWAIT
- P操作 : 获取/释放资源 V操作: 使用资源
7、信号灯 — 共享内存(熟练)----- 实际的例程
- 使用信号灯,实现两个进程间的通信;
- 创建一个 可写的信号灯,一个可读的信号灯,共两个信号灯组成信号灯集
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
#define N 64
#define READ 0
#define WRITE 1
union semun // 信号灯的设置 格式
{
int val;
struct semid_ds *df;
unsigned short *array;
struct seminfo *buf;
};
void init_sem(int semid , int s[] ,int n) // 信号灯的初始化
{
int i;
union semun myun;
for(i = 0;i<n;i++)
{
myun.val = s[i];
semctl(semid,i,SETVAL,myun); //初始化
}
}
void pv(int semid , int num, int op)
{
struct sembuf buf;
buf.sem_num = num; //信号灯编号
buf.sem_op = op; // -1 p操作 1 v 操作
buf.sem_flg = 0; //0/IPC_NOWAIT
semop(semid,&buf,1); //一个一个的操作信号灯
}
int main(int argc, const char *argv[])
{
int shmid,semid,s[] = {0,1};
pid_t pid;
key_t key;
char *shmaddr;
if((key = ftok(".",'q')) == -1) //得到key值
{
perror("ftok");
exit(-1);
}
if((shmid = shmget(key,2,IPC_CREAT|0666)) < 0) // 打开/创建 共享内存
{
perror("shmget");
exit(-1);
}
if((semid = semget(key,2,IPC_CREAT|0666)) < 0) //打开/创建 信号灯
{
perror("semget");
goto _error1;
}
init_sem(semid,s,2); // 初始化 2 个信号灯
if((shmaddr = (char*)shmat(shmid,NULL,0)) == (char*)-1) // 映射共享内存
{
perror("shmat");
goto _error2;
}
if((pid = fork()) < 0) // 创建子进程
{
perror("fork");
goto _error2;
}else if(pid == 0) //子进程
{
char *p,*q;
while(1)
{
pv(semid,READ,-1); //对编号0的信号灯 P操作
p = q = shmaddr;
while(*q)
{
if(*q != ' ')
{
*p++ =*q;
}
q++;
}
*p = '\0';
printf("%s",shmaddr);
pv(semid,WRITE,1);//对编号1的信号灯 V操作
}
}else // 父进程
{
while(1)
{
pv(semid,WRITE,-1);//对编号1的信号灯 P操作
printf("intput > \n");
fgets(shmaddr,N,stdin);
if(strcmp(shmaddr,"quit\n") == 0)break;
pv(semid,READ,1);//对编号0的信号灯 V操作
}
kill(pid,SIGUSR1); //向子进程发送 SIGUSR1信号
}
_error2:
semctl(semid,0,IPC_RMID); //删除信号灯
_error1:
shmctl(semid,IPC_RMID,NULL); // 删除共享内存
return 0;
}
- semop:
- 信号灯集 + 共享内存 : 实现内存的有序访问;