深入理解 Linux 内核---定时测量

很多计算机化的活动都是由定时测量驱动的,这常常对用户不可见。

Linux 内核必须完成两种主要的定时测量:

  • 保存当前的时间和日期,可由 time()、ftime()、gettimeofday() 返回给用户程序,也可由内核本身把当前时间作为文件和网络包的时间戳。
  • 维持定时器,告诉内核或用户程序某一时间间隔已经过去了。

定时测量是由基于固定频率振荡器计数器的几个硬件电路完成的。

时钟和定时器电路

实时时钟(RTC)

独立于 CPU 和其他芯片。即使 PC 被切断电源,RTC 仍能工作。

RTC 能在 IRQ8 上发出周期性中断,也可对 RTC 编程使之在特定时间激活 IRQ8 线。

Linux 只用 RTC 获取时间和日期,但通过对 /dev/rtc 设备文件操作,也允许对 RTC 编程。内核通过 0x70 和 0x71 I/O 端口访问 RTC。

时间戳计数器(TSC)

64 位的时间戳计数器,在每个时钟信号到来时加 1。rdtsc 汇编指令读该寄存器。

为了比可编程间隔定时器精确,Linux 在初始化系统的时候必须确定时钟信号的频率。calibrate_tsc() 通过计算一个大约在 5ms 的时间间隔内产生的时钟信号的个数算出 CPU 的实际频率。

可编程间隔定时器(PIT)

通过时钟中断通知内核有一个时间间隔过去了。PIT 永远以内核确定的固定频率发出中断。

Linux 给 PC 的第一个 PIT 编程,使它以大约 1000 Hz 的频率向 IRQ0 发出时钟中断,即每 1ms 产生一次时钟中断。该时间间隔为一个节拍,以纳秒为单位存放在 tick_nsec 变量中,被初始化为 9999848ns,如果被计算机外部时钟同步,可被内核自动调整。

短的节拍可产生较高分辨度的定时器。Linux 代码中,有几个宏产生决定时钟中断频率的常量:

  • HZ 产生每秒钟时钟中断的近似个数,即时钟中断频率。IBM PC 上,为 1000。
  • CLOCK_TICK_RATE 产生的值为 1193182,为 8254 芯片的内部振荡器频率。
  • LATCH 产生 CLOCK_TICK_RATE 和 HZ 的比值四舍五入后的整数值。

PIT 由 setup_pit_timer() 进行初始化

// 使 PIT 以大约 1000 Hz 的频率产生时钟中断,即每 1ms 产生一次时钟中断

setup_pit_timer()
spin_lock_irqsave(&i8253_lock, flags);

// 类似于 outb(),但会通过一个同操作而产生一个暂停,以避免硬件难以分辨
// 让 PIT 以新的频率产生中断
outb_p(0x34, 0x43);  

// 接下来的两条 outb_p()、out_b() 为设备提供新的中断频率
// 将 16 位 LATCH 常量作为两个连续的自己发送到设备的 8 位 I/O 端口 0x40
udelay(10);   // 引入一个更短的延迟
outb_p(LATCH & 0xff, 0x40);
udelay(10);
outb(LATCH >> 8, 0x40);   // 将第一个操作数拷贝到第二个操作数指定的 I/O 端口

spin_unlock_irqrestore(&i8253_lock, flags);

CPU 本地定时器

APIC 计数器是 32 位,而 PIC 计数器是 16 位;所以可对本地定时器编程产生很低频率的中断。

本地 APIC 定时器只把中断发送给自己的处理器,而 PIT 产生一个全局性中断。

APIC 定时器是基于总线时钟信号的,PIT 有自己的内部时钟振荡器,可更灵活地编程。

高精度事件定时器(HPET)

在终端用户机器上并不普遍。

主要包含 8 个 32 位或 64 位的独立计数器。每个计数器由自己的时钟信号驱动,时钟信号频率必须至少为 10MHz。每个计数器最多可与 32 个定时器关联,每个定时器由一个比较器和一个匹配寄存器组成。

可通过映射到内存空间的寄存器低 HPET 芯片编程。

ACPI 电源管理定时器(ACPI PMT)

是一个简单的计数器,在每个时钟节拍到来时增加一次。

