Linux内核机制—semaphore

一、信号量相关结构

1. 信号量描述结构体

struct semaphore {
    /* 保护信号量的spinlock锁 */
    raw_spinlock_t        lock;
    /*
     * 对于二值信号量,为1表示没有任务在临界区,为0表示只有1个任
     * 务在临界区,没有任务等待    在改信号量上,为-n表示有n个任务等
     * 待在该信号量上
     */
    unsigned int        count;
    /*
     * 获取不到信号量的任务对应的semaphore_waiter结构会串联在这个
     * 链表上,先入先出。
     */
    struct list_head    wait_list;
};

2. 信号量等待任务描述结构体

struct semaphore_waiter {
    /* 通过此成员挂在semaphore结构的wait_list链表上 */
    struct list_head list;
    /* 等待信号量的任务 */
    struct task_struct *task;
    /*
     * 被释放信号量的行为唤醒的情况下为真,其它比如
     * 信号、超时而导致的唤醒为假。
     */
    bool up;
};

 

二、信号量相关函数

1. 初始化函数

//定义并初始化一个二值信号量
DEFINE_SEMAPHORE(sem1);
//初始化一个二值或非二值的信号量
void sema_init(struct semaphore *sem, int val);

2. 获取信号量函数

//获取信号量,若是获取不到进入 TASK_UNINTERRUPTIBLE 状态
void down(struct semaphore *sem);
//同 down(),区别是若获取不到进入 TASK_INTERRUPTIBLE 状态,支持被信号唤醒
int __must_check down_interruptible(struct semaphore *sem);
//同 down(),区别是若获取不到进入 TASK_KILLABLE(TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)状态,表示只可被 SIGKILL 信号唤醒
int __must_check down_killable(struct semaphore *sem);
//尝试获取信号量,不会阻塞,返回1成功,0失败
int __must_check down_trylock(struct semaphore *sem);
//同 down(),区别是若获取不到不会一直等待,而是有一个超时时间,到期后自动唤醒
int __must_check down_timeout(struct semaphore *sem, long jiffies);

3. 释放信号量函数

void up(struct semaphore *sem);

 

三、获取信号量函数代码走读

我们基于 down() 函数进行解读:

/*
 * down - 获取信号量
 *
 * 获取信号量函数,若此信号量不允许更多任务获取它时,此函数会让任务进入休眠状态
 * 直到有人释放信号量。
 *
 * 此函数已经被弃用了,请使用 down_interruptible() 或 down_killable() 替代。
 */
