linux核心“任務”之定時器、核心線程、系統調用__linux

來源:互聯網
上載者:User
一、核心定時器 1.基本概念 在某些情境下,我們需要在特定的時間後做某些動作,但是又不想一直等待而浪費CPU,這個時候定時器是非常合適的機制。定時器用於在將來的某個時間點執行某個函數以完成特定的任務。
核心定時器告訴核心在指定的時間點使用特定的參數來調用特定的函數。定時器是非同步運行於其註冊者的,定時器運行時,註冊該定時器的任務可能在休眠也可能在其它處理器上運行,甚至可能已經退出。
linux中核心定時器是基於(軟)中斷實現的,也就是它處於中斷上下文而非進程上下文。在非進程上下文有些原則要遵循:
不允許訪問使用者空間 current無意義,因而也不可用 不能進行睡眠或者調度。 不能調用 schedule 或者某種 wait_event, 也不能調用任何可能引起睡眠的函數。訊號量也不可用,因為訊號量可能引起休眠。 核心代碼通過調用函數 in_interrupt()可以判斷當前是否處於中斷上下文, 只要返回非0就表示處於中斷上下文。核心可以通過調用in_atomic()判斷當前是否允許調度。不允許調度的情況包括:處於中斷上下文以及擁有自旋鎖的上下文。

由於定時器是非同步執行的,因而定時器處理函數必須注意進行互斥保護。 2.linux核心支援的定時器 linux核心支援兩種類型的定時器:
經典定時器:精度取決於電腦時鐘中斷運行頻率的定時器。該定時器精度一般比較低,為1000/HZ ms的精度。該定時器以固定的頻率產生,即每隔1000/HZ ms產生一次。如果沒有使能動態時鐘特性,則在該定時器到期時,可能並沒有真正的定時事件發生,比如系統中只添加了如下定時器:11ms,52ms,78ms到期的定時器,但是經典定時器會在4的倍數ms處到期(4,8,12...),因而說定時器到期的時間點不一定有定時事件發生。 高解析度定時器:經典定時器的精度比較低,在有的場合需要更高精度的定時器,比如多媒體應用,因而系統引入了該類型的定時器。該定時器本質上可以在任意時刻發生。 這裡也需要特別之處兩個概念:
動態時鐘:只有在有任務需要實際執行時,才啟用周期時鐘,否則就禁用周期時鐘的技術。作法是如果需要調度idle來運行,禁用周期時鐘;直到下一個定時器到期為止或者有中斷髮生時為止再啟用周期時鐘。單觸發時鐘是實現動態時鐘的前提條件,因為動態時鐘的關鍵特性是可以根據需要來停止或重啟時鐘,而純粹的周期時鐘不適用於這種情境。 周期時鐘:周期性的產生時鐘時間的時鐘。 從應用上來說,定時器主要有兩個用途:
逾時:表示在一定時間後會發生的事件,事實上,在使用逾時時,大多數情形下,並不期望逾時時間發生,定時器往往會在逾時之前被取消。另外即便不取消,逾時事件也往往不是一個精確的事件,比如在網路中用到的各種逾時定時器,它們表達的意思往往是如果在這個時間點之前還沒有...,則可以認為...,這個時間的取值往往是一個經驗值或者估算值,並不是精確的時間要求。在這種場合經典定時器是夠用的。 定時器:用於實現時序,比如播放聲音時,需要定期往音效卡發送資料,這種場合下,對時間就有比較嚴格的要求,如果不在某個時間點發送資料到音效卡,就會出現聲音失真。這時就要使用高精度定時器。 通過配置可以使得linux工作在如下模式:
高解析度動態時鐘 高解析度周期時鐘 低解析度動態時鐘 低解析度周期時鐘

3.低解析度核心定時器

