本篇是《Linux内核学习笔记》系列的第七篇。
系统定时器是一种可编程硬件芯片,能以固定的频率产生中断,这些中断驱动了内核中的相当一部分函数。利用定时器中断,我们可以使某些函数以固定的频率被重复执行,或者推后一个给定的时间再执行。不仅如此,它也在进程调度中发挥着重要作用。
定时器为内核带来了“时间”这个概念。
1. 时钟中断频率
可以在下面看到内核的时钟中断频率值。
# define HZ CONFIG_HZ /* Internal kernel timer frequency */
# define USER_HZ 100 /* some user interfaces are */
# define CLOCKS_PER_SEC (USER_HZ) /* in "ticks" like times() */
CONFIG_HZ可以在make menuconfig指令中设置,也可以直接修改内核配置文件,笔者的配置为x86的默认值100。
至于USER_HZ,这是用户空间中能看到的频率。因为在很长一段时间里内核都维持着100Hz的频率值,这导致某些用户程序错误地将100硬编码进代码里。当内核频率变为1000Hz,这些用户程序可能错误地将2小时判断为20小时。所以内核直接摆了决定只告诉用户空间这一固定值。内核如果要将jiffies(后面会提到)相关的数据传到用户空间,通过一些函数转化为clock即可。如果你使用过Linux系统提供的计时设施,那么一定不会对CLOCK_PER_SEC感到陌生。
HZ值可以调整,那么应该选取什么样的HZ值呢?显而易见,高频率能提供更精确的抢占调度,并更准确地分析系统资源消耗,但会增加处理器负担,导致高能耗。所以要视具体情况而定。
2. jiffies
jiffies是一个全局变量。它在科学应用中经常表示各种时间间隔,一般指10ms,特别适合用于表示Linux系统的时间,即系统启动后定时器产生的节拍的总数。它的声明如下:
/*
* The 64-bit value is not atomic - you MUST NOT read it
* without sampling the sequence number in jiffies_lock.
* get_jiffies_64() will do this for you as appropriate.
*/
extern u64 __cacheline_aligned_in_smp jiffies_64;
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;
后面我们会看到这个神奇变量的定义。先来明确一个换算关系:
jiffies = seconds * HZ
例如,如果HZ取值为100,那么一个jiffy就是10ms,同时seconds可以精确到小数点后两位。
2.1. jiffies的内部表示
32位系统的jiffies真的只有很少一点,对于100Hz系统来说,它在497天后就会溢出;而1000Hz系统则在49.7天就会溢出。但是64位的jiffies那可就大了,想想四十亿个50天是多长时间……
然而出于兼容性的考虑,特别是32位系统仍然使用原来32位的jiffies,不能放弃对该变量的维护,或者改变其定义;而同时维护32位和64位两个变量,又带来不必要的开销。如果能这样就好了:反正jiffies加1的时候,jiffies_64也加1,不论如何它们的低32位都是一样的!要是jiffies和jiffies_64能做到用一条add语句同步更新,那就一箭双雕了。原来的代码不受影响,新的代码又可以稳定地依靠jiffies_64。
我们知道jiffies_64和jiffies都是全局变量,具有强弱符号之分。要想达到上面的要求,应该考虑为jiffies_64作一个强符号定义,这样链接器会为它分配实际空间。而jiffies全部为弱符号,这样就可以想个办法作为引用,指向jiffies_64的空间。这样所有外部模块中对符号jiffies的引用,最终都指向jiffies_64。真是魔法一般的操作。
不过这对于设计Linux的魔法师们,还真不是问题。上面已经看过了jiffies和jiffies_64的声明,下面就列出jiffies_64的强符号定义。
__visible u64 jiffies_64 __cacheline_aligned_in_smp = INITIAL_JIFFIES;
EXPORT_SYMBOL(jiffies_64);
那么怎样处理jiffies,让它引用到jiffies_64?这是令笔者今日大长见识的地方。
jiffies = jiffies_64;
vmlinux.lds.S是主内核映像的链接脚本,靠这一条语句,让符号jiffies指向为jiffies_64分配的空间。在64位下,unsigned long就是64位,所以jiffies和jiffies_64是一回事。在32位下,unsigned long是32位,链接器会将jiffies和jiffies_64的小端对齐,从而jiffies指代jiffies_64的低32位。
链接和符号重定位的问题是解决了,但还有不少问题需要考虑周全才行。例如,这么链接之后,jiffies会以编译器难以预料的玄学方式改变,毕竟编译器只靠符号区分不同变量,才不知道具体指向了什么空间,后者是链接器负责的。所以,必须为jiffies加一个volatile修饰符,要求编译器每次使用jiffies都要直接从内存区域取值,防止在错误的假设(即认为寄存器中的值寄存期间一直未变)下进行错误的优化。
2.2. jiffies的使用
有时我们会比较两个节拍数,或者判断一个节拍数是否落在某区间内。对于这样的需求,内核准备了一套宏,比如:
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)((b) - (a)) < 0))
#define time_before(a,b) time_after(b,a)
它完备地考虑到了32位jiffies可能存在的溢出问题,并充分利用了编译优化,所以在编写例如设备驱动程序时,最好使用这些API来完成上述需求。不过,从这段代码的注释来看,似乎当时的gcc还不是很争气吧(
* Do this with "<0" and ">=0" to only test the sign of the result. A
* good compiler would generate better code (and a really good compiler
* wouldn't care). Gcc is currently neither.
3. 时钟中断处理程序
3.1. 注册
x86系统中,时钟中断处理程序的注册如下所示,可以看到时钟中断的IRQ号被无条件设成0。
static void __init setup_default_timer_irq(void)
{
unsigned long flags = IRQF_NOBALANCING | IRQF_IRQPOLL | IRQF_TIMER;
/*
* Unconditionally register the legacy timer interrupt; even
* without legacy PIC/PIT we need this for the HPET0 in legacy
* replacement mode.
*/
if (request_irq(0, timer_interrupt, flags, "timer", NULL))
pr_info("Failed to register legacy timer interrupt\n");
}
3.2. 时钟中断处理程序
书上说这一程序是与体系结构相关的例程,但看起来简单到令人失望:
/*
* Default timer interrupt handler for PIT/HPET
*/
static irqreturn_t timer_interrupt(int irq, void *dev_id)
{
global_clock_event->event_handler(global_clock_event);
return IRQ_HANDLED;
}
它调用一个函数句柄,但是我只能推测它调用的是tick_handle_periodic,还没有找到确切的证据。欢迎各位dalao在评论区补充。该函数调用另一位于个同一文件中的函数tick_periodic,它们都是体系结构无关的:
/*
* Periodic tick
*/
static void tick_periodic(int cpu)
{
if (tick_do_timer_cpu == cpu) {
raw_spin_lock(&jiffies_lock);
write_seqcount_begin(&jiffies_seq);
/* Keep track of the next tick event */
tick_next_period = ktime_add(tick_next_period, tick_period);
do_timer(1);
write_seqcount_end(&jiffies_seq);
raw_spin_unlock(&jiffies_lock);
update_wall_time();
}
update_process_times(user_mode(get_irq_regs()));
profile_tick(CPU_PROFILING);
}
tick_periodic所做的很多重要工作在do_timer和update_process_times中完成。
首先是do_timer。
/*
* Must hold jiffies_lock
*/
void do_timer(unsigned long ticks)
{
jiffies_64 += ticks;
calc_global_load();
}
在持有jiffies_64锁的情况下,可以调用该函数进行节拍值修改,然后更新系统平均负载统计值。修改节拍数后,会更新墙上时间,之后调用update_process_times,该函数更新进程花费时间。通过user_tick可判断时间花是在在用户空间还是内核空间。
/*
* Called from the timer interrupt handler to charge one tick to the current
* process. user_tick is 1 if the tick is user time, 0 for system.
*/
void update_process_times(int user_tick)
{
struct task_struct *p = current;
PRANDOM_ADD_NOISE(jiffies, user_tick, p, 0);
/* Note: this timer irq context must be accounted for as well. */
account_process_tick(p, user_tick);
run_local_timers();
rcu_sched_clock_irq(user_tick);
#ifdef CONFIG_IRQ_WORK
if (in_irq())
irq_work_tick();
#endif
scheduler_tick();
if (IS_ENABLED(CONFIG_POSIX_TIMERS))
run_posix_cpu_timers();
}
account_process_tick会实际上更新进程所花费的时间,它的参数是一个task_struct指针,本例中就是current宏的计算结果。下面的代码已根据笔者机器的配置做了一些删减。
/*
* Account a single tick of CPU time.
* @p: the process that the CPU time gets accounted to
* @user_tick: indicates if the tick is a user or a system tick
*/
void account_process_tick(struct task_struct *p, int user_tick)
{
u64 cputime, steal;
cputime = TICK_NSEC;
steal = steal_account_process_time(ULONG_MAX);
if (steal >= cputime)
return;
cputime -= steal;
if (user_tick)
account_user_time(p, cputime);
else if ((p != this_rq()->idle) || (irq_count() != HARDIRQ_OFFSET))
account_system_time(p, HARDIRQ_OFFSET, cputime);
else
account_idle_time(cputime);
}
steal似乎是和多虚拟机共享处理器有关的指标。这是笔者个人的理解,不一定正确:当虚拟机A和B共享同一台物理机时,如果hypervisor正将资源分配给B,那么A的虚拟CPU就要等待。这段时间A的虚拟CPU没有睡眠,但也没有在执行计算,所以这段时间被B偷走了(steal)。具体内容不再深挖。
不过还有一件事值得注意,那就是cputime似乎被全额分给了current进程。这么分不一定准确,因为上个时间片中,很有可能发生中断,中断处理程序可不属于进程上下文,这些时间不该算到进程中;同时该进程也可能和其他进程共享时间片。《Linux内核设计与实现》提到传统的Unix就是这么处理,Linux 2.6并没有很好的解决办法。而在如今,如果确实还是没有良好的办法,可以靠提高时钟频率来提高准确度。现在1000Hz并不会给机器带来太大负担。
更新进程时间后,update_process_times又调用run_local_timers将发起一个软中断,即用下半部处理所有即将到时的定时器,我们会在下面讨论。
/*
* Called by the local, per-CPU timer interrupt on SMP.
*/
void run_local_timers(void)
{
struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);
hrtimer_run_queues();
/* Raise the softirq only if required. */
if (time_before(jiffies, base->next_expiry)) {
if (!IS_ENABLED(CONFIG_NO_HZ_COMMON))
return;
/* CPU is awake, so check the deferrable base. */
base++;
if (time_before(jiffies, base->next_expiry))
return;
}
raise_softirq(TIMER_SOFTIRQ);
}
最后,scheduler_tick负责为每一个进程减少时间片值,相信如果在《操作系统》课中学过抢占式调度原理,对这个操作应该会有印象。它就是在这里实现的。如果内核启用了SMP,那么它还要平衡每个处理器上的运行队列。
4. 定时器
中断下半部是推后执行的。有时我们不止希望推后,还希望推后一个指定的时间长度执行该任务,这时就需要在时钟中断处理程序中发起软中断。显然可以每一次时钟中断时都发起软中断,不过效率比较低,从run_local_timers的代码中我们也可以看出,它在jiffies到达某个expire的时刻时,才发起软中断。
很容易想象到定时器的用法:指定一个时刻(或时间间隔),再给一个回调函数(或许需要一些参数),接下来就可以倒一杯卡布奇诺静静等待函数执行了。
这是定时器的数据结构。
struct timer_list {
struct hlist_node entry;
unsigned long expires;
void (*function)(struct timer_list *);
u32 flags;
};
hlist_node是哈希链表,在第四节有介绍。使用起来比较简单,首先定义并初始化它:
DEFINE_TIMER(my_timer, my_function);
一步到位,但是一定注意,my_timer的生存期起码得能坚持到回调函数执行完毕之时,一般来说全局变量的生存期还是算够格的。或者这样也行:
struct timer_list my_timer;
如果是这么定义的,接下来要对定时器初始化。选什么函数,取决于定时器在哪里初始化。
timer_setup(&my_timer, my_function, my_flags);
// timer_setup_on_stack(&my_timer, my_function, my_flags);
初始化定时器后,可以选择性地填充timer_list的expires、function和flags,分别用于指定定时器的执行时间、回调函数和定时器选项,然后用下面的代码激活定时器
add_timer(&my_timer);
就大功告成了!当然,也可以用下面的语句修改已激活的定时器,或者激活一个未激活的定时器:
mod_timer(&my_timer, jiffies_64 + new_delay);
还可以取消定时器。通常要像这样进行同步取消,因为有的处理器可能已经在运行这个回调函数,需要等待它们执行完毕。
del_timer_sync(&my_timer);
说起来,回调函数的格式看起来很奇怪,它传入的是timer_struct结构的指针,但是这指针指向的结构里啥也没有啊?那回调函数需要参数的时候怎么办?别着急,我们会在schedule_timeout的实现中进行描述。
不过时刻牢记,定时器是用软中断实现的,所以传入的回调函数必须得遵守一点规矩。具体来说:
- 软中断不属于进程上下文,不能访问用户空间;
- 允许响应中断,但不能休眠;
- 多处理器下允许并发执行,需要注意数据一致性(也可以只使用单处理器数据)。
后面的章节有定时器的模块编程示例。
5. 延迟执行的手段
5.1. 短延迟
有时要求内核代码在很小的一个时延后执行,可能是微秒或纳秒级,比定时器周期还小。这种情况应采用忙等待的办法,Linux已经提供了具体的接口:mdelay、udelay和ndelay。它们通过执行确定次数的循环来达尽可能到精确的延时效果(通常是长一点点),因为CPU每秒能执行的指令数是可以获知的。
然而应时刻注意,持锁忙等或禁止中断都是粗鲁的做法,它们大幅会降低系统性能。只有等待时间很少且需要精确等待时间时,才用这些函数。
5.2. schedule_timeout
a
相比之下这是一种比较礼貌的方式:
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(s*HZ);
先将任务设置为可睡眠的状态(TASK_UNINTERRUPTIBLE也行,不过睡眠期间不接受任何信号),然后告诉调度程序,s秒后再重新调度我执行。
不过既然要调用调度程序,那么也需要满足一些性质。即必须处于进程上下文(禁止在中断上下文使用),而且不能持有锁,这样的代码才是可睡眠的。
schedule_timeout是用定时器实现的。其思路可简单地归结为:程序先设一个定时器,要求在一个给定的时间后唤醒它,然后就调用schedule让出处理器。等时间一到,内核会调用回调函数将该进程唤醒。
上源代码。
/**
* schedule_timeout - sleep until timeout
* @timeout: timeout value in jiffies
*
* Make the current task sleep until @timeout jiffies have elapsed.
* The function behavior depends on the current task state
* (see also set_current_state() description):
*
* %TASK_RUNNING - the scheduler is called, but the task does not sleep
* at all. That happens because sched_submit_work() does nothing for
* tasks in %TASK_RUNNING state.
*
* %TASK_UNINTERRUPTIBLE - at least @timeout jiffies are guaranteed to
* pass before the routine returns unless the current task is explicitly
* woken up, (e.g. by wake_up_process()).
*
* %TASK_INTERRUPTIBLE - the routine may return early if a signal is
* delivered to the current task or the current task is explicitly woken
* up.
*
* The current task state is guaranteed to be %TASK_RUNNING when this
* routine returns.
*
* Specifying a @timeout value of %MAX_SCHEDULE_TIMEOUT will schedule
* the CPU away without a bound on the timeout. In this case the return
* value will be %MAX_SCHEDULE_TIMEOUT.
*
* Returns 0 when the timer has expired otherwise the remaining time in
* jiffies will be returned. In all cases the return value is guaranteed
* to be non-negative.
*/
signed long __sched schedule_timeout(signed long timeout)
{
struct process_timer 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;
timer.task = current;
timer_setup_on_stack(&timer.timer, process_timeout, 0);
__mod_timer(&timer.timer, expire, MOD_TIMER_NOTPENDING);
schedule();
del_singleshot_timer_sync(&timer.timer);
/* Remove the timer from the object tracker */
destroy_timer_on_stack(&timer.timer);
timeout = expire - jiffies;
out:
return timeout < 0 ? 0 : timeout;
}
EXPORT_SYMBOL(schedule_timeout);
首先是有一些边边角角的情况要处理。比如,如果参数timeout是MAX_SCHEDULE_TIMEOUT,那不能让它休眠,而是直接进行调度。想想看,如果无限期休眠,定时器不叫醒它,那谁把它叫醒?长眠不起可就坏了。其次,timeout不能是负数,这没啥意义。
定时器的创建、激活和注销没什么好说的,一切尽在代码中。关键是,process_timeout怎么知道唤醒的是哪个进程?参数是从哪里传入的?接下来看看这个函数的实现:
struct process_timer {
struct timer_list timer;
struct task_struct *task;
};
static void process_timeout(struct timer_list *t)
{
struct process_timer *timeout = from_timer(timeout, t, timer);
wake_up_process(timeout->task);
}
看到from_timer这条语句,其实能猜出个八九了。timer_list就像list_head一样,内核是将定时器结构attach在真正要传入回调函数的参数旁边,而不是向定时器中塞入参数。用之前讲过的container_of技术,就可以拿到包含定时器结构的父结构指针。这可比变长参数优雅多了,再一次被Linux开发者的智慧所折服!
#define from_timer(var, callback_timer, timer_fieldname) \
container_of(callback_timer, typeof(*var), timer_fieldname)