什么叫信号量?它由哪几部分组成?信号量的值有什么含义

什么叫信号量?它由哪几部分组成?信号量的值有什么含义,第1张

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。

描述

编辑

以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。

在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

分类

编辑

整型信号量(integer semaphore):信号量是整数

记录型信号量(record semaphore):每个信号量s除一个整数值s.value(计数)外,还有一个进程等待队列s.L,其中是阻塞在该信号量的各个进程的标识

二进制信号量(binary semaphore):只允许信号量取0或1值

每个信号量至少须记录两个信息:信号量的值和等待该信号量的进程队列。它的类型定义如下:(用类PASCAL语言表述)

semaphore = record

value: integer

queue: ^PCB

end

其中PCB是进程控制块,是操作系统为每个进程建立的数据结构。

s.value>=0时,s.queue为空;

s.value<0时,s.value的绝对值为s.queue中等待进程的个数;

特性

编辑

抽象的来讲,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程/进程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。当一个线程调用Wait操作时,它要么得到资源然后将信号量减一,要么一直等下去(指放入阻塞队列),直到信号量大于等于一时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为释放了由信号量守护的资源。

操作方式

编辑

对信号量有4种操作(include<semaphore>):

1. 初始化(initialize),也叫做建立(create) int sem_init(sem_t *sem, int pshared, unsigned int value)

2. 等信号(wait),也可叫做挂起(suspend)int sem_wait(sem_t *sem)

3. 给信号(signal)或发信号(post) int sem_post(sem_t *sem)

4.清理(destroy) int sem_destory(sem_t *sem)[1]

创建

编辑

同共享内存一样,系统中同样需要为信号量集定制一系列专有的操作函数(semget,semctl等)。系统命令ipcs可查看当前的系统IPC的状态,在命令后使用-s参数。使用函数semget可以创建或者获得一个信号量集ID,函数原型如下:

#include <sys/shm.h>

int semget( key_t key, int nsems, int flag)

函数中参数key用来变换成一个标识符,每一个IPC对象与一个key相对应。当新建一个共享内存段时,使用参数flag的相应权限位对ipc_perm结构中的mode域赋值,对相应信号量集的shmid_ds初始化的值如表1所示。

shmid_ds结构初始化值表

ipc_perm结构数据

初 值

ipc_perm结构数据

初 值

Sem_otime

0

Sem_nsems

Nsems

Sem_ctime

系统当前值

  

参数nsems是一个大于等于0的值,用于指明该信号量集中可用资源数(在创建一个信号量时)。当打开一个已存在的信号量集时该参数值为0。函数执行成功,则返回信号量集的标识符(一个大于等于0的整数),失败,则返回–1。函数semop用以操作一个信号量集,函数原型如下:

#include <sys/sem.h>

int semop( int semid, struct sembuf semoparray[], size_t nops )

函数中参数semid是一个通过semget函数返回的一个信号量标识符,参数nops标明了参数semoparray所指向数组中的元素个数。参数semoparray是一个struct sembuf结构类型的数组指针,结构sembuf来说明所要执行的操作,其定义如下:

struct sembuf{

unsigned short sem_num

short sem_op

short sem_flg

}

在sembuf结构中,sem_num是相对应的信号量集中的某一个资源,所以其值是一个从0到相应的信号量集的资源总数(ipc_perm.sem_nsems)之间的整数。sem_op指明所要执行的操作,sem_flg说明函数semop的行为。sem_op的值是一个整数,如表2所示,列出了详细sem_op的值及所对应的操作。

sem_op值详解

Sem_op

操 作

正数

释放相应的资源数,将sem_op的值加到信号量的值上

0

进程阻塞直到信号量的相应值为0,当信号量已经为0,函数立即返回。如果信号量的值不为0,则依据sem_flg的IPC_NOWAIT位决定函数动作。sem_flg指定IPC_NOWAIT,则semop函数出错返回EAGAIN。sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生。信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;此信号量被删除(只有超级用户或创建用户进程拥有此权限),函数smeop出错返回EIDRM;进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR

负数

