Linux时间子系统(十六) clockevent

一、clock event控制的通用逻辑

1、产生clock event的设备

各种系统的timer硬件形形色色,不过在general clock event device layer,struct clock_event_device被来抽象一个可以产生clock event的timer硬件设备,如下:

struct clock_event_device {
    void          (*event_handler)(struct clock_event_device *);
    int            (*set_next_event)(unsigned long evt,   struct clock_event_device *);
    int            (*set_next_ktime)(ktime_t expires,     struct clock_event_device *);
    ktime_t       next_event;
    u64            max_delta_ns;
    u64            min_delta_ns;
    u32            mult;
    u32            shift;
    enum clock_event_mode    mode;
    unsigned int        features;
    unsigned long        retries;

void            (*broadcast)(const struct cpumask *mask);
    void            (*set_mode)(enum clock_event_mode mode, struct clock_event_device *);
    void            (*suspend)(struct clock_event_device *);
    void            (*resume)(struct clock_event_device *);
    unsigned long        min_delta_ticks;
    unsigned long        max_delta_ticks;

const char        *name;
    int            rating;
    int            irq; -------------使用的IRQ number
    const struct cpumask    *cpumask; ----该clock event device附着在哪一个CPU core上
    struct list_head    list;
    struct module        *owner;
} ____cacheline_aligned;

在把目光投向每一个成员之前,我们先聊一聊____cacheline_aligned,其实最开始定义这个数据结构的时候没有这个属性,而且struct
clock_event_device的各个成员也不是这样排列的。为了提高cacheline hit
radio,内核工程师对这个数据结构进行的改造,一方面对齐了cache
line,另外一方面重排了那些有密切关系的成员(所谓密切关系就是指在代码执行上下文中,如果访问了A成员,有非常大的概率访问B成员)。

event_handler顾名思义就是产生了clock event的时候调用的handler。一般而言,底层的clock event chip

driver会注册中断处理函数,在硬件timer中断到来的时候调用该timer中断handler,而在这个中断handler中再调用event_handler。

既然是产生clock
event的device,那么总是要控制下一次event产生的时间点,我们有两个成员函数完成这个功能:set_next_event和set_next_ktime。set_next_ktime函数可以直接接收ktime作为参数,而set_next_event设定的counter的cycle数值。一般的timer硬件都是用cycle值设定会比较方便,当然,不排除有些奇葩可以直接使用ktime(秒、纳秒),这时候clock
event device的features成员要打上CLOCK_EVT_FEAT_KTIME的标记。

features成员是描述底层硬件的功能feature的,包括:

#define CLOCK_EVT_FEAT_PERIODIC        0x000001------具备产生周期性event的能力
#define CLOCK_EVT_FEAT_ONESHOT        0x000002-----具备产生oneshot类型event的能力
#define CLOCK_EVT_FEAT_KTIME        0x000004-------上面已经描述了

#define CLOCK_EVT_FEAT_C3STOP        0x000008
#define CLOCK_EVT_FEAT_DUMMY        0x000010

CLOCK_EVT_FEAT_C3STOP是一个很有意思的feature:内核中有一个模块叫做cpuidle
framework,当没有任务做的时候,cpu会进入idle状态。这种CPU的sleep
state叫做C-states,有C1/C2…Cn种states(具体多少种和CPU设计相关),当然不同的状态是在功耗和唤醒时间上进行平衡,CPU睡的越浅,功耗越大,但是能够很快的唤醒。一般而言,在sleep
state的CPU可以被local timer唤醒,但是,当CPU进入某个深度睡眠状态的时候,停止了local
timer的运作,这时候,local timer将无法唤醒CPU了。

kernel的注释说这是一个x86(64)的功能设计失误(misfeature),不过,在嵌入式平台上,这也可以认为是对功耗的极致追求(ARM
的generic timer也有这个misfeature,呵呵~~)。为了让系统可以继续运作,传说中tick broadcast
framework粉墨登场了。struct clock_event_device中的broadcast这个callback函数是和clock
event广播有关。在per CPU 的local
timer硬件无法正常运作的时候,需要一个独立于各个CPU的timer硬件来作为broadcast clock event
device。在这种情况下,它可以将clock event广播到所有的CPU core,以此推动各个CPU core上的tick
device的运作。当然它不是本文的主要内容,如果有兴趣可以参考本站其他文档。

