《深刻理解Linux內核》 讀書筆記

深刻理解Linux內核 讀書筆記

1、概論

操做系統基本概念

  • 多用戶系統
    • 容許多個用戶登陸系統,不一樣用戶之間的有私有的空間
  • 用戶和組
    • 每一個用於屬於一個組,組的權限和其餘人的權限,和擁有者的權限不同。對應的是Linux的文件權限系統
  • 進程
    • 和程序的區別。幾個進程能併發執行同一個程序,一個進程能順序執行幾個程序
    • 程序更像是代碼片斷,進程是執行代碼的容器
    • linux是搶佔式操做系統,也就是一個進程只能佔用CPU一段時間。非搶佔式系統中,進程若是不釋放CPU,能夠一直佔用
  • 內核體系結構
    • Linux是單塊內核,同時提供模塊(module)功能
    • 模塊是指:例如一個程序,引用了一個系統模塊,這個系統模塊不會是這個進程單獨擁有,當其餘程序也須要這個模塊時,內核會把這個模塊連接到其餘程序。這樣能夠節省內存,也就是這個模塊只會在內存中存在一份。模塊就是一組函數,或者一段代碼。

文件系統

  • 文件
    • 文件是以字節序列組成的信息載體(container)
    • 文件目錄是樹結構
    • 每一個進程都有一個工做目錄,經過pwdx 進程ID 命令能夠查看
  • 硬連接和軟鏈接
    • 連接相似window的快捷方式,建立一個文件,指向另外一個文件
    • ln p1 p2 就是建立一個文件p2,指向p1
    • 硬連接只能指向文件,不能指向目錄,由於會致使循環指向
    • 硬連接只能指向同一個文件系統的文件(文件系統是物理劃分,例如不一樣硬盤)
    • 軟連接沒有硬連接這些限制,建立方法是加-s參數
  • 文件類型
    • 普通文件
    • 目錄
    • 符號連接
    • 面向塊的設備文件
    • 面向字符的設備文件
    • 管道和命名管道(pipe named pipe)
    • 套接字(socket)
  • 文件描述符與索引節點
    • 每一個文件都有一個索引節點(inode)的數據結構,用來存儲文件的描述信息,和文件的內容是區分開的。
      • inode有(經過ll命令看到的):
        • 文件類型
        • 硬連接個數
        • 文件長度
        • 文件擁有者的uid
        • 用戶組的id
        • 修改時間等
        • 訪問權限
  • 訪問權限和文件模式
    • 擁有者,組,其餘人,各有讀寫執行3種權限
  • 文件操做
    • 打開文件
    • 移動光標
    • 關閉

