深入理解linux内核-其他-定时测量

Linux内核必须完成两种主要的定时测量,我们可对此加以区别:

  1. 保存当前的时间和日期,以便能通过time,ftime,gettimeofday系统调用把它们返回给用户程序,也可由内核本身把当前时间作为文件和网络包的时间戳。
  2. 维持定时器,这种机制能告诉内核或用户程序某一时间间隔已经过去了。

定时测量是由基于固定频率震荡器和计数器的几个硬件电路完成的。前两节描述建立定时机制的硬件设备,给出Linux计时体系结构的总体概貌;接下来描述内核中与时间相关的主要任务:实现CPU分时,更新系统时间,资源使用统计数,维护软定时器。最后一节讨论与定时测量相关的系统调用及相应的服务例程。

时钟和定时器电路

在80x86体系结构上,内核必须显式地与几种时钟和定时器电路打交道。时钟电路同时用于跟踪当前时间和产生精确的时间度量。定时器电路由内核编程,所以它们以固定的,预先定义的频率发出中断。这样周期性的中断对实现内核和用户程序使用的软定时器是至关重要的。

实时时钟(RTC)

所有的PC都包含一个叫实时时钟(RTC)的时钟,它是独立于CPU和所有其他芯片的。即使当PC被切断电源后,RTC还继续工作,因为它靠一个小电池或蓄电池供电。CMOS RAM和RTC被集成在一个芯片上。

RTC能在IRQ8上发出周期性的中断,频率在2~8192Hz之间。也可对RTC进行编程以使当RTC到达某个特定的值时激活IRQ8线,也就是作为一个闹钟来工作。

Linux只用RTC来获取时间和日期,不过,通过对/dev/rtc设备文件进行操作,也允许进程对RTC编程。内核通过0x70和0x71 I/O端口访问RTC。系统管理员通过执行Unix系统时钟程序(直接作用于这两个I/O端口)可以设置时钟。

时间戳计数器(TSC)

所有的80x86微处理器都包含一条CLK输入引线,它接收外部振荡器的时钟信号。从Pentium开始,80x86微处理器就都包含一个计数器,它在每个时钟信号到来时加1。该计数器是利用64位的时间戳计数器(TSC)寄存器来实现的,可通过汇编语言指令rdtsc读这个寄存器。
当使用这个寄存器时,内核必须考虑到时钟信号的频率:如,如时钟节拍的频率是1GHz,那么时间戳计数器每纳秒增加一次。

与可编程间隔定时器传递来的时间测量相比,Linux利用这个寄存器可获得更精确的时间测量。为了做到这点,Linux在初始化系统的时候必须确定时钟信号的频率。事实上,因为编译内核时并不声明这个频率,所以同一内核映像可运行在产生任何时钟频率的CPU上。

算出CPU实际频率的任务是在系统初始化期间完成的。calibrate_tsc通过计算一个大约在5ms的时间间隔内所产生的时钟信号的个数来算出CPU实际频率。通过适当地设置可编程间隔定时器的一个通道来产生这个时间常量。

可编程间隔定时器(PIT)

IBM兼容PC还包含了第三种类型的时间测量设备,叫可编程间隔定时器。PIT的作用类似于微波炉的闹钟。这个设备通过时钟中断来通知内核又一个时间间隔过去了。与闹钟的另一个区别是, PIT永远以内核确定的固定频率不停地发出中断。每个IBM兼容PC都至少包含一个PIT,PIT通常是使用0x40~0x43 I/O端口的一个8254CMOS芯片。

下一节将看到,Linux给PC的第一个PIT进行编程,使它以(大约)1000Hz的频率向IRQ0发出时钟中断,即每1ms产生一次时钟中断。
这个中断间隔叫做一个节拍,它的长度以纳秒为单位存放在tick_nsec变量中。在PC上,tick_nsec被初始化为999848ns(产生的时钟信号频率大约为1000.15Hz),但如计算机被外部时钟同步的话,它的值可能被内核自动调整。