低解析度定時器是最常見的核心定時器,核心會使用處理器的時鐘中斷或者其他任何適當的周期性時鐘源作為定時器的時間基準。時鐘中斷會定期發生,每秒HZ次。該中斷對應的定時器處理函數一般為timer_interrupt,在該函數的處理中,最終會調到do_timer和update_process_timers。其中do_timer會負責全系統範圍的、全域的任務:更新jiffies,處理進程統計;而後一個函數則會進行進程統計,產生TIMER_SOFTIRQ,向調度器提供時間感知。

當定時器到期時,在定時器處理函數被調用之前,定時器會被從啟用鏈表移走,因而如果想要在本次執行完後再過一段時間後重新執行,就需要重新添加定時器。在SMP系統中,定時器函數會由註冊它的CPU執行。

核心定時器的實現要滿足以下要求和假設:
定時器管理必須儘可能簡化. 其設計必須在活動定時器大量增加時具有很好的伸縮性 大部分定時器在幾秒或最多幾分鐘內到期, 而帶有長延時的定時器是相當少見. 一個定時器應當在註冊它的同一個 CPU 上運行.

低解析度核心定時器的實現是很巧妙的。它基於一個每-CPU 資料結構。timer_list的base欄位包含了指向該結構的指標。如果 base是NULL, 這個定時器沒有被調用運行; 否則, 這個指標告知哪個資料結構(也就是哪個CPU)正在運行它。 無論何時核心代碼註冊一個定時器(通過add_timer或者mod_timer), 操作最終由internal_add_timer執行( 在kernel/timer.c), 它會將新的定時器添加到和當前CPU關聯的“級聯表”中的定時器雙向鏈表中。
級聯表的工作方式:
如果定時器在接下來的 0 到 255 jiffies 內到期, 則它被添加到專供短時定時器的256個鏈表中的一個, 使用expires(即添加到那個鏈表是由到期時間的位元位決定的)的低8位決定加到那個鏈表中 如果它在將來更久時間到期(但又在16384個jiffies之前 ), 則它被添加到64個鏈表之一,這64個鏈表與expires的8-13位元位相關,expires的這6個位元決定了使用哪個鏈表。 類似的技術被應用於expires的位元位14-19,20-25以及26-31。 如果定時器在更久的經來到期,則將其放入26-31位元對應的鏈表中,具體使用那個鏈表取決於(0xffffffff+base->timer_jiffies的結果的高6位) 這裡描述的低8:6:6:6:6位元組並不特別準確(每個位元組定義了該級級聯表的大小),根據配置還可能採用6:4:4:4:4的位元組。這裡只是描述下其設計原理。
當__run_timers被啟用時, 它會執行當前定時器滴答上的所有pending的定時器。如果當前jiffies是256的倍數, 這個函數會將下一級定時器鏈表重新散列到256短期鏈表中,同時還可能根據jiffies上的位元位劃分對其它層級的定時器做級聯處理。它的基本原理在於級聯表是以定時器到期的jiffies組織的,而base->timer_jiffies則記錄了當前jiffies,因而如果__run_timers被執行時,它首先取base->timer_jiffies最低的幾個位元確定的鏈表中的定時器並執行它們,並且如果base->timer_jiffies為0,則就開始檢查下一個位元組。對於除了最低的位元組外的所有位元組,其處理邏輯為:如果該位元組不為0,則就將該位元組確定的鏈表中的定時器重新添加到系統中,並且不再繼續檢查下一個位元組,否則檢查會檢查下一個位元組。

這種技術使用了較多的鏈表頭,佔用了一些額外的記憶體,但是很好的滿足了對核心定時器的要求和假設。但是由於會受其它中斷的影響,因而它並不絕對準確。該技術的圖形化示意如圖:


