Linux时间子系统

目录

一、时间子系统简介.... 2

二、时间子系统的硬件实现.... 3

硬件timer 3

体系代码.... 3

三、Linux时间子系统的基础.... 4

四、定时器模块.... 4

clockevent 4

tick device. 5

tick-sched.. 5

tick-oneshot 5

broadcast tickdevice. 5

定时器.... 6

低精度定时器.... 6

高精度定时器.... 6

五、时间模块.... 6

clocksource. 6

内核中时间的计算方法.... 7

内核中的时间.... 7

timekeeping.. 8

六、Linux系统的定时和睡眠.... 9

Linux内核态的睡眠函数.... 9

Linux应用层的睡眠.... 9

Linux内核态的定时函数.... 10

Linux应用层的定时函数.... 10

Linux中获取精确的时间.... 10

实际测试.... 11

内核睡眠和高精度定时器测试:... 11

用户态睡眠和定时函数测试.... 12




一、时间子系统简介

时间子系统框图


Linux时间子系统_第1张图片

如时间子系统的框架所示,Linux内核主要负责中间部分的抽象,以及系统中控制时间设备和定时器设备的逻辑代码,还要向上提供系统调用。

[if !supportLists]1.    [endif]Arch chip driver跟board配置和平台有关,具体的硬件定时器操作由平台通用代码完成。

[if !supportLists]2.    [endif]clocksource和clockevent非常类似,其底层的硬件工作方式一致,但是软件上的不同目的使得Linux将其抽象成时钟源和定时中断两种设备。

[if !supportLists]3.    [endif]clocksource属于时间管理方面,建立在其基础上的timekeeping模块负责维护内核各个时钟向前行。ntp模块负责通过网络同步时间,并修正内核的时间误差。time模块向应用层提供时间相关的系统调用接口。

[if !supportLists]4.    [endif]clockevent属于定时触发设备,向上抽象成tick device,形成一个可以设定时间产生滴答的设备。内核的定时器模块使用滴答设备来做定时功能,CPU也使用滴答设备来做调度、更新jiffies、定时等功能。timer向内核其他部分提供定时器调用方法,向应用层提供系统调用接口。


二、时间子系统的硬件实现

硬件timer

硬件timer一般有两个重要的寄存器,count寄存器和reload寄存器。

count寄存器顾名思义是用来计数的,其中的数值在内核中又称作cycle,1个cycle表示count数值变化了1。count寄存器内的数值会在该定时器的硬件时钟的每个clock自减一,等数值减到0的时候,会触发中断(可选),并从reload寄存器载入新的值,然后继续自减。

可以由此看到,timer的精度受到硬件输入时钟的影响。s2芯片的时钟来源于apb(72MHz),所以每个cycle大约是13.88ns的时间长度。硬件timer不能度量比这个更小的时间。

通过硬件timer这种工作原理,我们可以设置定时器的两种工作方式。一种是periodic类型的工作,事先向count和reload寄存器内填入相同的值,然后启动timer,后续就会有稳定的中断到来。另一种是one shot类型工作,向count内填入某个值,然后启动timer,等到触发中断的时候,向count内填入新的值,这样就能每次设定不同的触发时间,甚至可以达到timer最小精度的定时效果。


体系代码


Linux时间子系统_第2张图片

arch中要提供具体硬件timer的操作方法,包括timer的启动和禁用,timer的工作模式设置,timer中断的处理,timer设置定时功能,注册中断等。还要从设备树获取时钟、timer属性、寄存器等信息,将timer注册为clocksource设备或者clockevent设备。

设备树中的timer

三、Linux时间子系统的基础


Linux时间子系统_第3张图片

Linux时间子系统各个模块之间的关系

Linux抽象出来clocksource设备和clockevent设备,clocksource设备用来负责时间相关的动作,推动内核各种时间向前;clockevent设备用来负责tickevent设备的功能,负责产生滴答事件。内核中各个模块之间的联系可以参考上图。


四、定时器模块

clockevent

Linux将可以产生时钟事件的timer抽象为clockevent。clockevent设备的主要操作就是设置下一次event的时间点,一般都是设置cycle数目。clockevent一般有periodic和oneshot两种工作模式。

