Linux作業系統已是非常常見的作業系統,大家或許都有使用過的經驗。但多數主要是在使用方面去接觸它,對於一個作業系統核心層,除非真的有必要,可能也沒什麼人會有機會再深入去了解。「圖解Linux核心工作原理」的作者就針對一個作業系統的幾個主要主題進行說明,說明的過程中除了如書名中提到的加了不少的圖解說明外,其實我覺得還有一項值得一提的是,作者也用了一些小程式及相關的Linux指令來進行實驗,並呈現及說明作者想要解釋的概念。經由這本書,如果你原本對作業系統核心不了解的,應該也可以學到基本知識;如果你已有基本概念的話,看過這本書除了可以加深你的概念外,也可以學到如何用小程式及相關指令來驗證這些概念。
這本書中大約分成以下幾個主題在來說明:
- 使用者模式
- 行程管理
- 行程排程器
- 記憶體管理
- 記憶體階層
- 檔案系統
- 儲存管理
使用者模式
一般我們在使用Linux或是在開發Linux程式時,都是在所謂的「使用者模式」中執行及使用。書中也說明了這個概念以及為何要區分成這兩種模式。主要是因為許多功能是各行程間共用的,例如裝置存取/行程排程器等等,這些最好統一由核心模式來執行,並給使用者模式來使用。
在使用者模式中如要取用核心模式的功能(例如進行硬體IO的存取或記憶體配置等),就要進行所謂的system call。作都在書中也透過小程式及strace的指令來說明這個概念。並且也用到sar指令來識別一個程式在執行過程中有多少時間是在使用者模式中執行,有多少是在核心模式中執行。
行程管理/行程排程器
行程是每個執行程式的最基本概念,作業系統會為每個行程進行資源管理,例如分配CPU使用時間,配置記憶體等等。每個Linux的可執行檔都是採用ELF格式,如果想要查看某個可執行檔的ELF內容,可以使用以下的指令:
[justin@localhost centos7-3]$ readelf -h /bin/sleep
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4017b0
Start of program headers: 64 (bytes into file)
Start of section headers: 31208 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
如果是想要看可執行檔的每個區段的偏移量,則可以加上-S,例如:
[justin@localhost centos7-3]$ readelf -S /bin/sleep
There are 30 section headers, starting at offset 0x79e8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
00000000000005d0 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400888 00000888
0000000000000289 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400b12 00000b12
000000000000007c 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400b90 00000b90
0000000000000060 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400bf0 00000bf0
00000000000000a8 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400c98 00000c98
00000000000004f8 0000000000000018 AI 5 24 8
[11] .init PROGBITS 0000000000401190 00001190
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004011b0 000011b0
0000000000000360 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000401510 00001510
00000000000030aa 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004045bc 000045bc
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004045e0 000045e0
0000000000000a4b 0000000000000000 A 0 0 32
[16] .eh_frame_hdr PROGBITS 000000000040502c 0000502c
0000000000000264 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000405290 00005290
0000000000000bf4 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000606d28 00006d28
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000606d30 00006d30
0000000000000008 0000000000000008 WA 0 0 8
[20] .jcr PROGBITS 0000000000606d38 00006d38
0000000000000008 0000000000000000 WA 0 0 8
[21] .data.rel.ro PROGBITS 0000000000606d40 00006d40
00000000000000a8 0000000000000000 WA 0 0 32
[22] .dynamic DYNAMIC 0000000000606de8 00006de8
00000000000001d0 0000000000000010 WA 6 0 8
[23] .got PROGBITS 0000000000606fb8 00006fb8
0000000000000038 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 0000000000607000 00007000
00000000000001c0 0000000000000008 WA 0 0 8
[25] .data PROGBITS 00000000006071c0 000071c0
0000000000000080 0000000000000000 WA 0 0 32
[26] .bss NOBITS 0000000000607240 00007240
0000000000000180 0000000000000000 WA 0 0 32
[27] .gnu_debuglink PROGBITS 0000000000000000 00007240
0000000000000010 0000000000000000 0 0 4
[28] .gnu_debugdata PROGBITS 0000000000000000 00007250
0000000000000678 0000000000000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 000078c8
000000000000011a 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
程式被載入到記憶體中後,接著就是要分配CPU的時間來執行,這時靠的就是行程排程器了。為了說明行程排程器,作者也做了個實驗來測試在多少個CPU及執行多少個行程間的條件下,每個行程所花費的時間。細節可參考書中的說明。至於要控制使用到多少個CPU就可以使用taskset指令:
$taskset -c 0 <your-program>
其中參考-c 0表示要使用到一顆編號第0號的CPU,如要指定多顆CPU的話,就可以用逗號區隔。
不過要特別補充的是,使用taskset的方式只是說你的程式「只能」用所指定的CPU來執行,並不是說這些被指定的CPU只能給你的程式來使用。這兩者間有什麼差別呢? 主要是差在如果作業系統這時間有很多程式在執行時,那你程式的總執行時間可能會比你真正在需要的時間來得長,因為CPU在某些時段被其它行程給佔用了。
如果你真的想要讓這個CPU只給你的行程使用的話,我想只能透過CPU isolation的方式來設定。但這個需要在整個作業系統啟動時就設定,因為要讓作業系統的排程器由一開始就排除這個CPU。
除了指定程式要使用哪個CPU來執行外,也可以用nice的指令來設定你的程式在行程排程器中的優先序。
記憶體管理/階層
記憶體管理也是一個很重要的概念,因為在執行程式的過程中主要就是針對記憶體中的資料在進行處理。
要查看系統的記憶體資訊,可以使用free指令:
[justin@localhost centos7-3]$ free
total used free shared buff/cache available
Mem: 3880368 371392 455076 8856 3053900 3212940
Swap: 2097148 8 2097140
或是使用sar指令來查看,不過有些系統預設沒安裝sar,可用yum install sysstat來安裝:
[justin@localhost centos7-3]$ sar -r 1
Linux 3.10.0-1160.66.1.el7.x86_64 (localhost.localdomain) 06/05/2022 _x86_64_ (1 CPU)
04:51:04 AM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
04:51:05 AM 445016 3435352 88.53 2104 2886384 610656 10.22 1227264 1825560 3040
04:51:06 AM 445016 3435352 88.53 2104 2886384 610656 10.22 1227268 1825560 3040
04:51:07 AM 445016 3435352 88.53 2104 2886384 610656 10.22 1227272 1825560 3040
作者在書中也特別提到當fork一個程式時,一開始記憶體並不會增加,主要是因為都使用相同的記憶體,直到針對資料區段的資料有修改時才會觸發「寫時複製」的機制才重新配置一塊新的記憶體給新的行程。
針對虛擬記憶體的部份作者也有清楚的圖示說明,也提到了隨選分頁法(demanding paging),就是程式一開始配置記憶體時,但因為還未真正使用到,所以這塊記憶體是沒有配置到實體記憶體的。由於有這樣的一個模式,表示程式可能執行到要存取這種記憶體時,系統會發生分頁錯誤的狀況,而去進行實體記憶體的配置。
至於要如何觀察這些現象,作者是用到sar -r的指令來觀察kbmemused欄位,如果記憶體已配置但還未存取到時,kbmemused是不會增加到。如果kbmemused有增加時,還可以再配合sar -B的指令來查看分頁錯誤(fault/s)的次數:
[justin@localhost centos7-3]$ sar -B 1
Linux 3.10.0-1160.66.1.el7.x86_64 (localhost.localdomain) 06/05/2022 _x86_64_ (1 CPU)
05:01:15 AM pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
05:01:16 AM 0.00 0.00 30.21 0.00 80.21 0.00 0.00 0.00 0.00
05:01:17 AM 0.00 42.71 27.08 0.00 40.62 0.00 0.00 0.00 0.00
05:01:18 AM 0.00 0.00 16.49 0.00 41.24 0.00 0.00 0.00 0.00
結論
對於剛入門Linux程式開發人員書中的概念可以給你一個基本的認知,知道程式在執行的過程中作業系統核心對你的程式的影響。而對於已有三年以上Linux程式開發經驗的人來說,這些概念應該都已具備,只是透過書中的實例驗證,以及相關的指令來觀察,除了可以加深你的基礎外,如果對程式執行過程中有效能議題的話也可以利用這些知識再來區別問題所在,看是出在CPU分配的時間,或是在於記憶體常發生分頁錯誤造成有些許的影響。在知道問題所在後,就可以對症下藥來思考解法方案。