如果操作系统或 BIOS 通过动态降低 CPU 的工作频率或工作电压来解释电池的电能,TSC 的频率会改变,而 ACPI PMT 的频率不会变。但 TSC 计数器的高频率便于测量特别小的时间间隔。

Linux 计时体系结构

是一组与时间流相关的内核数据结构和函数。

  • 单处理系统上,计时活动由全局定时器产生的中断触发。
  • 多处理器系统上,普通活动由全局定时器产生的中断触发,具体 CPU 活动由本地 APIC 定时器产生的中断触发。

Linux 计时器体系结构还依赖于 TSC、ACPI PMT、HPET 的可用性。内核使用两个基本的计时函数:

  • 保持当前最新的时间。
  • 计算在当前秒内走过的纳秒数。可通过 TSC 或 HPET 等方式获得。

计时体系的数据结构

定时器对象

timer_opts

  • name
  • mark_offset,记录上一个节拍的准确时间,由时钟中断处理程序调用
  • get_offset,返回自上一个节拍开始所经过的时间
  • monotonic_clock
  • delay

mark_offset、get_offset 使得 Linux 计时体系结构能够以比节拍周期更高的精度测定当前的时间,这种操作称为“定时插补”

变量 cur_timer 存放了某个定时器对象的地址。

  • 最初,cur_timer 指向 timer_none,timer_none 是一个虚拟的定时器资源对象,内核在初始化的时候用它。
  • 在内核初始化期间,select_timer() 函数将 cur_timer 设置为指向适当定时器对象的地址。select_timer() 按优先级选则的顺序:HPET、ACPI PMT、TSC、PIT。

本地 APIC 定时器没有对应的定时器对象。因为本地 APIC 定时器仅用来产生周期性中断而不用来获得子节拍的分辨度。

jiffies 变量

jiffies 变量是一个计数器,记录自系统启动以来产生的节拍总数。jiffies 是一个 32 位的变量,每隔约 50 天会回绕到 0。

jiffies 在系统启动时初始化为 0xfffb6c20,等于 -300000,系统启动 5 分钟内处于溢出状态,使得不对 jiffies 作溢出检测的有缺陷的内核代码在开发阶段被及时发现。

内核有时需获得自系统启动以来产生的系统节拍的真实数目,因此,80x86 系统中,jiffies 变量通过连接器被换算为一个 64 位计数器 jiffies_64 的低 32 位,1 ms 一个节拍的情况下,数十亿年后才会回绕。

get_jiffies_64() 函数用来读取并返回 jiffies_64 的值:

unsigned long long get_jiffies_64(void)
{
	unsigned long seq;
	unsigned long long ret;
	do {
		seq = read_seqbegin(&xtime_lock);    // xtime_lock 顺序锁用来保护 64 位的读操作
		ret = jiffies_64;   
	}while(read_seqretry(&xime_lock, seq));  // 一直读 jiffies_64 变量直到确认该变量没有被其他内核控制路径更新
}

在临界区增加 jiffies_64 变量的值时必须使用 write_seqlock(&xtime_lock) 和 write_sequnlock(&xtime_lock) 进行保护。

xtime 变量

xtime 变量存放当前时间和日期:是一个 timespec 类型的数据结构,有两个字段:

  • tv_sec,存放自 1970年1月1日(UTC)午夜以来经过的秒数。
  • tv_nsec,存放在上一秒开始经过的纳秒数(值域范围:0~999999999)。

xtime 每个节拍更新一次。

单处理器系统上的计时体系结构

单处理系统上,所有与定时有关的活动都是由 IRQ0 上的可编程间隔定时器 PIT 产生的中断触发的。

初始化阶段

