歡迎光臨
每天分享高質量文章

Linux時間子系統之:POSIX Clock

一、前言

clock是timer的基礎,任何一個timer都需要運作在一個指定的clock上來。核心中維護了若干的clock。根據計時的特點,clock分成兩種:一種是真實世界的時間概念,另外一個是僅僅計算CPU執行時間 。從clock的生命週期來看,可以分成靜態和動態的posix clock,靜態是一直存在於核心中的,而動態clock有建立和銷毀的概念。

二、基本概念

1、核心資料結構

所謂clock,實際上就是一種計時工具,可能是硬體,也可能是軟體,當然對於POSIX clock而言,當然是指軟體抽象了。clock能夠記錄一段時間的流逝,這段時間可能是真實的牆上時間,也可能是虛擬的時間,例如基於某個行程或者執行緒的CPU執行時間。在linux kernel中,用struct k_clock來抽象,具體定義如下:

struct k_clock {
int (*clock_getres) (const clockid_t which_clock, struct timespec *tp);
int (*clock_set) (const clockid_t which_clock, const struct timespec *tp);
int (*clock_get) (const clockid_t which_clock, struct timespec * tp);
int (*clock_adj) (const clockid_t which_clock, struct timex *tx);
int (*timer_create) (struct k_itimer *timer);
int (*nsleep) (const clockid_t which_clock, int flags, struct timespec *, struct timespec __user *);
long (*nsleep_restart) (struct restart_block *restart_block);
int (*timer_set) (struct k_itimer * timr, int flags, struct itimerspec * new_setting,
struct itimerspec * old_setting);
int (*timer_del) (struct k_itimer * timr);
void (*timer_get) (struct k_itimer * timr, struct itimerspec * cur_setting);
};

clock作為一個計時工具當然有計時精度,透過clock_getres函式可以獲取該clock的時間精度,需要說明的是這個精度是和timer相關的,用於將使用者設定的timer超時時間規整到clock精度允許的數值上。clock_get和clock_set函式可以分別獲取和設定當前的時間,這個時間值是一個絕對時間值(對於時間軸而言,這個絕對時間也是相對的,是相對於該timeline的epoch而言),標記了當前時間點。clock計時有可能是不準確的,例如基於系統晶振的clock。一方面本身晶振的精度有限,時間累積長了會出現較大誤差。另外,晶振也會隨著使用時間的推移、溫度的變化等等因素而導致誤差。clock_adj函式允許系統根據外部的精確時間資訊對本clock進行調整。nsleep和nsleep_restart這兩個成員函式可以讓行程sleep一段時間。timer_xxx系列函式是和POSIX interval timer相關,具體會在POSIX timer檔案中描述。

2、靜態定義的clock

static struct k_clock posix_clocks[MAX_CLOCKS];

posix_clocks陣列定義了系統支援的所有的clock,相關的定義如下:

#define CLOCK_REALTIME            0
#define CLOCK_MONOTONIC            1
#define CLOCK_PROCESS_CPUTIME_ID    2
#define CLOCK_THREAD_CPUTIME_ID        3
#define CLOCK_MONOTONIC_RAW        4
#define CLOCK_REALTIME_COARSE        5
#define CLOCK_MONOTONIC_COARSE        6
#define CLOCK_BOOTTIME            7
#define CLOCK_REALTIME_ALARM        8
#define CLOCK_BOOTTIME_ALARM        9
#define CLOCK_SGI_CYCLE            10    /* Hardware specific */
#define CLOCK_TAI            11

#define MAX_CLOCKS            16

POSIX標準定義了4種型別的clock,CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID,其他是linux specific。如果一個clock的timeline是基於CPU執行時間的,那麼我們稱之CPU-time clock。CPU-time clock主要是用來為某個行程或者執行緒的執行時間進行計時的,一旦執行緒(行程)被切換,那麼該clock就停掉了,直到下次排程器切換回該執行緒(行程)執行。

