linux内核技术文章
生活随笔
收集整理的這篇文章主要介紹了
linux内核技术文章
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
Linux 引導過程內幕
從主引導記錄到第一個用戶空間應用程序的指導引導 Linux? 系統的過程包括很多階段。不管您是引導一個標準的 x86 桌面系統,還是引導一臺嵌入式
的 PowerPC? 機器,很多流程都驚人地相似。本文將探索 Linux 的引導過程,從最初的引導到啟動第一
個用戶空間應用程序。在本文介紹的過程中,您將學習到各種與引導有關的主題,例如引導加載程序、
內核解壓、初始 RAM 磁盤以及 Linux 引導的其他一些元素。
早期時,啟動一臺計算機意味著要給計算機喂一條包含引導程序的紙帶,或者手工使用前端面板地址/數
據/控制開關來加載引導程序。盡管目前的計算機已經裝備了很多工具來簡化引導過程,但是這一切并沒
有對整個過程進行必要的簡化。
各個步驟到底發生了什么。在整個過程中,參考一下內核源代碼可以幫助我們更好地了解內核源代碼樹
,并在以后對其進行深入分析。
概述
圖 1 是我們在 20,000 英尺的高度看到的視圖。
圖 1. Linux 引導過程在 20,000 英尺處的視圖
Linux 引導過程在 20,000 英尺處的視圖
中,這個位置在基本輸入/輸出系統(BIOS)中,它保存在主板上的閃存中。嵌入式系統中的中央處理單
元(CPU)會調用這個重置向量來啟動一個位于閃存/ROM 中的已知地址處的程序。在這兩種情況下,結
果都是相同的。因為 PC 提供了很多靈活性,BIOS 必須確定要使用哪個設備來引導系統。稍后我們將詳
細介紹這個過程。
當找到一個引導設備之后,第一階段的引導加載程序就被裝入 RAM 并執行。這個引導加載程序在大小上
小于 512 字節(一個扇區),其作用是加載第二階段的引導加載程序。當第二階段的引導加載程序被裝入 RAM 并執行時,通常會顯示一個動畫屏幕,并將 Linux 和一個可選
的初始 RAM 磁盤(臨時根文件系統)加載到內存中。在加載映像時,第二階段的引導加載程序就會將控
制權交給內核映像,然后內核就可以進行解壓和初始化了。在這個階段中,第二階段的引導加載程序會
檢測系統硬件、枚舉系統鏈接的硬件設備、掛載根設備,然后加載必要的內核模塊。完成這些操作之后
啟動第一個用戶空間程序(init),并執行高級系統初始化工作。
這就是 Linux 引導的整個過程。現在讓我們深入挖掘一下這個過程,并深入研究一下 Linux 引導過程
的一些詳細信息。系統啟動
系統啟動階段依賴于引導 Linux 系統上的硬件。在嵌入式平臺中,當系統加電或重置時,會使用一個啟
動環境。這方面的例子包括 U-Boot、RedBoot 和 Lucent 的 MicroMonitor。嵌入式平臺通常都是與引
導監視器搭配銷售的。這些程序位于目標硬件上的閃存中的某一段特殊區域,它們提供了將 Linux 內核
映像下載到閃存并繼續執行的方法。除了可以存儲并引導 Linux 映像之外,這些引導監視器還執行一定
級別的系統測試和硬件初始化過程。在嵌入式平臺中,這些引導監視器通常會涉及第一階段和第二階段
的引導加載程序。
提取 MBR 的信息
要查看 MBR 的內容,請使用下面的命令:# dd if=/dev/hda of=mbr.bin bs=512 count=1 # od -xa mbr.bin
這個 dd 命令需要以 root 用戶的身份運行,它從 /dev/hda(第一個 IDE 盤) 上讀取前 512 個字節
的內容,并將其寫入 mbr.bin 文件中。od 命令會以十六進制和 ASCII 碼格式打印這個二進制文件的內
容。
在 PC 中,引導 Linux 是從 BIOS 中的地址 0xFFFF0 處開始的。BIOS 的第一個步驟是加電自檢(POST
)。POST 的工作是對硬件進行檢測。BIOS 的第二個步驟是進行本地設備的枚舉和初始化。給定 BIOS 功能的不同用法之后,BIOS 由兩部分組成:POST 代碼和運行時服務。當 POST 完成之后,
它被從內存中清理了出來,但是 BIOS 運行時服務依然保留在內存中,目標操作系統可以使用這些服務
。
要引導一個操作系統,BIOS 運行時會按照 CMOS 的設置定義的順序來搜索處于活動狀態并且可以引導的
設備。引導設備可以是軟盤、CD-ROM、硬盤上的某個分區、網絡上的某個設備,甚至是 USB 閃存。通常,Linux 都是從硬盤上引導的,其中主引導記錄(MBR)中包含主引導加載程序。MBR 是一個 512?
字節大小的扇區,位于磁盤上的第一個扇區中(0 道 0 柱面 1 扇區)。當 MBR 被加載到 RAM 中之后
,BIOS 就會將控制權交給 MBR。
第一階段引導加載程序
MBR 中的主引導加載程序是一個 512 字節大小的映像,其中包含程序代碼和一個小分區表(參見圖 2)
。前 446 個字節是主引導加載程序,其中包含可執行代碼和錯誤消息文本。接下來的 64 個字節是分區
表,其中包含 4 個分區的記錄(每個記錄的大小是 16 個字節)。MBR 以兩個特殊數字的字節(0xAA55
)結束。這個數字會用來進行 MBR 的有效性檢查。
圖 2. MBR 剖析
MBR 剖析
主引導加載程序的工作是查找并加載次引導加載程序(第二階段)。它是通過在分區表中查找一個活動分區來實現這種功能的。當找到一個活動分區時,它會掃描分區表中的其他分區,以確保它們都不是活
動的。當這個過程驗證完成之后,就將活動分區的引導記錄從這個設備中讀入 RAM 中并執行它。
第二階段引導加載程序
次引導加載程序(第二階段引導加載程序)可以更形象地稱為內核加載程序。這個階段的任務是加載?
Linux 內核和可選的初始 RAM 磁盤。
GRUB 階段引導加載程序
/boot/grub 目錄中包含了 stage1、stage1.5 和 stage2 引導加載程序,以及很多其他加載程序(例如,CR-ROM 使用的是 iso9660_stage_1_5)。
在 x86 PC 環境中,第一階段和第二階段的引導加載程序一起稱為 Linux Loader(LILO)或 GRand?
Unified Bootloader(GRUB)。由于 LILO 有一些缺點,而 GRUB 克服了這些缺點,因此下面讓我們就
來看一下 GRUB。(有關 GRUB、LILO 和相關主題的更多內容,請參閱本文后面的 參考資料 部分的內容
。)
關于 GRUB,很好的一件事情是它包含了有關 Linux 文件系統的知識。GRUB 不像 LILO 一樣使用裸扇區
,而是可以從 ext2 或 ext3 文件系統中加載 Linux 內核。它是通過將兩階段的引導加載程序轉換成三
階段的引導加載程序來實現這項功能的。階段 1 (MBR)引導了一個階段 1.5 的引導加載程序,它可以
理解包含 Linux 內核映像的特殊文件系統。這方面的例子包括 reiserfs_stage1_5(要從 Reiser 日志
文件系統上進行加載)或 e2fs_stage1_5(要從 ext2 或 ext3 文件系統上進行加載)。當階段 1.5 的
引導加載程序被加載并運行時,階段 2 的引導加載程序就可以進行加載了。
當階段 2 加載之后,GRUB 就可以在請求時顯示可用內核列表(在 /etc/grub.conf 中進行定義,同時
還有幾個軟符號鏈接 /etc/grub/menu.lst 和 /etc/grub.conf)。我們可以選擇內核甚至修改附加內核
參數。另外,我們也可以使用一個命令行的 shell 對引導過程進行高級手工控制。
將第二階段的引導加載程序加載到內存中之后,就可以對文件系統進行查詢了,并將默認的內核映像和?
initrd 映像加載到內存中。當這些映像文件準備好之后,階段 2 的引導加載程序就可以調用內核映像
了。
內核
GRUB 中的手工引導
在 GRUB 命令行中,我們可以使用 initrd 映像引導一個特定的內核,方法如下:
grub> kernel /bzImage-2.6.14.2
[Linux-bzImage, setup=0x1400, size=0x29672e]
grub> initrd /initrd-2.6.14.2.img
[Linux-initrd @ 0x5f13000, 0xcc199 bytes]
grub> boot
Uncompressing Linux... Ok, booting the kernel.
如果您不知道要引導的內核的名稱,只需使用斜線(/)然后按下 Tab 鍵即可。GRUB 會顯示內核和?
initrd 映像列表。
當內核映像被加載到內存中,并且階段 2 的引導加載程序釋放控制權之后,內核階段就開始了。內核映
像并不是一個可執行的內核,而是一個壓縮過的內核映像。通常它是一個 zImage(壓縮映像,小于?
512KB)或一個 bzImage(較大的壓縮映像,大于 512KB),它是提前使用 zlib 進行壓縮過的。在這個
內核映像前面是一個例程,它實現少量硬件設置,并對內核映像中包含的內核進行解壓,然后將其放入
高端內存中,如果有初始 RAM 磁盤映像,就會將它移動到內存中,并標明以后使用。然后該例程會調用
內核,并開始啟動內核引導的過程。
當 bzImage(用于 i386 映像)被調用時,我們從 ./arch/i386/boot/head.S 的 start 匯編例程開始
執行(主要流程圖請參看圖 3)。這個例程會執行一些基本的硬件設置,并調用?
./arch/i386/boot/compressed/head.S 中的 startup_32 例程。此例程會設置一個基本的環境(堆棧等
),并清除 Block Started by Symbol(BSS)。然后調用一個叫做 decompress_kernel 的 C 函數(在?
./arch/i386/boot/compressed/misc.c 中)來解壓內核。當內核被解壓到內存中之后,就可以調用它了
。這是另外一個 startup_32 函數,但是這個函數在 ./arch/i386/kernel/head.S 中。
在這個新的 startup_32 函數(也稱為清除程序或進程 0)中,會對頁表進行初始化,并啟用內存分頁
功能。然后會為任何可選的浮點單元(FPU)檢測 CPU 的類型,并將其存儲起來供以后使用。然后調用?
start_kernel 函數(在 init/main.c 中),它會將您帶入與體系結構無關的 Linux 內核部分。實際上
,這就是 Linux 內核的 main 函數。
圖 3. Linux 內核 i386 引導的主要函數流程
Linux 內核 i386 引導的主要函數流程
通過調用 start_kernel,會調用一系列初始化函數來設置中斷,執行進一步的內存配置,并加載初始?RAM 磁盤。最后,要調用 kernel_thread(在 arch/i386/kernel/process.c 中)來啟動 init 函數,
這是第一個用戶空間進程(user-space process)。最后,啟動空任務,現在調度器就可以接管控制權
了(在調用 cpu_idle 之后)。通過啟用中斷,搶占式的調度器就可以周期性地接管控制權,從而提供
多任務處理能力。
在內核引導過程中,初始 RAM 磁盤(initrd)是由階段 2 引導加載程序加載到內存中的,它會被復制
到 RAM 中并掛載到系統上。這個 initrd 會作為 RAM 中的臨時根文件系統使用,并允許內核在沒有掛
載任何物理磁盤的情況下完整地實現引導。由于與外圍設備進行交互所需要的模塊可能是 initrd 的一
部分,因此內核可以非常小,但是仍然需要支持大量可能的硬件配置。在內核引導之后,就可以正式裝
備根文件系統了(通過 pivot_root):此時會將 initrd 根文件系統卸載掉,并掛載真正的根文件系統
。
decompress_kernel 輸出
函數 decompress_kernel 就是顯示我們通常看到的解壓消息的地方:
Uncompressing Linux... Ok, booting the kernel.
initrd 函數讓我們可以創建一個小型的 Linux 內核,其中包括作為可加載模塊編譯的驅動程序。這些
可加載的模塊為內核提供了訪問磁盤和磁盤上的文件系統的方法,并為其他硬件提供了驅動程序。由于
根文件系統是磁盤上的一個文件系統,因此 initrd 函數會提供一種啟動方法來獲得對磁盤的訪問,并
掛載真正的根文件系統。在一個沒有硬盤的嵌入式環境中,initrd 可以是最終的根文件系統,或者也可
以通過網絡文件系統(NFS)來掛載最終的根文件系統。
Init
當內核被引導并進行初始化之后,內核就可以啟動自己的第一個用戶空間應用程序了。這是第一個調用
的使用標準 C 庫編譯的程序。在此之前,還沒有執行任何標準的 C 應用程序。
在桌面 Linux 系統上,第一個啟動的程序通常是 /sbin/init。但是這不是一定的。很少有嵌入式系統
會需要使用 init 所提供的豐富初始化功能(這是通過 /etc/inittab 進行配置的)。在很多情況下,
我們可以調用一個簡單的 shell 腳本來啟動必需的嵌入式應用程序。
結束語
與 Linux 本身非常類似,Linux 的引導過程也非常靈活,可以支持眾多的處理器和硬件平臺。最初,加
載引導加載程序提供了一種簡單的方法,不用任何花架子就可以引導 Linux。LILO 引導加載程序對引導
能力進行了擴充,但是它卻缺少文件系統的感知能力。最新一代的引導加載程序,例如 GRUB,允許?
Linux 從一些文件系統(從 Minix 到 Reise)上進行引導。
========
Linux slab 分配器剖析
了解 Linux 內存管理的方式良好的操作系統性能部分依賴于操作系統有效管理資源的能力。在過去,堆內存管理器是實際的規范,
但是其性能會受到內存碎片和內存回收需求的影響?,F在,Linux? 內核使用了源自于 Solaris 的一種
方法,但是這種方法在嵌入式系統中已經使用了很長時間了,它是將內存作為對象按照大小進行分配。
本文將探索 slab 分配器背后所采用的思想,并介紹這種方法提供的接口和用法。
動態內存管理
內存管理的目標是提供一種方法,為實現各種目的而在各個用戶之間實現內存共享。內存管理方法應該
實現以下兩個功能:
最小化管理內存所需的時間
最大化用于一般應用的可用內存(最小化管理開銷)
內存管理實際上是一種關于權衡的零和游戲。您可以開發一種使用少量內存進行管理的算法,但是要花
費更多時間來管理可用內存。也可以開發一個算法來有效地管理內存,但卻要使用更多的內存。最終,
特定應用程序的需求將促使對這種權衡作出選擇。
每個內存管理器都使用了一種基于堆的分配策略。在這種方法中,大塊內存(稱為 堆)用來為用戶定義
的目的提供內存。當用戶需要一塊內存時,就請求給自己分配一定大小的內存。堆管理器會查看可用內
存的情況(使用特定算法)并返回一塊內存。搜索過程中使用的一些算法有 first-fit(在堆中搜索到
的第一個滿足請求的內存塊 )和 best-fit(使用堆中滿足請求的最合適的內存塊)。當用戶使用完內
存后,就將內存返回給堆。
這種基于堆的分配策略的根本問題是碎片(fragmentation)。當內存塊被分配后,它們會以不同的順序
在不同的時間返回。這樣會在堆中留下一些洞,需要花一些時間才能有效地管理空閑內存。這種算法通
常具有較高的內存使用效率(分配需要的內存),但是卻需要花費更多時間來對堆進行管理。
另外一種方法稱為 buddy memory allocation,是一種更快的內存分配技術,它將內存劃分為 2 的冪次
方個分區,并使用 best-fit 方法來分配內存請求。當用戶釋放內存時,就會檢查 buddy 塊,查看其相
鄰的內存塊是否也已經被釋放。如果是的話,將合并內存塊以最小化內存碎片。這個算法的時間效率更
高,但是由于使用 best-fit 方法的緣故,會產生內存浪費。
本文將著重介紹 Linux 內核的內存管理,尤其是 slab 分配提供的機制。
slab 緩存
Linux 所使用的 slab 分配器的基礎是 Jeff Bonwick 為 SunOS 操作系統首次引入的一種算法。Jeff?
的分配器是圍繞對象緩存進行的。在內核中,會為有限的對象集(例如文件描述符和其他常見結構)分
配大量內存。Jeff 發現對內核中普通對象進行初始化所需的時間超過了對其進行分配和釋放所需的時間
。因此他的結論是不應該將內存釋放回一個全局的內存池,而是將內存保持為針對特定目而初始化的狀
態。例如,如果內存被分配給了一個互斥鎖,那么只需在為互斥鎖首次分配內存時執行一次互斥鎖初始
化函數(mutex_init)即可。后續的內存分配不需要執行這個初始化函數,因為從上次釋放和調用析構
之后,它已經處于所需的狀態中了。
Linux slab 分配器使用了這種思想和其他一些思想來構建一個在空間和時間上都具有高效性的內存分配
器。
圖 1 給出了 slab 結構的高層組織結構。在最高層是 cache_chain,這是一個 slab 緩存的鏈接列表。
這對于 best-fit 算法非常有用,可以用來查找最適合所需要的分配大小的緩存(遍歷列表)。
cache_chain 的每個元素都是一個 kmem_cache 結構的引用(稱為一個 cache)。它定義了一個要管理
的給定大小的對象池。
圖 1. slab 分配器的主要結構
圖 1. slab 分配器的主要結構
每個緩存都包含了一個 slabs 列表,這是一段連續的內存塊(通常都是頁面)。存在 3 種 slab:
slabs_full
完全分配的 slab
slabs_partial
部分分配的 slab
slabs_empty
空 slab,或者沒有對象被分配
注意 slabs_empty 列表中的 slab 是進行回收(reaping)的主要備選對象。正是通過此過程,slab 所
使用的內存被返回給操作系統供其他用戶使用。
slab 列表中的每個 slab 都是一個連續的內存塊(一個或多個連續頁),它們被劃分成一個個對象。這
些對象是從特定緩存中進行分配和釋放的基本元素。注意 slab 是 slab 分配器進行操作的最小分配單
位,因此如果需要對 slab 進行擴展,這也就是所擴展的最小值。通常來說,每個 slab 被分配為多個
對象。
由于對象是從 slab 中進行分配和釋放的,因此單個 slab 可以在 slab 列表之間進行移動。例如,當
一個 slab 中的所有對象都被使用完時,就從 slabs_partial 列表中移動到 slabs_full 列表中。當一
個 slab 完全被分配并且有對象被釋放后,就從 slabs_full 列表中移動到 slabs_partial 列表中。當
所有對象都被釋放之后,就從 slabs_partial 列表移動到 slabs_empty 列表中。
slab 背后的動機
與傳統的內存管理模式相比, slab 緩存分配器提供了很多優點。首先,內核通常依賴于對小對象的分
配,它們會在系統生命周期內進行無數次分配。slab 緩存分配器通過對類似大小的對象進行緩存而提供
這種功能,從而避免了常見的碎片問題。slab 分配器還支持通用對象的初始化,從而避免了為同一目而
對一個對象重復進行初始化。最后,slab 分配器還可以支持硬件緩存對齊和著色,這允許不同緩存中的
對象占用相同的緩存行,從而提高緩存的利用率并獲得更好的性能。
回頁首
API 函數
現在來看一下能夠創建新 slab 緩存、向緩存中增加內存、銷毀緩存的應用程序接口(API)以及 slab?
中對對象進行分配和釋放操作的函數。
第一個步驟是創建 slab 緩存結構,您可以將其靜態創建為:
struct struct kmem_cache *my_cachep;
然后其他 slab 緩存函數將使用該引用進行創建、刪除、分配等操作。kmem_cache 結構包含了每個中央
處理器單元(CPU)的數據、一組可調整的(可以通過 proc 文件系統訪問)參數、統計信息和管理?
slab 緩存所必須的元素。
kmem_cache_create
內核函數 kmem_cache_create 用來創建一個新緩存。這通常是在內核初始化時執行的,或者在首次加載
內核模塊時執行。其原型定義如下:
struct kmem_cache *
kmem_cache_create( const char *name, size_t size, size_t align,
? ? ? ? ? ? ? ? ? ? ? ?unsigned long flags;
? ? ? ? ? ? ? ? ? ? ? ?void (*ctor)(void*, struct kmem_cache *, unsigned long),
? ? ? ? ? ? ? ? ? ? ? ?void (*dtor)(void*, struct kmem_cache *, unsigned long));
name 參數定義了緩存名稱,proc 文件系統(在 /proc/slabinfo 中)使用它標識這個緩存。 size 參
數指定了為這個緩存創建的對象的大小, align 參數定義了每個對象必需的對齊。 flags 參數指定了
為緩存啟用的選項。這些標志如表 1 所示。
表 1. kmem_cache_create 的部分選項(在 flags 參數中指定)
選項 說明
SLAB_RED_ZONE 在對象頭、尾插入標志,用來支持對緩沖區溢出的檢查。
SLAB_POISON 使用一種己知模式填充 slab,允許對緩存中的對象進行監視(對象屬對象所有,不過
可以在外部進行修改)。
SLAB_HWCACHE_ALIGN 指定緩存對象必須與硬件緩存行對齊。
ctor 和 dtor 參數定義了一個可選的對象構造器和析構器。構造器和析構器是用戶提供的回調函數。當
從緩存中分配新對象時,可以通過構造器進行初始化。
在創建緩存之后, kmem_cache_create 函數會返回對它的引用。注意這個函數并沒有向緩存分配任何內
存。相反,在試圖從緩存(最初為空)分配對象時,refill 操作將內存分配給它。當所有對象都被使用
掉時,也可以通過相同的操作向緩存添加內存。
kmem_cache_destroy
內核函數 kmem_cache_destroy 用來銷毀緩存。這個調用是由內核模塊在被卸載時執行的。在調用這個
函數時,緩存必須為空。
void kmem_cache_destroy( struct kmem_cache *cachep );
kmem_cache_alloc
要從一個命名的緩存中分配一個對象,可以使用 kmem_cache_alloc 函數。調用者提供了從中分配對象
的緩存以及一組標志:
void kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
這個函數從緩存中返回一個對象。注意如果緩存目前為空,那么這個函數就會調用 cache_alloc_refill?
向緩存中增加內存。 kmem_cache_alloc 的 flags 選項與 kmalloc 的 flags 選項相同。表 2 給出了
標志選項的部分列表。
表 2. kmem_cache_alloc 和 kmalloc 內核函數的標志選項
標志 說明
GFP_USER 為用戶分配內存(這個調用可能會睡眠)。
GFP_KERNEL 從內核 RAM 中分配內存(這個調用可能會睡眠)。
GFP_ATOMIC 使該調用強制處于非睡眠狀態(對中斷處理程序非常有用)。
GFP_HIGHUSER 從高端內存中分配內存。
kmem_cache_zalloc
內核函數 kmem_cache_zalloc 與 kmem_cache_alloc 類似,只不過它對對象執行 memset 操作,用來在
將對象返回調用者之前對其進行清除操作。
kmem_cache_free
要將一個對象釋放回 slab,可以使用 kmem_cache_free。調用者提供了緩存引用和要釋放的對象。
void kmem_cache_free( struct kmem_cache *cachep, void *objp );
kmalloc 和 kfree
內核中最常用的內存管理函數是 kmalloc 和 kfree 函數。這兩個函數的原型如下:
void *kmalloc( size_t size, int flags );
void kfree( const void *objp );
注意在 kmalloc 中,惟一兩個參數是要分配的對象的大小和一組標志(請參看 表 2 中的部分列表)。
但是 kmalloc 和 kfree 使用了類似于前面定義的函數的 slab 緩存。kmalloc 沒有為要從中分配對象
的某個 slab 緩存命名,而是循環遍歷可用緩存來查找可以滿足大小限制的緩存。找到之后,就(使用?
__kmem_cache_alloc)分配一個對象。要使用 kfree 釋放對象,從中分配對象的緩存可以通過調用?
virt_to_cache 確定。這個函數會返回一個緩存引用,然后在 __cache_free 調用中使用該引用釋放對
象。
其他函數
slab 緩存 API 還提供了其他一些非常有用的函數。 kmem_cache_size 函數會返回這個緩存所管理的對
象的大小。您也可以通過調用 kmem_cache_name 來檢索給定緩存的名稱(在創建緩存時定義)。緩存可
以通過釋放其中的空閑 slab 進行收縮。這可以通過調用 kmem_cache_shrink 實現。注意這個操作(稱
為回收)是由內核定期自動執行的(通過 kswapd)。
unsigned int kmem_cache_size( struct kmem_cache *cachep );
const char *kmem_cache_name( struct kmem_cache *cachep );
int kmem_cache_shrink( struct kmem_cache *cachep );
回頁首
slab 緩存的示例用法
下面的代碼片斷展示了創建新 slab 緩存、從緩存中分配和釋放對象然后銷毀緩存的過程。首先,必須
要定義一個 kmem_cache 對象,然后對其進行初始化(請參看清單 1)。這個特定的緩存包含 32 字節
的對象,并且是硬件緩存對齊的(由標志參數 SLAB_HWCACHE_ALIGN 定義)。
清單 1. 創建新 slab 緩存
static struct kmem_cache *my_cachep;
static void init_my_cache( void )
{
? ?my_cachep = kmem_cache_create(?
? ? ? ? ? ? ? ? ? "my_cache", ? ? ? ? ? ?/* Name */
? ? ? ? ? ? ? ? ? 32, ? ? ? ? ? ? ? ? ? ?/* Object Size */
? ? ? ? ? ? ? ? ? 0, ? ? ? ? ? ? ? ? ? ? /* Alignment */
? ? ? ? ? ? ? ? ? SLAB_HWCACHE_ALIGN, ? ?/* Flags */
? ? ? ? ? ? ? ? ? NULL, NULL ); ? ? ? ? ?/* Constructor/Deconstructor */
? ?return;
}
使用所分配的 slab 緩存,您現在可以從中分配一個對象了。清單 2 給出了一個從緩存中分配和釋放對
象的例子。它還展示了兩個其他函數的用法。
清單 2. 分配和釋放對象
int slab_test( void )
{
? void *object;
? printk( "Cache name is %s\n", kmem_cache_name( my_cachep ) );
? printk( "Cache object size is %d\n", kmem_cache_size( my_cachep ) );
? object = kmem_cache_alloc( my_cachep, GFP_KERNEL );
? if (object) {
? ? kmem_cache_free( my_cachep, object );
? }
? return 0;
}
最后,清單 3 演示了 slab 緩存的銷毀。調用者必須確保在執行銷毀操作過程中,不要從緩存中分配對
象。
清單 3. 銷毀 slab 緩存
static void remove_my_cache( void )
{
? if (my_cachep) kmem_cache_destroy( my_cachep );
? return;
}
回頁首
slab 的 proc 接口
proc 文件系統提供了一種簡單的方法來監視系統中所有活動的 slab 緩存。這個文件稱為?
/proc/slabinfo,它除了提供一些可以從用戶空間訪問的可調整參數之外,還提供了有關所有 slab 緩
存的詳細信息。當前版本的 slabinfo 提供了一個標題,這樣輸出結果就更具可讀性。對于系統中的每
個 slab 緩存來說,這個文件提供了對象數量、活動對象數量以及對象大小的信息(除了每個 slab 的
對象和頁面之外)。另外還提供了一組可調整的參數和 slab 數據。
要調優特定的 slab 緩存,可以簡單地向 /proc/slabinfo 文件中以字符串的形式回轉 slab 緩存名稱
和 3 個可調整的參數。下面的例子展示了如何增加 limit 和 batchcount 的值,而保留 shared?
factor 不變(格式為 “cache name limit batchcount shared factor”):
# echo "my_cache 128 64 8" > /proc/slabinfo
limit 字段表示每個 CPU 可以緩存的對象的最大數量。 batchcount 字段是當緩存為空時轉換到每個?
CPU 緩存中全局緩存對象的最大數量。 shared 參數說明了對稱多處理器(Symmetric MultiProcessing
,SMP)系統的共享行為。
注意您必須具有超級用戶的特權才能在 proc 文件系統中為 slab 緩存調優參數。
回頁首
SLOB 分配器
對于小型的嵌入式系統來說,存在一個 slab 模擬層,名為 SLOB。這個 slab 的替代品在小型嵌入式?
Linux 系統中具有優勢,但是即使它保存了 512KB 內存,依然存在碎片和難于擴展的問題。在禁用?
CONFIG_SLAB 時,內核會回到這個 SLOB 分配器中。更多信息請參看 參考資料 一節。
回頁首
結束語
slab 緩存分配器的源代碼實際上是 Linux 內核中可讀性較好的一部分。除了函數調用的間接性之外,
源代碼也非常直觀,總的來說,具有很好的注釋。如果您希望了解更多有關 slab 緩存分配器的內容,
建議您從源代碼開始,因為它是有關這種機制的最新文檔。 下面的 參考資料 一節提供了介紹 slab 緩
存分配器的參考資料,但是不幸的是就目前的 2.6 實現來說,這些文檔都已經過時了。
========
Linux 調度器內幕
內核中這個非常重要的組件的最新版本改進了可伸縮性Linux? 內核繼續不斷發展并采用新技術,在可靠性、可伸縮性和性能方面獲得了長足的發展。2.6 版本
的內核最重要的特性之一是由 Ingo Molnar 實現的調度器。這個調度器是動態的,可以支持負載均衡,
并以恒定的速度進行操作 —— O(1)。本文將介紹 Linux 2.6 調度器的這些屬性以及更多內容。
本文將回顧一下 Linux 2.6 的任務調度器及其最重要的一些屬性。在深入介紹調度器的詳細信息之前,
讓我們先來理解一下調度器的基本目標。
什么是調度器?
通常來說,操作系統是應用程序和可用資源之間的媒介。典型的資源有內存和物理設備。但是 CPU 也可
以認為是一個資源,調度器可以臨時分配一個任務在上面執行(單位是時間片)。調度器使得我們同時
執行多個程序成為可能,因此可以與具有各種需求的用戶共享 CPU。
調度器的一個重要目標是有效地分配 CPU 時間片,同時提供很好的用戶體驗。調度器還需要面對一些互
相沖突的目標,例如既要為關鍵實時任務最小化響應時間,又要最大限度地提高 CPU 的總體利用率。下
面我們來看一下 Linux 2.6 調度程序是如何實現這些目標的,并與以前的調度器進行比較。
回頁首
早期 Linux 調度器的問題
O-notation 的重要性
O-notation 可以告訴我們一個算法會占用多少時間。一個 O(n) 算法所需要的時間依賴于輸入的多少(
與 n 是線性關系),而 O(n^2) 則是輸入數量的平方。O(1) 與輸入無關,可以在固定的時間內完成操
作。
在 2.6 版本的內核之前,當很多任務都處于活動狀態時,調度器有很明顯的限制。這是由于調度器是使
用一個復雜度為 O(n) 的算法實現的。在這種調度器中,調度任務所花費的時間是一個系統中任務個數
的函數。換而言之,活動的任務越多,調度任務所花費的時間越長。在任務負載非常重時,處理器會因
調度消耗掉大量的時間,用于任務本身的時間就非常少了。因此,這個算法缺乏可伸縮性。
在對稱多處理系統(SMP)中,2.6 版本之前的調度器對所有的處理器都使用一個運行隊列。這意味著一
個任務可以在任何處理器上進行調度 —— 這對于負載均衡來說是好事,但是對于內存緩存來說卻是個
災難。例如,假設一個任務正在 CPU-1 上執行,其數據在這個處理器的緩存中。如果這個任務被調度到?
CPU-2 上執行,那么數據就需要先在 CPU-1 使其無效,并將其放到 CPU-2 的緩存中。
以前的調度器還使用了一個運行隊列鎖;因此在 SMP 系統中,選擇一個任務執行就會阻礙其他處理器操
作這個運行隊列。結果是空閑處理器只能等待這個處理器釋放出運行隊列鎖,這樣會造成效率的降低。
最后,在早期的內核中,搶占是不可能的;這意味著如果有一個低優先級的任務在執行,高優先級的任
務只能等待它完成。
回頁首
Linux 2.6 調度器簡介
2.6 版本的調度器是由 Ingo Molnar 設計并實現的。Ingo 從 1995 年開始就一直參與 Linux 內核的開
發。他編寫這個新調度器的動機是為喚醒、上下文切換和定時器中斷開銷建立一個完全 O(1) 的調度器
。觸發對新調度器的需求的一個問題是 Java? 虛擬機(JVM)的使用。Java 編程模型使用了很多執行線
程,在 O(n) 調度器中這會產生很多調度負載。O(1) 調度器在這種高負載的情況下并不會受到太多影響
,因此 JVM 可以有效地執行。
2.6 版本的調度器解決了以前調度器中發現的 3 個主要問題(O(n) 和 SMP 可伸縮性的問題),還解決
了其他一些問題?,F在我們將開始探索一下 2.6 版本的調度器的基本設計。
主要的調度結構
首先我們來回顧一下 2.6 版本的調度器結構。每個 CPU 都有一個運行隊列,其中包含了 140 個優先級
列表,它們是按照先進先出的順序進行服務的。被調度執行的任務都會被添加到各自運行隊列優先級列
表的末尾。每個任務都有一個時間片,這取決于系統允許執行這個任務多長時間。運行隊列的前 100 個
優先級列表保留給實時任務使用,后 40 個用于用戶任務(參見圖 1)。我們稍后將來看一下為什么這
種區別非常重要。
圖 1. Linux 2.6 調度器的運行隊列結構
Linux 2.6 調度器的運行隊列結構
除了 CPU 的運行隊列(稱為活動運行隊列(active runqueue))之外,還有一個過期運行隊列。當活
動運行隊列中的一個任務用光自己的時間片之后,它就被移動到過期運行隊列(expired runqueue) 中
。在移動過程中,會對其時間片重新進行計算(因此會體現其優先級的作用;稍后會更詳細地介紹)。
如果活動運行隊列中已經沒有某個給定優先級的任務了,那么指向活動運行隊列和過期運行隊列的指針
就會交換,這樣就可以讓過期優先級列表變成活動優先級的列表。
調度器的工作非常簡單:它在優先級最高的隊列中選擇一個任務來執行。為了使這個過程的效率更高,
內核使用了一個位圖來定義給定優先級列表上何時存在任務。因此,在大部分體系架構上,會使用一條?
find-first-bit-set 指令在 5 個 32 位的字(140 個優先級)中哪一位的優先級最高。查找一個任務
來執行所需要的時間并不依賴于活動任務的個數,而是依賴于優先級的數量。這使得 2.6 版本的調度器
成為一個復雜度為 O(1) 的過程,因為調度時間既是固定的,而且也不會受到活動任務個數的影響。
更好地支持 SMP 系統
那么什么是 SMP 呢?SMP 是一種體系架構,其中多個 CPU 可以用來同時執行各個任務,它與傳統的非
對稱處理系統不同,后者使用一個 CPU 來執行所有的任務。SMP 體系架構對多線程的應用程序非常有益
。
盡管優先級調度在 SMP 系統上也可以工作,但是它這種大鎖體系架構意味著當一個 CPU 選擇一個任務
進行分發調度時,運行隊列會被這個 CPU 加鎖,其他 CPU 只能等待。2.6 版本的調度器不是使用一個
鎖進行調度;相反,它對每個運行隊列都有一個鎖。這樣允許所有的 CPU 都可以對任務進行調度,而不
會與其他 CPU 產生競爭。
另外,由于每個處理器都有一個運行隊列,因此任務通常都是與 CPU 密切相關的,可以更好地利用 CPU?
的熱緩存。
任務搶占
Linux 2.6 版本調度器的另外一個優點是它允許搶占。這意味著當高優先級的任務準備運行時低優先級
的任務就不能執行了。調度器會搶占低優先級的進程,并將這個進程放回其優先級列表中,然后重新進
行調度。
回頁首
但是請等一下,還有更多功能呢!
似乎 2.6 版本調度器的 O(1) 特性和搶占特性還不夠,這個調度器還提供了動態任務優先級和 SMP 負
載均衡功能。下面就讓我們來討論一下這些功能都是什么,以及它們分別提供了哪些優點。
動態任務優先級
為了防止任務獨占 CPU 從而會餓死其他需要訪問 CPU 的任務,Linux 2.6 版本的調度器可以動態修改
任務的優先級。這是通過懲罰 CPU 綁定的任務而獎勵 I/O 綁定的任務實現的。I/O 綁定的任務通常使
用 CPU 來設置 I/O,然后就睡眠等待 I/O 操作完成。這種行為為其他任務提供了 CPU 的訪問能力。
用戶響應能力更好
與用戶進行通信的任務都是交互型的,因此其響應能力應該比非交互式任務更好。由于與用戶的通信(
不管是向標準輸出上發送數據,還是通過標準輸入等待輸入數據)都是 I/O 綁定型的,因此提高這些任
務的優先級可以獲得更好的交互式響應能力。
由于 I/O 綁定型的任務對于 CPU 訪問來說是無私的,因此其優先級減少(獎勵)最多 5 個優先級。
CPU 綁定的任務會通過將其優先級增加最多 5 個優先級進行懲罰。
任務到底是 I/O 綁定的還是 CPU 綁定的,這是根據交互性 原則確定的。任務的交互性指標是根據任務
執行所花費的時間與睡眠所花費的時間的對比程度進行計算的。注意,由于 I/O 任務先對 I/O 進行調
度,然后再進行睡眠,因此 I/O 綁定的任務會在睡眠和等待 I/O 操作完成上面花費更多的時間。這會
提高其交互性指標。
有一點值得注意,優先級的調整只會對用戶任務進行,對于實時任務來說并不會對其優先級進行調整。
SMP 負載均衡
在 SMP 系統中創建任務時,這些任務都被放到一個給定的 CPU 運行隊列中。通常來說,我們無法知道
一個任務何時是短期存在的,何時需要長期運行。因此,最初任務到 CPU 的分配可能并不理想。
為了在 CPU 之間維護任務負載的均衡,任務可以重新進行分發:將任務從負載重的 CPU 上移動到負載
輕的 CPU 上。Linux 2.6 版本的調度器使用負載均衡(load balancing) 提供了這種功能。每隔?
200ms,處理器都會檢查 CPU 的負載是否不均衡;如果不均衡,處理器就會在 CPU 之間進行一次任務均
衡操作。
這個過程的一點負面影響是新 CPU 的緩存對于遷移過來的任務來說是冷的(需要將數據讀入緩存中)。
記住 CPU 緩存是一個本地(片上)內存,提供了比系統內存更快的訪問能力。如果一個任務是在某個?
CPU 上執行的,與這個任務有關的數據都會被放到這個 CPU 的本地緩存中,這就稱為熱的。如果對于某
個任務來說,CPU 的本地緩存中沒有任何數據,那么這個緩存就稱為冷的。
不幸的是,保持 CPU 繁忙會出現 CPU 緩存對于遷移過來的任務為冷的情況。
回頁首
挖掘更多潛能
2.6 版本調度器的源代碼都很好地封裝到了 /usr/src/linux/kernel/sched.c 文件中。我們在表 1 中
對在這個文件中可以找到的一些有用的函數進行了總結。
表 1. Linux 2.6 調度器的功能
函數名 函數說明
schedule 調度器主函數。調度優先級最高的任務執行。
load_balance 檢查 CPU,查看是否存在不均衡的情況,如果不均衡,就試圖遷移任務。
effective_prio 返回任務的有效優先級(基于靜態策略,但是可以包含任何獎勵和懲罰)。
recalc_task_prio 根據任務的空閑時間確定對任務的獎勵或懲罰。
source_load 適當地計算源 CPU(任務從中遷移出的 CPU)的負載。
target_load 公平地計算目標 CPU(任務可能遷移到的 CPU)的負載。
migration_thread 在 CPU 之間遷移任務的高優先級的系統線程。
運行隊列的結構也可以在 /usr/src/linux/kernel/sched.c 文件中找到。2.6 版本的調度器還可以提供
一些統計信息(如果啟用了 CONFIG_SCHEDSTATS)。這些統計信息可以從 /proc 文件系統中的?
/proc/schedstat 看到,它為系統中的每個 CPU 都提供了很多數據,包括負載均衡和進程遷移的統計信
息。
回頁首
展望
Linux 2.6 調度器從早先的 Linux 調度器已經跨越了一大步。它極大地改善了最大化利用 CPU 的能力
,同時還為用戶提供了很好的響應體驗。搶占和對多處理器體系架構的更好支持使整個系統更接近于多
桌面和實時系統都非常有用的操作系統。Linux 2.8 版本的內核現在談論還為時尚早,但是從 2.6 版本
的變化中,我們可以期望會有更多的好東西。
========
相關鏈接
http://www.ibm.com/developerworks/cn/linux/l-linux-kernel/總結
以上是生活随笔為你收集整理的linux内核技术文章的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: STL应用总结
- 下一篇: Linux 应用编程