目录
一、基本概念
二、有名信号量
三、无名信号量
一、基本概念
信号量(Semaphore)是一种实现进程/线程间通信的机制,可以实现进程/线程之间同步或临界资源的互斥访问, 常用于协助一组相互竞争的进程/线程来访问临界资源。在多进程/线程系统中,各进程/线程之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。
在 POSIX标准中,信号量分两种,一种是无名信号量,一种是有名信号量。 无名信号量一般用于线程间同步或互斥,而有名信号量一般用于进程间同步或互斥。 有名信号量和无名信号量的差异在于创建和销毁的形式上,但是其他工作一样,无名信号量则直接保存在内存中, 而有名信号量则要求创建一个文件。
抽象的来讲,信号量中存在一个非负整数,所有获取它的进程/线程都会将该整数减一, 当该整数值为零时,所有试图获取它的进程/线程都将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数, 表示剩下的可被占用的互斥资源数。其值的含义分两种情况:
0:表示没有可用的信号量,进程/线程进入睡眠状态,直至信号量值大于 0。
正值:表示有一个或多个可用的信号量,进程/线程可以使用该资源。进程/线程将信号量值减1, 表示它使用了一个资源单位。
对信号量的操作可以分为两个:
P 操作:如果有可用的资源(信号量值大于0),则占用一个资源(给信号量值减去一,进入临界区代码); 如果没有可用的资源(信号量值等于0),则被阻塞,直到系统将资源分配给该进程/线程(进入等待队列, 一直等到资源轮到该进程/线程)。这就像你要把车开进停车场之前,先要向保安申请一张停车卡一样, P操作就是申请资源,如果申请成功,资源数(空闲的停车位)将会减少一个,如果申请失败,要不在门口等,要不就走人。
V 操作:如果在该信号量的等待队列中有进程/线程在等待资源,则唤醒一个阻塞的进程/线程。如果没有进程/线程等待它, 则释放一个资源(给信号量值加一),就跟你从停车场出去的时候一样,空闲的停车位就会增加一个。
二、有名信号量
如果要在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()函数就是主动删除一个信号量,直接删除指定名字的信号量文件。
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
int pid;
sem_t *sem;
const char sem_name[] = "my_sem_test";
pid = fork();
if (pid < 0) {
printf("error in the fork!\n");
}
/* 子进程 */
else if (pid == 0) {
/*创建/打开一个初始值为1的信号量*/
sem = sem_open(sem_name, O_CREAT, 0644, 1);
if (sem == SEM_FAILED) {
printf("unable to create semaphore...\n");
sem_unlink(sem_name);
exit(-1);
}
/*获取信号量*/
sem_wait(sem);
for (int i = 0; i < 3; ++i) {
printf("child process run: %d\n", i);
/*睡眠释放CPU占用*/
sleep(1);
}
/*释放信号量*/
sem_post(sem);
}
/* 父进程 */
else {
/*创建/打开一个初始值为1的信号量*/
sem = sem_open(sem_name, O_CREAT, 0644, 1);
if (sem == SEM_FAILED) {
printf("unable to create semaphore...\n");
sem_unlink(sem_name);
exit(-1);
}
/*申请信号量*/
sem_wait(sem);
for (int i = 0; i < 3; ++i) {
printf("parent process run: %d\n", i);
/*睡眠释放CPU占用*/
sleep(1);
}
/*释放信号量*/
sem_post(sem);
/*等待子进程结束*/
wait(NULL);
/*关闭信号量*/
sem_close(sem);
/*删除信号量*/
sem_unlink(sem_name);
}
return 0;
}
示例代码由于信号量的控制,运行后得到的结果是:进程A连续打印0,1,2三条语句, 而进程B在A释放信号量后,B连续打印0,1,2三条语句。
假如注释掉示例代码所有跟信号量相关的操作(保留for循环里的sleep)那么由于sleep的存在,运行后得到的结果是:进程A打印0后进入睡眠释放CPU,进程B打印0后进入睡眠释放CPU;进程A打印1、进程B打印1… 即这两个进程轮流执行,轮流打印,如下图所示。
三、无名信号量
无名信号量的操作与有名信号量差不多,但它不使用文件系统标识,直接存在程序运行的内存中, 不同进程之间不能访问,不能用于不同进程之间相互访问。
无名信号量使用到的函数接口:
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()等函数与有名信号量的使用是一样的。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define THREAD_NUMBER 3 /* 线程数 */
#define REPEAT_NUMBER 2 /* 每个线程中的小任务数 */
sem_t sem[THREAD_NUMBER]; /* 保存信号量 */
/*线程函数*/
void *thread_func(void *arg)
{
int num = (int)arg;
int count = 0;
/* 等待信号量,进行 P 操作 */
sem_wait(&sem[num]);
printf("Thread %d is starting\n", num);
for (int count = 1; count < REPEAT_NUMBER; count++)
{
printf("\tThread %d: Task %d \n",num, count);
sleep(1);
}
printf("Thread %d finished\n", num);
/*退出线程*/
pthread_exit(NULL);
}
int main(void)
{
pthread_t thread[THREAD_NUMBER]; //保存线程id
int i = 0, res;
void * thread_ret;
/*创建三个线程,三个信号量*/
for (i = 0; i < THREAD_NUMBER; i++)
{
/*创建信号量,初始信号量值为0*/
sem_init(&sem[i], 0, 0);
/*创建线程*/
res = pthread_create(&thread[i], NULL, thread_func, (void*)i);
if (res != 0)
{
printf("Create thread %d failed\n", i);
exit(res);
}
}
printf("Create treads success\n Waiting for threads to finish...\n");
/*按顺序释放信号量 V操作*/
for (i = 0; i<THREAD_NUMBER ; i++)
{
/* 进行 V 操作 */
sem_post(&sem[i]);
/*等待线程执行完毕*/
res = pthread_join(thread[i], &thread_ret);
if (!res)
{
printf("Thread %d joined\n", i);
}
else
{
printf("Thread %d join failed\n", i);
}
}
for (i = 0; i < THREAD_NUMBER; i++)
{
/* 删除信号量 */
sem_destroy(&sem[i]);
}
return 0;
}
实例代码在主线程的控制下,它所创建的线程ABC按照释放信号量的次序执行,而且即使上一线程有释放CPU的操作,下一个线程也不会得到CPU的光顾, 因为它未等到自己的信号量。从而在控制下不会出现ACBBAC之类的乱序操作。