linux学习:进程通信(system-V IPC+消息队列+共享内存)

目录

system-V IPC 简介

api ftok

注意

使用命令来查看或删除当前系统中的 IPC 对象:

消息队列(MSG)

使用方法

api msgget

api msgsnd msgrcv

api msgctl

例子

共享内存(SHM)

使用方法

api shmget

api shmat  shmdt

api shmctl

例子


system-V IPC 简介

消息队列、共享内存和信号量被统称为 system-V IPC,V 是罗马数字 5,是 Unix 的 AT&T 分支的其中一个版本,一般习惯称呼他们为 IPC 对象,这些对象的操作接口都比较 类似,在系统中他们都使用一种叫做 key 的键值来唯一标识,而且他们都是“持续性”资 源——即他们被创建之后,不会因为进程的退出而消失,而会持续地存在,除非调用特殊的函数或者命令删除他们。

跟文件类型,进程每次“打开”一个 IPC 对象,就会获得一个表征这个对象的 ID,进 而再使用这个 ID 来操作这个对象。IPC 对象的 key 是唯一的,但是 ID 是可变的。key 类 似于文件的路径名,ID 类似于文件的描述符

api ftok

注意

  • 如果两个参数相同,那么产生的 key 值也相同
  • 第一个参数一般取进程所在的目录,因为在一个项目中需要通信的几个进程通常会 出现在同一个目录当中。
  • 如果同一个目录中的进程需要超过 1 个 IPC 对象,可以通过第二个参数来标识。
  • 系统中只有一套 key 标识,也就是说,不同类型的 IPC 对象也不能重复

使用命令来查看或删除当前系统中的 IPC 对象:

  • 查看消息队列:ipcs -q
  • 查看共享内存:ipcs -m
  • 查看信号量:ipcs -s
  • 查看所有的 IPC 对象:ipcs -a
  • 删除指定的消息队列:ipcrm -q MSG_ID 或者 ipcrm -Q msg_key
  • 删除指定的共享内存:ipcrm -m SHM_ID 或者 ipcrm -M shm_key
  • 删除指定的信号量:ipcrm -s SEM_ID 或者 ipcrm -S sem_key

消息队列(MSG)

消息队列提供一种带有数据标识的特殊管道,使得每一段被写入的数据都变成带标识的 消息,读取该段消息的进程只要指定这个标识就可以正确地读取,而不会受到其他消息的干 扰,从运行效果来看,一个带标识的消息队列,就像多条并存的管道一样

使用方法

  1. 发送者
    1. 获取消息队列的 ID
    2. 将数据放入一个附带有标识的特殊的结构体,发送给消息队列。
  2. 接收者
    1. 获取消息队列的 ID
    2. 将指定标识的消息读出。

当发送者和接收者都不再使用消息队列时,及时删除它以释放系统资源

api msgget

注意

  • 选项 msgflg 是一个位屏蔽字,因此 IPC_CREAT、IPC_EXCL 和权限 mode 可以 用位或的方式叠加起来,比如:msgget(key, IPC_CREAT | 0666); 表示如果 key 对应 的消息队列不存在就创建,且权限指定为 0666,若已存在则直接获取 ID
  • 权限只有读和写,执行权限是无效的,例如 0777 跟 0666 是等价的
  • 当 key 被指定为 IPC_PRIVATE 时,系统会自动产生一个未用的 key 来对应一个 新的消息队列对象。一般用于线程间通信

api msgsnd msgrcv

注意

  • 发送消息时,消息必须被组织成以下形式
    struct msgbuf
    {
        long mtype; // 消息的标识
        char mtext[1]; // 消息的正文
    };

    也就是说:发送出去的消息必须以一个 long 型数据打头,作为该消息的标识,后 面的数据则没有要求

  • 消息的标识可以是任意长整型数值,但不能是 0L
  • 参数 msgsz 是消息中正文的大小,不包含消息的标识

api msgctl

