linux內核學習(轉2)

·進程調度 

進程的調度是內核裏面非常重要的一個部分,該模塊完成了進程的切換功能,既它要選擇一個最合適的進程去執行。我們知道進程的切換是要花費一定的時間的,如果調度器一個勁的調度進程,那麼系統的利用率可想而知了——那會非常低。但是這樣做有一個好處,那就是進程的響應時間變快了。相反,如果我們選擇儘量讓進程多運行一會時間,儘量少發生進程的調度,不錯,如你所想,提高了系統的吞吐量。可是另一方面就帶來一個缺點,那就是進程的響應時間變慢了。

所以在這塊,內核開發者需要動一些腦筋了,他們需要用到大量的算法。以此來解決這樣一對矛盾:如何平衡進程響應速度(響應時間短)和最大系統利用率(高吞吐量)。從2.4版本到現在最新的 2.6版本已經變動了兩次調度器,現在最新的調度器叫完全公平調度器(CFS)。更新調度器是爲了更加找到上面所說的那種平衡。

由於 linux的被使用在了不同的環境,從桌面環境、嵌入式系統一直到服務器。不同的環境,那麼他們肯定會選擇一個更加適合這個環境的調度器的。嵌入式系統需要的是響應實時,既響應時間短。而服務器一般則是需要的是一種公平的對待每個任務的調度器。Linux在實時方面並不是做的很好,起碼內核源代碼中的是這樣。不過有很多人針對實時性,專門對內核源碼做出了相應的修改,提高內核的實時性,這樣的代碼可以用到嵌入式系統上。

Linux的調度器像一般現代操作系統一樣,提供了基於時間片的可搶佔式的進程調度方式。基於時間片可以照顧那些低優先級的進程,而可搶佔式有在一定程度上滿足了實時性(響應時間短)的要求。

進程調度發生的時機是我們要注意的,既什麼時候發現進程的切換。注意:schedule()函數爲發生進程切換的函數。

主動式:進程自己調用schedule()函數。——我擔心很多愛提問的人對這句話不懂,就解釋這樣一句話還要花很長時間。第一點,進程自己調用 schedule(),分爲兩種形式。一是進程在應用程序中調用了進程調度的系統調用,所以“進程自己調用schedule()函數”。二是進程爲了得到某個硬件資源,所以進程發生了阻塞式的系統調用,並進入了內核空間。此時內核代進程去訪問那個硬件資源,可是結果是硬件已經被佔用,你現在得不到硬件資源。於是內核先把進程掛到這個資源的等待隊列上面,並設置進程的狀態爲掛起狀態(不可再被調度),然後內核代進程主動調用schedule()函數,我們也叫“進程自己調用schedule()函數”。你懂了嗎?

這邊要解釋在進程管理中留下的一個問題:爲什麼要有兩種任務掛起的狀態呢?這邊很多書籍都會這樣簡單的寫:一個可以接受信號,一個不會接受信號。但是這樣講大多部分讀者並不能弄明白的。其實是這樣的:TASK_INTERRUPTIBLE這個掛起狀態可以認爲是可中斷的掛起,TASK_UNINTERRUPTIBLE則是不可以中斷的掛起。掛起是因爲要等待某種資源,這點你要明白,否則我再怎麼講解,你都不會明白的。那等待某個資源爲什麼還要兩種掛起方式呢?先說第一種,可中斷的掛起,在等待某種資源的時候,當他接受到信號的時候,會被提前的喚醒,去執行信號處理函數,然後回到進程執行過程。什麼是提前的喚醒?你可以認爲是僞喚醒,既不是真正的硬件條件達到被調度執行,而是因爲到來了一個信號,提前喚醒了。那麼聰明的讀者看到這邊一定會問:那麼被喚醒之後怎麼辦呢?豈不是還沒有等到資源就被提前喚醒了嗎?所以在這邊,一般是用一個while(1)循環操作,當每次被喚醒的時候都會去循環的檢查一次資源是否真的到來了。如果是被假喚醒了:那麼內核就會再次執行循環體:內核代表應用程序本身,把進程自己設置爲掛起狀態,然後執行schedule()。如果是真的條件到來了,那麼就把進程移出等待隊列。(所以大家要注意了,不是把進程放到等待隊列之上就發生了進程的切換,進程的切換還是需要內核代進程完成的。)那麼大家就會發現一個問題:進程由可能會被經常的僞喚醒,去檢查資源,再去發生進程切換,這樣不是明顯的浪費了處理器時間了嗎?實時就是這樣的,不過因爲信號處理也是很重要的一個部分,所以內核中一般還是用這個掛起的狀態。那麼TASK_UNINTERRUPTIBLE就是不可中斷的,當他收到信號之後,不會發生提前喚醒去處理信號。只有當資源達到之後,提供資源的模塊發出喚醒操作,這才真正的喚醒了此進程。(由於篇幅有限,具體信號處理方面不再過多的解釋,關於信號處理大家要去找書籍看。)

剛纔講到了發生進程切換的時機,一個是主動發生,那還有一個就是被動發生了,既進程被其他進程搶佔了。被動式發生進程調度是這樣的:當一個進程的時間片被用完了,那麼它就會被其他進程搶佔。還有一種情況是有優先級更加高的進程進入了可執行狀態,當前進程也會被搶佔。那麼搶佔發生在什麼時機呢?一種是用戶搶佔,另一種是內核搶佔。

用戶搶佔:你可以這樣理解,發生在用戶空間的搶佔。我們知道用戶空間和內核空間只有兩種途徑:一個是系統調用,還有一個就是中斷。所以可想而知,進入或退出內核空間都會有量段特定的代碼的:一段代碼完成了用戶態到核心態的切換,還有一段代碼則是完成了從核心態到用戶態的切換。那麼我想告訴大家的是,用戶搶佔就發生在從核心態到用戶態的切換代碼中。既當系統調用完成之後從內核態到用戶態的時候和中斷處理完成之後從內核態到用戶態的時候發生用戶搶佔。那麼內核怎麼知道要發生進程的搶佔呢?若need_resched標誌被設置之後,則會發生用戶搶佔,否則不會發生。那麼誰來設置need_resched這個標誌呢?一個是在定時器中斷處理程序中(稍後會介紹),scheduler_tic()函數會減少當前進程的時間片。當時間片減爲零的時候,設置那個標誌,告訴內核:嗨,內核你注意了,這邊有個進程需要被調度啦。稍後,內核就會檢查這個標誌,併發生進程切換。還有一個就是當一個優先級高的進程進入了可執行狀態的時候,try_to_wake_up()(這個函數是喚醒等待隊列上的進程的操作,這個是真的喚醒操作:喚醒等待該資源的一個進程,設置進程的優先級,並把它設計爲可執行狀態。)也會設置這個標誌。

內核搶佔:內核搶佔是2.6內核新加入的一個內容。在沒有內核搶佔之前,搶佔只能發生在了用戶空間,然後在實時性方面會有很差的表現。引入了內核搶佔,進程調度可以發生在內核空間了。這個時候引入了 preempt_count,叫內核搶佔計數器。爲什麼要這個計數器?因爲內核中不像應用程序中那麼簡單,內核路徑非常複雜,有非常多的情況是不能發生搶佔的(具體的不一一列出),所以設置這個計算器完成是出於安全考慮。只有當內核搶佔計數器和need_resched同時滿足條件的時候,纔可以發生進程內核搶佔。那麼內核搶佔發生在什麼時機呢?一個是中斷回到內核空間,二是解鎖和使能軟中斷,具體這兩個我們後面有機會介紹。

關於進程調度還有一個要注意的,那就是讀者你要去理解進程切換髮生了什麼事情。第一點非常重要,我在這邊做簡單的介紹。我們知道每個處理器都有一個叫程序計數器(PC)的寄存器,(如果你不知道PC的作用,也不用看關於內核的東西了,先把處理器最基本的知識搞定。)PC指向了下一條指令的地址,所以我們大概可以這樣理解:發生進程切換時,PC指針被保存到了一個安全的地方,然後從某個地方把要切換的進程的保存的PC指針放到這邊,然後改變堆棧指針,就這樣。具體細節請讀者一定看明白。

那麼進程調度也講的夠多了,到這邊大家應該對進程調度有了一定的瞭解了吧。這個模塊還是比較重要的和有意思的,讀者可以話一定時間去了解。如果你還有疑問,就多看幾遍,或者找本書看。

 

