天天看點

深入分析Linux核心源代碼閱讀筆記 第一章、第二章

我的GIS/CS學習筆記:https://github.com/yunwei37/ZJU-CS-GIS-ClassNotes

<一個浙江大學大學生的計算機、地理資訊科學知識庫 >

第一章 走進 Linux

Linux 核心具有下列基本特征:

  • Linux 核心的組織形式為整體式結構
  • Linux 的程序排程方式簡單而有效
  • Linux 支援核心線程(或稱守護程序)
  • Linux 支援多種平台的虛拟記憶體管理
  • Linux 核心另一個獨具特色的部分是虛拟檔案系統(VFS Virtul File Systen)
  • Linux 的子產品機制使得核心保持獨立而又易于擴充
  • 增加系統調用以滿足特殊的需求
  • 網絡部分面向對象的設計思想使得 Linux 核心支援多種協定、多種網卡驅動程式變得容易

本書所分析的 Linux 核心版本是 2.4 版的 2.4.16 版。

Linux 作業系統由 4 個部分組成:

  • 使用者程序:使用者應用程式是運作在 Linux 作業系統最高層的一個龐大的軟體集合
  • 系統調用接口:在應用程式中,可通過系統調用來調用作業系統核心中特定的過程,以實作特定的服務
  • Linux 核心:核心實際是抽象的資源操作到具體硬體操作細節之間的接口
  • 硬體:包括了 Linux 安裝時需要的所有可能的實體裝置

其中,Linux 核心由 5 個主要的子系統組成

  • 程序排程(SCHED)控制着程序對 CPU 的通路
  • 記憶體管理(MM)允許多個程序安全地共享主記憶體區域
  • 虛拟檔案系統(Virtul File System,VFS)隐藏了各種不同硬體的具體細節
  • 網絡接口(NET)提供了對各種網絡标準協定的存取和各種網絡硬體的支援
  • 程序間通信(IPC) 支援程序間各種通信機制

各個子系統之間互相依賴。

第二章 Linux 運作的硬體基礎

作業系統是橫跨軟體和硬體的橋梁。

與硬體相關的代碼全部放在 arch(architecture 一詞的縮寫,即體系結構相關)目錄下。

i386 的寄存器:

  • 通用寄存器:8 個通用寄存器
  • 段寄存器:6 個 16 位的段寄存器
  • 狀态和控制寄存器:标志寄存器(EFLAGS)、指令指針(EIP)和 4 個控制寄存器組成
  • 系統位址寄存器: 4 個系統位址寄存器
  • 調試寄存器: 8 個 32 位的調試寄存器 DR0~DR7
  • 測試寄存器:兩個 32 位的測試寄存器 TR6 和 TR7

記憶體位址:

在 8086 的實模式下,把某一段寄存器左移 4 位,然後與位址 ADDR 相加後被直接送到記憶體總線上,這個相加後的位址就是記憶體單元的

實體位址

,而程式中的這個位址就叫

邏輯位址

(或叫虛位址)。在 80386 的保護模式下,這個邏輯位址不是被直接送到記憶體總線,而是被送到

記憶體管理單元

(MMU)。

區分以下 3 種不同的位址:

  • 邏輯位址:機器語言指令仍用這種位址指定一個操作數的位址或一條指令的位址
  • 線性位址:線性位址是一個 32 位的無符号整數,可以表達高達 2^32(4GB)的位址
  • 實體位址: 實體位址是記憶體單元的實際位址,用于晶片級記憶體單元尋址

段機制和描述符

在 80386 的段機制中,邏輯位址由兩部分組成,即段部分(選擇符)及偏移部分。

  • 段是形成邏輯位址到線性位址轉換的基礎,包含:
    • 段的基位址(Base Address)
    • 段的界限(Limit)
    • 段的屬性(Attribute)

描述符(Descriptor):描述段的屬性的一個 8 位元組存儲單元。

  • 使用者段描述符
  • 系統段描述符

門也是一種描述符,有調用門、任務門、中斷門和陷阱門 4 種門描述符。

各種各樣的使用者描述符和系統描述符,都放在對應的描述符表中。描述符表(即段表)定義了 386 系統的所有段的情況。

  • 全局描述符表(GDT)
  • 中斷描述符表(IDT)
  • 局部描述符表(LDT)