一般,段的节拍产生较高分辨度的定时器,当这种定时器执行同步I/O多路复用时,有助于多媒体的平滑播放和较快的响应时间。不过,短的节拍需要CPU在内核态花费较多时间,,导致用户程序运行的稍慢些。

时钟中断的频率取决于硬件体系结构。较慢的机器,其节拍大约为10ms,较快的机器,其节拍大约为1ms。在Linux代码中,有几个宏产生决定时钟中断频率的常量,对此讨论如下:

  1. HZ产生每秒时钟中断的近似个数,也就是时钟中断的频率。在IBM PC上,这个值设置为1000。
  2. CLOCK_TICK_RATE产生的值为1193182,这个值是8254芯片的内部振荡器频率。
  3. LATCH产生CLOCK_TICK_RATE和HZ的比值再四舍五入后的整数值。这个值用来对PIT编程。

PIT由setup_pit_timer进行如下的初始化:

spin_lock_irqsave(&i8253_lock, flags);
outb_p(0x34, 0x43);
udelay(10);
outb_p(LATCH & 0xff, 0x40);
udelay(10);
outb(LATCH >> 8, 0x40);
spin_unlock_irqrestore(&i8253_lock, flags);

out_b:它把第一个操作数拷贝到由第二个操作数指定的I/O端口。
outb_p,它会通过一个空操作而产生一个暂停,以避免硬件难以分辨。
udelay引入一个更短的延迟。第一条outb_p让PIT以新的频率产生中断。接下来两条outb_p和outb为设备提供新的中断频率。把16位LATCH常量作为两个连续的字节发送到设备的8位I/O端口0x40。结果,PIT将以大约1000Hz的频率产生时钟中断。

CPU本地定时器

在最近80x86微处理器的本地APIC中还提供了另一种定时测量设备:CPU本地定时器。CPU本地定时器是一种能产生单步中断或周期性中断的设备,它类似于方才描述的可编程间隔定时器,不过还有几点区别:

  1. APIC计数器是32位,而PIC计数器是16位;因此,可对本地定时器编程来产生很低频率的中断。
  2. 本地APIC定时器把中断只发送给自己的处理器,而PIT产生一个全局性中断,系统中的任一CPU都可对其处理。
  3. APIC定时器是基于总线时钟信号的。每隔1,2,4,8,16,32,128总线时钟信号到来时对该定时器进行递减可实现对其编程的目的。相反,PIT有其自己内部时钟振荡器,可更灵活地编程。

高精度事件定时器(HPET)

高精度事件定时器是由Intel和Microsoft联合开发的一种新型定时器芯片。Linux 2,6能支持它们。HPET提供了许多可被内核使用的硬定时器。这种新定时器芯片主要包含8个32位或64位的独立计数器。每个计数器由它自己的时钟信号所驱动,该时钟信号的频率必须至少为10MHz。故,计数器最少可每100ns增长一次。任何计数器最多可与32个定时器相关联,每个定时器由一个比较器和一个匹配寄存器组成。比较器是一组用于检测计数器中的值与匹配寄存器中的值是否匹配的电路,如找到一组匹配值就产生一个硬件中断。一些定时器可被激活来产生周期性中断。

可通过映射到内存空间的寄存器来对HPET芯片编程。BIOS在自举阶段建立起映射并向操作系统内核报告它的起始内存地址。HPET寄存器允许内核对计数器和匹配寄存器的值进行读或写,允许内核对单步中断进行编程,还允许内核在支持HPET的定时器上激活或禁止周期性中断。下一代主板很可能同时含HPET和8254 PIT。

ACPI电源管理定时器

包含在几乎所有基于ACPI的主板上。它的时钟信号拥有大约为3.58MHz的固定频率。该设备实际上是一个简单的计数器,它在每隔时钟节拍到来时增加一次。为读取计数器的当前值,内核需访问某个I/O端口,该I/O端口的地址由BIOS在初始化阶段确定。

