LDD-Time, Delays, and Deferred Work

Measuring Time Lapses

系统用来计时的变量是jiffies_64,在系统启动时会初始化为0,相关的函数都定义在linux/jiffies.h中;相关的宏定义还有HZ。下列代码是jiffies和HZ的使用方法:
1 #include <linux/jiffies.h>
2 unsigned long j, stamp_1, stamp_half, stamp_n;
3 j = jiffies; /* read the current value */
4 stamp_1 = j + HZ; /* 1 second in the future */
5 stamp_half = j + HZ/2; /* half a second */
6 stamp_n = j + n * HZ / 1000; /* n milliseconds */

要判断两个时间点的先后关系,可以采用下列方法:

1 #include <linux/jiffies.h>
2 int time_after(unsigned long a, unsigned long b);
3 int time_before(unsigned long a, unsigned long b);
4 int time_after_eq(unsigned long a, unsigned long b);
5 int time_before_eq(unsigned long a, unsigned long b);

用户程序用来表示时间的数据结构是struct timeval,struct timespec。前者包含两个数,分别代表秒和毫秒;后者包含两个数,分别代表秒和纳秒。相关的函数定义如下:

1 #include <linux/time.h>
2 unsigned long timespec_to_jiffies(struct timespec *value);
3 void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
4 unsigned long timeval_to_jiffies(struct timeval *value);
5 void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);
每个CPU平台一般都包含一个寄存器,每个时钟周期增加一次。x86下相关的操作函数:
1 #include <asm/msr.h>
2 rdtsc(low32, high32);
3 rdtscl(low32);
4 rdtscll(var64);

通常情况下,只采用寄存器的低32位已经足够——在1GHz的CPU上,每4.2秒才会溢出一次。

内核提供了通用的接口,定义在asm/timex.h:
#include <linux/timex.h>
cycles_t get_cycles(void);
关于处理器的时钟寄存器,有以下一段话:
There is one other thing worth knowing about timestamp counters: they are not necessarily synchronized across processors in an SMP system. To be sure of getting a coherent value, you should disable preemption for code that is querying the counter.
 
内核提供了函数将wall-clock time转化为jiffies:
1 #include <linux/time.h>
2 unsigned long mktime (unsigned int year, unsigned int mon,
3                       unsigned int day, unsigned int hour,
4                       unsigned int min, unsigned int sec);

以及获取当前时间的方法:

1 #include <linux/time.h>
2 void do_gettimeofday(struct timeval *tv);
3 struct timespec current_kernel_time(void);
 
Delaying Execution

下列代码可以实现延时功能:
while (time_before(jiffies, j1))
    cpu_relax();

这种延时功能会导致CPU空转,即忙等待,严重影响系统性能。如果在进入循环之前恰巧关闭了中断,jiffies的值不会得到更新,导致循环的条件一直为真。

书中所说/proc/jitbusy的模块,我在3.10的内核中并未找到。
将上述代码的cpu_relax()函数替换为schedule(),可以在延时的过程中将CPU让出;但是不能保证进程在之后能够重新运行。
同样地,/proc/jitsched也没有找到。
 
要实现高效的延时,还需要内核的帮助,跟阻塞的I/O调用的函数相似:
1 #include <linux/wait.h>
2 long wait_event_timeout(wait_queue_head_t q, condition, long timeout);
3 long wait_event_interruptible_timeout(wait_queue_head_t q,
4                                 condition, long timeout);

一种典型的使用方法如下:

1 set_current_state(TASK_INTERRUPTTABLE);
2 schedule_timeout(delay);

 

内核提供了函数进行短时延时函数:
1 #include <linux/delay.h>
2 void ndelay(unsigned long nsecs);
3 void udelay(unsigned long usecs);
4 void mdelay(unsigned long msecs);

具体实现在asm/delay.h,体系结构相关。三个延时函数都是忙等待。

不是忙等待的函数如下:
1 void msleep(unsigned int millisecs);
2 unsigned long msleep_interruptible(unsigned int millisecs);
3 void ssleep(unsigned int seconds);

不带有interruptible的函数不可中断,一定能够睡眠足够的时间。

 
Kernel Timers

内核计时器可以在不阻塞当前进程的情况下在某个时间点(时钟周期)执行特定的参数。内核计数器定义在linux/timer.h,实现在kernel/timer.c中。
被调度执行的函数和其注册函数通常是异步执行的。许多操作需要进程的上下文才能执行,处于进程上下文之外时(例如在中断上下文中),必须遵守以下规则:
  1. 不能访问用户空间——没有进程上下文就没有访问用户空间的路径
  2. current指针在原子模式下没有意义——current和被中断的进程没有关系
  3. 不能休眠或者直接间接调用调度函数,例如调用wait_event,kmalloc,以及信号量
