学习目标:
学习信号量及其属性
进行同步实验
研究临界区的行为
使用POSIX命名信号量和无名信号量
理解信号量的管理
1. 临界区
临界区是指必须以互斥的方式执行的代码段,也就是说临界区范围内只能由一个活动的线程。例如:修改共享变量的过程中其他的执行线程可能会访问共
享变量,那么修改共享变量的代码就被看成是临界区的一部分。
临界区问题指用安全、公平和对称的方式来执行临界区代码的问题
2. 信号量
信号量是(S)一个整型变量,它带有两个原子操作信号量锁定wait和信号量解锁signal。
可以将其看成一个整数值和一个等待signal操作的进程列表。
wait操作:如果S大于零,wait就在一个原子操作中测试S,并对其进行减量运算;
如果S等于零,wait就在一个原子操作中测试S,并阻塞调用程序。
signal操作:如果有线程在信号量上阻
塞,S就等于零,signal就会解除对某个等待线程的阻塞;
如果没有线程在信号量上阻塞,signal就对S进行增量运算。
信号量作用:
a:保护临界区
wait(&s)
signal(&s);
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
sem_t sem;
信号量的初始化:必须在使用信号量之前对其进行初始化
#include
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
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
int sem_post(sem_t *sem);
成功返回0,不成功返回-1并设置errno,必须检测的错误码:
EINVAL *sem不对应有效的信号量
函数sem_init是信号安全的,可以在信号处理程序中调用它。
wait操作:
#include
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
#include
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
#include
#include
#include
#include
#include
#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 semaphore");
return 1;
}
if (sem_init(&semlock, 0, 1) == -1)
{
perror ("Failed
to initialize semaphore");
return 1;
}
for (i=0; 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
{
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
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
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
#include
#include
#include
#include
#include
#include
#include
#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
{
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
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
int sem_close(sem_t *sem);
成功返回0,不成功返回-1并设置errno,检测错误码:
EINVAL *sem不是一个有效的信号量
删除命名信号量:
#include
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
#include
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没有提供方法来确定那些命名信号量是存
在的。当显示目录内容是,他们又可能会出现,也有可能不出现。当系统重启时,他们有可能被销毁,也可能不被销毁。