内核初始期间,time_init() 函数被调用来建立计时体系结构。

  1. 初始化 xtime 变量。get_cmos_time() 从实时时钟 RTC 上读取自 1970年1月1日午夜以来经过的秒数。设置 xime 的 tv_nsec 字段,使得 jiffies 变量的溢出与 tv_sec 字段的增加都落在秒的范围内。
  2. 初始化 wall_to_monotonic 变量。同 xtime 一样是 timespec 类型,但存放将被加到 xtime 上的秒数和纳秒数。
  3. 如果内核支持 HPET,调用 hpet_enable() 函数确认 ACPI 固件是否探测到该芯片并将其寄存器映射到内存地址空间。如果是,hpet_enable() 将对 HPET 芯片的第一个定时器编程,使其以每秒 1000 次的频率引发 IRQ0 处的中断引发 IRQ0 处的中断;否则,不能获得 HPET 芯片,内核将使用 PIT:该芯片已被 init_IRQ() 编程,以每秒 1000 次的频率引发 IRQ 0 处的中断。
  4. 调用 select_timer() 挑选系统中可利用的最好的定时器,将 cur_timer 指向其地址。
  5. 调用 set_irq(0, &irq0) 创建与 IRQ0 相应的中断门,IRQ0 引脚线连接着系统时钟中断源(PIT 或 HPET)。irq0 变量的定义:
struct irqaction irq0 = {timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL}; 
// timer_interrupt() 会在每个节拍到来时被调用,
// 而中断被禁止,因为 IRQ0 主描述符的状态字段中的 SA_INTERRUPT 标志被置位。

时钟中断处理程序

timer_interrupt() 是 PIT 或 HPET 中的中断服务例程(ISR),执行以下步骤:

  1. 在 xtime_lock 顺序锁上产生一个 write_seqlock() 来保护与定时相关的内核变量。
  2. 执行 cur_timer 定时器对象的 mark_offset 方法,有 4 种情况:
    a. cur_timer 指向 timer_hpet 对象:HPET 芯片作为时钟中断源。mark_offset 方法检查自上一个节拍以来是否丢失时钟中断,如果丢失,更新 jiffies_64。接着,记录 HPET 周期计数器的当前值。
    b. cur_timer 指向 timer_pmtmr 对象:PIT 芯片作为时钟中断源,但内核采用 APCI PMT 以更高分辨度测量时间。mark_offset 方法检查自上一个节拍以来是否丢失时钟中断,如果丢失,更新 jiffies_64。接着,记录 APIC PMT 计数器的当前值。
    c. cur_timer 指向 timer_tsc 对象:PIT 芯片作为时钟中断源,但内核采用 TSC 以更高分辨度测量时间。mark_offset 方法检查自上一个节拍以来是否丢失时钟中断,如果丢失,更新 jiffies_64。接着,记录 TSC 计数器的当前值。
    d. cur_timer 指向 timer_pit 对象:PIT 芯片作为时钟中断源,mark_offset 什么方法也不做。
  3. 调用 do_timer_interrupt() 函数,执行以下操作:
    a. 使 jiffies_64 值增 1.
    b. 使用 update_times() 更新系统日期和时间,并计算当前系统负载。
    c. 调用 update_process_times() 为本地 CPU 执行几个与定时相关的计数操作。
    d. 调用 profile_tick()。
    e. 如果使用外部时钟同步系统时钟,则每隔 660 秒调用一次 set_rtc_mmss() 调整实时时钟。
  4. 调用 write_sequnlock() 释放顺序锁。
  5. 返回 1,报告中断已被有效处理。

多处理其系统上的计时体系结构

多处理器系统可依赖两种不同的时钟中断源:PIT 或 HPET,及 APIC 计时器 产生的中断。

Linux 2.6 中,PIT 或 HPET 产生的全局时钟中断不涉及具体 CPU 的活动,而 APIC 时钟中断涉及本地 CPU 的计时活动。

初始化阶段

全局时钟中断处理程序由 time_init() 函数初始化。

Linux 内核为本地时钟中断保留第 239 号(0xef)中断向量。

内核初始化阶段,

  1. apic_intr_init() 根据第 239 号向量和低级中断处理程序 apic_timer_interrupt() 的地址设置 IDT 的中断门。
  2. calibrate_APIC_clock() 通过正在启动的 CPU 的本地 APIC 来计算一个节拍内收到了多少个总线时钟信号。
  3. setup_APIC_timer() 用第 2 步中的值对本地所有 APIC 编程,使得每个 APIC 在每个节拍产生一次本地时钟中断。

所有本地 APIC 定时器都是同步的,因为它们都基于公共总线时钟信号,因此第 2 步算出的值对系统中的其他 CPU 同样有效。

全局时钟中断处理程序