clock event mode的定义如下:

enum clock_event_mode {
    CLOCK_EVT_MODE_UNUSED = 0, ------未使用
    CLOCK_EVT_MODE_SHUTDOWN, ------被软件shutdown
    CLOCK_EVT_MODE_PERIODIC, -------工作状态,处于periodic模式,周期性产生event
    CLOCK_EVT_MODE_ONESHOT, -------工作状态,处于one shot模式,event是一次性的
    CLOCK_EVT_MODE_RESUME, -------处于系统resume中
};

具体的mode设定是由set_mode这个callback函数来完成的,毫无疑问,这是需要底层的clock event chip
driver需要设定的。虽然定义了这么多mode,实际上底层的硬件未必支持,有些HW
timer硬件支持oneshot和periodic的设定,不过有些就不支持(例如:ARM general timer)。具体clock
event device如何在各个mode中切换会在tick device的文档中描述。

rating、mult和shift的概念和clocksource中的类似,这里就不描述了。此外很多成员这里也不细述了,在后面的逻辑分析中自然就清晰了。

2、如何组织系统中的clock event device?

相关的全局变量定义如下:

static LIST_HEAD(clockevent_devices);
static LIST_HEAD(clockevents_released);

clock event device core模块使用两个链表来管理系统中的clock event
device,一个是clockevent_devices链表,该链表中的clock event
device都是当前active的device。active的clock device有两种情况,一种是cpu core的current 
clockevent device,这些device是各个cpu上产生tick的那个clock event
device,还有一些active的device由于优先级比较低,当前没有使用,不过可以作为backup的device。另外一个是clockevents_released链表,这个链表中clock
event device都是由于种种原因,无法进入active list,从而挂入了该队列。

二、向上层的其它driver提供操作clock event的通用API

1、设定clock event device的触发event的时间参数

int clockevents_program_event(struct clock_event_device *dev, ktime_t expires, bool force)
{
    unsigned long long clc;
    int64_t delta;
    int rc;

dev->next_event = expires; -------设定下一次触发clock event的时间
    if (dev->features & CLOCK_EVT_FEAT_KTIME)----- -------------(1)
        return dev->set_next_ktime(expires, dev);

delta = ktime_to_ns(ktime_sub(expires, ktime_get()));----- ----------(2)
    if (delta <= 0)
        return force ? clockevents_program_min_delta(dev) : -ETIME; -----------(3)

delta = min(delta, (int64_t) dev->max_delta_ns);
    delta = max(delta, (int64_t) dev->min_delta_ns); ------------------(4)

clc = ((unsigned long long) delta * dev->mult) >> dev->shift; ---------转换成cycle值

rc = dev->set_next_event((unsigned long) clc, dev); ---调用底层driver callback函数进行设定

return (rc && force) ? clockevents_program_min_delta(dev) : rc; -----------(5)
}

我们先看看该接口函数的参数值:dev指向具体的clock event
device,expires参数是设定下一次产生event的时间点,force参数控制在expires设定异常的时候(例如设定在一个过去的时间点上产生event)该函数
的行为,一种是出错返回,另外一种还是进行event的产生,只是设定一个最小的delta。

(1)如果chip driver支持使用ktime的设定(这需要硬件支持,设定了CLOCK_EVT_FEAT_KTIME flag的那些clock event device才支持哦),事情会比较简单,直接调用set_next_ktime就搞定了。

(2)对于一个“正常”的clock event device,需要转换成cycle这样的单位。不过在转换成cycle之前,需要先将ktime格式的时间(传入的expires参数就是这样的格式)转换成纳秒这样的时间单位。

(3)delta小于0意味着用户设定的时间点已经是过去的一个时间点,如果强制产生event的话,那么事不宜迟,要立刻产生event,这需要调用clockevents_program_min_delta函数,代码如下:

static int clockevents_program_min_delta(struct clock_event_device *dev)
{
    unsigned long long clc;
    int64_t delta;

delta = dev->min_delta_ns; ---------------------------(a)
    dev->next_event = ktime_add_ns(ktime_get(), delta); ----------------(b)

if (dev->mode == CLOCK_EVT_MODE_SHUTDOWN)
        return 0;

dev->retries++; -------记录retry的次数
    clc = ((unsigned long long) delta * dev->mult) >> dev->shift; ------转换成cycle值
    return dev->set_next_event((unsigned long) clc, dev); ----调用底层driver callback函数进行设定
}

(a)在逻辑思维的世界里,你可以想像任意小的时间片段,但是在实现面,这个要收到timer硬件能力的限制。一个输入频率是1Hz的timer硬件,其最小时间粒度就是1秒,如何产生0.1秒的clock
event呢?因此,所谓立刻产出也就是在硬件允许的最小的时间点上产生event。在注册clock event
device的时候已经设定这个参数了,就是struct clock_event_device的min_delta_ns这个成员。

(b)ktime_get函数获取当前的时间点,加上min_delta_ns就是下一次要触发event的时间点,struct clock_event_device的next_event 这个成员就是用来记录下一次要触发event的时间点信息的。

(4)有最小值就有最大值,struct clock_event_device的max_delta_ns这个成员就是设定next
event触发的最大时间值。这个最大值是和硬件counter的bit数目有关:如果一个硬件timer最大能表示60秒的时间长度,那么设定65秒后触发clock

event是没有意义的,因为这时候counter会溢出,如果强行设定那么硬件实际会在5秒后触发event。同样的,max_delta_ns也是在注册clock
event device的时候设定了这个参数。

(5)如果调用底层driver
callback函数进行实际的cycle设定的时候出错(例如:由于种种原因,在实际设定的时候发现时间点是过去值,如果仍然设定,那么上层软件实际上要等到硬件counter

溢出后在下一个round中才会触发event,实际这时候黄花菜都凉了),并且是强制产生event的话,那么也需要调用clockevents_program_min_delta函数在最小时间点上产生clock
event。

2、更换clock event设备

上层的tick device驱动层会根据情况(clock event的精度,是否local cpu的)调用clockevents_exchange_device 进行clock event设备的更换,具体代码如下:

void clockevents_exchange_device(struct clock_event_device *old,  struct clock_event_device *new)
{
    unsigned long flags;

local_irq_save(flags);

if (old) { -------------------------------(1)
        module_put(old->owner);
        clockevents_set_mode(old, CLOCK_EVT_MODE_UNUSED);
        list_del(&old->list);
        list_add(&old->list, &clockevents_released);
    }

if (new) { ------------------------------(2)
        BUG_ON(new->mode != CLOCK_EVT_MODE_UNUSED);
        clockevents_shutdown(new);
    }
    local_irq_restore(flags);
}

(1)旧的clock event device要被替换掉,因此将其模式设定为CLOCK_EVT_MODE_UNUSED,并且从全局clock event device链表中摘下来,挂入clockevents_released链表

(2)我们要确保新的clock event设备没有被使用,如果新的clock
event设备不是CLOCK_EVT_MODE_UNUSED状态,说明其目前被其他的上层软件使用,因此要kernel
panic,并且shutdown该clock event
device。这是该设备状态是CLOCK_EVT_MODE_SHUTDOWN状态。这里没有插入clockevent_devices全局链表的动作,主要是因为在调用该函数之前,新的clock
event device已经挂入队列了。

三、向底层clockevent chip driver提供的接口

1、配置clock event device

底层的clock event chip
driver可以调用clockevents_config对该设备进行配置。这里的操作类似clocksource中的内容,也就是说调用者输入频率参数,在clockevents_config中计算struct
clock_event_device中一些类似mult、shift等成员的配置值:

void clockevents_config(struct clock_event_device *dev, u32 freq)
{
    u64 sec;

if (!(dev->features & CLOCK_EVT_FEAT_ONESHOT))--------------(1)
        return;

sec = dev->max_delta_ticks;-------------------------(2)
    do_div(sec, freq);
    if (!sec)
        sec = 1;-----限制最小值
    else if (sec > 600 && dev->max_delta_ticks > UINT_MAX)
        sec = 600; -------------------------------(3)

clockevents_calc_mult_shift(dev, freq, sec);------------------(4)
    dev->min_delta_ns = cev_delta2ns(dev->min_delta_ticks, dev, false);-------(5)
    dev->max_delta_ns = cev_delta2ns(dev->max_delta_ticks, dev, true);
}