4.低解析度定時器相關API struct timer_list
{
  /* ... */
  unsigned long expires;
  void (*function)(unsigned long);
  unsigned long data;
};
這裡並沒有列出timer_list的所有元素,僅列出來了比較重要的幾個:
expires:以jiffies為單位的逾時時間。 function:定時器處理函數 data:定時器處理函數的參數 該結構在使用之前必須被初始化,初始化可以保證所有欄位被正確的設定,包括那些對使用者不可見的欄位。對於定時器資料結構要用init_timer初始化,或者對於靜態資料結構可以通過將TIMER_INITIALIZER賦值給它來完成初始化。
void init_timer(struct timer_list *timer);
完成對動態申請的定時器資料結構的初始化。
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
完成對靜態定時器資料結構的初始化
void add_timer(struct timer_list * timer);
添加定時器到系統
int del_timer(struct timer_list * timer);
從系統中刪除定時器。
在初始化後,調用add_timer將定時器添加到系統之前可以修改列出的定時器資料結構的三個域。
int mod_timer(struct timer_list *timer, unsigned long expires);
更新一個定時器的逾時時間。
int del_timer_sync(struct timer_list *timer);
該函數功能類似於del_timer,但是它保證當它返回時,定時器函數不在任何 CPU 上運行。在SMP架構中使用del_timer_sync可以避免競態。大多數情況下應該優先考慮使用del_timer_sync。對於該函數要注意:
如果在非原子上下文調用了該函數,則函數可能休眠 如果在原子上下文調用了它,它會保持忙等待 不能在中斷上下文調用它 調用這不能持有可能阻塞定時器處理函數完成處理的鎖。在擁有鎖時,調用del_timer_sync要特別小心,因為如果定時器函數也企圖擷取相同的鎖,就可能造成死結。 如果調用了該函數,則要求定時器處理函數不能重新註冊自己。如果定時器函數會重新註冊自己, 則需要進行特殊處理以確保在調用了該函數來刪除定時器時,定時器不被重新註冊。可以設定一個" 關閉 "標誌,然後由定時器處理函數檢查這個標誌來確保滿足這個要求。 int timer_pending(const struct timer_list * timer);

返回真或假來表示定時器當前是否正被調度執行。 5.高解析度定時器 常規的低解析度核心定時器針對“逾時”的應用情境做了最佳化,同時低解析度定時器的實現是和jiffies緊密相連的,因而無法應用於需要高精度定時的場合,為此linux提供了高解析度定時器hrtimer。

hrtimer 是建立在 per-CPU 時鐘事件裝置上的,如果SMP系統中只存在一個全域的時鐘事件裝置,則這樣的系統無法支援高解析度定時器。高解析度定時器需要由CPU的本地時鐘事件裝置來支援,即它是per-CPU的。為了支援 hrtimer,核心需要配置 CONFIG_HIGH_RES_TIMERS=y。hrtimer 有兩種工作模式:低精度模式(low-resolution mode)與高精度模式(high-resolution mode)。雖然 hrtimer 子系統是為高精度的 timer 準備的,但是系統可能在運行過程中動態切換到不同精度的時鐘源裝置,因此,hrtimer 必須能夠在低精度模式與高精度模式下自由切換。由於低精度模式是建立在高精度模式之上的,因此即便系統只支援低精度模式,部分支援高精度模式的代碼仍然會編譯到核心當中。 高解析度定時器基於紅/黑樹狀結構實現。它獨立於周期時鐘,並且使用納秒作為時間單位。 高解析度定時器可以基於兩種時鐘實現: 單調時鐘CLOCK_MONOTONIC:從0開始單調遞增 系統時鐘CLOCK_REALTIME:系統實際時間,可能跳躍,比如系統時間改變。 高解析度模式下高解析度定時器的時鐘事件裝置會在定時器到期時引發中斷,該中斷由hrtimer_interrupt處理。如果沒有提供高解析度時鐘,則高解析度定時器的到期操作由hrtimer_run_queues完成。 在最新的代碼中,如果高精度定時器的處理函數沒有返回HRTIMER_NORESTART,則定時器處理函數處理完後定時器架構會自動重啟高精度定時器,相關程式碼片段如下:

        restart = fn(timer);        ...        /*         * Note: We clear the CALLBACK bit after enqueue_hrtimer and         * we do not reprogramm the event hardware. Happens either in         * hrtimer_start_range_ns() or in hrtimer_interrupt()         */        if (restart != HRTIMER_NORESTART) {                BUG_ON(timer->state != HRTIMER_STATE_CALLBACK);                enqueue_hrtimer(timer, base);        }