内核的代码可以调用in_interrupt()函数判断当前是否运行在中断上下文中,是的话返回0。in_atomic()可用来判断是否能够进行调度——可能是硬件或者软件中断上下文中,也可能持有自选锁。后者current指针可能有效,但不能访问用户空间——可能导致调度。两个函数都定义在asm/hardirq.h。
内核计数器的一个特点:一个进程可以重新注册自己在之后的时间再次执行。这是因为timer可以重新注册为活跃状态(timer_list)。在SMP系统上,计时函数和注册其的函数执行在相同的CPU,以此实现更好地cache局部性。因此,重新注册后运行在相同的CPU。还有,即使在单处理器系统上,计时器也会引入竞争——因为他们总是和其他代码异步执行。因此,计时函数并行访问的所有数据结构都必须通过自选锁或原子类型进行保护。
内核提供的操作计时器的代码:
 1 #include <linux/timer.h>
 2 struct timer_list {
 3     /* ... */
 4     unsigned long expires;
 5     void (*function)(unsigned long);
 6     unsigned long data;
 7 };
 8 void init_timer(struct timer_list *timer);
 9 struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
10 void add_timer(struct timer_list * timer);
11 int del_timer(struct timer_list * timer);

计时器在expires指明的jiffies后以data作为参数执行function,如果要传入多个参数,可以将其转化为指针。

内核提供的其他接口:
1 int mod_timer(struct timer_list *timer, unsigned long expires);
2 int del_timer_sync(struct timer_list *timer);
3 int timer_pending(const struct timer_list * timer);

mod_timer可以修改活跃的或者非活跃的计时器。del_timer_sync保证在返回时,计时器不会在任何CPU上运行;这个函数还可以避免SMP系统的竞争条件,和UP内核上的del_timer相同;持有锁的时侯调用del_timer_sync函数要十分小心,如果计时函数也试图获得相同的锁,系统可能会死锁。timer_pending用来判断计时器当前受否被调度来执行。

 
内核实现计时器时遵循以下要求和假设:
  1. 计时器的管理必须尽可能轻量
  2. 活跃的计时器增加时design should scale well
  3. 绝大多数计时器会在几秒或至多几分钟内失效,更长时间的计时器十分罕见
  4. 计时器运行在注册其上的CPU
计时器实现在一个per-CPU数据结构上,timer_list数据结构包含一个指向该数据结构的指针base。如果base为空,表明这个计时器没有调度来运行;否则这个指针表明运行这个计时器的数据结构。
内核代码注册一个计时器时(mod_timer、add_timer),最终会调用internal_add_timer(实现在kernel/timer.c)将新的计时器加入到一个和当前CPU相关的级联表(cascading table)内的双向链表中。
级联表工作机制如下:如果计时器在0到255个jiffies内失效,将其加入到根据expires的低8位确定的256个表中;类似的还有4个表(9-14,15-20,21-26,27-31)。如果计时器超时的时间更长,将其映射到0xffffffff。
调用__run_timers时,会执行所有当前计时刻度的所有待定的计时器。如果当前的jiffies是256的整数倍,将expires值更大的表重新映射。这种实现方法管理每个活跃的计时器时所需的时间和已经注册的计时器的数量无关,只是需要花费4KB的内存空间来保存512个(256+64*4书中之前的说法27-31只有32项,和此处相矛盾,综合来看应该以此处为准0xffffffff)表头。
__run_timers运行在原子上下文,有一个特点:即使在可抢占内核CPU在内核空间很忙,计时器也能在正确的时间失效。需要注意的是,虽然计时器能够胜任一些简单的工作,但是对于工业环境中的生产系统,最好还是使用实时系统的内核。
 
Tasklets

任务集通常用来管理中断。任务集和其调度程序运行在相同的CPU,接收一个unsinged long型的参数;但是无法指定执行代码的具体时间——内核会选择执行的时间。和内核计时器类似,两者都是执行在软中断上下文中。
内核提供的创建任务集的接口:
 1 #include <linux/interrupt.h>
 2 struct tasklet_struct {
 3     /* ... */
 4     void (*func)(unsigned long);
 5     unsigned long data;
 6 };
 7 void tasklet_init(struct tasklet_struct *t,
 8 void (*func)(unsigned long), unsigned long data);
 9 DECLARE_TASKLET(name, func, data);
10 DECLARE_TASKLET_DISABLED(name, func, data);

任务集具有以下特点:

  1. 可以启用或者关闭,但是只有启用的次数和关闭的次数一样多时才会执行
  2. 可以重新注册自己
  3. 可以以高优先级或者普通优先级调度执行。前者通常先执行
  4. 在负载较低的系统上任务集可能会立刻执行,但不会晚于下一个计时器刻度
  5. 多个任务集可以并发,但是一个任务集只会运行在注册其的CPU上
