linux设备驱动七(时间、延迟及延缓操作)

 

知识点:

  • 如何度量时间差,如何比较时间
  • 如何获得当前时间
  • 如何将操作延迟指定一段时间
  • 如何调度异步函数到指定时间之后执行

 

 

度量时间差

     HZ指一秒内产生的时钟中断次数,即时钟中断频率

     jiffies_64记录自上一次操作系统引导以来的时钟滴答数。即使在32位架构上也是64位。

     驱动程序通常访问jiffies变量。它是unsigned long类型,要么和jiffies_64相同,要么仅仅是jiffies_64的低32位.使用它的原因是访问快,而对64位的访问不是在所有架构删都是原子的。

 

 

使用jiffies

     jiffies有溢出的问题,比较时应该使用宏:

     time_afer timer_before time_after_eq time_before_eq

 

     有时需要将来自用户空间的时间表述方法(struct timeval和struct timespec)与内核表述方式进行转换。使用:

     timespec_to_jiffies jiffies_to_timespec timeval_to_jiffies jiffies_to_timeval

 

     32位架构中,jiffies_64的访问不是原子的,不能直接读取,应该使用辅助函数get_jiffies_64

 

     通过/proc/interrupts的计数值除以/proc/uptime报告的系统运行时间,即可获得确切的HZ值

 

处理器特定的寄存器

大部分的CPU设计中,指令时序本质上不可预测,依赖于指令周期的经验型性能描述方法不再使用,CPU制造商引入了一种通过计算时钟周期来度量时间差的方法。

使用一个随时钟周期不断递增的计数寄存器,最有名的计数器寄存器就是TSC(时间戳计数器)

rdtsc(low32,high32);

rdtscl(low32);

rdtscll(var 64); 64位在可能会溢出的情况下使用。但这些函数是x86专用的。

 

get_sycles是一个体系结构无关的函数,在不支持计数器寄存器的平台上返回0

 

时间戳计数器不会再SMP的多个处理器保持同步,为获得一致的值,需要使查询该计数器的代码禁止抢占。

 

获取当前时间

 

内核一般使用jiffies来获取当前时间,该值表示最近一次系统启动到当前的时间间隔

驱动程序可以利用jiffies来测量不同事件的时间间隔

如果要测量更短则职能使用处理器特定的寄存器

 

墙钟时间(日常生活中的时间),有更高的策略相关性,不能归于内核,内核也提供了将墙钟时间转为jiffies的函数:mktime

 

do_gettimeofday在许多体系结构上有接近微妙级的分辨率,依赖实际的硬件机制。

 

也可以通过直接访问xtime变量获得当前时间,但是精度要差一些,因为很难原子的访问,所以不建议直接访问。提供了函数curent_kernel_time

 

延迟执行

 

涉及多个时钟滴答的延迟称为“长延迟”

常常与时钟滴答的延迟可以使用系统时钟,非常短的延迟通常必须用软件循环。

 

长延迟:

 

长于一个时钟滴答

 

忙等待

 

while(time_before(jiffies, jl)) cpu_relax();

跟具体架构相关,不执行大量的处理器代码,某些系统可能不做任何事情,也可能把CPU让给其他现成,但是仍然应该避免。

忙等待严重降低系统性能,内核配置为非抢占时,循环会锁住处理器。配置为抢占时,可以用作他用,但仍然有些浪费。

如果进入循环之前,禁止了中断,jiffies则不会更新,此时wilie条件永远为真

 

让出处理器

 

while(time_before(jiffies,jl)) schedule();

当系统中只有一个进程,调度器还是回调度它,而空闲任务(Idle进程号是0)得不到调度。运行Idle会降低CPU负荷。

还有一个问题是可能导致等待时间更长,在延迟时间到达时,CPU在执行其他任务。

 

超时

 

long wait_event_timeout(wait_queue_head_t q, condition, long timeout);

long wait_event_interruptible_timeout(queue_head_t_q, condition, long timeout);

 

超时到期返回0,如果由其他事件唤醒,返回剩余时间,不会返回负数,即使超过了等待时间。参数timeout是要等待的jiffies值,不是绝对时间。如果传递的是负数会产生oops

 

signed long schedule_time(signed long timeout);

可以避免使用多余的等待队列,但是首先要设置当前进程的状态。set_current_state

调度器会在超时时间到期且其状态变成TASK_RUNNING时才会运行这个进程