如操作系统或BIOS可通过动态降低CPU的工作频率或工作电压来节省电池的电能,则ACPI电源管理定时器就比TSC更优越。当发生这种情况时,TSC的频率发生改变(将造成时间偏差和其他一些不良效果),而ACPI PMT的频率不会改变。另一方面,TSC计数器的高频率非常便于测量特别小的时间间隔。

如系统中存在HPET设备,则比起其他电路而言它总是成为首选。因为它功能更强。

Linux计时体系结构

内核周期性地:

  1. 更新自系统启动以来经过的时间
  2. 更新时间和日期
  3. 确定当前进程在每个CPU上已运行了多长时间。
  4. 更新资源使用计数。
  5. 检查每个软定时器的时间间隔是否达到。

Linux的计时体系结构是一组与时间流相关的内核数据结构和函数。实际上,基于80x86多处理器机器所具有的计时体系结构与单处理器机器所具有的稍有不同:

  1. 单处理器系统上,所有的计时活动都是由全局定时器产生的中断触发的。
  2. 在多处理器系统上,所有普通的活动都是由全局定时器产生的中断触发的,而具体的CPU的活动(像监控当前运行进程的执行时间)由本地APIC定时器产生的中断触发。

Linux的计时体系结构还依赖于时间戳计数器,ACPI电源管理定时器,高精度时间定时器的可用性。内核使用两个基本的计时函数:一个保持当前最新的时间,一个计算在当前秒内走过的纳秒数。

有几种不同方式获得后一个值:如CPU有TSC或HPET,可用一些更精确的方法。其他情况下,使用精确性差些的方法。

计时体系机构的数据

我们描述80x86体系结构下最重要的变量。

定时器对象

timer_opts

字段名说明
name标识定时器源的一个字符串
mark_offset记录上一个节拍的准确时间,由时钟中断处理程序调用
get_offset返回自上一个节拍开始所经过的时间
monotonic_clock返回自内核初始化开始所经过的纳秒数
delay等待指定数目的循环

mark_offset由时钟中断处理程序调用,以适当的数据结构记录每个节拍到来时的准确时间。get_offset用已记录的值来计算自上一次时钟中断以来经过的时间(以us为单位)。

cur_timer存放了某个定时器对象的地址,该定时器是系统可利用的定时器资源中"最好的"。

定时器对象名称说明定时插补延迟
timer_hpet高精度事件定时器HPETHPET
timer_pmtmrACPI电源管理定时器ACPI PMTTSC
timer_tsc时间戳计数器TSCTSC
timer_pit可编程间隔定时器PIT紧致循环
timer_none普通虚拟定时器资源紧致循环

jiffies变量

记录自系统启动以来产生的节拍总数。由于使用了time_after,time_after_eq,time_before,time_before_eq,内核很好的处理了jiffies变量的溢出。

jiffies被初始化为0xfffb6c20,它是一个32位有符号值,等于-300000。因此,计数器将会在系统启动后的5分钟内处于溢出状态。使那些不对jiffies做溢出检测的有缺陷代码在开发阶段被及时发现。
80x86中,jiffies通过连接器被换算成一个64位计数器的低32位,这个64位计数器称作jiffies_64。

unsigned long long get_jiffies_64(void)
{
	unsigned long seq;
	unsigned long long ret;
	do{
		seq = read_seqbegin(&xtime_lock);
		ret = jiffies_64;
	} while(read_seqretry(&xtime_lock, seq));
	return ret;
}

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

xtime变量

xtime存放当前时间和日期。

tv_sec
	存放自1970.1.1午夜以来经过的秒数
tv_nesc
	存放自上一秒开始经过的纳秒数

通常每个节拍更新一次。

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

单处理器系统上,所有与定时有关的活动都是由IRQ线0上的可编程间隔定时器产生的中断触发的。Linux中,某些活动都尽可能在中断产生后立即执行,其余活动延迟。

初始化阶段