请求sem_op的绝对值的资源。如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。当相应的资源数不能满足请求时,这个操作与sem_flg有关。sem_flg指定IPC_NOWAIT,则semop函数出错返回EAGAIN。sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:当相应的资源数可以满足请求,该信号的值减去sem_op的绝对值。成功返回;此信号量被删除(只有超级用户或创建用户进程拥有此权限),函数smeop出错返回EIDRM:进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR

基本流程

编辑

下面实例演示了关于信号量操作的基本流程。程序中使用semget函数创建一个信号量集,并使用semop函数在这个信号集上执行了一次资源释放操作。并在shell中使用命令查看系统IPC的状态。

(1)在vi编辑器中编辑该程序。

程序清单14-10 create_sem.c 使用semget函数创建一个信号量

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

#include <stdio.h>

#include <stdlib.h>

int main( void )

{

int sem_id

int nsems = 1

int flags = 0666

struct sembuf buf

sem_id = semget(IPC_PRIVATE, nsems, flags)/*创建一个新的信号量集*/

if ( sem_id <0 ){

perror( "semget ")

exit (1 )

}

/*输出相应的信号量集标识符*/

printf ( "successfully created a semaphore : %d\n", sem_id )

buf.sem_num = 0/*定义一个信号量操作*/

buf.sem_op = 1/*执行释放资源操作*/

buf.sem_flg = IPC_NOWAIT/*定义semop函数的行为*/

if ( (semop( sem_id, &buf, nsems) ) <0) { /*执行操作*/

perror ( "semop")

exit (1 )

}

system ( "ipcs -s " )/*查看系统IPC状态*/

exit ( 0 )

}

(2)在vmware中编译该程序如下:

gcc -o a.o testc_semaphore.c

(3)在shell中运行该程序如下:

./a3.o

successfully created a semaphore : 0

------ Semaphore Arrays --------

key semid owner perms nsems

0x00000000 0 zcr 666 1

在上面程序中,用semget函数创建了一个信号量集,定义信号量集的资源数为1,接下来使用semop函数进行资源释放操作。在程序的最后使用shell命令ipcs来查看系统IPC的状态。

%注意:命令ipcs参数-s标识查看系统IPC的信号量集状态。

希望能帮到你,满意望采纳哦。

sem_init:初始化信号量sem_t,初始化的时候可以指定信号量的初始值,以及是否可以在多进程间共享。

sem_wait:一直阻塞等待直到信号量>0。

sem_timedwait:阻塞等待若干时间直到信号量>0。

sem_post:使信号量加1。

sem_destroy:释放信号量。和sem_init对应。 答案补充 关于各函数的具体参数请用man查看,如man sem_init可查看该函数的帮助

信号量强调的是线程(或进程)间的同步:“信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都 在sem_wait的时候,就阻塞在那里)。当信号量为单值信号量是,也可以完成一个资源的互斥访问。

有名信号量:可以用于不同进程间或多线程间的互斥与同步

创建打开有名信号量

sem_t *sem_open(const char *name, int oflag)

sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value)

成功返回信号量指针;失败返回SEM_FAILED,设置errnoname是文件路径名,但不能写成/tmp/a.sem这样的形式,因为在linux下,sem都是在/dev/shm目录下,可写成"/mysem"或"mysem",创建出来的文件都 是"/dev/shm/sem.mysem",mode设置为0666,value设置为信号量的初始值.所需信号灯等已存在条件下指定O_CREAT|O_EXCL却是个错误。

关闭信号量,进程终止时,会自动调用它

int sem_close(sem_t *sem)

成功返回0;失败返回-1,设置errno

删除信号量,立即删除信号量名字,当其他进程都关闭它时,销毁它

int sem_unlink(const char *name)

等待信号量,测试信号量的值,如果其值小于或等于0,那么就等待(阻塞);一旦其值变为大于0就将它减1,并返回

int sem_wait(sem_t *sem)

int sem_trywait(sem_t *sem)

成功返回0;失败返回-1,设置errno

当信号量的值为0时,sem_trywait立即返回,设置errno为EAGAIN。如果被某个信号中断,sem_wait会过早地返回,设置errno为EINTR

发出信号量,给它的值加1,然后唤醒正在等待该信号量的进程或线程

int sem_post(sem_t *sem)