clockevent向内核提供定时编程的接口clockevents_program_event。这个函数很关键,tick device会调用该函数,用于设置下一次event触发的时间。这个时间也有最大值和最小值,如果超出界限,将会表示成边界值。

clockevent是绑定cpu的,一个clockevent可以在多个cpu上工作。


tick device

基于clockevent设备,Linux抽象出了tick device。

struct tick_device { 

struct clock_event_device*evtdev; 

enum tick_device_mode mode; 

};

从tick device的结构上来看,可以很明显的发现,tick device就是工作在某种模式下的clockevent设备。

Linux为每个处理器核心都建立了一个local tick device,要注意,不同的tick device可能会使用同一个clockevent,但是这个clockevent一定要能将中断送到tick device所服务的CPU。在ARM(安霸)体系下,每个tickdevice都使用独立的clockevent,它跟硬件的timer绑定,且支持oneshot模式。

Linux有一些系统级的任务,比如更新jiffies,更新墙上时钟,这些是不适合让每个tick device都参与的,tickdevice模块会选择一个local tick device中作为global tick device。被选择的CPU号被计入变量tick_do_timer_cpu 中,该CPU的tick触发的时候会做这些额外的工作,例如更新jiffies。

tick-sched

tick-sched是工作在periodic模式的tick。中断会执行定时任务。在开启了高精度定时器选项的系统中,周期性的硬件中断处理中还会call起一个软中断,在软中断中检查是否可以切换到oneshot模式。在软中断中,如果要切换到oneshot模式,除了配置一系列动作之外,还会调用tick_setup_sched_timer去设定一个高精度定时器,而且这个定时器中断内会调用hrtimer_forward设置下次触发的时间,还会返回HRTIMER_RESTART,让自己反复的周期触发。

tick-oneshot

tick-oneshot是工作在oneshot模式的tick,也成为tickless,即非固定时间触发的tick。这种模式主要用于高精度定时器的工作。在每次中断触发的时候,除了做一些跟周期性tickdevice类似的工作,还会从红黑树结构的定时器列表中读出下一个要触发的tick,计算要配置的触发时间,并设置下一次的触发。这样,就可以根据定时器列表来不断地触发中断,而不用像periodic tick一样在固定的中断中处理所有到期的定时器。

broadcast tickdevice

Linux的配置有NO_HZ_IDLE和NO_HZ_FULL等选项,主要用来配置tickless。这些选项来源于CPU的休眠模式。在多核处理器中,经常会有一些核无事可做而要休眠的情况,有些CPU支持C3级的深度休眠,在这种情况下会停止local tickdevice的工作,连tick device绑定的硬件时钟都会挂起。

那么这些CPU怎么从睡眠中恢复呢,这时候就需要一个tickdevice能照顾到这些CPU上的tick工作。在X86上,会有一个系统级的硬件timer可以做这个任务,它是一个独立的tick device,不绑定到某个CPU上,可以通过SPI(Shared

Peripheral Interrupt)唤醒CPU,这时候甚至所有CPU都可以睡眠。在ARM体系下,一般没有这样一种硬件的timer,所以要挑选一个CPU,让其不进入休眠模式来做这个工作,通过IPI唤醒其他CPU。


定时器

低精度定时器

void init_timer(struct timer_list *timer);

初始化定时器

void add_timer(struct timer_list *timer);

将定时器列表添加到内核

int del_timer(struct timer_list *timer);

删除定时器

int mod_timer(struct timer_list *timer,unsigned long expires);

修改定时器的到期时间


高精度定时器

void hrtimer_init(struct hrtimer *timer,

clockid_t clock_id, enum hrtimer_mode mode);

初始化高精度定时器,指定时钟ID

int hrtimer_start(struct hrtimer *timer,ktime_t tim, const enum hrtimer_mode mode);

在当前cpu启动定时器

int hrtimer_cancel(struct hrtimer *timer);

取消高精度定时器


五、时间模块

clocksource