SMP 版本的 timer_interrupt() 与 UP 版本有几处差异:

  • timer_interrupt() 调用 do_timer_interrupt() 向 I/O APIC 芯片的一个端口写入,以应答定时器的中断请求。
  • update_process_times()、profile_tick() 不被调用,因为它们执行与特定 CPU 相关的操作。

本地时钟中断处理程序

执行与特定 CPU 相关的计时活动。

apic_timer_interrupt:
	pushl $(239-256)
	SAVE_ALL
	movl %esp, %eax
	call smp_apic_timer_interrupt
	jmp ret_from_intr

smp_apic_timer_interrupt() 的高级中断处理函数执行如下步骤:

  1. 获得 CPU 逻辑号(比如 n)。
  2. 使 irq_stat 数组中第 n 项的 apic_timer_irqs 字段加 1。
  3. 应答本地 APIC 上的中断。
  4. 调用 irq_enter() 函数。
  5. 调用 smp_local_timer_interrupt() 函数。
  6. 调用 irq_exit() 函数。

smp_local_timer_interrupt() 执行每个 CPU 的计时活动:

  1. 调用 profile_tick() 函数。
  2. 调用 update_process_times() 检查当前进程运行的时间并更新一些本地 CPU 统计数。

更新时间和日期

用户程序从 xtime 变量中获得当前时间和日期,内核必须周期性地更新该变量。

void update_times(void)
{
	unsigned long ticks;
	ticks = jiffies - vall_jiffies;   

	if(ticks)
	{
		wall_jiffies += ticks;           // 存放 xtime 变量最后更新的时间
		update_wall_time(ticks);
	}
	calc_load(ticks);  // 记录系统负载
}

update_wall_time() 调用 update_wall_time_one_tick() ticks 次

  • 每次调用都给 xtime.tv_nsec 字段加 1000000。
  • 当 tv_nsec 的值大于 999999999,update_wall_time() 会更新 xtime 的 tv_sec 字段。

如果系统发出 adjtimex() 调用,函数可能会调整 1000000 这个值。

更新系统统计计数

内核在与定时相关的其他任务中必须周期性地收集若干数据用于:

  • 检查运行进程的 CPU 资源限制
  • 更新与本地 CPU 工作负载有关的统计数
  • 计算平均系统负载
  • 监管内核代码

更新本地 CPU 统计数

update_process_times() 更新一些内核统计计数,执行以下步骤:

  1. 检查当前进程运行了多长时间。时钟中断发生时,根据当前进程运行在用户态还是内核态,选择调用 account_user_time() 还是 account_system_time(),执行以下步骤:
    a. 更新当前进程描述符的 utime 字段或 stime 字段。进程描述符中的 cutime 和 cstime 附加字段分别用来统计子进程在用户态和内核态下所经过的 CPU 节拍数。为保证效率update_process_times() 只有当父进程询问它的一个子进程的状态时,这些字段才会更新。
    b. 检查是否已达到总的 CPU 时限,如果是,向 current 进程发送 SIGXCPU 和 SIGKILL 信号。
    c. 调用 account_it_virt() 和 account_it_prof() 检查进程定时器。
    d. 更新一些内核统计数,这些统计数存放在每 CPU 变量 kstat 中。
  2. raise_softirq() 激活本地 CPU 上的 TIMER_SOFTIRQ 任务队列。
  3. 如果必须回收一些老版本的、受 RCU 保护的数据结构,那么检查本地 CPU 是否经历了静止状态并调用 tasklet_schedule() 激活本地 CPU 的 rcu_tasklet 任务队列。
  4. scheduler_tick() 将当前进程的时间片计数器减 1,并检查计数器是否已减到 0。

记录系统负载

update_times() 在每个节拍都要调用 calc_load() 计算处于 TASK_RUNNING 或 TASK_UNINTERRUPTIBLE 状态的进程数,并用这个数据更新平均系统负载。

监管内核代码

Linux 包含一个被称为 readprofiler 的最低要求的代码监管器,用来确定内核的“热点”— 执行最频繁的内核代码片段。

监管器基于非常简单的蒙特卡洛断方:每次时钟中断发生时,内核确定该中断是否发生在内核态;如果是,内核总堆栈取回中断发生前 eip 寄存器的值,以判断中断发生前内核正在做什么。

profile_tick() 采集数据。在单处理器系统上被 do_timer_interrupt() 调用;多处理器系统上由 smp_local_timer_interrupt() 调用。

为激活代码监管器,Linux 在内核启动时传递字符串参数“profile=N", 2 N 2^N 2N 表示要监管的代码段的大小。

采集的数据可通过 readprofile 系统命令从 /proc/profile 文件中读取。可通过修改该文件重置计数器;多处理器系统上,还可改变抽样频率。

oprofile 监管器比 readprofile 更灵活、更可定制,还能发现内核代码、用户态程序及系统中的热点。

检查非屏蔽中断(NMI)监视器

在多处理器系统中,看门狗系统可探测引起系统冻结的内核 bug,为激活它,必须在内核启动时传递 nmi_watchdog 参数。

看门狗系统基于本地 I/O APIC,在每个 CPU 上产生周期性的 NMI 中断,且不能被屏蔽。

一旦每个时钟节拍到来,所有 CPU 都必须执行 NMI 中断处理程序;该中断处理程序调用 do_nmi()。

do_nmi():

  • 获得 CPU 的逻辑号 n。
  • 检查 irq_stat 数组第 n 项的 apic_timer_irqs 字段。如果该 CPU 字段正常,第 n 项的 apic_timer_irq 字段会被本地时钟中断处理程序增加,如果计数器没有增加,说明本地时钟中断处理程序在整个时钟节拍期间没有被执行。

当 NMI 中断处理程序检测到一个 CPU 冻结时,就好敲响所有的钟:把引起恐慌的信号记录在系统日志,转储该 CPU 寄存器的内容和内核占,最后杀死当前进程。为内核开发者提供了发现错误的机会。

软定时器和延迟函数

每个定时器都包含一个字段,表示定时器将需要多长时间才到期。该字段的初值是 jiffies 的当前值加上合适的节拍数。每当内核检查定时器时,就把该字段和当前时刻的 jiffies 值比较,如果小于 jiffies,则定时器到期。

动态定时器由内核使用。间隔定时器可由进程在用户态创建。

因为对定时器函数的检查总是由可延迟函数执行,所以不适用于实时应用。

除了软定时器,内核还使用延迟函数,执行一个紧凑的指令循环直到指定的时间间隔用完。

动态定时器

struct timer_list
{
	struct list_head entry;           // 将定时器插入双向循环链表队列种,根据定时器 expires 字段的值将它们分组存放
	unsigned long expires;            // 定时器到期时间,用节拍数表示,<= jiffies 值时,到期
	spinlock_t lock;
	unsigned long magic;
	void (*function)(unsigned long);  // 定时器到期时执行的函数
	unsigned long data;               // 传递给定时器函数的参数,如设备 ID 等
	tvsec_base_t *base;
};

为创建并激活一个动态定时器,内核必须:

  1. 如果需要,创建一个新的 timer_list 对象 t,可通过以下几种方式创建:
  • 在代码种定义一个静态全局变量。
  • 在函数内定义一个局部变量,存放在内核堆栈种。
  • 在动态分配的描述符种包含该对象。
  1. init_timer(&t) 初始化该对象。 t.base = NULL,将 t.lock 自旋锁设为”打开“。
  2. 将定时器到期时激活函数的地址存放 function 字段,如果需要,将传递函数的参数存入 data 字段。
  3. 如果动态定时器还没有被插入链表中,给 expires 赋一个合适的值,add_timer(&t) 将 t 插入到合适的链表中。
  4. 否则,mod_timer() 更新 expires 字段。

定时器到期后,内核会自动把 t 从链表中删除。进程也可通过调用 del_timer()、del_timer_sys() 或 del_singleshot_timer_sync() 显式删除。

Linux 2.6 中,动态定时器需要 CPU 来激活。

动态定时器与竞争条件

释放资源前停止定时器:

...
del_timer(&t);
X_Release_Resource();
...

但在多处理器系统上,调用 del_timer() 时,定时器函数可能已经在其他 CPU 上运行了。结果,当定时器函数还作用在资源上时,资源可能已经被释放。为避免这种竞争条件,内核提供了 del_timer_sync() 函数,从链表中删除定时器,如果定时器函数还在其他 CPU 上运行,等待直到定时器函数结束。

