Linux驱动---时间、延迟及延缓操作(转)

度量时间差

 

时钟中断由系统定时硬件以周期性的间隔产生,这个间隔由内核根据 HZ 值来设定,HZ 是一个体系依赖的值,在 <linux/param.h>中定义或该文件包含的某个子平台相关文件中。作为通用的规则,即便如果知道 HZ 的值,在编程时应当不依赖这个特定值,而始终使用HZ。对于当前版本,我们应完全信任内核开发者,他们已经选择了最适合的HZ值,最好保持 HZ 的默认值。

对用户空间,内核HZ几乎完全隐藏,用户 HZ 始终扩展为 100。当用户空间程序包含 param.h,且每个报告给用户空间的计数器都做了相应转换。对用户来说确切的 HZ 值只能通过 /proc/interrupts 获得:/proc/interrupts 的计数值除以 /proc/uptime 中报告的系统运行时间。

 

对于ARM体系结构:在<linux/param.h>文件中的定义如下:

#ifdef __KERNEL__
# define HZ         CONFIG_HZ    
# define USER_HZ     100        
# define CLOCKS_PER_SEC    (USER_HZ)    
#else
# define HZ         100
#endif

也就是说:HZ 由__KERNEL__和CONFIG_HZ决定。若未定义__KERNEL__,HZ为100;否则为CONFIG_HZ。而CONFIG_HZ是在内核的根目录的.config文件中定义,并没有在make menuconfig的配置选项中出现。Linux的/arch/arm/configs/s3c2410_defconfig文件中的定义为:

#
# Kernel Features
#
# CONFIG_PREEMPT is not set
# CONFIG_NO_IDLE_HZ is not set
CONFIG_HZ=200
# CONFIG_AEABI is not set
# CONFIG_ARCH_DISCONTIGMEM_ENABLE is not set

所以正常情况下s3c24x0的HZ为200。这一数值在后面的实验中可以证实。

每次发生一个时钟中断,内核内部计数器的值就加一。这个计数器在系统启动时初始化为 0,因此它代表本次系统启动以来的时钟嘀哒数。这个计数器是一个 64-位 变量( 即便在 32-位的体系上)并且称为 “jiffies_64”。但是驱动通常访问 jiffies 变量(unsigned long)(根据体系结构的不同:可能是 jiffies_64 ,可能是jiffies_64 的低32位)。使用 jiffies 是首选,因为它访问更快,且无需在所有的体系上实现原子地访问 64-位的 jiffies_64 值。

使用 jiffies 计数器

这个计数器和用来读取它的工具函数包含在 <linux/jiffies.h>, 通常只需包含 <linux/sched.h>,它会自动放入 jiffies.h 。 jiffies 和 jiffies_64 必须被当作只读变量。当需要记录当前 jiffies 值(被声明为 volatile 避免编译器优化内存读)时,可以简单地访问这个 unsigned long 变量,如:

 

#include <linux/jiffies.h>
unsigned long j, stamp_1, stamp_half, stamp_n;

j = jiffies;
stamp_1 = j + HZ;
stamp_half = j + HZ/2;
stamp_n = j + n * HZ / 1000;

 

以下是一些简单的工具宏及其定义:

 

#define time_after(a,b)         /
    (typecheck(unsigned long, a) && /
     typecheck(unsigned long, b) && /
     ((long)(b) - (long)(a) < 0))
#define time_before(a,b)     time_after(b,a)
#define time_after_eq(a,b)     /
    (typecheck(unsigned long, a) && /
     typecheck(unsigned long, b) && /
     ((long)(a) - (long)(b) >= 0))
#define time_before_eq(a,b)     time_after_eq(b,a)

 

用户空间的时间表述法(struct tim 和 struct timespec )与内核表述法的转换函数:

 

#include <linux/time.h>

struct timespec {
    time_t     tv_sec;        
    long     tv_nsec;    
};
#endif

struct tim {
    time_t         tv_sec;        
     suseconds_t     tv_usec;    
};

unsigned long timespec_to_jiffies(struct timespec *value);
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long tim_to_jiffies(struct tim *value);
void jiffies_to_tim(unsigned long jiffies, struct tim *value);

 

访问jiffies_64 对于 32-位处理器不是原子的,这意味着如果这个变量在你正在读取它们时被更新你可能读到错误的值。若需要访问jiffies_64,内核有一个特别的辅助函数,为你完成适当的锁定:

 

#include <linux/jiffies.h>
u64 get_jiffies_64(void);

 

处理器特定的寄存器

若需测量非常短时间间隔或需非常高的精度,可以借助平台依赖的资源。许多现代处理器包含一个随时钟周期不断递增的计数寄存器,他是进行高精度的时间管理任务唯一可靠的方法。最有名的计数器寄存器是 TSC ( timestamp counter), 在 x86 的 Pentium 处理器开始引入并在之后所有的 CPU 中出现(包括 x86_64 平台)。它是一个 64-位 寄存器,计数 CPU 的时钟周期,可从内核和用户空间读取。在包含了 <asm/msr.h> (一个 x86-特定的头文件, 它的名子代表"machine-specific registers")的代码中可使用这些宏:

 

 

rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var64);


unsigned long ini, end;
rdtscl(ini); rdtscl(end);
printk("time lapse: %li/n", end - ini);

 

一些其他的平台提供相似的功能, 并且内核头文件提供一个体系无关的功能用来代替 rdtsc,称 get_cycles(定义在 <asm/timex.h>( 由 <linux/timex.h> 包含)),原型如下:

 

#include <linux/timex.h>
cycles_t get_cycles(void);



typedef unsigned long cycles_t;

static inline cycles_t get_cycles (void)
{
    return 0;
}

 

 


 

 

获取当前时间

驱动一般无需知道时钟时间(用年月日、小时、分钟、秒来表达的时间),只对用户程序才需要,如 cron 和 syslogd。 内核提供了一个将时钟时间转变为秒数值的函数:

 

 

unsigned long
mktime(const unsigned int year0, const unsigned int mon0,
       const unsigned int day, const unsigned int hour,
       const unsigned int min, const unsigned int sec)
{
    unsigned int mon = mon0, year = year0;

    
    if (0 >= (int) (mon -= 2)) {
         mon += 12;    
         year -= 1;
    }

    return ((((unsigned long)
         (year/4 - year/100 + year/400 + 367*mon/12 + day) +
         year*365 - 719499
     )*24 + hour
     )*60 + min
    )*60 + sec;
}

 

为了处理绝对时间, <linux/time.h> 导出了 do_gettimeofday 函数,它填充一个指向 struct tim 的指针变量。绝对时间也可来自 xtime 变量,一个 struct timespec 值,为了原子地访问它,内核提供了函数 current_kernel_time。它们的精确度由硬件决定,原型是:

 

#include <linux/time.h>
void do_gettimeofday(struct tim *tv);
struct timespec current_kernel_time(void);

 

 

以上两个函数在ARM平台都是通过 xtime 变量得到数据的。

 

 


全局变量xtime:它是一个tim结构类型的变量,用来表示当前时间距UNIX时间基准1970-01-01 00:00:00的相对秒数值。

 

结构tim是Linux内核表示时间的一种格式(Linux内核对时间的表示有多种格式,每种格式都有不同的时间精度),其时间精度是微秒。该结构是内核表示时间时最常用的一种格式,它定义在头文件include/linux/time.h中,如下所示:

struct tim {

time_t tv_sec;

suseconds_t tv_usec;

};

其中,成员tv_sec表示当前时间距UNIX时间基准的秒数值,而成员tv_usec则表示一秒之内的微秒值,且1000000>tv_usec>=0。

 

Linux内核通过tim结构类型的全局变量xtime来维持当前时间,该变量定义在kernel/timer.c文件中,如下所示:

 

volatile struct tim xtime __attribute__ ((aligned (16)));

但是,全局变量xtime所维持的当前时间通常是供用户来检索和设置的,而其他内核模块通常很少使用它(其他内核模块用得最多的是jiffies),因此对xtime的更新并不是一项紧迫的任务,所以这一工作通常被延迟到时钟中断的底半部(bottom half)中来进行。由于bottom half的执行时间带有不确定性,因此为了记住内核上一次更新xtime是什么时候,Linux内核定义了一个类似于jiffies的全局变量wall_jiffies,来保存内核上一次更新xtime时的jiffies值。时钟中断的底半部分每一次更新xtime的时侯都会将wall_jiffies更新为当时的jiffies值。全局变量wall_jiffies定义在kernel/timer.c文件中:

 

unsigned long wall_jiffies;

延迟执行

设备驱动常常需要延后一段时间执行一个特定片段的代码, 常常允许硬件完成某个任务.

长延迟

有时,驱动需要延后执行相对长时间,长于一个时钟嘀哒。

忙等待(尽量别用)

若想延迟执行若干个时钟嘀哒,精度要求不高。最容易的( 尽管不推荐 ) 实现是一个监视 jiffy 计数器的循环。这种忙等待实现的代码如下:

 

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

 

对 cpu_relex 的调用将以体系相关的方式执行,在许多系统中它根本不做任何事,这个方法应当明确地避免。对于ARM体系来说:

 

#define cpu_relax()             barrier()

 

也就是说在ARM上运行忙等待相当于:

while (time_before(jiffies, j1)) ;

这种忙等待严重地降低了系统性能。如果未配置内核为抢占式, 这个循环在延时期间完全锁住了处理器,计算机直到时间 j1 到时会完全死掉。如果运行一个可抢占的内核时会改善一点,但是忙等待在可抢占系统中仍然是浪费资源的。更糟的是, 当进入循环时如果中断碰巧被禁止, jiffies 将不会被更新, 并且 while 条件永远保持真,运行一个抢占的内核也不会有帮助, 唯一的解决方法是重启。

让出处理器

忙等待加重了系统负载,必须找出一个更好的技术:不需要CPU时释放CPU 。 这可通过调用schedule函数实现(在 <linux/sched.h> 中声明):

while (time_before(jiffies, j1)) {
     schedule();
}

在计算机空闲时运行空闲任务(进程号 0, 由于历史原因也称为swapper)可减轻处理器工作负载、降低温度、增加寿命。

