System V IPC
主要有三种通信方式:共享内存、消息队列、信号量;
现在来简单了解一下消息队列和信号量;以及内核当中是如何管理System VIPC
资源的。
消息队列
消息队列的原理
要想实现进程间通信,就要让进程先看到同一份资源,管道是让进程看到同一个文件、共享内存是让进程看到同一份内存;
而消息队列就是在内核中维护一个队列,让两个进程看到同一个队列,让在这一个队列中读取和写入数据。

消息队列的接口
1. msgget
创建消息队列

int msgget(key_t key, int msgflg);
参数:
key_t key
:和共享内存的key
一样,内核当做用来区别消息队列的标识符;由ftok
生成;
int msgflg
:和shmget
第二个参数一样,标志位:IPC_CREAT
、IPC_EXCL
以及带上权限。
返回值:
如果创建成功,就返回消息队列的唯一标识符;
认个错创建失败就返回
-1
,并且错误码被设置。
2. msgctl
控制消息队列
首先,释放消息队列可以使用指令
ipcrm -q
.
消息队列和共享内存一样,声明周期是随内核的;只要我们不释放,它就一直存在(系统关机/重启就没了)。
删除消息队列使用的接口是msgctl

msgctl
是对消息队列进行管理的接口,可以使用它删除消息队列,也可以使用它来获取内核数据结构对象等等。
参数:
int msgid
:消息队列的标识符id
,msgget
创建消息队列的返回值。op
:标志位,传递要对消息队列进行什么操作;删除IPC_RMID
、获取内核数据结构对象IPC_STAT
。struct msgid_ds *buf
:输出型参数,如果要获取内核数据结构对象,就传递对应类型的指针;这里删除消息队列不关系就传nullptr
返回值:
这里只关心释放消息队列:
- 如果释放成功就返回
0
;- 释放失败就返回
-1
,并且错误码被设置。
3. msgsnd
发送信息
共享内存在使用时需要映射到进程的地址空间中,空间拿到起始虚拟地址,这样就可以拿着起始虚拟地址去访问共享内存了;
而消息队列不同,在进程中只能拿到消息队列的标识符,那要想发送和接受信息,就只能通过系统调用;

发送信息用到的系统调用就是msgsnd
参数:
int msqidid
:消息队列的标识符,msgget
的返回值。size_t msgsz
:表示要写入内容的大小。int msgflg
:标志位,这里传0
即可
这里msgsnd
第二个参数,void msgp[.msgsz]
,本质上就是一个指针;
但是在发送信息时,需要我们自己构建一个类型,这个自定义类型形式如下:

这个自定义类型存在两个成员:
long mtype
:表示发送信息的类型,要求必须大于0
;为什么要存在这个呢?简单来说就是进程
A
和进程B
要进行通信,要发送信息,那在读取信息时要知道这个信息是谁发送的;这个成员就来标识是谁发送的信息。
char mtext[1]
:这个成员是一个柔性数组,这里存储的就是我们要发送的数据。这里注意:一般情况下
msgsnd
中的msgsz
参数指的是我们要发送数据的大小,不算mtype
标志位。
4. msgrcv
接受信息
能够发送信息,那也就可以接受信息;接受信息用到的系统调用是msgrcv