del_timere_sync() 必须小心考虑:定时器函数重新激活自己。如果内核开发者直到定时器函数从不重新激活定时器,可使用更简单快速的 del_singleshot_timer_sync()。

还有其他竞争条件,如修改已激活定时器 expires 字段的方法 mod_timer(),为避免多个内核路径交错,每个 timer_list 对象包含 lock 自旋锁,每当内核访问动态定时器的链表时,就禁止中断并获取该自旋锁。

动态定时器的数据结构

为提高效率,将 expires 值划分成不同的大小,并允许动态定时器按 expires 值从大到小进行有效过滤。此外,在多处理器系统中活动的动态定时器集合被分配到各个不同的 CPU 中。

动态定时器的主要数据结构时一个叫 tvec_bases 的每 CPU 变量:包含 NR_CPUS 个元素,每个 CPU 一个。每个元素是一个 tvec_base_t 类型的数据结构

typedef struct tvsec_t_base_s
{
	spinlock_t lock;
	unsigned long timer_jiffies;       // 需要检查的动态定时器的最早到期时间,如果小于 jiffies,说明前几个节拍相关的可延迟函数必须处理
	struct timer_list *running_timer;  // 指向由本地 CPU 当前正处理的动态定时器的 timer_list 数据结构
	tvec_root_t tvl;   // 包含一个 vec 数组,由 256 个 list_head 元素组成。包含了紧接到来的 255 个节拍内将要到期的所有动态定时器
	tvec_t tv2;  // 有一个 vec 数组,包含 64 个 list_head 元素,包含了紧接到来的 $2^{14}-1$ 个节拍内将要到期的所有动态定时器
	tvec_t tv3;  // 包含了紧接到来的 $2^{20}-1$ 个节拍内将要到期的所有动态定时器
	tvec_t tv4;  // 包含了紧接到来的 $2^{26}-1$ 个节拍内将要到期的所有动态定时器
	tvec_t tv5;  // 包含了一个大 expires 字段值的动态定时器链表
}tvsec_base_t;

动态定时器处理

对软定时器的处理是一种耗时活动,不应由时钟中断处理程序执行,而是由可延迟函数— TIMER_SOFTIRQ 软中断执行。

run_timer_softirq() 是与 TIMER_SOFTIRQ 软中断请求相关的可延迟函数,实际执行如下操作:

  1. 将于本地 CPU 相关的 tvec_base_t 数据结构的地址存放到 base 本地变量中。
  2. 获得 base->lock 自旋锁并禁止本地中断。
  3. while(base->timer_jiffies <= jiffies):
    a. index = base->timer_jiffies & 255; // 该索引保存着下一次将要处理的定时器
    b. if(index == 0),说明 base_tvl 中的所有链表都被检查过了,所以为空,于是调用 cascade() 过滤动态定时器。
    c. base->timer_jiffies += 1;
    d. 对于 base->tv1.vec[index] 链表上的每一个定时器,执行它所对应的定时器函数。链表上的每个 timer_list 元素 t 执行一下步骤:
    (1)将 t 从 base->tv1 的链表上删除。
    (2)多处理器系统中,base->running_timer = &t;
    (3)t.base = NULL;
    (4)释放 base->lock 自旋锁,允许本地中断。
    (5)传递 t.data 作为参数,执行定时器函数 t.function。
    (6)获得 base->lock 自旋锁,并禁止本地中断。
    (7)如果链表中还有其他定时器,则继续处理。
    e. 链表上的所有定时器已经被处理。继续执行 while 循环。
  4. 所有到期的定时器已经被处理,多处理器系统中,base->running_timer = NULL;
  5. 释放 base->lock 自旋锁并允许本地中断。

3.b. 中的 casecade():

if(!index && 
   !casecade(base, &base->tv2, (base->timer_jiffies>>8) & 63)) &&   //  将 base->tv2 中链表上所有动态定时器移到 base->tv1 的适当链表上
   !casecade(base, &base->tv3, (base->timer_jiffies>>14) & 63)) && // 如果 base->tv2 中的链表不为空,返回一个正值;否则,casecade() 再次被调用
   !casecade(base, &base->tv4, (base->timer_jiffies>>20) & 63)) )
	 casecade(base, &base->tv5, (base->timer_jiffies>>26) & 63);