各個具體的作業系統實現可以定義自己特有的clock,對於Linux kernel,我們定義了若干種clock。CLOCK_MONOTONIC_RAW啟動時間點被設成0,此後一直不斷累加,而且能設定,不會隨NTP調整。CLOCK_REALTIME_COARSE、CLOCK_MONOTONIC_COARSE的概念和CLOCK_REALTIME、CLOCK_MONOTONIC的概念是類似的,只不過是精度是比較粗的版本。有時候,timer沒有必要要求那麼高的精度,那麼我們可以使用這種clock,從而可以獲取更好的效能。CLOCK_BOOTTIME和CLOCK_MONOTONIC類似,也是單調上述,在系統初始化的時候設定的基準數值是0,不過CLOCK_BOOTTIME計算系統suspend的時間,也就是說,不論是running還是suspend(這些都算是啟動時間),CLOCK_BOOTTIME都會累積計時,直到系統reset或者shutdown。

CLOCK_REALTIME_ALARM和CLOCK_BOOTTIME_ALARM主要用於Alarmtimer,這種timer是基於RTC的,更詳細的內容請參考本站Alarmtimer的檔案。CLOCK_TAI是原子鐘的時間,和基於UTC的CLOCK_REALTIME類似,不過沒有leap second。

使用者空間的clock_xxx函式會傳遞clock id的引數,在核心態,根據id作為index在posix_clocks陣列中可以索引到對應的clock,然後呼叫clock對應的callback函式就OK了。當然基本意思就是這樣,具體實現如下:

static struct k_clock *clockid_to_kclock(const clockid_t id)
{
if (id < 0)
return (id & CLOCKFD_MASK) == CLOCKFD ?
&clock;_posix_dynamic : &clock;_posix_cpu;

    if (id >= MAX_CLOCKS || !posix_clocks[id].clock_getres)
return NULL;
return &posix;_clocks[id];
}

clockid_to_kclock這個函式用來將clock id和具體的posix clock的k_clock 資料結構對應起來。在linux平臺上,clockid是int型別的資料,共32個bit,高29個bit用來儲存一個pid(用於CPU-time clock)或者fd(動態分配的clock),bit 2用來說明該CPU-time clock是一個行程clock還是執行緒clock。bit 1和bit 0用來說明該clock id的型別:PROF=0, VIRT=1, SCHED=2, or FD=3。

當clock id小於0的時候,要麼是CPU-time clock,要麼是動態分配的clock,可以根據clock id的型別來判斷。CPU-time clock和動態分配的clock後面會具體介紹。

三、各種real timeclock的定義

系統初始化的時候會呼叫init_posix_timers函式對各種靜態定義的real time clock進行註冊。註:monotonic clock也是real time clock的一種,全稱是monotonic real time clock。

1、real time clock的定義如下(timer相關內容不在本文描述):

struct k_clock clock_realtime = {
.clock_getres    = hrtimer_get_res,
.clock_get    = posix_clock_realtime_get,
.clock_set    = posix_clock_realtime_set,
.clock_adj    = posix_clock_realtime_adj,
.nsleep        = common_nsleep,
.nsleep_restart    = hrtimer_nanosleep_restart,
};

real time clock需要呼叫timekeeping模組的介面來獲取和設定當前時間值。對於獲取當前時間值的函式posix_clock_realtime_get而言,是呼叫ktime_get_real_ts函式,該函式是timekeeping模組的介面函式,以timespec的格式回了real time clock的當前值。posix_clock_realtime_set函式主要是呼叫do_settimeofday這個timekeeping模組的介面函式。posix_clock_realtime_adj是呼叫do_adjtimex介面函式來實現具體的功能。

納秒級別的sleep是透過高精度timer實現的,real time clock的精度和hrtimer相關,具體可以參考hrtimer相關檔案。

2、monotonic clock的定義如下:

struct k_clock clock_monotonic = {
.clock_getres    = hrtimer_get_res,
.clock_get    = posix_ktime_get_ts,
.nsleep        = common_nsleep,
.nsleep_restart    = hrtimer_nanosleep_restart,
};

