
目录
[三、system V进程间通信](#三、system V进程间通信)
[✍️system V共享内存](#✍️system V共享内存)
[✍️system V消息队列](#✍️system V消息队列)
[✍️system V信号量](#✍️system V信号量)
[✍️system V IPC(Inter-Process Communication,进程间通信)联系](#✍️system V IPC(Inter-Process Communication,进程间通信)联系)
三、system V进程间通信
我们之前说的管道通信本质就是基于文件的一种通信方式,也就说我们并没有让操作系统做太多的相关设计工作,而我们现在说的system V IPC是操作系统设计的一种通信方式,但是本质上和之前的是一样的,都是让不同的进程看到了同一份资源。
system V IPC通信的方式有下面三种:
1、system V共享内存(类比成一个共享白板,所有人都可以看到并操作)
2、system V消息队列(类比成一个老式的投递箱,顺序的投递和取出)
3、system V信号量(类比成马路上面的红绿灯,控制资源的访问)
这里面的前两个是为了传送数据的,而第三个信号量则是起到了一个保障我们进程间同步和互斥而设计。那么接下来,我们将一个一个地介绍他们几个:
✍️system V共享内存
📝共享内存的基本原理
共享内存的最终目的就是实现不同的进程可以看到同一份资源,那么它是怎么实现的呢?
实际上,它是在物理内存上申请了一块内存空间,然后将这块内存空间分别通过页表映射到了不同进程的页表上,再在虚拟地址空间上面开辟一段空间并把地址填入进页表的对应位置,这样就实现了进程看到了同一份物理内存,也就是说进程通过该虚拟地址访问共享内存的时候,最终就会装换成对同一块物理内存的读写操作,从而实现多个进程对同一份资源的访问了,这块物理内存就被称之为共享内存。

我们在使用操作系统时,系统中会有大量的进程,那么这么多的进程我们势必要对他们进行管理,也就是我们要对进程先描述再组织,描述则必然要有他们对应的数据结构。
共享内存的数据结构:
cpp
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
这里为了唯一标识一个共享内存,也就是让不同的进程看到的是同一个共享内存,因此每一个共享内存被申请的时候都有一个key值,这个key值用于标识系统中共享内存的唯一性。
这个key试讲上是在上面这个结构的第个成员shm_perm里面ipc_perm结构体如下:
cpp
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
说明一下:
这两个结构体分别在\include\linux\shm.h和\include\linux\ipc.h中定义。
📝共享内存的建立与释放的概念
共享内存的建立流程如下:
1、在物理内存申请共享内存空间
2、将申请到的共享内存空间通过页表映射到虚拟地址空间上。
共享内存的释放流程也包括了下面两个流程:
1、将共享内存和虚拟地址空间去除关联
2、释放共享内存空间
📝共享内存的创建
创建共享内存我们需要使用到shmget函数,shmget函数的函数原型如下:
cpp
int shmget(key_t key, size_t size, int shmflg);
参数说明:
- 参数key,表示的是等待创建的共享内存在系统中的唯一标识。
- 参数size,表示的是等待创建的共享内存的打下。
- 参数shmflg,表示创建共享内存的方式。
放回值说明:
- 调用成功,返回一个有效的共享内存标识符。
- 调用失败,返回-1。
说明一下:
在计算机编程中我们把具有标识某种资源能力的东西称之为句柄,这里的shmget函数的返回值实际上就是共享内存的句柄,后续操作中我可以使用这个句柄对指定的共享内存进行各种操作。
这里我们可能会有这样一个疑问,那就是我们的第一个参数是怎么传入的,实际上我们的第一个参数并不是我人为的干预,需要使用到ftok函数来进行获取:
函数原型如下:
cpp
key_t ftok(const char *pathname, int proj_id);
fork函数的作用就是根据一个存在的pathname和一个标识符转换成一个key值,称之为IPC键值。使用的时候需要注意的是我们的pathname所指定的文件必须是存在的且是可取的。
敲黑板:
使用ftok函数生成的key值可能会发生冲突,这个时候我们就要对传入的参数进行修改了。
传入shmget函数的第三个参数常用的有下面三种
宏名 | 作用说明 |
---|---|
IPC_CREAT |
若不存在则创建,返回对应的句柄 |
IPC_EXCL |
和 IPC_CREAT 一起使用,表示只在不存在时才创建并返回对应的句柄,否则报错 |
0 |
不创建,只尝试获取已存在的共享内存 |
我们这里可以写个代码试着创建出一块共享内存,并对相关星系进行打印:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#define PATHNAME "/root/xywl/test/test.c"
#define PROJ_ID 0x666
#define SIZE 4096
int main() {
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0) {
perror("ftok error");
exit(1);
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);
if(shm < 0) {
perror("shmget error");
exit(2);
}
printf("key: %x\n", key);
printf("shm: %d\n", shm);
return 0;
}
运行之后我们就可以看到key值和句柄值了:

事实上,我们在Linux当中可以使用ipcs命令来查看有关进程的通信设施的信息。

我们这里介绍几个对应的选项:
- -q:对应于消息队列的相关信息
- -m:对应于共享内存的相关信息
- -s:对应于信号量的相关信息
例如,我们可以使用**-m**选项查看共享内存的相关信息:

说明一下:
这里的各个列的信息如下:
列名 | 含义说明 |
---|---|
key |
通过 ftok 生成的键值(key_t ),用于识别 IPC 对象 |
shmid |
共享内存段 ID(通过 shmget 返回) |
owner |
创建共享内存段的用户 |
perms |
权限(八进制格式,如 666 ) |
bytes |
共享内存段的大小(以字节为单位) |
nattch |
当前有多少进程附加(attach)到该共享内存段 |
status |
状态标志(通常为空,或者包含 dest 等) |
📝共享内存的释放
我们发现即使我们的进程退出了,但是我们申请的共享内存依然存在,并没有被操作系统释放掉,实际上,我们这里需要进行区分,管道的生命周期是随进程的,然而我们的共享内存的生命周期是随内核的,也就是说即使我们的进程退出了,我们之前创建好的共享内存也是不会随着进程退出而释放的。
这也就是说我们要自己主动地删除创建好的共享内存,除非关机重启。这个时候我们有两种释放共享内存的函数:
使用命令释放共享内存的资源
我们可以使用ipcrm -m +shmid命令来释放我们的共享内存资源
bash
ipcrm -m 0

使用程序释放共享内存资源
控制共享内存我们需要使用的函数是shmctl,这个函数的原型如下:
bash
int schmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
- shmid,表示的是所控制的共享内存的用户级标识符。
- cmd,表示的是具体的控制动作。
- buf,表示的是用于获取或设置所控制的共享内存的数据结构。
返回值说明:
- 调用成功,返回0。
- 调用失败,返回-1。
这里重点介绍几个cmd的常用选项:
宏定义常量 | 值 | 含义 |
---|---|---|
IPC_STAT |
2 | 获取共享内存段的当前状态,将其填充到 struct shmid_ds 结构中。 |
IPC_SET |
1 | 设置共享内存段的某些属性(如权限),由 struct shmid_ds 提供信息(只能由超级用户或创建者设置)。 |
IPC_RMID |
0 | 删除共享内存段。立即使该段不可再附加,但在当前已附加进程分离后才真正释放。 |
我们这里写个代码演示一下第三个选项:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#define PATHNAME "/root/xywl/test/test.c"
#define PROJ_ID 0x666
#define SIZE 4096
int main() {
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0) {
perror("ftok error");
exit(1);
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);
if(shm < 0) {
perror("shmget error");
exit(2);
}
printf("key: %x\n", key);
printf("shm: %d\n", shm);
sleep(5);
shmctl(shm, IPC_RMID, NULL);
sleep(5);
return 0;
}
我们还可以写一个监控脚本来进行监控共享内存的状态:
bash
while :; do ipcs -m; echo"+++++++++++++++++++++++++++++++++++"; sleep 1; done
这里我们发现我们的共享内存资源成功被释放了。

📝共享内存的关联
将共享内存连接到进程地址空间上需要我们使用shmat函数,函数原型如下:
cpp
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
- shmid,表示的是关联共享内存的用户级标识符。
- shamddr,用户指定将共享内存映射到进程地址空间上的某一个地址,通常设置成NULL,表示的是让内核自己做出决定一个合适的位置。
- shmflg,表示关联共享内存时的某一些属性。
返回值说明:
- 调用成功,返回共享内存映射到进程地址空间的起始地址。
- 调用失败,返回(void*)-1。
其中作为shmat函数的第三个参数传入的常用的选项如下:
选项 | 描述 |
---|---|
SHM_RND |
允许将共享内存段的映射地址调整为符合系统要求的地址边界。 |
SHM_RDONLY |
以只读方式映射共享内存。 |
0 |
默认为读写权限 |
我们可以写个代码来验证一下:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#define PATHNAME "/root/xywl/test/test.c"
#define PROJ_ID 0x6666
#define SIZE 4096
int main() {
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0) {
perror("ftok error");
exit(1);
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);
if(shm < 0) {
perror("shmget error");
exit(2);
}
printf("key: %x\n", key);
printf("shm: %d\n", shm);
printf("attach begin!\n");
sleep(2);
char* mem = shmat(shm, NULL, 0);
if(mem == (void*)-1) {
perror("shmat error");
exit(1);
}
printf("end\n");
sleep(2);
shmctl(shm, IPC_RMID, NULL);
return 0;
}
我们运行之后发现,我们关联进程失败了,主要原因是我们使用shmget函数创建共享内存时,没有对创建的共享内存设置权限,所以进程没有权限关联共享内存。

这里我们应该在使用shmget函数的时候,在其第三个参数处设置共享内存的权限,权限的设置和设置文件的权限的方式一致。
cpp
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
这个时候我们再次运行我们的程序就可以关联成功了, 权限显示的是666,关联的进程个数也从0变成了1。

📝共享内存的去关联
我们要实现共享内存与进程地址空间去除关联需要使用我们的shmdt函数,函数的原型如下:
cpp
int shmdt(const void *shmaddr);
参数说明:
- shmaddr指的是待去关联的起始地址,也就是调用shmat函数的返回值。
返回值说明:
- 调用成功返回0。
- 失败返回-1。
我们同样可以写个代码来实现一下:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#define PATHNAME "/root/xywl/test2/test.c"
#define PROJ_ID 0x6667
#define SIZE 4096
int main() {
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0) {
perror("ftok error");
exit(1);
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shm < 0) {
perror("shmget error");
exit(2);
}
printf("key: %x\n", key);
printf("shm: %d\n", shm);
printf("attach begin!\n");
sleep(2);
char* mem = shmat(shm, NULL, 0);
if(mem == (void*)-1) {
perror("shmat error");
exit(1);
}
printf("attach end\n");
sleep(2);
printf("detach begin!\n");
sleep(2);
shmdt(mem);
printf("detch end!\n");
sleep(2);
shmctl(shm, IPC_RMID, NULL);
return 0;
}
我们运行程序之后,可以通过监控发现我们的关联数从1变成了0,也就是说我们取消了共享内存与该进程之间的关联。

📝使用共享内存实现server&client通信
我们之前使用的是管道通信,这里我们根据上面的知识点实现通过共享内存实现通信。
我们为了更加的严谨,可以先确保这两个进程成功挂接到了一个共享内存上。
服务端的逻辑:创建好了共享内存之后,将共享内存和服务端进程进行关联,之后进入进入死循环方便查看服务端是否挂接成功。
服务端代码:
cpp
#include "comm.h"
int main() {
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0) {
perror("ftok error");
exit(1);
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shm < 0) {
perror("shmget error");
exit(2);
}
printf("key: %x\n", key);
printf("shm: %d\n", shm);
char* mem = shmat(shm, NULL, 0);
while(1) {
// ...
}
shmdt(mem);
shmctl(shm, IPC_RMID, NULL);
return 0;
}
客户端逻辑:客户端只需要直接和服务端的共享内存进行关联即可,之后也是进入死循环,便于观察客户端进程是否挂接成功。
客户端代码:
cpp
#include "comm.h"
int main() {
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0) {
perror("ftok error");
exit(1);
}
int shm = shmget(key, SIZE, IPC_CREAT);
if(shm < 0) {
perror("shmget error");
exit(2);
}
printf("key: %x\n", key);
printf("shm: %d\n", shm);
char* mem = shmat(shm, NULL, 0);
int i = 0;
while(1) {
// ...
}
shmdt(mem);
return 0;
}
为了方便两个端访问的路径名和标识符一样,我们可以将这些公共的内容封装成一个公共头文件。
公共头文件代码:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/root/xywl/sharedmemory/server.c"
#define PROJ_ID 0x6666
#define SIZE 4096
打开监控,先后运行我们的客户端和服务端代码,我们观察到下面这个情况说明我们两个端都关联成功了,进程数变成了2。

