linux 内核 ---信号量(semaphore)

发布时间 2023-10-18 23:36:27作者: 流水灯

信号量使用说明

(1)定义信号量

struct semaphore sem;

(2)初始化信号量

void sema_init(struct semaphore *sem, int val);

该函数初始化信号量,并设置信号量sem的值为val。

(3)获得信号量

extern void down(struct semaphore *sem);
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);

void down(struct semaphore * sem);

该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文中使用。

int down_interruptible(struct semaphore * sem);

该函数功能与down类似,不同之处为,因为down()进入睡眠状态的进程不能被信号打断,但因为down_interruptible()进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非0。在使用down_interruptible()获取信号量时,对返回值一般会进行检查,如果非0,通常立即返回- ERESTARTSYS

int down_trylock(struct semaphore * sem);

该函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回非0值。它不会导致调用者睡眠,可以在中断上下文中使用。

(4)释放信号量

void up(struct semaphore * sem);

该函数释放信号量sem,唤醒等待者。

信号量原理

struct semaphore {
    raw_spinlock_t        lock; // 对于信号量的操作,比如获取信号量时的减一操作,都需要做临界区保护(critial section),通过自旋锁实现临界区
    unsigned int        count; 
    struct list_head    wait_list; // 当 count 为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);
}

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为0,把当前线程记录到信号量的链表里,释放自旋锁,启调度


void down(struct semaphore *sem)
{
    unsigned long flags;

    might_sleep();
    raw_spin_lock_irqsave(&sem->lock, flags);
    if (likely(sem->count > 0))
        sem->count--;
    else
        __down(sem);
    raw_spin_unlock_irqrestore(&sem->lock, flags);
}
static noinline void __sched __down(struct semaphore *sem)
{
    __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

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

无论是释放信号量,还是获取信号量,临界区的实现都是通过自旋锁。所以在多核场景下,一个核如果获取了自旋锁操作信号量参数,其他核尝试获取锁会自旋在那。在临界区内如果需要睡眠,会先释放自旋锁,从而允许其他线程能够执行。