内核提供的其他接口如下:
1 void tasklet_disable(struct tasklet_struct *t);
2 void tasklet_disable_nosync(struct tasklet_struct *t);
3 void tasklet_enable(struct tasklet_struct *t);
4 void tasklet_schedule(struct tasklet_struct *t);
5 void tasklet_hi_schedule(struct tasklet_struct *t);
6 void tasklet_kill(struct tasklet_struct *t);

tasklet_disable关闭给定的任务集,仍然能够通过tasklet_schedule调度,但是直到开启之后才会执行。如果任务集当前正在执行,这个函数会忙等待直到任务集退出;因此,调用此函数之后,可以保证此任务集不会在系统中执行。有nosync的函数不会等正在执行的任务集退出才返回;因此此函数返回时任务集可能还在运行。

tasklet_enable开启给定的任务集,如果任务集已经被调度,会立刻执行。此函数调用的次数必须和关闭函数的调用次数相匹配——内核会记录每个任务集的关闭次数。

tasklet_schedule调度给定的任务集来执行,如果一个任务集在执行之前又被调度,只会执行一次。如果任务集在执行的时候被调度,在执行完成后会再次执行;这样才能保证和其他事件同时发生的事件得到应有的关注,同样保证一个任务集能够重新调度自己。hi的函数可以以高优先级调度任务集。

tasklet_kill保证给定的任务集不会再被调度,如果任务集被调度执行,此函数会等待其完成。在调用此函数前,任务集必须阻止其重新调度自己。

任务集实现在kernel/softirq.c中,两个任务集列表(正常和高优先级)都声明为一个per-CPU数据结构。由于不需要排序,管理任务集的数据机构是一个简单地链表。
 
Workqueues

工作队列和任务集表面上类似,但是有以下不同:
  1. 任务集的代码运行在软中断上下文中,必须是原子的;而工作队列运行在特殊的内核进程中,因此更零活,可以休眠
  2. 任务集通常运行在注册其的处理器,工作队列默认也是如此
  3. 内核代码可以要求工作队列的函数推迟一段明确的时间
  4. 最关键的是,由于任务集是原子的,执行速度更快;而工作队列不需要是原子的,延迟可能更高
工作队列通过struct workqueue_struct描述,定义在linux/workqueue.h,创建函数如下:
1 struct workqueue_struct *create_workqueue(const char *name);
2 struct workqueue_struct *create_singlethread_workqueue(const char *name);
每个工作队列都有一个或多个专有的进程(内核线程)来执行提交给这个队列的函数。调用create_workqueue会在系统中的每个处理器上创建一个线程作为worker,而create_singlethread_workqueue只会创建一个线程。
要提交一个任务到工作队列,首先需要填充一个work_struct结构。下列代码可以在编译时完成:
1 DECLARE_WORK(name, void (*function)(void *), void *data);
name是需要声明的数据结构的名称,function是要调用的函数,data是要传递的参数。若要在运行时创建,可以通过下列代码:
1 INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
2 PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data);

初次创建时要调用INIT_WORK,PREPARE_WORK功能类似,但是不会初始化将work_sturct指向工作队列的指针。如果要对数据结构进行修改,采用后者。

下列函数可以将work_struct提交给工作队列:
1 int queue_work(struct workqueue_struct *queue, struct work_struct *work);
2 int queue_delayed_work(struct workqueue_struct *queue,
3                        struct work_struct *work, unsigned long delay);

queue_delayed_work直到delay指明的jiffies后才会执行work。function会在worker线程的上下文中运行,因此可以睡眠。f由于worker线程位于内核空间,unction无法访问用户空间。要取消未完成的工作队列项:

1 int cancel_delayed_work(struct work_struct *work);
如果工作在执行前已经取消,函数返回非零值;如果返回0,工作可能已经在不同的处理器上运行。如果要保证返回0时工作不再执行,在cancel_delayed_work后调用
1 void flush_workqueue(struct workqueue_struct *queue);
若要销毁工作队列
1 void destroy_workqueue(struct workqueue_struct *queue);

 

很多情况下,设备驱动不需要有自己的工作队列,如果只是偶尔向工作队列提交任务,可以使用效率更高的内核提供的共享工作队列。相关的函数
1 int schedule_work(struct work_struct *work);
2 int schedule_delayed_work(struct work_struct *work, unsigned long delay);
3 void flush_scheduled_work(void);

 

 
posted @ 2018-11-14 09:37  glob  阅读(164)  评论(0编辑  收藏  举报