linux设备驱动程序(第三版)阅读笔记(七)

 

说明:版权所有归作者,只供学习交流,若有其它用途请联系作者,转载请遵守IT人职业规范,请注明转载地址

 

第七章:时间、延迟及延缓操作

1,(度量时间差)内核通过定时器中断来跟踪时间流。时钟中断由系统定时硬件以周期性的间隔产生,这个间隔由内核根据HZ的值设定,HZ是一个与体系结构有关的常数,定义在<linux/param.h>或者该文件包含的某个子平台相关的文件中。对真实硬件,已发布的linux内核源代码为大多数平台定义的默认值HZ值范围为50-1200,而对软件仿真的HZ值是24.大多数平台每秒有1001000次时钟中断,而在常见的X86 PC平台上,默认定义为1000(在包括2.4在内的早期版本中,该平台上的HZ值定义为100)。作为一般性规则,即使知道对应平台上的确切HZ值,也不应该在编程时依赖该HZ.

 

每次当时钟中断发生时,内核内部计数器的值就增加一。这个计数器的值在系统引导时被初始化0,因此,它的值就是自上次操作系统引导以来的时钟滴答数。这个计数器是一个64位的变量(即使在32位的架构上也是64位),称为“jiffies_64”。但是,驱动程序开发者通常访问的是jiffies变量,它是unsigned long型的变量,要么和jiffies_64相同,要么仅仅是jiffies_64的低32位。通常首选使用jiffies,因为对它的访问很快,从而对64jiffies_64值的访问并不需要在所有架构上都是原子的。

 

用户空间的时间描述一般使用struct timevalstruct timespec,我们有时需要将来自用户空间的时间表述方法和内核表述方法进行转换。这两个结构使用两个数来表示精确的时间:在老的、流行的struct timeval中使用秒和毫秒值,而较新的struct timespec中则使用秒和纳秒,前者比后者出现的早,但更常用。为了完成jiffies值和这些结构间的转换,内核提供了下面四个辅助函数:

#include<linux/time.h>

Unsigned longtimespec_to_jiffies(struct timespec *value);

Voidjiffies_to_timespec(unsigned long jiffies, struct timespec *value);

Unsigned longtimeval_to_jiffies(struct timeval *value);

Voidjiffies_to_timeval(unsigned long jiffies, struct timeval *value);

64jiffies_64的访问不像对jiffies的访问那样直接。在64位计算机架构上,这两个变量其实是同一个;但在32位处理器上,对64位值得访问不是原子的。这意味着,在我们读取64位值得高32及低32位时,可能会发生更新,从而获得错误的值。因此,对64位计数器的直接读取是很靠不住的,但如果必须读取64位计数器,则应该使用内核导出的一个特殊辅助函数,该函数为我们完成了适当的锁定:

#include<linux/jiffies.h>

U64get_jiffies_64(void);

最后,需要注意,实际的时钟频率对用户空间来讲几乎是完全不可见的。当用户空间包含param.h时,HZ宏始终被扩展为100,而没报告给用户空间的计算器值均做了相应的转换。对于用户空间来说,如果想知道定时器中断的确切HZ值,只能通过/proc/interrupts获得。

 

2,(内核定时器)如果我们需要在将来的某个时间点调度执行某个动作,同时在该时间点到达之前不会阻塞当前进程,则可以使用内核定时器。内核定时器可用来在未来的某个特定时间点(基于时钟滴答)调度执行某个函数,从而可用于完成许多任务。一个内核定时器是一个数据结构,它告诉内核在用户定义的时间点使用用户定义的参数来执行一个用户定义的函数。其实现位于<linux/time.h><kernel/timer.c>文件。

被调度运行的函数几乎肯定不会在注册这些函数的进程正在执行时运行。相反,这些函数会异步地运行。

定时器函数必须原子的运行,但是这种非进程上下文还带来其他一些问题。现在我们要讨论这些限制,原子上下文中的这些规则必须遵守,否则会导致大麻烦。许多动作需要在进程上下文中才能执行。如果进程处于上下文之外(比如中断上下文中),则必须遵守如下原则:

(1)       不充许访问用户空间。因为没有进程上下文,无法将任何特定进程与用户空间关联起来。

(2)       current指针在原子模式下是没有任何意义的,也是不可用的,因为相关代码和被中断的进程没有任何关联。

(3)       不能执行休眠或调度。原子代码不可用调度schedule或者wait_evevt,也不能调用任何可能引起休眠的函数。例如,调用kmalloc(,GFP_KERNEL)就不符合本规定。信号量也不能用,因为可能引起休眠。

内核代码可通过调用函数in_interrupt()来判断自己是否正运行于中断上下文,该函数无需参数,如果处理器运行在中断上下文就返回非零值,而无论是硬件中断还是软件中断。

内核定时器的另一个重要特性是,任务可以将自己注册以在稍后的时间重新运行。这种可能性是因为每个timer_list结构都会在运行之前从活动定时器链表中移走,这样就可以立即链入其他的链表。尽管多次调度同一任务是一件没有多大意义的操作,但有时还是很有用的。

关于定时器,还有一个要谨记的重要特性:即使在单处理器系统上,定时器也会是竞态的来源。这是由其异步执行的特点直接导致的。因此任何通过定时器函数访问的数据结构都应该针对并发访问进行保护。

 

描述定时器的结构体:

Struct timer_list

{

/**/

Unsigned long expires;

Void (*function)(unsignedlong);

Unsigned long data;

};

