信号量

kernel 专栏收录该内容
13 篇文章 2 订阅

版权声明:本文为博主原创文章,未经博主允许不得转载。
https://blog.csdn.net/huangweiqing80/article/details/83038154

Linux中的常用信号量是锁的另一种实现机制,Linux中提供了两种信号量,一种用于内核程序中,一种用于应用程序中。这里讲解的是内核中的信号量

一、信号量概述

和自旋锁一样,信号量也是保护临界资源的一种有用方法。信号量只有当得到信号量时,进程或者线程才能够进入临界区,执行临界代码(down等函数后面的代码块)。

信号量与自旋锁的最大不同点在于,当一个进程试图去获取一个已经锁定的信号量时,该进程不会像自旋锁一样在自旋忙等待,而是会将自身加入一个等待队列中去睡眠,直到其他进程释放信号量后,处于等待队列中的进程才会被唤醒。当进程唤醒之后,就立刻重新从睡眠的地方开始执行,又一次试图获得信号量,当获得信号量后,程序继续执行。

所以,从信号量的原理上来说,没有获得信号量的函数可能睡眠。这就要求只有能够睡眠的进程才能够使用信号量,不能睡眠的进程不能使用信号量。例如中断处理程序中,由于中断需要立刻完成,所以不能睡眠,也就是说在中断处理程序中不能使用信号量。

1 定义信号量

下面代码定义名为sem的信号量。
struct semaphore sem;

struct semaohore结构体在内核中定义如下:
/include/linux/semaphore.h

struct 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);
  • 2
    点赞
  • 2
    评论
  • 28
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 创作都市 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值