1. 程式人生 > >Linux時間子系統(十六) clockevent

Linux時間子系統(十六) clockevent

events puma == 精度 軟件 head 我們 exp suspend

一、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。具體的代碼邏輯就不分析,大家可以自己閱讀代碼理解。

Linux時間子系統(十六) clockevent