信号量

简介: 信号量

相对于自旋锁,信号量的最大特点是允许调用它的线程进入睡眠状态·这意味着试图获得某一信号的进程会导致对处理器拥有权的丧失,也即出现进程的切换。

信号量的定义

struct semaphore {
raw_spinlock_t        lock;//lock是个自旋锁变量,用于实现对信号量的另一个成员count的原子操作。
unsigned int        count;//无符号整型变量count用于表示通过该信号量允许进入临界区的执行路径的个数。
struct list_head    wait_list;//wait_list用于管理所有在该信号量上睡眠的进程,无法获得该信号量的进程将进入睡眠状态。
};

如果驱动程序中定义了一个struct semaphore型的信号量变量,需要注意的是不要直接对该变量的成员进行赋值,而应该使用sema_init函数来初始化该信号量。sema_init函数定义如下:

static inline void sema_init(struct semaphore *sem, int val)
{
    static struct lock_class_key __key;
    *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
    lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

初始化主要通过__SEMAPHORE_INITIALIZER宏完成:

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

所以 sema_init(struct semaphore *sem, int val)调用会把信号量sem的lock值设定为解锁状态,count值设定为函数的调用参数val,同时初始化wait_list链表头。

DOWN操作

信号量上的主要操作是DOWN和UP,在Linux内核中对信号量的DOWN操作有:

  • void down(struct semaphore *sem) 获取信号量,不建议使用此函数,因为是 UNINTERRUPTABLE 的睡眠。
  • int down_interruptible(struct semaphore *sem) 可被中断地获取信号量,如果睡眠被信号中断,返回错误-EINTR。
  • int down_killable (struct semaphore *sem) 可被杀死地获取信号量。如果睡眠被致命信号中断,返回错误-EINTR。
  • int down_trylock(struct semaphore *sem) 尝试原子地获取信号量,如果成功获取,返回0,不能获取,返回1。
  • int down_timeout(struct semaphore *sem, long jiffies) 在指定的时间jiffies内获取信号量,若超时未获取,返回错误-ETIME。

上面这些函数中,驱动程序使用最频繁的是down_interruptible函数,本节将重点讨论该函数,之后再对其他入操作的功能作一概述性的描述。

int down_interruptible(struct semaphore *sem)
{
    unsigned long flags;
    int result = 0;
    raw_spin_lock_irqsave(&sem->lock, flags);//保证对sem->count操作的原子性防止多个进程对sem->count同时操作
    //可能引起混乱
    if (likely(sem->count > 0))
        sem->count--;
    else
        result = __down_interruptible(sem);
    raw_spin_unlock_irqrestore(&sem->lock, flags);
    return result;
}

函数首先通过spin_lock_irqsave的调用来保证对sem->count操作的原子性防止多个进程对sem->count同时操作

可能引起混乱。如果代码成功进入临界区,则判断sem->count是否大于0:如果count大于0,表明当前进程可以获得信号量,就将count值减I,然后退出:如果count不大于0,表明当前进程无法获得该信号量,此时调用down_interruptible,由后者完成一个进程无法获得信号量时的操作,在内部调用__down_common(struct semaphore *sem,long state,longt timeout),调用时的参数state=TASK_INTERRUPTIBLE,timeout=LONG_MAX所以当一个进程无法获得信号量时,最终调用的函数为__down_common:

__down_interruptible->__down_common

static inline int __sched __down_common(struct semaphore *sem, long state,
                                long timeout)
{
    struct task_struct *task = current;
    /*通过对一个struct semaphore_waiter变量waiter的使用,把当前进程放到信号量sem的成员变量wait所管理的队列中*/
    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))//把当前进程的状态设置为TASK_INTERRUPTIBLE
            goto interrupted;
        if (unlikely(timeout <= 0))
            goto timed_out;
        __set_task_state(task, state);
        raw_spin_unlock_irq(&sem->lock);
        timeout = schedule_timeout(timeout);//使当前进程进入睡眠状态,函数将停留在schedule_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;
}

