【讀書筆記】圖解Linux核心工作原理

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分配的時間,或是在於記憶體常發生分頁錯誤造成有些許的影響。在知道問題所在後,就可以對症下藥來思考解法方案。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *