讨论的问题:
- 如何度量时间差,如何比较时间
- 如何获得当前时间
- 如何将操作延迟指定的一段时间
- 如何调度异步函数到制定的时间之后执行
度量时间差
一般性规则,即使知道对应平台上的确切Hz值,也不应该在编程时依赖该HZ值
可以通过修改系统的时钟改变系统时钟中断发生的频率,但是必须重新编译内核以及所有模块,某些计算机内部的实现仅仅适用于12 <HZ<1535
使用jiffies计数器
内核内部计数器jiffies_64每次时钟发生中断加一
为了保证访问速度,驱动开发通常访问jiffies变量
计算未来时间戳如下:
#include <linux/jiffies.h>
unsigned long j, stamp_1, stamp_half, stamp_n;
j = jiffies; /* read the current value */
stamp_1 = j + HZ; /* 1 second in the future */
stamp_half = j + HZ/2; /* half a second */
stamp_n = j + n * HZ / 1000; /* n milliseconds */
为防止在32位平台上溢出(50天):
#include <linux/jiffies.h>
int time_after(unsigned long a, unsigned long b);
int time_before(unsigned long a, unsigned long b);
int time_after_eq(unsigned long a, unsigned long b);
int time_before_eq(unsigned long a, unsigned long b);
用户空间时间表示方法与内核时间表示方法的转换:
#include <linux/time.h>
unsigned long timespec_to_jiffies(struct timespec *value);//s and n
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value); //s and m
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);
32位处理器上对64位数据的访问不是原子的,需要适当的锁定:
#include <linux/jiffies.h>
u64 get_jiffies_64(void);
注:系统的时钟频率对用户空间几乎不可见,用户空间经过转换HZ始终为100
如果想获取系统时钟频率只能通过/proc/interrupts。中断数除以运行时间=HZ
处理器特定的寄存器
如果度量非常短的时间,可以使用特定平台相关的资源
绝大多数处理器都包含一个随时间不断递增的计数寄存器,这个计数器是完成高分辨率计时任务的唯一可靠途径,如果某些处理器没有这个计数器可通过外部设备 来实现
最有名的计数器是TSC,奔腾开始就有了,用户空间和内核空间都都可以读取
rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var64);
32位的4..2s溢出一次
如下计算指令运行时间:
unsigned long ini, end;
rdtscl(ini); rdtscl(end);
printk("time lapse: %li\n", end - ini);
平台无关的函数get_cycles无计数器时始终返回0
原型:
#include <linux/timex.h>cycles_tget_cycles(void);
MIPS处理器实现一个rdctl
#define rdtscl(dest) \
__asm__ __volatile__("mfc0 %0,$9; nop" : "=r" (dest))
获取当前时间
驱动程序可以利用jiffies计算不同事件的时间间隔,比如分辨鼠标的双击,如果需要测量更短的时间差,则需要使用特殊的寄存器
墙钟时间转换为jiffies值:
#include <linux/time.h>
unsigned long mktime ( unsigned int year, unsigned int mon,
unsigned int day, unsigned int hour,
unsigned int min, unsigned int sec);
获取当前时间
#include <linux/time.h>
voiddo_gettimeofday(struct timeval *tv);
接近微妙级的精度
#include <linux/time.h>
struct timespec current_kernel_time(void);
精度比较低
延迟执行
设备驱动程序经常需要某些特定代码延迟一段时间执行
长延迟
涉及多个时钟滴答延迟的称为长延迟,几个毫秒。
忙等待
如果想延迟若干个时钟周期,并且精度要求不高,最简单的就是监视jiffies计数器的循环:
while (time_before(jiffies, j1))
cpu_relax();
这个等待循环回严重降低系统性能,如果内核配置并非抢占的,这个循环再延迟期会一直锁住处理器
更糟糕的是,如果在进入循环之前正好禁止了中断,jiffies不会更新,只能重新启动机器。
几种延迟方法在jit模块中实现了,由该模块创建的所有的 /proc/jit*文件每次被读取一行都会延迟整整1s
phon% dd bs=20 count=5 < /proc/jitbusy
1686518 1687518
1687519 1688519
1688520 1689520
1689520 1690520
1690521 1691521
但是如果运行在有大量cpu密集型进程的系统(非抢占内核)上运行时:
phon% dd bs=20 count=5 < /proc/jitbusy
1911226 1912226
1913323 1914323
1919529 1920529
1925632 1926632
1931835 1932835每个系统调用恰好延迟1s,但是调度dd进程执行下一个系统调用时间延迟很多
抢占式内核高负荷系统上:
phon% dd bs=20 count=5 < /proc/jitbusy
1911226 1912226
1913323 1914323
1919529 1920529
1925632 1926632
1931835 1932835
两次系统调用之间没有很大的延迟,但是单个延迟可能长于1s
让出处理器
在不需要cpu时释放cpu
while (time_before(jiffies, j1)) {
schedule();
}
抢占式低负荷:
phon% dd bs=20 count=5 < /proc/jitsched
1760205 1761207
1761209 1762211
1762212 1763212
1763213 1764213
1764214 1765217
当前进程虽然释放cpu而不做任何事情,但是它仍然在运行队列里面,如果负载过高,read延迟的时间还会加长,因为在延迟到期时其他进程正在使用cpu。
超时
实现延迟最好的办法应该是让内核帮我们完成相应的工作:
#include <linux/wait.h>
long wait_event_timeout(wait_queue_head_t q, condition, long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout);
上面的函数会在给定的等待队列上休眠,但是会在超时到期时返回。实现了有界的休眠,这种休眠不会永远继续。
phon% dd bs=20 count=5 < /proc/jitqueue
2027024 2028024
2028025 2029025
2029026 2030026
2030027 2031027
2031028 2032028
即使是在抢占式高负荷的系统上运行也看不到任何区别。
为了适应不等待特定事件而延迟的特殊情况,内核为我们提供了schedule_timeout函数,可以避免声明等待队列头。
#include <linux/sched.h>
signed long schedule_timeout(signed long timeout);
此函数要求调用者首先设置当前进程的状态,例如下面这样:
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (delay);
短延迟
ndelay, udelay, 以及 mdelay分别延后执行指定的纳秒数, 微秒数或者毫秒数.
#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
所有的体系结构都会实现udelay,但其他函数可能未定义
实际上当前平台都无法达到纳秒精度
Udelay的实现使用了软件循环,
这三个函数均是忙等待函数
实现毫秒级别的延迟还有一种办法,这种办法不涉及忙等待:
void msleep(unsigned int millisecs);//不可中断
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds)
内核定时器
一个内核定时器是一个数据结构,它告诉内核在用户定义的时间点使用用户定义的参数来执行一个用户定义的函数
许多动作需要在进程上下文中才嫩执行,如果不在进程上下文中则
l 不允许访问用户空间,因为没有进程上下文,无法将任何特定的进城与用户空间关联起来
l Current指针在院子模式下市没有任何意义的,也是不可用的,因为相关代码和中断的进程没有任何关联
l 不能执行休眠或者调度,不能调用任何引起休眠的函数,信号量也不能用,因为可能引起休眠
内核代码可以通过in_interrupt()来判断自己是否处在正运行的中断上下文,通过in_atomic()判断是否允许被调度。
另一个重要特性是任务可以将自己注册以在稍后的时间重新运行
一个注册自己的定时器始终会在同一个cpu上运行。
即使在单处理器上定时器也会是竞态的潜在来源,这是由其异步执行的特点直接导致的。
定时器API
内核为驱动程序提供了一组用来声明、注册、删除内核定时器的函数,摘录如下:
#include <linux/timer.h>
struct timer_list
{
/* ... */
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
};
void init_timer(struct timer_list *timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);
expires 字段表示定时器期望运行的 jiffies 值; 到达该jiffies 值, 调用function 函数并传递data 作为一个参数
phon% cat /proc/jitimer
time delta inirq pid cpu command
33565837 0 0 1269 0 cat
33565847 10 1 1271 0 sh
33565857 10 1 1273 0 cpp0
33565867 10 1 1273 0 cpp0
33565877 10 1 1274 0 cc1
33565887 10 1 1274 0 cc1
第一行由read操作文件生成,其他行由定时器完成
Time表示jiffies值,delta表示jiffies变化量,inirq表示in_interrupt返回值,pid和command表示当前进程
还包括其他几个函数:
int mod_timer(struct timer_list *timer,unsigned long expires);
更新定时器的到期时间
int del_timer_sync(struct timer_list *timer);
该函数可以确保返回时没有任何cpu在运行定时器函数,防止竞态
int timer_pending(const struct timer_list *timer);
通过读取timer_list结构的一个不可见字段来判断定时器是否正在被调度运行。
内核定时器的实现
内核定时器需要满足如下的需求及假定
l 定时器的管理必须尽可能做到轻量级
l 其设计必须在活动定时器大量增加时具有很好的伸缩性
l 大部分定时器会在最多几秒或者几分钟到期,而很少存在长期延迟的定时器
l 定时器在注册他的同一个cpu上运行
内核开发者使用的解决方案是使用per_cpu数据结构,timer_list的base字段包含一个指向该结构的指针,通过判断是否为null来判断定时器是否运行
Tasklet
在很多方面类似内核定时器:如始终在中断期间运行,始终会在调度他们的同意cpu上运行,接收一个unsignedlong参数。
不同的是:我们不要求tasklet在某个给定的时间执行,调度一个tasklet只是表明我们希望内核选择某个其后的时间来执行给定的函数。
Tasklet以数据结构的形式存在,使用前必须初始化:
#include <linux/interrupt.h>
struct tasklet_struct {
/* ... */
void (*func)(unsigned long);
unsigned long data;
};
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data);
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data);
一些特性:
l 一个tasklet可以 稍后被禁止或者启动,只有启用的次数和禁止的次数相等时tasklet才会执行
l Tasklet可以注册自己本身
l Tasklet可以被调度以在较高的优先级上执行
l 如果系统负荷不重,tasklet会立即执行,但始终不会晚于下一个时钟滴答
l 一个tasklet可以和另一个tasklet并发执行,但是同一个tasklet不会在多个处理器上同时运行
phon% cat /proc/jitasklet
time delta inirq pid cpu command
6076139 0 0 4370 0 cat
6076140 1 1 4368 0 cc1
6076141 1 1 4368 0 cc1
6076141 0 1 2 0 ksoftirqd/0
6076141 0 1 2 0 ksoftirqd/0
6076141 0 1 2 0 ksoftirqd/0
其他的tasklet内核接口:
void tasklet_disable(struct tasklet_struct*t);
禁止指定的tasklet
void tasklet_disable_nosync(structtasklet_struct *t);
禁止制定的tasklet,但不会等待任何正在运行的tasklet退出
void tasklet_enable(struct tasklet_struct*t);
启用一个先前被禁用的tasklet
void tasklet_schedule(struct tasklet_struct*t);
调度执行指定的tasklet
void tasklet_hi_schedule(structtasklet_struct *t);
调度指定的tasklet以高优先级执行
void tasklet_kill(struct tasklet_struct *t);
该函数确保tasklet不会再次被调度执行
工作队列
表面上看,工作队列类似于tasklet,他们都允许内核代码请求某个函数在将来的时间被调用。但是存在一些重要区别,包括:
l Tasklet在软件中断上下文中执行,因此所有的tasklet代码必须是原子的。而工作队列在相应的内核进程上下文上执行,具有更好的灵活性,尤其是工作队列可以休眠。
l Tasklet始终运行在被调度的同一个处理器上,但这只是工作队列的默认方式
l 内核代码可以请求工作队列的执行延迟指定的时间间隔
工作队列有structworkqueue_struct结构。使用之前,我们必须显示的创建一个工作队列:
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
create_workqueue会在每个处理器上位该工作队列创建专用的线程。
create_singlethread_workqueue创建单个工作线程
要向工作队列提交一个任务,首先需要初始化work_struct
DECLARE_WORK(name, void (*function)(void *), void *data);
如果想在运行时构建该宏:
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data);
INIT_WORK 首次构造时使用这个宏
PREPARE_WORK 如果结构已经提交到工作队列,而只是需要修改该结构,则使用这个宏
如果要将工作提交到工作队列:
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, unsigned long delay);
如果取消每个工作队列入口项并确保其不会在任何地方运行:
int cancel_delayed_work(struct work_struct *work);
void flush_workqueue(struct workqueue_struct *queue);
结束工作队列的使用的可调用如下函数释放资源
void destroy_workqueue(struct workqueue_struct *queue);
共享队列
许多情况下设备驱动程序不需要自己的工作队列,如果我们只是偶尔的想队列中提交任务,则更简单和有效的办法是共享内核中提供的共享的默认工作队列。
如果使用,不应该长期独占该队列,即不能长期休眠
PS:本文为学习《linux设备驱动程序》第七章整理的学习笔记