函数的功能是,首先通过对一个struct semaphore_waiter变量waiter的使用,把当前进程放到信号量sem的成员变量wait所管理的队列中,接着在一个for循环中把当前进程的状态设置为TASK_INTERRUPTIBLE,再调用schedule_timeout使当前进程进入睡眠状态,函数将停留在schedule_timeout调用上,直到再次被调度执行。当该进程再一次被调度执行时,schedule_timeout开始返回,接下来根据进程被再次调度的原因进行处理:如果waiter.up不为0,说明进程在信号量sem的wati_list队列中被该信号量的UP操作所唤醒,进程可以获得信号量,返回0。如果进程是因为被用户空间发送的信号所中断或者是超时引起的唤醒,则返回相应的错误代码。因此对面down_interruptible的调用总是应该坚持检查其返回值,以确定函数是已经获得了信号量还是因为操作被中断因而需要特别处理,通常驱动程序对返回的非0值要做的工作是返回-ERESTARTSYS,比如下面的代码段:

//定义一个信号量
struct semaphore demosem;
sema_init(&demosem,2);
if(down_interruptiable(&demosem));
    return -ERESTARTSYS;

然而对down_interruptible的调用最常见的可能还是返回0表明调用者获得了信号量。为了让讨论具体化,下面以一个例子来说明,假设一个信号量sem的count=2,说明允许有两个进程进入临界区,假设有进程A、B、C、D和E先后调用down_interruptible来获得信号量,那么进程A和B将得到信号量进入临界区,C、D和E将睡眠在sem的wait_list中,此时的情形如图2所示:

在接下来的UP操作中还会用到这里的例子,来讨论进程A和B结束临界区中的操作返回时执行UP操作对wait_list中进程C、D和E的影响。

在讨论完驱动程序最常使用的down_interruptible函数之后,再回过头来看看其他几种DOWN操作:

  • void down(struct semaphore *sem);
    与down_interruptible相比,down函数是不可中断的,这意味着调用它的进程如果无法获得信号量,将一直处于睡眠状态直到有别的进程释放了该信号量。从用户空间的角度,如果应用程序阻塞在了驱动程序的down函数中,将无法通过一些强制措施比如按Ctrl+D组合键等来结束该进程。因此,除非必要,否则驱动程序中应该避免使用down函数。
  • int down_killable(struct semaphore *sem);
    睡眠的进程可以因收到一些致命性信号(fatal signal)被唤醒而导致获取信号量的操作被中断,在驱动程序中极少使用。
  • int down_trylock(struct semaphore *sem);
    进程试图获得信号量,但若无法获得信号量则直接返回1而不进入睡眠状态,返回0意味着函数的调用者己经获得了信号量。
  • int down_timeout(struct semaphore *sem, long jiffies);
    函数在无法获得信号量的情况下将进入睡眠状态,但是处于这种睡眠状态有时间限制,如果在jiffies指明的时间到期时函数依然无法获得信号量,则将返回一错误码-ETIME,在到期前进程的睡眠状态为TASK_UNINTERRUPTIBLE。成功获得信号量的函数返回0。

UP操作

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

如果信号量的wait_list队列为空,则表明没有其他进程正在等待该信号量,那么只要把sem的count加1即可。如果wait_list队列不为空,则说明有其他进程正睡眠在wait_list上等待该信号量,此时调用__up(sem)来唤醒进程:

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

下面在图2的基础上讨论此处的操作。__up函数首先用list_first_entry取得sem->wait_list链表上的第一个waiter节点C,然后将其从sem->wait_list链表中删除,waiter->up=1,最后调用wake_up_process来唤醒waiter C上的进程C。这样进程C将从之前down_interruptible。

调用中的timeout=schedule_timeout(timeout)处醒来,waiter->up=1,down_interruptible返回0,进程c获得信号量,进程D和E继续等待直到有进程释放信号量或者被用户空间中断掉。即使不是信号量的拥有者,也可以调用up函数来释放一个信号量,这点与下节介绍的mutex是不同的。

在Linux系统中,信号量的一个常见的用途是实现互斥机制,这种情况下信号量的count值为1,也就是任意时刻只允许一个进程进入临界区。为此Linux内核源码提供了一个宏DECLARE_MUTEX,专门用于这种用途的信号量定义和初始化:

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

该宏定义了一个count=1的信号量变量name,并初始化了相关成员。所以接下来就可以使用信号量的DOWN和UP操作来实现互斥,比如下面的这个用DECLARE_MUTEX定义的信号量来实现互斥的代码段:

//先用DECLARE_MUTEX定义一个全局的信号量demo_sem
DECLARE_MUTEX(demo_sem);
//函数demo_write里面使用demo_sem作互斥用
int demo_write{
    //打算进入临界区,调用down_interruptible获得信号量
    if(down_interruptible(&down_sem)){
        return -ERESTARTSYS            
    }
    //成功获取信号量进入临界区
    .....
    //离开临界区,调用up释放信号量
    up(&demo_sem)
}