·內存管理 

在大家學習內存管理之前,一定要先把MMU這個東西弄明白,要不就會很難往深處理解,或者壓根看不懂關於內存的管理。現在的出來器都含有了內存管理單元(MMU),MMU管理內存並把虛擬地址轉換爲物理地址。通常是以頁爲單位進行處理的,所以你會發現你所有得到的內存大小一般都是4KB的倍數(我們只討論32位機器),很少有例外。

內存管理這節要介紹的是內核如何管理和分配內存,下一小節進程地址空間,介紹內核如何管理用戶進程的地址空間。首先我們要知道只有內核纔可以管理內存,內核需要用內存,那麼它就自己給自己分配一塊內存。如果是應用程序要用內存,那麼應用程序會發出一個請求分配內存的系統調用,然後內核就給它分配一塊內存空間。

我們需要弄明白什麼是虛擬地址,什麼是物理地址,它們是如何轉化的。這個就是MMU要做的事情了。兩個地址之間相互的轉化還需要頁表來完成操作,頁表也是同樣重要的哦。這部分內核需要理解的東西也就在這邊了,把虛擬地址,頁表,物理地址弄明白。理解這些對下部分內容也是非常重要的。如果你想要研究,是很容易理解的,出於篇幅限制,不講解那部分內容。其他的就是隻要掌握內存分配的接口,在你的驅動程序中要用到這樣的接口。

當然如果你覺得MMU真的麻煩,那就不去看它了。MMU對應用程序來說完全是屏蔽的。如果你覺得沒必要了解,那就不瞭解吧。

內核採用夥伴系統管理內存,至於夥伴系統,大家可以自己瞭解。現在就稱它爲分配器吧,它對外提供一個接口函數:要用內存就調用我吧,不要管我是這麼分給你的了。當然這一切都必須由內核開發人員提前做好。所以我們這邊只需要了linux操作系統是如何組織和安排內存的,然後就是要會用它們提供的請求內存分配接口。

這邊還有一個重要的東西,那就是slab分配器。它建立在夥伴系統之上,在這緩衝者內核常用數據結構。這就大大提高了某些大型的數據結構的分配速度。比如在進程管理我們提過的task_struct,就在這邊分配得到。同樣slab分配器模塊也給內核提供了相應的接口,如果內核要用,就請調用相應的接口吧。

還有一個地方需要注意了,那就是內核棧非常小,一般陪分配爲8KB。不像用戶空間的棧,可以動態的增長。如果你在編寫你的內核程序中(大多部分是驅動程序),請儘量不要靜態分配。也就是局部變量,當使用大型的數據結構的時候,爲了防止可憐的內核態棧溢出,請儘量選擇動態分配內存。

如果你理解了MMU,消化了虛擬地址和物理地址的關係,知道了頁表的作用和位置,那麼這章是不是還是蠻簡單的?所以這部分的重點就是請了解內存分配的接口,以後你總會用的到的。

這邊爲大家展示一幅關於內存分配的結構圖,對照這個圖,有助於理解這兩部分的內容。

 

 

·進程地址空間

這一部分內容全部是圍繞着虛擬地址來講訴的。由於有了MMU之後,帶來了虛擬地址的這樣一個概念。應用程序使用的地址,變爲了虛擬的地址。我們知道虛擬地址有3GB空間的大小,應用程序在於不用擔心內存太小的問題了。如果你還問:要是我的物理內存只有2GB怎麼辦呢?那顯然就是你對虛擬地址和進程地址空間的知識瞭解不多了,那麼本部分的內容就是爲了解決這個問題。

由於有了虛擬地址的出現,程序只有在鏈接加載的時候纔給分配虛擬地址,並建立到物理內存的映射。系統中每個進程都有3GB的虛擬地址空間可以使用,但是隻有當進程確確實實的需要內存的時候,內核纔會給進程分配內存。