内核初始化期间,time_init函数被调用来建立计时体系结构,通常执行如下:

  1. 初始化xtime。用get_cmos_time从实时时钟上读取自1970年1月1日午夜以来经过的秒数。设置xtime的tv_nsec,
  2. 初始化wall_to_monotonic。它存放将被加到xtime上的秒数和纳秒数,以此来获得单向的时间流。外部时钟的闰秒和同步都可能突发低改变xtime的tv_sec和tv_nsec,使得它们不再是单向递增。
  3. 如内核支持HPET,将调hpet_enable来确认ACPI固件是否探测到该芯片并将它的寄存器映射到内存地址空间中。如结果是肯定,则hpet_enable将对HPET的第一个定时器编程使其以每秒1000次的频率引发IRQ0处的中断。否则,内核将使用PIT:该芯片已经被init_IRQ编程,使得它以每秒1000次的频率引发IRQ0处的中断。
  4. 调select_timer来挑选系统中可利用的最好的定时器资源,并设置cur_timer变量指向该定时器资源对应的定时器对象的地址。
  5. 调setup_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

  1. 在xtime_lock顺序锁上产生一个write_seqlock
  2. 执行cur_timer的mark_offset。
    2.1. cur_timer指向timer_hpet对象,这种情况下,HPET芯片作为时钟中断源。make_offset检查自上一节拍以来是否丢失时钟中断,相应地更新jiffies_64。记录HPET周期计数器当前值。
    2.2. cur_timer指向timer_pmtmr:PIT芯片作为时钟中断源,但内核用APIC电源管理定时器以更高的分辨率来测量时间。make_offset检查自上一个节拍以来是否丢失时钟中断,如丢失,则更新jiffies_64。它记录APIC电源管理定时计数器当前值。
    2.3. cur_timer指向timer_tsc对象:PIT芯片作为时钟中断源,但是内核用时间戳计数器以更高的分辨率来测量时间。make_offset检查自上一个节拍以来是否丢失时钟中断,如丢失,则更新jiffies_64。它记录TSC计数器当前值。
    2.4. cur_timer指向timer_pit:PIT芯片作为时钟中断源,除此之外没别的定时器电路。mark_offset啥也不做。
  3. 调do_timer_interrupt
    3.1. 使jiffies_64增1.
    3.2. 调update_times更新系统日期和时间。
    3.3. 调update_process_times
    3.4. 调profile_tick
    3.5. 如使用外部时钟来同步系统时钟,则每隔660s调一次set_rtc_mmss来调整实时时钟。
  4. 调write_sequnlock释放xtime_lock顺序锁
  5. 返回值1,报告中断已经被有效地处理了。

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

多处理器系统可依赖两种不同的时钟中断源:可编程间隔定时器,高精度事件定时器,CPU本地定时器产生的中断。

Linux2.6中,PIT或HPET产生的全局时钟中断触发不涉及具体CPU的活动,如,处理软定时器,保持系统时间的更新。一个CPU本地时钟中断触发涉及本地CPU的计时活动,如,监视当前进程的运行时间,更新资源使用统计数

初始化阶段

全局时钟中断处理程序由time_init初始化。Linux内核为本地时钟中断保留第239号中断向量。在内核初始化阶段,函数apic_intr_init根据第239号向量和低级中断处理程序apic_timer_interrupt的地址设置IDT的中断门。每个APIC必须被告知多久产生一次本地时钟中断。calibrate_APIC_clock通过正在启动的CPU的本地APIC来计算一个节拍内收到了多少个总线时钟信号。然后这个确切的值被用来对本地所有APIC编程,由此在每个节拍产生一次本地时钟中断。这是由setup_APIC_timer完成的。

所有本地APIC定时器都是同步的,因为它们都基于公共总线时钟信号。意味着用于引导CPU的calibrate_APIC_clock计算出来的值对系统中的其他CPU同样有效

全局时钟中断处理程序