挂接好之后,我们就可以来让服务端和客户端进行通信了,这里我们用发送字符串为例。
客户端行为:
cpp
int i = 0;
while(1) {
mem[i] = 'A' + i++;
mem[i] = '\0';
sleep(1);
}
服务端行为:
cpp
while(1) {
printf("client: %s\n", mem);
sleep(1);
}
运行效果如下:

📝管道和共享内存的对比
我们在实际操作之后发现,管道通信是需要调用read、write等系统调用接口的,而我们的共享内存是不需要这么做的,这也就是为什么共享内存是所有进程间通信最快的一种。
我们可以画图理解这两个通信的原理:
管道通信:

我们可以从图中发现,管道通信是有4次的拷贝操作的,两次读操作拷贝和两次写操作拷贝。
共享内存通信:

从这个图中我们可以知道使用共享内存进行通信,将一个文件从一个进程传输到了另一个进程只需要进行两次拷贝操作。所以共享内存是所有进程间通信方式中最快的一种方式,因为这样的方式需要进行的拷贝此时最少。但是这样做有一个明显的缺点,那就是我们的管道机制是自带了互斥和同步的,但是我们的共享内存是没有提供任何的保护机制的。
✍️system V消息队列
📝消息队列的基本原理
消息队列和它的名字一样,本质上是在系统中创建的一个队列,队列中的元素是一个一个由类型和信息所构成的数据块,两个可以同时看到这样一个消息队列,这两个进程收发数据的时候会在消息队列的队尾添加数据块(发),从队列的头部取数据块(收)。如图:

这里的数据块发给谁是由数据块的类型决定的。
敲黑板:
和共享内存一样,消息队列的资源也是必须要自行删除的,否则会自动的被清除掉,因为system V IPC资源的生命周期是随内核的。
📝消息队列的数据结构
我们上面也是介绍了,要对大量的数据块进行管理,必然是要有相关的内核数据结构。
数据结构如下:
cpp
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
我们这里看到这个消息队列的第一个成员是msg_perm,它和我们之前讲的shm_perm实际上是同一个类型的结构体变量,ipc_perm的结构如下:
cpp
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
这里简单说明一下:
这两个数据结构分别在/include/linux/msg.h和/include/linux/ipc.h当中定义。
📝消息队列的创建
我们要创建消息队列需要使用的函数是msgget,这个函数的原型如下:
cpp
int msgget(key_t key, int msgflg);
参数说明:
第一个参数和之前shmget函数一样,这里的key值是由ftok函数生成。
第二个参数和之前的shmget函数也是一样的。
返回值说明:
创建成功,函数返回一个有效的消息队列的标识符(用户层)。
📝消息队列的释放
释放消息队列我们需要用的是msgctl函数,函数的原型如下:
cpp
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数说明:
其实这里和我们之前说的shmctl函数是一样的,只是第三个参数传入的结构体不一样罢了。
📝向消息队列发送数据
我们向消息队列里面发送数据需要用到msgsnd函数,函数的原型如下:
cpp
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数说明:
第一个参数msqid,表示的是消息队列用户层标识符。
第二个参数msgp,表示的是待发送的数据块。
第三个参数msgflg,表示的是发送数据大小。
第四个参数msgflg,表示的是发送数据块的方式,一般默认是0。
返回值说明:
调用成功返回0。
调用失败放回-1。
其中msgsnd函数的第二个参数必须是下面的结构:
cpp
struct msgbuf {
long mtype; // 消息类型(必须是long类型)
char mtext[1]; // 消息内容(通常为字符数组)
};
简单说明一下:
这里的mtext就是待发送的消息,数组的大小是可以我们自己定的。
📝从消息队列里面获取数据
从消息队列里面获取数据使用的是msgrcv函数,msgrcv函数的原型如下:
cpp
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数说明:
- 第一个参数msqid,表示的是消息队列的用户层标识符。
- 第二个参数msgp,表示的是获取到的数据块,是一个输出型参数。
- 第三个参数msgsz,表示的是获取数据块的大小。
- 第四个参数msgtyp,表示的是要接收的数据块的类型。
返回值说明:
- msgsnd调用成功,返回的是mtext数组中的字节数。
- msgsnd调用失败,返回-1。
✍️system V信号量
📝信号量的概念
其实信号量是一个保障措施而不是通信方式。
这里我们来介绍几个相关的概念:
- 系统中的一些资源只允许一个进程使用,我称这样的资源,为临界资源或是互斥资源。
- 在进程中涉及到临界资源的程序段叫临界区。
- 由于进程要求共享资源,而且有些资源需要互斥使用,所以各个进程竞争使用这些资源,进程的这种关系叫做进程互斥。
📝信号量数据结构
我们的信号量也是要维护的,那么必然要有相关的数据结构来做支撑。
信号量的数据结构如下:
cpp
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
信号量数据结构的第一个成员是ipc_perm类型的结构体变量,定义如下:
cpp
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
这两个结构体分别在/include/linux/msg.h和/include/linux/ipc.h中
📝信号量集的创建
创建信号量我们需要使用semget函数,semget函数的原型是:
cpp
int semget(key_t key, int nsems, int semflg);
参数说明:
第一个参数key,这里的key也是由ftok函数生成的。
第二个参数nsems,表示的是创建信号量的个数。
第三个参数semget,与创建共享内存时使用的shmget函数的第三个参数是一样的。
返回值说明:
信号量创建成功,返回的是一个有效的信号量标识符(用户层)。
📝信号量集的删除
幸好领的删除需要使用的是semctl函数,函数的原型如下:
cpp
int semctl(int semid, int semnum, int cmd, ...);
参数说明:
- 第一个参数semid, 信号量集合的标识符
- 第二个参数semum, 这个是信号量的索引
- 第三个参数cmd,是操作命令,决定了semctl的行为
选项 | 描述 |
---|---|
IPC_STAT |
获取信号量集合的状态信息(struct semid_ds )。此时会将信号量集合的状态信息返回到一个 semid_ds 结构体中。 |
IPC_SET |
设置信号量集合的属性(如权限、时间戳等)。需要传入一个 struct semid_ds 结构体。 |
IPC_RMID |
删除信号量集合。信号量集合被销毁,所有关联的资源被释放。 |
GETVAL |
获取指定信号量的当前值。返回指定信号量的值。 |
SETVAL |
设置指定信号量的值。传入一个 union semun ,其 val 字段指定信号量的值。 |
GETPID |
获取最后操作该信号量的进程的 PID(进程 ID)。返回该进程的 PID。 |
GETALL |
获取信号量集合中所有信号量的值。返回一个 unsigned short 数组,表示集合中每个信号量的当前值。 |
SETALL |
设置信号量集合中所有信号量的值。传入一个 unsigned short 数组,设置集合中每个信号量的值。 |
- 第四个参数...,是用来添加附加信息的。
📝信号量集的操作
实现对信号量集的操作使用的函数是semop,函数的原型如下:
cpp
int semop(int semid, struct sembuf *sops, unsigned nspos);
参数说明:
- 第一个参数semid,是由semget返回,是信号量集合的标识符。
- 第二个参数sops,是一个指向struct sembuf类型数组的指针。
- 第三个参数nsops,操作数组sops中元素数量,即信号量操作的个数。
返回值说明:
- 调用成功返回0。
- 调用失败放回-1。
下面是struct sembuf的结构体:
cpp
struct sembuf {
unsigned short sem_num; // 信号量集合中的信号量的索引
short sem_op; // 信号量的操作,通常是增或减
short sem_flg; // 操作的标志(通常是 0 或 IPC_NOWAIT)
};
📝信号量机制
我们之前也对比了管道和共享内存,发现共享内存虽然效率更优,但是也带来了新的问题,那就是不同进程共用了临界资源,如果对临界资源不加以保护的话,这会导致各个进程之间发生数据不一致的等问题。
保护临界资源就是保护临界区,我们把进程访问临界资源的代码称之为临界区,信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量。
信号量的本质就是一个计数器,在二元信号量中计数器只有0和1,二元信号量解决了临界资源的问题,下面是实现逻辑的过程:

