linux驱动开发(9)- 信号量

相对于自旋锁,信号量的最大特点是允许调用它的线程进入睡眠(休眠)状态。这意味着试图获得某一信号量的进程会导致对处理器拥有权的丧失,也即出现进程的切换。我们记忆的时候可以把信号量想象成信号灯(红绿灯),获取不到信号量(红灯)的时候,需要休眠(排队等待)。休眠实际上也就是进入CPU调度的一个队列里面。

信号量的定义与初始化

信号量的定义如下:

c 复制代码
<include/linux/semaphore.h>
struct semaphore {
    spinlock_t       lock;
    unsigned int      count;
    struct list_head  wait_list;
};

其中,lock是个自旋锁变量,用于实现对信号量的另一个成员count的原子操作。无符号整型变量count用于表示通过该信号量允许进入临界区的执行路径的个数。wait_list用于管理所有在该信号量上睡眠的进程,无法获得该信号量的进程将进入睡眠状态。如果驱动程序中定义了一个struct semaphore型的信号量变量,需要注意的是不要直接对该变量的成员进行赋值,而应该使用sema_init函数来初始化该信号量。sema_init函数定义如下:

c 复制代码
<include/linux/semaphore.h>
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宏完成:

c 复制代码
#define__SEMAPHORE_INITIALIZER(name,n)                \
{                                      \
    .lock     =__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操作有:

c 复制代码
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_killable(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
int down_timeout(struct semaphore *sem, long jiffies);

上面这些函数中,驱动程序使用最频繁的是down_interruptible函数,我们将重点讨论该函数,之后再对其他DOWN操作的功能作一概述性的描述。down_interruptible函数定义如下:

c 复制代码
<kernel/semaphore.c>
int down_interruptible(struct semaphore *sem)
{
    unsigned long flags;
    int result = 0;
    spin_lock_irqsave(&sem->lock, flags);
    if (likely(sem->count > 0))
    sem->count--;
    else
    result = __down_interruptible(sem);
    spin_unlock_irqrestore(&sem->lock, flags);
    return result;
}

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

c 复制代码
<kernel/semaphore.c>
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 = 0;
    for (;;) {
        if (signal_pending_state(state, task))
              goto interrupted;
        if (timeout <= 0)
              goto timed_out;
        __set_task_state(task, state);
        spin_unlock_irq(&sem->lock);
        timeout = schedule_timeout(timeout);
        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_list所管理的队列中,接着在一个for循环中把当前进程的状态 设置为TASK_INTERRUPTIBLE,再调用schedule_timeout使当前进程进入睡眠状态,函数将停留在schedule_timeout调用上,直到再次被调度执行。当该进程再一次被调度执行时,schedule_timeout开始返回,接下来根据进程被再次调度的原因进行处理:如果waiter.up不为0,说明进程在信号量sem的wati_list队列中被该信号量的UP操作所唤醒,进程可以获得信号量,返回0。如果进程是因为被用户空间发送的信号所中断或者是超时引起的唤醒,则返回相应的错误代码。因此对down_interruptible的调用总是应该坚持检查返回值,以确定函数是已经获得了信号量还是因为操作被中断因而需要特别处理,通常驱动程序对返回的非0值要做的工作是返回-ERESTARTSYS,比如下面的代码段:

c 复制代码
//定义一个信号量
struct semaphore demosem;
sema_init(&demosem,2);
if(down_interruptible(&demosem))
    return -ERESTARTSYS;

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

来看看其他几种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操作

Linux下只有一个UP函数:

c 复制代码
<kernel/semaphore.c>
void up(struct semaphore *sem)
{
    unsigned long flags;
    spin_lock_irqsave(&sem->lock, flags);
    if (likely(list_empty(&sem->wait_list)))
        sem->count++;
    else
        __up(sem);
    spin_unlock_irqrestore(&sem->lock, flags);
}

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

c 复制代码
<kernel/semaphore.c>
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 = 1;
    wake_up_process(waiter->task);
}

下面在之前图片的基础上讨论此处的操作。__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,专门用于这种用途的信号量定义和初始化:

c 复制代码
<include/linux/semaphore.h>
#define DECLARE_MUTEX(name) \
    struct semaphore  name=__SEMAPHORE_INITIALIZER(name,1)

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

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

读取者与写入者信号量rwsem

如同spinlock一样,如果对操作共享资源的访问类型进行细分,在普通信号量的基础上可以实现读取者与写入者信号量。这里的概念完全等同于读取者与写入者自旋锁。读取者与写入者信号量的定义如下:

c 复制代码
<include/linux/rwsem-spinlock.h>
struct rw_semaphore {
    __s32         activity;
    spinlock_t      wait_lock;
    struct list_head  wait_list;
};

其中activity的确切含义是● activity=0,表明当前在该信号量上没有任何活动的读取者或者是写入者。● activity=-1,表明当前在该信号量上有一个活动的写入者。● activity为正值n,表明当前信号量上有n个活动的读取者。静态定义一个rwsem变量同时用DECLARE_RWSEM宏进行初始化:

c 复制代码
    <include/linux/rwsem-spinlock.h>
    #define __RWSEM_INITIALIZER(name) \
    { 0, __SPIN_LOCK_UNLOCKED(name.wait_lock), LIST_HEAD_INIT((name).wait_list) \
      __RWSEM_DEP_MAP_INIT(name) }
    #define DECLARE_RWSEM(name) \
        struct rw_semaphore name = __RWSEM_INITIALIZER(name)

对一个rwsem变量动态初始化使用init_rwsem宏,其展开形式为

c 复制代码
void __init_rwsem(struct rw_semaphore *sem)
{
    sem->activity = 0;
    spin_lock_init(&sem->wait_lock);
    INIT_LIST_HEAD(&sem->wait_list);
}

rwsem的初始状态是没有任何活动的读取者与写入者。

c 复制代码
读取者的DOWN操作:
void __sched down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
读取者的UP操作:
void up_read(struct rw_semaphore *sem);
写入者的DOWN操作:
void __sched down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
写入者的UP操作:
void up_write(struct rw_semaphore *sem)
相关推荐
再ZzZ1 小时前
Docker安装PaddleOCR并提供接口服务(CPU)
运维·docker·容器
love530love2 小时前
是否需要预先安装 CUDA Toolkit?——按使用场景分级推荐及进阶说明
linux·运维·前端·人工智能·windows·后端·nlp
m0_694845572 小时前
日本云服务器租用多少钱合适
linux·运维·服务器·安全·云计算
jasonslaex2 小时前
stm32传感器通用驱动代码
驱动开发·stm32·嵌入式硬件
一心0922 小时前
Linux部署bmc TrueSight 监控agent步骤
linux·运维·服务器·监控·bmc truesight
Florence232 小时前
linux中执行脚本命令的source和“.”和“./”的区别
linux·运维·服务器
白日依山尽yy2 小时前
Linux02
linux·运维·服务器
JavaAlpha3 小时前
Jenkins 配置信息导出 的详细说明(中英对照)
运维·servlet·jenkins
liulilittle3 小时前
通过高级处理器硬件指令集AES-NI实现AES-256-CFB算法并通过OPENSSL加密验证算法正确性。
linux·服务器·c++·算法·安全·加密·openssl
Blossom.1183 小时前
基于区块链的去中心化身份验证系统:原理、实现与应用
运维·服务器·网络·人工智能·机器学习·去中心化·区块链