前言
如果有多个程序试图在同一时间更新这个数据库,数据就可能会遭到破坏。两个不同的程序要求不同的用户向数据库输入数据,问题会出现在对数据库进行更新的代码上。
临界区域:真正执行数据更新的代码需要独占式的执行。为了防止出现因多个程序同时访问一个共享资源而引发的问题,则需要使用一种方法,可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。
之前说过可以使用互斥量和信号量来控制对临界区域的访问。
在此会对信号量做更加普遍意义的介绍。
定义
信号量:是一个特殊变量,只允许对它进行等待(wait)和发送信号(signal)这两种操作。在Linux编程中,将用原先定义的符号来表示这两种操作。
- P(信号量变量):用于等待;
- V(信号量变量):用于发送信号。
最简单的信号量是只能取值0和1的变量,即二进制信号量。这也是信号量最常见的一种形式。
可以取多个正整数值得信号量被称为通用信号量。
PV操作的定义非常简单。假设有一个信号量变量sv,则这两个操作的定义如下表所示:
P(sv) | 如果sv的值大于零,就减去1;如果它的值等于零,就挂起该进程的执行 |
V(sv) | 如果有其他进程因等待sv而被挂起,就让它回复运行;如果没有进程因等待sv而被挂起,就加1 |
例如:
- 当临界区域可用时,信号量变量sv的值是true,然后P(sv)操作将它减1使它变为false以表示临界区域正在被使用;
- 当进程离开临界区域时,使用V(sv)操作将它加1,使临界区域再次变为可用。
再例如:
- 假设有两个进程proc1和proc2,这两个进程都需要在其执行过程中的某一时刻对一个数据库进行独占式的访问。
- 定义一个二进制信号量sv,该变量的初始值为1,两个进程都可以访问它。
- 要想对代码中的临界区域进行访问,这两个进程都需要执行相同的处理步骤,但实际上两个进程可以只是同一程序的两个不同执行实例。
- 两个进程共享信号量变量sv。一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区域。
- 而第二个进程将被阻止进入临界区域,会被挂起以等待第一个进程离开临界区域并执行V(sv)操作释放信号量。
Linux的信号量机制
Linux系统中的信号量接口经过了精心设计,提供了比通常所需更多的机制。所有的Linux信号量函数都是针对成组的通用信号量进行操作,而不是只针对一个二进制信号量。
信号量函数的定义如下所示:
#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...);
int semget(key_t key, int num_sems, int sem_flags);
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
头文件sys/sem.h通常依赖于另两个头文件sys/types.h和sys/ipc.h。通常它们被包含与sys/sem.h中。
参数key:很像一个文件名,它代表程序可能要使用的某个资源,如果多个程序使用相同的key值,它将负责协调工作。
函数介绍
- semget函数
semget函数的作用是创建一个新信号量或取得一个已有信号量的键:
int semget(key_t key, int num_sems, int sem_flags);
- key是整数值,不相关的进程可以通过它访问同一信号量。程序对所有信号量的访问都是间接的,它先提供一个键,再由系统生成一个相应的信号量标识符。
只有semget函数才直接使用信号量键,所有其他信号量函数都是使用由semget函数返回的信号量标识符。
特殊的信号量键值IPC_PRIVATE的作用:创建一个只有创建者进程才可以访问的信号量。 - num_sems:指定需要的信号量数目,它几乎总是取值为1。
- sem_flags:是一组标志,与open函数的标志非常相似。我们可以通过联合使用标志IPC_CREAT和IPC_EXCL来确保创建出的是一个新的、唯一的信号量。如果该信号量已存在,它将返回一个错误。
- semget函数再成功时返回一个正数(非零)值,它就是其他信号量函数将用到的信号量标识符。
- semop函数
semop函数用于改变信号量的值,它的定义如下所示:
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
第一个参数sem_id是由semget返回的信号量标识符。第二个参数sem_ops是指向一个结构数组的指针,每个数组元素至少包含以下几个成员。
struct sembuf{
short sem_num;
short sem_op;
short sem_flg;
}
参数描述:
- sem_num:是一个信号量编号,除非需要使用一组信号量,否则它的取值一般为0。
- sem_op:是信号量在一次操作中需要改变的数值。通常稚晖用到两个值,一个是-1,也就是P操作,一个是+1,也就是V操作,表示信号量现在已可用。
- sem_flag:通常被设置为SEM_UNDO。它将使得操作系统跟踪当前进程对这个信号量的修改情况。如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量。
semop调用的一切动作都是一次性完成的,这是为了避免出现因使用多个信号量而可能发生的竞争现象。
- semctl函数
semctl函数用来直接控制信号量信息,它的定义如下所示:
int semctl(int sem_id, int sem_num, int command, ...);
参数描述:
- sem_id:由semget返回的信号量标识符。
- sem_num:是信号量编号,当需要用到成组的信号量时,就要用到这个参数,它一般取值为0,表示这是第一个也是唯一的一个信号量。
- command参数:是将要采取的动作。
- 如果还有第四个参数,它将会是一个union semun结构,至少包含以下几个成员:
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
}
semctl函数中的comman参数可以设置许多不同的值,但只有下面介绍的两个值最常用。
- SETVAL:用来把信号量初始化为一个已知的值。这个值通过union semun中的val成员设置。其作用是在信号量第一次使用之前对它进行设置。
- IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
semctl函数将根据command参数的不同而返回不同的值。对于SETVAL和IPC_RMID,成功时返回0,失败时返回-1.
信号量实现进程间通信
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
#include <time.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
static int sem_id = 0;
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
int main(int argc, char *argv[])
{
char message = 'X';
int i = 0;
srand((unsigned int) time(NULL));
//创建信号量
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if(argc > 1)
{
//程序第一次被调用,初始化信号量
if(!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
//设置要输出到屏幕中的信息,即其参数的第一个字符
message = argv[1][0];
sleep(2);
}
for(i = 0; i < 10; ++i)
{
//进入临界区
if(!semaphore_p())
exit(EXIT_FAILURE);
//向屏幕中输出数据
printf("%c", message);
//清理缓冲区,然后休眠随机时间
fflush(stdout);
sleep(rand() % 3);
//离开临界区前再一次向屏幕输出数据
printf("%c", message);
fflush(stdout);
//离开临界区,休眠随机时间后继续循环
if(!semaphore_v())
exit(EXIT_FAILURE);
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
if(argc > 1)
{
//如果程序是第一次被调用,则在退出前删除信号量
sleep(3);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue()
{
//用于初始化信号量,在使用信号量前必须这样做
union semun sem_union;
sem_union.val = 1;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}
static void del_semvalue()
{
//删除信号量
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\n");
}
static int semaphore_p()
{
//对信号量做减1操作,即等待P(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;//P()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}
static int semaphore_v()
{
//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;//V()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}
运行结果如下所示:
输入的参数为./seml_test m,第一个参数为执行代码,第二个参数为m,在程序中传给了message,然后依次,按照延迟时间打印出来。
信号量总结
信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。我们通常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的。
参考资料
《Linux程序设计 第四版》