void down(struct semaphore *sem) //kernel/locking/semaphore.c
{
    unsigned long flags;

    raw_spin_lock_irqsave(&sem->lock, flags);
    /* 或信号量值大于0,直接将去1然后返回,信号量值小于等于0时才会阻塞 */
    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() 获取不到信号量会进入D状态,而且无限超时。
     *
     * down() 的其它衍生函数都会调用到 __down_common() 只是传参不同。
     */
    __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

__down_common() 是所有down的衍生函数都要调用的一个公共函数,只是传参不同。

/*
 * __down 传参:(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT) 还可以指定超时时间。
 */
static inline int __sched __down_common(struct semaphore *sem, long state, long timeout)
{
    struct semaphore_waiter waiter;

    /*
     * 将当前任务放在链表末尾部,up初始化为假(唤醒后,被释放信号量的行
     * 为唤醒的情况下为真,被信号、等待超时而导致的唤醒为假)
     */
    list_add_tail(&waiter.list, &sem->wait_list);
    waiter.task = current;
    waiter.up = false;

    for (;;) {
        /*
         * down()的三个衍生函数响应不同:
         *     down() 不起作用,D状态不支持信号唤醒。
         *    down_interruptible() 这里可能被信号唤醒。
         *    down_killable() 这里只可能被SIGKILL信号唤醒。
         */
        if (signal_pending_state(state, current))
            goto interrupted;
        /* 定时时间到期,只有 down_timeout() 可能从这里退出 */
        if (unlikely(timeout <= 0))
            goto timed_out;
        /* 将当前进程的状态设置为非RUNNING状态,准备休眠 */
        __set_current_state(state);
        raw_spin_unlock_irq(&sem->lock);
        /* 触发切换,当前进程让出CPU */
        timeout = schedule_timeout(timeout);
        raw_spin_lock_irq(&sem->lock);
        /* 若是被释放信号量的行为唤醒的,返回0,见 up() 的讲解 */
        if (waiter.up)
            return 0;
    }

 timed_out:
     /* 对于 down_timeout() 等待超时后,将当前任务从链表上删除,返回-ETIME */
    list_del(&waiter.list);
    return -ETIME;

 interrupted:
     /* 对于down_interruptible()和down_killable()被信号唤醒返回-EINTR */
    list_del(&waiter.list);
    return -EINTR;
}

检测信号的 signal_pending_state():

static inline int signal_pending_state(long state, struct task_struct *p)
{
    /* 若state传参 TASK_INTERRUPTIBLE, 这里直接就退出了 */
    if (!(state & (TASK_INTERRUPTIBLE | TASK_WAKEKILL)))
        return 0;
    if (!signal_pending(p))
        return 0;

    /*对于 TASK_KILLABLE(TASK_WAKEKILL | TASK_UNINTERRUPTIBLE),只能被SIGKILL唤醒*/
    return (state & TASK_INTERRUPTIBLE) || __fatal_signal_pending(p);
}

触发进程切换的 schedule_timeout() 函数:

/*
 * schedule_timeout - 睡眠直到超时
 * @timeout: 超时值,单位 jiffies
 *
 * 使当前任务休眠,直到 @timeout jiffies 过去。函数行为取决于当前任务状态(另
 * 见 set_current_state() 描述):
 *
 * %TASK_RUNNING - 调度程序被调用,但任务根本不休眠。发生这种情况是因为 sched_submit_work()
 * 对处于 %TASK_RUNNING 状态的任务不执行任何操作。
 *
 * %TASK_UNINTERRUPTIBLE - 保证至少在 @timeout jiffies 之后才返回,除非当前任务被显式唤醒
 * (例如通过wake_up_process())。
 *
 * %TASK_INTERRUPTIBLE - 如果信号被传递到当前任务或当前任务被显式唤醒,可能会提前返回。
 *
 * 当此函数返回时,当前任务状态保证为 %TASK_RUNNING。
 *
 * @timeout 若被指定为 %MAX_SCHEDULE_TIMEOUT,将直接切走而不受超时限制。在这种情况下,
 * 返回值将是 %MAX_SCHEDULE_TIMEOUT。
 *
 * 当定时器超时返回 0,否则将返回剩余的 jiffies 时间。在所有情况下,返回值都保证为非负数。
 */
signed long __sched schedule_timeout(signed long timeout) //kernel/time/timer.c
{
    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;
        }
    }

    /* 定时器到期时间指定为当前jiffies值加上指定的到期时间 */
    expire = timeout + jiffies;

    /* 指定为当前将被阻塞的任务 */
    timer.task = current;
    timer_setup_on_stack(&timer.timer, process_timeout, 0);
    /* 启动定时器 */
    __mod_timer(&timer.timer, expire, MOD_TIMER_NOTPENDING);
    /* 将当前任务切走 */
    schedule();
    /* 当前任务被唤醒后继续执行 */
    del_singleshot_timer_sync(&timer.timer);

    /* Remove the timer from the object tracker */
    destroy_timer_on_stack(&timer.timer);

    /* 检查超时时间是否到期,到期返回0否则返回剩余的jiffies时间 */
    timeout = expire - jiffies;

 out:
    return timeout < 0 ? 0 : timeout;
}
EXPORT_SYMBOL(schedule_timeout);

/* 超时唤醒函数 */
static void process_timeout(struct timer_list *t) //kernel/time/timer.c
{
    struct process_timer *timeout = from_timer(timeout, t, timer);

    wake_up_process(timeout->task);
}

 

四、释放信号量函数代码走读

static noinline void __sched __up(struct semaphore *sem) //kernel/locking/semaphore.c
{
    /* 从链表首取待唤醒的任务进行唤醒 */
    struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list);
    /* 先从wait链表上取下来 */
    list_del(&waiter->list);
    /* 被up唤醒的任务up成员为true */
    waiter->up = true;
    /* 执行唤醒选核流程 */
    wake_up_process(waiter->task);
}

 

五、信号量使用场景

1. 二值信号量做成的锁,没有像mutex那样的乐观自旋,因此适合于保护较长的临界区。

2. 如果被保护的共享资源有多份,并不只是互斥访问的,那非常适合使用信号量。


六、总结

1. 信号量分为二值信号量和非二值信号量。前者可以当做锁来使用,主要用于保护临界区较大执行时间较长的代码段。后者可以用于多份资源的生产消费同步。

2. 可以指定获取不到信号量时任务的状态,Sleep、D、killable状态。还可以指定获取不到信号量时的等待超时时间,超时后自动退出等待。

 

posted on 2022-05-22 20:10  Hello-World3  阅读(836)  评论(0编辑  收藏  举报

导航