带您进入内核开发的大门 | 信号量

配套的代码可以从本号的github下载,https://github.com/shuningzhang/linux_kernel

前面我们介绍了Linux内核中的自旋锁的使用和具体的实现。接下来我们介绍一下在Linux内核中使用比较广泛的另外一种锁机制---信号量。信号量又称为信号灯(semaphore),其与自旋锁不同的地方是它可以引起调用者休眠,也就是信号量本质上是一种睡眠锁。如果有一个任务试图获得一个不可用(已经被占用)的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放后),处于等待队列中的那个任务将被唤醒,并将获得该信号量。
信号量一个有用的特性是它可以同时允许任意数量的锁持有者,而自旋锁和互斥锁在一个时刻最多允许一个任务持有它。信号量同时允许的持有者数量可以在声明信号量时指定。这个值称为使用者数量(usage count)或简单的叫做数量(count)。通常情况下,信号量和自旋锁一样,在一个时刻仅允许有一个锁持有者。这时计数等于1,这样的信号量被称为二值信号量或者称为互斥信号量。另一方面,初始化时也可以把数量设置为大于1的非0值。这种情况,信号量被称为计数信号量(counting semaphore),它允许在同一时刻至多有count个锁持有者。

基本接口

本节我们介绍一下信号量的基本接口,信号量的用法与自旋锁非常相似(分为初始化、加锁和解锁三部分)。主要需要注意的地方是信号量会引起休眠,因此不要在不能休眠的地方使用信号量。
信号量初始化

void sema_init (struct semaphore *sem, int val);
void init_MUTEX (struct semaphore *sem); //将sem的值置为1,表示资源空闲
void init_MUTEX_LOCKED (struct semaphore *sem); //将sem的值置为0,表示资源忙

信号量加锁

void down(struct semaphore * sem); // 可引起睡眠
int down_interruptible(struct semaphore * sem); 
// down_interruptible能被信号打断
int down_trylock(struct semaphore * sem); 
// 非阻塞函数,不会睡眠。无法锁定资源则马上返回

信号量解锁

void up(struct semaphore * sem);

应用示例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mm.h>

#include <linux/in.h>
#include <linux/inet.h>
#include <linux/socket.h>
#include <net/sock.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/semaphore.h>

#define BUF_SIZE 1024

struct task_data {
        int progress;
};

struct task_struct *main_task;
struct task_struct *client_task;

/* 在这里定义信号量及要保护的数据,这里只是为了说明用法
 * 实际生产中不会这么用,因为对于一个简单数据,可以通过
 * 原子变量实现。 */
struct semaphore lock;
struct task_data thread_data;

static inline void sleep(unsigned sec)
{
        __set_current_state(TASK_INTERRUPTIBLE);
        schedule_timeout(sec * HZ);
}

/* 这个是2个线程中的一个,只是为了说明自旋锁的用法。  */
static int multhread_server(void *data)
{
        struct task_data *cur = (struct task_data *)data;
        while (!kthread_should_stop()) {
                /* 通过信号量进行临界区保护,在信号量保护的
                 * 临界区中是可以休眠的。 */
                down(&lock);
                printk(KERN_NOTICE "server thread run begin %d\n", cur->progress);
                sleep(1);

                /* 临界区,也就是被保护的数据的操作 */
                cur->progress ++;           
                printk(KERN_NOTICE "server thread run after %d\n", cur->progress);
                /* 信号量解锁  */
                up(&lock);
        }

        return 0;
}

static int multhread_client(void *data)
{
        struct task_data *cur = (struct task_data *)data;
        while(!kthread_should_stop()) {
                down(&lock);
                printk(KERN_NOTICE "client thread run begin %d\n", cur->progress);
                sleep(1);
                cur->progress += 2;
                printk(KERN_NOTICE "client thread run after %d\n", cur->progress);
                up(&lock);

        }

        return 0;
}