系統中每個進程都會有一個叫做mm_struct的內存描述符,它表示進程的地址空間,存放了與進程的地址空間有關的全部信息。其中有一個很重要的成員就是虛擬內存區域vm_area_struct(VMA)。那麼這個VMA代表什麼呢?它代表着進程的一塊虛擬內存空間,在這個結構體裏面會有具體的虛擬地址空間的起始地址。VMA就是代表進程不同的虛擬地址空間區域。在這邊好多書上都會例舉一個程序,然後把程序所有的地址空間例舉出來,在那你會對我所講的東西弄的清清楚楚,雖然你現在可能一頭霧水。你會發現,不同的區域都有它存在的理由,存在的價值,所有的VMA合到了一起組成了一個進程的所有地址空間。

這邊的確是有點難懂,不過一旦你弄明白就會發現,哇,原來是這樣的,真的很神奇。要慢慢理清思路,首先你要知道進程中用到的都是虛擬的地址。然後你會想要一個進程需要一個代碼段,一個數據段,一個bss段,一個用戶棧空間,一個堆空間,還有呢?還有庫函數和動態鏈接程序(可能這邊讓你費解,現在不用管它,以後你會明白的)所需要的代碼段,數據段,bss段。那麼我告訴你,如果進程需要上面講訴的那樣的段或者堆棧空間,那麼都會有一個VMA與之對應。然後如果進程要增加內存,或者真正需要內存的操作了,而且這時候進程還沒有物理內存可以操作。那麼它就會產生一個缺頁異常,內核收到這個消息之後,會給進程分配一塊真正的物理空間,並使這塊物理內存映射到VMA中,那麼進程你就可以使用了。內核對於內存的分配,永遠是需要時才分配。看到這邊你再對照上面的那圖圖,是不是明白了什麼呢。

那麼進程是怎麼把虛擬地址轉化到對應的物理地址的呢?那就要用到頁表啦。所以你可以想象,每個進程一定都會有與之對於的頁表項。有這樣的頁表項,使得MMU會正確的安裝進程的虛擬地址找到它相應的物理地址。

沒有頭暈吧?書讀百遍,其義自見。

 

·系統調用 

相比之前所講訴的內容,系統調用就相對簡單些了。前面介紹了,系統調用是進程進入內核空間的一種方法,稍後我們會接觸到第二種進入內核的方式:中斷。前面的內容已經介紹過爲什麼要進行系統調用了:出於安全的考慮,進程不能直接調用內核函數或者訪問硬件。那麼對於系統調用,我感覺大家掌握一點就可以了,那就是進出內核態的過程和路徑。至於說會編寫自己的系統調用就沒有太大的必要了。

在我們研究進出內核態的過程和路徑的問題,先介紹一個重要的東西,那就是C函數庫。應用程序員編程是大多不必要了解內核的細節,他們只需要瞭解C庫給他們提供的函數就可以了。這就是Unix編程的一大特色風格,“提供機制而不是策略”。就是說只需要提供給上層什麼樣的功能(機制),而上層完成不需要關心這是如何實現這些功能的(策略)。還是那個比方,你要在屏幕輸出一串字符串,那麼你在你的程序中會調用printf()函數輸出你要輸出的內容。那麼你可想過C庫幫你做了什麼了嗎?當我們調用C庫來幫我們完成某些功能,C庫幫我們做了封裝,然後C庫裏面相應的例程幫我們調用了相應的系統調用完成了某些功能。還是這個比方,在C庫調用了相應的封裝例程之後調用的是 write()系統調用,進入內核。

那麼我們現在就來看進程進出內核態的過程和路徑的問題。首先我們知道:進程不可以直接調用內核函數,但是最終處理器還是從用戶空間進入了內核空間,那這是怎麼實現的呢?C庫幫我做了一些事情之後,還是調用了系統調用。在編譯彙編程序的時候,編譯器會幫我們在這邊放置一條軟中斷指令。然後引發軟中斷異常實現的:通過引發一起異常來促使系統切換到內核態去執行異常處理程序。這裏就是關鍵:從用戶空間進入了內核空間。如果你還不明白,那麼我就來稍微解釋下吧:

當產生一個異常的時候,程序計數器PC就會跳轉到一個固定的地址。然後內核會在這個地方放置相應系統調用服務程序。然後在這個系統調用服務程序中,內核通過設置一些寄存器,改變處理器模式,使系統進入了內核態。現在我們已經在內核空間啦,你反應過來了吧。

