Linux 时钟处理机制(二)(2)
该函数根据索引,取出相应的 tv ( tv2~tv5 )中的链表,然后遍历链表每一个元素。按照其到期时间重新将软件时钟加入到软件时钟的 base 中。该函数返回 tv 中被调整的链表索引值(参见图3-1)。
清单3-9中调整软件时钟的代码如下:
int index = base->timer_jiffies & TVR_MASK;if (!index && (!cascade(base, &base->tv2, INDEX(0))) && (!cascade(base, &base->tv3, INDEX(1))) && !cascade(base, &base->tv4, INDEX(2))) cascade(base, &base->tv5, INDEX(3));
这部分代码表明:如果 index 有0再到0时( index 是对 timer_jiffies 取模),说明时间已经过了256个 tick ,这时要把 tv2 中软件时钟转移到 tv1 中。如果 index 和第一个 cascade 函数的返回值都从0再到到0时,说明时间已经过了256*64个 tick ,这时要把 tv3 中软件时钟转移到 tv1 或者 tv2 中。之后的调整过程依次类推。
3.4 自我激活
软件时钟可分为两种类型:
仅仅激活一次
激活多次或者周期性激活
多次激活的实现机制就是要在软件时钟处理函数中重新设置软件时钟的到期时间为将来的一个时间,这个过程通过调用 mod_timer 函数来实现。该函数的实现如清单3-11
清单3-11 mod_timer 函数
int mod_timer(struct timer_list *timer, unsigned long expires){ …… if (timer->expires == expires && timer_pending(timer)) return 1; return __mod_timer(timer, expires);}
从代码中可以看出,该函数实际上调用 __mod_timer 函数(参见3.3.1节)来调整软件时钟的到期时间。
3.5 软件时钟的应用
软件时钟的处理是在处理软中断时触发的,而软中断的处理又会紧接着硬件中断处理结束而进行,并且系统会周期地产生时钟中断(硬件中断),这样,软件 时钟的处理至少会在系统每一次时钟中断处理完成后触发(如果软件时钟的到期时间大于系统当前的 jiffies ,表明时间未到期,则不会调用保存在软件时钟中的函数,但此时的确提供了处理软件时钟的时机)。从这点上看,软件时钟会有较快的相应——一旦时间到期,保 存在软件时钟中的函数会将快地被调用(在时钟软中断中被调用,参见3.3.2节)。所以内核中凡是需要隔一段时间间隔后作指定操作的过程都通过软件时钟完 成。例如大部分设备驱动程序使用软件时钟探测异常条件、软盘驱动程序利用软件时钟关闭有一段时间没有被访问软盘的设备马达、进程的定时睡眠( schedule_timeout 函数)和网络超时重传等等。
本节主要通过介绍进程的定时睡眠( schedule_timeout 函数)和网络超时重传来说明软件时钟的应用。
3.5.1 进程的定时睡眠
函数 schedule_timeout 的代码如清单3-12
清单3-12 函数 schedule_timeout
signed long __sched schedule_timeout(signed long timeout){ struct timer_list timer; unsigned long expire; …… expire = timeout + jiffies; setup_timer(&timer, process_timeout, (unsigned long)current); __mod_timer(&timer, expire); schedule(); del_singleshot_timer_sync(&timer); timeout = expire - jiffies; out: return timeout < 0 ? 0 : timeout;}
函数 schedule_timeout 定义了一个软件时钟变量 timer ,在计算到期时间后初始化这个软件时钟:设置软件时钟当时间到期时的处理函数为 process_timeout ,参数为当前进程描述符,设置软件时钟的到期时间为 expire 。之后调用 schedule() 函数。此时当前进程睡眠,交出执行权,内核调用其它进程运行。但内核在每一个时钟中断处理结束后都要检测这个软件时钟是否到期。如果到期,将调用 process_timeout 函数,参数为睡眠的那个进程描述符。 process_timeout 函数的代码如清单3-13。
清单3-13 函数 process_timeout
static void process_timeout(unsigned long __data){ wake_up_process((struct task_struct *)__data);}
函数 process_timeout 直接调用 wake_up_process 将进程唤醒。当内核重新调用该进程执行时,该进程继续执行 schedule_timeout 函数,执行流则从 schedule 函数中返回,之后调用 del_singleshot_timer_sync 函数将软件时钟卸载,然后函数 schedule_timeout 结束。函数 del_singleshot_timer_sync 是实际上就是函数 del_timer_sync (参见3.3.2节),如清单3-14
清单3-14 函数del_singleshot_timer_sync
#define del_singleshot_timer_sync(t) del_timer_sync(t)
以上就是进程定时睡眠的实现过程。接下来介绍的是软件时钟在网络超时重传上的应用。
3.5.2 网路超时重传
对于 TCP 协议而言,如果某次发送完数据包后,并超过一定的时间间隔还没有收到这次发送数据包的 ACK 时, TCP 协议规定要重新发送这个数据包。
在 Linux2.6.25 的内核中,这种数据的重新发送使用软件时钟来完成。这个软件时钟保存在面向连接的套接字(对应内核中 inet_connection_sock 结构)中。对这个域的初始在函数 tcp_init_xmit_timers 中,如清单3-15
清单3-15 函数 tcp_init_xmit_timers 、函数 inet_csk_init_xmit_timers 和函数 setup_timer
void tcp_init_xmit_timers(struct sock *sk){ inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer);}void inet_csk_init_xmit_timers(struct sock *sk, void (*retransmit_handler)(unsigned long), void (*delack_handler)(unsigned long), void (*keepalive_handler)(unsigned long)){ struct inet_connection_sock *icsk = inet_csk(sk); setup_timer(&icsk->icsk_retransmit_timer, retransmit_handler, (unsigned long)sk); ……}static inline void setup_timer(struct timer_list * timer, void (*function)(unsigned long), unsigned long data){ timer->function = function; timer->data = data; init_timer(timer);}
在函数 inet_csk_init_xmit_timers 中,变量 icsk 就是前面提到的面向连接的套接字,其成员 icsk_retransmit_timer 则为实现超时重传的软件时钟。该函数调用 setup_timer 函数将函数 tcp_write_timer (参考函数 tcp_init_xmit_timers )设置为软件时钟 icsk->icsk_retransmit_timer 当时间到期后的处理函数。初始化的时候并没有设置该软件时钟的到期时间。
在 TCP 协议具体的一次数据包发送中,函数 tcp_write_xmit 用来将数据包从 TCP 层发送到网络层,如清单3-16。
清单3-16 tcp_write_xmit 函数
static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle){ struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; …… if (unlikely(tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC))) break; tcp_event_new_data_sent(sk, skb); …… return !tp->packets_out && tcp_send_head(sk);}
注意该函数中加粗的函数,其中 tcp_transmit_skb 函数是真正将数据包由 TCP 层发送到网络层中的函数。数据发送后,将调用函数 tcp_event_new_data_sent ,而后者又会调用函数 inet_csk_reset_xmit_timer 来设置超时软件时钟的到期时间。
当函数 tcp_event_new_data_sent 结束之后,处理超时的软件时钟已经设置好了。内核会在每一次时钟中断处理完成后检测该软件时钟是否到期。如果网络真的超时,没有 ACK 返回,那么当该软件时钟到期后内核就会执行函数 tcp_write_timer 。函数 tcp_write_timer 将进行数据包的重新发送,并重新设置超时重传软件时钟的到期时间。
表3-2 struct tvec_base 类型的成员
域名 类型 描述
lock spinlock_t 用于同步操作
running_timer struct timer_list * 正在处理的软件时钟
timer_jiffies unsigned long 当前正在处理的软件时钟到期时间
tv1 struct tvec_root 保存了到期时间从 timer_jiffies 到 timer_jiffies + 之间(包括边缘值)的所有软件时钟
tv2 struct tvec 保存了到期时间从 timer_jiffies + 到 timer_jiffies +之间(包括边缘值)的 所有软件时钟
tv3 struct tvec 保存了到期时间从 timer_jiffies +到 timer_jiffies +之间(包括边缘值)的所有软件时钟
tv4 struct tvec 保存了到期时间从 timer_jiffies +到 timer_jiffies +之间(包括边缘值)的所有软件时钟
tv5 struct tvec 保存了到期时间从 timer_jiffies +到 timer_jiffies +之间(包括边缘值)的所有软件时钟
其中 tv1 的类型为 struct tvec_root ,tv 2~ tv 5的类型为 struct tvec ,清单3-1显示它们的定义
清单3-1 struct tvec_root 和 struct tvec 的定义
struct tvec { struct list_head vec[TVN_SIZE];};struct tvec_root { struct list_head vec[TVR_SIZE];};
可见它们实际上就是类型为 struct list_head 的数组,其中 TVN_SIZE 和 TVR_SIZE 在系统没有配置宏 CONFIG_BASE_SMALL 时分别被定义为64和256。
3.2 数据结构之间的关系
图3-1显示了以上数据结构之间的关系:
从图中可以清楚地看出:软件时钟( struct timer_list ,在图中由 timer 表示)以双向链表( struct list_head )的形式,按照它们的到期时间保存相应的桶( tv1~tv5 )中。tv1 中保存了相对于 timer_jiffies 下256个 tick 时间内到期的所有软件时钟; tv2 中保存了相对于 timer_jiffies 下256*64个 tick 时间内到期的所有软件时钟; tv3 中保存了相对于 timer_jiffies 下256*64*64个 tick 时间内到期的所有软件时钟; tv4 中保存了相对于 timer_jiffies 下256*64*64*64个 tick 时间内到期的所有软件时钟; tv5 中保存了相对于 timer_jiffies 下256*64*64*64*64个 tick 时间内到期的所有软件时钟。具体的说,从静态的角度看,假设 timer_jiffies 为0,那么 tv1[0] 保存着当前到期(到期时间等于 timer_jiffies )的软件时钟(需要马上被处理), tv1[1] 保存着下一个 tick 到达时,到期的所有软件时钟, tv1[n] (0<= n <=255)保存着下 n 个 tick 到达时,到期的所有软件时钟。而 tv2[0] 则保存着下256到511个 tick 之间到期所有软件时钟, tv2[1] 保存着下512到767个 tick 之间到期的所有软件时钟, tv2[n] (0<= n <=63)保存着下256*(n+1)到256*(n+2)-1个 tick 之间到达的所有软件时钟。 tv3~tv5 依次类推。
注:一个tick的长度指的是两次硬件时钟中断发生之间的时间间隔
从上面的说明中可以看出:软件时钟是按照其到期时间相对于当前正在处理的软件时钟的到期时间( timer_jiffies 的数值)保存在 struct tvec_base 变量中的。而且这个到期时间的最大相对值(到期时间 - timer_jiffies )为 0xffffffffUL ( tv5 最后一个元素能够表示的相对到期时间的最大值)。
还需要注意的是软件时钟的处理是局部于 CPU 的,所以在 SMP 系统中每一个 CPU 都保存一个类型为 struct tvec_base 的变量,用来组织、管理本 CPU 的软件时钟。从图中也可以看出 struct tvec_base 变量是 per-CPU 的(关于 per-CPU 的变量原理和使用参见参考资料)。
由于以后的讲解经常要提到每个 CPU 相关的 struct tvec_base 变量,所以为了方便,称保存软件时钟的 struct tvec_base 变量为该软件时钟的 base ,或称 CPU 的 base 。
- 最新评论
