进程间通信 – 信号量

2017年6月2日23:31:07 1 188

 

信号量是什么

信号量的本质是对临界资源的计数器,它是一种数据操作锁,信号量决定是否可以访问临界资源,从而对临界资源起到保护作用。信号量通过对临界区的互斥保护,来保护临界资源。临界区是指执⾏数据更新的代码需要独占式地执行。

既然信号量不能向管道或消息队列一样完成进程间通信,那为什么把信号量归属于进程间通信范畴?因为要让两个进程同时看到一个信号量,这里也遵守了System V版的进程间通信原理。

当请求一 个使用信号量来表的资源时,进程需要先读取信号量的值来判断资源是否可用。⼤于0,资源可以请求,等于0,无资源可用,进程会进⼊睡眠状态直至资源可用。所以信号量实际上是一种挂起等待锁。

信号量是为了保护临界资源,但是信号量的实现依靠了进程间通信原理,本质上信号量也是临界资源。那么问题来了,信号量为了保护临界资源访问的互斥,但是谁来保护信号量的同步与互斥?这里就不得不提信号量的两个基本操作,P操作和V操作:

P操作:当进程申请到一个信号量时,信号量的值 -1

V操作:当进程不再使用一个信号量控制的共享资源时,信号量的值 +1

对信号量的值进行的增减操作均为原子操作,但是在信号量的创建及初始化上,不能保证操作均为原子性。

为什么要保护临界资源呢?因为如果多个进程同时访问临界资源,可能会导致数据的二义性,比如读取到无效数据,写入数据覆盖其他数据。

新概念解释

  • 临界资源:一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。
  • 临界区:临界区内的数据一次只能同时被一个进程使用,当一个进程使用临界区内的数据时,其他需要使用临界区数据的进程进入等待状态。
  • 互斥:指某一资源同时只允许一个访问者对其进行访问。
  • 原子性:一个事务包含多个操作,这些操作要么全部执行,要么都不执行。
  • 同步: 基本上都是以互斥为条件,让不同的进程访问临界资源,以某种特定的顺序去访问。

二元信号量

二元信号量是最简单的一种锁,它只用两种状态(占有与非占用)。它适合只能被唯一一个线程访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,伺候其他的所有试图获取该二元信号量的线程将会等待,直到该锁被释放。

信号量的创建

System V进程间通信(包括消息队列、信号量以及共享内存),都依托标识符和键来实现的。其中 key 可以用 ftok函数生成。

在System V中信号量并非是单个非负值,而必须将信号量定义为含有一个或多个信号量值的集合。当创建一个信号量时,要指定该集合中信号量值的数量。参数 nsems 就是信号量集中信号量数。

semflg 参数

IPC_CREAT   如果IPC资源不存在,则创建⼀个IPC资源,否则打开操作。

IPC_EXCL:只有在IPC资源不存在的时候,新的IPC才创建,否则就产生错误。

如果将IPC_CREAT和IPC_EXCL标志一起使⽤,semget()将返回一个新建的信号量集;如果该IPC资源已存在,或者返回-1(保证返回的是全新的IPC资源)。

查看信号量集

通过shell命令:ipcs -s

即使进程结束了,创建的信号量集如果没有显式删除,则会一直存在,所以System V版本的IPC资源的声明周期都是随内核的

删除信号量集

通过shell命令:ipcrm -s semid 可以删除信号量集(其中semid可以通过ipcs -s查询)

进程间通信 - 信号量此外系统也提供了系统调用删除信号量集

其中semid为信号量集标识符,cmd设置为IPC_RMID时,semnum可以忽略为0。删除是可变参数列表设置为NULL。

This function has three or four arguments, depending on cmd.  When there are four, the fourth has the type union  semun.

信号量集的初始化

System V版本创建信号量和对信号量初始化是分开进行的,这是一个弱点,因为不能原子地创建一个信号量集合,并且对该集合中各个信号量赋初值。

当semctl函数中的 cmd 设置为SETVAL,表示给第 semnum 个信号量初始化,信号量集实质上是一个 数组,semnum 就是数组下标。

其中可变参数列表传 union semun 联合体,用里面的value初始化信号量。

信号量的操作

semop() 对由semid指定的集合中的选定信号量执行操作。

参数 sops 是 sembuf 结构体的数组,先来看 struct sembuf 结构,我们用 grep -ER "struct sembuf {" /usr/include 命令查找这个结构在哪里定义的

sem_num 是对信号量集中哪个信号量操作,集合的第一个信号量被编号0

sem_op 允许对多个信号量同时操作,如果sem_op为正整数,则操作会将此值添加到信号量值(semval)

二元信号量中,当sem_op等于-1就是我们所说的P操作,sem_op等于1时是V操作;

如果sem_op为0,则该进程必须对信号量集具有读取权限。 这是一个“等待零”操作:如果semval为0,操作即可立即进行。

sem_flg 参数

IPC_NOWAIT:不是所有操作都可以立即执行系统调用的行为

SEM_UNDO:当过程终止时,系统更新此信号量的进程撤消计数(semadj)。即在退出时恢复信号量值为初始值。

返回值

If successful semop() and semtimedop() return 0; otherwise they return -1 with errno.

semtimedop函数

当timeout参数为NULL时,它的行为和semop完全相同,另外这个函数可以设置进程睡眠时间,该睡眠受到timeout传递的 timespec 结构指定的经过时间的限制。如果达到指定的时间限制,则semtimedop() 失败,errno设置为EAGAIN

二元信号量实现互斥

我们让子进程连续打印两个A,父进程连续打印两个B,不加锁的情况下发现结构并不是预期的效果,而是A和B交叉出现。当我们在打印两个A前P操作申请信号量,打印完后V操作释放信号量。这样保证打印两个A的操作是原子的,打印B同理。

我们再验证一个问题:如果进程没有V操作,会导致其他进程阻塞式申请信号量

我给子进程里的代码加上exit(0)

这里运行程序发现进程阻塞了

进程间通信 - 信号量

然后我们sembuf的sem_flg设置为SEM_UNDO

再次编译运行程序发现打印一次AA后就一直打印BB,而不是一直等待子进程V操作

进程间通信 - 信号量

我们知道设置SEM_UNDO,当过程终止时,系统更新此信号量的进程撤消计数(semadj)。即在退出时恢复信号量值为初始值。但这个是怎么做到的呢?

 

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

目前评论:1   其中:访客  1   博主  0

    • avatar 请输入您的QQ号 2

      希望博主解答下文章最后留下的这个问题,十分感谢