当前位置: 首页 > news >正文

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

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

信号量的定义与初始化

信号量的定义如下:

<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函数定义如下:

<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宏完成:

#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操作有:

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函数定义如下:

<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--;elseresult = __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:

<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,比如下面的代码段:

//定义一个信号量
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函数:

<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)来唤醒进程:

<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,专门用于这种用途的信号量定义和初始化:

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

该宏定义了一个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(&demo_sem))return -ERESTARTSYS;//成功获得信号量进入临界区//离开临界区,调用up释放信号量up(&demo_sem);
}

读取者与写入者信号量rwsem

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

<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宏进行初始化:

    <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宏,其展开形式为

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

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

读取者的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)
http://www.xdnf.cn/news/1031761.html

相关文章:

  • 《Elasticsearch 分布式搜索在聊天记录检索中的深度优化》
  • 驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接,
  • 【Elasticsearch】分词机制详解(含实战案例)
  • [学习] FIR多项滤波器的数学原理详解:从多相分解到高效实现(完整仿真代码)
  • 【FineDance】训练:accelerate config 的作用
  • tshark命令行语法详解
  • 量化面试绿皮书:13. 贴错标签的袋子
  • Python爬虫实战:研究simpleq相关技术
  • 同步与异步编程范式全景研究——从CPU时钟周期到云原生架构的范式演进
  • Windows平台进程加速方案研究:以网盘下载优化为例
  • 再参数化视角下的批量归一化:缩放平移操作的本质意义
  • ESP32-S3 学习之旅开篇:课程与芯片基础全解析
  • php 数学公式转成SVG,并下载到服务器本地
  • 查看哪些IP在向kafka的broker生产消息
  • 智能穿戴平台与医疗AI融合发展路径研究
  • 基于springboot+servlet、jsp的潮服购物商城系统的设计与实现,论文7000字
  • Linux免驱使用slcan,使用方法以Ubuntu为例
  • Zookeeper 3.8.4 安装部署帮助手册
  • 数据库管理员密码重置指南:MySQL, Oracle, PostgreSQL
  • 【Flutter】性能优化总结
  • Flutter 与原生技术(Objective-C/Swift,java)的关系
  • Java-46 深入浅出 Tomcat 核心架构 Catalina 容器全解析 启动流程 线程机制
  • 负载均衡器:Ribbon和LoadBalance
  • NY248NY254美光科技闪存NY258NY261
  • [架构之美]解决Windows 10主机与Windows 10虚拟机之间无法拖拽复制问题
  • 使用 Flutter 在 Windows 平台开发 Android 应用
  • MATLAB提供的两种画误差矩阵的函数
  • 矩阵混剪系统源码搭建全流程技术解析,矩阵OEM
  • 篇章七 论坛系统——业务开发——前端
  • 山东大学软件学院项目实训:基于大模型的模拟面试系统项目总结(十)