前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【源码分析】——信号量

【源码分析】——信号量

作者头像
董哥聊技术
发布2023-09-28 16:51:24
2780
发布2023-09-28 16:51:24
举报
文章被收录于专栏:嵌入式艺术嵌入式艺术

【深入理解Linux内核锁】六、信号量

除了原子操作,中断屏蔽,自旋锁以及自旋锁的衍生锁之外,在Linux内核中还存在着一些其他同步互斥的手段。

下面我们来理解一下信号量,互斥体,完成量机制。

1、信号量介绍

信号量(Semaphore)是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或者n。信号量与操作系统中的经典概念PV操作对应。

P(Produce):

  • 将信号量S的值减1,即S=S-1;
  • 如果S≥0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。

V(Vaporize):

  • 将信号量S的值加1,即S=S+1;
  • 如果S>0,唤醒队列中等待信号量的进程。

信号量核心思想

信号量的操作,更适合去解决生产者和消费者的问题,比如:我做出来一个饼,你才能吃一个饼;如果我没做出来,你就先释放CPU去忙其他的事情。

2、信号量的API

代码语言:javascript
复制
struct semaphore sem;  // 定义信号量

void sema_init(struct semaphore *sem, int val);  // 初始化信号量,并设置信号量sem的值为val。

void down(struct semaphore * sem);     // 获得信号量sem,它会导致睡眠,因此不能在中断上下文中使用。
int down_interruptible(struct semaphore * sem);  // 该函数功能与down类似,不同之处为,因为down()进入睡眠状态的进程不能被信号打断,但因为down_interruptible()进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非0 
int down_trylock(struct semaphore * sem);   // 尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回非0值。它不会导致调用者睡眠,可以在中断上下文中使用。

void up(struct semaphore * sem);  // 释放信号量,唤醒等待者。

由于新的Linux内核倾向于直接使用mutex作为互斥手段,信号量用作互斥不再被推荐使用。 信号量也可以用于同步,一个进程A执行down()等待信号量,另外一个进程B执行up()释放信号量,这样进程A就同步地等待了进程B。

3、API实现

3.1 semaphore

代码语言:javascript
复制
struct semaphore {
    raw_spinlock_t  lock;
    unsigned int  count;
    struct list_head wait_list;
};

结构体名称semaphore

文件位置include/linux/semaphore.h

主要作用:用于定义一个信号量。

  • raw_spinlock_t:信号量结构体也使用了自旋锁,避免互斥。
  • count:表示信号量的计数器,表示资源的数量
  • struct list_head wait_list: 这是一个链表头,用于管理等待信号量的线程。当信号量的 count 等于0,即没有可用资源时,等待信号量的线程会被加入到这个链表中,以便在资源可用时进行唤醒。

3.2 sema_init

代码语言:javascript
复制
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);
}

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

#define LIST_HEAD_INIT(name) { &(name), &(name) }

函数名称sema_init

文件位置include/linux/semaphore.h

主要作用:初始化信号量,并设置信号量sem的值为val

实现流程

  1. 使用__SEMAPHORE_INITIALIZER宏定义来初始化信号量
    • 使用__RAW_SPIN_LOCK_UNLOCKED宏定义来初始化自旋锁
    • 直接将val赋值给信号量的值count
    • 使用LIST_HEAD_INIT来初始化一个链表
代码语言:javascript
复制
#define LIST_HEAD_INIT(name) { &(name), &(name) }

该宏接受一个参数 name,并返回一个结构体对象。这个对象有两个成员 nextprev,分别指向 name 本身。

这样,当我们使用该宏来初始化链表头节点时,会得到一个拥有 nextprev 成员的结构体对象。其中 nextprev 成员都指向该结构体对象本身。

这种初始化方式可以用于创建一个空的双向链表,因为在初始状态下,链表头节点的 nextprev 指针都指向自身,表示链表为空。

3.3 down

代码语言:javascript
复制
/**
 * down - acquire the semaphore
 * @sem: the semaphore to be acquired
 *
 * Acquires the semaphore.  If no more tasks are allowed to acquire the
 * semaphore, calling this function will put the task to sleep until the
 * semaphore is released.
 *
 * Use of this function is deprecated, please use down_interruptible() or
 * down_killable() instead.
 */
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);
}
EXPORT_SYMBOL(down);

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