SMP版本的timer_interrupt与UP版本的该处理程序在几个地方有差异:

  1. timer_interrupt调do_timer_interrupt向I/O APIC芯片的一个端口写入,以应答定时器的中断请求。
  2. update_process_times不被调用,因为该函数执行与特定CPU相关的一个操作。
  3. profile_tick不被调用,因为该函数执行与特定CPU相关的操作。

本地时钟中断处理程序

该处理程序执行系统中与特定CPU相关的计时活动,即监管内核代码并检测当前进程在特定CPU上已经运行了多长时间。apic_timer_interrupt等价于:

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逻辑号
  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统计数。系统管理员通过写入/proc/profile可修改内核代码监管器的抽样频率。为实现修改,内核改变本地时钟中断产生的频率。smp_local_timer_interrupt保持每个节拍精确调用update_process_times一次。

更新时间和日期

用户程序从xtime变量中获得当前时间和日期。内核必须周期性地更新该变量,才能使它的值保持相当的精确。

void update_times(void)
{
	unsigned long ticks;
	ticks = jiffies - wall_jiffies;
	if(ticks){
		wall_jiffies += ticks;
		update_wall_time(ticks);
	}
	calc_load(ticks);
}

对丢失的定时器中断的检查在cur_timer的mark_offset中完成。update_wall_time连续调update_wall_time_one_tick ticks次,每次调用都给xtime.tv_nsec加上1000000。如xtime.tv_nsec大于999999999,则update_wall_time更新xtime的tv_sec。

更新系统统计数

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

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

更新本地CPU统计数

update_process_times:

  1. 检查当前进程运行了多长时间。时钟中断发生时,根据当前进程运行在用户态还是内核态,选择调account_user_time还是account_system_time
    1.1. 更新当前进程描述符的utime或stime。在进程描述符中提供两个被称作cutime和cstime的附加字段,分别来统计子进程在用户态和内核态下所经过的CPU节拍数。
    1.2. 检查是否已达到总的CPU时限。如是, 向current进程发SIGXCPU和SIGKILL。
    1.3. 调account_it_virt和account_it_prof来检查进程定时器
    1.4. 更新一些内核统计数
  2. 调raise_softirq来激活本地CPU上的TIMER_SOFTIRQ任务队列
  3. 如必须回收一些老版本的,受RCU保护的数据结构,则检查本地CPU是否经历了静止状态并调tasklet_schedule来激活本地CPU的rcu_tasklet任务队列。
  4. 调scheduler_tick,函数使当前进程的时间片计数器减1,并检查计数器是否已减到0。

记录系统负载

任何Unix内核都要记录系统进行了多少CPU活动。这些统计数据由各种管理实用程序来使用。用户输入uptime后可看到一些统计数据:如相对于最后1分钟,5分钟,15分钟的平均负载。单处理器系统上,值0意味着没活跃的进程。值1意味着一个单独的进程100%占有CPU。值大于1说明几个运行着的进程共享CPU。

监管内核代码

Linux包含一个被称作readprofiler的最低要求的代码监管器,Linux开发者用其发现内核在内核态什么地方花费时间。

监管器基于非常简单的蒙特卡洛算法:每次时钟中断发生时,内核确定该中断是否发生在内核态。如是,内核从堆栈取回中断发生前eip的值,并用这个值揭示中断发生前内核在做什么。采样数据积聚在"热点"上。

profile_tick为代码监管器采集数据。为激活代码监管器,Linux内核启动时必须传递"profile=N",这里2^N标识要监管的代码段大小。采集的数据可从/proc/profile读取。可通过修改这个文件来重置计数器。多处理器系统上,修改此文件还可改变抽样频率。

内核开发者用readprofile命令。Linux 2.6内核还包括另一个监管器,oprofile。使用oprofile时,profile_tick调timer_notify收集数据。

检查非屏蔽中断监视器

多处理器系统上,Linux为内核开发者提供另一种功能:看门狗系统。这对于探测引起系统冻结的内核bug可能有用,启动内核时传递nmi_watchdog