clocksource是从硬件定时器抽象出来的让其自由累计的设备。不只是定时器,jiffies也可以作为clocksource设备。在启动的过程中,clocksource会先初始化起始时间,尽快选择一个能用的源来推动时间前行。系统中可能存在很多个clocksource设备,包括jiffies、timer等,但是用来维护系统时间的设备只能有一个,所以clocksource模块就要从不同的时钟源中选择一个最优的。每个clocksource设备都有一个rating,来表示这个时钟源的评级,每当一个新的clocksource设备添加进来的时候,模块都会根据rating将其插入链表的合适位置,保证链表头到尾的rating是从高到低的。

clocksource设备所绑定的硬件timer,不会产生中断,也不会在系统运行中配置其中的count值,而是让其自由累计。通过硬件的频率可以获知其每一个cycle代表的时间是多少,通过count的最大值可以获知硬件计时的最大周期。通过访问cycle的值,再根据上一次访问时cycle的值,就可以计算出两次访问经过了多少时间,从而将时间累加到Linux系统时间中。

clocksource设备的一个cycle所代表的时间单位是ns纳秒,n个cycle代表的时间就是

delta time = n*cycle = n*1s/freq

例如我们72MHZ的timer一个cycle代表的时间大约就是1000,000,000ns/72,000,000=13.8888…≈14ns。


举例:

Cycle数目真实时间MultShift计算时间误差

10138.8ns553137.50ns0.5ns

10138.8ns272138ns0.8ns

10138.8ns1311308.8ns


内核中时间的计算方法

Linux内核会考虑到各种不同平台硬件的差别,例如有些平台没有硬件除法器,且内核中不允许用浮点运算。所以不会用上述公式直接计算,而是借用mult和shift值计算出时间。mult和shift值是根据硬件timer的频率计算出来的。n个cycle的时间就表达为:

       delta time = n*cycle =cycle*mult >> shift

这样就避免了除法运算和浮点运算。

内核中的时间

内核中有多种timeline时间线,比较基础且有代表性的时间线有:

CLOCK_REALTIME:

UTC,又称墙上时间,内核中一般写作xtime,代表真实世界的时间,实际上保存的是距离1970-1-1 0时(linux epoch)的时间。UTC时间其实并不是绝对准确的,它和原子钟之间是有偏差的,UTC在有些时候会加入闰秒来调整世界时。

CLOCK_MONOTONIC

内核中单调递增的时间,不包含系统睡眠的时间。其中保存的实际上是距离realtime的时间差,也就是说它的值要加上CLOCK_REALTIME的值才是monotonic时间。这里可能会有一些疑问,比如修改了系统时间不就会对这个时间产生影响吗?其实并不会,在修改系统时间的时候,会将设置时间和原系统时间的偏差加到CLOCK_MONOTONIC中,这样就保证了通过CLOCK_MONOTONIC获得是时间是单调递增的。

CLOCK_BOOTTIME

内核从boot启动后经过的时间,包含系统睡眠的时间,相当于CLOCK_MONOTONIC+睡眠时间。

CLOCK_MONOTONIC_RAW

绝对单调的时间,不受ntp系统的影响,不会微调

CLOCK_TAI

原子钟时间,比UTC更严格。

timekeeping

struct timekeeper {

       struct clocksource      *clock;

       u32                     mult;

       u32                     shift;

       cycle_t                cycle_interval;

       cycle_t                cycle_last;

       u64                    xtime_interval;

       s64                    xtime_remainder;

       u32                    raw_interval;


       u64                     xtime_sec;

       u64                    xtime_nsec;


       s64                     ntp_error;

       u32                    ntp_error_shift;


         struct timespec         wall_to_monotonic;

        ktime_t                 offs_real;

        struct timespec        total_sleep_time;

        ktime_t                 offs_boot;

        struct timespec         raw_time;

        s32                     tai_offset;

        ktime_t                 offs_tai;

 };    


内核提供的时间线有realtime clock,monotonic clock,monitonic raw clock等等。内核中同过timekeeping模块维护系统中各个时间线,并向内核其他模块提供时间服务。除了维护系统的时间,timekeeping模块还要向其他模块提供时间的操作方法,例如常见的gettimeofday。

