找回密码
 注册
关于网站域名变更的通知
查看: 369|回复: 1
打印 上一主题 下一主题

Linux内核设计与实现之定时器和时间管理

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2020-12-24 15:44 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式

EDA365欢迎您登录!

您需要 登录 才可以下载或查看,没有帐号?注册

x
本帖最后由 pulbieup 于 2020-12-24 15:55 编辑 5 C9 ^3 w  [8 V+ q0 n6 a8 \
( g8 O! G6 j7 h7 o
系统中有很多与时间相关的程序(比如定期执行的任务,某一时间执行的任务,推迟一段时间执行的任务),因此,时间的管理对于linux来说非常重要。
8 j$ \4 S9 n# W( i/ A
主要内容:
  • 系统时间
  • 定时器
  • 定时器相关概念
  • 定时器执行流程
  • 实现程序延迟的方法
  • 定时器和延迟的例子, ]: r) n! S! o! \

- s1 l$ l/ a+ m7 L% A$ j1. 系统时间
系统中管理的时间有2种:实际时间和定时器。
1.1  实际时间
实际时间就是现实中钟表上显示的时间,其实内核中并不常用这个时间,主要是用户空间的程序有时需要获取当前时间,
所以内核中也管理着这个时间。
, e9 }+ @2 M, r
实际时间的获取是在开机后,内核初始化时从RTC读取的。
内核读取这个时间后就将其放入内核中的 xtime 变量中,并且在系统的运行中不断更新这个值。
注:RTC就是实时时钟的缩写,它是用来存放系统时间的设备。一般和BIOS一样,由主板上的电池供电的,所以即使关机也可将时间保存。

