linux内核时钟机制及延时

来源:互联网 发布:美图秀秀叠图软件 编辑:程序博客网 时间:2024/05/21 10:54

时钟时间维护和利用是操作系统的一个基础任务。操作系统中的时间相关的服务包括:

  • 时间维护
  • 时钟同步
  • time-of-day的表示
  • 下一个事件的调度
  • 处理器以及内核定时器
  • 进程统计
  • 进程度量

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来表示时钟源。它是时间管理的基础。每个时钟源都定义了一个单调增加的计数器。该结构的关键域:

  1. 该结构包含一个表示该时钟源质量的rating域,值越大质量越好,系统会从所有的时钟源中选择一个质量最好的作为自己的时钟源

  2. 包括一个read函数指针,用于读取该时钟源的时钟

  3. flags包含若干标志,其中CLOCK_SOURCE_IS_CONTINUOUS表示该时钟是一个连续时钟。如果没有设置该标志,则表示该时钟可能丢失某些时钟周期(根据此来理解所谓的连续)。

  4. multshift:乘数和位移数,用于在时钟周期和ns之间进行转换

时钟源抽象提供了一个管理各种时钟源的通用的代码框架,该框架允许用户自己选择时钟源。该框架要求各个时钟源以纳秒为单位管理自己的时钟。该框架与架构无关,使得不需要为了支持每种架构而增添相应的代码。

系统中所有的时钟源都会被存放于一个全局的链表clocksource_list中,系统启动期间会从所有时钟源中选取一个最好的,最差的时候使用基于jiffies的时钟clocksource_jiffies,curr_clocksource用于保存当前系统使用的时钟源,该时钟源会被TimeKeeping机制所使用用于更新维护系统时间等服务。修改系统所使用的时钟源的时机有三个:

  • 内核快启动完成时
  • 每次注册一个新的时钟源时
  • 用户显式修改时

clocksource_register用于向系统中添加时钟源。

可以通过/sys/devices/system/clocksource/clocksource0/current_clocksource来指定优先选择的时钟源。

二、时钟事件设备数据结构

linux内核使用struct clock_event_device来表示时钟事件设备,并提供了一套框架来管理时钟事件设备。该框架提供了对周期性事件和单触发事件的支持。该框架了提供了注册时钟事件的基础设施,它与架构无关,使得不必为每一架构编写相关的代码,降低了系统的复杂性。

该框架提供了对高精度定时器的支持,也建立了动态定时器的基础,当然如果要支持这些功能,则必须注册具有相应能力的时钟事件设备。

时钟事件设备允许注册一个在未来的一个指定的时间点上发生的事件,注意它只能存储一个事件。该结构的关键域:

  1. set_next_event:设置事件发生的时间

  2. event_handler:事件发生时要采取的动作

  3. feature:时钟事件设备的特性。有两类比较典型的:CLOCK_EVT_FEAT_PERIODIC表示支持周期性事件,CLOCK_EVT_FEAT_ONESHOT表示支持单触发事件

  4. set_mode:用于设置模式的函数

  5. multshift:乘数和位移数,用于在时钟后期和ns之间进行转换

  6. rating:表示其质量

  7. 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_periodictick_setup_oneshot(根据时钟设备的工作模式)

内核的工作模式:

  1. 没有动态时钟的低分辨率系统,总是用周期时钟。这时不会支持单触发模式

  2. 启用了动态时钟的低分辨率系统,将以单触发模式是用时钟设备

  3. 高分辨率系统总是用单触发模式,无论是否启用了动态时钟特性

非广播时最终的处理函数:

  • 高分辨率动态时钟: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的。

高分辨率定时器在设计思想上采取了事件驱动的方式,即由时钟事件来驱动定时器前进,因而高分辨率定时器需要时钟设备支持单触发模式。

1.高分辨率模式下的周期时钟仿真

当系统切换到高分辨率模式时,周期性的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)...。

五、动态时钟