我们来解释一下上面的逻辑,当进程A访问共享资源的时候,这个时候的sem的值是1(这个数量表示的是信号量的个数),这个时候我们的进程A就申请资源成功了,这个时候需要将sem--,然后我们的进程A就可以访问共享资源了,这个时候我们的进程B申请访问共享内存资源,由于这个时候的sem的值一件事0了,我们的进程B会被挂起,直到A访问共享内存结束sem++,这个时候进程B被唤醒,然后B就可以对共享内存进行访问了。这样一来,我们无论是在什么时候只有一个进程对同一份资源进行访问,这也就解决了临界资源的互斥问题。
我们这里说的还是比较的不专业,这里的sem--实际上叫做P操作,sem++叫做V操作,一个是申请而另一个则是释放。如图:

✍️system V IPC(Inter-Process Communication,进程间通信)联系
我们根据上面的知识点发现共享内存、消息队列和信号量,虽然内部的属性差别很大,但是他们数据结果的第一个成员都是ipc_perm类型的成员变量。
这个设计的好处是在操作系统内可以定义一个ipc_perm类型的数组,当我们申请IPC资源的时候就可以开辟出来。也就是说我们在内核里面所有的IPC资源的获取都可以通过切片获得该IPC资源的起始地址的方式获取了该IPC资源的每一个成员了。