在Linux内核代码中,信号量被定义成semaphore结构体(代码位于include/linux/semaphore.h中):

struct semaphore {raw_spinlock_t     lock;unsigned int       count;struct list_head  wait_list;
};

这个结构体由三部分组成:

  • lock:用来保护这个信号量结构体的自旋锁。
  • count:信号量用来保护的共享资源的数量。如果该值大于0,表示资源是空闲的,调用者可以立即获得这个信号量,进而访问被保护的共享资源;如果该值等于0,表示资源是忙的,但是没有别的进程在等待这个信号量被释放;如果这个值小于0,表示资源是忙的,且有至少一个别的进程正在等待该信号量被释放。
  • wait_list:在这个信号量上等待的所有进程链表。

所有插入wait_list等待链表的进程节点由semaphore_waiter结构体表示:

struct semaphore_waiter {struct list_head list;struct task_struct *task;bool up;
};

这个结构体也由三部分组成:

  • list:插入链表的节点。
  • task:指向等待进程的task_struct结构体。
  • up:真表示该等待进程是因为信号量释放被唤醒的,否则都是假。

初始化信号量

信号量在使用之前要对其进行初始化,一般有两种方法:

DEFINE_SEMAPHORE(sem1);struct semaphore sem2;
sema_init(&lock2);

第一种方法是用宏直接定义并且初始化一个顺序锁变量:

#define __SEMAPHORE_INITIALIZER(name, n)             \
{                                   \.lock      = __RAW_SPIN_LOCK_UNLOCKED((name).lock),   \.count     = n,                       \.wait_list = LIST_HEAD_INIT((name).wait_list),        \
}#define DEFINE_SEMAPHORE(name) \struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)

所以,直接通过宏定义初始化就是定义了一个semaphore结构体变量,将其内部的自旋锁lock初始化为未加锁,将表示共享资源数量的count变量初始化为1,将等待进程链表初始化为空链表。count的值为1,表示这个信号量只能同时由一个进程持有,也就是说被保护的共享资源只能被互斥的访问,这种特殊的信号量也称作二值(Binary)信号量。但是,通常大家用的都是这种二值信号量,用法如下:

down(&sem);
/* 临界区 */
up(&sem);

第二种方法是自己定义一个semaphore结构体变量,然后调用sema_init函数将其初始化:

static inline void sema_init(struct semaphore *sem, int val)
{......*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);......
}

最终也是通过__SEMAPHORE_INITIALIZER对信号量进行的初始化。但是,通过这种方式初始化可以指定count的值,而不像前者默认设置成1。

获取信号量

要想获取一个信号量,通常是通过调用down函数:

void down(struct semaphore *sem)
{unsigned long flags;/* 获得自旋锁并关中断 */raw_spin_lock_irqsave(&sem->lock, flags);/* 如果信号量的count大于0 */if (likely(sem->count > 0))/* 直接获得该信号量并将count值递减 */sem->count--;else__down(sem);/* 释放自旋锁并开中断 */raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(down);

如果信号量的count值大于0,则直接将其递减然后返回,调用的进程直接获得该信号量。如果小于或等于0,则接着调用__down函数:

static noinline void __sched __down(struct semaphore *sem)
{__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

__down函数直接调用了__down_common函数。传入的第二个参数是TASK_UNINTERRUPTIBLE,表示;传入的第三个参数是MAX_SCHEDULE_TIMEOUT,表示会一直等待该信号量,直到获得为止,没有到期时间。

除了最基本的down函数外,还可以通过下面几个函数来获得信号量:

  • down_interruptible:基本功能和down相同,区别是如果无法获得信号量,会将该进程置于TASK_INTERRUPTIBLE状态。因此,在进程睡眠时可以通过信号(Signal)将其唤醒。
  • down_killable:如果无法获得信号量,会将该进程置于TASK_KILLABLE状态。
  • down_timeout:不会一直等待该信号量,而是有一个到期时间,时间到了后即使没有获得信号量也会返回。等待的时候也和down函数一样,会将进程置于TASK_UNINTERRUPTIBLE状态。

所有获得信号量的方法最终都是调用了__down_common函数,只不过传入的第二个和第三个参数不一样。

static noinline int __sched __down_interruptible(struct semaphore *sem)
{return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}static noinline int __sched __down_killable(struct semaphore *sem)
{return __down_common(sem, TASK_KILLABLE, MAX_SCHEDULE_TIMEOUT);
}static noinline int __sched __down_timeout(struct semaphore *sem, long timeout)
{return __down_common(sem, TASK_UNINTERRUPTIBLE, timeout);
}

我们接着回来看__down_common函数的实现:

static inline int __sched __down_common(struct semaphore *sem, long state,long timeout)
{struct semaphore_waiter waiter;/* 将表示等待进程的节点插入信号量的等待列表中 */list_add_tail(&waiter.list, &sem->wait_list);waiter.task = current;waiter.up = false;for (;;) {/* 如果有待处理的信号则跳到interrupted标签处 */if (signal_pending_state(state, current))goto interrupted;/* 如果超时了则跳到timed_out标签处 */if (unlikely(timeout <= 0))goto timed_out;/* 设置当前进程的状态 */__set_current_state(state);/* 释放自旋锁 */raw_spin_unlock_irq(&sem->lock);/* 当前进程睡眠 */timeout = schedule_timeout(timeout);/* 再次获得自旋锁 */raw_spin_lock_irq(&sem->lock);/* 如果是因为自旋锁释放而被唤醒的则返回0 */if (waiter.up)return 0;}timed_out:list_del(&waiter.list);return -ETIME;interrupted:list_del(&waiter.list);return -EINTR;
}

该函数先将表示等待进程的节点插入信号量的等待列表中,该节点的task变量指向表示当前进程的task_struct结构体,up变量被初始化为否。然后会进入一个大的循环中,循环的退出条件有三个:

  1. 当前进程有待处理的信号。
  2. 当前进程等待信号量超时了。
  3. 当前进程被另一个释放信号量的进程唤醒。

signal_pending_state函数用来判断当前进程是否有待处理的信号(代码位于include/linux/sched/signal.h中):

static inline int signal_pending_state(long state, struct task_struct *p)
{if (!(state & (TASK_INTERRUPTIBLE | TASK_WAKEKILL)))return 0;if (!signal_pending(p))return 0;return (state & TASK_INTERRUPTIBLE) || __fatal_signal_pending(p);
}

所以,当前进程的状态必须包含TASK_INTERRUPTIBLE或TASK_WAKEKILL才行。也就是说,只有通过调用down_interruptible或down_killable函数获得信号量时,signal_pending_state函数才有可能返回真。

schedule_timeout用来将当前进程休眠,同时设置到期时间(代码位于kernel/time/timer.c中):

signed long __sched schedule_timeout(signed long timeout)
{struct process_timer timer;unsigned long expire;switch (timeout){case MAX_SCHEDULE_TIMEOUT:/* 没有到期时间间隔直接睡眠 */schedule();goto out;default:if (timeout < 0) {printk(KERN_ERR "schedule_timeout: wrong timeout ""value %lx\n", timeout);dump_stack();current->state = TASK_RUNNING;goto out;}}/* 计算到期时间 */expire = timeout + jiffies;timer.task = current;/* 设置定时器到期处理函数为process_timeout */timer_setup_on_stack(&timer.timer, process_timeout, 0);/* 设置定时器 */__mod_timer(&timer.timer, expire, 0);/* 休眠 */schedule();/* 删除定时器 */del_singleshot_timer_sync(&timer.timer);....../* 计算距离到期时间还剩多少时间 */timeout = expire - jiffies;out:return timeout < 0 ? 0 : timeout;
}
EXPORT_SYMBOL(schedule_timeout);

如果传入的到期时间间隔被设置成了MAX_SCHEDULE_TIMEOUT,表示当前进程在获得信号量时没有设置超时,因此直接调用schedule调度另一个进程执行,本进程进入休眠。如果设置了一个有效的到期时间间隔,那么在调用schedule之前必须要先添加一个定时器,让其在到期时间点被触发。但是,表示定时器本身的结构并不包含由哪个进程设置它的信息,所以还需要定义一个新的结构体process_timer:

struct process_timer {struct timer_list timer;struct task_struct *task;
};

注意,表示定时器的结构体timer_list是在process_timer结构体中的第一个变量。这样,如果我们有了一个指向表示定时器的timer_list结构体的指针,那么它其实也是指向process_timer结构体的。通过前面的分析可以看到,定时器的到期函数被设置成了process_timeout,它的参数就是一个指向timer_list结构体的指针:

static void process_timeout(struct timer_list *t)
{/* 从timer_list指针获得包含它的process_timer */struct process_timer *timeout = from_timer(timeout, t, timer);/* 唤醒到期进程 */wake_up_process(timeout->task);
}

就是通过指向定时器结构体timer_list的指针获得包含它的process_timer结构体指针,从而获得设置该定时器的进程,然后唤醒它。

释放信号量

要想释放一个信号量,只能通过调用up函数:

void up(struct semaphore *sem)
{unsigned long flags;/* 获得自旋锁并关中断 */raw_spin_lock_irqsave(&sem->lock, flags);/* 如果等待进程链表为空 */if (likely(list_empty(&sem->wait_list)))/* 直接将count递增 */sem->count++;else__up(sem);/* 释放自旋锁并开中断 */raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(up);

如果释放的时候发现等待进程链表是空的,表示当前没有被的进程在等这个信号量,直接递增count值就行了;如果不为空,表示有别的进程在等这个信号量被释放,那么当前进程需要接着调用__up函数试着将某个等待进程唤醒。

static noinline void __sched __up(struct semaphore *sem)
{/* 获得等待链表中的第一个节点 */struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,struct semaphore_waiter, list);/* 将该节点从等待链表中删除 */list_del(&waiter->list);waiter->up = true;/* 将该等待进程唤醒 */wake_up_process(waiter->task);
}

功能很简单,获得等待链表中的第一个节点,然后唤醒该节点表示的进程。需要把该节点的up字段置为真,让被唤醒的进程知道其是因为信号量被释放才被唤醒的。

使用场景

要使用信号量,一般要满足以下的一些使用场景或条件:

  • 信号量适合于保护较长的临界区。它不应该用来保护较短的临界区,因为竞争信号量时有可能使进程睡眠和切换,然后被再次唤醒,代价很高,这种场景下应该使用自旋锁。
  • 如果被保护的共享资源有多份,并不只是互斥访问的,那非常适合使用信号量。
  • 只有允许睡眠的场景下才能使用内核信号量,也就是说在中断处理程序和可延迟函数中都不能使用信号量。

Linux内核同步原语之信号量(Semaphore)相关推荐

  1. Linux内核同步机制之信号量与锁

    Linux内核同步控制方法有很多,信号量.锁.原子量.RCU等等,不同的实现方法应用于不同的环境来提高操作系统效率.首先,看看我们最熟悉的两种机制--信号量.锁. 一.信号量 首先还是看看内核中是怎么 ...

  2. linux 内核互斥体,Linux 内核同步(六):互斥体(mutex)

    互斥体 互斥体是一种睡眠锁,他是一种简单的睡眠锁,其行为和 count 为 1 的信号量类似.(关于信号量参考:Linux 内核同步(四):信号量 semaphore). 互斥体简洁高效,但是相比信号 ...

  3. Linux内核同步机制之(四):spin lock【转】

    转自:http://www.wowotech.net/kernel_synchronization/spinlock.html 一.前言 在linux kernel的实现中,经常会遇到这样的场景:共享 ...

  4. linux内核同步问题

    linux内核同步问题 Linux内核设计与实现 十.内核同步方法 [手把手教Linux驱动5-自旋锁.信号量.互斥体概述]() 基础概念: 并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观 ...

  5. Linux内核同步 - Read/Write spin lock

    一.为何会有rw spin lock? 在有了强大的spin lock之后,为何还会有rw spin lock呢?无他,仅仅是为了增加内核的并发,从而增加性能而已.spin lock严格的限制只有一个 ...

  6. linux 内核同步--理解原子操作、自旋锁、信号量(可睡眠)、读写锁、RCU锁、PER_CPU变量、内存屏障

    内核同步 内核中可能造成并发的原因: 中断–中断几乎可以在任何时刻异步发生,也就可以随时打断当前正在执行的代码. 软中断和tasklet–内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在 ...

  7. linux kernle 同步原语

    转载:同步原语 如何避免由于对共享数据的不安全访问导致的数据崩溃? 内核使用的各种同步技术: 技术 说明 适用范围 每CPU变量 在CPU之间复制数据结构 所有CPU 原子操作 对一个计数器原子地&q ...

  8. 内核同步机制-读写信号量(rw_semaphore)

    四.读写信号量(rw_semaphore) 读/写信号量适于在读多写少的情况下使用.如果一个任务需要读和写操作时,它将被看作写者,在不需要写操作的情况下可降级为读者.任意多个读者可同时拥有一个读/写信 ...

  9. Linux内核同步:RCU

    linux内核 RCU机制详解 简介 RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用.RCU主要针对的数据对象是链表,目的是提高遍历读取数据的 ...

  10. linux 内核 同步机制

    原子操作   原子操作是由编译器来保证的,保证一个线程对数据的操作不会被其他线程打断.    自旋锁 原子操作只能用于临界区只有一个变量的情况,实际应用中,临界区的情况要复杂的多.对于复杂的临界区,L ...

最新文章

  1. 分享:Dlib 17.49 发布,跨平台 C++ 通用库
  2. JavaScript语言基础2
  3. Java Lambda表达式入门
  4. 区块链BaaS云服务(17)纸贵科技Z-BaaS“数据治理”
  5. 下列属于计算机人工智能应用领域的是多选题,每天五道选择题(10)
  6. java泛型不是计算运行时的数据类型
  7. WebAssembly:随风潜入夜
  8. 内核程序员的职位面试技巧
  9. 算法提高 高精度乘法(java)
  10. python + opencv: kalman 跟踪
  11. 请问投稿中要求上传的author_文章投稿如何做到时间管理?(二)
  12. ViewData 和TempData ,Session用法
  13. 联想计算机主机编号,联想如何查找主机编号
  14. ssm read time out的原因_为什么得肝病的男人越来越多?爱喝酒不是原因,或跟老婆有关系!...
  15. 【综述阅读】Ad hoc网络路由相关的几篇综述
  16. java 知网 语义 相似度,基于知网的词汇语义相似度计算-hownet!.doc
  17. Autodesk 3ds Max2020安装说明
  18. IDEA 强大文件对比功能
  19. 又是DDoS,你玩的游戏被黑客攻击停服了吗?
  20. 2022 年 GIS 就业状况

热门文章

  1. 在我心目中的霸气海贼王——路飞 不一样的路飞
  2. html文件 加壳,CDHtmlDialog加壳HTML5页面跳转错误解决(原)
  3. [转载]一位也许是真正的hack说的话
  4. ubantu软件安装
  5. html动画如何延迟,css3animation延迟
  6. 计算机锁屏如何取消密码,Win10锁屏密码怎么取消?Win10系统取消锁屏密码的方法教程...
  7. win10怎么取消开机密码
  8. springboot-1-搭建一个springboot项目
  9. 81章 老子1章到_《道德经》81章全文,建议全文背诵,终身体悟
  10. 33暴力破解(MD5撞击)