6.高解析度定時器資料結構和API hrtimer_bases 是實現 hrtimer 的核心資料結構,通過 hrtimer_bases,hrtimer 可以管理掛在每一個 CPU 上的所有 timer。每個 CPU 上的 timer list 不再使用 timer wheel 中多級鏈表的實現方式,而是採用了紅/黑樹狀結構(Red-Black Tree)來進行管理。其定義如下:
DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases)
高解析度時鐘使用了結構體hrtimer_clock_base來表示時鐘基礎。主要包括: cpu_base:該時鐘基礎所屬包括的各CPU時鐘基礎結構 get_time:擷取時間的函數 resolution:該定時器的解析度 active:所在的紅/黑樹狀結構的根相關資訊 結構體hrtimer_cpu_base表示每CPU時鐘基礎。主要包括:
expires_next:將要到期的下一個事件的絕對時間 hres_active:是否啟用了高精度模式 結構體hrtimer用於定時高精度定時器。主要包括: node:用於將定時器維護在紅/黑樹狀結構中 _softexpires:定時器到期的絕對時間 function:定時器到期時執行的函數 base:指向時鐘基礎 state:狀態 HRTIMER_STATE_INACTIVE:定時器處於非使用中 HRTIMER_STATE_ENQUEUED:定時器在時鐘基礎上排隊,等待到期 HRTIMER_STATE_CALLBACK:正在執行到期的回呼函數 HRTIMER_STATE_MIGRATE:被遷移到了其他CPU。

void hrtimer_init(struct hrtimer *timer, clockid_t which_clock, enum hrtimer_mode mode);初始化定時器

int hrtimer_cancel(struct hrtimer *timer);嘗試取消定時器,並且如果定時器在被執行就等待它被執行完
int hrtimer_try_to_cancel(struct hrtimer *timer);嘗試取消定時器,如果此時定時器是active的就返回1,如果是非active的就返回0,如果正被執行就返回-1,前兩種情況定時器會被取消,最後一種情形定時器不被取消。
int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode);在當前CPU上啟動定時器
u64 hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);使定時器在now後interval的時間點逾時 二、核心線程 1.基本概念

核心線程實際上是直接由核心本身啟動的進程。當前系統中有一些工作是由核心線程完成的: 周期性地將修改的記憶體頁與頁來源塊裝置同步 如果記憶體頁很少使用,則寫入交換區 管理延時動作 實現檔案系統的交易記錄 執行非強制中斷(ksoftirqd) 核心線程可能用於兩種情境:
啟動一個核心線程,然後一直處於等待狀態直到被喚醒以完成某種服務 啟動一個周期性啟動並執行核心線程,以檢查特定資源的使用方式,並作出適當的反映 核心線程由核心自身產生,其特點在於:
它們在核心態執行,而不是使用者態。 它們只可以訪問虛擬位址空間的核心部分(高於TASK_SIZE的所有地址),但不能訪問使用者空間。 