看门狗基于本地和I/O APIC一个特性:它们能在每个CPU上产生周期性的NMI中断。

一旦每个时钟节拍到来,所有的CPU,都开始执行NMI中断处理程序;该中断处理程序又调用do_nmi。这个函数获得CPU的逻辑号n,检查irq_stat数组第n项的apic_timer_flags。如工作正常,则第n项的必定不同于前一个NMI中断中读出的值。CPU正常运行时,第n项的apic_timer_irq被本地时钟中断处理程序增加。当NMI中断处理程序检测到一个CPU冻结时,把引起恐慌的信息记录在日志文件, 转储该CPU寄存器的内容和内核栈的内容,最后杀死当前进程。

软定时器和延迟函数

Linux考虑两种类型的定时器:动态定时器,间隔定时器第一种类型由内核使用,间隔定时器可以由进程在用户态创建。对定时器函数的检查总是由可延迟函数进行,内核不能确保定时器函数正好在定时到期时开始执行,只能保证在适当的时间执行它们。对必须严格遵守定时时间的实时应用而言,定时器并不适合。

动态定时器

struct timer_list{
	struct list_head entry;
	unsigned long expires;
	spinlock_t lock;
	unsigned long magic;
	void (*function)(unsigned long);
	unsigned long data;
	tvec_base_t *base;
};

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

  1. 如需要,创建一个新的timer_list
  2. 调init_timer(&t)
  3. 把定时器到期时激活函数地址存入function,设置data。
  4. 尚未插入链表时,赋值expires,调add_timer(&t)插入链表
  5. 已经插入链表时,调mod_timer更新expires。

一旦定时器到期,内核就自动把元素t从它的链表中删除。有时进程应用del_timer,del_timer_sync,del_singleshot_timer_sync显式从定时器链表删除一个定时器。LInux 2.6中,定时器函数总会在第一个执行add_timer或稍后执行mod_timer的那个CPU上运行。

动态定时器与竞争条件

一种凭经验做法是释放资源前停止定时器。

...
del_timer(&t);
X_Release_Resources();
...

多处理器系统上,这段代码是不安全的。因为调del_timer时,定时器函数可能已在其他CPU上运行了。为避免这种竞争条件,内核提供了del_timer_sync:从链表删除定时器,检查定时器函数是否还在CPU上运行,如是,等待运行结束。如内核开发者知道定时器函数从不重新激活定时器,就能使用更简单更快速的del_singleshot_timer_sync来使定时器无效,并等待直到定时器函数结束。

也存在其他种类的竞争条件:如,修改已激活定时器expires正确方法是调mod_timer,而非删除再创建。后一种途径中,要修改同一定时器expires的两个内核控制路径可能交错在一起。定时器函数在SMP上的安全实现是通过每个timer_list对象包含的lock达到:内核访问动态定时器链表时,需禁止中断,获取自旋锁。

动态定时器的数据结构

把expires值划分成不同的大小,并允许动态定时器从大expires值的链表到小expires值的链表进行有效的过滤。多处理器系统中活动的动态定时器集合被分配到各个不同的CPU中。

动态定时器的主要数据结构是一个叫tvec_bases的每CPU变量:它包含NR_CPUS个元素,系统中每个CPU各有一个。每个元素是一个tvec_base_t的结构:

typedef struct tvec_t_base_s{
	spinlock_t lock;
	unsigned long timer_jiffies;
	struct timer_list* running_timer;
	tvec_root_t tv1;
	tvec_t tv2;
	tvec_t tv3;
	tvec_t tv4;
	tvec_t tv5;
} tvec_base_t;

tvec_root_t包含一个vec数组,数组由256个list_head元素组成。结构包含了在紧接着到来的255个节拍内将要到期的所有动态定时器。字段tv2,tv3,tv4结构都是tvec_t,该类型有一个数组vec(包含64个list_head)。这些链表包含在紧接着到来的214-1,220-1,2^26-1个节拍内将到期的所有动态定时器。字段tv5,vec数组最后一项是一个大expires字段值的动态定时器链表。timer_jiffies表示需检查的动态定时器的最早到时时间。多处理器系统中,running_timer指向由本地CPU当前正在处理的动态定时器的timer_list结构。

