【Linux】进程间通信——信号量

让大家久等啦,本期我们来讲讲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,其本质就是一个计数器!

下面模拟一下我们使用代码访问临界资源的场景:

【Linux】进程间通信——信号量_第1张图片

我们可以看到只要是要访问临界资源的代码主要可以分为两部分:临界区和非临界区

● 当进程运行到临界区时,必须要遵守访问临界资源的规则:和信号量打交道

● 在进入临界区访问资源之前先要申请信号量,一旦信号量申请成功,该进程未来就一定可以拿到一个子资源,同时该信号量就会进行--操作;如果没有申请成功,该进程将会进入阻塞状态等待资源(上面这个过程被称为P(荷兰语:Passeren)操作

● 当我们的进程访问完临界资源后,一旦出了临界区就要释放信号量资源,同时该信号量就会进行++操作,一旦信号量进行++就表示对应的资源进行了归还(上面这个过程被称为V(荷兰语:Vrijgeven)操作

● 但是整个过程有一个问题:那必须让所以的进程看到同一个信号量啊,那这样信号量本身不就变成了共享资源了吗?那共享资源时需要被维护的,那谁来维护信号量呢?

这就需要保证信号量是经过特殊处理的,其--和++操作一定要有原子性,不能说信号量正在加或者正在减

● 那两个进程能看到同一个信号量吗?我们知道子进程会继承父进程中的环境变量,所以它们一定是可以看到同一份信号量的,但是一旦对其进行P或者V操作会发生写时拷贝啊,这样子进程之间不就各自玩各自的了吗?

所以我们要让不同的进程之间共同使用同一个信号量,最终导致信号量要被归纳到进程间通信(IPC)

 

2.2 信号量的内核结构

在Linux中信号量是这样被定义的:

The semid_ds data structure is defined in  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函数(包含在头文件中)创建或访问一个信号量集(多个不同的信号量可以被作为一个集合)

【Linux】进程间通信——信号量_第2张图片

● 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指令获取当前各个信号量的关键信息

例如:

【Linux】进程间通信——信号量_第3张图片

其中:

  • key:信号量集(semaphore set)的标识符(具有唯一性)
  • semids:信号量数组的唯一标识符。
  • owner:拥有该信号量的用户。
  • perms:表示权限的八进制数,接近于文件系统的权限,规定谁可以访问这些信号量。
  • nsems:信号量组中信号量的数量。

3.3 ipcrm -s

我们可以使用指令:ipcrm -s后面跟上一个semids(信号量数组的唯一标识符),删除指定的信号量

3.4 semctl

如果我们想在程序中删除信号量,可以使用semctl函数

【Linux】进程间通信——信号量_第4张图片

其中: 

  • 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 
#include 
#include 

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

我们现在将进程间主要的方式对应的数据结构拿出来:

消息队列:

【Linux】进程间通信——信号量_第5张图片

共享内存:

【Linux】进程间通信——信号量_第6张图片

信号量:

【Linux】进程间通信——信号量_第7张图片

我们可以看到这三个主要的通信方式对应的结构中都有struct ipc_perm这玩意

这是为什么呢?我们来看到下面:

在Linux内核中有个ipc_id_ary的柔性数组,该数组中存储的是一个个ipc_perm结构体的地址:

【Linux】进程间通信——信号量_第8张图片

每当我们创建一个进程间通信的结构体,其内部都会产生一个ipc_perm,操作系统会将该ipc_perm结构体对应的地址存入ipc_id_ary数组中,所以Linux操作系统会将内核中的所有ipc资源统一以数组的方式进行管理

这样子我们就可以拿到特有的key值去找到ipc_id_ary数组中对应的结构体,找到对应下标,那有了对应的的下标后该怎么管理内部资源呢?ipc_perm*的指针怎么访问其他成员呢?

指针强转一下不就好了嘛:((struct XXXid_ds*)ipc_id_arr[n])->other....

不过上面讲述的都是原理,其内核的具体实现方式相当复杂

最后有没有发现,对于上述的管理方式好像多态?

没错,这就是C语言中的多态表现方式!所以面向对象的语言并不是偶然产生的,这是由大量工程经验所导致的必然结果~

你可能感兴趣的:(Linux,linux,操作系统)