在實模式下,段寄存器存儲的是真實的段位址,在保護模式下,16 位的段寄存器無法放下 32 位的段位址,是以,它們被稱為選擇符,即段寄存器的作用是用來選擇描述符。

選擇符有 3 個域:

  • 索引域:用于指向全局描述符表中相應的描述符
  • 選擇域:局部還是全局
  • 特權級:請求者特權級 RPL

386 的每一個段選擇符都有一個程式員不可見(也就是說程式員不能直接操縱)的 88 位寬的段描述符高速緩沖寄存器與之對應。當選擇符的值改變時,處理器自動裝載不可見部分。

在沒有分頁操作時,尋址一個存儲器操作數的步驟:

  1. 在段選擇符中裝入 16 位數,同時給出 32 位位址偏移量(比如在 ESI、EDI 中等)。
  2. 根據段選擇符中的索引值、TI 及 RPL 值,再根據相應描述符表寄存器中的段位址和段界限,進行一系列合法性檢查(如特權級檢查、界限檢查),該段無問題,就取出相應的描述符放入段描述符高速緩沖寄存器中。
  3. 将描述符中的 32 位段基位址和放在 ESI、EDI 等中的 32 位有效位址相加,就形成了 32 位實體位址。

在保護模式下,32 位段基位址不必向左移 4 位,而是直接和偏移量相加形成 32位實體位址(隻要不溢出)

描述符投影寄存器:

每個段寄存器都有與之相聯系的描述符投影寄存器。在這些寄存器中,容納有由段寄存器中的選擇符确定的段的描述符資訊。段寄存器對程式設計人員是可見的,而與之相聯系的容納描述符的寄存器,則對程式設計人員是不可見的,故稱之為投影寄存器。

Linux 中的段:

  • Linux 核心的設計并沒有全部采用 Intel 所提供的段方案,僅僅有限度地使用了一下分段機制。
  • Linux 讓所有的程序(或叫任務)都使用相同的邏輯位址空間,是以就沒有必要使用局部描述符表 LDT。
  • Linux 在啟動的過程中設定了段寄存器的值和全局描述符表 GDT 的内容
  • Linux 核心不區分資料段和堆棧段
  • 核心代碼段和資料段具有最高特權,是以其 RPL為 0,而使用者代碼段和資料段具有最低特權,是以其 RPL 為 3

GDT 放在數組變量 gdt_table 中

ENTRY(gdt_table)
	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x0000000000000000	/* not used */
	.quad 0x00cf9a000000ffff	/* 0x10 kernel 4GB code at 0x00000000 */
	.quad 0x00cf92000000ffff	/* 0x18 kernel 4GB data at 0x00000000 */
	.quad 0x00cffa000000ffff	/* 0x23 user   4GB code at 0x00000000 */
	.quad 0x00cff2000000ffff	/* 0x2b user   4GB data at 0x00000000 */
	.quad 0x0000000000000000	/* not used */
	.quad 0x0000000000000000	/* not used */
	/*
	 * The APM segments have byte granularity and their bases
	 * and limits are set at run time.
	 */
	.quad 0x0040920000000000	/* 0x40 APM set up for bad BIOS's */
	.quad 0x00409a0000000000	/* 0x48 APM CS    code */
	.quad 0x00009a0000000000	/* 0x50 APM CS 16 code (16 bit) */
	.quad 0x0040920000000000	/* 0x58 APM DS    data */
	.fill NR_CPUS*4,8,0		/* space for TSS's and LDT's */
           
  • 段的基位址全部為 0x00000000;
  • 段的上限全部為 0xffff;
  • 段的粒度 G 為 1,即段長機關為 4KB;
  • 段的 D 位為 1,即對這 4 個段的通路都為 32 位指令;
  • 段的 P 位為 1,即 4 個段都在記憶體。

這樣 Linux 巧妙地繞過了邏輯位址到線性位址的映射

分頁機制

分頁機制在段機制之後進行,以完成線性—實體位址的轉換過程。

  • 分頁機制由 CR0 中的 PG 位啟用。如 PG=1,啟用分頁機制
  • 分頁機制管理的對象是固定大小的存儲塊,稱之為頁(page)
  • 80386 使用 4K 位元組大小的頁

線性—實體位址的轉換,可将其意義擴充為允許将一個線性位址标記為無效:

  • 線性位址是作業系統不支援的位址
  • 在虛拟存儲器系統中,線性位址對應的頁存儲在磁盤上