task_struct進程描述符中包含兩個跟進程地址空間相關的欄位mm, active_mm,對於普通使用者進程來說,mm指向虛擬位址空間的使用者空間部分,而對於核心線程,mm為NULL。active_mm主要用於最佳化,由於核心線程不與任何特定的使用者層進程相關,核心並不需要倒換虛擬位址空間的使用者層部分,保留舊設定即可。由於核心線程之前可能是任何使用者層進程在執行,故使用者空間部分的內容本質上是隨機的,核心線程決不能修改其內容,故將mm設定為NULL,同時如果切換出去的是使用者進程,核心將原來進程的mm存放在新核心線程的active_mm中。假如核心線程之後啟動並執行進程與之前是同一個,核心並不需要修改使用者空間地址表,TLB中 資訊仍然有效;只有在核心線程之後執行的進程與此前使用者層進程不同時,才需要切換,並清除對應TLB資料。 2.核心線程的訊號處理 預設情況下核心線程以及daemonize的使用者線程阻塞所有訊號。如果核心線程想要允許訊號發送過來,則需要調用allow_signal以允許發送訊號給自己。但是核心線程的訊號處理方式和使用者空間的是不同的,使用者線程的訊號處理常式由系統自動調用,其入口為do_signal,該函數在使用者線程重新被調度時可能會被執行。但是對於核心線程來說,是不會走到該函數的,這就意味著核心線程的訊號處理必須採用不同的方式。簡單的來說核心線程需要採用如下方式來處理髮送給它的訊號:

do {    /* the process procedure of the kernel thread */} whle(!signal_pending(current)) 
3.實現方式即相關API

核心線程可以通過兩種方式實現: 將一個函數傳遞給kernel_thread 建立核心更常用的方法是輔助函數kthread_create,該函數建立一個新的核心線程。最初線程是停止的,需要使用wake_up_process啟動它。或使用kthread_run,與kthread_create不同的是,其建立新線程後立即喚醒它。

long kernel_thread(int (fn) (void *), void *arg, unsigned long flags)

參數及意義: fn:為要執行的函數的指標 arg:函數參數 flags:線程標誌 struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
其參數及意義:
threadfn:thread的入口函數 data:threadfn的參數 namefmt:thread的名字 該函數用於建立一個最初處於停止狀態的核心線程。當被喚醒時,threadfn會被調用,同時data作為其參數,threadfn可以有兩種方式結束:
直接調用do_exit來退出 或者在某個地方會對該thread調用kthread_stop kthread_run(threadfn, data, namefmt, ...) 
它是一個宏,建立完一個thread後立即喚醒它。它實際上是在調用完了kthread_create後立即調用了wake_up_process
int kthread_stop(struct task_struct *k) 
該函數用於停止一個由kthread_creat建立的thread。如果thread會自己調用do_exit則不能對該thread調用該thread_stop
void kthread_bind(struct task_struct *k, unsigned int cpu)
將剛建立的thread綁定到指定的CPU。CPU不一定是online的,但是調用該函數時thread必須處於stopped狀態(也就是剛執行完kthread_create)。
三、系統調用 1.基本概念 系統調用:系統調用指的是作業系統提供給應用程式調用的一組“特殊”介面。應用程式可以通過這組“特殊”介面來獲得作業系統核心提供的服務。
從邏輯上來說,系統調用可被看成是一個核心與使用者空間程式互動的介面——系統調用把應用程式的請求傳給核心,調用相應的的核心功能完成所需的處理,將處理結果返回給應用程式。
系統服務之所以需要通過系統調用來提供給使用者空間的根本原因是為了對系統進行“保護”,防止惡意使用者破壞系統或者由於不小心而破壞了系統。系統調用的特殊之處在於規定了使用者進程進入核心的具體位置。

Linux系統只提供了最基本和最有用的系統調用,可以通過man 2 syscalls 命令查看,或到 ./include/linux/syscalls.h源檔案中查看。 2.系統調用的功能

系統調用主要完成以下任務:

進程管理 時間操作 訊號處理 調度 模組 檔案系統 記憶體管理 處理序間通訊 網路功能 系統資訊和設定 系統安全

從另一反面來說無論完成什麼任務,都可以歸屬到下面幾類:  

控制硬體——系統調用往往作為硬體資源和使用者空間的抽象介面,比如讀寫檔案時用到的write/read調用。 設定系統狀態或讀取核心資料——因為系統調用是使用者空間和核心的唯一通訊手段,所以使用者佈建系統狀態,比如開/關某項核心服務(設定某個核心變數),或讀取核心資料都必須通過系統調用。 進程管理——建立、執行進程,以及擷取進程的狀態等資訊。

