Linux内核深入学习 - 定时测量

目录

时钟和定时器电路

实时时钟

计数器

可编程间隔定数

CPU本地定时器

高精度事件定时器HPET

ACPI电源管理定时器

Linux计时体系

计时体系结构的数据结构

定时器对象

jiffies变量

xtime变量

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

初始化阶段

时钟中断处理程序

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

初始化阶段

全局时钟中断处理程序

本地时钟中断处理程序

更新时间和日期

更新系统统计数

更新本地CPU统计数

记录系统负载

监管内核代码

检查非屏蔽中断监视器

软定时器和延迟函数

动态定时器

动态定时器与竞争条件

动态定时器的数据结构

动态定时器处理

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

延迟函数

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

time和gettimeofday系统调用

settimer和alarm


为了准备讨论稍后的进程调度等相关内容(这是因为很多计算机的活动高度依赖定时器),我们需要首先讨论一下定时驱动。这通常对用户并不可见!为了进行这些跟定时高强度相关的操作程序,必须能从每个文件中检索到文件的最后访问时间及时间戳,因此这些的时间标记是必须由定时测量来完成的。

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

  • 保存当前的时间和日期以便能通过time()ftime()gettimeofday()系统调用把它们返回给用户程序,也可以由内核本身把当前时间作为文件和网络包的时间戳维持

  • 定时器这种机制能够告诉内核或用户程序某一时间间隔已经过去了

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

时钟和定时器电路

实时时钟

所有的PC都包含一个叫做实时时钟的时钟,它是独立于CPU和其他芯片的。即使当PC被切断电源,RTC还在工作。因为它靠一个小电池或蓄电池工作,CMOS RAM和RTC被集成在一个芯片上,RTC能在IRQ8上发出周期性的中断,频率在二到8192赫兹之间,也可以对RTC进行编程使得RTC达到某个特定的值时,激活IRQ。也就是作为一个闹钟来工作。Linux只用R TC来获取时间和日期,不过通过对/dev/rtc设备文件进行操作也允许进程对RTC进行编程,内核通过0X70和0X71 IO端口访问RTC时间戳。

计数器

所有的8086微处理器都包括一条clk输入引线,它接受外部振荡器的时钟信号。从奔腾开始,8086微处理器都会包含一个计数器,它在每个时钟信号到来时加一。该计数器是利用64位的时间戳计数器寄存器来实现的。可以通过汇编语言指令读这个寄存器,当使用这个寄存器时,内核必须考虑到时钟信号的频率,例如如果时钟节拍的频率是1G赫兹,那么时钟抽计数器每纳秒增加一次!

可编程间隔定数

定时器除了实时时钟和时间戳计数器,IBM兼容PC还包含了的三种类型的时间测量设备,叫做可编程间隔计数器。它类似于微波炉的闹钟,即让用户意识到烹饪的时间已经过了!不过他的工作方式是发出一个特殊的中断,即时钟中断来通知内核又一个时间间隔过去了。与闹钟的另一个区别是PIT永远以内核确定的固定频率不停地发出中断

CPU本地定时器

还有一种测量设备是CPU本地定时器,它能够产生单步中断或周期性中断的设备,有点儿像刚刚所说的可编程间隔定时器。不过还是有几点区别:

  1. 首先它是32位的而,pic计数器是16位的。因此可以对本地定时器编程来产生很低频率的中断

  2. 本地APIC定时器只发送给自己的处理器,而PIT产生一个全局性中断。系统中的任意一个中CPU都可以对其进行处理

  3. APIC定时器是基于总线时钟信号的每隔1,2,4,8,16,32,64,128总线时钟信号到来时,对该定时器进行递减,就可以实现编程的目的!相反PIT有自己的内部时钟振荡器,可以更加灵活的编程

高精度事件定时器HPET

提供了许多可以被内核应用的应聘时器这种新定时器芯片,主要由八个32位或64位的独立计时器。每个计时器都由它们的时钟信号所驱动,可以通过映射到内存空间的寄存器来对HPET芯片进行编程

ACPI电源管理定时器

它是另一种时钟设备,包含在几乎所有基于ACPI的主板上,它的时钟信号拥有大约为3.58兆赫兹的固定频率。该设备实际上是一个简单的计数器,它在每个时钟节拍到来时增加一次。为了读取计数器的当前值,需要访问io端口!其io端口的地址由BIOS在初始化阶段进行确定!

Linux计时体系

Linux必须与之执行与定时相关的操作,例如

  • 内核可以周期性地更新自系统启动以来所经过的时间!

  • 更新时间和日期确定当前进程在每个CPU上已运行了多久时间,如果已经超过了分配给他的时间,那么就抢占他!

  • 更新资源使用统计数检查

每个软定时器Linux的计时体系结构是一组与时间流相关的内核结构与函数

计时体系结构的数据结构

定时器对象

为了使用统一的方式管理定时器资源:我们使用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做溢出检测的有缺陷代码在开发阶段被及时发现。(这个技巧是为了迫使内核程序员编写程序时考虑jifffies的溢出)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作为一个extern的timespec的全局变量,存放当前时间和日期。

struct timespec
 {
     __kernel_time_t       tv_sec;         /* 秒数 */
     long                  tv_nsec;        /* 纳秒数,1纳秒(ns)=1e-9秒(s) */
 };
extern struct timespec 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,他是PIT或HPET的中断服务例程

  1. 在xtime_lock顺序锁上产生一个write_seqlock

  2. 执行cur_timer的mark_offset。

  1. cur_timer指向timer_hpet对象,这种情况下,HPET芯片作为时钟中断源。make_offset检查自上一节拍以来是否丢失时钟中断,相应地更新jiffies_64。记录HPET周期计数器当前值。

  2. cur_timer指向timer_pmtmr:PIT芯片作为时钟中断源,但内核用APIC电源管理定时器以更高的分辨率来测量时间。make_offset检查自上一个节拍以来是否丢失时钟中断,如丢失,则更新jiffies_64。它记录APIC电源管理定时计数器当前值。

  3. cur_timer指向timer_tsc对象:PIT芯片作为时钟中断源,但是内核用时间戳计数器以更高的分辨率来测量时间。make_offset检查自上一个节拍以来是否丢失时钟中断,如丢失,则更新jiffies_64。它记录TSC计数器当前值。

  4. cur_timer指向timer_pit:PIT芯片作为时钟中断源,除此之外没别的定时器电路。mark_offset啥也不做。

  1. 调do_timer_interrupt

  1. 使jiffies_64增1.

  2. 调update_times更新系统日期和时间。

  3. 调update_process_times

  4. 调profile_tick

  5. 如使用外部时钟来同步系统时钟,则每隔660s调一次set_rtc_mmss来调整实时时钟。

  1. 调write_sequnlock释放xtime_lock顺序锁

  2. 返回值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时终止每次循环中:

    1. 计算base->tv1中索引,索引保存着下一次要处理的定时器index = base->timer_jiffies & 255

    2. 如索引值为0:调cascade来过滤动态定时器

    3. 使base->timer_jiffies值加1

    4. 对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. 如链表中还有其他定时器,继续处理

    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来确定自上次时钟中断走过的微妙数

    1. 如cur_timer指向timer_hpet,将HPET计数器的当前值与上一次时钟中断处理程序执行时在同一个计数器中保存的值比较。

    2. 如cur_timer指向timer_pmtmr,将ACPI PMT计数器的当前值与上一次时钟中断处理程序执行时在同一个计数器里保存的值比较。

    3. 如cur_timer指向timer_tsc,将时间戳计数器的当前值与上一次时钟中断处理程序执行时在同一个TSC里保存的值比较

    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. 检查tv_usec字段是否溢出,如必要调整

while(tv->tv_usec >= 1000000){
    tv->tv_usec -= 1000000;
    tv->tv_sec++;
}

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

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时钟中断处理程序调用;多处理器上由本地时钟中断处理程序调用。

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值