动态定时器处理

Linux 2.6中该活动由可延迟函数执行。即由TIMER_SOFTIRQ软中断执行。run_timer_softirq是与TIMER_SOFTIRQ软中断请求相关的可延迟函数。它实质上执行如下操作:

  1. 把与本地CPU相关的tvec_base_t地址存放到base
  2. 获得base->lock并禁止本地中断
  3. 开始执行一个while,当base->timer_jiffies大于jiffies时终止每次循环中:
    3.1. 计算base->tv1中索引,索引保存着下一次要处理的定时器index = base->timer_jiffies & 255
    3.2. 如索引值为0:调cascade来过滤动态定时器
    3.3. 使base->timer_jiffies值加1
    3.4. 对base->tv1.vec[index]链表上的每一个定时器,执行它对应的定时器函数。特别是,链表上每个timer_list元素t实质上执行:
    3.4.1. 将t从base->tv1的链表上删除
    3.4.2. 多处理器系统中,将base->running_timer设置为&t
    3.4.3. 设置t.base为NULL
    3.4.4. 释放base->lock,允许本地中断
    3.4.5. 传递t.data为参数,执行定时器函数t.function
    3.4.6. 获得base->lock,禁止本地中断
    3.4.7. 如链表中还有其他定时器,继续处理
    3.5. 链表上所有定时器已经被处理。继续执行最外层while循环的下一次循环。
  4. 最外层的while循环结束。base->running_timer=NULL
  5. 释放base->lock自旋锁,允许本地中断。

调用每个动态定时器函数前,激活中断并释放自旋锁。这保证了动态定时器的数据结构不被交错执行的内核控制路径所破坏。

动态定时器应用:nanosleep系统调用

current->state = TASK_INTERRUPTIBLE;
remaining = schedule_timeout(timespec_to_jiffies(&t)+1);

内核使用动态定时器来实现进程的延时。

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

延迟函数

由于动态定时器通常有很大的设置开销和一个相当大的最小等待时间(1ms),所以设备驱动器使用它会很不方便。这时,内核使用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 nsecs)
{
	unsigned long loops;
	loops = (nsecs*HZ*current_cpu_data.loops_per_jiffy)/1000000000;
	cur_timer->delay(loops);
}

两个函数都依赖于cur_timer的delay,它接收"loops"中的时间间隔作为参数。不过每次"loops"精确的持续时间取决于cur_timer涉及的定时器对象。

  1. 如cur_timer指向timer_hpet,timer_pmtmr和timer_tsc,则一次"loop"对应一个CPU循环–也就是两个连续CPU时钟信号间的时间间隔
  2. 如cur_timer指向timer_none或timer_pit,则一次"loop"对应于一条紧凑指令循环在一次单独的循环中所花费的时间。

初始化阶段,select_timer设置好cur_timer后,内核通过calibrate_delay决定一个节拍里有多少次"loop"。值保存在current_cpu_data.loops_per_jiffy中,这样udelay,ndelay能根据它来把微妙和纳秒转换成"loops"。

如可利用HPET或TSC硬件电路,则cur_timer->dalay使用它们来获取精确的时间测量。否则,该方法执行一个紧凑指令循环的loops次循环。

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

time和gettimeofday系统调用

time
	返回从1970年1月1日午夜开始所走过的秒数
gettimeofday
	返回从1970年1月1日午夜开始所走过的秒数及前一秒内走过的微妙数,
	值存放在数据结构timeval中。

