Linux時間子系統之時鐘源層(Clock Source)

所謂時鐘源設備,Linux將其抽象爲一個可以記錄時間流逝的設備,其值隨着時間的流逝遞增。將前一次讀取的值和當前的值做比較就知道過去了多長的時間。但是它不一定是記錄當前具體時間的設備,它只記錄過去了多少時間。

對於時間Linux還抽象了一個叫做定時事件設備(Clock Event Device),這兩種設備的區別是,時鐘源雖然也會按照一定週期遞增,但其並不會觸發中斷,如果CPU不讀,那時鐘源設備就自己默默的在那裏遞增,不會打擾CPU。

時鐘源設備在Linux內核中使用clocksource結構來表示(代碼位於include/linux/clocksource.h中):

struct clocksource {
	u64 (*read)(struct clocksource *cs);
	u64 mask;
	u32 mult;
	u32 shift;
	u64 max_idle_ns;
	u32 maxadj;
#ifdef CONFIG_ARCH_CLOCKSOURCE_DATA
	struct arch_clocksource_data archdata;
#endif
	u64 max_cycles;
	const char *name;
	struct list_head list;
	int rating;
	int (*enable)(struct clocksource *cs);
	void (*disable)(struct clocksource *cs);
	unsigned long flags;
	void (*suspend)(struct clocksource *cs);
	void (*resume)(struct clocksource *cs);
	void (*mark_unstable)(struct clocksource *cs);
	void (*tick_stable)(struct clocksource *cs);

#ifdef CONFIG_CLOCKSOURCE_WATCHDOG
	......
#endif
	struct module *owner;
};

這個結構體中的各個字段含義如下:

  • read:當要讀取時鐘源設備當前的週期數值時,會調用對應設備的該函數。

  • mask:代表了這個時鐘源一共用了多少二進制位來計數。如果用了56位,那這個mask就是最低56位是1,最高8位是0。

  • mult和shift:用於將時鐘週期數值轉換成納秒值。爲什麼要用這兩個值進行轉換,怎麼轉換,以及這兩個值怎麼算在後面會介紹(基本上和定時事件設備一樣,只不過剛好反過來)。

  • max_idle_ns:表示該時鐘源設備最多能記錄多長跨度的時間,其值是根據max_cycles、mult和shift算出來的。

  • maxadj:對mult的最大調整值,其比率是固定的,是mult值的11%,也就是說如果需要對mult值進行調整的話,不能超過正負11%的範圍。這個值在時間維持層(Time Keeping)會用到。

static u32 clocksource_max_adjustment(struct clocksource *cs)
{
	u64 ret;
	
	ret = (u64)cs->mult * 11;
	do_div(ret,100);
	return (u32)ret;
}
  • archdata:存放不同架構平臺專用的一些數據。

  • max_cycles:表示該時鐘源設備最多能記錄多長週期數值。首先,這個值肯定要小於mask的值,硬件本身都記錄不了,那肯定不行。其次,還要考慮mult和shift的值,因爲在將週期數轉換成納秒數時是需要用這兩個參數計算的,如果計算的過程中產生了溢出(64位)那也不行。

  • name:是給這個時鐘源設備起的一個名字,一般比較直觀,在/proc/timer_list中或者dmesg中都會出現。

  • list:系統中所有的時鐘源設備實例都會保存在全局鏈表clocksource_list中,這個變量作爲鏈表的元素(代碼位於kernel/time/clocksource.c)。

static LIST_HEAD(clocksource_list);
  • rating:代表這個時鐘源設備的精度值,其取值範圍從1到499,數字越大代表設備的精度越高。當系統中同時有多個定時事件設備存在的時候,內核可以根據這個值選一個最佳的設備。

  • enable:當要打開時鐘源設備時,會調用對應設備的該函數。

  • disable:當要關閉時鐘源設備時,會調用對應設備的該函數。

  • flags:表示該時鐘源設備的一些特徵屬性,一個時鐘源可以同時包含多個屬性。如果新註冊的時鐘源的的屬性包含CLOCK_SOURCE_MUST_VERIFY,表示該時鐘源需要經過看門狗(Watch Dog)的監測。如果誤差過大,則會被標記爲CLOCK_SOURCE_UNSTABLE。CLOCK_SOURCE_IS_CONTINUOUS表示該時鐘源設備是連續的。CLOCK_SOURCE_VALID_FOR_HRES表明該設備是高分辨率的。

