linux内核之时间子系统
一、概要
???????? 時間管理在內核中占有非常重要的地位,內核中有大量的函數(shù)都是基于時間驅動的。有些函數(shù)需要周期執(zhí)行,而有些需要等待一個相對時間后才運行,此外內核還必須管理系統(tǒng)的運行時間以及當前日期和時間。內核必須在硬件的幫助下才能計算和管理時間。
???????? 硬件為內核提供了一個系統(tǒng)定時器用以計算流逝的時間,該時鐘在內核中可看成是一個電子時間資源。系統(tǒng)定時器以某種頻率自行觸發(fā)或射中時鐘中斷,當時鐘中斷發(fā)生時內核就通過一種特殊的中斷處理程序對其進行處理。該頻率可以通過編程預定(即節(jié)拍率),連續(xù)兩次時鐘中斷的間隔時間稱為節(jié)拍(tick)。利用時間中斷周期執(zhí)行的主要工作:
A.更新系統(tǒng)運行時間
B.更新墻上時間
C.在smp系統(tǒng)上,均衡調度程序中各處理器上的運行隊里
D.檢查當前進程是否用盡了自己的時間片
E.運行超時的動態(tài)定時器
F.更新資源消耗和處理器時間的統(tǒng)計值
?
1、節(jié)拍率:HZ
???????? 系統(tǒng)定時器頻率是通過靜態(tài)預處理定義的,就是HZ,在系統(tǒng)啟動時按照HZ值對硬件進行設置。不同體系結構,HZ不同,但大部分都是100即一個tick是10ms。
???????? 一個合適的HZ是相當重要的,下面分析了高HZ的優(yōu)勢和劣勢:
優(yōu)勢:
A.內核定時器能夠以更高的頻度和更高的準確度運行
B.依賴定時值的系統(tǒng)調用(poll和select)能夠以更高的精度運行
C.對諸如資源消耗和系統(tǒng)運行時間等的測量會有更精細的解析度
D.提高進程搶占的準確度
劣勢:
A.中斷頻率越高,切換頻繁,系統(tǒng)負擔越重
B.中斷處理程序占用的CPU時間越多
C.更頻繁的打亂處理器高速緩存并增加耗電
2、jiffies
???????? 全局變量jiffies用來記錄自系統(tǒng)啟動以來產生的節(jié)拍的總數(shù)。內核給jiffies賦了一個特殊的初值,引起盡早溢出捕捉bug,每次時鐘中斷處理程序就會增加該變量的值。
???????? 在include/linux/jiffies.h文件中定義了該變量:
extern unsigned long volatile __jiffy_datajiffies;
jiffies為無符號長整形,在32位體系結構上是32位,在64位體系結構上是64位。
???????? 當jiffies變量的值超過它的最大存放范圍后就會發(fā)生溢出,回繞到0。因此,內核提供了四個宏來幫助比較節(jié)拍計數(shù),能正確地處理節(jié)拍計數(shù)回繞情況:
time_after(a, b) time_before(a, b)time_after_eq(a, b) time_before_eq(a, b)
3、硬件時鐘和定時器
???????? 體系結構提供了兩種設備進行計時:實時時鐘和系統(tǒng)定時器。
實時時鐘:
???????? 用來持久存放系統(tǒng)時間的設備,即便系統(tǒng)關閉后也可以靠主板上的微型電池提供的電力保持系統(tǒng)的計時。當系統(tǒng)啟動時,內核通過讀取RTC來初始化墻上時間,該時間存放在xtime變量中,然后定期同步兩者時間保持一致,重啟時可以得到一個相對準確的時間。
系統(tǒng)定時器:
???????? 內核定時機制中最為重要的角色,提供一種周期性觸發(fā)中斷機制。相應的中斷處理程序主要完成以下工作:
A.更新jiffies
B.更新墻上時間
C.更新資源消耗的統(tǒng)計值,比如當前進程所消耗的系統(tǒng)時間和用戶時間
D.執(zhí)行到期的動態(tài)定時器
E.執(zhí)行scheduler_tick()函數(shù)
F.計算平均負載值
4、實際時間
???????? 當前實際時間定義在文件kernel/time/timekeeping.c中:
static struct timekeeper timekeeper
struct timekeeper {
…
u64????????xtime_sec;
u64????????xtime_nsec;
…
}
xtime_sec以秒為單位,存放著自1970年1月1日(UTC)以來經過的時間,該時間點被稱為紀元,多數(shù)Unix系統(tǒng)的墻上時間都是基于該紀元而言的。xtime_nsec上一秒開始經過的ns數(shù)。
???????? 通過gettimeofday和settimeofday來獲取和設置當前時間,設置時間需要具有CAP_SYS_TIME權能。
5、動態(tài)定時器
???????? 它是管理內核流逝的時間的基礎,使用簡單。只需要執(zhí)行一些初始化工作,設置一個超時時間,指定超時發(fā)生后執(zhí)行的函數(shù),然后激活定時器就可以了。
???????? 定時器由結構timer_list表示,定義在文件<include/linux/timer.h >
struct timer_list {??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
???/*???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
????* All fields that change during normal runtime grouped to the???????????????????????????????????????????????????????????????????????????????
????* same cacheline????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
????*/??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
???struct list_head entry;? ?//定時器鏈表入口????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
???unsigned long expires;? ?//以jiffies為單位的定時值?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
???struct tvec_base *base;? //定時器內部值,用戶不使用? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
???void (*function)(unsigned long); //定時器處理函數(shù)????????????????????????????????????????????????????????????????????????????????????????????????????????????
???unsigned long data;????????? //傳遞給處理函數(shù)的長整型參數(shù)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
intslack;????????
???????? …
}
???????? 使用方法如下:
創(chuàng)建定時器:structtimer_list my_timer
初始化定時器:init_timer(&my_timer)
定時值初始化:
??????????????????????????? my_timer.expires= jiffies + delay
??????????????????????????? my_timer.data= 0
??????????????????????????? my_timer.function= my_function( void my_function(unsigned long data))
激活定時器:add_timer(&my_timer)
修改已經激活的定時器超時時間:mod_timer(&my_timer,jiffies+new_delay)
在定時器超市前停止定時器:del_timer(&my_timer)或 del_timer_sync(&my_timer)
? ?????? 定時器作為軟中斷在下半部上下文中執(zhí)行,時鐘中斷處理程序會通過raise_softirq(TIMER_SOFTIRQ)喚醒定時器軟中斷,從而在當前處理器上運行所有的超時定時器。雖然所有定時器都以鏈表形式存放在一起,但是為了提高搜索效率,內核將定時器按照他們的超時時間劃分成五組,當定時器超時時間接近時,定時器將隨組一起下移。
6、延遲執(zhí)行
???????? 內核代碼(尤其是驅動程序)除了使用定時器或下半部機制以外,還需要其他方法來推遲執(zhí)行任務。內核提供了許多延遲方法處理各種延遲要求:
A.忙等待
???????? 該方法想要延遲的時間是節(jié)拍的整數(shù)倍,或者精確率要求不高時使用
unsinged long timeout = jiffies + 10
while (time_before(jiffies, timeout))
???????? ;
或者
while(time_before(jiffies, delay))
???????? cond_resched();
B.短延遲:
???????? voidudelay(unsigned long usecs)
???????? voidndelay(unsigned long nsecs)
???????? voidmdelay(unsigned long msecs)
/proc/cpuinfo的BogoMIPS主要被udelay()和mdelay()函數(shù)使用,記錄處理器在給定時間內忙循環(huán)執(zhí)行的次數(shù)。
C.schedule_timeout()
???????? 該方法會讓需要延遲執(zhí)行的任務睡眠到指定的延遲時間耗盡后再重新運行。唯一的參數(shù)是延遲的相對時間,單位是jiffies。需要注意一點:
該函數(shù)需要調用調度程序,所以調用它的代碼必須保證能夠睡眠。換句話說,調用代碼必須處于進程上下文中,并且不能持有鎖。
二、linux時間框架
1、框架
隨著技術發(fā)展,出現(xiàn)了下面兩種新的需求:
(1)嵌入式設備需要較好的電源管理策略。傳統(tǒng)的linux會有一個周期性的時鐘,即便是系統(tǒng)無事可做的時候也要醒來,這樣導致系統(tǒng)不斷的從低功耗(idle)狀態(tài)進入高功耗的狀態(tài)。這樣的設計不符合電源管理的需求。
(2)多媒體的應用程序需要非常精確的timer,例如為了避免視頻的跳幀、音頻回放中的跳動,這些需要系統(tǒng)提供足夠精度的timer。
???????? 因此,內核時間子系統(tǒng)也進行了框架調整,引進了高精度timer。和低精度timer不同,高精度timer使用了人類的最直觀的時間單位ns(低精度timer使用的tick是和內核配置相關,不夠直接)。本質上linuxkernel提供了高精度timer之后,其實不必提供低精度timer了,不過由于低精度timer存在了很長的歷史,并且滲入到內核各個部分,如果去掉容易引起linuxkernel穩(wěn)定性和健壯性的問題,因此kernel保持兩種并存。其示意圖如下:
???????? 在smp情況下,driver硬件定時器劃分成了兩部分:一個提供給clocksource模塊使用,一個提供給clockevent事件使用。而clockevent又簡單化為兩個小部分:一個是每個CPU自己的定時器,一個是全局定時器;每個CPU定時器管理本CPU的任務運行情況、資源統(tǒng)計等,第一個啟動CPU(一般是CPU0)還會更新tick和墻上時鐘,而globaltimer則主要用于低功耗模式下CPU進入睡眠時喚醒所有CPU。
???????? Kernel抽象了底層驅動,劃分為clockevent和clocksource連個模塊,驅動加載時會調用兩個模塊接口進行注冊。clocksource的精度就是定時器時鐘頻率的精度(ns級別),可以認為是一個timeline,而clockevent則是這個timeline上的特定時間點,產生中斷后調用相應的callback函數(shù)進行事件處理。
???????? tickdevice layer 基于clockevent設備進行工作:每個CPU都有自己唯一的tickdevice,管理自己的調度,進程統(tǒng)計等。Tickdevice可以工作在兩種模式:periodic 和 one shot mode。有多少CPU就有多少個tickdevice稱之為local tickdevice,在所有的local tickdevice中會有一個被選為globaltick device(一般是CPU0的tick device),該device負責維護整個系統(tǒng)的jiffies,更新wall clock,計算全局負荷什么的。
???????? 高精度hrtimer需要高精度的clockevent,工作在one shotmode的tick device提供高精度的clockevent。雖然有了高精度hrtimer的出現(xiàn),內核并沒有拋棄老的低精度timer機制。當系統(tǒng)處于高精度timer的時候,系統(tǒng)會setup一個特別的高精度hrtimer(sched_timer),該高精度timer會周期性的觸發(fā),從而模擬傳統(tǒng)的periodictick,推動傳統(tǒng)低精度timer的運轉(代碼沒有弄明白sched_timer?)。
2、內核配置
(1)GENERIC_CLOCKEVENTS和GENERIC_CLOCKEVENTS_BUILD:代表使用新的時間子系統(tǒng)架構,默認就是配置好的
(2)新時間子系統(tǒng)框架下,Timerssubsystem配置選項主要和tick已經是否支持高精度hrtimer有關。Tick有2種配置(二選一):
CONFIG_HZ_PERIODIC:無論何時都啟用周期性的tick,即便是在系統(tǒng)idle的時候。
CONFIG_NO_HZ_IDLE:該選項默認會打開 CONFIG_TICK_ONESHOT 和CONFIG_NO_HZ_COMMON,在系統(tǒng)idle的時候,停掉周期性tick。
CONFIG_HIGH_RES_TIMERS:支持高精度hrtimer
配置了高精度hrtimer或NO_HZ_COMMOM就一定配置CONFIG_TICK_ONESHOT,表示系統(tǒng)支持one-shot類型的tick_device。
???????? 因此,在新時間子系統(tǒng)下有4種配置:
A.低精度timer和周期tick
B.低精度timer和dynamic tick(tickless idle)(當前系統(tǒng)使用情況)
C.高精度hrtimer和周期tick
D.高精度hrtimer和dynamic tick(tickless idle)
3、四種配置(轉載自附錄網(wǎng)站)
(1)低精度timer + 周期tick
我們首先看周期性tick的實現(xiàn)。起始點一定是底層的clocksource chip driver,該driver會調用接口clockevents_register_device向clock event注冊。一旦增加了一個clockevent device,需要通知上層的tickdevice layer,有可能新注冊的這個device更好、更適合某個tick device。要是這個clock eventdevice被某個tickdevice收留了(要么該tickdevice之前沒有匹配的clockevent device,要么新的clockevent device更適合該tickdevice),那么就啟動對該tickdevice的配置(參考tick_setup_device)。根據(jù)當前系統(tǒng)的配置情況(周期性tick),會調用tick_setup_periodic函數(shù),這時候,該tick device對應的clock event device的clock event handler被設置為tick_handle_periodic。底層硬件會周期性的產生中斷,從而會周期性的調用tick_handle_periodic從而驅動整個系統(tǒng)的運轉。需要注意的是:即便是配置了CONFIG_NO_HZ和CONFIG_TICK_ONESHOT,系統(tǒng)中沒有提供one shot的clock event device,這種情況下,整個系統(tǒng)仍然是運行在周期tick的模式下。
下面來到低精度timer模塊了,其實即便沒有使能高精度timer,內核也會把高精度timer模塊的代碼編譯進kernel的image中,這一點可以從Makefile文件中看出。在這種構架下,各個內核模塊也可以調用linuxkernel中的高精度timer模塊的接口函數(shù)來實現(xiàn)高精度timer,但是,這時候高精度timer模塊是運行在低精度的模式,也就是說這些hrtimer雖然是按照高精度timer的紅黑樹進行組織,但是系統(tǒng)只是在每一周期性tick到來的時候調用hrtimer_run_queues函數(shù),來檢查是否有expire的hrtimer。毫無疑問,這里的高精度timer也就是沒有意義了。
由于存在周期性tick,低精度timer的運作毫無壓力,和過去一樣。
(2)低精度timer + Dynamic Tick
系統(tǒng)開始的時候并不是直接進入Dynamictick mode的,而是經歷一個切換過程。開始的時候,系統(tǒng)運行在周期tick的模式下,各個cpu對應的tick device的(clock event device的)event handler是tick_handle_periodic。在timer的軟中斷上下文中,會調用tick_check_oneshot_change進行是否切換到one shot模式的檢查,如果系統(tǒng)中有支持one-shot的clock event device,并且沒有配置高精度timer的話,那么就會發(fā)生tick mode的切換(調用tick_nohz_switch_to_nohz),這時候,tick device會切換到one shot模式,而event handler被設置為tick_nohz_handler。由于這時候的clock eventdevice工作在oneshot模式,因此當系統(tǒng)正常運行的時候,在eventhandler中每次都要reprogramclock event,以便正常產生tick。當cpu運行idle進程的時候,clock eventdevice不再reprogram產生下次的tick信號,這樣,整個系統(tǒng)的周期性的tick就停下來。
(3)高精度timer + Dynamic Tick
同樣的,系統(tǒng)開始的時候并不是直接進入Dynamictick mode的,而是經歷一個切換過程。系統(tǒng)開始的時候是運行在周期tick的模式下,event handler是tick_handle_periodic。在周期tick的軟中斷上下文中(參考run_timer_softirq),如果滿足條件,會調用hrtimer_switch_to_hres將hrtimer從低精度模式切換到高精度模式上。這時候,系統(tǒng)會有下面的動作:
A.Tickdevice的clockevent設備切換到oneshotmode(參考tick_init_highres函數(shù))
B.Tickdevice的clockevent設備的eventhandler會更新為hrtimer_interrupt(參考tick_init_highres函數(shù))
C.設定schedtimer(也就是模擬周期tick那個高精度timer,參考tick_setup_sched_timer函數(shù))
這樣,當下一次tick到來的時候,系統(tǒng)會調用hrtimer_interrupt來處理這個tick(該tick是通過sched timer產生的)。
在Dynamictick的模式下,各個cpu的tick device工作在one shot模式,該tick device對應的clock event設備也工作在one shot的模式,這時候,硬件Timer的中斷不會周期性的產生,但是linuxkernel中很多的模塊是依賴于周期性的tick的,因此,在這種情況下,系統(tǒng)使用hrtime模擬了一個周期性的tick。在切換到dynamic tick模式的時候會初始化這個高精度timer,該高精度timer的回調函數(shù)是tick_sched_timer。這個函數(shù)執(zhí)行的函數(shù)類似周期性tick中event handler執(zhí)行的內容。不過在最后會reprogram該高精度timer,以便可以周期性的產生clockevent。當系統(tǒng)進入idle的時候,就會stop這個高精度timer,這樣,當沒有用戶事件的時候,CPU可以持續(xù)在idle狀態(tài),從而減少功耗。
(4)高精度timer + 周期性Tick
這種配置不多見,多半是由于硬件無法支持oneshot的clockevent device,這種情況下,整個系統(tǒng)仍然是運行在周期tick的模式下。
三、用戶接口
1、系統(tǒng)時間相關服務
(1)秒級函數(shù):time和stime
#include <time.h>
time_t time(time_t *t); //獲取時間秒
int stime(time_t *t); ?//設置時間秒
對應的系統(tǒng)調用為:sys_time和sys_stime。time函數(shù)返回當前點到linuxepoch的秒數(shù),stime設定當前時間點到linuxepoch的秒數(shù)。
???????? 與上面函數(shù)配套的還有一系列時間點與linuxepoch轉換函數(shù):mktime,localtime_r。
(2)微秒級函數(shù):gettimeofday和settimeofday
#include <sys/time.h>
int gettimeofday(struct timeval *tv, structtimezone *tz);
int settimeofday(const struct timeval *tv,const struct timezone *tz);
struct timeval {
time_t????? tv_sec;???? /* seconds */
suseconds_ttv_usec;??? /* microseconds */
};
struct timezone {
inttz_minuteswest;???? /* minutes west ofGreenwich */
inttz_dsttime;????? ???/* type of DST correction */
};
對應的系統(tǒng)調用:sys_gettimeofday和sys_settimeday。Gettimeofday獲取linux epoch到當前時間點的秒數(shù)以及微秒數(shù);settimeofday則設定從linuxepoch到當前時間點的秒數(shù)以及微秒數(shù)。
值得一提的是:這些系統(tǒng)調用在新的POSIX標準中接口被clock_gettime和clock_settime取代。
(3)納秒級別的時間函數(shù):clock_gettime和clock_settime
#include <time.h>
int clock_getres(clockid_t clk_id, structtimespec *res); //獲取clock_id的系統(tǒng)時鐘精度
int clock_gettime(clockid_t clk_id, structtimespec *tp);
int clock_settime(clockid_t clk_id, conststruct timespec *tp);
struct timespec {
time_t?? tv_sec;??????? /* seconds */
long???? tv_nsec;?????? /* nanoseconds */
};
clk_id識別systemclock的ID,定義如下:
CLOCK_REALTIME:真實的墻上時鐘,前面函數(shù)就是從獲取該ID的值
CLOCK_MONOTONIC:該時鐘是單調遞增的,也是真實的墻上時鐘只是起始點不一定是linuxepoch,一般會把系統(tǒng)啟動的時間點設定為基準點。除了NTP和adjtime對該時鐘進行調整外,其他任何接口不允許設定該時鐘,保證該時鐘的單調性。可以了解系統(tǒng)啟動時間。
CLOCK_MONOTONIC_RAW:具備CLOCK_MONOTONIC的特性,但它不受NTP和adjtime影響,是完全基于本地晶振的時鐘。一般程序員不大用。
CLOCK_BOOTTIME:類似CLOCK_MONOTONIC,但是系統(tǒng)suspend時依然增加。
CLOCK_PROCESS_CPUTIME_ID:每個CPU的高精度進程定時器,clock_getcpuclockid獲取進程的clock_id
CLOCK_THREAD_CPUTIME_ID:線程的CPU時間,pthread_getcpuclockid獲取線程的clock_id。
(4)系統(tǒng)時鐘調整
???????? 上面設定系統(tǒng)時間是一個比較粗暴的做法,一旦修改了系統(tǒng)時間,系統(tǒng)中很多以來絕對時間的進程會有各種奇怪的行為。所以系統(tǒng)提供了時間同步的接口函數(shù),可以讓外部的精準計時服務器不斷的修正系統(tǒng)時鐘。
A.adjtime
int adjtime(const struct timeval *delta,struct timeval *olddelta);
struct timeval {
time_t????? tv_sec;???? /* seconds */
suseconds_ttv_usec;?? ?/* microseconds */
};
???????? 該函數(shù)可以根據(jù)delta參數(shù)緩慢的修正系統(tǒng)時鐘(CLOCK_REALTIME那個)。olddelta返回上一次調整中尚未完成的delta。
B.adjtimex
#include <sys/timex.h>
int adjtimex(struct timex *buf);
struct timex {
intmodes;?????????? /* mode selector */
longoffset;???????? /* time offset (usec) */
longfreq;?????????? /* frequency offset(scaled ppm) */
longmaxerror;?????? /* maximum error (usec)*/
longesterror;?????? /* estimated error (usec)*/
intstatus;????????? /* clock command/status*/
longconstant;?????? /* pll time constant */
longprecision;????? /* clock precision (usec)(read-only) */
longtolerance;????? /* clock frequencytolerance (ppm)
??????????????????????????????????????(read-only) */
structtimeval time; /* current time (read-only) */
longtick;?????????? /* usecs between clockticks */
};
???????? 該函數(shù)用來顯示或這修改linux內核的時間變量的工具,提供了對與內核時間變量直接訪問功能,可以實現(xiàn)對于系統(tǒng)時間的飄逸進行修正。任何用戶都可以使用它查看,但是只有root用戶才可以更改這些參數(shù)。
2、進程睡眠
(1)秒級函數(shù):sleep
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
???????? 該函數(shù)會導致當前進程sleepseconds之后(基于CLOCK_REALTIME)返回繼續(xù)執(zhí)行程序。返回值說明了進程沒有進入睡眠的時間。
(2)微秒級別函數(shù):usleep
#include <unistd.h>
int usleep(useconds_t usec);
???????? 該函數(shù)功能和上面一樣,不過返回值定義不同。0:表示執(zhí)行成功,-1:執(zhí)行失敗,錯誤碼在errno中。
(3)納秒級別函數(shù):nanosleep
#include <time.h>
int nanosleep(const struct timespec *req,struct timespec *rem);
struct timespec {
time_ttv_sec;??????? /* seconds */
long?? tv_nsec;?????? /* nanoseconds */
};
該函數(shù)取代了usleep函數(shù),req中設定你要sleep的秒以及納秒值,rem表示還有多少時間沒睡完。返回0表示成功,返回-1說明失敗。
sleep/usleep/nanosleep的系統(tǒng)都是通過kernel的sys_nanosleep系統(tǒng)調用實現(xiàn)(底層基于hrtimer)。
(4)更高級的sleep函數(shù):clock_nanosleep
#include <time.h>
int clock_nanosleep(clockid_t clock_id, intflags,
?????????????????????????? const structtimespec *request,
?????????????????????????? struct timespec*remain);
clock_id說明該函數(shù)不僅能基于real_timeclock睡眠,還可以基于其他的系統(tǒng)時鐘睡眠。flag等0或1,分別指明request參數(shù)設定的時間值是相對時間還是絕對時間。
3、timer相關的服務
(1)alarm函數(shù)
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
???????? 該函數(shù)在指定秒數(shù)(基于CLOCK_REALTIME)的時間過去后,向該進程發(fā)送SIGALRM信號。調用該接口的程序需要設定signalhandler。
四、代碼解析
第二章節(jié)講述內核配置有兩種主要模式:periodic和 one-shot,下面代碼分析主要根據(jù)實際使用的低精度tick和dynamic tick (tickles tick)即使用one-shot配置進行講解(Linux-3.10.y)。
1、數(shù)據(jù)結構
(1)clocksource
內核使用structclocksource數(shù)據(jù)結構記錄時鐘源所有信息,主要作為系統(tǒng)時間的基準,當有多個時鐘源時選擇最優(yōu)那個,沒有時鐘源時默認使用基于jiffies的時鐘clocksource_jiffies。內核通過一個鏈表clocksource_list管理所有注冊的時鐘源,每個時鐘源定義了一個單調增加的計數(shù)器并以ns為單位。
???????? structclocksource結構體詳細如下(include/linux/clocksource.h):
struct clocksource {
???cycle_t (*read)(struct clocksource *cs);?//讀取指定CS的cycle值(定時器當前計數(shù)值)
???cycle_t cycle_last;?? //保存最近一次read的cycle值(其中一個重要作用翻轉)
cycle_tmask; //counter是32位還是64位
//公式:ns =(cycles/F) * NSEC_PER_SEC = (cycle* mult) >> shift
???u32 mult; //cycle轉化為ns的乘數(shù)
???u32 shift; //cycle轉化為ns的除數(shù),采用移位的方式
???u64 max_idle_ns; //該時鐘允許的最大空閑時間(沒搞明白如何用)
???u32 maxadj;???? //最大調整值與mult相關
#ifdef CONFIG_ARCH_CLOCKSOURCE_DATA //未用
???struct arch_clocksource_data archdata;
#endif
???const char *name; //時鐘源名字
???struct list_head list; //注冊時鐘源鏈表頭
intrating; //時鐘源精度值,
1--99: 不適合于用作實際的時鐘源,只用于啟動過程或用于測試;
100--199:基本可用,可用作真實的時鐘源,但不推薦;
200--299:精度較好,可用作真實的時鐘源;
300--399:很好,精確的時鐘源;
400--499:理想的時鐘源,如有可能就必須選擇它作為時鐘源;
??? int(*enable)(struct clocksource *cs); //使能時鐘源
???void (*disable)(struct clocksource *cs); //禁止時鐘源
???unsigned long flags; //時鐘源屬性,CLOCK_SOURCE_IS_CONTINUOUS連續(xù)時鐘
???void (*suspend)(struct clocksource *cs); //掛起時鐘源
???void (*resume)(struct clocksource *cs); //恢復時鐘源
#ifdef CONFIG_CLOCKSOURCE_WATCHDOG?? //未用
???/* Watchdog related data, used by the framework */
???struct list_head wd_list;
???cycle_t cs_last;
???cycle_t wd_last;
#endif
}____cacheline_aligned;
(2)clockevent
內核使用struct clock_event_device數(shù)據(jù)結構記錄時鐘的事件信息,包括硬件時鐘中斷發(fā)生時要執(zhí)行的那些操作。提供了對周期性事件和單觸發(fā)事件的支持。還提供了高精度定時器和動態(tài)定時器的支持。內核通過一個clockevent_devices管理所有注冊的clock event設備。
???????? 該結構體在頭文件include/linux/clockchips.h中定義,詳細定義如下:
struct clock_event_device {
???void ???????? (*event_handler)(structclock_event_device *); //事件處理函數(shù),主要有三種:peridioc\one-shot\broadcast
???int??? (*set_next_event)(unsignedlong evt, struct clock_event_device *); //設置下次觸發(fā)事件基于clocksource的cycles
???int??? (*set_next_ktime)(ktime_texpires, struct clock_event_device *); //設置下次觸發(fā)事件基于ktime(用得比較少)
???ktime_t??? next_event;
???u64? max_delta_ns; //可設置的最大時間差
???u64? min_delta_ns; //可設置的最小時間差
???u32? mult; //和clocksource一樣
???u32? shift; //和clocksource一樣
???enum clock_event_mode?? mode; //clockevent工作模式,見下面
???unsigned int??????? features; //clockevent設備特征,見下面
?? ?unsigned long?????? retries;
???void? (*broadcast)(const structcpumask *mask); //廣播所有CPU函數(shù)
???void? (*set_mode)(enumclock_event_mode mode, struct clock_event_device *); //設置模式
???void? (*suspend)(structclock_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;
???const struct cpumask????? *cpumask;//CPU掩碼,判斷是否屬于某一個CPU,或廣播支持的CPU
???struct list_head??????? list;
} ____cacheline_aligned;
???????? clockevent設備工作模式:
enum clock_event_mode {
???CLOCK_EVT_MODE_UNUSED = 0,??????????
???CLOCK_EVT_MODE_SHUTDOWN,?????????? //關閉模式
???CLOCK_EVT_MODE_PERIODIC,?????????????? //周期性模式
???CLOCK_EVT_MODE_ONESHOT,????? //單次模式
???CLOCK_EVT_MODE_RESUME,??????? //恢復模式
};
???????? clockevent設備特征:
#define CLOCK_EVT_FEAT_PERIODIC???? 0x000001 ???????? //可以產生周期觸發(fā)事件特征
#define CLOCK_EVT_FEAT_ONESHOT???? 0x000002????????? //可以產生單觸發(fā)事件特征
#define CLOCK_EVT_FEAT_KTIME?????????? 0x000004 ?????? //產生事件的事件基準ktime
//X86下使用,進入省電情況
#define CLOCK_EVT_FEAT_C3STOP???????? 0x000008 ?????? //clocksource停止,需要廣播事件支持,本ARM平臺也使用了該選項
#define CLOCK_EVT_FEAT_DUMMY?????? 0x000010????????? // Local APIC timer使用該選項
(3)tick_device
???????? struct tick_device只是對struct clock_event_device的一個封裝,加入了運行模式變量,支持PERIODIC和ONESHOT兩種模式。
struct tick_device {
???struct clock_event_device *evtdev;
???enum tick_device_mode mode;
};
enum tick_device_mode {
???TICKDEV_MODE_PERIODIC,
???TICKDEV_MODE_ONESHOT,
};
2、內核初始化
???????? 在內核啟動函數(shù)start_kernel里對時間系統(tǒng)進行了初始化
(1)tick_init
???????? 該函數(shù)初始化tick控制。向clockevents_chain通知鏈中添加一個tick通知分發(fā)器tick_notifier(分發(fā)回調函數(shù):tick_notify)。在底層驅動注冊設備時,CLOCK_EVT_NOTIFY_ADD消息就是添加了一個新的clockevent設備。
初始化tick broadcast掩碼,如果配置了CONFIG_TICK_ONESHOT相關掩碼也要初始化。
(2)init_timers
初始化本CPU上的低精度定時器相關的數(shù)據(jù)結構,將通知分發(fā)器timers_nb添加到cpu_chain通知鏈;初始化定時器軟中斷open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
(3)hrtimers_init
初始化本CPU上的高精度精度定時器相關的數(shù)據(jù)結構,將通知分發(fā)器hrtimers_nb添加到cpu_chain通知鏈;如果開啟高精度定時器宏,則初始化高精度定時器軟中斷open_softirq(HRTIMER_SOFTIRQ, run_hrtimer_softirq)(本平臺為使用)。
(4)timekeeping_init
初始化時鐘源clocksource及timekeeping模塊時間初始值,如果平臺沒有更好的時鐘源,系統(tǒng)使用jiffies作為時鐘源。
clock = clocksource_default_clock();
struct clocksource * __init __weakclocksource_default_clock(void)
{
???return &clocksource_jiffies;
}
struct clocksource clocksource_jiffies = {
???.name?????? = "jiffies",
???.rating???? = 1, /* lowest validrating*/
???.read?????? = jiffies_read,
???.mask?????? = 0xffffffff,/*32bits*/
???.mult?????? = NSEC_PER_JIFFY<< JIFFIES_SHIFT, /* details above */
???.shift????? = JIFFIES_SHIFT,
};
(5)time_init
前面函數(shù)時內核通用架構,該函數(shù)為硬件時鐘初始化平臺相關,一般由各個平臺自己實現(xiàn),細節(jié)見下節(jié)。
void __init time_init(void)
{??
???if (machine_desc->init_time)
? ??????machine_desc->init_time(); (=hi3536_timer_init)
???else
???????clocksource_of_init();
???sched_clock_postinit();
}
?
3、硬件時鐘初始化
(1)平臺注冊
MACHINE_START(HI3536, "hi3536")
.atag_offset? = 0x100,
???.map_io?????? = hi3536_map_io,
???.init_early?? = hi3536_init_early,
???.init_irq???? =hi3536_gic_init_irq,
#ifdef CONFIG_HI3536_SYSCNT
???.init_time??? = arch_timer_init,
#else
??? .init_time??? =hi3536_timer_init,
#endif?
???.init_machine = hi3536_init,
???.smp????????? =smp_ops(hi3536_smp_ops),
???.reserve????? = hi3536_reserve,
???.restart????? = hi3536_restart,
???MACHINE_END
???????? 上一節(jié)machine_desc的具體實現(xiàn)即為該宏定義,init_time就是hi3536_timer_init,其具體內容見下面。
?
(2)平臺定時器初始化
void __init hi3536_timer_init(void)
{
/* 設置所有定時器的工作時鐘(未初始化,默認3MHZ)
???????? Hi3536有time0~910個時鐘
???????? 根據(jù)配置,所有定時器都配置成了總線時鐘125Mhz(8ns)
*/
???writel(readl((const volatile void *)IO_ADDRESS(REG_BASE_SCTL)) |
???????(1 << 16) | (1 << 18),
???????(volatile void *)IO_ADDRESS(REG_BASE_SCTL));???
#ifdef CONFIG_SP804_LOCAL_TIMER
???hi3536_local_timer_init(); //每個CPU使用一個定時器作為local timer,該平臺有4核CPU0~CPU3分別對應:timer4~timer7,注冊為clockevent設備
#endif
???hi3536_clocksource_init((void *)TIMER(0)->addr,
???????TIMER(0)->name); //timer0作為clocksource設備
???sp804_clockevents_init((void *)TIMER(1)->addr,
???????TIMER(1)->irq.irq, TIMER(1)->name); //timer1作為clockevent設備的globaltimer
}
???????? 從上面看,hi3536總共支持10個timer,實際使用了6個,timer0/timer1,timer4~timer7,其他保留。其注冊的順序如下:timer0(clocksource)—> timer1(clockevent global timer) —> timer4(clockevent cpu0 local timer)—> time5~7(clockevent cpu1~3 local timer)。每類定時器的初始化,見下面細節(jié)分析。
(3)clocksource初始化
???????? 初始化函數(shù)源碼如下:
static void __inithi3536_clocksource_init(void __iomem *base, const char *name)?????????????????????????????????????????????????????????????????
{????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
???long rate = sp804_get_clock_rate(name); ?//獲取定時器時鐘62.5MHz? ????
???struct clocksource *clksrc = &hi3536_clocksource.clksrc;???
???
???if (rate < 0)????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
???????return;??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
???? ???
???clksrc->name?? = name;? //name=timer0 ???????
???clksrc->rating = 200; //時鐘源精度值????????????
???clksrc->read?? =hi3536_clocksource_read;? ?//獲取計數(shù)值,系統(tǒng)主要調用該接口轉化為系統(tǒng)時間? ???????
???clksrc->mask?? =CLOCKSOURCE_MASK(32),? //計數(shù)值32位
???clksrc->flags? = CLOCK_SOURCE_IS_CONTINUOUS,?//持續(xù)的時鐘源
???clksrc->resume = hi3536_clocksource_resume,? ????
???hi3536_clocksource.base = base;? ?????
???hi3536_clocksource_start(base);? //初始化寄存器
??? clocksource_register_hz(clksrc, rate); ?//計算出mult和shift,為系統(tǒng)選擇更好的時鐘源??
???setup_sched_clock(hi3536_sched_clock_read, 32, rate); //通用sched_clock模塊,這個模塊主要是提供一個sched_clock的接口函數(shù),獲取當前時間點和系統(tǒng)啟動之間的納秒值。
}
(4)per CPU定時器初始化
???????? 每CPU定時器初始化函數(shù):
static void __inithi3536_local_timer_init(void)
{??
? ??unsigned int cpu = 0;
???unsigned int ncores = num_possible_cpus(); //獲取CPU個數(shù)
? ? local_timer_rate = sp804_get_clock_rate("sp804"); //獲取定時器時鐘
? ? for (cpu = 0; cpu < ncores; cpu++) { //為每個CPU分配各自的定時器
???????struct hi_timer_t *cpu_timer = GET_SMP_TIMER(cpu);
? ? ? ? cpu_timer->irq.handler = sp804_timer_isr; //中斷處理函數(shù)
???????cpu_timer->irq.dev_id = (void *)cpu_timer; //定時器分別是timer0~3
???????setup_irq(cpu_timer->irq.irq, &cpu_timer->irq); //注冊中斷號
???????disable_irq(cpu_timer->irq.irq); //關閉中斷
?? ?}
local_timer_register(&hi3536_timer_tick_ops);//注冊定時器操作函數(shù)
?/* 以上只是為每個CPU分配了定時器,并沒有注冊每個cpu的clockevent,啟動CPU(CPU0)在稍后注冊,而其他次CPU則直到kernel_init啟動它們后才會注冊 */
}
//定時器注冊clockevent操作函數(shù)
static struct local_timer_opshi3536_timer_tick_ops __cpuinitdata = {
???.setup? =hi3536_local_timer_setup,
???.stop?? = hi3536_local_timer_stop,
};
static int __cpuinithi3536_local_timer_setup(struct clock_event_device *evt)
{
???unsigned int cpu = smp_processor_id();
???struct hi_timer_t *timer = GET_SMP_TIMER(cpu);
???struct irqaction *irq = &timer->irq;
? ? evt->name = timer->name;
???evt->irq? = irq->irq;
???evt->features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT
????????????????????????? |CLOCK_EVT_FEAT_C3STOP;
? ? evt->set_mode = sp804_set_mode;
???evt->set_next_event = sp804_set_next_event;
???evt->rating = 350;
? ? timer->priv = (void *)evt;
? ? clockevents_config_and_register(evt, local_timer_rate, 0xf, 0xffffffff);//注冊clockevent
???irq_set_affinity(evt->irq, evt->cpumask);
???enable_irq(evt->irq);
? ? return 0;
}
//中斷處理函數(shù)
static irqreturn_t sp804_timer_isr(int irq,void *dev_id)
{
???struct hi_timer_t *timer = (struct hi_timer_t *)dev_id;
???unsigned int clkevt_base = timer->addr;
???struct clock_event_device *evt
???????= (struct clock_event_device *)timer->priv;
? ? /* clear the interrupt */
???writel(1, IOMEM(clkevt_base + TIMER_INTCLR));
? ? evt->event_handler(evt);? //periodic 和 one-shot模式處理函數(shù)不一樣,見4小節(jié)細節(jié)。
? ? return IRQ_HANDLED;
}
(5)per CPU注冊clockevent
???????? 上面只是初始化為每個CPU分配一個定時器,并沒有將定時器注冊到clockevent。而注冊則是在各個CPU啟動后,主CPU和次CPU注冊是分開的,具體如下:
A.主CPU(cpu0)
???????? 注冊過程:kernel_init—> kernel_init_freeable —> smp_prepare_cpus —> percpu_timer_setup :
static void __cpuinitpercpu_timer_setup(void)
{
???unsigned int cpu = smp_processor_id();
???struct clock_event_device *evt = &per_cpu(percpu_clockevent, cpu);
? ?evt->cpumask = cpumask_of(cpu);
? ?if (!lt_ops || lt_ops->setup(evt)) //這里的setup就是前面初始化的:hi3536_local_timer_setup,將CPU0的timer注冊到clockevent設備。
???????broadcast_timer_setup(evt);
}
B.次CPU(cpu1~cpu3)
???????? 注冊過程:kernel_init—> kernel_init_freeable —> smp_init:
void __init smp_init(void)
{??
?? …
???/* FIXME: This should be done in userspace --RR */
???for_each_present_cpu(cpu) {
???????if (num_online_cpus() >= setup_max_cpus)
???????????break;
???? ???if (!cpu_online(cpu)) //啟動所有未啟動的CPU
???????????cpu_up(cpu);
??? }
…
}
???????? cpu_up—> _cpu_up —> __cpu_up —> boot_secondary —> smp_ops.smp_boot_secondary—> hi3536_boot_secondary —> hi3536_secondary_startup(匯編) —> secondary_startup(匯編) —> __secondary_switched(匯編) —> secondary_start_kernel —>percpu_timer_setup該函數(shù)就是上面主CPU注冊本地定時器的函數(shù)。
(6)clockevent初始化
???????? 這里主要是注冊clockevent的global timer,在periodic模式下不起作用,在one-shot模式下才會作用。注冊過程過程如下:
???????? sp804_clockevents_init—> __sp804_clockevents_init —> clockevents_config_and_register —> clockevents_register_device—> clockevents_do_notify —> tick_notify(tick_init初始化的通知分發(fā)器) —>tick_check_new_device
?
4、periodic和one-shot模式
???????? 針對clockevent有periodic和one-shot兩種模式,主要的不同點:event_handler事件處理函數(shù)不同,細節(jié)見下列分析。
(1)periodic模式
???????? 根據(jù)上一節(jié)的注冊初始化,其先后順序如下:timer1(clockeventglobal timer) —> timer4(clockevent cpu0 local timer) —> time5~7(clockeventcpu1~3 local timer)。
???????? 注冊timer1時運行在CPU0上,次CPU1~3還沒有啟動,這時tick_check_new_device接口會把timer1注冊為CPU0的local timer,此刻timer1的event_handler= tick_handle_periodic。當CPU0調用smp_prepare_cpus注冊自己的local timer時會用timer4替換timer1,此刻timer4的event_handler= tick_handle_periodic,而timer1的event_handler在tick_setup_device中被修改為clockevents_handle_noop(空實現(xiàn)),并在接口tick_check_broadcast_device中被注冊為globaltimer設備(在周期模式下不起任何作用)。
???????? CPU1~3啟動時調用secondary_start_kernel注冊local timer時只有一個定時器分別是timer5~7,它們的event_handler= tick_handle_periodic。
???????? clockevents_handle_noop:空實現(xiàn)
???????? tick_handle_periodic主要完成如下工作:
A.如果是CPU0,則調用do_timer完成tick更新和墻上時鐘更新
B.update_process_times完成:啟動本地軟中斷;更新本CPU的運行隊列;調度任務;SMP下觸發(fā)運行隊列均衡。
?
(2)one-shot模式
???????? 設備啟動時一開始是periodic模式,過程和上面一模一樣。各個CPU觸發(fā)本地軟中斷后發(fā)生切換,run_timer_softirq—> hrtimer_run_pending —> tick_check_oneshot_change —> tick_nohz_switch_to_nohz—> tick_switch_to_oneshot:
將CPU0~3的event_handler切換為:tick_nohz_handler
將timer1的event_handler切換為:tick_handle_oneshot_broadcast
切換只會發(fā)生一次,切換好后hrtimer_run_pending接口就會直接返回。
tick_nohz_handler:
???????? 和tick_handle_periodic做的事情大致一樣。
tick_handle_oneshot_broadcast主要完成:喚醒各個CPU,補償tick的偏差。
???????? one-shot模式下event_handler處理函數(shù)每次都要重新設置next_event。該模式的好處就是可以節(jié)省CPU功耗。
5、系統(tǒng)調用例子講解
(1)gettimeofday
???????? 該函數(shù)主要用于獲取微妙級別的時間。其對應的系統(tǒng)調用為sys_gettimeofday,實際起作用的是do_gettimeofday:
void do_gettimeofday(struct timeval*tv)?????????????????????????????????????????????????????????????????????????????????????????????????????????
{????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
???struct timespec now;?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
? ?getnstimeofday(&now); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
???tv->tv_sec = now.tv_sec;?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
???tv->tv_usec = now.tv_nsec/1000;??????????????????????????????????????????????????????????????????????????????????????????????????????????????
}
int __getnstimeofday(structtimespec *ts) ????????????????????????????????????????????????????????????????????????????????????????????????????????
{??
???struct timekeeper *tk = &timekeeper;
???unsigned long seq;???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
???s64 nsecs = 0;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
???do {
???????seq = read_seqcount_begin(&timekeeper_seq);? //使用順序鎖進行數(shù)據(jù)同步訪問
? ts->tv_sec = tk->xtime_sec;??? //獲取秒數(shù),xtime_sec更新由update_all_time進行??????????????????????????????????????????????????????????????????????????????????????????????????????????????
???????nsecs = timekeeping_get_ns(tk); //調用clocksource的read函數(shù)(即上面的hi3536_clocksource_read),將計數(shù)轉化為相應的ns數(shù)
??? }while (read_seqcount_retry(&timekeeper_seq, seq));
?
???ts->tv_nsec = 0;
???timespec_add_ns(ts, nsecs); //調整ns數(shù),傳遞給用戶
??????…..
}
?
(2)nanosleep
???????? 這是一個ns級別的睡眠函數(shù),對應的系統(tǒng)調用sys_nanosleep(kernel/hrtimer.c),實際起作用的是hrtimer_nanosleep—> do_nanosleep:
static int __sched do_nanosleep(structhrtimer_sleeper *t, enum hrtimer_mode mode)
{??
???????? //初始化一個新的hrtimer
???hrtimer_init_sleeper(t, current);
???????
???do {
???????set_current_state(TASK_INTERRUPTIBLE); //設置當前任務睡眠
???????hrtimer_start_expires(&t->timer, mode);//將新的hrtimer加入到timer_list
???????if (!hrtimer_active(&t->timer)) //如果沒有激活hrtimer則直接退出
???????????t->task = NULL;
???????????
???????if (likely(t->task)) //如果激活了,就開始調度
???????????freezable_schedule();
???
???????hrtimer_cancel(&t->timer); //運行到這里,說明定時器到期,那么取消定時器。
???????mode = HRTIMER_MODE_ABS;
???
??? }while (t->task && !signal_pending(current));
?
???__set_current_state(TASK_RUNNING); //設置進程狀態(tài)可執(zhí)行
?
???return t->task == NULL;
}
???????? 不管內核有沒有配置HIGH_RES_TIMERS,內核都編譯httimer.c接口。如果沒有配置hrtimer則使用低精度的tick方案,這時定時器是相當不準確;如果進行了配置,則使用高精度方案。兩種方案下,定時器中斷處理方法不同:
低精度下的hrtimer:
???????? update_process_times—> run_local_timers —> hrtimer_run_queues
高精度下的hrtimer:
run_hrtimer_softirq—>hrtimer_peek_ahead_timers —> __hrtimer_peek_ahead_timers —> hrtimer_interrupt
?
?
附錄A
參考資料
http://www.wowotech.net/timer_subsystem/time-subsyste-architecture.html
http://www.wowotech.net/timer_subsystem/timer_subsystem_userspace.html
?
?
?
總結
以上是生活随笔為你收集整理的linux内核之时间子系统的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: couchbase_Couchbase
- 下一篇: 天融信防火墙配置