成功返回0;失败返回-1,不会改变它的值,设置errno,该函数是异步信号安全的,可以在信号处理程序里调用它无名信号量,用于进程体内各线程间的互斥和同步,使用如下API(无名信号量,基于内存的信号量)

(1)、sem_init

功能:用于创建一个信号量,并初始化信号量的值。

头文件:

函数原型: int sem_init (sem_t* sem, int pshared, unsigned int value)

函数传入值: sem:信号量。pshared:决定信号量能否在几个进程间共享。由于目前LINUX还没有实现进程间共享信息量,所以这个值只能取0。

(2)其他函数。

int sem_wait (sem_t* sem)

int sem_trywait (sem_t* sem)

int sem_post (sem_t* sem)

int sem_getvalue (sem_t* sem)

int sem_destroy (sem_t* sem)

功能:sem_wait和sem_trywait相当于P操作,它们都能将信号量的值减一,两者的区别在于若信号量的值小于零时,sem_wait将会阻塞进程,而sem_trywait则会立即返回。sem_post相当于V操作,它将信号量的值加一,同时发出唤醒的信号给等待的进程(或线程)。

sem_getvalue 得到信号量的值。

sem_destroy 摧毁信号量。

如果某个基于内存的信号灯是在不同进程间同步的,该信号灯必须存放在共享内存区中,这要只要该共享内存区存在,该信号灯就存在。

互斥锁(又名互斥量)强调的是资源的访问互斥:互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的”

也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。

在linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化:

对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER, 或者调用pthread_mutex_init.

对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy.

原型:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric attr)

int pthread_mutex_destroy(pthread_mutex_t *mutex)

头文件:

返回值: 成功则返回0, 出错则返回错误编号.

说明: 如果使用默认的属性初始化互斥量, 只需把attr设为NULL. 其他值在以后讲解.

首先说一下加锁函数:

头文件:

int pthread_mutex_lock(pthread_mutex_t *mutex)

int pthread_mutex_trylock(pthread_mutex_t *mutex)

返回值: 成功则返回0, 出错则返回错误编号.

说 明: 具体说一下trylock函数, 这个函数是非阻塞调用模式, 也就是说, 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限如果互斥量 被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态.

再说一下解所函数:

头文件:

原型: int pthread_mutex_unlock(pthread_mutex_t *mutex)

返回值: 成功则返回0, 出错则返回错误编号.

条件变量常与互斥锁同时使用,达到线程同步的目的:条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。在发 送信号时,如果没有线程 等待在该条件变量上,那么信号将丢失;而信号量有计数值,每次信号量post操作都会被记录

1. 互斥锁必须是谁上锁就由谁来解锁,而信号量的wait和post操作不必由同一个线程执行。

2. 互斥锁要么被锁住,要么被解开,和二值信号量类似

3. sem_post是各种同步技巧中,唯一一个能在信号处理程序中安全调用的函数

4. 互斥锁是为上锁而优化的;条件变量是为等待而优化的; 信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性

5. 互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。

6. 信号量有计数值,每次信号量post操作都会被记录,而条件变量在发送信号时,如果没有线程在等待该条件变量,那么信号将丢失。

读写锁

读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。

读写锁可以由三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写

锁。

在读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥 有这个锁。当读写锁在读状态下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。

读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当他以写模式锁住时,它是以独占模式锁住的。

初始化和销毁:

#include

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr)

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)

成功则返回0, 出错则返回错误编号.

同互斥量以上, 在释放读写锁占用的内存之前, 需要先通过thread_rwlock_destroy对读写锁进行清理工作, 释放由init分配的资源.

读和写:

#include

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)

成功则返回0, 出错则返回错误编号.

这3个函数分别实现获取读锁, 获取写锁和释放锁的操作. 获取锁的两个函数是阻塞操作, 同样, 非阻塞的函数为:

#include

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)

成功则返回0, 出错则返回错误编号.

非阻塞的获取锁操作, 如果可以获取则返回0, 否则返回错误的EBUSY.

虽然读写锁提高了并行性,但是就速度而言并不比互斥量快.

可能这也是即使有读写锁存在还会使用互斥量的原因,因为他在速度方面略胜一筹。这就需要我们在写程序的时候综合考虑速度和并行性并找到一个折中。