上面给出的数据结构其实包含其他的一些未列出的字段,但给出的三个字段是可由定时器代码以外的代码访问。expires字段表示期望定时器执行的jiffies值;到达jiffies值时,将调用function函数,并传递data作为参数。如果需要通过这个参数传递多个数据项,那么可以将这些数据项捆绑成一个数据结构,然后将该数据结构的指针强制转化为unsigned long传入。这种技巧在所有内核支持的体系架构上都是安全的,而且内存管理中非常常见expires的值并不是jiffies_64项,这是因为定时器并不适用于长的未来的时间点,而且在32位平台上的64位操作会比较慢。

 

定时器的初始化:

Void init_timer(structtimer_list *timer);

Struct timer_listTIMER_INITIALIZER(_function, _expires, _data);

定时器数据结构使用前必须初始化。初始化步骤可确保所有字段被正确设置,包括那些对调用者不可见的字段。通过调用init_timer或者TIMER_INITIALIZER赋予某个静态链表结构,即可完成初始化,具体使用哪个取决于我们的需求。

 

添加(注册)一个定时器:

Void add_timer(structtimer_list *timer);

 

删除(销毁)一个定时器:

int del_timer(structtimer_list *timer);如果要在定时器到期之前禁止一个以注册的定时器,则可以调用del_timer函数。

intdel_timer_sync(struct timer_list *timer);

del_timer的工作类似,但该函数可确保在返回时没有任何cpu在运行定时器函数。int del_timer_sync可用于在SMP系统上避免竞态,这和单处理器内核中的del_timer是一样的。在大多数情况下,应优先考虑调用这个函数而不是del_timer函数。如果从非原子上下文调用,该函数可能休眠,但在其他情况下会进入忙等待。在拥有锁时,应该格外小心调用del_timer_sync,因为如果定时器函数企图获取相同的锁,系统就会进入死锁。如果定时器函数会重新注册自己,则调用者必须首先确保不会发生重新注册;这通常通过设置一个由定时器函数检查的“关闭”标志来实现。

 

 

更新某个定时器的到期时间:

int mod_timer(structtimer_list *timer, unsigned long expires);

更新某个定时器的到期时间,经常用于超时定时器(典型的例子是软驱的关马达定时器)。我们也可以在通常使用add_timer的时候在不活动的定时器上调用mod_timer

int timer_pending(conststruct timer_list *timer);

该函数通过读取timer_list结构的一个不可见字段来返回定时器是否正在被调度运行。

 

3,(tasket(小任务)机制)

Tasklet在很多方面类似于内核定时器:它们始终在中断期间运行,始终会在调度它们的同一个cpu上运行,而且都接收一个unsigned long参数。和内核定时器不同的是,我们不能要求tasklet在某个给定时间执行。调度一个tasket,表明我们只是希望内核选择某个其后的时间来执行给定的函数。这种行为对中断处理例程来说尤其有用,中断处理例程必须尽可能快地管理硬件中断,而大部分数据管理则可以安全地延迟到其后的时间,实际上,和内核定时器类似,tasket也会在“软件中断”上下文以原子模式执行。软件中断是打开硬件中断的同时执行某些异步任务的一种内核机制。

tasklet以数据结构的形式存在,并在使用前必须初始化。调用特定的函数或者使用特定的宏来声明该结构,即可完成tasklet的初始化:

#include<linux/interrupt.h>

Struct tasklet_struct

{

/**/

Void (*func)(unsignedlong);

Unsigned long data;

};

Void tasklet_init(structtasklet_struct *t

Void (*func)(unsigned long), unsigned long data);

DECLARE_TASKLET(name, func, data);

DECLARE_TASKLET_DISABLED(name, func, data);

tasket为我们提供了许多有意思的特性:

(1)             一个tasklet可在稍后被禁止或者重新启用;只有启用的次数和禁止的次数相同时,tasklet才会被执行。

(2)             和定时器类似,tasklet可以注册自己本身。

(3)             tasklet可被调度以在通常的优先级或者高优先级执行。高优先级的tasklet总会首先执行。

(4)             如果系统负荷不重,则tasklet会立即得到执行,但始终不会晚于下一个定时器滴答。

(5)             一个tasklet可以和其他tasklet并发,但对自身来讲是严格的串行处理的,也就是说,同一个tasklet永远不会 在多个处理器上同时运行。当然我们已经指出,tasklet始终会在调度自己的同一cpu上运行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux设备驱动程序是用于控制和管理硬件设备的软件模块。学习Linux设备驱动程序可以帮助开发人员理解和掌握Linux内核的工作原理,以及如何编写和调试设备驱动程序。 以下是一些学习Linux设备驱动程序笔记和建议: 1. 理解Linux设备模型:Linux设备模型是一种用于管理设备的框架,它提供了一种统一的方式来表示和操作设备。学习Linux设备模型可以帮助你理解设备的注册、初始化和销毁过程。 2. 学习字符设备驱动程序:字符设备是一种以字节为单位进行读写的设备,如串口、终端等。学习字符设备驱动程序可以帮助你了解字符设备的打开、关闭、读写等操作,并学习如何实现设备文件的注册和操作。 3. 学习块设备驱动程序:块设备是一种以块为单位进行读写的设备,如硬盘、闪存等。学习块设备驱动程序可以帮助你了解块设备的分区、缓存、IO调度等操作,并学习如何实现块设备的注册和操作。 4. 学习中断处理:中断是设备向处理器发送信号的一种机制,用于通知处理器设备的状态变化。学习中断处理可以帮助你了解中断的注册、处理和释放过程,并学习如何编写中断处理程序。 5. 学习设备驱动程序的调试技巧:设备驱动程序的调试是一个重要的技能,可以帮助你快速定位和解决问题。学习设备驱动程序的调试技巧可以帮助你理解和使用调试工具,如 printk、kprobe等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值