詳解Linux 2.6核心新變化(2)(轉)

worldblog發表於2007-08-10
詳解Linux 2.6核心新變化(2)(轉)[@more@]

  虛擬記憶體的變化

  從虛擬記憶體的角度來看,新核心融合了 Rik van Riel 的 r-map (反向對映,reverse mapping)技術,將顯著改善虛擬記憶體 在一定程度負載下的效能。

  為了理解反向對映技術,讓我們來首先簡單瞭解 Linux 虛擬記憶體系統的一些基本原理。

  Linux 核心工作於虛擬記憶體模式:每一個虛擬頁對應一個相應的系統記憶體的物理頁。虛擬頁和物理頁之間的地址轉換由硬體的頁表來完成。對於一個特定的虛擬頁,根據一條頁表記錄可以找到對應的物理頁,或者是頁無法找到的提示(說明存在一個頁錯誤)。但是這種"虛擬到物理"的頁對映不是總是一一對應的:多個虛擬頁(被不同的程式共享的頁)有可能指向同一個物理頁。在這種情況下,每個共享程式的頁記錄將有指向對應物理頁的對映。如果有類似這樣的情況,當核心想要釋放特定的物理頁時,事情會變得複雜,因為它必須遍歷所有的程式頁表記錄來查詢指向這個物理頁的引用;它只能在引用數達到0時才能釋放這個物理頁,因為它沒有別的辦法可以知道是不是還存在實際指向這個頁的引用。這樣當負載較高時會讓虛擬記憶體變得非常慢。

  反向地址對映補丁透過在結構頁引入一個叫做 pte_chain 的資料結構(物理頁結構)來解決這一問題。pte_chain 是一個指向頁的 PTE 的簡單連結列表,可以返回特定的被引用頁的 PTE 列表。頁釋放一下子變得非常簡單了。 不過,在這種模式中存在一個指標開銷。系統中的每一個結構頁都必須有一個額外的用於 pte_chain 的結構。在一個256M記憶體的系統中,有64K個物理頁,這樣就需要有 64KB * (sizeof(struct pte_chain)) 的記憶體被分配用於 pte_chain 的結構??一個很可觀的數字。

  有一些可以解決這個問題的技術,包括從結構頁中刪掉 wait_queue_head_t 域(用於對頁的獨佔訪問)。因為這個等待佇列極少用到,所以在 rmap 補丁中實現了一個更小的佇列,透過雜湊佇列來找到正確的等待佇列。

  儘管如此,rmap 的效能??尤其是處於高負載的高階系統??相對於2.4核心的虛擬記憶體系統還是有了顯著的提高。

  Linux 2.6的驅動程式移植

  2.6核心給驅動程式開發人員帶來了一系列非常有意義的變化。本節重點介紹將驅動程式從2.4核心移植到2.6核心的一些重要方面。

  首先,相對於2.4來說,改進了核心編譯系統,從而獲得更快的編譯速度。加入了改進的圖形化工具:make xconfig(需要Qt庫)和make gconfig(需要GTK庫)。

  以下是2.6編譯系統的一些亮點:

  當使用make時自動建立 arch-zImage 和模組

  使用 make -jN 可以進行並行的 make

  make 預設的不是冗餘方式(可以透過設定 KBUILD_VERBOSE=1 或者使用 make V=1來設定為冗餘方式)

  make subdir/ 將編譯 subdir/ 及其子目錄下的所有檔案

  make help 將提供 make 目標支援

  在任何一個階段都不需要再執行 make dep

  核心模組載入器也在2.5中完全被重新實現,這意味著模組編譯機制相對於2.4有了很大不同。需要一組新的模組工具來完成模組的載入和?載 (他們的下載連結可以在參考資料中找到),原來的2.4所用的 makefile 在2.6下不能再用。

  新的核心模組載入器是由 Rusty Russel 開發的。它使用核心編譯機制,產生一個 .ko(核心目標檔案,kernel object)模組目標檔案而不是一個 .o 模組目標檔案。核心編譯系統首先編譯這些模組,並將其連線成為 vermagic.o。這一過程在目標模組建立了一個特定部分,以記錄使用的編譯器版本號,核心版本號,是否使用核心搶佔等資訊。

  現在讓我們來看一個例子,分析一下新的核心編譯系統如何來編譯並載入一個簡單的模組。這個模組是一個“hello world”模組,程式碼和2.4模組程式碼基本類似,只是 module_init 和 module_exit 要換成 init_module 和 cleanup_module (核心2.4.10模組已經使用這種機制)。這個模組命名為 hello.c,Makefile 檔案如下:

  清單 3. 驅動程式 makefile 檔案示例

  KERNEL_SRC = /usr/src/linux

    SUBDIR = $(KERNEL_SRC)/drivers/char/hello/

    all: modules