注意

  • IPC_STAT 获得的属性信息被存放在以下结构体中:
    struct msqid_ds
    {
        struct ipc_perm msg_perm; /* 权限相关信息 */
        time_t msg_stime; /* 最后一次发送消息的时间 */
        time_t msg_rtime; /* 最后一次接收消息的时间 */
        time_t msg_ctime; /* 最后一次状态变更的时间 */
        unsigned long __msg_cbytes; /* 当前消息队列中的数据尺寸 */
        msgqnum_t msg_qnum; /* 当前消息队列中的消息个数 */
        msglen_t msg_qbytes; /* 消息队列的最大数据尺寸 */
        pid_t msg_lspid; /* 最后一个发送消息的进程 PID */
        pid_t msg_lrpid; /* 最后一个接收消息的进程 PID */
    };

    权限相关的信息结构体

    struct ipc_perm
    {
        key_t __key; /* 当前消息队列的键值 key */
        uid_t uid; /* 当前消息队列所有者的有效 UID */
        gid_t gid; /* 当前消息队列所有者的有效 GID */
        uid_t cuid; /* 当前消息队列创建者的有效 UID */
        gid_t cgid; /* 当前消息队列创建者的有效 GID */
        unsigned short mode; /* 消息队列的读写权限 */
        unsigned short __seq; /* 序列号 */
    };
  • 当使用 IPC_INFO 时,需要定义一个如下结构体来获取系统关于消息队列的限制值 信息,并且将这个结构体指针强制类型转化为第三个参数的类型
    struct msginfo
    {
        int msgpool; /* 系统消息总尺寸(千字节为单位)最大值 */
        int msgmap; /* 系统消息个数最大值 */
        int msgmax; /* 系统单个消息尺寸最大值 */
        int msgmnb; /* 写入消息队列字节数最大值 */
        int msgmni; /* 系统消息队列个数最大值 */
        int msgssz; /* 消息段尺寸 */
        int msgtql; /* 系统中所有消息队列中的消息总数最大值 */
        unsigned short int msgseg; /* 分配给消息队列的数据段的最大值 */
    };
  • 当使用选项 MSG_INFO 时,跟 IPC_INFO 一样也是获得一个 msginfo 结构体的 信息,但是有如下几点不同
    • 成员 msgpool 记录的是系统当前存在的 MSG 的个数总和
    • 成员 msgmap 记录的是系统当前所有 MSG 中的消息个数总和
    • 成员 msgtql 记录的是系统当前所有 MSG中的所有消息的所有字节数总和

例子

进程 Jack 如何使用消息队列给另一个进程 Rose 发送消息的过 程,以及如何使用 msgctl( )函数,删除不再使用的消息队列

head4msg.h

1 #ifndef _HEAD4MSG_H_
2 #define _HEAD4MSG_H_
3
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <unistd.h>
7 #include <string.h>
8 #include <strings.h>
9 #include <errno.h>
10
11 #include <sys/types.h>
12 #include <sys/ipc.h>
13 #include <sys/msg.h>
14
15 #define MSGSIZE 64 // 单个消息最大字节数
16
17 #define PROJ_PATH "." // 使用当前路径来产生消息队列的键值 key
18 #define PROJ_ID 1
19
20 #define J2R 1L // Jack 发送给 Rose 的消息标识
21 #define R2J 2L // Rose 发送给 Jack 的消息标识
22
23 struct msgbuf // 带标识的消息结构体
24 {
25     long mtype;
26     char mtext[MSGSIZE];
27 };
28
29 #endif

Rose.c

1 #include <signal.h>
2 #include "head4msg.h" 3
4 int main(int argc, char **argv)
5 {
6     key_t key = ftok(PROJ_PATH, PROJ_ID);// 获取消息队列的key
7     int msgid = msgget(key, IPC_CREAT | 0666); // 获取消息队列 ID
8
9     struct msgbuf buf;// 发送消息的结构体
10     bzero(&buf, sizeof(buf));// 清0
11
12     if(msgrcv(msgid, &buf, MSGSIZE, J2R, 0) == -1) // 等待接收消息标识为J2R的消息
13     {
14         perror("msgrcv() error");
15         exit(1);
16     }
17     printf("from msg: %s", buf.mtext);
18
19     msgctl(msgid, IPC_RMID, NULL); // 删除消息队列
20     return 0;
21 }