目录
相关文章
|
3月前
|
数据采集 数据库 索引
新闻网站的数据采集与更新思路
该方案设计了一个跨站点的增量更新引擎,用于高效采集央视新闻、中国新闻网和环球网等多源新闻数据。通过代理IP和内容哈希签名技术,实现新闻的新增与更新检测,大幅降低冗余抓取和带宽消耗。实验表明,该方法在多源新闻采集中具备高效性和实用性,可拓展为行业级舆情雷达系统,支持事件追踪与趋势分析。
189 2
新闻网站的数据采集与更新思路
|
NoSQL 程序员 C语言
探秘Segmentation Fault错误:程序猿的噩梦
探秘Segmentation Fault错误:程序猿的噩梦
2783 0
|
资源调度 前端开发 数据可视化
构建高效的数据可视化仪表板:D3.js与React的融合之道
【10月更文挑战第25天】在数据驱动的时代,将复杂的数据集转换为直观、互动式的可视化表示已成为一项至关重要的技能。本文深入探讨了如何结合D3.js的强大可视化功能和React框架的响应式特性来构建高效、动态的数据可视化仪表板。文章首先介绍了D3.js和React的基础知识,然后通过一个实际的项目案例,详细阐述了如何将两者结合使用,并提供了实用的代码示例。无论你是数据科学家、前端开发者还是可视化爱好者,这篇文章都将为你提供宝贵的洞见和实用技能。
373 5
|
Kubernetes 安全 网络协议
操作系统的未来之路:探索微内核架构与分布式系统
随着计算需求的不断演变和技术的快速进步,传统的宏内核操作系统正面临性能和安全性的挑战。本文旨在探讨操作系统的未来发展道路,特别是微内核架构和分布式系统在提升性能和安全性方面的潜力。通过分析微内核设计的优势、分布式系统的特性以及两者结合的可能性,文章旨在为读者提供对操作系统未来发展趋势的深入理解。
531 27
|
存储 NoSQL API
【小小思考】Redis实现去重任务队列
【2月更文挑战第1天】思考一下如何用Redis实现去重的任务队列,主要有List 、List + Set/Hash/Bloom Filter、ZSet、Lua和开源库等方式。
613 1
|
SQL XML 存储
Idea Mybatis插件:提高CRUD效率
将mybatis xml转成真实SQL语句、参数mock、SQL规范检查、SQL索引检查、SQL运行、SQL压测及Mybatis SQL语句扫描
1240 1
Idea Mybatis插件:提高CRUD效率
|
存储 缓存 监控
【Redis源码】bloomfilter布隆过滤器
【Redis源码】bloomfilter布隆过滤器
251 1
|
人工智能 缓存 NoSQL
在Github中77k星的AutoGPT安装及配置教程,能实现独立思考自动化
十分重磅!GPT3.5都还没玩明白,傍着GPT4的AutoGPT就又要乱杀了,特斯拉前 AI 总监、刚刚回归 OpenAI 的 Andrej Karpathy也提到“AutoGPT”将成为提示工程的下一个前沿,网上很多人只提突破性,不提局限性,twitter的原话是prompt engineering领域,因此其他领域还是坐观新测
2357 0
在Github中77k星的AutoGPT安装及配置教程,能实现独立思考自动化
|
Linux Windows
安装Linux系统对硬件的要求
很多初学者在安装 Linux 系统时,都对自己的电脑配置存在质疑,担心其是否能够满足安装 Linux 的要求。本节就从 CPU、内存、硬盘、显卡等这些方面,详细介绍一下安装 Linux 系统的最低配置。 基于硬件的快速发展以及操作系统核心功能的增加,势必将淘汰掉一批老旧的电脑,它们已经没有能力负荷新的操作系统了。举个最直观的例子,奔腾-III 之前的硬件配置可能已经无法再搭载如今的 Linux 发行版了,而且这部分电脑很可能因为电子零件老化等因素,导致其在运行过程出现无法解释的宕机情况。 不过,Linux 系统所需的硬件配置也不需要太高端,大体来说,生产期限在 5 年以内的电脑,基本上就可
409 0
|
C++
【C/C++】uin8_t uint16_t uint32_t相互转换
uin8_t uint16_t uint32_t 数据类型相互转换
1268 0