那麼還有一個問題:那就是內核怎麼知道她要做什麼呢?所以這邊引出一個非常重要的東西,叫系統調用號。這個號是由內核開發官方人員放出的,如果你要用,請申請。這個系統調用號和系統調用一一對應,編譯器和內核都知道這個對應關係。在對程序進行編譯的時候,編譯器會把這個號碼放到一個寄存器當中。接下來當發生系統調用的時候,在內核的系統調用服務程序中,檢查這個號碼。這個號碼就告訴了內核,進程想要請求的是哪種服務。然後內核查看系統調用表 sys_call_table,找到所調用的內核函數的入口地址。接着,就調用該函數,完成相應的系統調用服務。這時我們稱:內核代表應用程序來執行該系統調用。等返回後,做一些系統檢查最後返回到用戶空間(這邊的檢查就是進程調度部分所說的用戶搶佔發生的時機)。我們又回到了用戶空間啦。

那麼到現在你明白系統調用的過程了吧。

 

·中斷異常

中斷異常也是一種進入內核態的方法之一,不過這個方法是不受程序所控制的,既我們無法預知和控制中斷的到來。所以在中斷的到來之前,我們必須做好準備。中斷和系統調用一樣,都會進入核心態,而且他們使用了一個同樣的辦法。那就是PC會跳到一個固定的地址,那麼內核就會在那個地址事先安放一個跳轉地址,這個地址跳轉到中斷服務程序的入口處。那麼當中斷髮生以後,一切不可知的內容變爲可知:內核永遠知道代碼的執行流程了。

那麼中斷處理程序來自什麼地方呢?那我們就要看中斷來自什麼地方了。基本上所有的硬件只要與處理器通信,都會產生中斷。這邊拿鍵盤做比方,如果你按下一個按鍵,那麼鍵盤硬件會產生一箇中斷。這個中斷信號是一個電信號,處理器會檢查是否有中斷的到來了。如果它發現中斷來了,那麼它會自動的跳轉到一個固定的地址,而那個地址早已經被內核安排好了相應的內容了。進入了中斷服務程序的入口之後,入口程序會檢查中斷號(原理與系統調用號差不多),然後選擇相應的中斷服務程序來處理這個中斷。

所以這樣看來,固定的硬件都會有自己的中斷服務程序。實時也是這樣,當一個硬件需要中斷的時候,那麼在硬件相應的驅動程序中就會含有該硬件的中斷服務程序,只需要在驅動程序中註冊好硬件的中斷服務程序就可以了。在2.6以後的內核中,中斷有了自己的中斷棧,以前都是用當前進程的內核棧。這樣做可以使中斷服務程序可以安心的使用自己的棧了。在執行中斷服務的時候,是不允許發生阻塞,中斷上下文中的代碼應當迅速簡潔,因爲它打算了其他代碼的執行。

在中斷部分還有一個相當重要的內容,那就是所謂的中斷下半部分。什麼是中斷下半部分?爲什麼要有中斷下半部分?因爲我們知道中斷處理程序打斷了其他代碼的執行,所以它的執行應該短、平、快。但是很多時候我們會遇到這樣的情況,那就是在中斷服務程序中我們需要處理很多的東西,那怎麼辦呢?這個時候就引入了中斷下半部分機制。在中斷服務程序中完成一些與硬件有關的必要的操作,然後進入中斷下半部分。在中斷下半部分中,完成不是太急的工作。而且在中斷下半部分的時候,代碼是可以被中斷的。所以這就保證了系統處於中斷屏蔽的狀態時間儘可能的短,以此來提高系統的響應能力。中斷下半部分在驅動程序中用的非常多。如果你嘗試寫自己的驅動程序的時候,可以讀一下別人的中斷處理程序和相應的下半部分會有助於你的理解和編程。

 

·定時器中斷

講解完中斷處理之後,介紹一下操作系統的心臟——定時器中斷。它給系統提供了固定時間的中斷間隔,也就是每過一個固定的時間,定時器都會發生一次中斷。通過上面的學習,我們知道中斷的發生,可以檢查是否要發生進程的切換。所以在一定程度上,定時器中斷可以調高系統的相應時間,儘量減少了進程長時間得到不處理器運行的飢餓情況。但是如果定時器中斷頻率太高,那麼大部分處理器時間都花在了定時器中斷處理程序上了,這顯然是可以讓人接受的。所以這裏也存在一個平衡。那麼這個時間定爲多少呢?在X86個人計算機上,這個數字HZ被設置爲1000,也就是說每秒鐘,定時器會產生1000次中斷。ARM處理器目前被設置爲每秒100此,這個HZ的數值是可以自己修改的。