分頁機構:

兩級頁表結構,第一級稱為頁目錄,第二級稱為頁表

頁目錄項:

  • 第 31~12 位是 20 位頁表位址
  • 第 0 位是存在位
  • 第 1 位是讀/寫位
  • 第 2 位是使用者/管理者位
  • 第 3 位是 PWT(Page Write-Through)位
  • 第 4 位是 PCD(Page Cache Disable)位
  • 第 5 位是通路位
  • 第 7 位是 Page Size 标志,隻适用于頁目錄項

頁面項:

第 6 位是頁面項獨有的,當對涉及的頁面進行寫操作時,D 位被置 1

線性位址到實體位址的轉換:

  1. 第一步,CR3 包含着頁目錄的起始位址,用 32 位線性位址的最高 10 位 A31~A22 作為頁目錄的頁目錄項的索引,将它乘以 4,與 CR3 中的頁目錄的起始位址相加,形成相應頁表的位址。
  2. 第二步,從指定的位址中取出 32 位頁目錄項,它的低 12 位為 0,這 32 位是頁表的起始位址。用 32 位線性位址中的 A21~A12 位作為頁表中的頁面的索引,将它乘以 4,與頁表的起始位址相加,形成 32 位頁面位址。
  3. 第三步,将 A11~A0 作為相對于頁面位址的偏移量,與 32 位頁面位址相加,形成 32 位實體位址。

擴充分頁:它允許頁的大小為 4MB

頁面高速緩存:

為了提高速度,在 386 中設定一個最近存取頁面的高速緩存硬體機制,它自動保持 32 項處理器最近使用的頁面位址,是以,可以覆寫 128K 位元組的存儲器位址。有些書上也把頁面高速緩存叫做 “聯想存儲器” 或 “轉換旁路緩沖器(TLB)”

Linux 中的分頁機制

Linux 主要采用分頁機制來實作虛拟存儲器管理,原因如下:

  • Linux 設計目标之一就是能夠把自己移植到絕大多數流行的處理器平台
  • Linux 的分段機制使得所有的程序都使用相同的段寄存器值

為了保持可移植性,Linux 采用三級分頁模式而不是兩級,為此,Linux定義了 3 種類型的頁表:

  • 總目錄 PGD(Page Global Directory)
  • 中間目錄 PMD(Page Middle Derectory)
  • 頁表 PT(Page Table)

盡管 Linux 采用的是三級分頁模式,但我們的讨論還是以 Intel 奔騰處理器的兩級分頁模式為主,是以,Linux 忽略中間目錄層,以後,我們把總目錄就叫頁目錄。

