
- 中文名称
- 信号量
- 作 用
- 两个或多个关键代码不被并发调用
- 别 名
- 信号灯
- 外文名称
- Semaphore
- 要 求
- 线程必须获取一个信号量
- 类 型
- 计算机、电子
-
信号量
2018-10-13 17:08:06Linux中的常用信号量是锁的另一种实现机制,Linux中提供了两种信号量,一种用于内核程序中,一种用于应用程序中。这里讲解的是内核中的信号量 一、信号量概述 和自旋锁一样,信号量也是保护临界资源的一种有用方法...版权声明:本文为博主原创文章,未经博主允许不得转载。
https://blog.csdn.net/huangweiqing80/article/details/83038154Linux中的常用信号量是锁的另一种实现机制,Linux中提供了两种信号量,一种用于内核程序中,一种用于应用程序中。这里讲解的是内核中的信号量
一、信号量概述
和自旋锁一样,信号量也是保护临界资源的一种有用方法。信号量只有当得到信号量时,进程或者线程才能够进入临界区,执行临界代码(down等函数后面的代码块)。
信号量与自旋锁的最大不同点在于,当一个进程试图去获取一个已经锁定的信号量时,该进程不会像自旋锁一样在自旋忙等待,而是会将自身加入一个等待队列中去睡眠,直到其他进程释放信号量后,处于等待队列中的进程才会被唤醒。当进程唤醒之后,就立刻重新从睡眠的地方开始执行,又一次试图获得信号量,当获得信号量后,程序继续执行。
所以,从信号量的原理上来说,没有获得信号量的函数可能睡眠。这就要求只有能够睡眠的进程才能够使用信号量,不能睡眠的进程不能使用信号量。例如中断处理程序中,由于中断需要立刻完成,所以不能睡眠,也就是说在中断处理程序中不能使用信号量。
1 定义信号量
下面代码定义名为sem的信号量。
struct semaphore sem;struct semaohore结构体在内核中定义如下:
/include/linux/semaphore.hstruct semaphore { raw_spinlock_t lock; /** * 如果count该值大于0,表示资源是空闲的。如果等于0,表示信号量是忙的,但是没有进程在等待这个资源。 * 如果count为负,表示资源忙,并且至少有一个进程在等待。 * 但是请注意,负值并不代表等待的进程数量。 */ unsigned int count; struct list_head wait_list; };
1.1 lock变量
lock变量是用来对count变量起保护作用的。当要改变count要改变时,及在down/up函数中应该会调用spin_lock/spin_unlock锁定lock锁和释放lock锁
1.2. count变量
count是信号量中一个非常重要的成员变量,这个变量的值决定了线程是否要进入休眠,并且决定了允许这个信号量的持有者数量
1.2.1 count值等于0,表示信号量被其他进程使用,现在不可以用这个信号量,但是wait_list等待队列中没有线程在等待信号量
1.2.2 count值小于0,表示至少有一个进程在wait_list队列中等待信号量被释放
1.2.3 count值大于0,表示这个信号量是空闲的,程序可以使用这个信号量信号量另一个重要特性是可以规定任意数量的锁持有者。允许的持有者数量可以在声明信号量时指定。这个值是count指定。最常见的count值是1,只允许有一个锁持有者,这种信号量也被成为二元信号量(因为只有两种状态:被持有和没有被持有)或者互斥信号量(因为强制互斥访问)。count值也可以被设定为一个比1大的值,这种情况下被称为计数信号量,计数信号量用于对特定代码进行限制,同一时刻最多只能有规定数量的任务进入临界区,计数信号量很少使用,互斥信号量用得最多。
如:当count值等于3时,说明允许三个进程持有这个信号量,即允许有三个进程同时运行,而自旋锁只能允许一个进程持有自旋锁。
1.3. wait_list变量
wait_list是一个等待队列的链表头,这个链表将所有等待该信号量的进程组成一个链表结构。在这个链表中,存放了正在睡眠的进程链表
2.使用信号量
下面我们来看一下如何来使用一个信号量
//创建一个信号量,并将其允许的持有者数量初始化为count struct semaphore mr_sem; sema_init(&mr_sem, count); //请求信号量 if (down_interruptible(&mr_sem)) { /* signal received, semaphore not acquired ... */ } /* 临界区 critical region ... */ //释放获得的信号量 up(&mr_sem);
2.1.定义合初始化信号量
定义信号量:
struct semaphore mr_sem;
一个信号量必须初始化才能被使用,下面是三种信号量的初始化方式
static inline void sema_init(struct semaphore *sem, int val) //初始化信号量,并设置sem中count的值为val static inline void init_MUTEX (struct semaphore *sem) //初始化semaphore将count字段初始化为1 static inline void init_MUTEX_LOCKED (struct semaphore *sem) //初始化semaphore将count字段初始化为0
2.2. 锁定(获得)信号量
2.2.1 void down(struct semaphore *sem);
该函数用于获取信号量sem,它会导致睡眠,因此不能在中断上下文使用。
kernel/semaphore.c:void down(struct semaphore *sem) { unsigned long flags; raw_spin_lock_irqsave(&sem->lock, flags); if (likely(sem->count > 0)) sem->count--; else __down(sem); raw_spin_unlock_irqrestore(&sem->lock, flags); }
这里重点看if (likely(sem->count > 0)),这句话表示当获取信号量成功时,就执行sem->count—; 即对信号量的值减1。else表示获取信号量失败,此时调用__down函数进入睡眠状态,并将此进程插入到等待队列尾部。
内核定义了信号量的等待队列结构体:
struct semaphore_waiter { struct list_head list; struct task_struct *task; int up; };
此结构体是一个双向循环链表。
2.2.2 int down_interruptible(struct semaphore *sem);
该函数功能与down()类似,不同之处是,down()在获取信号量失败进入睡眠状态时的进程是不能被打断的,而down_interruptible()在进入睡眠状态时的进程能被信号打断,信号也会导致函数返回。注意这里是信号而不是信号量,如Ctrl+ C等外部信号。下面我们也来看一看这个函数的源码:
在kernel/semaphore.c文件里:
int down_interruptible(struct semaphore *sem) { unsigned long flags; int result = 0; raw_spin_lock_irqsave(&sem->lock, flags); if (likely(sem->count > 0)) sem->count--; else result = __down_interruptible(sem); raw_spin_unlock_irqrestore(&sem->lock, flags); return result; }
这里我们可以看到,当获取信号量成功时,返回0,而获取信号量失败时,返回一个非0的值。在使用down_interruptible()函数获取信号量时,对返回值一般会进行检查,如果非0,通常立即返回-ERESTARTSYS。如:
if ( down_interruptible(&sem) ) return -ERESTARTSYS;
这里还有一个问题:在获取信号量失败后,为什么down不能被中断,而down_interruptible却可以被中断呢?我们从down和down_interruptible的源代码可以得知,在获取信号量失败后,down函数运行了__down函数,而down_interruptible函数运行了__down_interruptible。那么让我们来看一下这两个函数的源码:
在kernel/semaphore.c文件里:
static noinline void __sched __down(struct semaphore *sem) { __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); } static noinline int __sched __down_interruptible(struct semaphore *sem) { return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); }
在__down函数里,是把进程的状态设置为TASK_UNINTERRUPTIBLE ,即不可中断状态。
而在__down_interruptible里,是把进程的状态设置为TASK_INTERRUPTIBLE ,即可中断状态。这就解释了以上提出的问题。
2.3.释放信号量
void up(struct semaphore *sem);
该函数用于释放信号量sem,唤醒等待者。它的源代码如下:
void up(struct semaphore *sem) { unsigned long flags; raw_spin_lock_irqsave(&sem->lock, flags); if (likely(list_empty(&sem->wait_list))) sem->count++; else __up(sem); raw_spin_unlock_irqrestore(&sem->lock, flags); }
up函数首先判断等待队列是否为空,如果是空的话,就执行sem->count++;否则,执行__up() 函数,释放掉等待队列尾部的信号量。
二、信号量用于同步举例:
前面已经说过,如果信号量被初始化为0,那么又可以将这种信号量叫做互斥体。互斥体可以用来实现同步的功能。同步表示一个线程的执行需要依赖于另一个线程的执行,这样可以保证线程的执行先后顺序。
如上图所示,线程A执行到被保护代码A之前,一直处于睡眠状态。直到线程B执行完被保护代码B并调用up()函数后,才会执行被保护代码A。即执行单元A执行代码区域a之前,必须等待执行单元B执行完代码区域b后释放信号量给它。三、信号量的实例
#include <linux/init.h> #include <linux/module.h> #include <linux/sched.h> #include <linux/sem.h> struct semaphore sem1; struct semaphore sem2; int num[2][5] = { {0,2,4,6,8}, {1,3,5,7,9} }; int thread_one(void *p); int thread_two(void *p); int thread_one(void *p) { int *num = (int *)p; int i; for(i = 0; i < 5; i++){ down(&sem1); //获取信号量1 printk("%d ", num[i]); up(&sem2); //释放信号量2 } return 0; } int thread_two(void *p) { int *num = (int *)p; int i; for(i = 0; i < 5; i++){ down(&sem2); //获取信号量2 printk("%d ", num[i]); up(&sem1); //释放信号量1 } return 0; } static int lan_init(void) { printk("lan is coming\n"); init_MUTEX(&sem1); //初始化信号量1, 使信号量1最初可被获取 init_MUTEX_LOCKED(&sem2); //初始化信号量2,使信号量2只有被释放后才可被获取 kernel_thread(thread_one, num[0], CLONE_KERNEL); kernel_thread(thread_two, num[1], CLONE_KERNEL); return 0; } static void lan_exit(void) { printk("\nlan exit\n"); } module_init(lan_init); module_exit(lan_exit);
-
-
【系统架构设计师】第一章:操作系统(1.2.2) 信号量与pv操作
2020-12-26 01:02:521.2.2 信号量与pv操作 pv操作指的是两个:p操作和v操作。 有时候我们的进程在工作的时候,需要同时配合来干多件事情。比如,我们规定一个进程用来写入数据,另一个进程用来读取数据。 很显然,这连个进程是不能互相...本篇帖子继续上篇。有兴趣可以点击链接进行查看以前写过的文章。
【系统架构设计师】第一章:操作系统(1.2.2)
参考教材:
《系统架构设计师考试全程指导(第二版)》
《系统架构设计师教程》1.2.2 信号量与pv操作
pv操作指的是两个:p操作和v操作。
有时候我们的进程在工作的时候,需要同时配合来干多件事情。比如,我们规定一个进程用来写入数据,另一个进程用来读取数据。
很显然,这连个进程是不能互相干扰的,因此我们就需要提前对系统进行一下告知:使用这些资源的权力我先占用了,其他人不要进来。
系统得知以后,如果有其他进程要写入数据或者读取数据,就会被系统安排成等待态。
等我们的进程使用资源完成以后,在告知系统,其他进程可以使用这些资源。告知系统不要让其他进程操作这片内存的操作,就是p操作。
告知系统此进程已使用内存完毕,就是v操作。我们因此可以得出使用pv操作的目的:
为了解决不同进程的互斥(都需要共享独占性资源时)和同步(完成异步的两个进程的协作)问题,需要使用pv操作。想想看,如果是你,你会怎么用代码的方式解决呢?
我这里用的是c语言的方式。
首先,我们要有一个变量s。
在pv操作中被称为信号量,表现形式是一个整数s和一个队列。你可以姑且认为这是个整形变量。
当s>=0,代表某个资源的可用数;
当s<0,其绝对值代表阻塞队列中等待该资源的进程数。其次是p操作。
根据我们开始所说,p操作就是告知系统这片资源已经被使用,因此只要在本来的资源数上减1即可。不过我们要考虑一种情况,就是当可用资源为0或负数的时候,那么就要让程序进入等待。
所以我们可以得出:if((s=s-1)<0){ printf("执行p操作的进程进入等待"); }
最后是v操作。
这个同理,减一换成加一即可。if((s=s+1)<=0){ printf("从阻塞队列中唤醒一个其他处于阻塞态的进程"); }
互斥实例
说了这么多,先来个实例,比如,对两个进程进行互斥控制。
我这里给了两个程序。
这两个程序是同时运行的,并且变量s是共享的,且初值为1。为了省事,这里我就简写了,省去了main之类的格式。我对p和v进行了一些小的修改,这样更符合c语言的标准。
p:
if((s=s-1)<0){ printf("执行p操作的进程进入等待"); return 0; } else{ return 1; }
v:
if((s=s+1)<=0){ printf("从阻塞队列中唤醒一个其他处于阻塞态的进程"); return 0; } else{ return 1; }
进程A
while(1){ if(p(s)){ printf("操作一下"); } v(s) }
进程B
while(1){ if(p(s)){ printf("操作二下"); } v(s) }
仔细分析。
我们这里先说明一个前提,即使我们的程序同时运行,由于某些特殊的原因,是不可能真正的同时启动的,稍微会有一些误差,比如某个进程会晚0.00000000001秒。我们这里就假设A进程是比较快的那个。第一次循环时,A先执行了p,然后s自减,输出了“操作一下”。
此可我们的B启动,也执行了p,注意此可A还没有执行V,也就是说s还没有自增。由于我们的s已经从1自减到了0,再次执行,s=-1,if不成立,什么都不输出。
当A执行完成v操作以后,我们的B已经到了第二轮,此可if成立,所以输出了“操作二下”。
以此类推,你会发现,A和B永远都时一个执行,另一个就跳过。这样,我们就使用pv操作对进程的互斥。
同步实例
同步也是类似的操作。直接上例程。不过这次比较特殊,需要用到两个信号量,s1=1和s2=1。
p:if((s=s-1)<0){ printf("执行p操作的进程进入等待"); return 0; } else{ return 1; }
v:
if((s=s+1)<=0){ printf("从阻塞队列中唤醒一个其他处于阻塞态的进程"); return 0; } else{ return 1; }
进程A
while(1){ if(p(s1)){ printf("操作一下"); } v(s2) }
进程B
while(1){ if(p(s2)){ printf("操作二下"); } v(s1) }
仔细分析。
这个其实A和B无论是否同时运行都行。
假设我们的A先运行,执行第一遍的时候没什么问题,正常输出“操作一下”。
但是当执行第二遍的时候,你会发现,A不会输出了,因为我们的s1从开始循环第二遍的0变成了-1,所以此刻if时不成立的。
而当我们运行B的时候,B执行v(s2),最终将p增到1,A才会继续走。
这样,我们就完成了进程之间的同步。
本篇内容主要介绍的是pv操作的一些细节。
有兴趣的可以加我qq:1392969921
下一节已更新。
【系统架构设计师】第一章:操作系统(1.2.3)死锁问题 -
公用信号量 私用信号量
2019-10-15 22:31:23公用信号量 私用信号量 公用信号量 用来实现进程间的互斥,初值为1,允许它所联系的一组进程对它执行P/V操作。 私用信号量 用来实现进程间的同步,初值为0或者某个正整数,仅允许拥有它的进程对其执行P/V操作。 ...公用信号量 私用信号量
公用信号量 用来实现进程间的互斥,初值为1,允许它所联系的一组进程对它执行P/V操作。
私用信号量 用来实现进程间的同步,初值为0或者某个正整数,仅允许拥有它的进程对其执行P/V操作。
-
2.3.4 操作系统之信号量机制(整型信号量、记录型信号量P、V)
2020-03-18 20:11:50记录型信号量(1)举一个生动形象的例子了解记录型信号量(2)梳理一下记录型信号量的知识点(P、V) 0.思维导图 1.为什么引入信号量机制? 为了更好的解决进程互斥与同步的问题 2.什么是信号量机制? 3.整型...