那麼定時器中斷具體來完成什麼樣的操作呢?在每秒HZ次的定時器中斷服務程序中,內核需要完成這樣幾個比較重要的工作:更新系統時鐘,執行到期的動態定時器,更新進程的時間片。

更新系統時鐘並使用牆上時間更新實時時鐘(RTC),來保證系統的時間準確。

執行到期的動態定時器,動態定時器是非常有用的一個東西,這個東西我們會經常接觸到。最常見的就是我們用所有的電子產品的時候,爲了省電,如果不去操作它,那麼過一段時間之後屏幕就會熄滅。好比這個熄滅屏幕的時間爲30秒,那麼30秒的計時開始是從你最後一次操作器件開始的。所以這就需要一個動態的定時器,一旦操作一次之後就會註冊一個動態定時器。在定時器到期(到期後屏幕會熄滅)之前,假如又有一次操作,那麼就銷燬前一個動態定時器,並創建一個新的動態定時器。定時器中斷處理程序,會去處理那些到期的動態定時器。所以動態定時器就是完成這樣的一個工作:幫助內核完成推後執行某些代碼。在看內核書籍的時候,大部分書籍在這部分內容會教你如何去動態註冊你的動態定時器:對於相應的接口,給定一個要延遲的時間,給定一個到期後執行的函數指針,你就完成了動態定時器的動態註冊。

更新進程的時間片,我們在進程調度那邊討論過了,在定時器中斷處理程序中會調用scheduler_tick(),來跟新進程的時間片。也就是減小進程的時間片,如果該函數發現進程的時間片給零,那麼它就會設置need_resched這個標誌。然後在定時器中斷處理程序返回的時候,會有一個固定的例程來查看這個標誌,如果它發現了這個標誌被設置了,就會去調用schedule()來選擇一個合適的進程運行。

定時器中斷就介紹到這邊,那麼你現在應該知道定時器中斷的重要性了吧,它比一般中斷出現的更加普遍。理解定時器中斷做了哪些事情非常有利於我們理解整個計算機系統的工作流程,不是嗎?

 

·內核同步

內核同步是保證整個軟件系統安全運行的一個非常重要的手段。我們一直說:引入了操作系統,使得訪問硬件變得安全可靠,軟件的執行也更加的安全。那麼這是一種什麼樣的安全呢?內核又是通過提供什麼樣的機制來保證了這些安全性的呢?在這部分,我們還會接觸到一個新的東西,那就是進程間通信,內核提供了一些機制,保證了進程間安全的通信。

先討論第一部分:爲什麼要有內核同步?內核同步是用來保護哪些內容的?在我們學習操作系統原理的時候,會接觸到這樣的一個內容,那就是臨界區:訪問和操作共享數據的代碼段。那我們要知道什麼是同步,同步就是避免併發和防止競爭條件,也就是要防止多個進程同時訪問同一個數據或者臨界區。這裏有一個非常經典的比方,那就是你的銀行卡里面有100元錢。然後你去自動取款機取錢的動作和你愛人用存摺小本在銀行裏取錢這個動作同時發生了,你們都想取80元錢,那麼在這個時候會發生什麼情況呢?你會都會取到80元嗎?那銀行絕對會保證不會讓這樣的事情發生的。在這個例子裏面,存款就是數據,是取錢這個動作的代碼要操作的數據。所以這段代碼就是臨界區,我們保證同步,就是爲了防止這樣的事情發生:多個進程同時進入了臨界區。內核中有非常多的地方是需要保護的,防止多個進程同時訪問,你現在明白爲什麼會需要內核同步了吧。