进入最外层循环前,禁止中断并获取 base->lock 自旋锁,调用每个动态定时器函数前,激活中断并释放自旋锁,直到函数运行结束,保证了动态定时器的数据不被交错执行的内核路径破坏。

以上复杂的算法保证了极好的性能。

动态定时器应用之一:nanosleep() 系统调用

nanosleep() 调用服务例程 sys_nanosleep(),它将 timespec 指针作为参数,将调用进程挂起直到特定的时间间隔用完。

sys_nanosleep()
1.先调用 copy_from_user() 将包含在 timespec 结构中的值赋值到局部变量 t 中。
2.current->state = TASK_INTERRUPTIBLE;
remaining = schedule_timeout(timespec_to_jiffies(&t)+1);
3. 如果 schedule_timeout() 返回的值表示进程延时到期(0),系统调用结束;否则,系统调用将自动重新启动。

内核使用动态定时器实现进程的延时, schedule_timeout():

struct timer_list timer;
unsigned long expire = timeout + jiffies;
init_timer(&timer);
timer.expires = expire;
timer.data = (unsigned long) current;
timer.function = process_timeout;
add_timer(&timer);
schedule();  // 进程挂起直到定时器到时
del_singleshot_timer_sync(&timer);  // 进程恢复执行,删除该动态定时器
timeout = expire - jiffies;
return (timeout < 0 ? 0 : timeout);  // 0 表示延时到期,timeout 表示进程被其他原因唤醒,到期时还剩余的节拍数

延时到期时,内核执行下列函数:

void process_timeout(unsigned long __data)  // 将进程描述符指针作为参数
{
	wake_up_process((task_t *)__data);
}

延迟函数

当内核需要等待一个较短时间间隔,无需使用软定时器。这时,可用 udelay() 和 ndelay() 函数:前者参数为微秒级的时间间隔,后者的参数为纳秒级。

void udelay(unsigned long usecs)
{
	unsigned long loops;
	loops = (usecs * HZ * current_cpu_data.loops_per_jiffy) / 1000000;
	cur_timer->delay(loops);
}

void ndelay(unsigned long usecs)
{
	unsigned long loops;
	loops = (nsecs * HZ * current_cpu_data.loops_per_jiffy) / 1000000000;
	cur_timer->delay(loops);
}

每一次“loop”精确的持续时间取决于 cur_timer 涉及的定时器对象

  • 如果 cur_timer 指向 timer_hpet、timer_pmtmr 和 timer_tsc 对象,一次“loop”对应一个 CPU 循环。
  • 如果 cur_timer 指向 timer_none 或 timer_pit 对象,一次“loop”对应于一条紧凑指令循环在一次单独的循环中花费的时间。

在初始化阶段:

  • select_timer() 设置号 cur_timer
  • 内核通过执行 calibrate_delay() 决定一个节拍里有多少次“loop”,存于 current_cpu_data.loops_per_jiffy 变量中
  • udelay() 和 ndelay() 根据它将微妙和纳秒转换成“loops”。

与定时测量相关的系统调用

time() 和 gettimeofday() 系统调用

time() 返回从 1970 年 1月 1日午夜(UTC)开始走过的秒数。
gettimeofday() 返回从 UTC 开始所走过的秒数及在前 1 秒内走过的微妙数,存放于 timeval 中。

