2015年(100)
分类: LINUX
2015-05-18 13:05:48
时钟时间维护和利用是操作系统的一个基础任务。操作系统中的时间相关的服务包括:
linux最初的实现包括了对这些服务的支持。其模型如图所示(TOD:time of day):
这种实现下每一种架构都有自己的一套时钟实现方案代码,同时也只支持低分辨率定时器,无法支持高分辨率定时器。在新的方案中添加了通用时间抽象层以及对高分辨率定时器的支持。新的方案如下;
在linux中,低分辨率子系统使用jiffies作为时间单位,而高分辨率子系统使用ns作为时间单位。同时内核区分以下两种时钟类型:
全局时钟:负责提供周期时钟,主要用于更新jiffies的值(do_timer)。记录在tick_do_timer_cpu变量中
每个CPU一个的本地时钟:用来进行进程统计(update_process_times)、实现高分辨率定时器、进程度量(profile_tick)。
全局时钟是由一个明确选择的局部时钟担任的。高分辨率定时器只能在每个CPU都提供了本地时钟源的系统上实现。
有两个相关的配置选项用于配置定时器:
CONFIG_NO_HZ:是否支持动态时钟
CONFIG_HIGHRES_TIMERS:是否支持高分辨率定时器
高分辨率定时器使用64位的ktime_t来表示时间。
通用时间子系统定义了一些数据结构来支持复杂的时间系统:表示时钟源的数据结构,表示时钟事件设备的数据结构,表示时钟设备的数据结构。
linux内核使用struct clocksource来表示时钟源。它是时间管理的基础。每个时钟源都定义了一个单调增加的计数器。该结构的关键域:
该结构包含一个表示该时钟源质量的rating域,值越大质量越好,系统会从所有的时钟源中选择一个质量最好的作为自己的时钟源
包括一个read函数指针,用于读取该时钟源的时钟
flags包含若干标志,其中CLOCK_SOURCE_IS_CONTINUOUS表示该时钟是一个连续时钟。如果没有设置该标志,则表示该时钟可能丢失某些时钟周期(根据此来理解所谓的连续)。
mult,shift:乘数和位移数,用于在时钟周期和ns之间进行转换
时钟源抽象提供了一个管理各种时钟源的通用的代码框架,该框架允许用户自己选择时钟源。该框架要求各个时钟源以纳秒为单位管理自己的时钟。该框架与架构无关,使得不需要为了支持每种架构而增添相应的代码。
系统中所有的时钟源都会被存放于一个全局的链表clocksource_list中,系统启动期间会从所有时钟源中选取一个最好的,最差的时候使用基于jiffies的时钟clocksource_jiffies,curr_clocksource用于保存当前系统使用的时钟源,该时钟源会被TimeKeeping机制所使用用于更新维护系统时间等服务。修改系统所使用的时钟源的时机有三个:
clocksource_register用于向系统中添加时钟源。
可以通过/sys/devices/system/clocksource/clocksource0/current_clocksource来指定优先选择的时钟源。
linux内核使用struct clock_event_device来表示时钟事件设备,并提供了一套框架来管理时钟事件设备。该框架提供了对周期性事件和单触发事件的支持。该框架了提供了注册时钟事件的基础设施,它与架构无关,使得不必为每一架构编写相关的代码,降低了系统的复杂性。
该框架提供了对高精度定时器的支持,也建立了动态定时器的基础,当然如果要支持这些功能,则必须注册具有相应能力的时钟事件设备。
时钟事件设备允许注册一个在未来的一个指定的时间点上发生的事件,注意它只能存储一个事件。该结构的关键域:
set_next_event:设置事件发生的时间
event_handler:事件发生时要采取的动作
feature:时钟事件设备的特性。有两类比较典型的:CLOCK_EVT_FEAT_PERIODIC表示支持周期性事件,CLOCK_EVT_FEAT_ONESHOT表示支持单触发事件
set_mode:用于设置模式的函数
mult,shift:乘数和位移数,用于在时钟后期和ns之间进行转换
rating:表示其质量
broadcast:广播
clockevents_register_device:用于向系统添加一个时钟事件设备。所有注册的时钟事件设备都会被存放在链表clockevent_devices中。
clockevents_set_mode:设置时钟事件设备的模式
clockevents_suspend:挂起时钟事件设备
clockevents_resume:重新启用时钟事件设备
linux内核使用struc ttick_device来表示时钟设备。这样的设备提供了时钟事件的连续流,各个时钟事件定期触发。其最主要的用途在于提供周期时钟。
enumtick_device_mode{
TICKDEV_MODE_PERIODIC,
TICKDEV_MODE_ONESHOT,
};
structtick_device {
struct clock_event_device *evtdev;
enumtick_device_mode mode;
};
根据其定义可以看出,它就是时钟事件设备的包装而已。其模式可以是单触发或者是周期模式。每当向系统添加一个时钟事件设备时,内核都会创建一个时钟设备,并调用tick_setup_device来设置它。该函数可能会完成如下动作(具体看代码):
选择全局时钟(如果还没有选择全局时钟)
设置tick_do_timer_cpu(如果还没有选择全局时钟),及全局时钟
设置tick_period(如果还没有选择全局时钟),在这里更新为一个tick有多少个纳秒
让该时钟设备工作在周期模式(如果该时钟设备没有相应的时钟事件设备)
调用tick_setup_periodic或tick_setup_oneshot(根据时钟设备的工作模式)
内核的工作模式:
没有动态时钟的低分辨率系统,总是用周期时钟。这时不会支持单触发模式
启用了动态时钟的低分辨率系统,将以单触发模式是用时钟设备
高分辨率系统总是用单触发模式,无论是否启用了动态时钟特性
非广播时最终的处理函数:
高分辨率动态时钟:hrtimer_interrupt
高分辨率周期时钟:hrtimer_interrupt
低分辨率动态时钟:tick_nohz_handler
低分辨率周期时钟:tick_handle_periodic
广播时最终的处理函数:
高分辨率动态时钟:tick_handle_oneshot_broadcast
高分辨率周期时钟:tick_handle_oneshot_broadcast
低分辨率动态时钟:tick_handle_oneshot_broadcast
低分辨率周期时钟:tick_handle_periodic_broadcast
在系统启动时,高分辨率定时器是无法使用的,并且也不需要使用高分辨率定时器(没有这么高的精度要求)。因而系统启动时默认使用的是低分辨率定时器。只有在时钟事件设备框架,时钟源框架,高分辨率定时器框架都已经初始化完,并且相应的时钟源和时钟事件已经注册到系统之后,高分辨率定时器才能工作。在高分辨率定时器子框架初始化时,其使用的还是低分辨率的周期性定时器模式,即基于常规的定时器来完成(此时高分辨率定时器处理的入口是hrtimer_run_queues,在run_timer_softirq的处理中会调用到)。时钟源框架和时钟事件框架都提供了通告机制来告知高分辨率定时器框架有新的硬件可用了。hrtimers框架会检查时钟源和时钟事件设备是否可用,如果可用就会迁移到高分辨率模式(run_timer_softirq会调用hrtimer_run_pending尝试进行向高分辨率模式的切换)。这样的工作方式使得高分辨率定时器总是可以工作。
如果SMP系统中只存在一个全局的时钟事件设备,则这样的系统无法支持高分辨率定时器。高分辨率定时器需要由CPU的本地时钟事件设备来支持,即它是per-CPU的。
高分辨率定时器在设计思想上采取了事件驱动的方式,即由时钟事件来驱动定时器前进,因而高分辨率定时器需要时钟设备支持单触发模式。
当系统切换到高分辨率模式时,周期性的tick功能就被关闭了,此时需要使用高分辨率定时器进行周期性时钟仿真,这是通过在hrtimer_switch_to_hres调用tick_setup_sched_timer来实现的。tick_setup_sched_timer会添加一个高分辨率定时器,该定时器的处理函数为tick_sched_timer,它会完成周期性函数完成的功能。注意高分辨率定时器是per-CPU的,因而这个定时器也是per-CPU的,进一步的tick_sched_timer的执行也是per-CPU的。为了避免所有的CPU同时执行tick_sched_timer,假设第一个为该功能注册的定时器的超时时间为0,系统中有N个CPU,则其它CPU上的定时器的超时时间起点分别为tick_period/(2*N), 2*tick_period/(2*N), 3*tick_period/(2*N)...。
Timekeeping是内核时间管理的一个核心组成部分。它负责更新系统时间,维持系统“心跳”。GTOD 是一个通用的框架,用来实现诸如设置系统时间 gettimeofday 或者修改系统时间 settimeofday 等工作。为了实现以上功能,Linux 实现了多种与时间相关但用于不同目的的数据结构。
为了管理这些不同的时间结构,Linux 实现了一系列辅助函数来完成相互间的转换。
ktime_to_timespec,ktime_to_timeval,ktime_to_ns/ktime_to_us,反过来有诸如 ns_to_ktime 等类似的函数。
timeval_to_ns,timespec_to_ns,反过来有诸如 ns_to_timeval 等类似的函数。
timeval_to_jiffies,timespec_to_jiffies,msecs_to_jiffies, usecs_to_jiffies, clock_t_to_jiffies 反过来有诸如 ns_to_timeval 等类似的函数。
clocksource_cyc2ns / cyclecounter_cyc2ns
在有的场景下,程序需要等待一段时间然后再接着执行,这时就用到了延时,延时可能长也可能很短,针对不同的场景,有不同的技术可以使用。
cpu_relax不做任何实际的事情,该断代码不断执行直到时间满足了自己的要求,即延时结束。但是在这段代码执行期间,CPU是被浪费的。因此这不是一个好注意
等待队列提供了对超时的支持:
它们都可以提供超时机制,当前线程在等待队列上挂起,直到条件被满足或者超时。二者的区别在于long wait_event_timeout在等待期间不会被打断,但是另一个可能被其它事件打断。
等待队列用于在内核中支持睡眠,它适用的场景是:线程睡眠,并期望在某个条件成立时被唤醒。相关API为:
定义和初始化等待队列:
对应于这些睡眠API,有相应的唤醒API