如果没有设置状态,则和调用schedule的作用是一样的,内核构造的定时器不会真正起作用

 

短延迟

 

void ndelay(unsigned long nsecs);

void udelay(unsigned long usecs);

void mdelay(unsigned long msecs);

udelay和ndelay传递的参数设置了上线,如果模块无法加载,并显示为解析的符号__bad_udelay,则说明调用udelay传递了太大的值

一般规则,上前毫秒应使用udelay,而不是ndelay,毫秒级延迟应使用mdelay,而不是其他粒度。

这三个函数都是忙等待函数。

 

非忙等待的毫秒级延迟函数

 

void msleep(unsigned int millisecs);

unsigned long msleep_interruptible(unsigned int msec); 返回0,被提前中断返回剩余毫秒数。

void ssleep(unsigned int seconds);

 

 

内核定时器

 

内核定时器可用来在未来某个特定时间点(基于时钟滴答)调度执行某个函数。

这个函数是异步执行的,而启动这个定时器的进程可能正在休眠火灾其他处理器上执行,也可能已经推出了。

内核定时器常常作为软件中断的结果来运行的。不处于进程上下文中。

 

不处于进程上下文时,应该遵循的规则:

    • 不允许访问用户空间,没有进程上下文,无法将任何特定进程与用户空间关联
    • current指针在原子模式下没有任何意义,也是不可用的,因为相关代码和被中断的进程没有任何关联
    • 不能执行休眠或调度,例如schedule或wait_event,以及GFP_KERNEL为标记的kmalloc,信号量等等。

 

in_interrupt()函数,处于中断上下文时返回非零值

in_atomic()函数,调度不被允许时,返回非零值,包括了中断上下文,以及拥有自旋锁的时候。此时current可用使用,但禁止访问用户空间,因为这回导致调度的发生

 

另外一个特定是,可以在定时器函数中再次注册自己稍后执行,这是因为函数执行前会从链表中移除

在SMP系统中,为了获得缓存的局部性(locality),定时器只会在注册他的同一CPU执行

异步执行导致并发,可能产生竟态,注意保护。

 

定时器API

 

void init_timer(struct timer_list *timer)

struct timer_list TIMER_INITIALIZER(_function, _expires, _data);

void add_timer(struct timer_list *timer)

void del_timer(struct timer_list*timer)

expires = jiffies + tdelay

 

int mod_timer(struct timer_list *timer, unsigned long expires)

int del_timer_sync(struct timer_list* timer) 可以在SMP系统上避免竟态发生,非原子上下文中调用,可能会休眠,其他情况进入忙等待,调用该函数之前,持有了锁,定时器函数企图获取相同锁可能导致死锁,定时器函数中如果重新注册自己,在调用该函数应该设置标记,确保不发生重新注册

int timer_pending(const struct timer_list *timer) 定时器是否在被调度运行

 

内核定时器实现

定时器用到了per-cpu变量,timer_list中的base域指向该变量

 

add_timer时,内核会将timer_list加入到512个链表头构成的级联链表中的一个链表上。具体加入哪个链表按照下面的方式计算:

 

如果在0-255个jiffies中到期,加入前256个链表中的一个,取决于expires字段低8位

256-16384个jiffies中到期,256 - 256 + 63,这64个链表中的一个 取决于expires字段的9-14位

更远的定时器,依次放在下面的3组64个链表中,分别取决于15-20位,21-26位,27-31位,超过超过了32位,则利用延迟值0xffffffff做散列运算

 

__run_timers被激发时,会执行当前时钟滴答上的所有挂起的定时器,如果jiffies当前是256的倍数,还会将下一级定时器链表重新散列到256个短期链表中,同时可能根据上jiffies的位划分对其他级别的定时器做级联处理。

__run_timers运行在原子上下文中,即使不是抢占式内核,系统忙等待,定时器仍然能够很好的工作(分析原因?)

定时器会受到硬件中断和其他定时器及异步任务的影响,适合步进电机和业余电子设备,不适合工业环境下的生产系统

 

 

tasklet

和定时器相同的是:

始终在中断期间执行,

始终在调度他们的同一CPU上运行

tasklet也会在软件中断上下文以原子模式执行(原子模式怎么理解?)

和定时器不同的是:

tasklet不能要求在某个给定时间执行。

 