monotonic clock沒有clock_set函式,不能被設定。透過ktime_get_ts這個timekeeping模組的介面可以獲得monotonic clock的當前值。納秒級別的sleep以及精度的獲取函式和real time clock一樣。

3、monotonic raw clock的定義如下:

struct k_clock clock_monotonic_raw = {
.clock_getres    = hrtimer_get_res,
.clock_get    = posix_get_monotonic_raw,
};

posix_get_monotonic_raw函式是呼叫timekeeping模組getrawmonotonic介面函式實現獲取monotonic raw clock當前時間數值的。和monotonic clock一樣,不能設定。和monotonic clock不同的是該clock沒有timer相關的callback函式。

4、coarse clock

struct k_clock clock_realtime_coarse = {
.clock_getres    = posix_get_coarse_res,
.clock_get    = posix_get_realtime_coarse,
};
struct k_clock clock_monotonic_coarse = {
.clock_getres    = posix_get_coarse_res,
.clock_get    = posix_get_monotonic_coarse,
};

這兩個clock的精度都是和tick相關的,KTIME_LOW_RES定義就是tick的納秒數值。clock_get函式分別呼叫current_kernel_time和get_monotonic_coarse獲取當前時間點的值。

CLOCK_BOOTTIME和CLOCK_TAI的clock實現非常簡單,大家自行閱讀程式碼就OK了。

四、CPU-time clock

1、概述

從使用者空間的角度看,有兩種CPU-time clock的應用場景:

(1)呼叫clock_xxx函式並傳遞CLOCK_PROCESS_CPUTIME_ID或者CLOCK_THREAD_CPUTIME_ID給該函式

(2)呼叫clock_getcpuclockid或者pthread_getcpuclockid函式來獲取指定行程或者執行緒的clock id,之後呼叫clock_xxx函式並傳遞該clock id引數

應對第一種場景,系統初始化的時候會呼叫init_posix_cpu_timers函式對靜態定義的CPU-time clock進行註冊。對於第二種場景,核心靜態定義了一個clock_posix_cpu的clock來應對這種需求。

2、指定行程或者執行緒的CPU-time clock

核心靜態定義了一個clock如下(去掉了timer的callback函式):

struct k_clock clock_posix_cpu = {
.clock_getres    = posix_cpu_clock_getres,
.clock_set    = posix_cpu_clock_set,
.clock_get    = posix_cpu_clock_get,
.nsleep        = posix_cpu_nsleep,
.nsleep_restart    = posix_cpu_nsleep_restart,
};

(1)獲取精度資訊

static int posix_cpu_clock_getres(const clockid_t which_clock, struct timespec *tp)
{
int error = check_clock(which_clock);--------引數校驗
if (!error) {
tp->tv_sec = 0;
tp->tv_nsec = ((NSEC_PER_SEC + HZ – 1) / HZ);
if (CPUCLOCK_WHICH(which_clock) == CPUCLOCK_SCHED) {
tp->tv_nsec = 1;
}
}
return error;
}

該函式的執行邏輯分成兩個部分,一部分是引數校驗,一部分是傳回精度。引數校驗需要檢查的包括:

(a)clock id中的高29個bit包含了pid,獲取pid的程式碼如下:

#define CPUCLOCK_PID(clock)        ((pid_t) ~((clock) >> 3))

從程式碼可知,實際上並不是將pid放到高29個bit,而是將反碼儲存到了高29個bit。為何儲存反碼?這樣做為了確保clock id是一個負數(MSB是1),還記得clockid_to_kclock的實現嗎?要獲取該clock id的精度,要確保該pid的task存在

(b)如果該clock id是一個行程相關的(呼叫clock_getcpuclockid獲得),那麼這個行程id應該是一個實實在在的行程id。在linux kernel中,pid實際上是執行緒ID,POSIX標準的行程ID,也就是PID在核心中被成為執行緒組ID。因此,所謂一個“實實在在的行程id”就是說該執行緒的id(pid)和tgid一樣,該pid標識的執行緒是執行緒組leader。當然,就是獲取精度而已,實際上要求並不要那麼嚴格,也許該pid標識的執行緒leader會退出,因此實際上要求該pid標識的task有thread group leader就OK了。(這裡有可能理解有誤,TODO)