(1)底层的硬件驱动知道自己的能力,可以通过struct
clock_event_device中的features成员宣称自己的功能:CLOCK_EVT_FEAT_PERIODIC说明该硬件timer可以产生周期性的clock
event,CLOCK_EVT_FEAT_ONESHOT说明自己只能产生one shot类型的clock event。如果能产生one
shot类型的event,那么即便是硬件不支持周期性的clock event,其实上层的软件可以通过不断设定next
event的方法来模拟周期性的clock event。但是如果只是支持周期性的clock event就有些麻烦了,这时候是无法模拟one
shot类型的event。也就是说,在这种情况下,整个系统必须有周期性tick,同时,系统无法支持高精度timer和dynamic
tick的情况。在这样的硬件条件下,后续的配置是没有意义的,因此直接return。

(2)底层的硬件驱动需要设定max_delta_ticks和min_delta_ticks。需要注意的是这里的tick不是system
tick,而是输入硬件timer的clock
tick,其实就是可以设定最大和最小cycle数目。这个cycle数目除以freq就是时间值,单位是秒。

(3)又是600秒,是否似曾相识。我们在clock source那篇文档中已经介绍过了,这里就不再赘述

(4)根据输入频率和最大的秒数可以计算出clock event的mult和shift这两个成员的数值,具体计算过程在clock source那篇文档中已经介绍过了,这里就不再赘述。

(5)计算min_delta_ticks和max_delta_ticks对应的ns值。

2、注册clock event device

底层的timer硬件驱动会调用clockevents_register_device函数向系统注册clock event device,代码如下:

void clockevents_register_device(struct clock_event_device *dev)
{
    unsigned long flags;

if (!dev->cpumask) {---------------------------(1)
        WARN_ON(num_possible_cpus() > 1);
        dev->cpumask = cpumask_of(smp_processor_id());
    }

raw_spin_lock_irqsave(&clockevents_lock, flags); --------------(2)

list_add(&dev->list, &clockevent_devices); ----加入clock event设备全局列表
    tick_check_new_device(dev); -----------------------(3)
    clockevents_notify_released(); ----------------------(4)

raw_spin_unlock_irqrestore(&clockevents_lock, flags);
}

(1)clock event
device的cpumask指明该设备为哪一个CPU工作,如果没有设定并且cpu的个数大于1的时候要给出warning信息并进行设定(设定为当前运行该代码的那个CPU
core)。在multi core的环境下,底层driver在调用该接口函数注册clock
event设备之前就需要设定cpumask成员,毕竟一个timer硬件附着在哪一个cpu上底层硬件最清楚,这里仅仅是亡羊补牢(毕竟我们还是希望代码不要随随便便就kernel
panic了,尽量让代码执行下去)。

(2)考虑到来自多个CPU上的并发,这里使用spin lock进行保护。关闭本地中断,可以防止来自本cpu上的并发操作。

(3)调用tick_check_new_device函数,让上层软件知道底层又注册一个新的clock
device,当然,是否上层软件要使用这个新的clock event device是上层软件的事情,clock event device
driver这个层次完成描述自己能力这部分代码就OK了。tick device相关内容请参考本站其他文档。

(4)如果上层软件想要使用新的clock event
device的话(tick_check_new_device函数中有可能会进行此操作),它会调用clockevents_exchange_device函数(可以参考上面的描述)。这时候,旧的clock

event会被从clockevent_devices链表中摘下,挂到clockevents_released队列中。在clockevents_notify_released函数中,会将old
clock event device重新挂入clockevent_devices,并调用tick_check_new_device函数。

3、配置并注册clock event device

是上面两个接口函数的综合体,这里不再详细描述。

四、用户空间接口

1、sysfs接口初始化

在系统初始化的时候会调用clockevents_init_sysfs函数(呵呵~~~对于clock
source,这个函数名字是init_clocksource_sysfs,很显然不是一个人的风格,我喜欢clockevents_sysfs_init和clocksource_sysfs_init这种函数定义风格)来初始化clock
event device layer的sys file system接口,如下:

static int __init clockevents_init_sysfs(void)
{
    int err = subsys_system_register(&clockevents_subsys, NULL); --注册clock event这种bus type

if (!err)
        err = tick_init_sysfs();
    return err;
}

该说的在clocksource那份文档中都描述了,这里直接不再赘述,重点关注tick_init_sysfs函数:

static int __init tick_init_sysfs(void)
{
    int cpu;

for_each_possible_cpu(cpu) {----------遍历各个CPU的clock event device
        struct device *dev = &per_cpu(tick_percpu_dev, cpu);--------------(1)
        int err;

dev->id = cpu;
        dev->bus = &clockevents_subsys;
        err = device_register(dev);--------------------------(2)
        if (!err)
            err = device_create_file(dev, &dev_attr_current_device);------------(3)
        if (!err)
            err = device_create_file(dev, &dev_attr_unbind_device);
        if (err)
            return err;
    }
    return tick_broadcast_init_sysfs(); -----------------------(4)
}

(1)我们始终还是要回归统一模型的三件套,bus type,device和driver。现在有了clock event bus
type,还缺少device。在clock source中,由于它是全局的,属于所有CPU
core的,因此我们定义了一个device就OK了。对于clock event,我们需要为每一个CPU定义一个device,如下:

static DEFINE_PER_CPU(struct device, tick_percpu_dev);

是否需要定义clock event device对应的driver呢?本来定义device和driver的目的是让它们在适当的时机(bus
type的match函数)相遇,相知(调用driver的probe函数)。对于clock event
device而言,是不需要定义driver的,因为设备树提供了其他的方法来进行具体底层chip级别的初始化(具体可以参考ARM generic timer驱动中的描述)。

还有一点需要注意,实际上,每一个硬件设备都是设备模型中的一个device,不过,在clock event
device这个场景下,实际上系统并不是为每一个timer硬件定义一个device,而仅仅是为每一个当前active(作为tick
device那个clock event
device)的clockevent设备定义了一个device数据结构,有空的时候大家可以思考一下这个问题。

(2)调用device_register就可以把所有的clock event device注册到系统中,统一设备模型会帮我们做一切事情。

(3)device_register之后,每个clock event device体现为一个sysfs中的目录,我们还需要为这个目录增加一些属性文件。定义如下:

static DEVICE_ATTR(current_device, 0444, sysfs_show_current_tick_dev, NULL);

static DEVICE_ATTR(unbind_device, 0200, NULL, sysfs_unbind_tick_dev);

(4)除了per cpu的device,还有一个broadcast device,定义如下:

static struct device tick_bc_dev = {
    .init_name    = "broadcast",
    .id        = 0,
    .bus        = &clockevents_subsys,
};

这个device在tick_broadcast_init_sysfs函数中调用device_register注册到系统,并创建了dev_attr_current_device的属性。和per
cpu device不同的是:没有提供用户空间的unbind操作,也就是说,userspace无法unbind当前的broadcast
clock event device。

2、属性文件操作:显示current tick device

读clock event device的current_device属性文件

static ssize_t sysfs_show_current_tick_dev(struct device *dev, struct device_attribute *attr, char *buf)
{
    struct tick_device *td;
    ssize_t count = 0;

raw_spin_lock_irq(&clockevents_lock);
    td = tick_get_tick_dev(dev); ----获取tick device
    if (td && td->evtdev)
        count = snprintf(buf, PAGE_SIZE, "%s\n", td->evtdev->name);
    raw_spin_unlock_irq(&clockevents_lock);
    return count;
}

一个active的clock event device对应的tick device有两种情况,一种是per cpu的tick
device,驱动系统运作。另外一种是broadcast tick device,这些内容留在在tick device部分描述吧。

3、属性文件操作:unbind tick device

一个tick device总是绑定一个属于该cpu core并且精度最高的那个clock event
device。通过sysfs的接口可以解除这个绑定。如果解除绑定的那个clock event
device是unused,可以直接从clockevent_devices全局链表中删除。如果该设备当前使用中,那么需要找到一个替代的clock
event device。如果找不到一个替代的clock event
device,那么不能unbind当前的device,返回EBUSY。具体的代码逻辑就不分析,大家可以自己阅读代码理解。

上一篇:Java-JVM-GC


下一篇:git查看历史操作