obj-m := module.o

     hello-objs := hello.o

EXTRA_FLAGS += -DDEBUG=1

modules:

      $(MAKE) -C $(KERNEL_SRC) SUBDIR=$(SUBDIR) modules

  makefile 檔案使用核心編譯機制來編譯模組。編譯好的模組將被命名為 module.ko,並透過編譯 hello.c 和連線 vermagic 而獲得。KERNEL_SRC 指定核心原始檔所在的目錄,SUBDIR 指定放置模組的目錄。EXTRA_FLAGS 指定了需要給出的編譯期標記。

  一旦新模組(module.ko)被建立,它可以被新的模組工具載入或?載。2.4中的原有模組工具不能用來載入或?載2.6的核心模組。這個新的模組載入工具會盡量減少在一個裝置仍在使用的情況下相應的模組卻被?載的衝突發生,而是在確認這些模組已經沒有任何裝置在使用後再?載它。產生這種衝突的原因之一是模組使用計數是由模組程式碼自己來控制的(透過MOD_DEC/INC_USE_COUNT)。

  在2.6中,模組不再需要對引用計數進行加或減,這些工作將在模組程式碼外部進行。任何要引用模組的程式碼都必須呼叫 try_module_get(&module),只有在呼叫成功以後才能訪問那個模組;如果被呼叫的模組已經被?載,那麼這次呼叫會失敗。相應的,可以透過使用 module_put() 來釋放對模組的引用。

  記憶體管理的變化

  在2.5的開發過程中,加入了記憶體池,以滿足無間斷地進行記憶體分配。其思想是預分配一個記憶體池,並保留到真正需要的時候。一個記憶體池由 mempool_create() 呼叫來建立(應該包含標頭檔案 linux/mempool.h)。

  mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,

  mempool_free_t *free_fn, void *pool_data);

  在這裡 min_nr 是需要預分配物件的數目,alloc_fn 和 free_fn 是指向記憶體池機制提供的標準物件分配和回收例程的指標。他們的型別是:

  typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);

  typedef void (mempool_free_t)(void *element, void *pool_data);

  pool_data 是分配和回收函式用到的指標,gfp_mask 是分配標記。只有當 __GFP_WAIT 標記被指定時,分配函式才會休眠。

  在池中分配和回收物件是由以下程式完成的:

  void *mempool_alloc(mempool_t *pool, int gfp_mask);

  void mempool_free(void *element, mempool_t *pool);

  mempool_alloc() 用來分配物件;如果記憶體池分配器無法提供記憶體,那麼就可以用預分配的池。

  系統使用 mempool_destroy() 來回收記憶體池。

  除了為記憶體分配引入了記憶體池之外,2.5核心還引入了三個用於常規記憶體分配的新的GFP標記,它們是:

  __GFP_REPEAT -- 告訴頁分配器盡力去分配記憶體。如果記憶體分配失敗過多,應該減少這個標記的使用。

  __GFP_NOFAIL -- 不能出現記憶體分配失敗。這樣,由於呼叫者被轉入休眠狀態,可能需要一段比較長的時間才能完成分配,呼叫者的需求才能得到滿足。

  __GFP_NORETRY -- 保證分配失敗後不再重試,而向呼叫者報告失敗狀態。

  除了記憶體分配的變化以外,remap_page_range()呼叫——用來對映頁到使用者空間——也經過了少量修改。相對於2.4來說,現在它多了一個引數。虛擬記憶體區域(VMA)指標要作為第一個引數,然後是四個常用的引數(start,end,size 和 protection 標記)。

  工作佇列介面

  工作佇列介面是在2.5的開發過程中引入的,用於取代任務佇列介面(用於排程核心任務)。每個工作佇列有一個專門的執行緒,所有來自執行佇列的任務在程式的上下文中執行(這樣它們可以休眠)。驅動程式可以建立並使用它們自己的工作佇列,或者使用核心的一個工作佇列。工作佇列用以下方式建立:

  struct workqueue_struct *create_workqueue(const char *name);

  在這裡 name 是工作佇列的名字。

  工作佇列任務可以在編譯時或者執行時建立。任務需要封裝為一個叫做 work_struct 的結構體。在編譯期初始化一個工作佇列任務時要用到:

  DECLARE_WORK(name, void (*function)(void *), void *data);

  在這裡 name 是 work_struct 的名字,function 是當任務被排程時呼叫的函式,data 是指向那個函式的指標。

  在執行期初始化一個工作佇列時要用到:

  INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);

  用下面的函式呼叫來把一個作業(一個型別為work_struct 結構的工作佇列作業/任務)加入到工作佇列中:

  int queue_work(struct workqueue_struct *queue, struct work_struct *work);

  int queue_delayed_work(struct workqueue_struct *queue, struct work_struct

  *work, unsigned long delay);

  在queue_delay_work()中指定 delay,是為了保證至少在經過一段給定的最小延遲時間以後,工作佇列中的任務才可以真正執行。

  工作佇列中的任務由相關的工作執行緒執行,可能是在一個無法預期的時間(取決於負載,中斷等等),或者是在一段延遲以後。任何一個在工作佇列中等待了無限長的時間也沒有執行的任務可以用下面的方法取消:

  int cancel_delayed_work(struct work_struct *work);

  如果當一個取消操作的呼叫返回時,任務正在執行中,那麼這個任務將繼續執行下去,但不會再加入到佇列中。清空工作佇列中的所有任務使用:

  void flush_workqueue(struct workqueue_struct *queue);

  銷燬工作佇列使用:

  void destroy_workqueue(struct workqueue_struct *queue);

  不是所有的驅動程式都必須有自己的工作佇列。驅動程式可以使用核心提供的預設工作佇列。由於這個工作佇列由很多驅動程式共享,任務可能會需要比較長一段時間才能開始執行。為了解決這一問題,工作函式中的延遲應該保持最小或者乾脆不要。

  需要特別注意的是預設佇列對所有驅動程式來說都是可用的,但是隻有經過GP許可的驅動程式可以用自定義的工作佇列:

  int schedule_work(struct work_struct *work); -- 向工作佇列中新增一個任務

  int schedule_delayed_work(struct work_struct *work, unsigned long delay); -- 向工作佇列中新增一個任務並延遲執行

  當模組被?載時應該去呼叫一個 flash_scheduled_work() 函式,這個函式會使等待佇列中所有的任務都被執行。

  中斷例程的變化

  2.5的中斷處理程式內部已經經歷了許多變化,但是絕大部分對於普通的驅動程式開發者來說沒有影響。不過,還是有一些重要的變化會影響到驅動程式開發者。

  現在的中斷處理函式的返回程式碼是一個 irqreturn_t 型別。這個由 Linus 引入的變化意味著中斷處理程式告訴通用的 IRQ 層是否真的要中斷。這樣做是為了當中斷請求不斷到來時(原因是驅動程式偶然啟用了一箇中斷位或者硬體壞掉了),捕獲假中斷(尤其是在共享的PCI線上),而任何驅動程式對此都是無能為力的。在2.6中,驅動程式如果要從一個裝置上發出一箇中斷需要返回 IRQ_HANDLED,如果不是的話返回 IRQ_NONE。這樣可以幫助核心的 IRQ 層清楚地識別出哪個驅動程式正在處理那個特定的中斷。如果一箇中斷請求不斷到來而且沒有註冊那個裝置的處理程式(例如,所有的驅動程式都返回 IRQ_NONE),核心就會忽略來自那個裝置的中斷。預設情況下,驅動程式 IRQ 例程應該返回 IRQ_HANDLED,當驅動程式正在處理那個中斷時卻返回了 IRQ_NONE,說明存在 bug。新的中斷處理程式可能是類似於這樣:

  清單 4. 2.6的中斷處理程式虛擬碼

         irqreturn_t irq_handler(...) {

    ..

    if (!(my_interrupt)

    return IRQ_NONE; // not our interrupt

    ...

    return IRQ_HANDLED; // return by default

    }

  注意,cli(),sti(),save_flags()和 restor_flags() 是不贊成使用的方法。取而代之的是 local_save_flags() 和 local_irq_disable(),用來禁止所有的本地中斷(本處理器內的)。禁止所有處理器的中斷是不可能的。

  統一的裝置模型

  2.5開發過程中另一個最值得關注的變化是建立了一個統一的裝置模型。這個裝置模型透過維持大量的資料結構囊括了幾乎所有的裝置結構和系統。這樣做的好處是,可以改進裝置的電源管理和簡化裝置相關的任務管理,包括對以下資訊的追蹤:

  系統中存在的裝置,其所連線的匯流排

  特定情形下裝置的電源狀態

  系統清楚裝置的驅動程式,並清楚哪些裝置受其控制

  系統的匯流排結構:哪個裝置連線在哪個匯流排上,以及哪些匯流排互連(例如,USB和PCI匯流排的互連)

  裝置在系統中的類別描述(類別包括磁碟,分割槽等等)

  在2.5核心中,與裝置驅動程式相關的其他發展包括:

  不再使用 malloc.h。所有包含 (用於記憶體分配)的程式碼現在要替換為

  用於 x86 體系結構的 HZ 值增加到1000。引入了一個叫做 jiffies_64 的瞬間計算器,以避免由於 HZ 值的變化而引起瞬間變數的迅速溢位。

  引入了一個叫做 ndelay() 的新的延遲函式,允許納秒級的等待。

  引入了一個叫做 seqlock() 的新型別的鎖,用於鎖定小段的經常被訪問的資料(不是指標)。

  由於2.6核心可以搶佔,應該在驅動程式中使用 preempt_disable() 和 preempt_enable(),從而保護程式碼段不被搶佔(禁止 IRQ 同時也就隱式地禁止了搶佔)。

  在2.5中加入了非同步 I/O。這意味著使用者程式可以同時進行多個 I/O 操作,而不用等待它們完成。在字元驅動程式中引入了非同步 API。

  塊層在2.5的開發過程中經歷了大幅度的變化。這意味著原來用於2.4的塊裝置需要進行重新設計。

  在2.5中引入了sys檔案系統,它給出了系統的裝置模型的使用者空間描述。它掛載在 /sys 目錄下。

  結束語

  由於相對於2.4來說 Linux2.6發生了太多的變化,所以在 Linux 核心界有一種說法是新的釋出版本應該命名為3.0。Linus 將最終決定如何命名,官方可能將於2003年11月釋出官方版本。不管最終採用哪個版本號,相對於2.4來說,新的核心釋出版本在多種平臺和體系結構上效能將更快,可擴充套件性更強,更加穩定。

  Linus 已經邀請世界各地的測試人員來查詢 bug 和報告問題,並要求發行者提供2.6版本的下載。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-940299/,如需轉載,請註明出處,否則將追究法律責任。

相關文章