相关函数:

  • 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);
  • void tasklet_disable(struct tasklet_struct *t); 该函数仍然可以用tasklet_schedule调度,但其执行被推迟,知道该tasklet被重启。如果tasklet当前正在运行,该函数会进入忙等待知道tasklet函数退出。因此,在调用该函数之后,可以确信该tasklet不会再系统中的任何地方运行
  • void tasklet_disable_nosync(struct tasklet_struct *t);该函数返回后,tasklet直到重启之前不会再被调度,但是有可能已经开始执行,该函数不会等待tasklet执行完毕。
  • void tasklet_enable(struct tasklet_struct *t);重启一个被禁止的tasklet,必须和tasklet_disable匹配调用,因为内核对每个tasklet保存有一个禁用计数。
  • void tasklet_schedule(struct tasklet_struct *t);未开始执行,多次调用该函数也只会执行一次,已经开始执行,会在执行完毕后,再次运行。通过这种方式,可以重启自己。
  • void tasklet_hi_schedule(struct tasklet_struct *t);以高优先级执行,会在处理其他软件中断任务之前处理高优先级的tasklet,低延迟需要的任务调用。
  • void tasklet_kill(struct tasklet_struct *t);确保指定的tasklet不会再次调度运行,已经开始执行,该函数等待其推出,和del_timer_sync相同,应该避免在调用tasklet_kill之前完成重新调度。

 

特性:

tasklet可以在稍后被禁用或启用,只有启动和禁用次数相同,tasklet才会被执行

tasklet也可以注册自己本身

tasklet可被调度在通常的优先级或者高优先级执行,高优先级的tasklet总会首先执行

如果系统负荷不重,则tasklet立即得到执行,但始终不会晚于下一个定时器滴答(最晚下一个滴答会运行,这个怎么来保证?)

一个tasklet可以和其他tasklet并发,但对自身来讲是严格串行处理的。同一个tasklet永远不会在多个处理器上同时运行。

void tasklet_enable(struct tasklet_struct *t);启用一个被禁用的tasklet,必须和tasklet_disable成对调用,因为tasklet保存有一个禁用计数

void tasklet_schedule(struct tasklet_struct *t);调度一个tasklet,还没有开始执行,多次调用也只会执行一次,如果已经开始执行了,在完成后会再次运行。

 

工作队列

和tasklet区别:

  • tasklet运行在中断上下文,工作队列运行在内核进程上下文,工作队列可以休眠,tasklet不能。
  • tasklet始终运行在被初始提交的同一个处理器上,而工作队列是可以配置的。
  • 内核代码可以请求工作队列函数在指定延迟后执行
  • tasklet会在很短的时间段内快速执行,而工作队列没有这个限制

 

相关函数:

    • struct workqueue_struct *create_workqueue(const char *name); 内核会在系统中每个处理器上为该工作队列创建专用线程。
    • struct workqueue_struct *create_singlethread_workqueue(const char* name); 如果单个线程足够使用,使用该函数
    • DECLARE_WORK(name, void (*func)(void  *), void *data);静态声明一个work_struct
    • INIT_WORK(struct work_struct *work, void (*func)(void *); void *data); 首次构造之后,使用该函数初始化
    • PREPARE_WORK(struct work_struct *work, void (*func)(void *), void *data); 如果该结构已经被提交到工作队列,使用该函数修改
    • int queue_work(struct workqueue_struct *queue, struct work_struct *work);
    • int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, unsignend long delay);加入工作队列,但是work_struct实际工作至少在指定时间之后执行。如果已经在队列中,则这两个函数都返回1.返回非0,表示工作已经在队列中,不能多次加入。
    • int cancel_delayed_work(struct work_struct *work); 返回非0,表示工作不会再执行。返回0,则表示工作可能已经在执行了。因此该函数返回之后,工作仍然可能在运行,
    • void flush_workqueue(struct workqueue_struct *queue);上面一个函数返回0时,应该调用该函数,该函数调用之后,在该函数调用之前提交的工作函数不会再执行
    • void destroy_workqueue(struct workqueue_struct *queue); 

 

注意:工作队列中的工作运行在内核线程,不能访问用户空间。如果工作发生了休眠,会影响该队列中的其他工作。

 

 

共享队列

 

默认的工作队列,和其他人共享,不应该长时间占用,不能长时间休眠。

 

相关函数

int schedule_work(struct work_struct *work);

int schedule_delayed_work(struct work_struct *work, unsigned long delay);

可以使用cancel_delayed_work,但是要调用void flush_scheduled_work(void);刷新共享队列

 

 

你可能感兴趣的:(linux,kernel)