(c)如果該clock id是一個執行緒相關的(呼叫pthread_getcpuclockid獲得),那麼呼叫者必須和該執行緒(clock id中指明的那個執行緒)屬於一個行程(執行緒組)。

傳回精度部分的程式碼邏輯很簡單,對於PROF和VIRT型別的CPU-time clock,其精度是tick,對於SCHED型別,精度是1ns。

(2)獲取當前時間值

同樣的,首先需要從clock id中獲取pid的值,然後根據pid的值獲取對應的task sturct,如果pid等於0,那麼不需要費勁去尋找。得到task struct之後,可以呼叫posix_cpu_clock_get_task函式獲取時間值:

static int posix_cpu_clock_get_task(struct task_struct *tsk,   const clockid_t which_clock,
struct timespec *tp)
{
int err = -EINVAL;
unsigned long long rtn;

    if (CPUCLOCK_PERTHREAD(which_clock)) {---per 執行緒的cpu clock
if (same_thread_group(tsk, current))---必須和呼叫者是同一個執行緒組,也就是同一個行程
err = cpu_clock_sample(which_clock, tsk, &rtn;);
} else {

        if (tsk == current || thread_group_leader(tsk))---行程的cpu clock
err = cpu_clock_sample_group(which_clock, tsk, &rtn;);
}

    if (!err)
sample_to_timespec(which_clock, rtn, tp); ---給傳回值賦值

    return err;
}

這裡仍然存在校驗問題,也就是說是否允許呼叫者獲取該task的CPU-time clock。對於行程,只允許呼叫者行程獲取自己的CPU-time clock,在多執行緒環境下,主執行緒(執行緒組leader)可以獲取整個行程的CPU-time clock資訊。對於per執行緒的操作,必須和呼叫者是同一個執行緒組,也就是同一個行程。

(a)獲取執行緒的clock資訊

static int cpu_clock_sample(const clockid_t which_clock, struct task_struct *p,
unsigned long long *sample)
{
switch (CPUCLOCK_WHICH(which_clock)) {
default:
return -EINVAL;
case CPUCLOCK_PROF:
*sample = prof_ticks(p);----獲取該task在使用者空間加上在kernel space的執行時間
break;
case CPUCLOCK_VIRT:
*sample = virt_ticks(p);----獲取該task在使用者空間的執行時間
break;
case CPUCLOCK_SCHED:
*sample = task_sched_runtime(p);----和排程器相關的cpu clock
break;
}
return 0;
}

計算行程或者執行緒在cpu上的執行時間是一個挺煩人的事,一方面想要精度高,另外一方面又不想計算量大。因此,實際上CPU-time clock有三種,CPUCLOCK_PROF和CPUCLOCK_VIRT這兩種都是比較粗略估計CPU執行時間的clock,它的工作原理就是在週期性tick中進行行程cpu time的統計,如果該tick是使用者態(timer中斷了使用者態程式的執行),那麼整個tick的時間都是該行程的使用者態執行時間。如果該tick是核心態,並且是使用者程式進行系統呼叫而陷入核心,那麼整個tick的時間都是該行程的系統態執行時間。

CPUCLOCK_SCHED clock和上面的方法不一樣,它的精度是納秒級別的,是在排程器上進行計算行程時間。具體的計算方法還是留到排程器文章中再描述吧。

(b)cpu_clock_sample_group函式概念類似,不過是統計一個行程上所有執行緒的時間而已。

3、CLOCK_PROCESS_CPUTIME_ID 型別的clock

struct k_clock process = {
.clock_getres    = process_cpu_clock_getres,
.clock_get    = process_cpu_clock_get,
.nsleep        = process_cpu_nsleep,
.nsleep_restart    = process_cpu_nsleep_restart,
};