#define CLOCK_SOURCE_IS_CONTINUOUS		0x01
#define CLOCK_SOURCE_MUST_VERIFY		0x02
#define CLOCK_SOURCE_WATCHDOG			0x10
#define CLOCK_SOURCE_VALID_FOR_HRES		0x20
#define CLOCK_SOURCE_UNSTABLE			0x40
#define CLOCK_SOURCE_SUSPEND_NONSTOP		0x80
#define CLOCK_SOURCE_RESELECT			0x100
  • suspend:當要暫停時鐘源設備時,會調用對應設備的該函數。

  • resume:當要恢復時鐘源設備時,會調用對應設備的該函數。

  • mark_unstable:如果Linux的系統看門狗(Watch Dog)發現這個時鐘源不穩定,會調用對應設備的該函數。

  • tick_stable:如果Linux的系統看門狗(Watch Dog)發現這個時鐘源比較穩定,會調用對應設備的該函數。

  • owner:擁有這個時鐘源設備的模塊。

1)mult、shift的計算和週期數到納秒的轉換

在時鐘源層,從週期數轉換成納秒是在函數clocksource_cyc2ns裏面完成的:

static inline s64 clocksource_cyc2ns(u64 cycles, u32 mult, u32 shift)
{
	return ((u64) cycles * mult) >> shift;
}

非常的簡單,不過同樣是從週期數轉換成納秒數,計算公式和在定時事件層是不一樣的。這裏是用(cycles * mult) >> shift公式計算出來的,而在定時事件層,是用(cycles << shift) / mult公式計算出來的,剛好相反。而這兩種設備mult和shift的值又都是調用時鐘源層的clocks_calc_mult_shift函數計算出來的:

void
clocks_calc_mult_shift(u32 *mult, u32 *shift, u32 from, u32 to, u32 maxsec)
{
	u64 tmp;
	u32 sft, sftacc= 32;
 
        /* 計算最大納秒數前面有多少個0 */
	tmp = ((u64)maxsec * from) >> 32;
	while (tmp) {
		tmp >>=1;
		sftacc--;
	}
	
        /* 試探計算mult和shift的最大值 */
	for (sft = 32; sft > 0; sft--) {
                /* 左移sft位 */
		tmp = (u64) to << sft;
                /* 四捨五入 */
		tmp += from / 2;
		do_div(tmp, from);
                /* 判斷是否會越界 */
		if ((tmp >> sftacc) == 0)
			break;
	}
	*mult = tmp;
	*shift = sft;
}
EXPORT_SYMBOL_GPL(clocks_calc_mult_shift);

細心觀察可以發現,兩個層調用clocks_calc_mult_shift函數傳遞的參數是不一樣的。在時鐘源層,from傳遞的是頻率,to傳遞的是NSEC_PER_SEC;而在定時時間層,from傳遞的是NSEC_PER_SEC,to傳遞的是頻率,剛好就是相反的。

2)時鐘源設備的註冊

時鐘源設備的註冊需要調用clocksource_register_hz或者clocksource_register_khz函數:

static inline int clocksource_register_hz(struct clocksource *cs, u32 hz)
{
	return __clocksource_register_scale(cs, 1, hz);
}

static inline int clocksource_register_khz(struct clocksource *cs, u32 khz)
{
	return __clocksource_register_scale(cs, 1000, khz);
}

這兩個函數最後都會調用函數__clocksource_register_scale,只是單位不同:

int __clocksource_register_scale(struct clocksource *cs, u32 scale, u32 freq)
{
	unsigned long flags;

	clocksource_arch_init(cs);

	/* 計算mult、shift、max_idle_ns和maxadj的值 */
	__clocksource_update_freq_scale(cs, scale, freq);

	mutex_lock(&clocksource_mutex);

	clocksource_watchdog_lock(&flags);
        /* 將時鐘源設備插入clocksource_list全局列表中 */
	clocksource_enqueue(cs);
	clocksource_enqueue_watchdog(cs);
	clocksource_watchdog_unlock(&flags);

        /* 選擇最好的時鐘源 */
	clocksource_select();
	clocksource_select_watchdog(false);
	__clocksource_suspend_select(cs);
	mutex_unlock(&clocksource_mutex);
	return 0;
}
EXPORT_SYMBOL_GPL(__clocksource_register_scale);

