linux信号量机制(semaphore)
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。它们都在头文件/usr/include/semaphore.h中定义。
信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不再阻塞,选择机制是由线程的调度策略决定的。
函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。
函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
函数sem_destroy(sem_t *sem)用来释放信号量sem。
例1:使用信号量。例子中一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 从文件1.dat读取数据,每读一次,信号量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*从文件2.dat读取数据*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}
void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}
int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止程序过早退出,让它在此无限期等待*/
pthread_join(t1,NULL);
}
在Linux下,用命令gcc -lpthread sem.c -o sem生成可执行文件sem。事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,运行./sem,得到如下的结果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11
从中可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来。这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。
信号量和临界区
学习目标:
学习信号量及其属性
进行同步实验
研究临界区的行为
使用POSIX命名信号量和无名信号量
理解信号量的管理
1. 临界区
临界区是指必须以互斥的方式执行的代码段,也就是说临界区范围内只能由一个活动的线程。
例如:修改共享变量的过程中其他的执行线程可能会访问共享变量,那么修改共享变量的代码就被看成是临界区的一部分。
临界区问题指用安全、公平和对称的方式来执行临界区代码的问题
2. 信号量
信号量是(S)一个整型变量,它带有两个原子操作:信号量锁定wait和信号量解锁signal。
可以将其看成一个整数值和一个等待signal操作的进程列表。
wait操作:如果S大于零,wait就在一个原子操作中测试S,并对其进行减量运算;
如果S等于零,wait就在一个原子操作中测试S,并阻塞调用程序。
signal操作:如果有线程在信号量上阻塞,S就等于零,signal就会解除对某个等待线程的阻塞;如果没有线程在信号量上阻塞,signal就对S进行增量运算。
信号量作用:
a:保护临界区
wait(&s)
<critical section>
signal(&s);
<remainder section>
b:线程同步
process 1 executes: process 2 executes:
a; wait(&sync);
signal(&sync); b;
3. POSIX:SEM无名信号量
信号量是一个sem_t类型的变量,有相关的原子操作来对它的值进行初始化、增量和减量操作。如果一个实现在unistd.h中定义了_POSIX_SEMAPHORES,那么这个实现就支持POSIX:SEM信号量。无名信号量和命名信号量之间的区别类似于普通管道和命名管道之间的区别
信号量的申明:
#include <semaphore.h>
sem_t sem;
信号量的初始化:必须在使用信号量之前对其进行初始化
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned value);
没有规定成功时返回值,不成功返回-1并设置errno,必须检测的错误码:
EINVAL value大于SEM_VALUE_MAX
ENOSPC 初始化资源已经耗尽,或者信号量的数目超出了SEM_NSEMS_MAX的范围
EPERM 调用程序不具有适当的特权
参数pshared等于0,说明信号量只能由初始化这个信号量的进程中的线程使用;
如果pshared非零,任何可以访问sem的进程都可以使用这个信号量。
注:在创建信号量之后创建一个子进程,并没有提供对信号量的访问,子进程收到的是信号量的拷贝,而不是真的信号量。
例:创建一个只有在进程中的线程使用的信号量
sem_t semA;
if (sem_init(&semA, 0, 1) == -1 )
{
perror (“failed to initialize semaphore semA”);
}
信号量的销毁:
#include <semaphore.h>
int sem_destroy(sem_t *sem);
成功返回0,不成功返回-1并设置errno,检测错误码:
EINVAL sem不是有效的信号量
例:
if (sem_destroy(&semA) == -1)
{
perror (“Failed to destroy semA”);
}
POSIX申明:销毁一个已经销毁的信号量的结果是未定义的。有其他线程阻塞在一个信号量上时,销毁这个信号量的结果也是未定义的。
4. POSIX:SEM信号量的操作
这里描述的信号量的操作适用与无名信号量,同时也适用命名信号量
signal操作:
#include <semaphore.h>
int sem_post(sem_t *sem);
成功返回0,不成功返回-1并设置errno,必须检测的错误码:
EINVAL *sem不对应有效的信号量
函数sem_init是信号安全的,可以在信号处理程序中调用它。
wait操作:
#include <semaphore.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
成功返回0,不成功返回-1并设置errno,必须检测的错误码
EINVAL *sem不对应有效的信号量
EAGAIN 函数sem_trywait不会阻塞,而是设置errno后返回
EINTR 被信号中断
如果信号量为0,则调用进程一直阻塞直到一个相应的sem_post调用解除了对它的阻塞为止,或者直到它被信号中断为止(被信号中断后必须手动重启)。
#include <errno.h>
#include <semaphore.h>
static int shared = 0;
static sem_t sharedsem;
int initshared(int val)
{
if (sem_init(&sharedsem, 0, 1) == -1)
{
return -1;
}
shared = val;
return 0;
}
int getshared(int *val)
{
while (sem_wait(&sharedsem) == -1) //必须考虑被信号中断,重启的情况
{
if (errno != EINTR)
return -1;
}
*val = shared;
return sem_post(&sharedsem); //信号安全的,无须考虑
}
int incshared()
{
while (sem_wait(&sharedsem) == -1)
{
if (errno != EINTR)
return -1;
}
shared++;
return sem_post(&sharedsem);
}
注:如果既要在main程序中,又要在信号处理程序中对一个变量进行增量操作,如何用上面的程序保护这个变量?
如果不做一些其他的操作,是不能用它来保护这个变量的。如果上面程序中的某个函数被调用并锁定了信号量,恰恰此时中断信号被捕捉到,那么在信号处理程序中对这些函数中的某一个进行调用的时候,就会引起死锁。
正确的做法是在调用getshared和incshared之前将信号阻塞,调用完成后,解除信号阻塞。
例:创建一个信号量,并将其传递给多个线程,线程函数调用信号量保护临界区
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define TEN_MILLION 10000000L
#define BUFSIZE 1024
void *threadout(void *args)
{
char buffer[BUFSIZE];
char *c;
sem_t *semlockp;
struct timespec sleeptime;
semlockp = (sem_t *)args;
sleeptime.tv_sec = 0;
sleeptime.tv_nsec = TEN_MILLION;
snprintf (buffer, BUFSIZE, "This is a thread from process %ld\n",
(long)getpid());
c = buffer;
//临界区入口
while (sem_wait(semlockp) == -1)
{
if (errno != EINTR)
{
fprintf (stderr, "Thread failed to lock semaphore\n");
return NULL;
}
}
//临界区
while (*c != '\0')
{
fputc (*c, stderr);
c++;
nanosleep(&sleeptime, NULL); //非忙等循环
}
//临界区出口
if (sem_post(semlockp) == -1)
{
fprintf (stderr, "Thread failed to unlock semaphore\n");
}
return NULL;
}
int main(int argc, char *argv[])
{
int error;
int i;
int n;
sem_t semlock;
pthread_t *tids;
if (argc != 2)
{
fprintf (stderr, "Usage: %s numthreads\n", argv[0]);
return 1;
}
n = atoi (argv[1]);
tids = (pthread_t *)calloc(n, sizeof(pthread_t));
if (tids == NULL)
{
perror ("Failed to initialize threads");
return 1;
}
if (sem_init(&semlock, 0, 1) == -1)
{
perror ("Failed to initialize semaphore");
return 1;
}
for (i=0; i<n; i++)
{
error = pthread_create(tids+i, NULL, threadout, (void *)&semlock);
if (error != 0)
{
fprintf (stderr, "Failed to create thread:%s\n", strerror(error));
return 1;
}
}
for (i=0; i<n; i++)
{
error = pthread_join(tids[i], NULL);
if (error != 0)
{
fprintf (stderr, "Failed to join thread:%s\n", strerror(error));
return 1;
}
}
return 0;
}
注:sem_init(&semlock, 0, 1) 将semlock初始化为1,如果0的话将产生死锁。
stderr 标准输出是排他性资源,同时只能由一个线程使用。
如果改称sem_init(&semlock, 0, 2),程序输出将会混乱。
检测命名信号量和无名信号量的值:
#include <semaphore.h>
int sem_getvalue(sem_t *restrict sem, int *restrict sval);
成功返回0,不成功返回-1并设置errno,必须检测错误码:
EINVAL *sem不对应一个有效的信号量
函数可以用来检测一个命名信号量或者无名信号量的值。
5. POSIX:SEM命名信号量
命名信号量用来同步那些不共享内存的进程。
命名信号量和文件一样,有一格名字、有一个用户ID、一个组ID和权限。
如果两个进程(线程)打开的信号量一“/”开头,则其引用同一个信号量。
因此,通常都要为POSIX:SEM命名信号量使用以“/”开头的名字。
5.1创建并打开命名信号量
#include <semaphore.h>
sem_t *sem_open( const char *name, int oflag, ...);
成功返回信号量的地址,不成功返回SEM_FAILED并设置errno,必须检测的错误码:
EACCES 权限不够
EEXIST 设置了O_CREATE和O_EXCL,而且信号量存在
EINTR 函数被信号中断
EINVAL name不能作为信号量打开、或者试图用大于SEM_VALUE_MAX的值创建信号量
EMFILE 进程使用了太多的文件描述符或信号量
ENAMETOOLONG name比PATH_MAX长、或者它有一个组件超出NAME_MAX范围
ENFILE 系统中打开了太多的信号量
ENOENT 没有设置O_CREATE,而且信号量也不存在
ENOSPC 没有足够的空间了创建信号量
函数sem_open功能说明:
参数oflag用来确定是创建信号量,还是仅仅由函数对其进行访问。
如果参数oflag设置了O_CREATE比特位就必须设置mode位(mode_t类型的权限位)和value位(unsigned类型的信号量初始值)。
如果O_CREATE和O_EXCL位都设置了,那么信号量已经存在的话,函数返回一个错误。
如果仅仅设置了O_CREATE位,那么信号量如果存在,信号量会忽略O_CREATE和其他额外的参数
在信号量已经存在的情况下,POSIX没有提供直接设置命名信号量值的方法
例:访问一个命名信号量,如果不存在就创建它
#include <errno.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#define PERMS (mode_t)(S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define FLAGS (O_CREAT | O_EXCL)
#define BUFSIZE 1024
int getnamed(char *name, sem_t **sem, int val)
{
while ( ((*sem = sem_open(name, FLAGS, PERMS, val)) == SEM_FAILED) &&
(errno == EINTR)); //创建命名信号量,遇信号中断重启
if (*sem != SEM_FAILED) //创建成功返回
return 0;
if (errno != EEXIST) //失败返回,已经存在执行读取
return -1;
//信号量已经存在,读取它
while ( ((*sem = sem_open(name, 0)) == SEM_FAILED) && (errno == EINTR));
if (*sem != SEM_FAILED)
return 0;
return -1;
}
int main(int argc, char *argv[])
{
char buffer[BUFSIZE];
char *c;
pid_t childpid = 0;
int delay;
volatile int dummy = 0;
int i;
int n;
sem_t *sem_lockp;
if (argc != 4)
{
fprintf (stderr, "Usage: %s processes delay semaphorename\n", argv[0]);
return 1;
}
n = atoi(argv[1]);
delay = atoi(argv[2]);
for (i=1; i<n; i++)
{
if (childpid = fork())
break; //重要,不能少,子进程退出循环,父进程继续执行循环
}
snprintf (buffer, BUFSIZE, "i:%d process ID:%ld parent ID:%ld child ID:%ld\n",
i, (long)getpid(), (long)getppid(), (long)childpid);
c = buffer;
if (getnamed(argv[3], &sem_lockp, 1) == -1)
{
perror ("Failed to create named semaphore");
return 1;
}
while (sem_wait(sem_lockp) == -1) //进入临界区
{
if (errno != EINTR)
{
perror("Failed to lock semlock");
return 1;
}
}
while (*c != '\0') //临界区
{
fputc (*c, stderr);
c++;
for (i=0; i<delay; i++)
dummy++;
}
if (sem_post(sem_lockp) == -1) //退出临界区
{
perror("Failed to unlock semlock");
return 1;
}
if (wait(NULL) == -1) //等待子进程结束返回
return 1;
return 0;
}
注1:命名信号量就像文件一样存在系统中的。如果同时运行两个以上程序在一台机器上,则还能够正常运行
注2:如果上面的程序正在运行,输入Ctrl-C退出,然后再次运行它,有可能进程都会阻塞,因为Ctrl-C产生的信号有可能在信号量的值为0时被传递。下次运行程序时,信号量的初始值是0,所以所有的进程阻塞。
命名信号量使多个进程可以实现同步和互斥,无名信号量使同一个进程的多个线程实现同步和互斥。
5.2关闭并删除命名信号量
与命名管道一样,命名信号量在单个程序的执行之外是具有持久性的。
关闭信号量:
#include <semaphore.h>
int sem_close(sem_t *sem);
成功返回0,不成功返回-1并设置errno,检测错误码:
EINVAL *sem不是一个有效的信号量
删除命名信号量:
#include <semaphore.h>
int sem_unlink(const char *name);
成功返回0,不成功返回-1并设置errno,检测错误码:
EACCES 权限不正确
ENAMETOOLONG name比PATH_MAX长、或者它有一个组件超出NAME_MAX范围
ENOENT 信号量不存在
说明1:函数在素有的进程关闭了命名信号量之后将命名信号量从系统中删除。当进程显示地调用SEM_CLOSE、_exit、exit、exec或执行从main的返回式,就会出现关闭操作。
说民2:sem_unlink之后,即使其他的进程仍然将老的信号量打开着,用相同的名字调用的sem_open引用的也是新的信号量。即使其他的进程将信号量打开着,sem_unlink函数也总是会立即返回。
例:关闭并删除命名信号量的函数
#include <semaphore.h>
#include <errno.h>
int destroynamed(char *name, sem_t *sem)
{
int error = 0;
if (sem_close(sem) == -1)
error = errno;
if ( (sem_unlink(name) != -1) && !error)
return 0;
if (error != 0)
errno = error;
return -1;
}
注:命名信号量具有持久性的。如果创建了这样的一个信号量,即使创建它的进程和所有可以访问它的进程都终止了,它还是一直存在于系统中,保持它的值直到被销毁为止。
POSIX没有提供方法来确定那些命名信号量是存在的。当显示目录内容是,他们有可能会出现,也有可能不出现。当系统重启时,他们有可能被销毁,也可能不被销毁。