/*
 * Because this function is inlined, the 'state' parameter will be
 * constant, and thus optimised away by the compiler.  Likewise the
 * 'timeout' parameter for the cases without timeouts.
 */
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 (;;) {
        if (signal_pending_state(state, current))
            goto interrupted;
        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);
        if (waiter.up)
            return 0;
    }

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

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

函数名称down

文件位置kernel/locking/semaphore.c

主要作用:获取信号量,如果信号量的值大于0,则消耗一个;如果不存在,则让线程进入休眠状态并等待信号量被释放。

函数调用流程

代码语言:javascript
复制
down(kernel/locking/semaphore.c)
    |--> raw_spin_lock_irqsave      //  获取锁,并保存中断信息
    ||-> sem->count--;              //  如果sem->count信号量存在,则消耗一个
     |-> __down                     //  如果sem->count信号量不存在,则进入休眠状态
        |--> __down_common
            |--> list_add_tail      //  将当前线程添加到信号量的等待链表中,表示当前线程正在等待信号量。
            |--> __set_current_state//  设置线程为休眠状态
            |--> raw_spin_unlock_irq//  释放自旋锁,让其他线程可用
            |--> schedule_timeout   //  让线程进入睡眠状态,等待信号量释放或超时。
            |--> raw_spin_lock_irq  //  重新获取自旋锁,继续执行后续操作
    |--> raw_spin_unlock_irqrestore //  释放锁,并恢复中断信息

实现流程

  1. 获取信号量时,先使用raw_spin_lock_irqsaveraw_spin_unlock_irqrestore将信号量的操作包裹起来,避免竞态发生。
  2. 然后对sem->count判断,如果信号量大于0,就消耗一个,否则的话,将当前线程设置为休眠态
  3. 调用__down_common接口,默认将当前线程设置为TASK_UNINTERRUPTIBLE中断不可打断状态,并设置最大超时时间MAX_SCHEDULE_TIMEOUT
  4. struct semaphore_waiter waiter:创建waiter结构体,表示当前线程的状态
  5. 调用__set_current_state接口,设置当前线程的状态信息,即TASK_UNINTERRUPTIBLE
  6. 调用schedule_timeout,让该线程让出CPU,进入休眠态,并且在前面加上raw_spin_unlock_irq保证其他线程可以正常使用信号量。
  7. 当线程时间片到时,获取CPU,并调用raw_spin_lock_irq,获取锁,来防止竞态发生。

3.4 up

代码语言:javascript
复制
/**
 * up - release the semaphore
 * @sem: the semaphore to release
 *
 * Release the semaphore.  Unlike mutexes, up() may be called from any
 * context and even by tasks which have never called down().
 */
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);
}
EXPORT_SYMBOL(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

文件位置kernel/locking/semaphore.c

主要作用:获取信号量,如果信号量的值大于0,则消耗一个;如果不存在,则让线程进入休眠状态并等待信号量被释放。

实现流程

相信分析完down后,up也变得很简单

  1. 释放信号量时,先使用raw_spin_lock_irqsaveraw_spin_unlock_irqrestore将信号量的操作包裹起来,避免竞态发生。
  2. 然后对sem->wait_list判断,如果其为空,说明没有等待的线程,直接将sem->count自增,如果有等待的线程,则唤醒下一个线程。
  3. 调用wake_up_process来唤醒线程

4、总结

信号量较为简单,一般常用来解决生产者和消费者的问题,其主要还是通过自旋锁来实现互斥的作用,通过链表来管理等待队列的线程信息,通过变量来代表资源的数量。

嵌入式艺术

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-28 11:30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 嵌入式艺术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 【深入理解Linux内核锁】六、信号量
    • 1、信号量介绍
      • 2、信号量的API
        • 3、API实现
          • 3.1 semaphore
          • 3.2 sema_init
          • 3.3 down
          • 3.4 up
        • 4、总结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档