比如: 假设使用互斥量需要0.5秒,使用读写锁需要0.8秒。在类似学生管理系统这类软件中,可能百分之九十的时间都是查询操作,那么假如现在突然来个个20个请求,如果使用的是互斥量,那么最后的那个查询请求被满足需要10后。这样,估计没人能受得了。而使用读写锁,应为 读锁能够多次获得。所以所有的20个请求,每个请求都能在1秒左右得到满足。

也就是说,在一些写操作比较多或是本身需要同步的地方并不多的程序中我们应该使用互斥量,而在读操作远大于写操作的一些程序中我们应该使用读写锁来进行同步

条件变量(condition)

条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其它线程在获得互斥量之前不会察觉到这种改变,因此必须锁定互斥量以后才能计算条件。

条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件

变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

1. 初始化:

条件变量采用的数据类型是pthread_cond_t, 在使用之前必须要进行初始化, 这包括两种方式:

静态: 可以把常量PTHREAD_COND_INITIALIZER给静态分配的条件变量.

动态: pthread_cond_init函数, 是释放动态条件变量的内存空间之前, 要用pthread_cond_destroy对其进行清理.

#include

int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr)

int pthread_cond_destroy(pthread_cond_t *cond)

成功则返回0, 出错则返回错误编号.

注意:条件变量占用的空间并未被释放。

当pthread_cond_init的attr参数为NULL时, 会创建一个默认属性的条件变量非默认情况以后讨论.

2. 等待条件:

#include

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restric mutex)

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout)

成功则返回0, 出错则返回错误编号.

这两个函数分别是阻塞等待和超时等待.

等待条件函数等待条件变为真, 传递给pthread_cond_wait的互斥量对条件进行保护, 调用者把锁住的互斥量传递给函数. 函数把调用线程放到等待条件的线程列表上, 然后对互斥量解锁, 这两个操作是原子的. 这样 便关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道, 这样线程就不会错过条件的任何变化.

当pthread_cond_wait返回时, 互斥量再次被锁住.

pthread_cond_wait函数的返回并不意味着条件的值一定发生了变化,必须重新检查条件的值。

pthread_cond_wait函数返回时,相应的互斥锁将被当前线程锁定,即使是函数出错返回。

阻塞在条件变量上的线程被唤醒以后,直到pthread_cond_wait()函数返回之前条件的值都有可能发生变化。所以函数返回以后,在锁定相应的互斥锁之前,必须重新测试条 件值。最好的测试方法是循环调用pthread_cond_wait函数,并把满足条件的表达式置为循环的终止条件。如:

pthread_mutex_lock()

while (condition_is_false)

pthread_cond_wait()

pthread_mutex_unlock()

阻塞在同一个条件变量上的不同线程被释放的次序是不一定的。

注意:pthread_cond_wait()函数是退出点,如果在调用这个函数时,已有一个挂起的退出请求,且线程允许退出,这个线程将被终止并开始执行善后处理函数,而这时和条 件变量相关的互斥锁仍将处在锁定状态。

pthread_cond_timedwait函数到了一定的时间,即使条件未发生也会解除阻塞。这个时间由参数abstime指定。函数返回时,相应的互斥锁往往是锁定的,即使是函数出错返回。

注意:pthread_cond_timedwait函数也是退出点。

超时时间参数是指一天中的某个时刻。使用举例:

pthread_timestruc_t to

to.tv_sec = time(NULL) + TIMEOUT

to.tv_nsec = 0

超时返回的错误码是ETIMEDOUT。

3. 通知条件:

#include

int pthread_cond_signal(pthread_cond_t *cond)

int pthread_cond_broadcast(pthread_cond_t *cond)

成功则返回0, 出错则返回错误编号.

这两个函数用于通知线程条件已经满足. 调用这两个函数, 也称向线程或条件发送信号. 必须注意, 一定要在改变条件状态以后再给线程发送信号.


欢迎分享,转载请注明来源:夏雨云

原文地址:https://www.xiayuyun.com/zonghe/287159.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-23
下一篇2023-04-23

发表评论

登录后才能评论

评论列表(0条)

    保存