之前我们学会了如何编写一个字符设备,并对其中的一些重要操作进行了说明。对于一个完整的设备而已,可能还有许多工作要做。
本节我们将要说一下内核中是如何对时间问题进行操作的。
本节主要涉及到以下内容:
- 内核中的时间描述;
- 如何获取当前时间;
- 如何进行延时操作;
1. 内核中的时间描述
在内核中,系统定时硬件以周期性的间隔产生中断,内核通过这个中断来跟踪时间流。
在内核中,上面的中断间隔是由 HZ
常数来决定的,该常数是与体系结构相关的,定义在 <linux/param.h>
中(或者在其中的某个子文件中)。HZ的含义是一秒内产生的中断数。如果需要,用户可以修改这个变量,修改完后需要重新编译整个内核以及模块,不过,不建议用户对该常数进行修改。
内核中使用一个变量来作为中断计数器,系统启动时,该计数器清零;上面的中断发生时,计数器的值加1,因此这个计数器记录了系统启动以来的中断数(也称为时钟滴答数)。这个变量称为 jiffies_64
,是一个64位的变量(即使在32位系统中也是64位的),而我们在编写驱动过程中,常用的变量名称为 jiffies
,该变量是一个 unsigned long
型变量,其值要么就是 jiffies_64
,要么就是jiffies_64的低32位。以上计数器变量及其相关操作定义在 <linux/jiffies.h>
文件中。
通过以上 jiffies
变量和 HZ
常数,就可以知道或定义一些时间:
#include <linux/jiffies.h>
unsigned long j = jiffies;
unsigned long stamp_1 = j + HZ; /* 未来1秒 */
unsigned long stamp_half = j + HZ / 2; /* 未来半秒 */
unsigned long stamp_n = j + n * HZ / 1000; /* 未来n毫秒 */
上面对于 jiffies
可以直接读取,但对于 jiffies_64
变量读取是非原子的,是不可靠的,如果需要使用 jiffies_64
变量,我们可以使用以下辅助函数:
#include <linux/jiffies.h>
u64 get_jiffies_64(void);
如果两个 unsigned long
变量表示获取到的 jiffies
时间,则我们可以通过比较其大小来判断时间先后(较大时间靠后),但我们还需要考虑时间过长而溢出问题(概率很低),因此,我们最好使用内核提供的宏来进行比较:
#include <linux/jiffies.h>
int time_after(unsigned long a, unsigned log b); /* 判断a时间是否比b时间靠后 */
int time_before(unsigned long a, unsigned log b); /* 判断a时间是否比b时间靠前 */
int time_after_eq(unsigned long a, unsigned log b); /* 判断a时间是否比b时间靠后或相等 */
int time_before_eq(unsigned long a, unsigned log b); /* 判断a时间是否比b时间靠前或相等 */
上面说了内核空间的时间描述。
在用户空间中,用于描述时间的变量是 struct timeval
(较老,包含秒和毫秒)和 struct timespec
(较新,包含秒和纳秒)。
如果需要对两个空间的时间描述,可以使用内核提供的函数进行转换:
#include <linux/time.h>
unsigned long timespec_to_jiffies(struct timespec *value);
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value);
void jiffies_to_timeval(unsgined long jiffies, struct timeval *value);
上面的函数看名字和变量就知道其含义和使用方法,这里就不再描述了。
2. 获取当前时间
通过读取 jiffies
变量,我们就可以获取当前时间(系统启动后经历的时间)。
但有时候我们也需要处理绝对时间戳,因此,内核中导出了以下两个函数来获取绝对时间:
#include <linux/time.h>
void do_gettimeofday(struct timeval *tv);
struct timespec current_kernel_time(void);
3. 延迟执行
设备驱动程序中经常在执行某个操作后需要等待一段时间,让硬件做后某些任务后再继续执行。
对于时间较长的延时(大于一个滴答时钟),可以使用等待队列的方式实现,等待队列的使用可以查看《休眠与唤醒》 :
wait_queue_head_t wait;
init_waitqueue_head(&wait);
wait_event_interrupt_timeout(wait, 0, delay);
上面的 condition
设为0,因为我们并不是在等待某个特定的事件;delay
是超时的时间(为 jiffies
数量,而不是绝对时间)。因此,上面的代码会进入休眠,等待指定的 jiffies
数量后继续执行。
为了使用超时功能而定义了等待队里头,这是多余的,因为我们并不需要他。为了避免定义多余的变量,内核提供了以下方法实现延时:
#include <linux/sched.h>
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(delay);
通过 set_current_state
来设置当前进程的状态(如果是不可中断的设置为 TASK_UNINTERRUPTIBLE
),这样调度器超时后将其设置为TASK_RUNNING,该进程才会继续执行;如果没有设置进程状态,则进程状态一直都是 TASK_RUNNING
,此时后面执行 schedule_timeout
时,其效果等效于 schedule
,延时不会起作用。
对于短延迟,内核提供了以下几个函数来完成:
#include <linux/delay.h>
void ndelay(unsigned long nscs); /* 延迟指定的纳秒 */
void udelay(unsigned long usecs); /* 延迟指定的微秒 */
void mdelay(unsigned long msecs); /* 延迟指定的毫秒 */
这些函数的实现是与具体架构相关的,所有的架构都实现了 udelay
,其他函数可能没有定义, <linux/delay.h>
会在 udelay
的基础上提供默认的未定义的其他函数。
需要知道的是,以上延迟并不是说精确延时指定的时间,而是至少延迟指定的时间,可能会更长。
虽然输入的参数均为 unsigned long
类型的,但一般性的规则是只用在其指定的量级上,即上千纳秒应该使用 udelay
,而上千微秒则应使用 mdelay
,而不是输入一个很大的值。
以上的延迟都是忙等待函数,因此在延迟期间不能执行其他任务。
对于毫秒级以上的延迟,内核还提供了一种非忙等待的实现:
#include <linux/delay.h>
void msleep(unsigned int millisecs); /* 延时等地指定毫秒 */
unsigned long msleep_interruptible(unsgined int millisecs); /* 返回剩余毫秒数,一般为0 */
void ssleep(unsgined int seconds); /* 延时等待指定秒 */
以上介绍了内核时间的概念、如何获取内核时间以及在当前线程中的延时操作。下一节继续说如何进行异步延时操作。