與頁相關的資料結構及宏的定義:

  • 表項的定義

    PGD、PMD 及 PT 表的表項:

    #if CONFIG_X86_PAE
    typedef struct { unsigned long pte_low, pte_high; } pte_t;
    typedef struct { unsigned long long pmd; } pmd_t;
    typedef struct { unsigned long long pgd; } pgd_t;
    
               
    Linux 沒有把這幾個類型直接定義長整數而是定義為一個結構,這是為了讓 gcc 在編譯時進行更嚴格的類型檢查。定義了幾個宏來通路這些結構的成分:
    #define pte_val(x) ((x).pte_low)
    #define pmd_val(x)((x).pmd)
    #define pgd_val(x) ((x).pgd)
    
               
    一個頁面保護結構 pgprot_t 和一些宏,字段 pgprot 的值與圖 2.24 頁面項的低 12 位相對應,其中的 9 位對應 0~9 位:
    typedef struct { unsigned long pgprot; } pgprot_t;
    #define pgprot_val(x) ((x).pgprot)
    
               
    #define _PAGE_PRESENT 0x001
    #define _PAGE_RW 0x002
    #define _PAGE_USER 0x004
    #define _PAGE_PWT 0x008
    #define _PAGE_PCD 0x010
    #define _PAGE_ACCESSED 0x020
    #define _PAGE_DIRTY0x040
    #define _PAGE_PSE 0x080 /* 4 MB (or 2MB) page, Pentium+, if present.. */
    #define _PAGE_GLOBAL 0x100 /* Global TLB entry PPro+ */
    
               
    頁目錄表及頁表在 pgtable.h 中定義。
  • 線性位址域的定義

    其中 PAGE_SHIFT 宏定義了偏移量的位數為 12,是以頁大小 PAGE_SIZE 為 212=4096 位元組;PTRS_PER_PTE 為頁表的項數;最後 PAGE_MASK 值定義為 0xfffff000,用以屏蔽掉偏移量域的所有位(12 位)。

    GDIR_SHIFT 是頁表所能映射區域線性位址的位數,它的值為 22(12 位的偏移量加上10 位的頁表);PTRS_PER_PGD 為頁目錄目錄項數;PGDIR_SIZE 為頁目錄的大小,為 222,即 4MB;PGDIR_MASK 為 0xffc00000,用于屏蔽偏移量位與頁表域的所有位。

    PMD_SHIFT 為中間目錄表映射的位址位數,其值也為 22。

    #define PAGE_SHIFT 12
    #define PAGE_SIZE (1UL << PAGE_SHIFT)
    #define PTRS_PER_PTE 1024
    #define PAGE_MASK (~(PAGE_SIZE-1))
    
    #define PGDIR_SHIFT22
    #define PTRS_PER_PGD 1024
    #define PGDIR_SIZE (1UL << PGDIR_SHIFT)
    #define PGDIR_MASK (~(PGDIR_SIZE-1))
    
    #define PMD_SHIFT 22
    #define PTRS_PER_PMD 1
               
  • 對頁目錄及頁表的處理
    • pgd_none()函數直接傳回 0,表示尚未為這個頁目錄建立映射,是以頁目錄項為空。
    • pgd_present()函數直接傳回 1,表示映射雖然還沒有建立,但頁目錄所映射的頁表肯定存在于記憶體(即頁表必須一直在記憶體)
    • pte_present 宏的值為 1 或 0,表示 P 标志位。
    • pgd_clear 宏實際上什麼也不做
    • pte_clear 就是把 0 寫到頁表表項中
    • 對頁表表項标志值進行操作的宏:這些宏的代碼在 pgtable.h 檔案中

Linux 中的彙編語言

  • 在 AT&T 中,寄存器前冠以“%”,而立即數前冠以“$”
  • 在 AT&T 中,十六進制立即數前冠以“0x“
  • Intel 與 AT&T 操作數的方向正好相反,在 AT&T 中,第一個數是源操作數,第二個數是目的操作數。
  • 在 AT&T 中,記憶體單元操作數用“()”括起來。
  • AT&T 間接尋址方式可能更晦澀難懂一些:%segreg:disp(base,index,scale)。這種尋址方式常常用在通路資料結構數組中某個特定元素内的一個字段,其中,base 為數組的起始位址,scale 為每個數組元素的大小,index 為下标。如果數組元素還是一個結構,則 disp 為具體字段在結構中的位移。
  • AT&T 的操作碼後面有一個字尾,其含義就是指出操作碼的大小。
  • 以.S 為擴充名的檔案是“純”彙編語言的檔案。
  • GNU 彙程式設計式 GAS(GNU Assembly)和連接配接程式
  • AT&T 中的節(Section):至少需要有以下 3 種節
    • section .data
    • .section .bss
    • section .text
  • 彙程式設計式指令(Assembler Directive):以句點(.)為開頭,後跟指令名(小寫字母)
    • .ascii “string”…
    • .byte 表達式
    • .fill 表達式
    • .globl symbol
    • .quad bignums
    • .rept count
    • .space size , fill
    • .word expressions
    • .long expressions
    • .org new-lc , fill
  • gcc 嵌入式彙編:

    __asm__ __volatile__ ("<asm routine>" : output : input : modify);

    • "<asm routine>"

      為彙編指令部分
    • 輸出部分(output),用以規定對輸出變量(目标操作數)如何與寄存器結合的限制(constraint)
    • 輸入部分(Input):輸入部分與輸出部分相似,但沒有“=”。
    • 修改部分(modify):這部分常常以“memory”為限制條件,以表示操作完成後記憶體中的内容已有改變
    • 指令部分為必選項,而輸入部分、輸出部分及修改部分為可選項
  • 一些常用的 386 彙編指令及其功能:
    • 位操作指令
    • 控制轉移類指令
    • 資料傳輸指令
    • 标志控制類指令
    • 邏輯類指令
    • 串操作指令
    • 多段類操作指令
    • 作業系統類指令

繼續閱讀