讓我們在仔細理解下上面的例子,我們爲了達到同步,保護的是什麼東西?是代碼嗎?在linux內核中,提供了鎖的機制,是給數據上鎖,而不是代碼上鎖。一旦上鎖就可以保證同一時間只可以有一個進程訪問加鎖的內容。那我們現在理解下,爲什麼是給數據加鎖而不是代碼?還有可能有很多讀者在這邊並不明白數據指的是什麼,我認爲那是因爲你對軟件方面的知識還不是太瞭解。我當初在接觸到這邊的時候,也對數據產生了一些迷惑。不過我相信通過一段時間的學習,你最終會明白這些數據到底可以是哪些了。其實這些數據就是內核的一些全局變量,或者一些重要的數據結構。如果當一段代碼要對一個非常重要而且不能被同時訪問的數據結構的某個數據操作的時候,內核就會首先對這段數據進行加鎖,保證了現在只有我這段代碼可以訪問你這堆數據。其他人要訪問,必須等待。我們在強調一下:保護是保護的數據,而不是代碼。

現在看第二個問題:內核提供什麼樣的機制來保證同步。這部分內容在每本內核書籍中都會講解的很詳細,這些機制是非常有效和有用的。當你在編寫驅動程序的時候應該會用到。其實我們已經接觸到了一個,那就是信號量與等待隊列。它們可以保證了對一個資源的安全訪問。

然後就是關於進程間通信(IPC)的相關知識。前面介紹過,內核對進程提供了虛擬的進程地址空間,所以進程們看到的都只有自己的地址空間。進程會認爲整個系統中只有自己一個進程在運行,那麼他們就是看不到其他的進程的。所以當我們設計應用程或者設備驅動的時候,可能會遇到這樣的問題:那就是進程需要和另一個進程通信。這個時候我們就要用到內核所給我們提供的IPC機制。IPC機制會提供給我們相應的接口,你只需要通過系統調用,然後就可以使用這些機制了。同樣,你並不需要知道這些機制的內部實現原理(對於一般應用程序開發來說),但是你卻達到了和進程通信的目的。我們要知道內核給我們提供了哪些進程間通信的機制,並掌握其中的一兩種。

那麼你現在應該清楚爲什麼要有內核同步機制了吧。你可以試着去了解他們的實現,不過這相當費力,不過蠻有意思的。如果你能做到了解那些接口,並知道什麼時候該用這些接口了,那也可以了。

 

·虛擬文件系統

下面介紹內核中相當重要的一個部分,那就是虛擬文件系統(VFS)。如果你沒有把內核的結構圖記住,那麼久請再回過去看一下吧,請看一下內核中虛擬文件系統 VFS所處的位置。它的位置是在系統調用的接口之下,因爲很大一部分的系統調用需要用到文件系統的內容,至於爲什麼下面講解。然後它又被放置在驅動程序之上,因爲你也許聽說過:linux中所有的硬件都被當成了文件來看待、來處理。所以當系統調用要用到硬件的時候,大部分都會進入VFS,由VFS來幫你完成那些硬件的操作。如果你曾經做過linux程序設計,那你現在一定會聯想到什麼了。既然我們說過了linux把所以的硬件都看出了文件,那麼對硬件的操作就變成了對文件的操作(永遠記得那句話:提供機制而不是策略。當你全心全意的做應用程序開發的時候,請直接去用接口,而不要去關心下面是如何實現的)。內核已經給我們屏蔽了所有的底層的操作,在我們只在意怎麼去用的時候,不必過多的關心內核是如何實現這樣的屏蔽和封裝的。

既然內核對所以的硬件都使用了相同的屏蔽封裝操作,所以我們就可以訪問不同的介質的文件系統。因爲內核(更確切的說是虛擬文件系統)對他們做了封裝,然後VFS對內核提供相應的接口,然後在內核中我們就可以使用VFS提供的文件接口。如果應用程序也要使用,那麼內核還要給他分配一個系統調用,然後應用程序通過系統調用,再去調用相應的接口,完成對不同硬件的操作。

當我們對對硬件操作的時候,其實已經是轉化到了對文件的操作。這個時候你只是需要對文件進行操作就可以了。所以當你要操作硬件的時候就要像操作文件那樣先打開文件,你會用到open()這個系統調用,這個系統調用會按照我上面所說的那個路徑,一步一步,最後訪問到了相應的硬件。

