这里我们介绍的这种通信方式也就是 system V IPC
在我们后面的使用和日常见到的其实并不多,但是包括其中的共享内存、消息队列、信号量,我们如果了解共享内存其原理的话,能够更好的帮助我们了解之前我们学过的进程地址空间的概念!
至于信号量,我们后面讲多线程的时候会再次讲,我们只引入一些概念如互斥等等,而消息队列我们就只说说其原理,不会细讲!
之前我们学过管道通信,分为匿名管道和命名管道,匿名管道通过父子进程的属性继承原理来完成父子进程看到同一份资源的目的,而命名管道则是通过路径与文件名来唯一标识管道文件,来让不同的进程之间进行通信!
而共享内存也是一样,我们得让不同的进程看到同一份资源,但是这次我们不是使用继承还是文件名路径来标识,而是通过在内存中的一段空间:共享内存区中申请一段空间,并且进程可以通过获得一个唯一的标识 ID 来获得这段共享内存的位置,当多个进程同时获得这段共享内存的时候,我们就称它们通过共享内存看到了同一份资源!这里面有许多的细节,我们一一来解释!
在 Linux
中,首先我们假设这里有两个进程分别被调度,那么它们就有各自对应的进程控制块 PCB
和地址空间 mm_struct
并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元 MMU
进行管理,由于两个进程拥有独立的数据结构,所以我们可以知道其是有独立性的!如下图所示:
但是这里就要一个问题了!既然两个进程的之间的数据结构是相互独立的,我们学过进程地址空间也知道,如果它们指向了同一段物理空间,那么其中一方进行修改,是会发生写时拷贝的,并不会影响另一方,那么这样子我们如何进行通信呢 ❓❓❓
所以为了让两个毫不相干的进程能看到同一份资源,操作系统会做以下几个工作:
那么就会有人问,调用 malloc
函数不也能在内存中开辟一段空间并且和进程之间映射起来吗 ❓❓❓
答案肯定是不行的!因为我们 malloc
出来的空间,只是属于某个进程的,而进程之间具有独立性,所以其他进程是压根看不到这份资源的,就算看到了,那么也不能进行通信,因为存在写时拷贝,而共享内存是特殊的,是运行不同进程之间共同操作的一段空间!
在 linux
中,共享内存也是需要被管理的,就像我们的进程控制块、文件描述符等等都是遵循一个原则:先描述、再组织!
在 Linux
内核中,每个共享内存都由一个名为 struct shmid_kernel
的结构体来管理 (shmid
表示共享内存的 id
),而且 Linux
限制了系统最大能创建的共享内存为 128
个。通过类型为 struct shmid_kernel
结构的数组来管理,其中 struct shmid_ds
结构体用于管理共享内存的属性信息,而 shm_segs数组
用于管理系统中所有的共享内存。
另外 struct shmid_ds
结构体中存在另一个结构体 struct ipc_perm
,它存储确定执行 IPC
操作的权限所需的信息!
这几个结构体如下:
struct shmid_kernel
{
struct shmid_ds u;
/* the following are private */
unsigned long shm_npages; /* size of segment (pages) */ // 表示共享内存使用了多少个内存页
pte_t *shm_pages; /* array of ptrs to frames -> SHMMAX */ // 指向了共享内存映射的虚拟内存页表项数组
struct vm_area_struct *attaches; /* descriptors for attaches */ //
};
static struct shmid_kernel *shm_segs[SHMMNI]; // SHMMNI等于128
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */ // 该空间的所有权等属性,其中包括了用来唯一标识的key
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 */ // 创建该空间的进程pid
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */ // 最后一个关联或者挂接该空间的进程pid
unsigned short shm_nattch; /* no. of current attaches */ // 当前空间的挂接数
......
};
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */ // 这个就是用来标识shmid唯一性的key
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effevtive GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
}
在我们讲接口之前看到这些可能会疑惑,但是在后面讲完接口调用的时候,这里对我们来说比较重要的就是这个 shmid_ds
以及 ipc_perm
中的 __key
!
从上面的讲解我们可以得到:通过让不同的进程,看到同一个内存块且进行通信的方式,叫做共享内存!
下面我们提出几个概念,为了后面我们引出共享内存申请、调用等等的内容做铺垫:
IPC
的,这和 malloc
虽然理论和思想上差不多,但是却有完全不一样的作用,进程间通信主要是为了解决进程间共享的内存问题。
那么第一个概念其实就是想说,既然专门设计了进程间通信,那么肯定底层会有对应的接口供我们使用!
而后面两个概念就是为了帮助我们理解这些接口参数的概念!下面我们来介绍一下这些接口!
首先我们得知道共享内存的一些特性:
接下来我们先来看看在 linux
下如何查看对应的 共享内存空间
、消息队列
以及 信号量
!方便我们调用接口的时候查看!
ipcs
指令的作用是报告进程间通信设施的状态,包括共享内存、消息队列以及信号量等等!
下面我们来看一下 ipcs
的指令选项,这里只使用几个比较常见的,其他的选项可以参考下面 ipcs -help
中的!
key
:用来传递给 shmid 的编号msqid
:消息队列的编号shmid
:共享内存段的编号semid
:信号量数组的编号owner
:创建该空间的用户perms
:权限nattch
:挂接数,也就是连接到共享内存的进程个数status
:共享内存段的状态(是否有进程关联等等)bytes
:创建的大小used-bytes
:消息队列已使用的大小nsems
:对应信号量数组中信号量的个数ipcs -m
:单独查看共享内存段ipcs -q
:单独查看消息队列ipcs -s
: 单独查看信号量数组ipcs -a
或 ipcs
: 查看所有的资源(设施) 这里以 -c
显示创建者和拥有者为例:ipcs -c
和 ipcs -c -s
,其它选项如 -t
、-p
、-l
、-u
、-b
也是同理的!
使用 -i
选项的时候要配合 semid
或者 shmid
一起使用,而不能一起单独使用!
本来这个共享内存段的删除内容是要在后面说的,这个顺序会比较合理一点,但是还是觉得不卖关子,我们先把删除解决了,并且要提一下为什么要进行删除操作!
之前我们说过一个进程如果不想使用该共享内存段了,那么就得将该进程与该共享内存的映射去掉,也就是去关联,这是为了防止我们后面做了不当的操作影响到该共享内存中的其它进程通信!
除此之外,共享内存段的生命周期是随操作系统的,而不是随进程的(System V
版本的通信生命周期都是随操作系统的),也就是说就算没有进程指向该共享内存段,这个内存段也是会存在的,那么如果我们不手动对其释放,那么就要等到操作系统关闭的时候才能关闭,而共享内存段是占有内存大小的,所以这极有可能成为内存耗尽的隐患!所以当我们不想使用该段空间的时候,我们就得手动将其释放掉,而这就要借助到 ipcrm
指令了!
ipcrm -m shmid编号
注意共享内存只有在当前映射链接数为0时才会被删除释放!
那么这里可能会有人问,为什么不是删除对应的 key
编号而是 shmid
编号呢 ❓❓❓
这里我们就得搞清楚 key
和 shmid
的关系:
key
只是用来在OS
层面上唯一标识的,不能用来管理共享内存的;shmid
是OS
给用户用来标识共享内存段的id
,用来在用户层进行共享内存段的管理的!
因为我们是用户,所以我们是在用户层进行操作,只能通过 shmid
编号来进行删除!
🐇 补充: ipcrm -a
表示释放所有进程间通信的资源
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
// 作用:得到一个共享内存标识符,或者创建一个共享内存对象并返回共享内存标识符
// 返回值:获取成功则返回一个非负数,即共享内存标识符,取决于shmflg的参数(不同操作系统返回值不同);获取失败则返回-1,并且设置错误码errno
我们来单独看看这个函数的参数:
ftok
函数来获取,下面会讲)0
时表示只获取共享内存open
等函数接口参数类似,其实就是宏,并且可以用按位或连接不同的选项) key
相等的共享内存空间则直接返回其内存标识符,若不存在则创建一个共享内存并返回其标识符key
相等的共享内存空间则报错,若不存在则创建一个共享内存并返回其标识符(IPC_EXCL
单独使用的时候没有意义)0666
、0664
等等 这里的参数其实有多个细节,我们先来谈一下,后面就不会再细谈了!
先来谈谈这里的 size
,一般来说,共享内存的大小我们都是建议是 4KB
的整数倍,因为系统分配共享内存是以 4KB
为单位的(这其实是内存划分内存块的基本单位 page
,这个我们后面会讲!)
那么如果我们将其大小设为 4099
个字节而不是 4096
个字节会发生什么呢 ❓❓❓
内核在分配大小的时候会进行以 4KB
为单位向上取整,也就是 8192
字节即 8KB
,但是我们会发现创建出来的共享内存打印大小的时候还是 4099
字节啊,这和我们想的不符合啊,为什么不是 8192
字节呢 ❓❓❓
其实就是因为 4099
字节只是我们申请的在共享内存段中可使用的大小,但是实际上 OS
还是会为我们在内存中申请 8192
字节大小的内存空间,但是我们只能使用 4099
字节大小的空间!
再来谈一谈这个 key
,首先我们这个 shmget
函数就是为了得到一个唯一标识的共享内存段标识符,但是我们怎么保证它就是唯一的呢 ❓❓❓
其实就是通过这个 key
,key
是多少不重要,最重要的是 key
要能进行唯一性标识,而这个 key
是通过我们下面会讲的 ftok
函数得到的,操作系统会将文件路径与项目标识符转化为一个 System V IPC
的 key
!
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
// 作用:这个函数会根据传的路径名和id值,通过算法形成一个key值
// 返回值:成功的话返回这个得到的key值(key_t其实就是int),失败的话返回-1,并设置错误码errno
// 参数:
// pathname:自定义路径,但是必须是一个存在的路径,不一定是当前文件的路径
// proj_id:自定义id,但是必须是非0
这样子只要我们让想要通信的进程,通过相同的 pathname
和 proj_id
传给 ftok
函数,那么其返回的必定是一个相同的 key
,因为底层使用的算法是不变的!再使用 shmget
函数获取共享内存段标识符,这样子就能让不同的进程看到同一份资源!
注意:key_t
类型其实就是 int
类型!
那么此时会有问题,既然 key
是一个唯一用来标识这个共享内存的位置的,那么为什么还要用 shmid
呢 ❓❓❓
我们可以先举个例子,我们一般在学校里用的是学号,在公司里用的是工号等等,为什么不直接使用我们的身份证呢,因为这样子的话即使我们的身份证被修改了,我们在学校、公司等场合用的也不是身份证,这种现象就叫做 低耦合,也是一种常见的编码思维!
我们的 key
是在内核层用来标定唯一性的,而 shmid
是在用户层来标定唯一性的,哪一天我们的操作系统可能改了,那我们用户层很多标识符就都得改了,但是如果每个软件都有各自的 shmid
的话而不是以统一的 key
为标识的话,这样子一来耦合性就降低了很多,当内核层出现问题时用户层也不必大费周章!
#include <sys/type.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 作用:对共享内存进行控制,如删除共享内存、得到或修改共享内存的状态
// 返回值:执行成功则返回0,失败返回-1,并且设置错误码errno
其中参数为:
shmid
shmid_ds
结构复制到 buf
中buf
指向的 shmid_ds
结构中的 uid
、gid
、mode
复制到共享内存中的 shmid_ds
结构体内struct shmid_ds*
也就是我们上面讲过的结构体用于管理共享内存的属性信息,如果我们想要获取这个共享内存的属性等则可以创建一个该类型的结构体传递过去,若不想获取任何内容的话可以直接设为 NULL
。💘注意: 共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为 0
时,表示没有进程访问了,共享内存才会真正被删除!
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 作用:把共享内存区对象映射到调用进程的地址空间
// 返回值:成功的话则返回关联好的共享内存的地址,失败的话返回-1,并设置错误码errno
其中参数为:
shmid
NULL
,交给 OS
自己决定一个合适的地址位置即可SHM_RDONLY
则表示只读方式关联,否则以读写方式关联,一般传 0
即可! 💘注意:fork
后子进程继承已关联(attach
)的共享内存地址。但是 exec
后该子进程与已连接的共享内存地址自动去关联(detach
)。进程结束后,已关联的共享内存地址也会自动去关联(detach
)。
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
// 作用:与对应shmaddr位置处的共享内存去关联
// 返回值:成功去关联则返回0,失败则返回-1,并且设置错误码errno
// 参数:shmaddr表示关联的共享内存的起始地址
💘注意:去关联不等同于删除共享内存!
有了上面这些函数与知识铺垫,我们大概理清一下步骤:
server
端: client
端: 现在我们可以把上述内容实现包装到一个 .hpp
文件中,然后 server
和 client
分别去调用即可,下面给出整体的代码:(具体现象自行上机观察)
comm.hpp
:
#ifndef _COMM_HPP_
#define _COMM_HPP_
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
using namespace std;
// 设置两个常量pathname和proj_id,用来获取key值
const char* PATHNAME = ".";
const int PROJ_ID = 0x66;
const int MAX_SIZE = 4096; // 共享内存的大小
// 调用ftok函数获得key
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJ_ID);
if(k == -1)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(1);
}
return k;
}
// 创建或者获取共享内存标识符的函数
int getShmHelper(key_t k, int flags)
{
int shmid = shmget(k, MAX_SIZE, flags);
if(shmid == -1)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
// 为client提供获取共享内存标识符的函数
int getShm(key_t k)
{
return getShmHelper(k, IPC_CREAT);
}
// 为server提供创建共享内存标识符的函数
int createShm(key_t k)
{
// 注意这里是创建shm,所以必须也得给共享内存带上访问权限
return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}
// 关联共享内存
void* attachShm(int shmid)
{
// 注意当前是64位机,所以指针大小为8字节,所以不能强转为int
void* start = shmat(shmid, nullptr, 0);
if((long long)start == -1L)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(4);
}
return start;
}
// 去关联
void detachShm(const void* shmaddr)
{
if(shmdt(shmaddr) == -1)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(5);
}
}
// 删除共享内存
void delShm(int shmid)
{
if(shmctl(shmid, IPC_RMID, nullptr) == -1)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(3);
}
}
#endif
server.cpp
:
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("0x%x\n", k);
int shmid = createShm(k);
printf("shmid: %d\n", shmid);
// 关联该共享内存
char* start = (char*)attachShm(shmid);
printf("attach success, address start: %p\n", start);
sleep(5);
// 使用
while(true)
{
sleep(1);
// 不需要读取到数组中,因为可以直接通过共享内存地址start打印出来
printf("client say: %s\n", start);
// 调用shmctl获取共享内存的属性
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
printf("获取属性: size: %d, pid: %d, myself: %d, key: 0x%x\n",\
ds.shm_segsz, ds.shm_cpid, getpid(), ds.shm_perm.__key);
}
// 去关联
detachShm(start);
sleep(5);
// 一般谁创建shm,谁删除shm!
delShm(shmid);
return 0;
}
client.cpp
:
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = getShm(k);
printf("shmid: %d\n", shmid);
// 关联该共享内存
char* start = (char*)attachShm(shmid);
printf("attach success, address start: %p\n", start);
sleep(3);
// 使用
pid_t id = getpid();
int cnt = 1;
const char* str = "i am client! i am talking with you!";
while(true)
{
sleep(1);
// 不需要先加载到字符数组中,直接可以通过共享内存地址写入即可
snprintf(start, MAX_SIZE, "%s : [pid: %d][cnt: %d]", str, id, cnt++);
}
// 去关联
detachShm(start);
sleep(3);
return 0;
}
makefile
:
.PHONY : all
all : shm_client shm_server
shm_client : client.cpp
g++ -o $@ $^ -std=c++11
shm_server : server.cpp
g++ -o $@ $^ -std=c++11
.PHONY : clean
clean :
rm -f shm_client shm_server
1、共享内存区是最快的 IPC
形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用(比如 write
、read
等)来传递彼此的数据。
2、System V IPC
的 生命周期是随内核的!(就算创建 System V
资源的进程退出了,但是它申请的资源还存在)只能通过 OS
重启,或者程序员手动释放来清理资源。
3、当 client
端没有写入,甚至没有启动的时候,server
端不会像管道一样等待 client
端写入!因为 共享内存不提供任何同步或互斥机制,需要程序员自行保证数据的安全!所以不太安全!
1、由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥。
2、系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
3、在进程中涉及到互斥资源(临界资源)的程序段叫临界区。(其余的就是非临界区)
4、要么不做,要么就做完,我们称这种情况为:原子性
5、所有的进程在访问共享资源之前,都必须先申请 sem
信号量,查看是否共享资源还有位置(就像我们去买电影票一样,必须看看还有没有座位),而在这之前所有进程必须先得看到同一个信号量,而信号量本身也是一个共享资源,那么信号量必须保证自己的安全性(不然随便进程的 ++
还是 --
操作可能就让信号量乱了,这样子自己都不安全,怎么去管理其它资源?)。当进程申请完 sem
信号量之后,对应的 sem--
,然后就要去到这段共享资源中查找空闲的子资源(相对于电影票是有的,但是我们得选空的座位),并且占位;之后释放的时候对应的 sem++
,也就是该位置腾了出来!
消息队列和共享内存、信号量一样,同属 System V IPC
通信机制。消息队列是一系列连续排列的消息,保存在内核中,通过消息队列的引用标识符来访问。使用消息队列的好处是对每个消息指定了特定消息类型,接收消息的进程可以请求接收下一条消息,也可以请求接收下一条特定类型的消息。
消息队列的结构如下:
整个消息队列具有以下两种类型的数据结构:
由上图可以看出,消息队列实为一个链表,虽然绝大多数教科书说消息队列按 FIFO
的方式读取,但据笔者来看,完全可以根据消息的类型来访问,并不一定要按 FIFO
的方式, 只是在组织形式上为 FIFO
的方式。在 Linux
操作系统中,对消息队列进行了以下规定:
16
个消息队列。16384
字节。8192
字节。 msqid_ds
的数据结构如下:
// come from /usr/include/msg.h
struct msqid_ds
{
struct ipc_perm msq_perm; // 对应于该消息队列的ipc_perm结构指针,就是权限
struct msg *msg_first; // msg结构指针(msg结构用于表示一个消息),此指针指向消息队列中的第一个消息
struct msg *msg_last; // msg结构指针,指向消息队列中的最后一个消息
ulong msg_ctypes; // 记录消息队列中当前的总字节数
ulong msg_qnum; // 记录消息队列中当前的总消息数
ulong msg_qbytes; // 记录消息队列中最大可容纳的字节数
pid_t msg_lspid; // 最近一个执行 msgsnd 函数的进程的 PID
pid_t msg_lrpid; // 最近一个执行 msgrcv 函数的进程的 PID
time_t msg_stime; // 最近一次执行 msgsnd 函数的时间
time_t msg_rtime; // 最近一次执行 msgrcv 函数的时间
time_t msg_ctime; // 最近一次改变该消息队列的时间
}
ipc_perm
结构体定义如下:
// come from /usr/include/bits/ipc.h
/* Data structure used to pass permission information to IPC operations. */
struct ipc_perm
{
__key_t __key; // IPC对象的key值,用于唯一标识该IPC对象
__uid_t uid; // 拥有者ID
__gid_t gid; // 拥有者组ID
__uid_t cuid; // 创建者ID
__gid_t cgid; // 创建者组ID
unsigned short int mode; // 读写权限
unsigned short int __pad1; // 填充字段,保证ipc_perm结构体的大小为32字节
unsigned short int __seq; // IPC对象的序列号,用于区分不同的IPC对象
unsigned short int __pad2; // 填充字段,保证ipc_perm结构体的大小为32字节
unsigned long int __unused1; // 未使用的字段,保证ipc_perm结构体的大小为64字节
unsigned long int __unused2; // 未使用的字段,保证ipc_perm结构体的大小为64字节
};
在 Linux
系统中,IPC
对象包括消息队列、共享内存和信号量等,它们用于实现进程间的通信和同步。ipc_perm
结构体用于描述这些 IPC
对象的权限信息,包括拥有者、读写权限等。这些信息对于进程间的安全和正确性非常重要,因此 ipc_perm
结构体在 Linux
系统中发挥着重要的作用。
struct meg
结构体的定义如下:
// come from /usr/src/kernels/'uname –r'/include/linux/msg.h
/* one msg_msg structure for each message */
struct msg_msg
{
struct list_head m_list; // 用于将消息加入到消息队列中的链表中,实现FIFO的队列访问方式。
long m_type; // 消息的类型,用于区分不同的消息。
int m_ts; // 消息大小,以字节为单位。
struct msg_msgseg* next; // 下一个消息位置
void *security; // 真正消息位置
/* the actual message follows immediately */
};
消息队列所传递的消息由两部分组成:即消息的类型和所传递的数据。一般用一个结构体来表示。通常消息类型用一个正的长整数表示,而数据则根据需要设定。比如设定一个传递 1024
个字节长度的字符串数据的消息如下:
struct msgbuf
{
long msgtype;
char msgtext[1024];
}
传递消息时将所传递的数据内容写入 msgtext
中,然后把这个结构体发送到消息队列中即可!
下面是消息队列的一些特点:
1、消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法!
2、每个数据块都被认为是有一个类型 type
,接收者进程接收的数据块可以有不同的类型值!
3、IPC
资源必须删除,因为不会自动清除,除非重启,所以 system V IPC
资源的生命周期随内核,参考共享内存!
消息队列的接口和共享内存的接口都是大同小异的!下面只写出几个常见的接口,具体的用法可自行查看 man
手册:
int msgget(key_t key, int msgflg); // 消息队列的创建与打开
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); // 向消息队列中发送消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); // 从消息队列中接收消息
int msgctl(int msqid, int cmd, struct msqid_ds *buf); // 消息队列的控制
在学习 IPC
信号量之前,让我们先来了解一下 Linux
提供两类信号量:
POSIX
信号量和 System V
信号量。💘 POSIX
信号量与 System V
信号量的区别如下:
POSIX
来说,信号量是个非负整数,常用于线程间同步。而 System V
信号量则是一个或多个信号量的集合,它对应的是一个信号量结构体,这个结构体是为 System V IPC
服务的,信号量只不过是它的一部分,常用于进程间同步。POSIX
信号量的引用头文件是 <semaphore.h>
,而 System V
信号量的引用头文件是 <sys/sem.h>
。System V
信号量的使用比较复杂,而 POSIX
信号量使用起来相对简单。下面我们来介绍 System V
信号量编程的基本内容。
信号量是一种用于对多个进程访问共享资源进行控制的机制。共享资源通常可以分为两大类:
信号量是为了 解决互斥共享资源的同步问题而引入的机制。信号量的实质是整数计数器,其中记录了可供访问的共享资源的单元个数。本文接下来提到的信号量都特指 System V IPC
信号量。
当有进程要求使用某一资源时,系统首先要检测该资源的信号量,如果该资源的信号量的值大于 0
,则进程可以使用这一资源,同时信号量的值减 1
。进程对资源访问结束时,信号量的值加 1
。如果该资源信号量的值等于 0
,则进程休眠,直至信号量的值大于 0
时进程被唤醒,访问该资源。
信号量中一种常见的形式是 双态信号量,即要么不做,要么做完。双态信号量对应于只有一个可供访问单元的互斥共享资源,它的初始值被设置为 1
,任一时刻至多只允许一个进程对资源进行访问。
信号量用于实现对任意资源的锁定机制。它可以用来同步对任何共享资源的访问。
System V
子系统提供的信号量机制是比较复杂的。我们不能单独定义一个信号量,而只能定义一个信号量集,其中包括一组信号量,同一信号量集中的信号量可以使用同一 ID
引用。每个信号量集都有一个与其相对应的结构,其中包含了信号量集的各种信息,该结构的声明如下:
// come from /usr/include/linux/sem.h
struct semid_ds
{
struct ipc_perm sem_perm; // 对应于该信号量集的ipc_perm结构,也就是权限
struct sem *sem_base; // sem结构指针,指向信号量集中第一个信号量的sem结构
ushort sem_nsems; // 信号量集中信号量的个数
time_t sem_otime; // 最近一次调用semop函数的时间
time_t sem_ctime; // 最近一次改变该信号量集的时间
struct sem_queue *sem_pending; // 阻塞在该信号量集上的进程队列
struct sem_queue **sem_pending_last; // 指向最后一个阻塞在该信号量集上的进程的指针
struct sem_undo *undo; // 用于实现撤销操作的undo队列
unsigned short sem_nsems; // 当前信号量集中已经分配的信号量的数量
};
sem
结构记录了一个信号量的信息,其声明如下:
struct sem
{
ushort semval; // 信号量的值
pid_t sempid; // 最后一次返回该信号量的进程ID
ushort semncnt; // 等待可利用资源出现的进程数
ushort semzcnt; // 等待全部资源可被独占的进程数
};
int semget(key_t key, int nsems, int semflg); // 信号量集的创建与打开
int semop(int semid, struct sembuf *sops, size_t nsops); // 对信号量集的操作
int semctl(int semid, int semnum, int cmd, union semun arg); // 信号量的控制
// 用于表示Linux系统中对信号量进行操作时的参数
struct sembuf
{
short sem_num; // 要操作的信号量在信号量集里的编号
short sem_op; // 进行的操作,可以为负数、零或正数,表示减少、不变或增加信号量的值
short sem_flag; // 操作的标志,可以为IPC_NOWAIT(非阻塞)或0(阻塞)
};
// 用于在Linux系统中进行信号量操作时的参数传递
union semun
{
int val; // 用于设置信号量的初始值,或者用于获取信号量的当前值
struct semid_ds *buf; // 指向semid_ds结构体的指针,用于获取或设置信号量集的属性
ushort *array; // 指向ushort类型数组的指针,用于获取或设置信号量集中多个信号量的值
};
观察仔细的话我们可以发现,system V IPC
的不同方式的接口相似度是非常高的,无论是 shmid_ds
、msgid_ds
还是 semid_ds
,它们这些结构体的其中第一个字段都是 struct ipc_perm
结构体类型,并且它们的属性内容都是相同的,都存放着 _key
、uid
、gid
等等属性!
其实在 linux
中实现的逻辑大概是这样子的:
这其实就对比于 c++
中的多态属性!但是当时 c++
还没诞生,其实 c语言
中早就有面向对象的思想了!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有