clocksource_arch_init是不同架構平臺需要自己實現的初始化函數,至少對於Arm64來說,其定義爲什麼都不做。然後調用__clocksource_update_freq_scale函數更新本設備的mult、shift、max_idle_ns和maxadj的值。mult和shift的計算前面已經說過了,我們來看看max_idle_ns和maxadj的值是怎麼計算的:

void __clocksource_update_freq_scale(struct clocksource *cs, u32 scale, u32 freq)
{
	u64 sec;

	if (freq) {
		/* 計算最大跨度是多少秒 */
		sec = cs->mask;
		do_div(sec, freq);
		do_div(sec, scale);
		if (!sec)
			sec = 1;
		else if (sec > 600 && cs->mask > UINT_MAX)
			sec = 600;

		clocks_calc_mult_shift(&cs->mult, &cs->shift, freq,
				       NSEC_PER_SEC / scale, sec * scale);
	}
	/* 計算maxadj的值 */
	cs->maxadj = clocksource_max_adjustment(cs);
	while (freq && ((cs->mult + cs->maxadj < cs->mult)
		|| (cs->mult - cs->maxadj > cs->mult))) {
		cs->mult >>= 1;
		cs->shift--;
		cs->maxadj = clocksource_max_adjustment(cs);
	}

        /* 對於freq爲0的情況,mult是自己定的,有可能會越界。 */
	WARN_ONCE(cs->mult + cs->maxadj < cs->mult,
		"timekeeping: Clocksource %s might overflow on 11%% adjustment\n",
		cs->name);

        /* 計算max_cycles和max_idle_ns的值 */
	clocksource_update_max_deferment(cs);

        /* 打印日誌 */
	pr_info("%s: mask: 0x%llx max_cycles: 0x%llx, max_idle_ns: %lld ns\n",
		cs->name, cs->mask, cs->max_cycles, cs->max_idle_ns);
}
EXPORT_SYMBOL_GPL(__clocksource_update_freq_scale);

這個初始化函數首先計算時鐘源設備能記錄的最大跨度是多少秒。mask表示時鐘源能用多少二進制位計數,那麼最大跨度當然就是從mask的值,然後用mask/freq就可以計算出最大跨度是多少秒。如果最大跨度大於10分鐘,且mask的位數大於32位的話,爲了保證精度,人爲限定最大跨度爲10分鐘。

前面說了,maxadj的值其實就是mult * 11%,不過如果mult + maxadj的值越界的話,還需要再相應調小mult和shift的值,然後再計算,直到不越界爲止。注意,它們都是32位的。

設置這些參數的前提條件是函數必須傳入一個大於0的freq參數值。如果freq傳入的是0,則mult和shift都由時鐘源自己設置,maxadj也不會調整。那什麼時鐘源註冊的時候會把freq設置成0呢?一般在系統初始化的時候,Linux內核會默認註冊一個精度最低的缺省時鐘源設備,叫做jiffies(代碼位於kernel/time/jiffies.c中):

static struct clocksource clocksource_jiffies = {
	.name		= "jiffies",
	.rating		= 1,
	.read		= jiffies_read,
	.mask		= CLOCKSOURCE_MASK(32),
	.mult		= TICK_NSEC << JIFFIES_SHIFT,
	.shift		= JIFFIES_SHIFT,
	.max_cycles	= 10,
};

可以看到,該時鐘源的mult和shift全都是自己事先定義好的,不需要再動態計算,所以註冊的時候freq設置成0了。這個時鐘源完全是根據系統jiffies來工作的,而系統jiffies又是由Tick設備來更新的,其更新頻率是由編譯選項決定的。如果編譯選項選擇了250Hz,那麼其更新週期就是4毫秒。這個精度是非常差的,所以其精度值被設置成了1,除非實在沒有可用的設備了,否則絕對不會使用。

