让大家久等啦,本期我们来讲讲Linux系统中的信号量
目录
一、引入
二、认识信号量
2.1 信号量的概念
2.2 信号量的内核结构
三、关于信号量的接口
3.1 semget
3.2 ipcs -s
3.3 ipcrm -s
3.4 semctl
3.5 semop
四、理解IPC
一、引入
在开始之前我们先来认识几个概念:
公共资源:多个进程或线程(至于线程我们后面会详细讲解)都可以访问和使用的资源
互斥:任何一个时刻,都只允许一个执行流在进行共享资源的访问
临界资源:任何一个时刻,都只允许一个执行流在进行访问的共享资源
临界区:通过代码访问临界资源的代码
原子性:要么不做,要么做完(没有正在做的状态),只有两种确定状态的属性
二、认识信号量
2.1 信号量的概念
有了上面的概念,我们下面来引入信号量的概念:
当我们想要访问临界资源时,由于其独有的特性,我们需要知道想要访问的临界资源是否在被别的执行流访问,即使此时没有被其他的执行流访问,也并不能确定我们是否能访问该临界资源(可能该时间段被其他的执行流预定了)
所以为了管理好临界资源的调配,引入了一个整数来描述资源数量,这个东西就被称为:信号量(信号灯)
照我们现在的理解就可以将信号量看做为一个int count,其本质就是一个计数器!
下面模拟一下我们使用代码访问临界资源的场景:
我们可以看到只要是要访问临界资源的代码主要可以分为两部分:临界区和非临界区
● 当进程运行到临界区时,必须要遵守访问临界资源的规则:和信号量打交道
● 在进入临界区访问资源之前先要申请信号量,一旦信号量申请成功,该进程未来就一定可以拿到一个子资源,同时该信号量就会进行--操作;如果没有申请成功,该进程将会进入阻塞状态等待资源(上面这个过程被称为P(荷兰语:Passeren)操作)
● 当我们的进程访问完临界资源后,一旦出了临界区就要释放信号量资源,同时该信号量就会进行++操作,一旦信号量进行++就表示对应的资源进行了归还(上面这个过程被称为V(荷兰语:Vrijgeven)操作)
● 但是整个过程有一个问题:那必须让所以的进程看到同一个信号量啊,那这样信号量本身不就变成了共享资源了吗?那共享资源时需要被维护的,那谁来维护信号量呢?
这就需要保证信号量是经过特殊处理的,其--和++操作一定要有原子性,不能说信号量正在加或者正在减
● 那两个进程能看到同一个信号量吗?我们知道子进程会继承父进程中的环境变量,所以它们一定是可以看到同一份信号量的,但是一旦对其进行P或者V操作会发生写时拷贝啊,这样子进程之间不就各自玩各自的了吗?
所以我们要让不同的进程之间共同使用同一个信号量,最终导致信号量要被归纳到进程间通信(IPC)
2.2 信号量的内核结构
在Linux中信号量是这样被定义的:
The semid_ds data structure is defined in <sys/sem.h> as follows:struct semid_ds {struct ipc_perm sem_perm; /* Ownership and permissions */time_t sem_otime; /* Last semop time */time_t sem_ctime; /* Last change time */unsigned long sem_nsems; /* No. of semaphores in set */};The ipc_perm structure is defined as follows (the highlighted fields are settable using IPC_SET):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 */};
但是上面展示出来的只是对于用户层面的,其内核还有其他更复杂的结构
我们只需了解用户使用层面最重要的结构即可,其他的结构控制就交给操作系统处理
三、关于信号量的接口
3.1 semget
我们可以使用semget函数(包含在头文件<sys/types.h>和<sys/ipc.h>中)创建或访问一个信号量集(多个不同的信号量可以被作为一个集合)
● key:这是一个键(key),用于标识信号量集。通过提供一个键值,你可以创建一个新的信号量集合或访问一个现有的集合。特殊值 IPC_PRIVATE 可被用作键来创建一个新的私有信号量集,该集只能被调用者的进程及其子进程访问。对于非私有信号量,通常会使用ftok()函数基于一个文件路径和一个项目标识符来生成一个唯一的键值。
● nsems:这是信号量集中信号量的数量。当创建一个新的信号量集时,这个参数指定集合中应该有多少个信号量。如果你正在访问一个现有的集合,这个值应该设为0。
● semflg:这是一个标志集,用于确定操作的行为,该参数可以是多个标志的按位 OR 组合,这些标志提供了对权限的控制以及其他选项。此外,semflg还可以包含权限位,这些位通常是八进制的数值,比如0600,代表只有创建者有读写权限,0666则表示所有用户都有读写权限。
通常使用的标志有:
- IPC_CREAT:如果指定的信号量集不存在,那么创建它。否则,返回现有的信号量集。
- IPC_EXCL:与 IPC_CREAT 一起使用时,如果信号量集已存在,则返回错误。防止不知不觉中访问现有的信号量集。
- IPC_NOWAIT:在某些系统 V IPC 操作中使用,用于指示非阻塞操作,虽然在semget函数中它不起作用,但对于后续对信号量的操作可能有影响。
该函数的返回值是信号量集的标识符(非负整数),如果失败则返回-1,同时设置errno以指示发生了什么错误。
3.2 ipcs -s
在Linux系统中我们可以使用ipcs -s指令获取当前各个信号量的关键信息
例如:
其中:
- key:信号量集(semaphore set)的标识符(具有唯一性)
- semids:信号量数组的唯一标识符。
- owner:拥有该信号量的用户。
- perms:表示权限的八进制数,接近于文件系统的权限,规定谁可以访问这些信号量。
- nsems:信号量组中信号量的数量。
3.3 ipcrm -s
我们可以使用指令:ipcrm -s后面跟上一个semids(信号量数组的唯一标识符),删除指定的信号量
3.4 semctl
如果我们想在程序中删除信号量,可以使用semctl函数
其中:
semid
: 这是信号量集的标识符(semaphore set identifier),它是由semget
系统调用返回的,用于标识要操作的信号量集。semnum
: 这是要操作的信号量集中的信号量的序号,从 0 开始计数。对于只影响单个信号量的操作,这个参数指明了信号量集中信号量的索引。cmd
: 这是一个命令参数,它决定了要对信号量执行哪种类型的操作。一些常用的命令包括SETVAL
(设置信号量的值)、GETVAL
(获取信号量的当前值)、IPC_RMID
(删除信号量集)等。...
: 这是一个可变参数列表,其内容会根据cmd
参数的值而变化(和printf最后一个参数使用方式相同)。比如,如果cmd
是SETVAL
,则需要提供一个union semun
类型的参数,其中包含将要设置的新值。
3.5 semop
提到信号量就不得不说说pv操作了,在Linux系统下我们可以使用semop接口在 Linux 操作系统中,semop 接口用于对信号量集(semaphore set)中的一个或多个信号量(semaphores)执行操作。信号量是系统 V 信号量的组成部分,通常用于进程间的同步和互斥。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semop(int semid, struct sembuf *sops, unsigned nsops);
semid
: 这是一个整数,表示信号量集的标识符,它是通过之前的semget
系统调用创建或获取的sops
: 是一个指向一个或多个sembuf
结构体数组的指针。每个结构体指定了要对单个信号量进行的操作。(该参数需要我们自己在外部定义传入)
nsops
: 表示sops
数组中sembuf
结构体的数量,即一次系统调用中要执行的操作数量
sembuf
结构体的定义如下所示:struct sembuf {unsigned short sem_num; /* 信号量集合中信号量的索引 */short sem_op; /* 信号量操作 */short sem_flg; /* 操作标志 */ };
sem_num
: 指定要操作的信号量在信号量集中的索引
sem_op
: 指定要执行的操作。其值可能是:大于0:进行信号量的释放操作(V操作),将
sem_op
的值加到对应的信号量值上。等于0:进行信号量的等待操作,进程将阻塞直到信号量值变为0。
小于0:进行信号量的获取操作(P操作),如果信号量的当前值至少是
sem_op
绝对值,那么sem_op
的绝对值会从信号量值中减去。如果不满足,则根据sem_flg
的设置可能会阻塞。
sem_flg
: 用于控制操作的行为。可能的标志包括:
IPC_NOWAIT
: 如果操作会导致进程阻塞,那么不阻塞而立刻返回。
SEM_UNDO
: 让系统记录这个操作,如果进程在没有释放信号量的情况下终止,系统将自动撤销此前进行的信号量操作。
四、理解IPC
我们现在将进程间主要的方式对应的数据结构拿出来:
消息队列:
共享内存:
信号量:
我们可以看到这三个主要的通信方式对应的结构中都有struct ipc_perm这玩意
这是为什么呢?我们来看到下面:
在Linux内核中有个ipc_id_ary的柔性数组,该数组中存储的是一个个ipc_perm结构体的地址:
每当我们创建一个进程间通信的结构体,其内部都会产生一个ipc_perm,操作系统会将该ipc_perm结构体对应的地址存入ipc_id_ary数组中,所以Linux操作系统会将内核中的所有ipc资源统一以数组的方式进行管理
这样子我们就可以拿到特有的key值去找到ipc_id_ary数组中对应的结构体,找到对应下标,那有了对应的的下标后该怎么管理内部资源呢?ipc_perm*的指针怎么访问其他成员呢?
指针强转一下不就好了嘛:((struct XXXid_ds*)ipc_id_arr[n])->other....
不过上面讲述的都是原理,其内核的具体实现方式相当复杂
最后有没有发现,对于上述的管理方式好像多态?
没错,这就是C语言中的多态表现方式!所以面向对象的语言并不是偶然产生的,这是由大量工程经验所导致的必然结果~