process_cpu_clock_getres用來獲取時間精度,該函式實際是呼叫posix_cpu_clock_getres(PROCESS_CLOCK, tp)來完成的。process_cpu_clock_get用來獲取當前時間值,實際上是透過呼叫posix_cpu_clock_get完成。posix_cpu_clock_xxx函式在上一節中已經描述。

4、CLOCK_THREAD_CPUTIME_ID型別的clock

很簡單,大家自行學習吧。

五、動態分配clock

1、源由

某些硬體提供了計時的能力,可以實現成一個posix clock,同時,這些硬體又類似USB裝置那樣可以熱拔插,這也就意味著該posix clock不能靜態定義。此外,除了標準的timer和clock相關的操作,這些提供計時能力的硬體還需要一些其他的類似字元裝置介面的控制介面,在這樣的需求推動下,核心提供了dynamic posix clock。

2、dynamic posix clock

系統中的每一個dynamic posix clock用struct posix_clock來抽象,如下:

struct posix_clock {
struct posix_clock_operations ops;--------------(1)
struct cdev cdev;----------------------(2)
struct kref kref;
struct rw_semaphore rwsem;
bool zombie;------------------------(3)
void (*release)(struct posix_clock *clk);-------------(4)
};

(1)ops是該dynamic posix clock的操作函式集,分成兩個group,一個是timer(例如:timer_create、timer_delete等)以及clock操作相關(例如clock_gettime、clock_settime等),另外一個是普通字元裝置的操作函式(例如:open、read、write等)。

(2)該dynamic posix clock對應的cdev資料結構。在struct posix_clock_operations中有一個owner,其實在cdev中也有一個指向moudle的owner成員,看起來似乎是重覆定義了。同樣的疑問也存在與kref成員,因為在cdev中有kobject成員,kobject抽象了核心最基礎的物件類別,包括名字、取用計數等,因此,我覺得只要struct posix_clock包括了cdev成員,struct posix_clock_operations中的owner以及struct posix_clock中的kref應該沒有存在的必要了。

(3)zombie記錄了底層硬體的狀態,對於hotplug的外設,有可能硬體被拔除。rwsem用來保護該狀態資訊

(4)當reference count等於0的時候會呼叫release函式釋放dynamic posix clock佔用的資源。

3、註冊和登出

底層的有計時能力的硬體driver可以呼叫posix_clock_register和posix_clock_unregister來註冊或者登出一個posix clock,註冊程式碼如下:

int posix_clock_register(struct posix_clock *clk, dev_t devid)
{
int err;

    kref_init(&clk-;>kref);
init_rwsem(&clk-;>rwsem);

    cdev_init(&clk-;>cdev, &posix;_clock_file_operations);-----VFS介面的操作函式集合
clk->cdev.owner = clk->ops.owner;
err = cdev_add(&clk-;>cdev, devid, 1);

    return err;
}

VFS介面的操作函式集合都非常簡單,基本上都是struct posix_clock_operations上的字元裝置操作函式集合上。這樣,使用者空間的程式可以透過標準的檔案描述符進行裝置操作。

4、clock和timer介面

透過clock_xxx或者timer_xxx函式可以指定clock id,對於dynamic posix clock可以透過下麵的操作來生成一個dynamic posix clock ID:

#define FD_TO_CLOCKID(fd)    ((~(clockid_t) (fd) << 3) | CLOCKFD)

其中fd是透過裝置節點開啟的那個有計時能力的硬體。在核心態會透過clockid_to_kclock操作將clock id轉換成

static struct k_clock *clockid_to_kclock(const clockid_t id)
{
if (id < 0)
return (id & CLOCKFD_MASK) == CLOCKFD ?
&clock;_posix_dynamic : &clock;_posix_cpu;

……
}

clock_posix_dynamic可以將dynamic posix clock ID轉換成對應的posix_clock,然後呼叫struct posix_clock_operations上的time和clock相關的函式即可。

本文轉自蝸窩科技

已同步到看一看
贊(0)

分享創造快樂