只有在有任务需要实际执行时,才激活周期时钟,否则就禁用周期时钟的技术。作法是如果需要调度idle来运行,禁用周期时钟;直到下一个定时器到期为止或者有中断发生时为止再启用周期时钟。单触发时钟是实现动态时钟的前提条件,因为动态时钟的关键特性是可以根据需要来停止或重启时钟,而纯粹的周期时钟不适用于这种场景。
动态时钟使用数据结构tick_sched。其关键域:
  • sched_timer:用于实现时钟的定时器
  • nohz_mode:当前工作模式。可能有三种值:NOHZ_MODE_INACTIVE,NOHZ_MODE_LOWRES,NOHZ_MODE_HIGHRES,
  • last_tick:存储在禁用周期时钟之前,上一个时钟信号的到期时间。这对于了解何时再起周期时钟非常重要,因为下一个时钟的到期时间必须和时钟禁用前完全一致,就像是没有禁用一样。
  • tick_stopped:周期时钟是否已经停用。

1.低分辨率下的动态时钟

在tick_check_oneshot_change中会进行检查,如果提供了支持单触发模式的时钟事件设备,并且系统当前使用的timekeeper适用于高分辨率模式,则调用tick_nohz_switch_to_nohz启用动态时钟(这是PER-CPU的动作)。
如果系统没有添加对动态时钟的支持,则该函数什么都不做,否则,它将时钟事件设备设置为单触发模式,并安装一个定时器处理函数tick_nohz_handler,然后PER-CPU的tick_sched实例的nohz_mode被设置为NOHZ_MODE_LOWRES。最后内核会启动PER-CPU的tick_sched上的sched_timer。
tick_nohz_handler会完成:
  • 执行时钟机制所需的所有操作
  • 对时钟设备重新编程,使得下一个时钟信号在适当的时候到期
全局时钟设备的角色必须必须由一个CPU来承担,但是如果启用了动态时钟特性,而且一个CPU可能要休眠较长时间,则它就不能担任负责全局时钟的责任,需要撤销它。因而在该ick_nohz_handler中需要进行检查,如果当前没有CPU承担该角色,本CPU就要承担该角色。
同时在ick_nohz_handler的处理过程中如果本CPU承担了全局时钟设备的角色,则需要调用tick_do_update_jiffies64更新时间。

2.高分辨率下的动态时钟

高分辨率模式下也需要完成全局时钟设备角色检查确认的工作。实际上都有tick_sched_do_timer完成,只是在高分辨率模式下由tick_sched_timer调用。tick_sched_timer是高分辨率模式下周期时钟的仿真器。
另外在tick_setup_sched_timer会将per-CPU的tick_sched的nohz_mode设置为NOHZ_MODE_HIGHRES。
内核提供了两个API用来在动态时钟模式下停止和重启时钟,它们适用于高分辨率和低分辨率模式。
hrtimer_stop_sched_tick:用于停止时钟
hrtimer_restart_sched_tick:用于重启时钟

六、广播时钟

在某些情况下,为了节省电力,系统中的时钟设备可能进入休眠状态,即不工作。这时候仍需要有一个时钟设备来运行以提供一些基本的时钟功能,比如需要时唤醒其它时钟设备,这个设备称为广播时钟设备。tick_broadcast_device用于保存当前使用的广播时钟设备。广播时钟设备可能工作在单触发模式或者周期模式。无论工作在何种模式,其工作原理大致相同,当该时钟到期时,检查其它CPU是否有到期的时钟事件需要处理,如果有就发IPI给它。
单触发模式的广播时钟的处理函数为:tick_handle_oneshot_broadcast
周期模式的广播时钟的处理函数为:tick_handle_periodic_broadcast

七、Timekeeping & GTOD (Generic Time-of-Day)

Timekeeping是内核时间管理的一个核心组成部分。它负责更新系统时间,维持系统“心跳”。GTOD 是一个通用的框架,用来实现诸如设置系统时间 gettimeofday 或者修改系统时间 settimeofday 等工作。为了实现以上功能,Linux 实现了多种与时间相关但用于不同目的的数据结构。

  1. struct timespec 精度是纳秒。它用来保存从 00:00:00 GMT, 1 January 1970 开始经过的时间。内核使用全局变量 xtime 来记录这一信息,这就是通常所说的“Wall Time”或者“Real Time”。与此对应的是“System Time”。System Time 是一个单调递增的时间jiffies,每次系统启动时从 0 开始计时。
  2. struct timeval 精度是微秒。timeval 主要用来指定一段时间间隔。
  3. ktime_t 是 hrtimer 主要使用的时间结构。无论使用哪种体系结构,ktime_t 始终保持 64bit 的精度,并且考虑了大小端的影响。
  4. cycle_t 是从时钟源设备中读取的时钟类型。

