大家好,我是练习时长两年半的练习生,喜欢唱、跳、rap、敲代码,键来!
在前面我们已经讲过,早期的进程间通信有三种——无名管道、有名管道、信号,(传送门:Linux C 进程间的通信——无名管道、有名管道、信号),今天就来浅谈一下在system V IPC的三种对象,也是进程通信的另外三种姿势——共享内存、消息队列、信号量。
目录
一、共享内存
(一)概念
(二)基操
(三)相关API
(四)示例代码
二、消息队列
(一)消息队列通信原理
(二)基操
(三)相关API
(四)示例代码
三、信号量
(一)概念
(二)基操
(三)相关API
(四)代码实例
四、总结
补充:shell查看和删除IPC对象
共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间,进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高进程通信的效率。
1》创建或者获取共享内存
2》将共享内存映射到用户空间
3》解除映射
4》删除共享内存
1. 创建密钥 ftok
注意!!!
只要pathname和proj_id不变,就可以获得相同的key,如下,进程A和进程B获得的key相同。
例子:
进程A:key_t key1=ftok("xx.txt",0xa);
进程B:key_t key2=ftok("xx.txt",0xa);
#include
#include
/**
***********************************
*@brief 创建密钥key
*@param pathname:带路径的文件名
proj_id:数字
*@retval key_t
成功返回key
失败返回-1,并返回错误码EOF
***********************************
*/
key_t ftok(const char *pathname, int proj_id);
2. 创建共享内存 shmget
#include
#include
/**
***********************************
*@brief 创建或者获取共享内存
*@param key:密钥
IPC_PRIVATE:系统自动分配key
size:共享内存的大小
shmflg:权限,一般写为 IPC_CREAT|0666
*@retval int
成功返回共享内存的id
失败返回-1,并返回错误码EOF
***********************************
*/
int shmget(key_t key, size_t size, int shmflg);
3. 共享内存映射到用户空间 shmat
#include
#include
/**
***********************************
*@brief 将共享内存映射到用户空间
*@param shmid:共享内存的id
shmaddr:需要映射到什么地方(地址)
shmflg:访问权限
SH_RDONLY:只读
0:默认,可读可写
*@retval int
成功返回映射后的地址
失败返回-1,并返回错误码EOF
***********************************
*/
void *shmat(int shmid, const void *shmaddr, int shmflg);
4. 解除映射 shmdt
#include
#include
/**
***********************************
*@brief 解除映射
*@param shmaddr:需要解除映射的地址
*@retval int
成功返回0
失败返回-1,并返回错误码EOF
***********************************
*/
int shmdt(const void *shmaddr);
5. 管理共享内存 shmctl
#include
#include
/**
***********************************
*@brief 删除
*@param shmid:需要解除映射的地址
cmd:IPC_STAT (获取对象属性)
IPC_SET (设置对象属性)
IPC_RMID (删除对象)
buf:如果cmd为IPC_RMID,则此处置NULL
*@retval int
成功返回0
失败返回-1,并返回错误码EOF
***********************************
*/
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
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 */
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 */
...
};
shm_write.c 和 shm_read.c代码基本一样,代表两个不同的进程,一个写入一个读取。shm_write.c中每次输入一行数据就会覆盖原来在共享内存空间的数据,输入quit退出输入状态,然后解除映射删除空间。shm_read.c读取到quit后也会退出,然后解除映射。
shm_write.c:
#include
#include
#include
#include
#include
#include
#include
#define SIZE 200 //共享内存的大小
int main(int argc, char *argv[])
{
key_t key;
int shmid;
char *addr;
//手动创建key
if((key = ftok("./",0xa)) < 0){
perror("ftok error");
exit(1);
}
//创建共享内存,获取id号
if((shmid = shmget(key,SIZE,IPC_CREAT|0666)) < 0){
perror("shmget error");
exit(1);
}
//映射共享内存到用户空间
if((addr = shmat(shmid,NULL,0)) < 0){
perror("shmat error");
exit(1);
}
//往共享内存空间中写入数据,输入quit退出
while(1){
printf("please input string:");
if(!strncmp(fgets(addr,SIZE,stdin),"quit",4))
break;
}
//解除映射
if((shmdt(addr) < 0)){
perror("shmdt error");
exit(1);
}
//删除共享内存
if((shmctl(shmid,IPC_RMID,NULL) < 0)){
perror("shmctl error");
exit(1);
}
return 0;
}
shm_read.c:
#include
#include
#include
#include
#include
#include
#include
#define SIZE 200
int main(int argc, char *argv[])
{
key_t key;
int shmid;
char *addr;
if((key = ftok("./",0xa)) < 0){
perror("ftok error");
exit(1);
}
if((shmid = shmget(key,SIZE,IPC_CREAT|0666)) < 0){
perror("shmget error");
exit(1);
}
if((addr = shmat(shmid,NULL,0)) < 0){
perror("shmat error");
exit(1);
}
//读取共享内存中的数据,如果内存中的数据为quit则退出
while(1){
printf("%s",addr);
if(!strncmp(addr,"quit",4))
break;
sleep(1);
}
if((shmdt(addr) < 0)){
perror("shmdt error");
exit(1);
}
return 0;
}
需要在ubuntu中开两个终端,分别运行 shm_read.c 和 shm_write.c ,运行效果如下,共享内存的基本操作,很简单是吧。
1》消息队列中可以有不同类型的消息
2》发送消息和接收消息时,必须要指定消息的类型
3》进程在通信时,发送的消息类型必须和接收的消息类型相同
1》创建或者获取消息队列
2》写入或读取数据
3》管理消息队列
1. 创建或获取消息队列的ID msgget
#include
#include
#include
/**
***********************************
*@brief 创建或者获取消息队列
*@param key: 密钥
msgflg:权限 IPC_CREAT|0666
*@retval int
成功返回id
失败返回-1,并返回错误码EOF
***********************************
*/
int msgget(key_t key, int msgflg);
2. 发送消息到消息队列 msgsnd
/**
***********************************
*@brief 往消息队列里写入数据
*@param msqid:消息队列id
msgp:消息的数据包,需要自己定义结构体
struct msgbuf {
long mtype; // 消息类型,且要大于0 第一个成员必须是消息类型
char mtext[10]; // 消息正文
};
msgsz:消息正文中消息的实际长度
msgflg: 0 ---如果不能立即发送,则阻塞
IPC_NOWAIT ---- 如果不能立即发送,则返回
*@retval int
成功返回0
失败返回-1,并返回错误码EOF
***********************************
*/
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
3. 从消息队列中接收消息 msgrcv
/**
***********************************
*@brief 从消息队列中接收消息
*@param msqid:消息队列id
msgp:消息的数据包,需要自己定义结构体
struct msgbuf {
long mtype; // 消息类型,且要大于0 第一个成员必须是消息类型
char mtext[10]; // 消息正文
};
msgsz:消息正文中消息的实际长度
msgtyp:消息的类型
0:按顺序接收消息队列中第一个消息。
> 0:接收消息队列中第一个类型为msgtyp的消息.
< 0:接收消息队列中类型值不大于msgtyp的绝对值且类型值又最小的消息。
msgflg: 0 ---若无消息函数会一直阻塞
IPC_NOWAIT ---- 若没有消息,进程会立即返回ENOMSG
*@retval ssize_t
成功返回接收到的消息的长度
失败返回-1,并返回错误码EOF
***********************************
*/
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
4. 管理消息队列对象 msgctl
/**
***********************************
*@brief 管理消息队列对象
*@param msqid:消息队列id
cmd:IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
IPC_SET: 设置消息队列的属性。这个值取自buf参数。
IPC_RMID:从系统中删除消息队列。
buf:cmd为IPC_RMID时,此处为NULL
*@retval int
成功返回0
失败返回-1,并返回错误码EOF
***********************************
*/
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
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) */
};
msg_write.c 和 msg_read.c 基本一样,用两个终端分别运行,代表两个不同的进程,实现两个进程的通信。代码按照上面的基操一步一步来,没什么难度,注释尽量写的详细了。
msg_write.c:
#include
#include
#include
#include
#include
#include
#define N 50 //消息队列的大小
//数据包需要自己定义
struct msgbuf{
long mytype; //第一个成员必须代表消息的类型
char mydate[N]; //变量类型可以自己随便定义
};
int main(int argc, char *argv[])
{
key_t key; //密钥
int msgid; //消息队列的id
struct msgbuf sm;
if((key = ftok("./",0xa)) < 0){ //手动创建密钥,可以参考共享内存里的key创建
perror("ftok error");
exit(1);
}
//创建消息队列,成功则返回队列的id
if((msgid = msgget(key,IPC_CREAT|0666)) < 0){
perror("msgget error");
exit(1);
}
while(1){
bzero(sm.mydate,sizeof(sm.mydate)); //将结构体里的数组清零
fgets(sm.mydate,sizeof(sm.mydate),stdin); //从键盘上输入字符串,字符串中包含\n
msgsnd(msgid,&sm,sizeof(struct msgbuf)-sizeof(long),0); //发送数据到消息队列里
}
//删除消息队列
if(msgctl(msgid,IPC_RMID,NULL) < 0){
perror("msgctl error");
exit(1);
}
return 0;
}
msg_read.c:
#include
#include
#include
#include
#include
#include
#define N 50
struct msgbuf{
long mytype;
char mydate[N];
};
int main(int argc, char *argv[])
{
key_t key;
int msgid;
struct msgbuf sm;
if((key = ftok("./",0xa)) < 0){
perror("ftok error");
exit(1);
}
if((msgid = msgget(key,IPC_CREAT|0666)) < 0){
perror("msgget error");
exit(1);
}
while(1){
msgrcv(msgid,&sm,sizeof(struct msgbuf)-sizeof(long),0,0); //从消息队列中读取数据
printf("%s",sm.mydate);
}
if(msgctl(msgid,IPC_RMID,NULL) < 0){
perror("msgctl error");
exit(1);
}
return 0;
}
效果如下:
mag_write 不断往队列里写数据,msg_read 不断地从队列里取数据,所以消息队列的占用的内存大小为0,当然也可以先执行 msg_write ,往里面写多条数据,那队列占用的字节的大小就为 sizeof(struct msgbuf) * n,n为输入的次数。msg_read 去掉while循环也可以每次执行就取一条数据,要怎么玩得看需求,需求决定一切。输入quit退出while循环我没写,可以参考共享内存的quit退出。
由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等。信号量,也叫信号灯(semaphore),它是不同进程间或一个给定进程内部不同线程间同步的机制,也属于进程间的通信。
信号灯的种类
1》二值信号灯:表示资源是否可用
值为0或1。与互斥锁类似,资源可用时值为1,不可用时值为0。
2》计数信号灯:表示资源的多少
值在0到n之间。用来统计资源,其值代表可用资源数
3》P/V操作
P为减,V为加,加减某个自定义的整数,灯的值为负数(value < 0)则阻塞,值大于等于零(value >= 0)不阻塞。
1》创建或获取信号量
2》对信号灯进行PV操作(P为减法,V为加法)
3》删除信号量---收尾
1. 创建或获取信号量 semget
#include
#include
#include
/**
***********************************
*@brief 创建或获取信号量
*@param key:密钥
nsems:信号灯的个数
semflg:IPC_CREAT|0666
*@retval int
成功返回id
失败返回-1,并返回错误码EOF
***********************************
*/
int semget(key_t key, int nsems, int semflg);
2. 对信号灯进行PV操作 semop
/**
***********************************
*@brief 对信号灯进行操作
*@param semid:信号量id
sops:结构体指针
nsops:要操作的信号灯的个数
*@retval int
成功返回0
失败返回-1,并返回错误码EOF
***********************************
*/
int semop(int semid, struct sembuf *sops, size_t nsops);
struct sembuf {
unsigned short sem_num; /* semaphore index in array 信号灯的编号,从0开始 */
short sem_op; /* semaphore operation -1:p操作,1:表示v操作*/
short sem_flg; /* operation flags 默认:0, IPC_NOWAIT, SEM_UNDO */
};
3. 管理信号量 semctl
/**
***********************************
*@brief 管理信号量
*@param semid:信号量id
semnum:信号灯的下标
cmd: IPC_RMID:删除信号量
GETVAL:获取到信号灯的value值
SETVAL:设置信号灯的value值
GETALL:获取多个信号灯的value值
SETALL:设置多个信号灯的value值
...:变参,如果cmd不是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) */
};
*@retval int
GETVAL:成功返回value值
其他成功返回0
失败返回-1,并返回错误码EOF
***********************************
*/
int semctl(int semid, int semnum, int cmd, ...);
先执行sem_P.c(./sem_P 1 -10),输入一个-10传给灯ss1的value,让该进程处于阻塞状态,然后执行sem_V.c(./sem_V.c 1 2),每次执行灯ss1就会加2,执行5次后ss1的value会加到10,此时原来ss1的value = -10会被抵消为0,sem_P.c解除阻塞,打印“NO WAIT”,这就是进程的同步。注意,每个灯都是独立的灯,要对同一个灯进程操作(P或V)。
sem_P.c:
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
if(argc != 3){ //main传参,第一个为灯的下标,第二个为负数,表示P减操作
printf("argment int[0,1,2] and int[-]\n");
exit(1);
}
key_t key;
int semid;
char str[50];
int nsem = atoi(argv[1]); //字符串转整数
int value = atoi(argv[2]);
if((key = ftok("./",0xb)) < 0){ //获取密钥
perror("ftok");
exit(1);
}
if((semid = semget(key,3,IPC_CREAT|0666)) < 0){ //创建有3个信号灯的信号量,返回id
perror("semget");
exit(1);
}
struct sembuf ss0 = {0,value,0}; //初始化灯0
struct sembuf ss1 = {1,value,0}; //初始化灯1
struct sembuf ss2 = {2,value,0}; //初始化灯2
struct sembuf buf[3] = {ss0,ss1,ss2};
/*
//对单个ss0灯进行初始化,和上面的效果是一样的
buf[0].sem_num = 0; //灯的下标
buf[0].sem_op = -4; //要加或减的值
buf[0].sem_flg = 0; //0为阻塞,IPC_NOWAIT不阻塞
*/
//value为负数则阻塞等待,直到另一个进程将value加到>=0即可解除阻塞
if(semop(semid,&buf[nsem],1) < 0){
perror("semop");
exit(1);
}
printf("NO WAIT\n");
//printf("value:%d\n",semctl(semid,nsem,GETVAL,NULL));
if(semctl(semid,0,IPC_RMID,NULL) < 0){ //删除信号量
perror("semctl");
exit(1);
}
return 0;
}
sem_V.c:
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
if(argc != 3){ //main传参,第一个参数为灯的下标,第二个为正整数,表示V加操作
printf("argment int[0,1,2] and int[+]\n");
exit(1);
}
key_t key;
int semid;
char str[50];
int nsem = atoi(argv[1]); //字符串转整数
int value = atoi(argv[2]);
if((key = ftok("./",0xb)) < 0){ //密钥
perror("ftok");
exit(1);
}
if((semid = semget(key,3,IPC_CREAT|0666)) < 0){ //创建有3个灯的信号量
perror("semget");
exit(1);
}
struct sembuf ss0 = {0,value,0}; //初始化灯
struct sembuf ss1 = {1,value,0};
struct sembuf ss2 = {2,value,0};
struct sembuf buf[3] = {ss0,ss1,ss2};
if(semop(semid,&buf[nsem],1) < 0){ //对下标为nsem(传进来的第一个参数)进行操作
perror("semop");
exit(1);
}
printf("value:%d\n",semctl(semid,nsem,GETVAL,NULL)); //打印当前操作的灯的值
return 0;
}
执行结果:
到此为止,进程间的通信基本介绍完了,早期:有名管道、无名管道、信号,system V IPC:共享内存、消息队列、信号量。除了这六种以外呢其实还有另一种进程间的通信——域通信,不过域通信就要结合TCP或者UDP来操作,基本和管道差不多,这部分有缘再更。
不难发现,这次讲的三种进程通信的操作有很多相似之处,它们都属于IPC对象,操作和函数基本差不多,比如:创建或获取(shmget、msgget、semget),管理(shmctl、msgctl、semctl),功能参数大同小异,我们可以记住其中一种操作,就可以举一反三。
进程通信的操作并不难,难就难在函数多,参数多,参数还结合了结构体联合体,就很恶心,这也是我大部分代码都是介绍函数的原因,重点在对各种API函数的介绍上。
此外这些例子只能单向通信,要实现双向通信就得加个进程(fork)或线程(pthread_ctreate),也不难,有兴趣的可以自己试一下。
ipcs -m //查看共享内存
ipcrm -m id //要删除的共享内存id
ipcs -q //查看消息队列
ipcrm -q id //要删除的消息队列id
ipcs -s //查看信号量
ipcrm -s id //要删除的信号量id