Jack.c

1 #include "head4msg.h" 2
3 int main(int argc, char **argv)
4 {
5     key_t key = ftok(PROJ_PATH, PROJ_ID);// 获取消息队列的key
6     int msgid = msgget(key, IPC_CREAT | 0666); // 获取消息队列 ID
7
8     struct msgbuf message;// 发送消息的结构体
9     bzero(&message, sizeof(message));
10
11     message.mtype = J2R; // 指定该消息标识
12     strncpy(message.mtext, "I love you! Rose.\n", MSGSIZE);
13     // 发送该消息
14     if(msgsnd(msgid, &message, strlen(message.mtext), 0) != 0)
15     {
16         perror("msgsnd() error");
17         exit(1);
18     }
19
20     return 0;
21 }

他跟管道一样,都是需要“代理人”的进程通信机 制:内核充当了这个代理人,内核为使用者分配内存,检查边界,设置阻塞,以及各种权限 监控,使得我们用起来非常省心省力,但是任何事情都是有代价的:代理人机制使得他们的 效率都不高,因为两个进程的数据传递并不是直接了当的,而是要经过内核的辗转接力的, 因而他们都不适合用来传输海量数据 

共享内存(SHM)

共享内存是效率最高的 IPC,因为他抛弃了内核这个“代理人”,直截了当地将一块裸 露的内存放在需要数据传输的进程面前,让他们自己搞,这样的代价是:这些进程必须小心 谨慎地操作这块裸露的共享内存,做好诸如同步、互斥等工作,毕竟现在没有人帮他们来管 理了,一切都要自己动手。也因为这个原因,共享内存一般不能单独使用,而要配合信号量、 互斥锁等协调机制,让各个进程在高效交换数据的同时,不会发生数据践踏、破坏等意外

共享内存的思想很朴素,进程与进程之间虚拟内存空间本来相互独立,不能互相访问的, 但是可以通过某些方式,使得相同的一块物理内存多次映射到不同的进程虚拟空间之中,这 样的效果就相当于多个进程的虚拟内存空间部分重叠在一起

使用方法

  1. 获取共享内存对象的ID
  2. 将共享内存映射至本进程虚拟内存空间的某个区域
  3. 当不再使用时,解除映射关系
  4. 当没有进程再需要这块共享内存时,删除它

api shmget

所谓的“大页面”指的是内核为了提高程序性能,对内存实行分页管理时,采用比默认 尺寸(4KB)更大的分页,以减少缺页中断。Linux 内核支持以 2MB 作为物理页面分页的 基本单位

api shmat  shmdt

注意

  • 共享内存只能以只读或者可读写方式映射,无法以只写方式映射
  • shmat( )第二个参数 shmaddr 一般都设为 NULL,让系统自动找寻合适的地址。 但当其确实不为空时,那么要求 SHM_RND 在 shmflg 必须被设置,这样的话系统将会选 择比 shmaddr 小而又最大的页对齐地址(即为 SHMLBA 的整数倍)作为共享内存区域的 起始地址。如果没有设置 SHM_RND,那么 shmaddr 必须是严格的页对齐地址。 总之,映射时将 shmaddr 设置为 NULL 是更明智的做法,因为这样更简单,也更具移植性
  • 解除映射之后,进程不能再允许访问 SHM

api shmctl