static int multhread_init(void)
{
        ssize_t ret = 0;

        thread_data.progress = 0;
        sema_init(&lock, 1);

        printk("Hello, socket \n");
        main_task = kthread_run(multhread_server,
                                  &thread_data,
                                  "multhread_server");
        if (IS_ERR(main_task)) {
                ret = PTR_ERR(main_task);
                goto failed;
        }

        client_task = kthread_run(multhread_client,
                                  &thread_data,
                                  "multhread_client");
        if (IS_ERR(client_task)) {
                ret = PTR_ERR(client_task);
                goto client_failed;
        }

        return ret;
client_failed:
        kthread_stop(main_task);
failed:
        return ret;
}

static void multhread_exit(void)
{
        printk("Bye!\n");
        kthread_stop(main_task);
        kthread_stop(client_task);

}

module_init(multhread_init);
module_exit(multhread_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("SunnyZhang<shuningzhang@126.com>");

基本原理

本节介绍一下信号量的具体实现。照例,我们先从信号量的数据结构走起。下面是信号量的数据结构,可以看出其由自旋锁、计数和一个链表头组成。

struct semaphore {
        raw_spinlock_t          lock;
        unsigned int            count;  //标示信号量是否可用,也即是否处于加锁状态
        struct list_head        wait_list;
};

这里自旋锁 lock用于保护数据的访问,而链表 wait_list则用于记录有那些线程在等待该信号量。
如果之前看过本号前面的文章,结合这里关于结构体的介绍,估计能够猜出来信号量的实现方式。其大概原理是当有线程企图加锁的时候调用加锁函数(down),如果count大于0则表示可以加锁,并加锁成功。如果小于等于0则表示不可加锁,此时线程将被放入wait_list队列,然后线程被调度出CPU(休眠)。
加锁流程分析
如下代码是加锁的接口,可以看到入参就是上面定义的信号量的结构体指针。在进行实际操作之前需要使用自旋锁进行保护。

void down(struct semaphore *sem)
{
        unsigned long flags;
        /* 自旋锁保护临界区 */
        raw_spin_lock_irqsave(&sem->lock, flags);
        /* 是否可以直接加锁, 如果大于0则可以加锁成功 */
        if (likely(sem->count > 0))
                sem->count--;
        else
                __down(sem); /* 加锁等待流程, 调用实际执行函数 */
        raw_spin_unlock_irqrestore(&sem->lock, flags);
}

函数的调用关系是down->__down->__down_common,实际执行函数是__down_common。下面是函数的源代码,具体请看函数内的注释。

static inline int __sched __down_common(struct semaphore *sem, long state,
                                                                long timeout)
{
        struct task_struct *task = current;
        struct semaphore_waiter waiter;
        /* 记录受阻的线程,并且初始化等待任务。 */
        list_add_tail(&waiter.list, &sem->wait_list);
        waiter.task = task;
        waiter.up = false;

        /* 引起睡眠的逻辑就在这里,下面这段代码将本
         * 线程调度出去,并且在加锁的线程没有解锁的
         * 情况下回持续循环休眠。  */
        for (;;) {
                if (signal_pending_state(state, task))
                        goto interrupted;
                if (unlikely(timeout <= 0))
                        goto timed_out;
                __set_task_state(task, state);
                raw_spin_unlock_irq(&sem->lock);
                timeout = schedule_timeout(timeout);
                raw_spin_lock_irq(&sem->lock);
                if (waiter.up)
                        return 0;
        }

 timed_out:
        list_del(&waiter.list);
        return -ETIME;

 interrupted:
        list_del(&waiter.list);
        return -EINTR;
}

信号量解锁
信号量解锁的流程比较简单,如下是其实现代码,主函数比较简单,这里不做解释了。

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);
}

需要真正解锁的情况下将执行本函数。可以看到本函数的实现也并不复杂,在这里首先从队列中取出第一个等待的任务,然后调用线程唤醒函数进行唤醒。

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);
}

能力增强

除了上面的基本的接口外,还有一些辅助功能的接口。比如down_trylock可以探测一下是否可以加锁。而down_interruptible则设置当前线程的状态为TASK_INTERRUPTIBLE状态。

extern int __must_check down_interruptible(struct semaphore *sem);
extern int __must_check down_killable(struct semaphore *sem);
extern int __must_check down_trylock(struct semaphore *sem);
extern int __must_check down_timeout(struct semaphore *sem, long jiffies);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345