系统启动的时候,timekeeping会从RTC读取时间,初始化各个系统时钟。timekeeping会从clocksource中挑选最好的一个作为自己默认的时钟源,并且clocksource模块中有新时钟源加入的时候,clocksource模块会通知timekeeping模块重新选择时钟源。

timekeeping模块虽然是根据clocksource来维护内核的时间线,但它却是由clockevent触发的,因为clocksource模块本身并不会产生中断。

六、Linux系统的定时和睡眠

Linux内核态的睡眠函数

内核常用的睡眠函数有:

static inline void

ssleep(unsigned int seconds);

void msleep(unsigned int

msecs);

ssleep à msleepà__mod_timer

内核态的睡眠函数ssleep实际上是调用了msleep,msleep实际上是启动了一个低精度timer,并且是以jiffies精度为最小间隔的定时。所以即使指定了精确的毫秒数,msleep函数的休眠也会跟jiffies的触发对齐。msleep还会将计算出的需要睡眠的jiffies数+1,所以在HZ为100的平台上,睡眠1ms实际上会睡眠大约20ms。有极个别平台没有实现睡眠函数,直接用delay方法实现。


Linux应用层的睡眠

Linux应用层常用的睡眠函数有:

unsigned intsleep(unsigned int seconds);

int usleep(useconds_tusec);

int nanosleep(const structtimespec *req, struct timespec *rem);

int clock_nanosleep(clockid_tclock_id, int flags,

                    const struct timespec*request, struct timespec *remain);



sleepànanosleepàsyscallànanosleepàhrtimer

                                   ↑

usleep

应用层的休眠函数最终都是调用nanosleep系统调用,在内核态是设置了一个在stack的高精度定时器实现的。需要注意的是nanosleep这个系统调用,其第二个参数是返回剩余未睡眠的时间,也就是说,nanosleep可能没有完全经过睡眠就返回。这是因为nanosleep是可以被信号打断的,所以使用的时候要特别注意。

clock_nanosleep 是一个更强大的函数,可以基于realtime睡眠,也可以像nanosleep一样基于单调时间睡眠。但是glibc并未支持这些功能,仅仅调用了nanosleep。

Linux内核态的定时函数

mod_timer:低精度定时器,以jiffies为参数。

hrtimer:高精度定时器,以s64的ns数为参数。

Linux应用层的定时函数

应用层定时函数有很多,在此仅分析内核向上提供的系统调用。

setitimer:系统调用,利用高精度定时器实现。

alarm:系统调用,调用setitimer。

select:系统调用,借用selece内部的schedule_hrtimeout_range实现定时功能。


Linux中获取精确的时间

ktime_get:该函数会实时从硬件中读取时间,可以精确到ns纳秒


实际测试

内核睡眠和高精度定时器测试:


Linux时间子系统_第4张图片

用户态睡眠和定时函数测试


Linux时间子系统_第5张图片


最后的彩蛋:

有需求要实现手动切换到高精度模式。

首先,内核可以从低精度模式切换到高精度模式,但是无法从高精度模式切回低精度模式。其实也没有什么不可以的,代码都是人写的,sched和oneshot都是代码抽象出来的定时器操作方法,但是内核只处理和实现了由低精度切到高精度的方法,没有提供高向低切的,有兴趣可以自己实现。

内核中有代码解析uboot传递的变量"hres",可以指定内核是否开启高精度模式,这关系到一个static的变量hrtimer_hres_enabled的值。我们把tick-sched.c内的ts->nohz_mode != NOHZ_MODE_INACTIVE改为ts->nohz_mode == NOHZ_MODE_HIGHRES,控制hrtimer_hres_enabled默认值为0。然后在需要的时候将hrtimer_hres_enabled值改为1,然后调用tick_oneshot_notify()即可。

我们会发现一个有意思的现象,在smp系统上,哪个CPU运行了tick_oneshot_notify(),它的定时器就会进入高精度模式。从这里可以再次印证,tickdevice是跟cpu绑定的。我们甚至可以控制哪个cpu进入高精度模式,哪些不进入。

你可能感兴趣的:(Linux时间子系统)