Unix內核概述

  • 進程/內核模式
    • 進程有用戶態和內核態
    • 用戶態不能訪問內核的數據結構和內核程序
    • 兩種態會常常切換,例如在時刻A,進程在用戶態,在時刻B,進程在內核態
    • 從用戶態切換到內核態的狀況:
      • 調用系統調用
      • 執行進程的CPU發送異常
      • 外圍設備向CPU發出中斷
      • 內核線程被執行
  • 進程
    • 每一個進程有一個進程ID,pid
    • 內核切換執行的進程時,會保存舊進程的信息,包括:
      • 程序計數器和棧指針寄存器
      • 通用寄存器
      • 浮點寄存器
      • CPU狀態
      • 內存管理寄存器
  • 可重入內核
    • unix內核都是可重入的
    • 可重入是指,能夠被重複進入,也就是能夠同時有多個進程處於內核態
  • 進程地址空間
    • 每一個進程有本身私有的地址空間
  • 同步和臨界區
    • 相似鎖
    • linux是搶佔式內核,因此須要同步
    • 信號量
      • 每一個資源都有一個信號量,相似int類型,初始值是1
      • 每一個進程訪問資源,調用down方法,信號量減1,若是減1後,信號量小於0,進程被加入到訪問隊列中。若是大於等於0,進程能夠訪問資源
      • 每一個進程訪問完資源,調用up方法,信號量加1,若是信號量大於等於0,激活訪問隊列的第一個進程
      • 進程鎖,線程鎖的機制,應該都是這樣的
      • 這裏要保證down和up的操做都是原子性的,不能併發
      • 要防止死鎖
      • 鎖裏面的區域就是臨界區,也就是acquire和release之間的代碼
  • 信號和進程間通訊
    • 信號和信號量是不同的
    • linux有20多種不一樣的信號,例如kill -9 中的 9就是一種信號
    • 進程收到信號後,能夠
      • 忽略
      • 異步執行指定程序(新開一個線程?),這種須要事先定義信號處理函數。
    • 內核收到信號後,能夠
      • 終止進程(例如kill - 9)
      • 忽略信號
      • 掛起進程
      • 恢復進程
    • 進程間通訊(IPC)
      • 信號
      • 消息(msgget(),msgsnd())兩個系統調用,發信息和收信息,Python裏面的進程間Queue應該就是用這個實現的
      • 共享內存(shmget shmdt)兩個系統調用
  • 進程管理
    • fork來啓動一個子進程,通常在啓動的時候複製父進程的數據和代碼,可是這樣效率較低,因此會使用寫時複製,也就是一開始父子進程共享內存,當其中一個進程須要修改數據時,才執行復制操做
    • exec用於啓動子進程
    • exit用於結束子進程
    • wait4用於父進程等待子進程結束
  • 內存管理
    • 虛擬內存,在物理內存(MMU)和程序之間的抽象,至關於訪問內存的代理。
    • 內核內存分配器,KMA,用於管理內存
    • 高速緩存 因爲內存比硬盤快不少,因此從硬盤讀取得數據會緩存在內存,使下次能夠快速訪問
    • 2、內存尋址

  • 內存地址
    • 內存地址有3種
      • 邏輯地址,由一個段(segment)和偏移量(offset)組成,用來指明一個操做數,或者一條指令的地址
      • 線性地址。是一個32位無符號整數(在32位系統中是這樣),從0x00000000到0xffffffff。內存至關於一個超大的列表,下標(地址)是一個32位整數,值就是內存的內容,值得大小是1字節
      • 物理地址。內存芯片級的地址
    • 邏輯地址,通過分段單元,轉換爲線性地址,線性地址,通過分頁單元,轉換爲物理地址
  • 分段單元(用於把邏輯地址,轉換爲線性地址)
    • 概念
      • 段選擇符,也叫段標識符,也就是上面說的段,程序傳入給分段單元。有字段:
        • index,表示段描述符在GDT或者LDT中下標
        • TI,表示段描述符在GDT中仍是LDT中
        • RPL,特權級
      • 段描述符,8字節,存放在GDT或者LDT中,有字段
        • Base表示段在內存中首字節的線性地址
        • S,0表示系統段,1表示普通段
        • DPL,特權級,0表示只有內核態才能訪問,3表示內核態和用戶態都能訪問。(cs寄存器中,有一個兩位的字段,指明CPU的當前特權級,0表示內核級,3表示用戶級。因此經過這個機制,能夠限制用戶態的進程不能訪問內核態的內存數據
        • D或者B,表示這是代碼段,仍是數據段
      • GDT,是全局段列表,item是段描述符
      • LDT,是局部段列表,item是段描述符
    • 轉換流程
      1. 傳入邏輯地址給分段單元,邏輯地址包含段選擇符和偏移量
      2. 查看段選擇符的TI字段,決定是從GDT中仍是LDT中獲取段描述符,假如是GDT
      3. 查看段選擇符的index字段,假如是2,從gdtr寄存器中獲取GDT列表的首字節地址,假如是0x00002000,計算段描述符的位置=0x00002000+2*8,=0x00002016 (每一個段描述符8字節),因此段描述符在內存的0x00002016-0x00002024位置
      4. 查看段描述符的Base字段,假如是0x00003000,加上偏移量,假如是100,獲得線性地址是0x00003100

3、進程

進程,輕量級進程(LWP)和線程

  • 進程是程序執行時的一個實例
  • 線程 是進程裏面的一個執行流,線程的切換時在用戶態進行的。可是這樣就不能作到併發了
  • 輕量級進程,相似線程,可是切換時在內核態進行

因此Linux的作法是(TODO 這一塊還不是很明白)node

  • 把線程和輕量級進程關聯起來,因此線程和輕量級進程是等價的
  • 對內核來講,進程和LWP是同樣的,使用一樣的調度方法
  • LWP之間能夠共享部分數據

進程描述符

  1. 進程描述符是一個數據結構(c的struct,相似Python的字典)linux

  2. 進程描述符有字段:
    1. state 狀態
      1. 可運行狀態(TASK_RUNNING),要麼在運行,要麼準備運行
      2. 可中斷的等待狀態(TASK_INTERRUPTIBLE)進程被掛起(睡眠),表示它在等待一個事件的發生,例如等待某個系統資源。當這個系統資源可用,內核會產生一個硬件中斷,來喚醒進程
      3. 不可中斷的等待狀態(TASK_UNINTERRUPTIBLE),和可中斷的等待狀態相似,這個狀態較少用到
      4. 暫停狀態(TASK_TOPPED)進程被暫停執行,當進程收到信號SIGSTOP,SIGSTP,SIGTTIN SIGTTOU信號後,會進入暫停狀態
      5. 跟蹤狀態(TASK_TRACED)當進程被另外一個進程跟蹤,例如執行ptrace命令,
      6. 僵死狀態(EXIT_ZOMBIE)進程的執行被終止,可是父進程尚未發佈wait4或者waitpid命令來獲取進程信息。這時內核不會自動丟棄進程的信息,由於父進程可能還須要這些信息
        10.僵死撤銷狀態
    2. thread_info 進程的基本信息
    3. fs_struct 當前目錄
    4. signal_struct 收到的信號
    5. pid 進程的ID。順序遞增,最大是32767,超事後,從1開始獲取閒置的PID值。進程裏面的線程,也擁有本身的pid,同時每一個線程有一個tgid(thread group id),表示線程組ID,這個ID等於進程中第一個線程的pid。
      1. 一個進程裏面至少有一個線程

進程鏈表

  • 一個進程描述符表示一個進程
  • Linux把全部進程放在一個雙向鏈表裏面,每一個item是一個進程描述符
  • TASK_RUNNING狀態的進程鏈表
    • 因爲CPU在進行進程切換時,須要快速知道下一個執行的進程是什麼,因此Linux把全部能夠執行的進程都放在一個單獨的鏈表。
    • 因爲不一樣進程有不一樣的優先級,因此linux的作法是
      • 因爲有140種優先級(優先級用prio表示,0-139),因此用140個鏈表來保存
      • 用一個140長度的位圖(bitmap)來表示140個連接中,哪些有數據
      • 因此獲取下一個優先級最高的進程的作法是:
        • 查看位圖,看第一個=1的位的下標是多少,例如是15
        • 訪問第15個鏈表,queue[15],獲取第一個元素

進程間的關係

進程描述符裏面有特定的字段,記錄每一個進程的父進程,兄弟進程和子進程redis

  • real_parent 父進程的描述符指針,若是父進程不存在,指向進程1
  • parent 當前父進程,一般和real_parent一致,指引當進程被追蹤時不一致
  • children 鏈表,記錄全部子進程
  • sibling 有prev和next兩個元素,表示上一個兄弟進程,和下一個兄弟進程

pidhash

有時候內核須要根據pid來獲取進程描述符
因此內核會保存一個pidhash數據結構,是個hash表(c裏面的hash表的實現和redis的hash表實現相似),key是pid,value是進程描述符緩存

進程切換

進程切換,任務切換,上下文切換是同樣的數據結構

每一個進程都有本身的地址空間(在內存),可是進程之間是共享寄存器的,因此進程的切換須要(硬件上下文是寄存器的數據):併發

  • 保存prev進程的硬件上下文
  • 用next硬件上下文替換prev

上面的操做使用一個switch_to宏來實現,傳入參數prev,next,prev。傳入兩次prev是怕切換上下文後,把第一個prev丟了。異步

建立進程

Linux進程的特性:socket

  • 寫時複製
  • 輕量級進程容許父子進程共享不少數據結構

建立進程的系統調用:函數

  • close()ui

    • fn 子進程建立後執行的函數,函數結束,子進程終止
    • arg 傳給函數的數據
    • 其餘還有不少參數
  • fork close函數的封裝
  • vfork close函數的封裝

內核進程

內核進程是一直運行在內核態的

進程0
進程0是linux啓動後的第一個進程,由它建立進程1
進程1
進程1也叫init進程,進程1會一直運行知道linux關閉

撤銷進程

進程執行完指定的代碼後,就會終止,這時必須通知內核回收進程的資源。
通常是exit系統調用,c編譯程序會本身動把exit函數插入到main函數最後
內核能夠強迫整個線程組死掉(例如收到kill -9)

進程刪除 當進程終止後,進程會進入僵死狀態,直到父進程調用wait4來獲取進程的狀態數據,而後進程就會被刪除。 若是父進程已經不存在,進程會交給init進程託管,init進程會按期執行wait4命令來查看進程的狀態,若是進程已經終止,就會刪除這個進程

相關文章
相關標籤/搜索