超时

实现延迟的最好方法应该是让内核为我们完成相应的工作。

(1)若驱动使用一个等待队列来等待某些其他事件,并想确保它在一个特定时间段内运行,可使用:

 

#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);

 

(2)为了实现进程在超时到期时被唤醒而又不等待特定事件(避免声明和使用一个多余的等待队列头),内核提供了 schedule_timeout 函数:

 

#include <linux/sched.h>
signed long schedule_timeout(signed long timeout);


set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (delay);

短延迟

当一个设备驱动需要处理硬件的延迟(latency潜伏期), 涉及到的延时通常最多几个毫秒,在这个情况下, 不应依靠时钟嘀哒,而是内核函数 ndelay, udelay和 mdelay ,他们分别延后执行指定的纳秒数, 微秒数或者毫秒数,定义在 <asm/delay.h>,原型如下:

#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

重要的是记住这 3 个延时函数是忙等待; 其他任务在时间流失时不能运行。每个体系都实现 udelay, 但是其他的函数可能未定义; 如果它们没有定义, <linux/delay.h> 提供一个缺省的基于 udelay 的版本。在所有的情况中, 获得的延时至少是要求的值, 但可能更多。udelay 的实现使用一个软件循环, 它基于在启动时计算的处理器速度和使用整数变量 loos_per_jiffy确定循环次数。

为避免在循环计算中整数溢出, 传递给udelay 和 ndelay的值有一个上限,如果你的模块无法加载和显示一个未解决的符号:__bad_udelay, 这意味着你调用 udleay时使用太大的参数。
作为一个通用的规则:若试图延时几千纳秒, 应使用 udelay 而不是 ndelay; 类似地, 毫秒规模的延时应当使用 mdelay 完成而不是一个更细粒度的函数。

有另一个方法获得毫秒(和更长)延时而不用涉及到忙等待的方法是使用以下函数(在<linux/delay.h> 中声明):

 

void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds)

 

若能够容忍比请求的更长的延时,应使用 schedule_timeout, msleep 或 ssleep。

 


 

 

内核定时器

当需要调度一个以后发生的动作, 而在到达该时间点时不阻塞当前进程, 则可使用内核定时器。内核定时器用来调度一个函数在将来一个特定的时间(基于时钟嘀哒)执行,从而可完成各类任务。
内核定时器是一个数据结构, 它告诉内核在一个用户定义的时间点使用用户定义的参数执行一个用户定义的函数,函数位于 <linux/timer.h> 和 kernel/timer.c 。被调度运行的函数几乎确定不会在注册它们的进程在运行时运行,而是异步运行。实际上, 内核定时器通常被作为一个"软件中断"的结果而实现。当在进程上下文之外(即在中断上下文)中运行程序时, 必须遵守下列规则:

(1)不允许访问用户空间;
(2)current 指针在原子态没有意义;
(3)不能进行睡眠或者调度. 例如:调用 kmalloc(..., GFP_KERNEL) 是非法的,信号量也不能使用因为它们可能睡眠。


通过调用函数 in_interrupt()能够告知是否它在中断上下文中运行,无需参数并如果处理器当前在中断上下文运行就返回非零。
通过调用函数 in_atomic()能够告知调度是否被禁止,若调度被禁止返回非零; 调度被禁止包含硬件和软件中断上下文以及任何持有自旋锁的时候。

在后一种情况, current 可能是有效的,但是访问用户空间是被禁止的,因为它能导致调度发生. 当使用 in_interrupt()时,都应考虑是否真正该使用的是 in_atomic 。他们都在 <asm/hardirq.h> 中声明。

内核定时器的另一个重要特性是任务可以注册它本身在后面时间重新运行,因为每个 timer_list 结构都会在运行前从激活的定时器链表中去连接,因此能够立即链入其他的链表。一个重新注册它自己的定时器一直运行在同一个 CPU.

即便在一个单处理器系统,定时器是一个潜在的态源,这是异步运行直接结果。因此任何被定时器函数访问的数据结构应当通过原子类型或自旋锁被保护,避免并发访问

定时器 API

内核提供给驱动许多函数来声明、注册以及删除内核定时器:

 


#include <linux/timer.h>
struct timer_list {
    struct list_head entry;
    unsigned long expires;
    void (*function)(unsigned long);
    unsigned long data;
    struct tvec_t_base_s *base;
#ifdef CONFIG_TIMER_STATS
    void *start_site;
    char start_comm[16];
    int start_pid;
#endif
};

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);
int del_timer_sync(struct timer_list *timer);
int mod_timer(struct timer_list *timer, unsigned long expires);
int timer_pending(const struct timer_list * timer);

 

内核定时器的实现《LDD3》介绍的比较笼统,以后看《ULK3》的时候再细细研究。

一个内核定时器还远未完善,因为它受到 jitter 、硬件中断,还有其他定时器和其他异步任务的影响。虽然一个简单数字 I/O关联的定时器对简单任务是足够的,但不合适在工业环境中的生产系统,对于这样的任务,你将最可能需要实时内核扩展(RT-Linux).

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值