另一个被广泛使用的函数ftime不再作为一个系统调用来执行,它返回从1970年1月1日午夜开始所走过的秒数与前1秒内所走过的毫秒数。
do_gettimeofday:

  1. 为读操作获取xtime_lock
  2. 调cur_timer的get_offset来确定自上次时钟中断走过的微妙数
    2.1. 如cur_timer指向timer_hpet,将HPET计数器的当前值与上一次时钟中断处理程序执行时在同一个计数器中保存的值比较。
    2.2. 如cur_timer指向timer_pmtmr,将ACPI PMT计数器的当前值与上一次时钟中断处理程序执行时在同一个计数器里保存的值比较。
    2.3. 如cur_timer指向timer_tsc,将时间戳计数器的当前值与上一次时钟中断处理程序执行时在同一个TSC里保存的值比较
    2.4. 如cur_timer指向timer_pit,读取PIT计数器的当前值来计算自上一次PIT时钟中断以来走过的微妙数。
  3. 如某定时器中断丢失,该函数为usec加上相应的延迟:usec += (jiffies - wall_jiffies) * 1000;
  4. 为usec加上前1秒内走过的微妙数usec += (xtime.tv_nsec / 1000);
  5. 将xtime的内容复制到系统调用参数tv指定的用户空间缓冲区中,并给微妙字段的值加上usec: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++;
}

拥有root权限的用户态下的进程可用stime,settimeofday来修改系统当前日期和时间。

adjtimex

网络定时协议以来adjtimex

settimer和alarm

Linux允许用户态的进程激活一种叫间隔定时器的特殊定时器。这种定时器引起的Unix信号被周期性地发送到进程。间隔定时器:

  1. 发送信号所必须的频率,或如只需产生一个信号,则频率为空。
  2. 在下一个信号被产生以前所剩余的时间settimer:
ITIMER_REAL
	真正过去的时间;进程接收SIGALRM
ITIMER_VIRTUAL
	进程在用户态下花费的时间;进程接收SIGVTALRM
ITIMER_PROF
	进程既在用户态下又在内核态下花费的时间;进程接收SIGPROF

为能实现每种策略,进程描述符要包含3对字段:

  1. it_real_incr和it_real_value
  2. it_virt_incr和it_virt_value
  3. it_prof_incr和it_prof_value
    每对中第一个字段存放着两个信号之间以节拍为单位的间隔;另一个字段存放着定时器当前值。ITIMER_REAL利用动态定时器实现。每个进程描述符包含一个real_timer的动态定时器对象。

ITIMER_VIRTUAL和ITIMER_PROF,只有当进程运行时,它们才能被更新。update_process_times在单处理器上由PIT时钟中断处理程序调用;多处理器上由本地时钟中断处理程序调用。

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

POSIX 1003.1b为用户态程序引入了一种新型软定时器,尤其针对多线程和实时应用程序。这些定时器常称为POSIX定时器。

要执行每个POSIX定时器,需向用户态程序提供一些POSIX时钟。即虚拟时间源预定义了分辨度和属性。只要应用程序想使用POSIX定时器,就创建一个新的定时器资源并指定一个现存的POSIX时钟来作为定时基准。Linux 2.6内核提供两种类型的POSIX时钟:

CLOCK_REALTIME
	该虚拟时钟表示系统的实时时钟--本质上是xtime的值
CLOCK_MONOTONIC
	该虚拟时钟表示由于与外部时间源的同步,
	每次回到初值的系统实时时钟。
	实际上该虚拟时钟由xtime和wall_to_monotonic两个变量的和表示。

Linux内核使用动态定时器来实现POSIX。相比传统间隔定时器:

  1. 传统间隔定时器到期时,内核会发一个SIGALRM信号给进程。一个POSIX定时器到期时,内核可发各种信号给整个多线程应用程序,即可发送给单个指定的线程。内核还能在应用程序的某个线程上强制执行一个通告器函数,或什么都不做。
  2. 如一个传统间隔定时器到期了很多次但用户态进程不能接收SIGALRM,则只有第一个信号被接收到,其他信号丢失。对POSIX定时器同样,但进程可调timer_getoverrun来得到自第一个信号以来产生定时器到期的次数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

raindayinrain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值