为了管理这些不同的时间结构,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

八、延时

在有的场景下,程序需要等待一段时间然后再接着执行,这时就用到了延时,延时可能长也可能很短,针对不同的场景,有不同的技术可以使用。

1.长延时

1.忙等待调用cpu_relax()

while (time_before(jiffies, j1))    cpu_relax();

cpu_relax不做任何实际的事情,该断代码不断执行直到时间满足了自己的要求,即延时结束。但是在这段代码执行期间,CPU是被浪费的。因此这不是一个好注意

2.让出CPU

while (time_before(jiffies, j1)) {    schedule();}
shedule使得在时间不到自己想要的点时,可以把CPU让给其他线程。与方式一相比,它已经很好了,但是在调度到本线程执行时,CPU时间是被浪费的,尤其是在时间不到期时,系统做了大量的工作来完成任务切换,结果换来的是立即切换到下一个任务,做的实际成了无用功,因而也不是很好的注意

3.使用超时机制

等待队列提供了对超时的支持:

#include <linux/wait.h>

wait_event_interruptible_timeout(wq, condition, timeout)wait_event_timeout(wq, condition, timeout)

它们都可以提供超时机制,当前线程在等待队列上挂起,直到条件被满足或者超时。二者的区别在于long wait_event_timeout在等待期间不会被打断,但是另一个可能被其它事件打断。

等待队列用于在内核中支持睡眠,它适用的场景是:线程睡眠,并期望在某个条件成立时被唤醒。相关API为:

定义和初始化等待队列:

DECLARE_WAIT_QUEUE_HEAD(name);或者动态地, 如下:wait_queue_head_t my_queue;init_waitqueue_head(&my_queue);
在队列上睡眠:

wait_event(wq, condition)wait_event_interruptible(wq, condition)wait_event_interruptible_timeout(wq, condition, timeout)wait_event_timeout(wq, condition, timeout)
这些都是宏,其中的interruptible版本是可中断版本,另一个版本是不可中断版本。

对应于这些睡眠API,有相应的唤醒API

wake_up(x)wake_up_interruptible(x)
它们用于唤醒在等待队列上睡眠的线程,其中interruptible版本用于唤醒处于可中断模式的线程,另一个用于处理正常的线程。
睡眠和唤醒都有很多变体,详细的可参见wait.h
另外一个超时机制是schedule,调度器提供了schedule_timeout,这个函数使得当前线程睡眠到指定的时间,但是该函数也要求调用者先设置线程的状态。线程状态可以是TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE,两者区别在于一个可以被打断,另一个不可以。典型的使用schedule_timeout的代码如下:

set_current_state(TASK_INTERRUPTIBLE);schedule_timeout (delay);

2.短延时

内核函数 ndelay, udelay, 以及 mdelay 适用于短延时的场景, 分别延后执行指定的纳秒数,微秒数或者毫秒数. 它们的原型是:
#include <linux/delay.h>void ndelay(unsigned long nsecs);void udelay(unsigned long usecs);void mdelay(unsigned long msecs);
这 3 个延时函数是忙等待; 其他任务在时间流失时不能运行.
内核还提供了一个msleep,它用于获取毫秒级的延时,同时不使用忙等待的方式。它的代码如下:
/** * msleep - sleep safely even with waitqueue interruptions * @msecs: Time in milliseconds to sleep for */void msleep(unsigned int msecs){        unsigned long timeout = msecs_to_jiffies(msecs) + 1;        while (timeout)                timeout = schedule_timeout_uninterruptible(timeout);}
也就是说它实际上使用了schedule_timeout机制
原创粉丝点击