初始化參數完成後,__clocksource_update_freq_scale函數會接着調用clocksource_enqueue函數,將本設備插入到clocksource_list全局鏈表中去:

static void clocksource_enqueue(struct clocksource *cs)
{
	struct list_head *entry = &clocksource_list;
	struct clocksource *tmp;

	list_for_each_entry(tmp, &clocksource_list, list) {
		if (tmp->rating < cs->rating)
			break;
		entry = &tmp->list;
	}
	list_add(&cs->list, entry);
}

可以看出來,clocksource_list全局鏈表裏面的所有時鐘源設備是按照精度值由高到低排序的。

最後,__clocksource_update_freq_scale函數會調用clocksource_update_max_deferment函數更新max_cycles和max_idle_ns的值:

u64 clocks_calc_max_nsecs(u32 mult, u32 shift, u32 maxadj, u64 mask, u64 *max_cyc)
{
	u64 max_nsecs, max_cycles;

	/* 必須保證max_cycles * mult不能越界 */
	max_cycles = ULLONG_MAX;
	do_div(max_cycles, mult+maxadj);

	/* max_cycles不能大於mask的值 */
	max_cycles = min(max_cycles, mask);
	max_nsecs = clocksource_cyc2ns(max_cycles, mult - maxadj, shift);

	/* 設置max_cycles參數 */
	if (max_cyc)
		*max_cyc = max_cycles;

	/* 只返回實際max_nsecs的一半 */
	max_nsecs >>= 1;

	return max_nsecs;
}

static inline void clocksource_update_max_deferment(struct clocksource *cs)
{
	cs->max_idle_ns = clocks_calc_max_nsecs(cs->mult, cs->shift,
						cs->maxadj, cs->mask,
						&cs->max_cycles);
}

clocksource_update_max_deferment函數直接調用了函數clocks_calc_max_nsecs,注意最後一個參數max_cyc是指針傳遞的。

max_cycles是系統能記錄的最大時間跨度的週期數。它有兩個限定條件,一個是必須小於mask的值,另外一個是max_cycles * mult的值不能64位溢出。由於在實際計算時,mult有可能要加上最大或減去最大maxadj調整值,所以max_cycles必須小於等於64位無符號數能便是的最大數值除以mult+maxadj。

max_cycles的值確定下來後,可以通過clocksource_cyc2ns函數,直接將對應的週期數值轉換成納秒值,計算公式就是(max_cycles * mult) >> shift。考慮到有可能會用maxadj調整的因素,這裏取其可能的最小值,也就是計算公式是(max_cycles * (mult - maxadj)) >> shift。這樣算出來後還不放心,又將其減了一半才返回。

2)時鐘源設備的挑選

前面的代碼中已經看到了,當註冊新的時鐘源設備到系統中時,內核會調用函數clocksource_select函數選擇出一個最佳的時鐘源:

static void clocksource_select(void)
{
	__clocksource_select(false);
}

其就是簡單調用了__clocksource_select函數:

static void __clocksource_select(bool skipcur)
{
	bool oneshot = tick_oneshot_mode_active();
	struct clocksource *best, *cs;

	/* 找到最好的時鐘源設備 */
	best = clocksource_find_best(oneshot, skipcur);
	if (!best)
		return;

	if (!strlen(override_name))
		goto found;

        /* 遍歷所有當前註冊的設備 */
	list_for_each_entry(cs, &clocksource_list, list) {
                /* 是否忽略當前正在使用的設備 */
		if (skipcur && cs == curr_clocksource)
			continue;
                /* 是否是指定的設備 */
		if (strcmp(cs->name, override_name) != 0)
			continue;
		/* 如果當前Tick設備處於單次觸發模式,則時鐘源設備必須支持高精度模式。*/
		if (!(cs->flags & CLOCK_SOURCE_VALID_FOR_HRES) && oneshot) {
			/* 如果時鐘源設備不穩定,則不能使用,並給出提示。 */
			if (cs->flags & CLOCK_SOURCE_UNSTABLE) {
				pr_warn("Override clocksource %s is unstable and not HRT compatible - cannot switch while in HRT/NOHZ mode\n",
					cs->name);
				override_name[0] = 0;
			} else {
				pr_info("Override clocksource %s is not currently HRT compatible - deferring\n",
					cs->name);
			}
		} else
			/* 使用指定設備 */
			best = cs;
		break;
	}

found:
	if (curr_clocksource != best && !timekeeping_notify(best)) {
		pr_info("Switched to clocksource %s\n", best->name);
		curr_clocksource = best;
	}
}

該函數的參數skipcur表示選擇的時候忽略當前正在使用的時鐘源設備,一定要選擇一個新的設備。該函數調用clocksource_find_best函數找尋最佳設備,如果沒有則直接返回。如果有的話,還有要判斷是不是系統用戶手動指定了一個時鐘源設備(如果手動指定的話,會將override_name設置成指定的時鐘源設備名稱)。最後,如果現在正在使用的設備和挑選出來的設備不同,且時間維持層“同意”(調用timekeeping_notify函數)對當前時鐘源設備的更改後,會正式將代表當前正在使用時鐘源設備的全局變量curr_clocksource指向新註冊的設備。

下面我們來看看clocksource_find_best函數是如何選擇最佳設備的:

static struct clocksource *clocksource_find_best(bool oneshot, bool skipcur)
{
	struct clocksource *cs;

	if (!finished_booting || list_empty(&clocksource_list))
		return NULL;

	list_for_each_entry(cs, &clocksource_list, list) {
                /* 如果skipcur是True,則跳過當前設備。 */
		if (skipcur && cs == curr_clocksource)
			continue;
                /* 如果當前Tick設備處於單次觸發模式,則時鐘源設備必須支持高精度模式。 */
		if (oneshot && !(cs->flags & CLOCK_SOURCE_VALID_FOR_HRES))
			continue;
		return cs;
	}
	return NULL;
}

如果系統還沒有完成啓動過程,或者clocksource_list全局鏈表爲空,則直接返回空指針,表示什麼都沒選中。所以,在系統正在啓動的過程中是不會選擇最佳設備的。

另外,如果當前的Tick設備已經切換到支持單次觸發模式了,則當前高精度定時器已經切換到高精度模式了,所以這裏時鐘源也必須同步支持高精度模式。

在前面講設備註冊的時候提到過,在將時鐘源設備插入clocksource_list全局鏈表的時候,已經根據精度值從高到低排序過了,所以這裏找到第一個全部滿足條件的設備一定是精度最高的。

前面提到了,系統正在啓動的過程中是不會選擇最佳設備的,那系統時鐘源設備是在什麼時機選擇的呢?答案在函數clocksource_done_booting裏面:

static int __init clocksource_done_booting(void)
{
	mutex_lock(&clocksource_mutex);
        /* 獲得系統缺省時鐘源設備 */
	curr_clocksource = clocksource_default_clock();
	finished_booting = 1;
	/* 啓動看門狗線程去除一些不穩定的時鐘源 */
	__clocksource_watchdog_kthread();
        /* 選擇時鐘源設備 */
	clocksource_select();
	mutex_unlock(&clocksource_mutex);
	return 0;
}
fs_initcall(clocksource_done_booting);

調用函數clocksource_default_clock獲得系統缺省的時鐘源設備,其定義如下(代碼位於kernel/time/jiffies.c中):

struct clocksource * __init __weak clocksource_default_clock(void)
{
	return &clocksource_jiffies;
}

所以其實那個精度最差的,以系統jiffies更新爲基準的時鐘源設備clocksource_jiffies是系統的缺省設備,如果實在沒得選,那至少還有一個可用。

3)註冊sysfs

時鐘源設備會在sysfs中註冊對應的文件,可以通過訪問這些文件的內容知道當前系統中關於時鐘源設備的基本信息。

註冊sysfs是在init_clocksource_sysfs函數中完成的:

static int __init init_clocksource_sysfs(void)
{
	int error = subsys_system_register(&clocksource_subsys, NULL);

	if (!error)
		error = device_register(&device_clocksource);

	return error;
}

device_initcall(init_clocksource_sysfs);

subsys_system_register函數會根據參數將一個子系統註冊在/sys/devices/system/目錄下。註冊信息保存在clocksource_subsys靜態全局變量中:

static struct bus_type clocksource_subsys = {
	.name = "clocksource",
	.dev_name = "clocksource",
};