虛擬文件系統的實現用了面向對象的編程思想,不過爲了效率起見,還是用了過程式的C語言來實現了面向對象的程序結構。也就是把一個對象的各種特性和狀態,以及對這個對象的操作都封裝到了一起。

這個部分還是需要讀者去看相關的書籍的。這邊會涉及到4個非常重要的結構。你可能要花點心思去了解其中相當重要的幾個結構和部分。這對驅動程序設計是有非常大的幫助的。驅動程序,我們下面就會去接觸到它。

 

·驅動程序

我們現在來看一個非常重要的內容,那就是設備驅動。在內核源碼中,設備驅動佔去了相當大的一部分。其實驅動程序已經不屬於內核的內容了,爲什麼這樣說呢?因爲設備驅動程序都是基於內核提供的各種不同的機制實現的,驅動程序本身只調用內核提供的函數。所以這也是驅動程序也被稱之爲內核中的“應用程序”的原因。那爲什麼我們還要把驅動程序放到內核中講解呢?原因是因爲設備驅動運行在內核空間(如你所猜的那樣,爲了出於安全的考慮)。我們在看下關於內核模塊結構圖的圖,你會發現,驅動之上是VFS。所以當我們訪問到硬件的時候,我們是通過虛擬文件系統訪問的。當然內核已經做了相應的屏蔽和封裝,你可以不用關心實現原理了。

因爲內核的開發並不是很容易的,當然也不需要我們去開發。那我們唯一可以做的事情就是編寫設備驅動程序了。我是學習嵌入式系統開發的。這也是我當初爲什麼要下決心研究內核的原因,我想了解計算機系統工作原理,然後能編寫設備驅動。學到現在我感覺我對已經有了內核的功底,那我就可以試着找一些驅動程序的資料來看,然後開發自己的設備驅動。這個能力在嵌入式系統設計的底層開發中是相當重要的。

編寫自己的設備驅動程序,我們要了解設備驅動到底用了哪些內核所提供的機制,利用了這些機制,來實現了一個什麼樣的功能。當然在內核往設備程序轉的時候,有個非常需要注意的那就是這個時候需要接觸到大量關於硬件的知識了。需要能看芯片的芯片手冊,看懂時序圖,然後找到一種最合適的辦法把芯片的功能展示出來,提供給應用程序。

這邊有兩本不錯的書可以推薦下,一本是《Linux Device Drivers》(中文名:《linux設備驅動程序》,目前有第三版了),還要一本書叫《Linux設備驅動開發詳解》(第二版)。如果你想做嵌入式系統的底層驅動程序開發,可以看看這兩本書。

此部分大部分數據都會講解如何編寫並註冊一個自己的設備驅動程序,一次來加深對設備驅動的理解。如果你理解了設備驅動的一個程序流程,或者說是應用程序訪問硬件的一個流程你能明白,那就可以了。硬件的中斷處理程序也是在設備驅動程序中的,需要被註冊到內核中,然後當發生中斷之後,內核找到相應的中斷服務程序。

 

·網絡

現在介紹簡單我所講述的內核中的最後一個模塊,那就是關於網絡。網絡設備是唯一一個沒有用到虛擬文件系統的硬件。這是由於網絡的層次性格結構造成的。不過內核還是對網絡做了相應的處理,使用了socket套接字。如果你想要網絡編程,那麼就使用內核提供的socket接口吧。並按照步驟建立連接之後,你就可以用 read()和write(),接收和發送數據。

 

 

 

以上就是全部的內容了,花了我整整3天的時間才完全寫好。因爲內容太長,並沒有時間檢查修改了。內容全都是對內核的理解和看法,也是一個學習過程的總結。如果你想學習內核,希望對你有幫助。

 

 

注:由於這是本人第一次寫這種半學術性的文章,而且本人並沒有花時間去檢查和修改。所以我想文章中應該會有一些不正確或者表達不明確的內容。如果你看到了,請您諒解。當然如果你對這篇文章或者對內核的學習有一些想法,我們可以交流交流。我在人人社區建立了一個小組叫:linux kernel。可惜那個社區好像不方便搜索小組,所以歡迎大家加我校內,進入我的內核學習社區,我在那邊分享了很多東西。你這樣這樣找到我:南京郵電大學 王偉。

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