1. 计算机系统中的 IPC(进程间通信)
- 定义 :
IPC (Inter-Process Communication) 指的是进程间通信。在操作系统中,不同的进程拥有独立的内存空间(进程隔离),默认情况下它们互不干扰。但为了协同工作,它们需要交换数据或进行同步,这种机制就是 IPC。
管道 消息队列 共享内存 信号量都是什么关系,都是用于两个进程之间通讯的吗?
它们确实都是操作系统中用于进程间通信(IPC) 的核心机制,但它们在角色分工 和工作方式上有着非常微妙且重要的区别。
假设现在有两个进程(比如进程A和进程B)想要互相配合完成工作。
- 管道(Pipe)和消息队列(Message Queue): 就像是**"传纸条"或"寄快递"** 。
进程A把数据写好,通过管道或消息队列传递给进程B。数据是从A"流动"到B的。它们主要负责数据的传输。 - 共享内存(Shared Memory): 就像是**"共用一块小黑板"** 。
操作系统在内存中划出一块公共区域,进程A和进程B都能直接看到并读写这块区域。A把数据写在黑板上,B直接去看,数据不需要来回拷贝,所以它的通信效率是最高的。 - 信号量(Semaphore): 它比较特殊,它不负责传递任何实际的数据 。它就像是一个**"红绿灯"或"计数器"** 。
它的作用是协调和指挥。比如,当进程A正在往"小黑板"(共享内存)上写字时,信号量就会亮红灯,让进程B先等着别动;等A写完了,信号量亮绿灯,B才能上去读。它主要用来解决多个进程抢着操作同一份资源时的冲突问题(即同步与互斥)。
🔗 它们之间是什么关系?
它们经常是**"搭档"**关系,而不是互斥的选项。
最经典的组合就是 "共享内存 + 信号量" :
因为共享内存虽然速度极快,但它本身没有"排队"或"提醒"机制。如果A和B同时往黑板上乱写,数据就全乱了。所以,必须搭配信号量来指挥交通,确保A写完B再读,或者同一时间只有一个人能写。
而管道 和消息队列自带了一定的排队和同步机制,所以通常可以单独使用来完成简单的数据传输。
📊 核心区别速览表
为了更直观,我为你整理了一个对比表格:
| 机制 | 核心作用 | 形象比喻 | 是否传输数据 |
|---|---|---|---|
| 管道 (Pipe) | 简单的单向数据传输 | 一根单向的水管 | 是 |
| 消息队列 (Message Queue) | 带格式的数据传输,可异步 | 邮局的信箱/快递柜 | 是 |
| 共享内存 (Shared Memory) | 极高效率的数据共享 | 公共小黑板 | 是 |
| 信号量 (Semaphore) | 指挥交通,防止冲突(同步/互斥) | 红绿灯 / 计数器 | 否 |
总结一下:
如果你只是想简单地在两个有亲缘关系(比如父子进程)的进程间传点数据,用管道 最方便;如果想要更灵活、跨任意进程传数据,可以用消息队列 ;如果数据量极大且追求极致速度,首选共享内存 ,但千万别忘了带上信号量来保驾护航。
核心角色区分
- 共享内存 :是真正共享的对象 (也就是数据本身)。
- 比喻 :它像是一个公共厕所,是大家真正要去读写的地方。
- 信号量 :是同步工具 (用来保护共享内存)。
- 比喻 :它像是厕所门的锁 ,或者白板旁边的记号笔领取登记簿。它本身不是大家要去操作的数据,它是为了让大家有序地操作数据而存在的。
- 进程 :是使用者。
共享内存(公厕)
函数全称与核心作用
这些函数名称都是英文单词的缩写组合,分别对应共享内存操作的不同阶段:
- shmget
全称:Shared Memory Get
作用:创建一个新的共享内存段,或者获取一个已经存在的共享内存段的标识符(ID)。
通俗理解:向系统申请一块公共的"黑板",如果黑板已经有了,就拿到它的钥匙(ID)。
- shmat
全称:Shared Memory Attach
作用:将共享内存段连接到调用进程的地址空间中。
通俗理解:把那块公共"黑板"挂到你自己进程的房间墙上,这样你才能看得到、写得了上面的内容。
- shmdt
全称:Shared Memory Detach
作用:将共享内存段从调用进程的地址空间中分离。
通俗理解:把挂在你房间墙上的"黑板"摘下来,你不再直接操作它了,但这块黑板本身还在系统里。
- shmctl
全称:Shared Memory Ctrl
作用:对共享内存段执行各种控制操作,如查询状态、修改权限或删除内存段。
通俗理解:作为管理员,对这块"黑板"进行管理,比如查看谁在用、修改谁能写,或者最后把这块黑板彻底销毁(删除)。
核心流程总结
这四个函数配合使用,完成了一个完整的生命周期:
shmget:获取/创建资源。
shmat:映射资源到当前进程。
读写操作:进程间交换数据。
shmdt:断开映射(释放进程视角)。
shmctl:最终控制或删除资源(释放系统视角)。
( 1 )创建共享内存区系统调用
int shmid=shmget(key_t key,int size,int flags);
控制标志和访问权限
控制标志 (Control Flags)
这些标志用于控制 shmget 函数创建或获取共享内存段的行为。
IPC_CREAT
如果系统中不存在与 key 参数对应的共享内存段,就创建一个新的。如果已经存在,则直接获取该内存段的标识符。
IPC_EXCL
这个标志通常与 IPC_CREAT 一起使用。它的作用是:如果 key 对应的共享内存段已经存在,则 shmget 调用会失败并返回错误。这可以确保你创建的是一个全新的、唯一的共享内存段。
IPC_EXCL 标志本身并没有太大的意义,但和 IPC_CREAT 标志一起使用可以
用来保证所得的共享存储区对象是新创建的而不是打开的已有的对象。
访问权限 (Access Permissions)
这些权限与文件的读写权限类似,用于指定不同用户对共享内存段的访问级别。
S_I R USR
Statement Is Readable by User
允许共享内存段的所有者(创建者)读取该内存段。
S_I W USR
Statement Is Writable by User
允许共享内存段的所有者(创建者)写入该内存段。
这些权限可以通过"按位或"操作符(|)组合使用。例如,S_IRUSR | S_IWUSR 表示所有者同时拥有读和写的权限。
组合使用示例
在实际编程中,通常会组合使用这些标志和权限。
例如:
1int shmid = shmget(key, size, IPC_CREAT | S_IRUSR | S_IWUSR);
这行代码的含义是:
尝试获取一个键值为 key 的共享内存段。
如果它不存在,就创建一个新的(IPC_CREAT)。
并设置其权限为:所有者可读可写(S_IRUSR | S_IWUSR)。
msgflg 一般是IPC_CREAT、IPC_EXCL与访问权限控制符的组合。例如要创建一个或打开一个所有进程都能读写的共享存储区,msgflg可以取:
IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH,或
IPC_CREAT|0666。访问权限0666的含义:八进制数0666对应二进制是110 110 110,最高三位110对应的创建共享存储区用户的访问权限,中间三位是同组用户的访问权限,最低三位对应其它用户的访问权限。
每一组的三位从左往右分别对应读(r)、写(w)、执行(x)的权限。
以下是几个系统调用 shmget()的例子:
创建了一个键值为 0x8888,大小为 1024 字节,访问权限为 0666 的共享内存 区,返回共享存储区标识符 shmid。
cs
(a)int shmid = shmget(0x8888, 1024, IPC_CREAT|0666) ;
如果 key 对应的共享存储区已经存在,返回错误(-1), 如果 key 对应的共享 存储区不存在,创建之,返回共享内存段标识符 shmid。
cpp
(b)int shmid = shmget(key, IPC_CREAT |IPC_EXCL|0666);
如果 key 对应的共享存储区已经存在,返回错误(-1), 如果 key 对应的共享 存储区不存在,创建之,返回共享内存段标识符 shmid。
cpp
(c) int shmid = shmget(IPC_PRIVATE, S_IRUSR|S_IWUSR);
创建仅在该进程内使用的的共享存储区,访问权限为 0600。
( 2 )将一段共享内存附加到调用进程中的系统调用
char *shmat(int shmid, char *shmaddr,int flags)
功能:将 shmid 标识的共享内存区对象映射到调用进程的地址空间上,随后进程可像访问本地空间一样访问该段内存。
传入参数:
shmid: 由系统调用 shmget() 创建的共享内存的标识符
shmaddr:一般总为 0,表示用调用者指定的指针指向共享段
flags:共享内存权限位
返回值:
调用成功后,返回附加的共享内存的首地址
如果调用不成功,返回-1 ,错误原因存于 error 中
cpp
shmaddr = (char *)shmat( shmid, NULL, 0 )
**(3)**对共享内存段读写
利用系统调用 shmget()得到的共享内存段标识符。对共享内存段进行读写。需要注意的是,该共享内存区需要进程之间互斥访问,如果读写进程之间符合生产者-消费者模型,需要在进程中利用信号量编写相应的代码,实现进程之间的互斥与同步。
cpp
strcpy( shmaddr, "Hi, I am child process!\n") ;
scanf("%[^\n]", shmaddr); //字符串的结束符为回车
sprintf(shmaddr,"Hi, there!");
(4)将一段共享内存从到调用进程中分离出去的系统调用
int shmdt(char *shmadr);
功能:与 shma()相反,将共享内存段从进程的地址空间中分离出去,此后本进程将被禁止访问此段共享内存。
传入参数:
shmadr:系统调用 shmat()返回的指向附加共享内存的指针(地址)
返回值:
shmdt() 调用成功将递减附加计数,当计数为 0,将删除共享内存。
调用不成功返回-1 ,错误原因存于 error 中
(5)删除共享内存段
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
功能:该系统调用完成对共享内存的控制,可以用来删除内存共享段。
传入参数:
shmid: 由系统调用 shmget() 创建的共享内存的标识符
cmd:
IPC_STAT:得到共享内存的状态,把共享内存的 shmid_ds 结构体
复制到 buf 中
IPC_SET:改变共享内存的状态,把 buf 所指的 shmid_ds 结构中的
uid、gid、mode 复制到共享内存的 shmid_ds 结构体内
IPC_RMID:删除这段共享内存
buf:共享内存管理结构体
返回值:调用成功,返回 0。调用不成功,返回-1,错误原因存于 error 中。
信号量(厕所锁)
信号量是一种特殊的共享变量,但它自带一套严格的"操作守则"。
信号量则像是这块白板配了一套只有特定按钮(P、V操作)才能使用的锁,保证了操作的秩序。
📖 普通共享变量
- 定义:在内存中开辟的一块区域,多个进程或线程都可以读写它。
- 问题 :如果缺乏保护,多个进程同时修改同一个变量会导致竞态条件。
- 操作方式:直接读写(Load/Store),指令级别无法保证原子性(即操作不可分割)。
🛡️ 信号量
- 定义 :它不仅仅是一个整数变量,更是一个抽象数据类型。它包含一个整数值以及两个标准的原子操作(通常称为 P 操作和 V 操作,或者 wait/signal)。
- 特点 :
- 原子性:对信号量的操作(P/V)是原子的。这意味着当一个进程在修改信号量时,其他进程无法同时修改它,也不会被打断。
- 受控访问 :你不能直接去读写信号量的值(比如不能直接写
sem = 5),你必须通过系统调用(如semop)来进行操作。 - 阻塞机制:如果信号量的值满足特定条件(例如资源数为0),P 操作会自动把进程挂起(睡眠),直到资源可用。
工作流程
多个进程利用信号量实现互斥与同步时,通常遵循以下四个步骤:
- 创建/获取 :
- 其中一个进程调用
semget()创建一个新的信号量集。 - 其他进程调用
semget()获取该信号量集的描述符。
- 其中一个进程调用
- 初始化 :
- 进程调用
semctl()对信号量进行初始化(例如赋初值)。通常由创建者进程完成。
- 进程调用
- 操作(P、V操作) :
- 进程调用
semop()对信号量进行操作,以实现具体的同步或互斥逻辑(即经典的P操作和V操作)。
- 进程调用
- 删除 :
- 进程调用
semctl()删除信号量集,释放资源。
- 进程调用
🚦 什么是信号量?
信号量本质上是一个受操作系统内核保护的整数计数器,主要用于解决多进程或多线程环境下的资源竞争问题。你可以把它想象成管理公共资源的"红绿灯"或"钥匙串"。
- 核心作用 :它不负责传输数据(如管道或共享内存那样),而是负责协调 和控制。它确保多个进程在访问共享资源(如共享内存、打印机、文件)时,不会发生冲突。
- 工作原理 :
- 计数器:信号量维护一个整数值。如果值大于0,表示有可用资源;如果值为0,表示资源已被占用。
- 原子操作:对信号量的操作(P操作和V操作)是原子的,意味着一旦开始就不能被打断,保证了计数的准确性。
⚖️ 什么是 PV 操作?
PV 操作是荷兰语术语,由计算机科学家 Edsger Dijkstra 提出,是操作信号量的两个最基本的原子操作。
P 操作
- 含义:申请资源。
- 动作:将信号量的值减 1。
- 逻辑 :
- 如果减 1 后值仍 ≥≥ 0,表示有资源可用,进程继续执行(获得资源)。
- 如果减 1 后值 < 0,表示资源已耗尽,进程进入阻塞(睡眠)状态,等待其他进程释放资源。
- 对应图片中的代码 :
buf.sem_op = -1;(即申请一个资源)。
V 操作
- 含义:释放资源。
- 动作:将信号量的值加 1。
- 逻辑 :
- 如果加 1 后值 ≤≤ 0,表示有进程正在等待该资源,系统会唤醒一个等待中的进程。
- 如果加 1 后值 > 0,表示资源空闲。
- 对应图片中的代码 :
buf.sem_op = 1;(即释放一个资源)。
📚 函数的全称(非简写英文)
你图片中提到的函数属于 System V IPC 标准。以下是它们的全称及含义:
semget
- 全称 :Sem aphore Get
- 含义:获取或创建一个信号量集。
- 功能:如果信号量不存在则创建它;如果存在,则获取它的标识符(ID)。
semop
- 全称 :Sem aphore Operation
- 含义:信号量操作。
- 功能:执行 P 操作或 V 操作(即改变信号量的值)。
semctl
- 全称 :Sem aphore C ont rol
- 含义:信号量控制。
- 功能 :用于对信号量进行直接控制,如初始化值(
SETVAL)、获取状态(GETVAL)或删除信号量(IPC_RMID)。
semnum
- 全称 :Sem aphore Number
- 含义:信号量编号。
- 功能:在信号量集中,用于指定具体操作的是哪一个信号量(因为一个集合里可以有多个信号量)。
📌 总结表格
| 简称 | 英文全称 | 中文含义 | 核心功能 |
|---|---|---|---|
| P | Proberen | 尝试/测试 | 申请资源(值减1,可能阻塞) |
| V | Verhogen | 增加/释放 | 释放资源(值加1,可能唤醒) |
| semget | Sem aphore Get | 获取信号量 | 创建或打开信号量集 |
| semop | Sem aphore Operation | 信号量操作 | 执行 P/V 操作 |
| semctl | Sem aphore C ont rol | 信号量控制 | 初始化、查询、删除 |
(1)创建一个信号量数组的系统调用
int semid=semget(key_t key,int nsems, int flags);
功能:创建一个新的键值为 key 的信号量数组,或获取一个已经存在的键值为key的信号量标识。
参数:
·key : 信号量数组的键值,可以为 IPC_PRIVATE,也可以指定一个整数
·nsems 信号量数组中信号量的个数
·flags 信号量数组权限位,含义同共享内存权限位。
返回值:
调用成功:
key不存在,且 flags 中设置了 IPC_CREAT 位,则返回一个新信号量标识符 semid。
key存在,则返回与 key 关联的标识符 semid。
调用不成功,返回-1,错误原因存于 error 中
(2)操作信号量数组的系统调用
int semop(int semid, structsembuf *sops, unsigned nsops);
功能:操作一个或一组信号量。
传入参数:
semid: 由 semget() 创建或获取的信号量数组的标识符
sops:指向存储信号量操作的结构体数组指针
nsops: 信号量数组元素的个数。
返回值:调用成功返回 0,不成功返回-1,错误原因存于 error 中。
(3)控制信号量数组的系统调用
int semctl(int semid, int semnum, int cmd, unionsemun arg);
功能:在 semid 标识的信号量数组,或者该数组的第 semnum 个信号量上执
行 cmd 所指定的控制命令。
可以利用该调用初始化信号量的值、获取当前信号量的值,以及删除已存在的
信号量。
传入参数:
·semid: 由 semget() 创建或获取的信号量数组的标识符
·semnum:该信号量数组中的第几个信号量(从 0 开始)
·cmd:对信号量发出的控制命令
如 GETVAL 返回当前信号量的值, SETVAL 设置信号量的值,
IPC_RMD 删除标识为 semid 的信号量
关于 cmd 的命令的使用详见注 1。
·arg: 保存信号量状态的联合体,包括信号量的值等。详见注 2.
返回值:执行成功,根据 cmd 的命令返回相应的值;执行不成功,返回 -1。
例如,若执行成功,cmd 取 IPC_STAT、SETVAL、IPC_RMID,返回 0;若
cmd 取 GETVAL,则返回信号量的当前值。
注1:第三个参数cmd****可以使用如下命令:
· IPC_STAT:读取一个数组的数据结构 semid_ds,并将其存储在 semun 中的
buf 参数中,调用进程应有对信号量数组的读权限。
· IPC_SET:设置信号量集的数据结构 semid_ds 中的元素 ipc_perm,其值取自
semun 中的 buf 参数。
· IPC_RMID:将信号量集从内存中删除,并唤醒因调用 semop()而阻塞的进程。
· GETALL:用于读取信号量集中的所有信号量的值。
· GETNCNT:返回正在等待资源的进程数目。
· GETPID:返回最后一个执行 semop 操作的进程的 PID,又称为 sempid。
· GETVAL:返回信号量集中的一个单个的信号量的值。
· GETZCNT:返回正在等待完全空闲的资源的进程数目。
· SETALL:设置信号量集中的所有的信号量的值。41
· SETVAL:设置信号量集中的一个单独的信号量的值。
· SEM_STAT:返回信号集标识 swmid
· SEM_INFO:同 IPC_INFO
· IPC_INFO :返回系统范围内的信号量的限制和参数,保存到第 4 个参数的
成员 struct seminfo 所定义的缓存中。
4.2.3****消息队列
消息队列是操作系统内核控制并发进程间共享资源的另一种进程间通信机制,
系统对于消息队列的管理,符合生产者-消费者模型。
进程之间利用消息队列进行通信时,需要经过下面几个步骤:
(1)首先需要其中的一个进程调用 msgget()创建一个消息队列,其它进程调
用 msgget()获取该消息队列的描述符。
(2)进程调用 msgsnd()给消息队列发送消息,或调用 msgrcv ()从消息队列接
收消息。
(3)进程调用 msgctl()删除消息队列
1**、消息队列有关的系统调用**
(1)创建消息队列的系统调用
int msgid=msgget(key_t key,int flags)
功能:创建一个新的键值为 key 的消息队列,或获取一个已经存在的键值为
key 的消息队列标识。
传入参数:
·key:消息队列的键值,可以为 IPC_PRIVATE,也可以指定一个整数。
·flags:消息队列权限位,含义同共享内存权限位和信号量权限位。
返回值:
调用成功:如果 key 用新整数指定(key 对应的消息队列不存在),且
flags 中设置了 IPC_CREAT 位,则返回一个新建立的消息队列标识符 msgid。 如果
指定的整数 key 已存在则返回与 key 关联的标识符 msgid。
调用不成功,返回-1,错误原因存于 error 中。
(2)发送一条新消息到消息队列的系统调用
#include <sys/msg.h>
int msgsnd(int msqid, structmsgbuf *msgp, size_t msgsz, int msgflg);
44功能:将消息缓冲区 msgp 中,数据长度为 msgsz 的消息发送到 msqid 所指定
的消息队列中。当消息队列已满时,发送进程是否阻塞,由 msgflg 的设置决定。
系统对于消息队列的管理,符合生产者-消费者模型。
传入参数:
·msqid :msgget()返回的消息队列的标识符
·msgp;消息缓冲区指针,暂存将要发送的消息,是一个结构体。
·msgsz:消息数据的长度 /*纯消息数据的长度,不包括消息类型*/
·msgflg: 设置为0,表示阻塞方式
设置 为 IPC_NOWAIT, 表示非阻塞方式
设置为 MSG_NOERROR,表示截取消息数据,不返回错误
返回值:调用成功返回 0,不成功返回-1,错误原因存于 error 中。
(3)从到消息队列中结束(读出)一条消息的系统调用
#include <sys.msg.h>
int msgrcv(int msqid, structmsgbuf *msgp, size_t msgsz,long msgtype, int msgflg);
功能:从 msqid 所标识的消息队列中,取走由 msgtype 所指定类型的消息,存
放到 msgp 所指向的消息缓冲区中,如果 msgsz 小于消息的数据长度,则截取
msgsz 个字节的消息数据,存放到消息缓冲区 msgp 中。若消息队列中没有 msgtype
类型的消息可以接收,是否阻塞接收进程由 msgflg 的设置决定。
当接收到一个符合接收类型的消息时,msgrcv()会将该消息从消息队列中删除。
传入参数:
·msqid 由消息队列的标识符
·msgp 消息缓冲区指针。
·msgsz: 消息数据的长度
·msgtype 决定从队列中接收哪条消息,分为三种情况:
msgtype =0 :接收消息队列中第一条消息
msgtype >0 :接收消息队列中等于 mtype 类型的第一条消息。
msgtype <0 :接收类型等于或小于 mtype 绝对值,且绝对值最小值
的第一条消息。
·msgflg 为0表示阻塞方式,设置 IPC_NOWAIT 表示非阻塞方式
返回值:调用成功,返回实际读到的消息数据长度
调用不成功,返回-1,错误原因存于 error 中。
(4)控制消息队列的系统调用
int msgctl(int msqid, int cmd, structmsqid_ds *buf);
功能:根据 cmd 中给出的命令,对 msqid 标识的消息队列施加相应的操作。
通常用来删除一个消息队列。
传入参数:
·msqid:消息队列的标识符
·cmd:一组 控制命令。常用的有:
IPC_RMID 删除 msgid 标识的消息队列
IPC_STAT 为非破坏性读,读取消息队列的 msgid_ds 结构,写到 buf 中
IPC_SET 改变消息队列的 UID、GID、访问权限等设置
·buf:指向存储结构体 msqid_ds 的缓冲区指针
返回值:调用成功返回 0,不成功返回-1,错误原因存于 error 中。
4.2.4常用IPC****命令
可以利用命令 ipcs -m 观察共享内存情况,ipcs -s 观察信号量
数组的情况,ipcs -q 观察消息队列的情况。我们还可以通过一些命令删除 IPC 对象,
也可以访问操作系统为 IPC 对象临时创建的几个虚拟文件查看它们的有关信息。
1、ipcs,或 ipcs --a,查看目前系统的所有的 ipc 对象资源
2、 ipcrm 命令在权限允许的情况下可以使用 ipcrm 命令删除系统当前存在的 IPC 对象中的任
一个对象。
(1)通过 IPC 对象的 id 号删除相应的 IPC 对象
格式:ipcrm -[m,s,q] id,删除 id 号标识的 IPC 对象。例如,
·ipcrm -m 21482,删除标号为 21482 的共享内存。
·ipcrm -s 32673, 删除标号为 32673 的信号量数组。
·ipcrm -q 18465, 删除标号为 18465 的消息队列。
(2)通过 IPC 对象的键值 key 删除相应的 IPC 对象
格式:ipcrm -[M,S,Q] key,删除键值 key 标识的 IPC 对象。例如,
·ipcrm -m 66666,删除键值为 0x66666 的共享内存。
·ipcrm -s 77777, 删除键值为 0x77777 的信号量数组。
·ipcrm -q 88888, 删除键值为 0x88888 的消息队列。
3、临时虚拟文件
应考策略
这些 API 的参数和命令确实很容易混淆,因为它们太琐碎了。但死记硬背其实效率最低,而且完全没必要------这些细节平时写代码时查文档就行,关键是要理解它们背后的统一逻辑。
只要掌握了这套"三步走"的通用设计模式,你只需要记住 3 个核心动词,剩下的参数和细节自然就顺下来了。
🔑 掌握"三步走"通用模式
你会发现,无论是信号量 、消息队列 还是共享内存(System V IPC 标准),它们的操作逻辑完全一样,都是遵循**"获取 -> 操作 -> 控制"**这三部曲。
第一步:获取/创建 (Get)
- 动词 :
...get - 函数 :
semget,msgget,shmget - 目的 :
- 要么新建一个资源(像申请一个新房间)。
- 要么获取 一个已存在资源的ID(像拿到房间的钥匙/门牌号)。
- 核心参数 :
key:资源的"名字"或"门牌号"。大家通过这个来找到同一个资源。flags:权限(读/写)+ 创建标志(IPC_CREAT)。
第二步:操作 (Operation)
- 动词 :
...op(Operation) - 函数 :
semop,msgrcv/msgsnd(收发也是操作),shmat/shmdt(挂载/卸载也是操作) - 目的 :真正干活。
- 信号量:P/V 操作(加减)。
- 消息队列:发/收 消息。
- 核心参数 :
id:第一步拿到的门牌号。buf/struct:要处理的数据(比如消息内容、P/V 操作的具体数值)。
第三步:控制/管理 (Control)
- 动词 :
...ctl(Control) - 函数 :
semctl,msgctl,shmctl - 目的 :管理资源的生命周期或属性。
- 最常用的:删除资源 (
IPC_RMID)。 - 其他:修改权限、获取状态信息。
- 最常用的:删除资源 (
- 核心参数 :
cmd:你要执行什么命令?(删除?设置?获取?)
🧠 记忆口诀与技巧
只需要记 3 个动词
不要记 10 个函数名,只记三个后缀:
get:拿钥匙(创建或打开)。op:干活(P/V 操作,发消息)。ctl:管家(删除、改权限)。
理解 key 和 id 的区别
key:是公共的,就像"文件名"或"门牌号"。大家约定好一个 key,就能找到同一个东西。id:是私有的 ,就像"文件描述符"。get函数返回给你之后,你拿着它去操作具体的资源。
关于 cmd 参数(不要死记)
在 ...ctl 函数中,cmd 参数决定了你要干嘛。
- 只要记住最常用的:
IPC_RMID(删除,Remove ID)。 - 其他的(
SETVAL,GETVAL,IPC_STAT)看名字就能猜出来:SET 是设置,GET 是获取,STAT 是状态。考试或写代码时,只需要知道有这个东西,具体拼写查一下文档或看提示即可。
📝 简化版"小抄" (Cheat Sheet)
如果你是为了应付考试或面试,只需要记住这张表的逻辑:
| 步骤 | 信号量 (Semaphore) | 消息队列 (Message Queue) | 动作含义 |
|---|---|---|---|
| 1. 获取/创建 | semget |
msgget |
拿到资源的 ID (句柄) |
| 2. 操作 | semop |
msgsnd (发) msgrcv (收) |
核心业务逻辑 (P/V 操作 或 收发消息) |
| 3. 控制/删除 | semctl |
msgctl |
管理资源 (通常是删除 IPC_RMID) |
💡 总结
不要背参数列表! 参数列表是给人查的,不是给人背的。
你应该背的是流程:
- 先
get拿到 ID。 - 拿着 ID 去
op干活。 - 干完了用
ctl删掉它(防止内存泄漏)。
只要你脑子里有这个流程图 ,具体的函数名和参数根据命名规则(如 sem 开头就是信号量,msg 开头就是消息队列)就能推导出来。
要解决抽烟者问题,我们需要用信号量实现进程间的同步,确保供应者正确提供材料、抽烟者正确获取材料并完成卷烟,且不会出现竞争或死锁。
1. 问题分析
- 角色:3个抽烟者(分别拥有烟草、纸、胶水)、2个供应者(轮流提供两种材料)。
- 需求:供应者每次提供两种材料,对应缺失这两种材料的抽烟者(即拥有第三种材料的抽烟者)获取材料卷烟,完成后通知供应者继续提供。
- 关键同步点 :
- 供应者提供材料后,需通知对应的抽烟者;
- 抽烟者卷烟完成后,需通知供应者可以下一次提供材料。
2. 信号量设计
- 供应者同步 :
mutex(二进制信号量,初始值1),用于保护供应者对材料的写入,确保每次只有一个供应者提供材料。 - 抽烟者信号量 :3个二进制信号量,分别对应三个抽烟者,初始值0(表示无材料可获取):
smoker_tobacco:拥有纸和胶水的抽烟者(需要烟草),当供应者提供纸+胶水时,该信号量变为1。smoker_paper:拥有烟草和胶水的抽烟者(需要纸),当供应者提供烟草+胶水时,该信号量变为1。smoker_glue:拥有烟草和纸的抽烟者(需要胶水),当供应者提供烟草+纸时,该信号量变为1。
- 供应者等待信号量 :
finish(二进制信号量,初始值1),用于供应者等待抽烟者完成卷烟(即抽烟者通知供应者可以继续提供材料)。
3. 进程逻辑
供应者进程
- 循环选择两种材料(如纸+胶水、烟草+胶水、烟草+纸,轮流提供);
- 等待
mutex(确保独占写入); - 将材料放入共享区(本题中无需显式存储,直接通过信号量通知对应抽烟者);
- 释放
mutex; - 发送信号给对应抽烟者(如提供纸+胶水时,发送
smoker_tobacco); - 等待
finish(等待抽烟者完成卷烟); - 重复。
抽烟者进程
- 等待自己的信号量(如拥有烟草的抽烟者等待
smoker_paper); - 获取材料(从供应者处获取缺失的两种材料);
- 卷烟并抽烟;
- 发送
finish信号(通知供应者可以继续提供材料); - 重复。
5. 代码说明
- 信号量初始化 :
sem_init用于初始化信号量,0表示线程间共享(而非进程间),初始值分别为mutex=1、抽烟者信号量=0、finish=1。 - 抽烟者线程 :每个抽烟者线程等待自己的信号量(如拥有烟草的抽烟者等待
smoker_paper),获取材料后卷烟并发送finish信号。 - 供应者线程 :循环选择三种材料组合(纸+胶水、烟草+胶水、烟草+纸),通过
sem_post发送信号给对应抽烟者,然后等待finish信号(表示抽烟者已完成)。 - 同步逻辑 :供应者通过
mutex保护材料选择的原子性,通过finish等待抽烟者完成,确保每次只有一个抽烟者获取材料,供应者不会重复提供。
编译
gcc exp4_1.c -o exp4_1 -lpthread
运行
./exp4_1

真实操作系统中提供的并发进程同步机制(如System V IPC信号量、消息队列等),本质上是对教材中进程同步原理的实现。它们通过原子操作 和阻塞/唤醒机制,解决了多进程/线程对共享资源的竞争问题,确保系统的正确性和稳定性。以下从同步机制的实现原理、信号量的作用、实验验证三个层面展开分析:
操作系统通过内核级的同步原语(如信号量、互斥锁、条件变量)实现进程同步,其核心思想与教材中的"信号量机制"完全一致,具体表现为:
1. 生产者/消费者问题的工程化实现
- 教材原理:生产者-消费者问题需要解决"缓冲区满时生产者等待""缓冲区空时消费者等待"的同步问题,以及"对缓冲区的互斥访问"问题。
- 操作系统实现 :
- 信号量 :用
empty(缓冲区空槽数量,初值=缓冲区大小)、full(缓冲区满槽数量,初值=0)实现同步;用mutex(互斥信号量,初值=1)实现互斥。 - 系统调用 :通过
semop(P/V操作)、semget(创建信号量)等系统调用,将教材中的"信号量操作"转化为内核级的原子操作,确保多进程访问时的安全性。
- 信号量 :用
2. 抽烟者问题的工程化实现
- 教材原理:抽烟者问题需要解决"供应者提供材料"与"抽烟者获取材料"的同步,以及"多个抽烟者对材料的竞争"问题。
- 操作系统实现 :
- 信号量 :用3个信号量(如
smoker_tobacco、smoker_paper、smoker_glue,初值=0)分别通知对应抽烟者;用mutex(初值=1)确保供应者对材料的"原子提供"。 - 系统调用 :供应者通过
semop发送信号(V操作)唤醒抽烟者,抽烟者通过semop等待信号(P操作),完成同步。
- 信号量 :用3个信号量(如
二、信号量机制的互斥与同步实现
信号量是一个整型变量,其操作(P/V)是原子的,通过"计数+阻塞队列"实现进程的互斥与同步:
1. 互斥的实现
- 信号量初值 :
mutex=1(表示共享资源初始可用)。 - P操作(wait) :进程申请资源时,
mutex--。若mutex<0,进程阻塞(进入等待队列);否则进入临界区。 - V操作(signal) :进程释放资源时,
mutex++。若mutex<=0,唤醒等待队列中的一个进程。 - 物理意义 :
mutex的值表示"可用资源的数量",初值1表示"资源初始可用",0表示"资源被占用",负数的绝对值表示"等待资源的进程数"。
2. 同步的实现
- 信号量初值 :根据同步条件设置(如生产者-消费者中
empty=缓冲区大小,full=0)。 - P操作 :进程等待同步条件(如消费者等待
full>0),full--。若full<0,进程阻塞。 - V操作 :进程满足同步条件(如生产者生产后
empty--,full++),full++。若full<=0,唤醒等待的消费者。 - 物理意义 :信号量的值表示"可用资源的数量"或"等待进程的计数"。例如,
full=3表示"缓冲区有3个满槽(可消费的资源)";full=-2表示"有2个消费者在等待满槽"。
三、实验验证:多进程、不同启动顺序与执行速率
通过调整生产者/消费者数量、启动顺序、执行速率,验证同步机制的正确性:
1. 生产者/消费者实验验证
- 场景1 :3个生产者、5个消费者,缓冲区大小=2。
- 启动顺序:先启动所有消费者,再启动生产者。
- 预期结果:消费者初始因
full=0阻塞;生产者生产后full++,唤醒消费者,缓冲区不会"空读"或"满写"。
- 场景2 :生产者和消费者以不同速率执行(如生产者快、消费者慢)。
- 预期结果:
empty信号量会限制生产者的速度(缓冲区满时生产者阻塞),确保数据一致性。
- 预期结果:
2. 抽烟者实验验证
- 场景1 :2个供应者、3个抽烟者,供应者轮流提供材料。
- 启动顺序:先启动所有抽烟者,再启动供应者。
- 预期结果:抽烟者初始因信号量=0阻塞;供应者提供材料后,对应抽烟者被唤醒,卷烟后通知供应者继续。
- 场景2 :供应者以不同速率提供材料(如供应者1快、供应者2慢)。
- 预期结果:
mutex确保供应者对材料的"原子提供",抽烟者不会获取到"不完整"的材料。
- 预期结果:
四、结论
真实操作系统的同步机制(如System V信号量)是教材中"信号量机制"的工程化落地:
- 互斥 :通过
mutex信号量(初值=1)的P/V操作,确保共享资源的"原子访问"。 - 同步 :通过
empty/full(生产者-消费者)或smoker_*(抽烟者)信号量的P/V操作,确保进程按"生产-消费"或"供应-卷烟"的顺序执行。 - 鲁棒性:多进程、不同启动顺序和执行速率的实验验证,证明了同步机制能正确处理复杂的并发场景,避免竞争、死锁等问题。