gettimeofday() 调用 sys_gettimeofday(),该函数调用 do_gettimeofday(),它执行下列动作:

  1. 为读操作获取 xtime_lock 顺序锁。
  2. usec = cur_timer->getoffset(); 确定自上一次时钟中断以来走过的微妙数。
    cur_timer 可能指向对象 timer_hpet、timer_pmtmr、timer_tsc、timer_pit,分别获取相应计数器的当前值与上一次时钟中断处理程序时的值比较。
  3. 如果某定时器中断丢失,usec += (jiffies - wall_jiffies) * 1000; usec 加上相应的延迟。
  4. usec += (xtime.tv_nsec / 1000); 为 usec 加上前 1 秒内走过的微妙数。
  5. tv->tv_sec = xtime->tv_sec;
    tv->tv_usec = usec;
  6. 在 xtime_lock 顺序锁上调用 read_seqretry(),如果另一条内核控制路径同时为写操作获得了 xtime_lock,跳回步骤 1。
  7. while(tv->tv_usec >= 1000000){
    {
    tv->tv_usec -= 1000000;
    tv->tv_sec++;
    }

adjtimex() 系统调用

通常把系统配置成能在常规基准上运行时间同步协议,如网络定时协议(NTP),在每个节拍逐渐调整时间。这依赖于 adjtimex()。

adjtimex() 接收指向 timex 结构的指针作为参数,用 timex 自动中的值更新内核参数,并返回具有当前内核值的同一结构。
update_wall_time_one_tick() 使用这以内核值对每个节拍中加到 xtime.tv_usec 的微秒进行微调。

setitimer() 和 alarm() 系统调用

间隔定时器可能引起 Unix 信号被周期性地发送到进程,也可能在指定的延时后仅发送一个信号。

setitimer() 可激活间隔定时器,第一个参数指定应当采取下面哪一个策略:

  • ITIMER_REAL,真正过去的时间,进程接收 SIGALRM 信号。
  • ITIMER_VIRTUAL,进程在用户态下花费的时间,进程接收 SIGVTALRM 信号。
  • ITIMER_PROF,进程既在用户态下又在内核态下所花费的时间,进程接收 SIGPROF 信号。

间隔定时器既能一次执行,也能周期循环。

setitimer() 的第二个参数指向一个 itimerval 类型的结构,它指定了定时器初始的持续时间以及定时器被重新激活后使用的持续时间。

setitimer() 的第三个参数是一个指针,可选,指向一个 itimerval 类型的结构,系统调用将先前定时器的参数填充到该结构中。

为分别实现前述每种策略的定时器,进程描述符包含 3 对字段:

  • it_real_incr、it_real_value
  • it_virt_incr、it_virt_value
  • it_prof_incr、it_prof_value

第一个字段存放两个信号之间以节拍为单位的间隔,第二个字段存放定时器当前值。

ITIMER_REAL 间隔定时器利用动态定时器实现,因为及即使进程不运行,内核也能向其发送信号。每个进程描述符包含一个叫 real_timer 的动态定时器对象。setitimer():

  • 初始化 real_timer 字段
  • 调用 add_timer() 将动态定时器插入到合适的链表中
  • 定时器到期时,it_real_fn() 函数向进程发送一个 SIGALRM 信号
  • 如果 it_real_incr 不为空,再次设置 expires 字段,并重新激活定时器

ITIMER_VIRTUAL、ITIMER_PROF 间隔定时器不需要动态定时器,因只有进程运行时才会被更新。

  • account_it_virt()、account_it_prof() 被 update_process_times() 调用
  • update_process_times() 在单处理器系统上被 PIT 的时钟中断处理程序调用,在多处理器上被本地时钟中断处理程序调用
    因此,每个节拍中,这两个间隔定时器都会被更新一次,如果到期,就给当前进程发送一个合适的信号。

alarm() 会在一个指定的时间间隔用完时向调用的进程发送一个 SIGALRM 信号,参数为 ITIMER_REAL 时类似于 setitimer()。

与 POSIX 定时器相关的系统调用

Linux 2.6 内核提高两种类型的 POSIX 时钟:

  • CLOCK_REALTIME,该虚拟时钟表示系统的实时时钟,本质上是 xtime 变量的值。clock_getres() 系统调用返回的分辨度为 999 848ns,1s 内更新 xtime 约 1000 次。
  • CLOCK_MONOTONIC,该虚拟时钟表示由于与外部时间源的同步,每次回到初值的系统实时时钟。实际上,该虚拟时钟由 xtime 和 wal_to_monotonic 两个变量的和表示。分辨度由 clock_getres() 返回,为 999 848ns。

Linux 内核使用动态定时器实现 POSIX 定时器,与 ITIMER_REAL 间隔定时器相似,但更灵活、可靠,区别如下:

  • 一个 POSIX 定时器到期时,内核可以发送各种信号给整个多线程应用程序,也可发送给单个指定线程。
  • 对于 POSIX 定时器,进程可调用 timer_getoverrun() 调用自第一个信号产生以来定时器到期的次数。
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值