【Linux系统编程】第三十七弹---深入理解System V IPC机制:消息队列、信号量与共享内存的实战解析
system V 消息队列:什么是System V消息队列,基本原理;system V信号量:基本概念,主要操作,相关函数;共享内存,消息队列,信号量~~~
✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】
目录
1、system V 消息队列
在Linux系统中,进程间通信(IPC)是一个关键机制,它允许不同的进程间交换数据和信号。System V消息队列是这些IPC机制中的一种,它提供了一个通过消息进行同步和通信的方式。
1.1、什么是System V消息队列
System V消息队列是UNIX和类UNIX系统(如Linux)中一种经典的IPC机制,它允许多个进程通过发送和接收消息来进行通信。这些消息队列是由内核管理的,它们为进程提供了一个可靠的数据交换方式。
1.2、基本原理
一个进程向另一个进程发送有类型数据块的方式。
1.2.1、消息队列的结构
在System V消息队列中,每个消息队列都由一个消息队列描述符(通常是一个整数值)来标识。消息本身则是一个固定大小的数据块,由两部分组成:消息类型(message type)和消息数据(message data)。消息类型是一个正整数,用于对消息进行分类,使得消费者可以根据消息类型来选择性地接收消息。
1.2.2、消息队列的操作
System V消息队列提供了以下基本操作:
- 创建或打开消息队列:使用
msgget()
系统调用。如果指定的消息队列不存在,且调用者有足够的权限,msgget()
将创建一个新的消息队列。
// 创建一个消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:
key:使用 "key_t ftok(const char *pathname, int proj_id);"函数获取key值
msgflg: IPC_CREAT IPC_EXCL ,原理同共享内存
返回值:
成功返回0,失败返回-1并更新错误码
// 获取key值
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
返回值:
成功返回key值,失败返回-1并更新错误码
- 发送消息:使用
msgsnd()
系统调用。它将一个消息发送到指定的消息队列中。如果队列已满(达到了系统允许的最大消息数),则发送操作可能会被阻塞或立即返回错误。
发送接收消息相关头文件
msgrcv, msgsnd - 消息队列操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
发送消息函数讲解
// 向指定消息队列发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
msgp:
msqid:消息队列的标识符,是一个由 msgget() 函数返回的非负整数。
msgp:指向消息缓冲区的指针,该缓冲区包含了要发送的消息。
消息缓冲区必须以 struct msgbuf 的形式组织,但通常使用更具体的结构体来匹配消息的实际内容。
struct msgbuf 通常至少包含 long mtype;(消息类型)和 char mtext[1];
(消息数据,实际大小可变)两个字段。
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
msgsz:消息数据部分的大小(不包括消息类型字段)。
msgflg:控制 msgsnd() 行为的标志。如果设置了 IPC_NOWAIT 标志,
则 msgsnd() 在队列满时将不会阻塞;相反,它会立即返回一个错误。
如果没有设置 IPC_NOWAIT,则 msgsnd() 可能会阻塞。
返回值:
成功时,msgsnd() 返回 0。
出错时,返回 -1,并设置 errno 以指示错误的原因。可能的错误包括
EAGAIN(在非阻塞模式下队列已满)、EIDRM(消息队列已被删除)、
EINTR(调用被信号中断)、EINVAL(消息大小无效或消息类型小于 0)、
EMSGSIZE(消息太大,无法放入队列)以及 EPERM(调用进程没有写权限)。
- 接收消息:使用
msgrcv()
系统调用。它从指定的消息队列中接收一个消息。消费者可以根据消息类型来过滤接收到的消息。
接收消息函数讲解
// 接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
参数
msqid:消息队列的标识符,是一个由 msgget() 函数返回的非负整数。
msgp:指向消息缓冲区的指针,该缓冲区用于存储接收到的消息。
消息缓冲区必须以 struct msgbuf 的形式组织,但通常使用更具体的结构体来匹配消息的实际内容。
struct msgbuf 通常至少包含 long mtype;(消息类型)和 char mtext[1];
(消息数据,实际大小可变)两个字段。
msgsz:缓冲区的大小(以字节为单位),它指定了 msgp 指向的缓冲区能够接收的最大消息数据量
(不包括消息类型字段)。
msgtyp:用于指定接收消息的类型。其值可以有以下几种情况:
0:接收队列中的第一个消息(不考虑消息类型)。
大于0:接收队列中类型等于 msgtyp 的第一个消息。
小于0:接收队列中类型值不大于 msgtyp 绝对值的最低优先级消息
(即类型值最小且不大于 msgtyp 绝对值的消息)。
msgflg:控制 msgrcv() 行为的标志。如果设置了 IPC_NOWAIT 标志,
则 msgrcv() 在没有符合条件的消息时将不会阻塞;相反,它会立即返回一个错误。
如果没有设置 IPC_NOWAIT,则 msgrcv() 可能会阻塞。
返回值
成功时,msgrcv() 返回实际接收到的消息数据的长度(不包括消息类型字段)。
出错时,返回 -1,并设置 errno 以指示错误的原因。可能的错误包括
EAGAIN(在非阻塞模式下没有符合条件的消息)、EIDRM(消息队列已被删除)、
EINTR(调用被信号中断)、ENOMSG(在非阻塞模式下且队列为空时尝试接收消息)、
EBADF(无效的消息队列标识符)等。
- 控制消息队列:使用
msgctl()
系统调用。它允许进程对消息队列执行各种控制操作,如删除队列、获取队列状态等。
msgctl - 控制消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
参数:
msqid:消息队列的标识符,是一个由 msgget() 函数返回的非负整数。
cmd:指定要执行的操作。对于删除消息队列,应使用 IPC_RMID 命令。
buf:指向 struct msqid_ds 结构体的指针。对于 IPC_RMID 命令,这个参数可以是 NULL,
因为删除操作不需要修改消息队列的属性。然而,在一些系统或上下文中,
为了保持代码的一致性和健壮性,建议总是传递一个有效的 struct msqid_ds 指针,
并将其内容初始化为零(尽管在这种情况下,内核可能会忽略它)。
返回值:
成功时,msgctl() 返回 0。
出错时,返回 -1,并设置 errno 以指示错误的原因。可能的错误包括
EINVAL(无效的消息队列标识符或命令)、EIDRM(消息队列已被删除)、
EPERM(调用进程没有相应的权限)等。
1.2.3、补充知识
消息队列声明周期也是随内核的,进程结束前不删除消息队列需要手动删除(命令!!!
ipcs -q # 查看消息队列属性信息
ipcrm -q msgid # 删除消息队列
[root@ubuntu:Msg]# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
关键字(key):用于在创建消息队列时指定一个唯一的标识符。
这个关键字可以是任何整数值,但通常使用ftok()函数生成,以确保其唯一性。
消息队列ID(msqid):每个消息队列都有一个唯一的标识符(ID),
用于区分系统中的其他消息队列。
所有者(owner):显示创建消息队列的用户ID(UID)和组ID(GID),
表示该消息队列的拥有者。
权限(perms):表示消息队列的访问权限,类似于文件系统的权限设置。
这些权限决定了哪些用户或组可以访问(读、写或控制)该消息队列。
已用字节数(used-bytes):表示当前消息队列中已经占用的字节总数。
这有助于了解消息队列的使用情况。
消息数量(messages):表示消息队列中当前存储的消息总数。
这是衡量消息队列负载的重要指标。
2、system V信号量
System V信号量是一种用于进程间通信(IPC)和同步的机制,它主要用于控制对共享资源的访问,解决竞争条件和死锁等并发编程中的问题。以下是对System V信号量的详细讲解:
2.1、基本概念
- 信号量集:System V信号量以信号量集的形式存在,一个信号量集可以包含多个信号量,每个信号量都可以独立地进行操作。
- 信号量类型:
- 二元信号量:其值只能为0或1,类似于互斥锁,用于控制单个资源的访问。
- 计数信号量:其值在0和某个限制值之间,表示可用资源的数量。
补充5个概念:
1、多个执行流(进程)能看的一份资源:共享资源
2、被保护起来的资源:临界资源;临界资源的两个特性:同步和互斥;用互斥的方式保护共享资源:临界资源
3、互斥:任何时候只能有一个进程在访问共享资源
4、资源 --- 要被程序员访问 --- 资源被访问,朴素的认识就是通过代码访问,代码 = 访问共享资源的代码(临界区) + 不访问共享资源的代码(非临界区)
5、所谓对共享资源进行保护,本质是对访问共享资源的代码进行保护
对信号量的理论理解
信号量(信号灯) --- 保护临界资源(code)
信号量本质是一个计数器
使用生活中看电影来理解信号量:
普通用户去电影院看电影
- 1、先买票
- 2、让买票人(执行流)和座位(资源)一一对应 (程序员编码实现),不关心
看电影买票的本质:是对资源的 预定 机制!
最担心超过资源个数的买票!
int count = 25; // 总共25张票
if(count > 0) count--;
else wait;
// 购票
- 电影院:共享资源(临界资源)
- 买票:申请信号量
- 票数:信号量初始值
申请信号量的本质是对公共资源的一种预定机制!!!
超级VIP去电影院看电影
使用信号量的通信过程:
- 1、申请信号量
- 2、访问共享内存
- 3、释放信号量
信号量是一个计数器,能不能使用全局变量 gcount 标记信号量?
不能!!!
1、因为全局变量不能让所有进程看到。因此有了信号量,和共享内存,消息队列一样,必须先让不同的进程看到同一块资源(计数器)。意味着信号量也是一个公共资源,作用是保护临界资源的安全,前提自己得是安全的!!!
2、gcount ++/--不是原子(要么执行要么不执行)的。
2.2、主要操作
System V信号量支持两种基本操作:P(也称为wait或down)操作和V(也称为signal或up)操作。
- P操作:
- 用于获取(或等待)一个信号量。
- 如果信号量的值大于0,则将其减一,并允许进程继续执行。
- 如果信号量的值已经是0,则阻塞当前进程,直到信号量的值变为非0(即有其他进程释放了信号量),然后再将其减一。
- V操作:
- 用于释放(或增加)一个信号量。
- 将信号量的值加一。
- 如果有其他进程因为等待该信号量而被阻塞,则唤醒其中一个被阻塞的进程。
2.3、相关函数
- 1、semget:
- 用于创建或获取一个System V信号量集。
- 参数包括唯一标识信号量集的键值、信号量集中的信号量数量以及控制函数行为的标志位(如IPC_CREAT和IPC_EXCL)。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数:
key:这是一个关键参数,用于标识信号量集。
nsems:指定需要创建或检查的信号量的数量。当创建一个新的信号量集时,
这个参数指定了集合中信号量的数量。如果是获取一个已存在的信号量集,
这个参数通常被设置为0,因为不需要改变已有信号量集的大小。
semflg:这个参数控制信号量集的访问权限和状态。
返回值
成功时:semget函数返回信号量集的标识符(一个非负整数),
该标识符用于后续的信号量操作(如semop和semctl)。
失败时:semget函数返回-1,并设置errno以指示错误类型。
- 2、semctl:
- 对一个信号量执行各种控制操作,如获取或设置信号量的值、获取信号量集的状态信息等。
- 常见的操作包括GETVAL(获取信号量的当前值)、SETVAL(设置信号量的值)、IPC_RMID(删除信号量集)等。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
四个参数:
semid:信号量集的标识符(ID),由semget函数返回。
semnum:操作信号在信号集中的编号,从0开始。
cmd:指定要执行的控制命令。
...:第四个参数根据cmd的不同而变化,可能是指向特定数据结构的指针,用于传递额外的信息或数据。
参数cmd:
PC_STAT:读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
这允许调用者获取信号量集的当前状态信息。
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Creation time/time of last
modification via semctl() */
unsigned long sem_nsems; /* No. of semaphores in set */
};
IPC_SET:设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
这允许调用者更改信号量集的权限等属性。
struct ipc_perm {
key_t __key; /* Key supplied to semget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
IPC_RMID:将信号量集从内存中删除。这个操作会唤醒所有因调用semop()
而阻塞在该信号量集合里的进程(这些调用会返回错误,并且errno被设置为EIDRM)。
- 3、semop:
- 用于执行P和V操作。
- 通过一个指向sembuf结构体的指针数组来指定要操作的信号量、操作类型(P或V)以及SEM_UNDO标志(用于在系统崩溃时自动恢复信号量的值)。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
参数:
semid:信号量集的标识符(ID),由semget函数返回。
sops:指向sembuf结构体数组的指针,该数组包含了要执行的操作序列。
sembuf结构体通常包含三个成员:sem_num(信号量在信号集中的编号)、
sem_op(要执行的操作,正数表示V操作,负数表示P操作,0表示等待信号量变为0)、
sem_flg(操作标志,如IPC_NOWAIT表示非阻塞操作,
SEM_UNDO表示在进程结束时自动撤销对该信号量的修改)。
nsops:sops数组中sembuf结构体的数量,即要执行的操作个数。
函数功能:
semop函数按照sops数组中sembuf结构体的顺序,原子性地执行指定的操作序列。
这意味着,要么所有操作都成功执行,要么都不执行,从而保证了操作的原子性。
当sem_op为正数时,表示V操作,即释放资源。如果信号量的值加上sem_op后
不会超出其允许的最大值,则操作成功,信号量的值相应增加。
当sem_op为负数时,表示P操作,即请求资源。如果信号量的绝对值
大于或等于sem_op的绝对值,则操作成功,信号量的值相应减少。如果信号量的值
小于sem_op的绝对值,且sem_flg中未设置IPC_NOWAIT标志,则调用进程将被阻塞,
直到信号量的值变为足够大或信号量集被删除。
当sem_op为0时,semop函数将检查信号量的值是否为0。如果是,
并且sem_flg中未设置IPC_NOWAIT标志,则调用进程将被阻塞,
直到信号量的值变为非零。如果设置了IPC_NOWAIT标志,
则函数将立即返回,并设置errno为EAGAIN。
返回值
成功执行时,semop函数返回0。
失败时,semop函数返回-1,并设置errno以指示错误类型。
可能的错误值包括EACCES(权限不足)、
EAGAIN(资源暂时不可用且设置了IPC_NOWAIT标志)、EINVAL(无效参数)、
EIDRM(信号量集已被删除)等。
信号量指令:
[root@ubuntu:Msg]# ipcs -s # 查看信号量属性信息
------ Semaphore Arrays --------
key semid owner perms nsems
键(key):用于创建信号量集时指定的键,它是信号量集在系统中的一个引用或名称。
信号量集ID(semid):这是信号量集的唯一标识符,用于在系统中唯一标识一个信号量集。
不同的进程可以通过相同的键来访问同一个信号量集。
拥有者(owner):信号量集的创建者或当前所有者,通常表示为UID(用户ID)和GID(组ID)。
权限(perms):信号量集的权限设置,类似于文件的权限设置,用于控制哪些进程可以访问信号量集。
信号量数量(nsems):信号量集中包含的信号量个数,这取决于信号量集在创建时的设置。
ipcrm -s semid # 删除指定信号量集
[root@ubuntu:Msg]# ipcs # 查看进程间通信属性信息
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
------ Semaphore Arrays --------
key semid owner perms nsems
2.4、工作原理
System V信号量通过内核中的数据结构来管理,每个信号量集都有一个对应的semid_ds结构体,用于存储信号量的权限、状态等信息。当进程通过semget、semctl和semop等函数对信号量进行操作时,内核会根据这些操作来更新信号量的值,并处理进程的阻塞和唤醒等操作。
3、共享内存,消息队列,信号量
OS是如何把共享内存,消息队列,信号量统一管理起来的?
先描述在组织。
1、System V标准
2、XXXget,XXXctl
3、xxxid_ds,struct ipc_perm
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)