理解信号量。
System V 版本有消息队列 ,共享内存, 信号量 这几种进程间通信方式。其中信号量的作用是为了协调进程之间的同步与互斥。
同步:多进程/线程 要进行共同完成一项工作必须依照一定的次序协同完成,否则无法完成或者效率低下。
互斥 : 在同一时刻,只能有一个进程 访问临界资源。
临界资源: 多个进程/线程 都可以看见和访问的资源(文件, 部分内存地址)
临界区:访问临界资源的代码叫临界区。当有进程进入临界区时,其他进程必须等待,有一些同步的机制必须在临界区段的进入和退出时实现。确保公共资源被按一定的合理次序互斥所得。
信号量的本质
信号量的本质是一个统计临界资源数目的计数器 + 等待使用临界资源进程的等待队列(实际上是等待使用临界资源的进程的PCB链表)。
当一个进程来请求一个使用信号量来表示的临界资源时,进程需要先读取信号量的值来判断自己是否可以访问该临界资源
信号量 > 0; 可以申请到临界资源。
信号量 = 0;已经没有临界资源可以被访问。当出现此状况时,进程进入睡眠状态,其PCB被放入等待队列中。直至有临界资源可以被访问。
为什么不能用全局变量来表示临界资源的个数?
因为所谓全局变量的全局性是相对于一个进程用的所有函数的全局性,这是相对于一个进程内部的语言层面表述。
每个进程有自己独立的地址空间,不通过进程间通讯的手段是不能从一个进程看到别的进程的变量。且:对变量的加减不是原子操作。
原子操作:表示不可分解的操作,这样的操作只用一步完成(汇编代码只有一行)
原子操作只有做了和没有做两种状态,没有正在做的状态。
当一份临界资源被一个进程所访问时,为了保证同步与互斥,需要对表示这一临界资源的信号量进行P(sv)操作 , 当一进程完成了访问一份临界资源的任务,为了保证同步与互斥,需要对表示这一临界资源的信号量进行V(sv)操作。
P(sv):如果信号量的值大于0, 信号量执行减一操作;如果等于0,则挂起该进程的执行。
V(sv):如果有其他进程因为等待sv而被挂起, 让其回复运行访问临界资源,信号量执行加1操作。
Linux System V信号量机制
1、在System V 中信号量并非是单个非负值,而必须将这个信号量定义为一个或多个信号量值得集合,创建一个信号量是要指定集合中信号量值的数量。
2、创建信号量(semget)与 对信号量赋初始值(semctl)分开进行,信号量的创建和初始化不是原子的。
3、信号量的生命周期随内核。要避免进程异常终止时没有解锁临界资源,
可以在函数中使用关键字(SEM_UNDO)在进程退出时恢复信号量的初始值。
信号量相关系统调用
1、ftok()
#include <sys/ipc.h>
#include <sys/types.h>
key_t ftok(const char* path, int id);
参数说明:
* ftok 函数把一个已存在的路径名和一个整数标识转换成一个key_t值,即IPC关键字
表示内核中唯一的一个IPC对象。
* path 参数就是一个指定的文件名(已经存在的文件名),一般使用当前目录。当产生键时,只使用id参数的低8位。
* id 是子序号, 只使用8bit (1-255)
* 返回值:若成功返回键值,若出错返回-1
在一般的UNIX实现中,是将文件的索引节点号取出(inode),前面加上子序号得到key_t的返回值
2、semget()用来创建一个信号量集,或者获得一个已经拥有的信号量集。
include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget( key_t key, int nsems, int semflg);
- key: 所创建或打开信号量集的键值(ftok函数执行的返回值)。
- nsems:创建的信号量集中的信号量个数,该参数只在创建信号量时有效。
- semflg :调用函数的操作类型,也可用于设置信号量集的访问权限,通过or运算使用。
- IPC_CREAT |
IPC _EXCL
| 0666 :一般用于创建,可保证返回一个新的ID,同时制定权限为666- IPC_CREAT : 用于获取一个已经存在的ID
- 返回值:成功返回信号量集的标识符,失败返回-1,errno被设置成以下的某个值:
- EACESS : 没有访问该信号量集的权限。
- EEXIST:信号量集已经存在,无法创建。
- EINVAL:参数nsems的值小于0,或者大于该信号量集的限制,或者是该key关联的信号量以存在,并且nsems的值大于该信号量集的信号量数。
- ENOENT:信号量集不存在,同时没有使用,IPC_CREAT。
- ENOMEM:没有足够的内存创建新的信号量集。
3、semctl( )可以用于初始化信号量和删信号量集
#include <sys/types.h
>#include <sys/ipc.h>
>#include <sys/sem.h>
> int semctl(int semid, int semun, int cmd, ...);
semid:信号量集I P C 标识符。
* semun:信号集实质上是一个数组,那么该参数则是操作信号在信号量数组中的下标。
* cmd:在semid指定的信号量集合上执行此命令。
第三个参数cmd常用命令:
* IPC_SEAT:对此集合取semid_ds 结构,并存放在由arg.buf指向的结构中。
* IPC_RMID:从系统中删除该信号量集合。
* SETVAL:设置信号量集中的一个单独的信号量的值,此时需要传入第四个参数,,当初始化信号量时就需要设置该值。
返回值:成功返回一个正数,失败返回-1。
第四个参数是可选的,当要初始化信号量数组中的某个信号量时,就需要初始化该结构体中的val成员。
如果使用该参数,则其类型是semun,它是多特定命令参数的联合(union):
union semun
{
int val; //信号量初始值
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
4、 semop()函数 执行P V操作的函数。
#include <sys/types.h>
#include <sys/ipc.h>lude <sys/sem.h>
int semop(int semid, struct sembuf * sops, unsigned nsops);
- semid:信号集的ID,可以通过semget获取。
- sops:是一个指针,指向一个信号量操作结构体数组。信号量操作由结构体sembuf 结构
- nsops:信号操作结构的数量,恒大于或等于1.
返回值:成功执行时,都会回0,失败返回-1,并设置errno错误信息。
struct sembuf
{
unsigned short sem_num; // 在信号集中的编码 0 , 1, ...nsems-1
short sem_op; //操作 负值或正值
short sem_flg; // IPC_NOWAIT, SEM_UNDO
};
- sembuf结构体参数说明:
- sem_num:操作信号在信号集中的编号,第一个信号的编号是0,最后一个信号的编号是nsems-1。
- sem_op:操作信号量
- 若sem_op 为负(P操作), 其绝对值又大于信号的现有值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权。
- 若sem_op 为正(V操作), 该值会加到现有的信号内值上。通常用于释放所控制资源的使用权。
- sem_op的值为0:如果没有设置IPC_NOWAIT,则调用该操作的进程或线程将暂时睡眠,直到信号量的值为0;否则进程或线程会返回错误EAGAIN。
- sem_flg: 信号操作标识,有如下两种选择:
- IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
- SEM_UNDO:程序结束时(正常退出或异常终止),保证信号值会被重设为semop()调用前的值。避免程序异常情况下结束时未解锁锁定的资源,早成资源被永远锁定。造成死锁
一个简单的二元信号量代码验证
临界资源为标准输出。
代码链接:
https://github.com/xym97/Linux/tree/master/IPC/sem