之所以使用系統服務是因為:
有些服務必須獲得核心資料,比如一些服務必須獲得中斷或系統時間等核心資料。 從安全形度考慮,在核心中提供的服務相比使用者空間提供的毫無疑問更安全,很難被非法訪問到。 從效率考慮,在核心實現服務避免了和使用者空間來回傳遞資料以及保護現場等步驟,因此效率往往要比在使用者空間實現高許多。比如,httpd等服務。 如果核心和使用者空間都需要使用該服務,那麼最好實現在核心空間,比如隨機數產生。 3.和C庫的關係

在大多數情況下,應用程式直接使用C庫中的API而不是系統調用,對於C庫來說,有的C庫提供的功能是完全由在使用者態完成的,有的要藉助一個系統調用來實現,有的要藉助多於一個系統調用來實現。 4.重啟系統調用

如果正被執行的系統調用被中斷了,核心需要通知調用者,系統調用被中斷了,這時會返回-EINTR錯誤,需要注意的是這裡的中斷是指系統調用被發送給該進程的訊號給中斷了,而不是一般意義上的中斷(硬體中斷、非強制中斷本身並不會使得系統調用返回-EINTR,否則系統調用被打斷就成了常態了。)。但是這會加大調用者的工作量,它必須檢查傳回值,如果是返回了該錯誤就要重新發起系統調用。可以通過設定訊號的SA_RESTART標識來使得系統調用在被該訊號中斷時被自動重新啟動。 5.系統調用的實現

當發起系統調用時,需要傳遞一個系統調用號給核心,一個系統調用號對應於一個系統調用,隨後CPU進入核心模式。不管使用何種方式實現系統調用(比如通過X86的int $0x80或者通過sysenter指令進入),核心都會得到一個系統調用號,然後根據該系統調用號執行相應的系統調用,並在執行完成後返回一個整數值給使用者程式,0表示系統調用成功,負數表示失敗。系統調用的處理過程: 在核心態儲存大多數寄存器的內容 調用相應的服務程式 退出系統調用程式:用儲存在核心棧中的值載入寄存器,並切換回使用者態 linux定義了NR_syscalls個系統調用。可以傳遞參數給系統調用,但是傳遞的參數要滿足兩個要求:
每個參數長度不能超過寄存器長度 參數個數不能超過6個,因為使用6個寄存器來傳遞參數 如果參數超過了6個,就要用一個寄存器來儲存存放這些參數的值所在的記憶體地區。如果使用了C庫,使用者就不用關心這些細節。在執行系統調用之前,核心也會做些合法性檢查比如參數驗證。如果系統調用要返回大量資料給其調用者,則必須通過調用者指定的記憶體區交換該資料,該記憶體區必須是調用者可訪問的地區。
系統調用雖然是在核心執行,但它並非一個純粹意義上的核心線程。它只是代表使用者進程在核心運行,因此它可以訪問進程的許多資訊(比如current結構——當前進程的控制結構),而且可以被其他進程搶佔(在從系統調用返回時,由system_call函數判斷是否該再調度),可以休眠,還可接收訊號等等。需要注意的是系統調用完成後,在回到或者說把控制權交回到發起調用的使用者進程前,核心會有一次調度。如果發現有優先順序別更高的進程或當前進程的時間片用完,那麼就會選擇高優先順序的進程或重新選擇進程運行。除了再調度需要考慮外,再就是核心需要檢查是否有掛起的訊號,如果發現當前進程有掛起的訊號,那麼還需要先返回使用者空間處理訊號處理常式(處於使用者空間),然後再回到核心,重新返回使用者空間,有些麻煩但這個反覆過程是必須的。
調用效能問題

系統調用需要涉及到使用者空間和核心空間的來回切換,因而需要花費一些額外的時間。在大多數情況下這個時間是可接受的,如果應用對效能要求很高,但是又想使用系統提供的服務,可以將程式放入核心。

可以使用ptrace或者strace來追蹤系統調用。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.