注意

  • IPC_STAT 获得的属性信息被存放在以下结构体中
    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; /* 映射该 SHM 的进程个数 */
    };

    权限信息结构体

    struct ipc_perm
    {
        key_t __key; /* 该 SHM 的键值 key */
        uid_t uid; /* 所有者的有效 UID */
        gid_t gid; /* 所有者的有效 GID */
        uid_t cuid; /* 创建者的有效 UID */
        gid_t cgid; /* 创建者的有效 GID */
        unsigned short mode; /* 读写权限 +
            SHM_DEST +
            SHM_LOCKED 标记 */
        unsigned short __seq; /* 序列号 */
    };

  • 当使用 IPC_RMID 后,上述结构体 struct ipc_perm 中的成员 mode 将可以检测 出 SHM_DEST,但 SHM 并不会被真正删除,要等到 shm_nattch 等于 0 时才会被真正 删除。IPC_RMID 只是为删除做准备,而不是立即删除
  • 当使用 IPC_INFO 时,需要定义一个如下结构体来获取系统关于共享内存的限制值 信息,并且将这个结构体指针强制类型转化为第三个参数的类型
  • 使用选项 SHM_INFO 时,必须保证宏_GNU_SOURCE 有效。获得的相关信息被 存放在如下结构体当中
    struct shm_info
    {
        int used_ids; /* 当前存在的 SHM 个数 */
        unsigned long shm_tot; /* 所有 SHM 占用的内存页总数 */
        unsigned long shm_rss; /* 当前正在使用的 SHM 内存页个数 */
        unsigned long shm_swp; /* 被置入交换分区的 SHM 个数 */
        unsigned long swap_attempts; /* 已废弃 */
        unsigned long swap_successes; /* 已废弃 */
    };
    
  • 选项 SHM_LOCK 不是锁定读写权限,而是锁定 SHM 能否与 swap 分区发 生交换。一个 SHM 被交换至 swap 分区后如果被设置了 SHM_LOCK,那么任何访问这个 SHM 的进程都将会遇到页错误。进程可以通过 IPC_STAT 后得到的 mode 来检测 SHM_LOCKED 信息

例子

进程 Jack 如何通过 SHM 给进程 Rose 发送一段数据的过程。 在 Rose 将数据打印出来之后,给 Jack 发送一个信号通知 Jack 将该 SHM 删除

head4shm.h

1 #ifndef _HEAD4SHM_H_
2 #define _HEAD4SHM_H_
3
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <unistd.h>
7 #include <string.h>
8 #include <signal.h>
9 #include <strings.h>
10
11 #include <sys/types.h>
12 #include <sys/ipc.h>
13 #include <sys/shm.h>
14
15 #define SHMSZ 1024
16
17 #define PROJ_PATH "." 
18 #define PROJ_ID 100
19
20 #endif

Jack.c

1 #include "head4shm.h" 2 #include <signal.h>
3
4 int shmid;
5
6 void rmid(int sig)
7 {
8     shmctl(shmid, IPC_RMID, NULL); // 信号来了就把 SHM 删除掉
9 }
10
11 int main(int argc, char **argv)
12 {
13     signal(SIGINT, rmid);
14
15     key_t key = ftok(PROJ_PATH, PROJ_ID);// 获取key
16     shmid = shmget(key, SHMSIZE, IPC_CREAT|0666);// 获取共享内存的ID
17
18     char *p = shmat(shmid, NULL, 0);// 对共享内存进行映射
19     bzero(p, SHMSIZE);// 清空共享内存
20
21     pid_t pid = getpid(); // Jack 将自身的 PID 放入 SHM 的前 4 字节里
22     memcpy(p, &pid, sizeof(pid_t));
23
24     fgets(p+sizeof(pid_t), SHMSZ, stdin); // 从键盘将数据填入 SHM
25     pause(); // 等到 Rose 的信号去删除 SHM
26
27     return 0;
28 }

Rose.c

1 #include "head4shm.h" 2
3 int main(int argc, char **argv)
4 {
5     key_t key = ftok(PROJ_PATH, PROJ_ID);// 获取key
6     int shmid = shmget(key, SHMSIZE, 0666);//获取共享内存的ID
7
8     char *p = shmat(shmid, NULL, 0);
9     printf("from SHM: %s", p+sizeof(pid_t)); // 打印 Jack 的信息
10
11     kill(*((pid_t *)p), SIGINT); // 数据已经读完,发信号给 Jack
12     shmdt(p);//解除映射
13
14     return 0;
15 }

必须先运行 Jack,而且必须输入数据,然后 Rose 才能运行,否则 Rose 不能获取 Jack 的信息

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值