15 POSIX信号量
15.1 POSIX信号量基本概念
信号量(Semaphore)是一种实现进程/线程间通信的机制,可以实现进程/线程之间同步或临界资源的互斥访问, 常用于协助一组相互竞争的进程/线程来访问临界资源。在多进程/线程系统中, 各进程/线程之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。
在 POSIX标准中,信号量分两种:
- 无名信号量,无名信号量一般用于进程/线程间同步或互斥,无名信号量则直接保存在内存中
- 有名信号量,有名信号量一般用于进程间同步或互斥。 而有名信号量则要求创建一个文件
而且在不同进程/线程之间,为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程/线程之间的互斥关系。 为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权, 在任一时刻只能有一个执行进程/线程访问代码的临界区域。
临界区域是指执行数据更新的代码需要独占式地执行。 而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个进程/线程在访问它, 因此信号量是可以用来调协进程/线程对共享资源的访问的。进程/线程之间的互斥与同步关系存在的根源在于临界资源。 临界资源是在同一个时刻只允许有限个(通常只有一个)进程/线程可以访问(读)或修改(写)的资源, 通常包括硬件资源(处理器、内存、存储器以及其他外围设备等)和软件资源(共享代码段,共享结构和变量等)。
抽象的来讲,信号量中存在一个非负整数,所有获取它的进程/线程都会将该整数减一(获取它当然是为了使用资源), 当该整数值为零时,所有试图获取它的进程/线程都将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数, 表示剩下的可被占用的互斥资源数。其值的含义分两种情况:
- 0:表示没有可用的信号量,进程/线程进入睡眠状态,直至信号量值大于 0。
- 正值:表示有一个或多个可用信号量,进程/线程可使用该资源。进程/线程将信号量值减1。
对信号量的操作可以分为两个:
- P 操作:如果有可用的资源(信号量值大于0),则占用一个资源(给信号量值减去一,进入临界区代码); 如果没有可用的资源(信号量值等于0),则阻塞,直到系统将资源分配给该进程/线程(进入等待队列, 一直等到资源轮到该进程/线程)。
- V 操作:如果在该信号量的等待队列中有进程/线程在等待资源,则唤醒一个阻塞的进程/线程。如果没有进程/线程等待它, 则释放一个资源(给信号量值加一)
15.2 POSIX有名信号量
如果要在Linux中使用信号量同步,需要包含头文件 semaphore.h
。
有名信号量其实是一个文件,它的名字由类似 “sem.[信号量名字]”
这样的字符串组成,注意看文件名前面有 “sem.”
, 它是一个特殊的信号量文件,在创建成功之后,系统会将其放置在 /dev/shm
路径下, 不同的进程间只要约定好一个相同的信号量文件名字,就可以访问到对应的有名信号量, 并且借助信号量来进行同步或者互斥操作,需要注意的是,有名信号量是一个文件,在进程退出之后它们并不会自动消失, 而需要手工删除并释放资源。
主要用到的函数:
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
- sem_open()函数用于打开/创建一个有名信号量,它的参数说明如下:
- name:打开或者创建信号量的名字。
- oflag:当指定的文件不存在时,可以指定 O_CREATE 或者 O_EXEL进行创建操作, 如果指定为0,后两个参数可省略,否则后面两个参数需要带上。
- mode:数字表示的文件读写权限,如果信号量已经存在,本参数会被忽略。
- value:信号量初始的值,这这个参数只有在新创建的时候才需要设置,如果信号量已经存在,本参数会被忽略。
- 返回值:返回值是一个sem_t类型的指针,它指向已经创建/打开的信号量, 后续的函数都通过改信号量指针去访问对应的信号量。
- sem_wait()函数是等待(获取)信号量,如果信号量的值大于0,将信号量的值减1,立即返回。如果信号量的值为0, 则进程/线程阻塞。相当于P操作。成功返回0,失败返回-1。
- sem_trywait()函数也是等待信号量,如果指定信号量的计数器为0,那么直接返回EAGAIN错误,而不是阻塞等待。
- sem_post()函数是释放信号量,让信号量的值加1,相当于V操作。成功返回0,失败返回-1。
- sem_close()函数用于关闭一个信号量,这表示当前进程/线程取消对信号量的使用,它的作用仅在当前进程/线程, 其他进程/线程依然可以使用该信号量,同时当进程结束的时候,无论是正常退出还是信号中断退出的进程, 内核都会主动调用该函数去关闭进程使用的信号量,即使从此以后都没有其他进程/线程在使用这个信号量了, 内核也会维持这个信号量。
- sem_unlink()函数就是主动删除一个信号量,直接删除指定名字的信号量文件。
15.3 POSIX无名信号量
无名信号量的操作与有名信号量差不多,但它不使用文件系统标识,直接存在程序运行的内存中, 不同进程之间不能访问,不能用于不同进程之间相互访问。同样的一个父进程初始化一个信号量, 然后fork其副本得到的是该信号量的副本,这两个信号量之间并不存在关系。
主要用到的函数:
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
- sem_init():初始化信号量。
- 其中sem是要初始化的信号量,不要对已初始化的信号量再做sem_init操作,会发生不可预知的问题。
- pshared表示此信号量是在进程间共享还是线程间共享,由于目前Linux 还没有实现进程间共享无名信号量, 所以这个值只能够取0,表示这个信号量是当前进程的局部信号量。
- value是信号量的初始值。
- 返回值:成功返回0,失败返回-1。
- sem_destroy():销毁信号量,其中sem是要销毁的信号量。只有用sem_init初始化的信号量才能用sem_destroy()函数销毁。 成功返回0,失败返回-1。
- sem_wait()、sem_trywait()、sem_post()等函数与有名信号量的使用是一样的。