所以總線名字叫做“clocksource”,而設備名字也叫做“clocksource”。

接着,函數調用device_register函數,在這個子系統下注冊不同的設備,傳入的參數是指向device_clocksource全局結構體變量的指針:

static struct device device_clocksource = {
	.id	= 0,
	.bus	= &clocksource_subsys,
	.groups	= clocksource_groups,
};

設備號一定是0,所以對應的目錄是/sys/devices/system/clocksource/clocksource0/。屬性組被設置成了clocksource_groups,也是一個全局變量:

static struct attribute *clocksource_attrs[] = {
	&dev_attr_current_clocksource.attr,
	&dev_attr_unbind_clocksource.attr,
	&dev_attr_available_clocksource.attr,
	NULL
};
ATTRIBUTE_GROUPS(clocksource);

共有三個屬性:

  1. dev_attr_current_clocksource:對應的文件名是current_clocksource,訪問權限是644,對root用戶可寫,對所有用戶可讀。寫入的內容是時鐘源設備的名字,內核會查找並用其替換當前的時鐘源。如果讀的話,其內容是當前正在使用的時鐘源設備名字。
  2. dev_attr_unbind_clocksource:對應的文件名是unbind_clocksource,訪問權限是200,對root用戶可寫。寫入的內容是時鐘源設備的名字,內核會查找並解綁該設備,如果要解綁的時鐘源設備是當前正在使用的,還會再選擇一個替換的。
  3. dev_attr_available_clocksource:對應的文件名是available_clocksource,訪問權限是444,對所有用戶可讀。該文件的內容是系統中所有註冊的時鐘源設備的名字。

接下來,我們接着看看dev_attr_current_clocksource的實現。其定義如下:

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

	mutex_lock(&clocksource_mutex);
	count = snprintf(buf, PAGE_SIZE, "%s\n", curr_clocksource->name);
	mutex_unlock(&clocksource_mutex);

	return count;
}

ssize_t sysfs_get_uname(const char *buf, char *dst, size_t cnt)
{
	size_t ret = cnt;

	if (!cnt || cnt >= CS_NAME_LEN)
		return -EINVAL;

	if (buf[cnt-1] == '\n')
		cnt--;
	if (cnt > 0)
		memcpy(dst, buf, cnt);
	dst[cnt] = 0;
	return ret;
}

static ssize_t current_clocksource_store(struct device *dev,
					 struct device_attribute *attr,
					 const char *buf, size_t count)
{
	ssize_t ret;

	mutex_lock(&clocksource_mutex);

        /* 讀取寫入的名字到全局變量override_name中 */
	ret = sysfs_get_uname(buf, override_name, count);
        /* 如果內容不爲空則重新選擇時鐘源設備 */
	if (ret >= 0)
		clocksource_select();

	mutex_unlock(&clocksource_mutex);

	return ret;
}
static DEVICE_ATTR_RW(current_clocksource);

經過DEVICE_ATTR_RW宏的定義,寫如文件會調用current_clocksource_store函數,而讀取文件會調用current_clocksource_show,後綴是固定的。讀取非常簡單,直接訪問代表當前正在使用時鐘源設備的全局變量curr_clocksource,讀取其name變量就行了。而寫入的話,會將輸入的內容讀入全局變量override_name中,然後調用clocksource_select函數再選一次。前面分析過了clocksource_select函數在選擇的時候會查看override_name,儘量選擇這個指定的設備。

最後,我們來看一個實際的例子,在64位樹莓派4系統下,訪問/sys/devices/system/clocksource/clocksource/current_device將會返回arch_sys_counter,表明其當前使用的是Arm通用計時器

用dmsg訪問內核日誌,可看到下面和時鐘源相關的日誌:

[    0.000000] clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x46d987e47, max_idle_ns: 440795202767 ns
[    0.191511] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
[    0.293498] clocksource: Switched to clocksource arch_sys_counter

就像前面分析的那樣,系統內註冊了兩個時鐘源設備,一個是缺省的jiffies(32位),另一個是Arm通用計時器架構提供的時鐘源arch_sys_counter(56位),系統最後毫無疑問的選擇了arch_sys_counter。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章