Ssystem V 共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核
,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据,而是直接使用内存中的共享区。
我们接下来认识一下常用的接口
接口
shmget 创建共享内存
需要同时引入<sys/ipc.h>
<sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key
是生成共享内存标示符
的 关键字,唯一的key
值能返回唯一的共享内存标示符
,这是获得同一个共享内存的关键参数size
是指共享内存的大小,按字节算shmflg
是一个位图,控制创建时的行为和 共享内存文件的权限
(缺省时为0
),常见选项如下IPC_CREAT
:单独一个时,如果申请的共享内存不存在,就创建,然后返回;若存在,则获取并返回IPC_CREAT | IPC_EXCL
: 如果申请的共享内存不存在,则创建;若存在,则出错并返回-1
IPC_EXCL
:不能单独使用IPC_CREAT | 0666
:创建一个权限为0666
的共享内存文件,注:0666
可改成其它权限
返回值
:若成功,则返回共享内存标示符
;若失败,则返回-1
由上可知,保证key
的唯一性是获得同一个共享内存的关键步骤,那么如何获得唯一的key_t
类型的呢?
这里使用新的接口ftok
,(需同时引入<sys/types.h>
和 <sys/ipc.h>
)
key_t ftok(const char *pathname, int proj_id);
pathname
:必须指向一个存在的目录,或者有权限的文件proj_id
: 该参数必须是非零的且至少有八位有效位的整型,可传0x8888
这样的大整型返回值
:成功时生成key
,失败时返回-1
经过一系列操作便可以创建共享内存了
查看共享内存
在shell
命令里输入命令ipcs -m
就可以看到的共享内存列表
可以看到我创建了一个大小为1145
比特的共享内存
然而共享内存的声明周期与内核相同,必须要手动删除,所以在命令行上还有指令ipcrm -m
指令删除共享内存
那么应该用表里的key
还是shmid
呢?结论是在用户层,统一使用shmid
管理共享内存(毕竟全称就是 共享内存描述符
)
例如上图就需要输入ipcrm -m 0
可以看到共享内存被删掉了
共享内存的权限
ipcs -m
列表中的perms
列就是共享内存文件的权限,没错,共享内存也是文件
当创建时没指定权限时,则默认为全0
,若要指定权限,需要在shmflg
处在|
上权限,例如0666
共享内存的挂接数
ipcs -m
列表中的nattch
下标注了共享内存的挂接数
shmat 挂接共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
:前面获取的共享内存描述符
shmadder
:一般传nullptr
让系统自动选择挂接用的共享内存段地址shmflg
:位图0
:传0
时默认以读写模式
挂接SHM_RDONLY
: 以只读模式
挂接- 没有只写模式挂接
返回值
: 若成功,返回共享内存段地址;若失败,返回(void *) -1
shmdt 取消挂接
虽然进程退出时会自动取消挂接,但如果要在进程内取消挂接,就要用shmdt
函数取消挂接
int shmdt(const void *shmaddr);
把shmat
返回的指针传进去即可
读写共享区内存
挂接上的进程是真正意义上的看到同一块内存,而且完全可以像malloc
申请出的一段内存一样操作,比如把共享区的内存当成字符串的缓冲区,直接把标准输入用fgets
拷贝到共享内存中
不过默认的共享内存并没有同步互斥行为,需要额外控制,比如使用FIFO命名管道
来完成同步操作
shmctl 控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
该函数内容较多,下面只列举常见用法,更多信息请查阅man
手册
shmid
:共享内存描述符cmd
:控制指令,有很多种IPC_RMID
:由共享内存创建者调用,声明该段共享内存已经取消挂接,传该参数时,后面的buf
可传nullptr
IPC_STAT
:获取共享内存的状态并拷贝到buf
指向的内存中
buf
:输出型参数,指向输出的内存缓冲区
shmid_ds
的部分声明如下
1 | struct shmid_ds { |
进程互斥
由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥。
临街资源
:系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源
或互斥资源
。就好比一个萝卜一个坑临界区
: 在进程中涉及到互斥资源的程序段叫临界区
同步
: 内存共享中的同步,主要指使写入
和读取
操作互斥
,使二者有明确的先后顺序,能够在共享内存中一次写入或读取完整报文
在共享内存中,使用FIFO命名管道
建立另一条进程间通信,就能较为简单地完成同步功能:
- 读端等待命名管道的信息
- 写端完成写入后利用命名管道,写入完成写入的信息,并等待读端的应答
- 读端接收到命名管道的信息后,才开始读取共享内存的内容
- 读端完成任务后向写端发送应答,写端返回第
1
步 - 写端开始继续写入
附加:System V 消息队列 和 信号量
System V 还提供了消息队列和信号量用于进程间通信
消息队列
消息队列是由内核维护的一种数据结构,用法和普通的队列一样,可以push
和pop
数据块,用于进程间通信
接口
头文件
1 |
msgget
int msgget(key_t key, int msgflg);
用于申请消息队列并获得消息队列id
key
:用法和获取与上一致,这里不赘述了msgflg
:创建消息队列的选项,基本和上文shmget
相同,内容较多,完整内容需翻阅man
手册IPC_CREAT
:单独一个时,如果申请的共享内存不存在,就创建,然后返回;若存在,则获取并返回IPC_CREAT | IPC_EXCL
: 如果申请的共享内存不存在,则创建;若存在,则出错并返回-1
IPC_EXCL
:不能单独使用IPC_CREAT | 0666
:创建一个权限为0666
的共享内存文件,注:0666
可改成其它权限
返回值
:若成功,则返回消息队列id
;若失败,则返回-1
查看消息队列
ipcs -q
可以查看所有可用的消息队列,剩余操作与上文相同
msgctl
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msgctl
可用于控制消息队列
该函数内容较多,下面只列举常见用法,更多信息请查阅man
手册
msqid
:共享内存描述符cmd
:控制指令,有很多种IPC_RMID
:由共享内存创建者调用,声明该段共享内存已经取消挂接,传该参数时,后面的buf
可传nullptr
IPC_STAT
:获取共享内存的状态并拷贝到buf
指向的内存中
buf
:输出型参数,指向输出的内存缓冲区
1 | struct msqid_ds { |
向消息队列收发消息
msgsnd
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgsnd
用于向消息队列发送消息
msqid
:消息队列的id
msgp
:是指向结构体数据块(消息)的指针msgsz
:数据块的大小,按字节算msgflg
: 位图,具体选项详见man
手册,一般使用可以为0
- 返回值:失败时返回
-1
,成功时返回0
其中,msgp
要遵循如下格式
1 | struct msgbuf { |
其中第一个成员必须是long mtype
,且大于0
。
而第二个成员是字符数组用于储存字节数据,长度没有限制,因此函数传参也是void*
,适配各种长度的结构体指针
msgrcv
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msgrcv
用于从消息队列中接收数据(消息)
msqid
:消息队列的id
msgp
:是指向数据块(消息)的指针msgsz
:数据块的大小,按字节算msgtyp
:消息的类型msgflg
: 位图,具体选项详见man
手册,一般使用可以为0
- 返回值:失败时返回
-1
,成功时返回读取的字节数
信号量
信号量的本质是是一种计数器,但它特殊在只能互斥访问
,它本身就是一种互斥资源,但也可用于描述另一种临界资源的多少
信号量的工作原理:
- 申请计数器成功,就表示我有访问资源的权限了
- 申请了计数器资源,不代表当前我要访问资源了。当前只是预定了资源
- 计数器可以有效保证进入共享资源的执行流的量
特殊情况
当信号量只能为0
或1
时,即二元信号量
,该信号量就可以作为一把互斥锁
来使用,访问前申请信号量,访问完毕再释放信号量即可
系统调用接口
这里不作详细介绍,具体用法请翻阅man
手册
semget
用于获取一个或多个信号量semctl
用于控制信号量,可以初始化
,删除
,获取状态参数
等semop
用于获取信号量或释放信号量