先来看msgrcv
的参数:
msqid
:消息队列的标识符;msgp[.msgsz]
:这是一个输出性参数参数,读取到的内容就存储在这个输出型参数指针指向的内容当中。msgsz
:标识要读取内容的大小;(一般情况下,并不算mtype
标志位)msgtyp
:这个参数指的是,要读取谁发送的信息(在发送信息时要带上mtype
标志位就是记录是谁发送的信息)msgflg
:标志位,这里传0
即可。
并发编程
System V IPC
进程间通信有三种通信方式:共享内存、消息队列和信号量;
而信号量主要是用于同步和互斥的,这里简单提及一下并发编程的相关概念。
共享资源:多个进程能够看到的同一份资源。
要进行进程间通信,首先就要让不同的进程看到同一份资源。
临界资源:被保护起来的共享资源。
临界区:在进程中访问临界资源的程序段
看到同一份资源,这是进程间通信的前提;但是没有数据保护机制就会导致数据不一致。(共享内存,数据写入和读取就没有保护机制);
那如何进行数据保护呢?如何进行保护呢?
在进程中访问临界资源的程序端,也就是临界区,只要将临界区保护起来不就做到了保护共享资源的数据吗。
保护方法:加锁;保证只有一个进程同时访问临界资源、让进程访问临界资源有一定的顺序性。
互斥:任意时刻,只允许一个进程访问临界资源。
同步:多个执行流在访问临界资源时,具有一定的顺序性。
保护临界区,做法就是加锁;简单来说就是一个进程要访问临界资源,先申请锁,如果申请成功,其他在要访问临界资源要申请锁就申请失败,无法访问临界资源;进程在访问完临界资源之后,释放锁;这样其他进程就可以申请锁然后访问临界资源。
锁具有原子性
锁的原子性是指锁操作(获取和释放)作为一个不可分割的单元执行的特性。这是锁机制能够正确实现线程同步的基础。
- 不可分割性:锁的获取和释放操作在执行过程中不会被中断,要么完全执行成功,要么完全不执行。
- 互斥保证:由于原子性,同一时间只有一个线程能够成功获取锁,从而保证临界区代码的互斥访问。
- 硬件支持:现代处理器通常提供特殊的原子指令(如CAS, Test-and-Set等)来实现锁的原子操作。
- 内存可见性:锁的原子操作通常还包含内存屏障,确保锁状态的变化对所有线程可见。
信号量
信号量的本质
在共享内存中,没有保护机制就导致了数据的不一致,这个问题的一种解决方案就是:信号量。
那是如何解决的呢?
简单来说就是一个进程在访问临界资源之前要先申请锁(这里就是申请
信号量本质上就是一个计数器。
进程在访问临界资源之前,申请锁(就是对信号量
--
)当信号量减到0
时进程就会阻塞,等待在访问临界资源的进程退出,释放锁(就是对信号量++
)。
这里就像显示生活中买票一样(电影票、火车票、飞机票...),一个人买票成功,这个作座位就是他的,其他人就无法买到这个座位的票。
所以,进程访问临界资源要先申请信号量,本质上就是对临界资源的一种预定机制。
这里申请信号量,信号量
++
就是P
操作、释放信号量,信号量++
就是V
操作。

共享内存是不同的进程看到同一份内存资源(在linux
内核中这份内存资源是基于文件的文件缓冲区)、消息队列是不同的进程看到同一份内存中的同一个消息队列;那信号量呢?
不同的进程要对申请/释放 同一个信号量,首先就要先看到同一个信号量。
虽然信号量不是用来输出数据的,但它也属于通信范畴。
信号量的接口
1. semget
创建信号量

这里第一个参数和
shmget
、msgget
一样,都是key
值;在内核中用来区别信号量的;(使用ftok
函数生成)第二个参数:表示要创建信号量的个数。
第三个参数:标志位、包含权限;(和
shmget
和msgget
一样)IPC_CREAT
、IPC_EXCL
和权限。
返回值:
如果创建成功就返回信号量的标识符;创建失败就返回
-1
。
2. semctl
控制信号量

semctl
和shmctl
、msgctl
一样,用来对信号量控制(包括释放、获取内核数据结构,设置信号量)
参数:
int semid
:信号量标识符,semget
的返回值。int semnum
:表示信号量在集合中的索引值。int op
:标志位,表示要对信号量进行什么操作;常见的标志有:IPC_RMID
释放信号量、IPC_STAT
获取内核数据结构、SET_VAL
设置一个信号量的值、SET_ALL
设置所有信号量的值、GET_VAL/GET_ALL
获取一个/所有信号量的值。
对于...
可变参数,取决于op
标志位,一般是一个union semnu
需要我们自己定义:
c
union semun {
int val; // SETVAL用的值
struct semid_ds *buf; // IPC_STAT, IPC_SET用的缓冲区
unsigned short *array; // GETALL, SETALL用的数组
};
返回值:
对于不同的标志位
op
,存在不同的返回值:
- 成功:返回值取决于
op
:
GETVAL
:信号量的当前值GETPID
:最后操作信号量的进程PID
GETNCNT
:等待信号量值增加的进程数GETZCNT
:等待信号量值变为0的进程数- 其他:0
- 失败:返回 -1 并设置
errno
3. semop
信号量操作
semget
创建信号量,semctl
控制信号量;那如何对信号量进程申请P
和释放V
呢?

参数:
int semid
:信号量标识符size_t nsops
:要操作信号量的数量
对于第二个参数是struct sembuf
类型的指针,它的组成:
c
struct sembuf {
unsigned short sem_num; // 信号量在集合中的索引
short sem_op; // 操作值(正数-V操作,负数-P操作)
short sem_flg; // 标志(如 IPC_NOWAIT, SEM_UNDO)
};
在我们进行操作时,就需要先构建一个struct sembuf
类型的对象,再设置其中成员变量的值,来对信号量进行不同的操作。
返回值:
操作成功进返回0
,失败则返回-1
并且错误码被设置。
内核对于System V
IPC资源的管理
了解了System V
IPC进程间通信:共享内存、消息队列和信号量;从系统调用接口上就可以看出它们非常相似。
都是使用key
值来创建唯一的资源,ctl
控制资源(删除、获取内核数据结构)。
那在内核中,是如何管理System V
IPC资源的呢?
还用,在内存中,存在非常多的进制需要进行通信,那就势必存在非常多的共享内存、消息队列或者信号量等资源;这些资源是新建的、有正在使用的、有即将释放的;
操作系统是不是也要将这些资源管理起来呢?如何管理?先描述后组织
在内核中一定存在描述共享内、消息队列和信号量对应的结构体;那操作系统是如何将这些IPC资源(结构体)组织起来的呢?
先来看一下共享内存、消息队列和信号量对应的结构体:

可以看到无论是
shmid_ds
、msqid_ds
还是semid_ds
,这些结构体的第一个变量都是struct ipc_perm
类型的对象,那它是什么呢?

c
struct ipc_perm {
key_t __key; /* IPC 对象的键值 */
uid_t uid; /* 所有者的有效用户ID */
gid_t gid; /* 所有者的有效组ID */
uid_t cuid; /* 创建者的有效用户ID */
gid_t cgid; /* 创建者的有效组ID */
unsigned short mode; /* 权限模式(读写权限) */
unsigned short __seq; /* 序列号(内部使用) */
};
可以看到在struct ipc_perm
中存储着_key
值,uid
、mode
权限等等。
了解了内核中共享内存、消息队列和信号量对应的结构体,现在来看内核是如何将其组织起来的
首先在内核在存在一个ipc_ids
的结构体,其中包含成员变量:当前IPC
对象的数量、序列号、最大序列号和ipc_id_ary
的数组(现在的新内核中已经使用IDR
树代替该数组了)。
对应的ipc_id_ary
也是一个结构体,其中包含数组总容量size
和一个柔性数组p
,这个柔性数组指针的类型是struct ipc_perm*
而共享内存struct shmid_ds
、消息队列msqid_ds
、信号量semid_ds
结构体的第一个成员都是struct ipc_perm
;
这样,在
ipc_id_ary
中的柔性数组p
指向这些结构体的第一个成员struct ipc_perm
。虽然共享内存、消息队列和信号量的结构体各不相同,但是它们的第一个成员变量都是
struct ipc_perm
;这样p
就指向不同结构体的第一个相同的成员变量。

我们知道结构体的第一个成员的地址和结构体整体的地址是一样的,所有在使用时找到对应的struct ipc_perm
的地址之后,进行强制类型转换,就可以访问结构体中的其他成员了。
到这里懂了,在内核中共享内存、消息队列和信号量是统一进行管理的,它们都使用key
值创建然后获得唯一的标识符;
所以,这里共享内存、消息队列和信号量使用的是一个数组的下标;
这里对应的标识符还是逐渐递增的,而在ipc_ids
中还存储着最大序列号max_id
,标识符在超过最大序列号之后就会轮回到起始位置。
到这里还存在一个疑问:在通过标识符(下标)找到对应的struct ipc_perm
的地址之后,通过强制类型转换就可以访问其他成员;那操作系统是如何知道要转换成什么类型的呢?
这里,我们可以发现共享内存、消息队列和信号量虽然系统调用有相似性,但是还是不同的系统调用接口;
所以,在我们调用对应的系统调用时,我们调用哪一个系统调用,操作系统就对应的转换成什么类型。
到这里本篇文章的内容就结束了,感谢支持