2 I% t: \7 V, {8 w7 Q- _( Z0 D% K9 H
实际时间存放的变量 xtime 在文件 kernel/time/timekeeping.c中。
+ F" u7 Y) h) v/ r) y" T% @1 {1 E
  • /* 按照16位对齐,其实就是2个long型的数据 */
  • struct timespec xtime __attribute__ ((aligned (16)));
  • /* timespec结构体的定义如下, 参考 <linux/time.h>  */
  • struct timespec {
  •     __kernel_time_t    tv_sec;            /* seconds */
  • long        tv_nsec;        /* nanoseconds */
  • };
  • /* _kernel_time_t 定义如下 */
  • typedef long        __kernel_time_t;
    4 G" G1 i& {( V- t- N) ]* j

2 [0 u0 @* z3 r( h+ b* f& J
6 D9 Q  |9 f* ]3 g1 U* E
- r. [+ v' P* L8 e6 q$ t
系统读写 xtime 时用的就是顺序锁。
7 e4 `( ]9 [0 T$ Q/ I2 H9 `
  • /* 写入 xtime 参考 do_sometimeofday 方法 */
  • int do_settimeofday(struct timespec *tv)
  • {
  • /* 省略 。。。。 */
  •     write_seqlock_irqsave(&xtime_lock, flags); /* 获取写锁 */
  • /* 更新 xtime */
  •     write_sequnlock_irqrestore(&xtime_lock, flags); /* 释放写锁 */
  • /* 省略 。。。。 */
  •     return 0;
  • }
  • void getnstimeofday(struct timespec *ts)
  • /* 读取 xtime 参考 do_gettimeofday 方法 */
  • void do_gettimeofday(struct timeval *tv)
  • {
  •     struct timespec now;
  •     getnstimeofday(&now); /* 就是在这个方法中获取读锁,并读取 xtime */
  •     tv->tv_sec = now.tv_sec;
  •     tv->tv_usec = now.tv_nsec/1000;
  • }
  • {
  • /* 省略 。。。。 */
  • /* 顺序锁中读锁来循环获取 xtime,直至读取过程中 xtime 没有被改变过 */
  •     do {
  •         seq = read_seqbegin(&xtime_lock);
  •         *ts = xtime;
  •         nsecs = timekeeping_get_ns();
  •         /* If arch requires, add in gettimeoffset() */
  •         nsecs += arch_gettimeoffset();
  •     } while (read_seqretry(&xtime_lock, seq));
  • /* 省略 。。。。 */
  • }
    ) r# {$ M2 [& ~3 N. }8 D
1 F( o: E1 k* f9 i6 y3 B3 {

) {# r: O9 k! C# q
上述场景中,写锁必须要优先于读锁(因为 xtime 必须及时更新),而且写锁的使用者很少(一般只有系统定期更新xtime的线程需要持有这个锁)。
这正是 顺序锁的应用场景。

+ h+ r# v3 i% v9 O7 s1.2 定时器
定时器是内核中主要使用的时间管理方法,通过定时器,可以有效的调度程序的执行。
动态定时器是内核中使用比较多的定时器,下面重点讨论的也是动态定时器。
4 h. [" P) M& m2 ?% B, ^
2. 定时器
内核中的定时器有2种,静态定时器和动态定时器。
静态定时器一般执行了一些周期性的固定工作:
  • 更新系统运行时间
  • 更新实际时间
  • 在SMP系统上,平衡各个处理器上的运行队列
  • 检查当前进程是否用尽了自己的时间片,如果用尽,需要重新调度。
  • 更新资源消耗和处理器时间统计值9 R) T- [% R: q
; G% Q; b3 b& \. g; y
动态定时器顾名思义,是在需要时(一般是推迟程序执行)动态创建的定时器,使用后销毁(一般都是只用一次)。
一般我们在内核代码中使用的定时器基本都是动态定时器,下面重点讨论动态定时器相关的概念和使用方法。
7 k6 t2 B1 m8 L" q
3. 定时器相关概念
定时器的使用中,下面3个概念非常重要:
  • HZ
  • jiffies
  • 时间中断处理程序1 e8 w, ^- s  ?5 s! a
7 Z  {; t9 j7 C; r3 z
3.1 HZ
节拍率(HZ)是时钟中断的频率,表示的一秒内时钟中断的次数。
比如 HZ=100 表示一秒内触发100次时钟中断程序。

  `$ S7 i: P3 E8 ]
HZ的值一般与体系结构有关,x86 体系结构一般定义为 100,参考文件 include/asm-generic/param.h
HZ值的大小的设置过程其实就是平衡 精度和性能 的过程,并不是HZ值越高越好。
HZ值
优势
劣势
高HZ时钟中断程序运行的更加频繁,依赖时间执行的程序更加精确, 1 }; o; ], ^6 V. X7 B
对资源消耗和系统运行时间的统计更加精确。
时钟中断执行的频繁,增加系统负担 0 S4 r7 W8 }! a8 ?
时钟中断占用的CPU时间过多

) d% P2 i0 S' [4 u; s* J3 N
此外,有一点需要注意,内核中使用的HZ可能和用户空间中定义的HZ值不一致,为了避免用户空间取得错误的时间,
内核中也定义了 USER_HZ,即用户空间使用的HZ值。
一般来说,USER_HZ 和 HZ 都是相差整数倍,内核中通过函数 jiffies_to_clock_t 来将内核来将内核中的 jiffies转为 用户空间 jiffies
- C9 n, r6 k( ]" ^7 D% ]
  • /* 参见文件: kernel/time.c  *
  • //*
  • * Convert jiffies/jiffies_64 to clock_t and back.
  • */
  • clock_t jiffies_to_clock_t(unsigned long x)
  • {
  • #if (TICK_NSEC % (NSEC_PER_SEC / USER_HZ)) == 0
  • # if HZ < USER_HZ
  •     return x * (USER_HZ / HZ);
  • # else
  •     return x / (HZ / USER_HZ);
  • # endif
  • #else
  •     return div_u64((u64)x * TICK_NSEC, NSEC_PER_SEC / USER_HZ);
  • #endif
  • }
  • EXPORT_SYMBOL(jiffies_to_clock_t);( N4 D0 L+ E: ~# x7 d' u
" y3 j- j" d" Y/ S) X& o0 E1 R

) ^4 [* T; y* T, F) `6 P- X3.2 jiffies
jiffies用来记录自系统启动以来产生的总节拍数。比如系统启动了 N 秒,那么 jiffies就为 N×HZ
jiffies的相关定义参考头文件 <linux/jiffies.h>  include/linux/jiffies.h

, u# M6 ]/ j% o9 N5 n( X8 \9 z
  • /* 64bit和32bit的jiffies定义如下 */
  • extern u64 __jiffy_data jiffies_64;
  • extern unsigned long volatile __jiffy_data jiffies;. c4 r3 O% h' C- @, `# S
% f& y* m! ?, V5 _

/ Z# Y0 Z8 a. D# |1 ~+ ^1 H0 x: t& G8 R7 ^( Y6 r
使用定时器时一般都是以jiffies为单位来延迟程序执行的,比如延迟5个节拍后执行的话,执行时间就是 jiffies+5
32位的jiffies的最大值为 2^32-1,在使用时有可能会出现回绕的问题。
比如下面的代码:
' b( o! A5 N5 B7 G! _
  • unsigned long timeout = jiffies + HZ/2; /* 设置超时时间为 0.5秒 */
  • while (timeout < jiffies)
  • {
  •     /* 还没有超时,继续执行任务 */
  • }
  • /* 执行超时后的任务 */7 y# J" ^% a& F
5 m/ n$ D( n! v8 {( D/ U0 K$ ?5 b' F8 B
8 n! Z$ ?& P$ Z3 y

" [9 E0 V+ O  S. Y+ V; T
正常情况下,上面的代码没有问题。当jiffies接近最大值的时候,就会出现回绕问题。
由于是unsinged long类型,所以jiffies达到最大值后会变成0然后再逐渐变大,如下图所示:
0 Z5 g3 W' y$ z
所以在上述的循环代码中,会出现如下情况:
  • 循环中第一次比较时,jiffies = J1,没有超时
  • 循环中第二次比较时,jiffies = J2,实际已经超时了,但是由于jiffies超过的最大值后又从0开始,所以J2远远小于timeout
  • while循环会执行很长时间(> 2^32-1 个节拍)不会结束,几乎相当于死循环了
    / y; I) M3 ~7 P1 z0 _( t
( G! \- W! u1 D! `9 x2 H
为了回避回扰的问题,可以使用<linux/jiffies.h>头文件中提供的 time_after,time_before等宏

, g$ C& H  Q* u0 u. E; C0 ^
  • #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)
    + Y% W4 e: ~9 d9 ]8 v4 w* C  B  ]

* M) l( H" o, {6 U
8 @1 [/ V8 ]2 E; p* y* I4 {' S& s1 m
上述代码的原理其实就是将 unsigned long 类型转换为 long 类型来避免回扰带来的错误,
long 类型超过最大值时变化趋势如下:
+ r: d8 a) |# b+ C! i' Z
long 型的数据的回绕会出现在 2^31-1 变为 -2^32 的时候,如下图所示:
  • 第一次比较时,jiffies = J1,没有超时
  • 第二次比较时,jiffies = J2,一般 J2 是负数
      g* K1 q/ G9 P/ |' c理论上 (long)timeout - (long)J2 = 正数 - 负数 = 正数(result)
    / H, B% n* L; z! z& D* p但是,这个正数(result)一般会大于 2^31 - 1,所以long型的result又发生了一次回绕,变成了负数。 9 ?! l  \1 n7 @7 [9 a  q
    除非timeout和J2之间的间隔 > 2^32 个节拍,result的值才会为正数(注1)。
    0 a: u% ^2 R8 x4 c6 _% t
注1:result的值为正数时,必须是在result的值 小于 2^31-1 的情况下,大于 2^31-1 会发生回绕。
上图中 X + Y 表示timeout 和 J2之间经过的节拍数。
result 小于 2^31-1 ,也就是 timeout - J2 < 2^31 – 1
timeout 和 -J2 表示的节拍数如上图所示。(因为J2是负数,所有-J2表示上图所示范围的值)
因为 timeout + X + Y - J2 = 2^31-1 + 2^32
所以 timeout - J2 < 2^31 - 1 时, X + Y > 2^32
也就是说,当timeout和J2之间经过至少 2^32 个节拍后,result才可能变为正数。
timeout和J2之间相差这么多节拍是不可能的(不信可以用HZ将这些节拍换算成秒就知道了。。。)

, d% F4 U' Q. @5 c
利用time_after宏就可以巧妙的避免回绕带来的超时判断问题,将之前的代码改成如下代码即可:
2 e+ x  j+ v  b8 r9 v! A0 G* h
  • unsigned long timeout = jiffies + HZ/2; /* 设置超时时间为 0.5秒 */
  • while (time_after(jiffies, timeout))
  • {
  •     /* 还没有超时,继续执行任务 */
  • }
  • /* 执行超时后的任务 */
    / l7 |5 T. v" ^. h0 @( @, M
; c" \% T2 Q7 t" H% ]; r" j+ z2 p4 M) f
$ l5 d! D$ i- o) B/ p; e* w* s
3.3 时钟中断处理程序
时钟中断处理程序作为系统定时器而注册到内核中,体系结构的不同,可能时钟中断处理程序中处理的内容不同。
但是以下这些基本的工作都会执行:
  • 获得 xtime_lock 锁,以便对访问 jiffies_64 和墙上时间 xtime 进行保护
  • 需要时应答或重新设置系统时钟
  • 周期性的使用墙上时间更新实时时钟
  • 调用 tick_periodic()
    5 g5 n% j* x, h2 N' j2 y6 W4 X( X. Q

. @% U6 ]! s2 D9 H5 x
tick_periodic函数位于: kernel/time/tick-common.c 中
# y) l5 p8 N0 D1 V, s
  • static void tick_periodic(int cpu)
  • {
  •     if (tick_do_timer_cpu == cpu) {
  •         write_seqlock(&xtime_lock);
  •         /* Keep track of the next tick event */
  •         tick_next_period = ktime_add(tick_next_period, tick_period);
  •         do_timer(1);
  •         write_sequnlock(&xtime_lock);
  •     }
  •     update_process_times(user_mode(get_irq_regs()));
  •     profile_tick(CPU_PROFILING);
  • }
    ) C8 ?$ I9 D% @  Y2 D# |6 H4 Z* W
% G1 L' i* Q/ U
$ p: N4 ^& \. W5 }

' Q/ \; _% U+ ?9 A. H% a. G
其中最重要的是 do_timer 和 update_process_times 函数。
我了解的步骤进行了简单的注释。

7 C0 W) ^) W' {1 |) l
  • void do_timer(unsigned long ticks)
  • {
  •     /* jiffies_64 增加指定ticks */
  •     jiffies_64 += ticks;
  •     /* 更新实际时间 */
  •     update_wall_time();
  •     /* 更新系统的平均负载值 */
  •     calc_global_load();
  • }
  • void update_process_times(int user_tick)
  • {
  •     struct task_struct *p = current;
  •     int cpu = smp_processor_id();
  •     /* 更新当前进程占用CPU的时间 */
  •     account_process_tick(p, user_tick);
  •     /* 同时触发软中断,处理所有到期的定时器 */
  •     run_local_timers();
  •     rcu_check_callbacks(cpu, user_tick);
  •     printk_tick();
  •     /* 减少当前进程的时间片数 */
  •     scheduler_tick();
  •     run_posix_cpu_timers(p);
  • }$ {9 `+ Z8 n% g# e
/ W4 {' z! H5 Z7 ]8 L

* t, ^) Y5 R5 `* L4. 定时器执行流程
这里讨论的定时器执行流程是动态定时器的执行流程。
8 T% x0 }, G. ], y6 `
4.1 定时器的定义
定时器在内核中用一个链表来保存的,链表的每个节点都是一个定时器。
参见头文件 <linux/timer.h>

( [# {# o! H! }) S% Y3 C% }2 p
  • struct timer_list {
  •     struct list_head entry;
  •     unsigned long expires;
  •     void (*function)(unsigned long);
  •     unsigned long data;
  •     struct tvec_base *base;
  • #ifdef CONFIG_TIMER_STATS
  •     void *start_site;
  •     char start_comm[16];
  •     int start_pid;
  • #endif
  • #ifdef CONFIG_LOCKDEP
  •     struct lockdep_map lockdep_map;
  • #endif
  • };
      s* R1 q2 p% P

( E0 ~, a1 r: D
通过加入条件编译的参数,可以追加一些调试信息。
  l+ }  W& d4 K; x- s% y1 o
4.2 定时器的生命周期
一个动态定时器的生命周期中,一般会经过下面的几个步骤:
1. 初始化定时器:
3 ^% L  v2 g3 b/ [4 a9 a( g$ K! z- c
  • struct timer_list my_timer; /* 定义定时器 */
  • init_timer(&my_timer);      /* 初始化定时器 */  R" ^1 m- Q3 L3 a" d" g$ c

) u/ q/ {  g/ c; c. E3 C% [& Q1 w
- x+ V0 T( e% I8 j7 u5 U9 Q+ t
2. 填充定时器:

  N2 M6 a8 x+ ^. K% `
  • my_timer.expires = jiffies + delay; /* 定义超时的节拍数 */
  • my_timer.data = 0;                  /* 给定时器函数传入的参数 */
  • my_timer.function = my_function;    /* 定时器超时时,执行的自定义函数 */
  • /* 从定时器结构体中,我们可以看出这个函数的原型应该如下所示: */
  • void my_function(unsigned long data);
    . u4 s) O5 f$ y) m

! J( x) U  j' T- @- y! C8 I  T6 O& m
3. 激活定时器和修改定时器:
激活定时器之后才会被触发,否则定时器不会执行。
修改定时器主要是修改定时器的延迟时间,修改定时器后,不管原先定时器有没有被激活,都会处于激活状态。
) x( f1 R  \0 Z, I
填充定时器结构之后,可以只激活定时器,也可以只修改定时器,也可以激活定时器后再修改定时器。
所以填充定时器结构和触发定时器之间的步骤,也就是虚线框中的步骤是不确定的。
# V$ Y; Z6 r0 u' i
  • add_timer(&my_timer);  /* 激活定时器 */
  • mod_timer(&my_timer, jiffies + new_delay);  /* 修改定时器,设置新的延迟时间 */
    4 i7 m5 A- e1 F2 ~6 f* J0 [  d

8 S$ C( @, i* \, ^6 k7 @8 O& d2 q9 U, J  M
4. 触发定时器:
每次时钟中断处理程序会检查已经激活的定时器是否超时,如果超时就执行定时器结构中的自定义函数。
5 m0 l# u- s0 C8 Q2 R/ K4 @
5. 删除定时器:
激活和未被激活的定时器都可以被删除,已经超时的定时器会自动删除,不用特意去删除。

  d$ I' i. F3 @' i. H- U
  • /*
  • * 删除激活的定时器时,此函数返回1
  • * 删除未激活的定时器时,此函数返回0
  • */
  • del_timer(&my_timer);% k, a7 l- B& `8 \9 D

. J- f9 R8 D  A- s
在多核处理器上用 del_timer 函数删除定时器时,可能在删除时正好另一个CPU核上的时钟中断处理程序正在执行这个定时器,于是就形成了竞争条件。
为了避免竞争条件,建议使用 del_timer_sync 函数来删除定时器。
del_timer_sync 函数会等待其他处理器上的定时器处理程序全部结束后,才删除指定的定时器。
" B& U+ O7 H( L6 N: K$ g3 P! F
  • /*
  • * 和del_timer 不同,del_timer_sync 不能在中断上下文中执行
  • */
  • del_timer_sync(&my_timer);
    ' z/ a3 `7 ?( j0 a8 R

2 I9 ?. Q, w+ I2 @/ E% ~' M/ H& z, {% }4 H2 U( D5 o

+ h1 x( H1 v" c% C2 c0 R* l% _5. 实现程序延迟的方法
内核中有个利用定时器实现延迟的函数 schedule_timeout
这个函数会将当前的任务睡眠到指定时间后唤醒,所以等待时不会占用CPU时间。
8 V5 X8 J4 Y* @( O. u
  • /* 将任务设置为可中断睡眠状态 */
  • set_current_state(TASK_INTERRUPTIBLE);
  • /* 小睡一会儿,“s“秒后唤醒 */
  • schedule_timeout(s*HZ);
    $ Q/ l0 ?. U& R3 j8 i. s( V
# I9 Z8 h7 i4 r$ d* C) K
1 Y  E! m+ `; |4 S8 K/ c/ }3 }
查看 schedule_timeout 函数的实现方法,可以看出是如何使用定时器的。
) {* ?; N" }" j" T/ N
' D3 m( j) q7 o: s
  • signed long __sched schedule_timeout(signed long timeout)
  • {
  •     /* 定义一个定时器 */
  •     struct timer_list timer;
  •     unsigned long expire;
  •     switch (timeout)
  •     {
  •     case MAX_SCHEDULE_TIMEOUT:
  •         /*
  •          * These two special cases are useful to be comfortable
  •          * in the caller. Nothing more. We could take
  •          * MAX_SCHEDULE_TIMEOUT from one of the negative value
  •          * but I' d like to return a valid offset (>=0) to allow
  •          * the caller to do everything it want with the retval.
  •          */
  •         schedule();
  •         goto out;
  •     default:
  •         /*
  •          * Another bit of PARANOID. Note that the retval will be
  •          * 0 since no piece of kernel is supposed to do a check
  •          * for a negative retval of schedule_timeout() (since it
  •          * should never happens anyway). You just have the printk()
  •          * that will tell you if something is gone wrong and where.
  •          */
  •         if (timeout < 0) {
  •             printk(KERN_ERR "schedule_timeout: wrong timeout "
  •                 "value %lx\n", timeout);
  •             dump_stack();
  •             current->state = TASK_RUNNING;
  •             goto out;
  •         }
  •     }
  •     /* 设置超时时间 */
  •     expire = timeout + jiffies;
  •     /* 初始化定时器,超时处理函数是 process_timeout,后面再补充说明一下这个函数 */
  •     setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);
  •     /* 修改定时器,同时会激活定时器 */
  •     __mod_timer(&timer, expire, false, TIMER_NOT_PINNED);
  •     /* 将本任务睡眠,调度其他任务 */
  •     schedule();
  •     /* 删除定时器,其实就是 del_timer_sync 的宏
  •     del_singleshot_timer_sync(&timer);
  •     /* Remove the timer from the object tracker */
  •     destroy_timer_on_stack(&timer);
  •     timeout = expire - jiffies;
  • out:
  •     return timeout < 0 ? 0 : timeout;
  • }
  • EXPORT_SYMBOL(schedule_timeout);
  • /*
  • * 超时处理函数 process_timeout 里面只有一步操作,唤醒当前任务。
  • * process_timeout 的参数其实就是 当前任务的地址
  • */
  • static void process_timeout(unsigned long __data)
  • {
  •     wake_up_process((struct task_struct *)__data);
  • }
    % H- W- T8 o  g3 e5 h4 S4 V2 R0 _* t
/ W$ }3 n/ \3 A3 a
schedule_timeout 一般用于延迟时间较长的程序。
这里的延迟时间较长是对于计算机而言的,其实也就是延迟大于 1 个节拍(jiffies)。

& ]# \9 V" }0 \9 V* A+ e0 r
对于某些极其短暂的延迟,比如只有1ms,甚至1us,1ns的延迟,必须使用特殊的延迟方法。
1s = 1000ms = 1000000us = 1000000000ns (1秒=1000毫秒=1000000微秒=1000000000纳秒)
假设 HZ=100,那么 1个节拍的时间间隔是 1/100秒,大概10ms左右。
所以对于那些极其短暂的延迟,schedule_timeout 函数是无法使用的。
好在内核对于这些短暂,精确的延迟要求也提供了相应的宏。

7 \  `. d( ^! ~% _
  • /* 具体实现参见 include/linux/delay.h
  • * 以及 arch/x86/include/asm/delay.h
  • */
  • #define mdelay(n) ...
  • #define udelay(n) ...
  • #define ndelay(n) ...
    ) T( ~2 V) X3 {) Q& i
' T( X% x+ `$ n, v2 f2 w. ^
通过这些宏,可以简单的实现延迟,比如延迟 5ns,只需 ndelay(5); 即可。
, {+ r# {2 _% o, Y. R* I1 f
这些短延迟的实现原理并不复杂,
首先,内核在启动时就计算出了当前处理器1秒能执行多少次循环,即 loops_per_jiffy
(loops_per_jiffy 的计算方法参见 init/main.c 文件中的 calibrate_delay 方法)。
然后算出延迟 5ns 需要循环多少次,执行那么多次空循环即可达到延迟的效果。
6 \1 f9 ~1 \( c. w$ ^; I7 p1 O
loops_per_jiffy 的值可以在启动信息中看到:

/ ~7 d0 t) a4 V" ]+ d* v' @- m
  • [root@vbox ~]# dmesg | grep delay
  • Calibrating delay loop (skipped), value calculated using timer frequency.. 6387.58 BogoMIPS (lpj=3193792)
    $ {  j) N" q8 K9 \7 a
! s. L* n( P. ~  `1 |* M2 m; ]
我的虚拟机中看到 (lpj=3193792)

& m( T" a( [' ~9 Z! _) n8 J1 }3 G6. 定时器和延迟的例子
下面的例子测试了短延迟,自定义定时器以及 schedule_timeout 的使用:
2 X3 j# O. g% h8 A) {1 y& K+ [4 k
  • #include <linux/sched.h>
  • #include <linux/timer.h>
  • #include <linux/jiffies.h>
  • #include <asm/param.h>
  • #include <linux/delay.h>
  • #include "kn_common.h"
  • MODULE_LICENSE("Dual BSD/GPL");
  • static void test_short_delay(void);
  • static void test_delay(void);
  • static void test_schedule_timeout(void);
  • static void my_delay_function(unsigned long);
  • static int testdelay_init(void)
  • {
  •     printk(KERN_ALERT "HZ in current system: %dHz\n", HZ);
  •     /* test short delay */
  •     test_short_delay();
  •     /* test delay */
  •     test_delay();
  •     /* test schedule timeout */
  •     test_schedule_timeout();
  •     return 0;
  • }
  • static void testdelay_exit(void)
  • {
  •     printk(KERN_ALERT "*************************\n");
  •     print_current_time(0);
  •     printk(KERN_ALERT "testdelay is exited!\n");
  •     printk(KERN_ALERT "*************************\n");
  • }
  • static void test_short_delay()
  • {
  •     printk(KERN_ALERT "jiffies [b e f o r e] short delay: %lu", jiffies);
  •     ndelay(5);
  •     printk(KERN_ALERT "jiffies [a f t e r] short delay: %lu", jiffies);
  • }
  • static void test_delay()
  • {
  •     /* 初始化定时器 */
  •     struct timer_list my_timer;
  •     init_timer(&my_timer);
  •     /* 填充定时器 */
  •     my_timer.expires = jiffies + 1*HZ; /* 2秒后超时函数执行 */
  •     my_timer.data = jiffies;
  •     my_timer.function = my_delay_function;
  •     /* 激活定时器 */
  •     add_timer(&my_timer);
  • }
  • static void my_delay_function(unsigned long data)
  • {
  •     printk(KERN_ALERT "This is my delay function start......\n");
  •     printk(KERN_ALERT "The jiffies when init timer: %lu\n", data);
  •     printk(KERN_ALERT "The jiffies when timer is running: %lu\n", jiffies);
  •     printk(KERN_ALERT "This is my delay function end........\n");
  • }
  • static void test_schedule_timeout()
  • {
  •     printk(KERN_ALERT "This sample start at : %lu", jiffies);
  •     /* 睡眠2秒 */
  •     set_current_state(TASK_INTERRUPTIBLE);
  •     printk(KERN_ALERT "sleep 2s ....\n");
  •     schedule_timeout(2*HZ);
  •     printk(KERN_ALERT "This sample end at : %lu", jiffies);
  • }
  • module_init(testdelay_init);
  • module_exit(testdelay_exit);
    % d" s/ x& E2 K

( w- S/ g/ A: E! r# q9 F3 H4 s' ?7 k& Q; r) S! U' j# W6 e
其中用到的 kn_common.h 和 kn_common.c 参见之前的博客 《Linux内核设计与实现》读书笔记(六)- 内核数据结构
Makefile如下:
/ x% A3 \! T) `2 x/ X3 S- f2 V
  • # must complile on customize kernel
  • obj-m += mydelay.o
  • mydelay-objs := testdelay.o kn_common.o
  • #generate the path
  • CURRENT_PATH:=$(shell pwd)
  • #the current kernel version number
  • LINUX_KERNEL:=$(shell uname -r)
  • #the absolute path
  • LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
  • #complie object
  • all:
  •     make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
  •     rm -RF modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
  • #clean
  • clean:
  •     rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned6 O/ `+ D" b' F  Q
+ B/ o- N  T; k( ^( e
! ?! K# t# j; X! Q6 L8 G* M1 C" z
执行测试命令及查看结果的方法如下:(我的测试系统是 CentOS 6.3 x64)
3 e" ~2 h# u3 G, P& t# H' y0 L
  • [root@vbox chap11]# make
  • [root@vbox chap11]# insmod mydelay.ko
  • [root@vbox chap11]# rmmod mydelay.ko
  • [root@vbox chap11]# dmesg | tail -14
  • HZ in current system: 1000Hz
  • jiffies [b e f o r e] short delay: 4296079617
  • jiffies [a f t e r] short delay: 4296079617
  • This sample start at : 4296079619
  • sleep 2s ....
  • This is my delay function start......
  • The jiffies when init timer: 4296079619
  • The jiffies when timer is running: 4296080621
  • This is my delay function end........
  • This sample end at : 4296081622
  • *************************
  • 2013-5-9 23:7:20
  • testdelay is exited!
  • ************************** Z$ B9 p9 U* D6 G. N

' w* P* A9 \' x3 l
# Z5 `# T% U" a  j% M: f
结果说明:
1. 短延迟只延迟了 5ns,所以执行前后的jiffies是一样的。
% X: W* E9 C- y+ |9 y
  • jiffies [b e f o r e] short delay: 4296079617
  • jiffies [a f t e r] short delay: 4296079617& M2 u; ]9 p3 }7 p! d
7 s4 S. K; d) k8 F/ U  P# N

( U) i; y. B. H9 u9 q, z
2. 自定义定时器延迟了1秒后执行自定义函数,由于我的系统 HZ=1000,所以jiffies应该相差1000

0 D7 G! W6 L8 Y2 Z) B
  • The jiffies when init timer: 4296079619
  • The jiffies when timer is running: 4296080621- w- V1 @; |1 f+ I
. W8 Y; z3 |2 m  u5 C
实际上jiffies相差了 1002,多了2个节拍
* t5 y5 F8 L0 q+ n# w7 i$ a
3. schedule_timeout 延迟了2秒,jiffies应该相差 2000
' q, a5 S$ r) \
  • This sample start at : 4296079619
  • This sample end at : 42960816220 h2 t3 d, t3 z& ]) D/ a

4 c) u3 T* D3 c. T" u
实际上jiffies相差了 2003,多了3个节拍

, D2 I% o+ C% I8 P8 ^8 m! Z6 Q
以上结果也说明了定时器的延迟并不是那么精确,差了2,3个节拍其实就是误差2,3毫秒(因为HZ=1000)
如果HZ=100的话,一个节拍是10毫秒,那么定时器的误差可能就发现不了了(误差只有2,3毫秒,没有超多1个节拍)。

1 u; f, J( v. H6 g  X, O
+ O' A' N9 Y0 z# s! d# P

该用户从未签到

2#
发表于 2020-12-24 16:37 | 只看该作者
Linux内核设计与实现之定时器和时间管理
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

推荐内容上一条 /1 下一条

EDA365公众号

关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

GMT+8, 2025-11-25 00:38 , Processed in 0.171875 second(s), 26 queries , Gzip On.

深圳市墨知创新科技有限公司

地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

快速回复 返回顶部 返回列表