linux核心設計與實現

kinnylee發表於2019-03-01

一. linux核心簡介

1. linux簡介

1.1 unix的特點

  • unix很簡潔,僅提供幾百個系統呼叫,並有非常明確的設計目的
  • unix所有東西都當作檔案對待,這種抽象使對資料和裝置都通過一套相同的系統呼叫介面進行
  • 核心用C語言編寫,移植能力很強
  • 程式建立迅速,獨特的fork呼叫
  • 提供了簡潔但是穩定的程式間通訊原語

1.2 unix和linux

  • linux克隆unix,但不是unix
  • linux借鑑了unix很多的設計,並且實現了 unix的api
  • linux沒有直接使用unix的原始碼,但完整表達了unix的設計目標並保證程式設計介面一致

2. 作業系統和核心簡介

  • 核心一般包括:
    • 中斷服務程式:負責響應中斷
    • 排程程式:管理多程式,分配處理器時間
    • 記憶體管理程式:管理記憶體空間
    • 系統服務程式:包括網路,程式間通訊
  • 應用程式通過系統呼叫和核心通訊來執行
  • 應用程式通常呼叫庫函式,庫函式通過系統呼叫讓核心帶其完成各種任務
  • 核心對硬體裝置的管理:硬體想要通訊時,傳送非同步訊號去打斷核心,核心通過中斷號查詢處理程式
  • linux核心開發的特定
    • 不能連結標準c函式庫。c庫太大了,會影響大小和效率。不過大部分常用的c函式在核心中都有實現
    • 沒有記憶體保護機制,要注意非法訪問記憶體地址
    • 不要輕易使用浮點數,要人工儲存和恢復浮點暫存器
    • 棧空間很小且固定。32為機器為8kb,64為16kb
    • 核心很容易產生競爭條件,注意同步和併發
    • 注意可移植性

二. 程式管理

1. 基本概念

  • unix系統的兩大抽象物件:程式,檔案。具體可參考另外三篇關於unix程式和檔案的文章序列
  • 程式是處於執行期的程式,linux通常也把程式叫做任務
  • 程式包括:程式碼段,資料段,開啟的檔案,掛起的訊號,地址空間,執行緒等
  • 執行緒是程式中活動的執行物件
  • 每個執行緒擁有獨立的程式計數器,程式棧和一組程式暫存器
  • 核心排程的物件是執行緒,而不是程式
  • linux的執行緒實現非常特別,並不特別區分執行緒和程式
  • 程式提供兩種虛擬機器制:虛擬處理器和虛擬記憶體
  • 同一個程式內的執行緒可以共享虛擬記憶體,但是有各自的虛擬處理器

2. 程式描述符及任務佇列

2.1 基本概念

  • 核心把程式存放在叫做任務佇列的雙向迴圈連結串列中
  • 連結串列中每一項都是task_struct型別,稱為程式描述符,包括一個程式的所有資訊。路徑:/include/linux/sched.h
    linux核心設計與實現

2.2 程式描述符如何分配

  • linux通過slab分配其分配task_struct結構,這樣能達到物件複用和快取著色
  • 通過預先分配和重複使用task_struct,避免動態分配和釋放帶來的效能損耗,這也是為什麼建立程式快的原因
  • task_struct放在核心棧的尾端,為了讓暫存器少的硬體體系只通過棧指標就能算出位置,避免使用額外暫存器儲存
  • slab分配器在核心棧的尾部建立新的struct thread_info,內部的task指向實際的task_struct。thread_info位置:<asm/thread_info.h>
    linux核心設計與實現

2.3 程式描述符存放在哪

  • current巨集可以查詢當前正在執行程式的程式描述符
  • 這個巨集的具體實現根據各自硬體體系結構有所不同
  • x86體系,通過棧尾部的thread_info結構的task指標找到程式描述符
  • 有的體系(IBM的RISC),分配一個專用的暫存器存放task_struct的地址

2.4 程式的狀態

  • 程式描述符的state欄位描述了程式當前的狀態,每個程式都處於五種狀態的一種
    • TASK_RUNNING:執行。程式是可執行的。
    • TASK_INTERRUPTIBLE:可中斷。程式被阻塞,等待被喚醒
    • TASK_UNINTERRUPTIBLE:不可中斷。收到訊號不做任何響應。ps命令檢視會顯示D
    • TASK_ZOMBIE:僵死。程式已經結束了,但是父程式還沒有呼叫wait4系統呼叫
    • TASK_STOPPED:停止。程式停止執行
  • 狀態變遷圖
    linux核心設計與實現
  • 設定當前程式:set_task_state(task,state)或set_current_state

2.5 程式上下文

  • 一般程式在使用者空間執行,執行系統呼叫或觸發異常時,進入核心空間,核心代表程式執行,並處於程式上下文中
  • 系統呼叫和異常處理是核心明確定義的介面,對核心的所有訪問也只能通過這些介面
  • linux程式有明顯的繼承關係,所有的程式都是pid為1的init程式的後代
  • 系統中的每個程式必有一個父程式,每個程式可以擁有一個或多個子程式
  • 程式間關係存放在程式描述符中。task_struct中的parent變數,指向一個task_struct,存放父程式地址。children變數是一個連結串列,指向所有的子程式

3. 程式建立

3.1 基本概念

  • unix程式建立分為:fork和exec兩步
  • fork通過拷貝當前程式建立子程式。子程式和父程式僅有很少差異:pid,ppid,某些資源和統計量
  • exec負責讀取可執行檔案並載入地址空間開始執行

3.2 寫時拷貝(COW)

  • 傳統的fork直接拷貝資源效率低下,linux使用寫時拷貝(copy on write)技術提高效率
  • COW並不會複製整個地址空間,而是讓父子程式以只讀方式共享記憶體,資料的複製只有在寫入時才進行

3.3 fork函式

  • linux通過clone()系統呼叫實現fork(), 這個呼叫通過引數標識(很多種型別)指明需要共享的資源
  • clone內部呼叫do_fork完成主要工作(kernel/fork.c)
  • do_fork內部呼叫copy_process,然後讓程式執行
  • copy_process呼叫過程:
    • 呼叫dup_task_struct為新程式建立核心棧,thread_info結構和task_struct,這些值與當前程式相同,此時描述符完全相同
    • 檢查系統擁有的程式數是否超過限制
    • 將很多成員重置
    • 設定狀態為TASK_UNINTERRUPTIBLE保證不會被執行
    • 呼叫copy_flags以更新task_struct的flags成員
    • 呼叫get_pid獲取新的pid
    • 根據引數標識,拷貝或共享開啟的檔案,檔案系統資訊,訊號處理函式,程式地址空間,名稱空間等。一般情況下,這些資源是執行緒共享的
    • 父子程式平分時間片
    • 掃尾工作,並返回指向子程式的指標
  • 新建立的程式被喚醒並讓其投入執行,一般優先子程式首先執行

3.4 vfork函式

  • 和fork功能相同,除了補考吧父程式的頁表項
  • 通過向clone系統呼叫傳遞一個特殊標誌進行的
  • 該函式的設計並不是很優良的

4. 執行緒在linux中的實現

4.1 liunx執行緒概述

  • 一組執行緒共享程式內的記憶體地址空間,開啟的檔案和其他資源
  • 執行緒機制支援併發程式設計技術,多處理器上保證真正的並行處理
  • linux實現執行緒的機制非常獨特,從核心角度看,沒有執行緒的概念
  • linux把所有執行緒都當做程式來實現,核心沒有特別的排程演算法或資料結構來表徵執行緒,被視為一個使用某些共享資源的程式
  • 每個執行緒有自己的task_struct,就像一個普通的程式,這個程式和其他程式共享某些資源
  • 與其他系統(windows,solaris)實現差異巨大,這些系統核心專門提供執行緒的支援

4.2 linux執行緒建立

  • 執行緒的建立和普通程式建立型別,只不過呼叫clone時需要傳遞一些引數標誌,指明需要共享的資源
  • 引數標誌說明:
    • CLONE_VM:父子程式共享地址空間
    • CLONE_SIGHAND:父子程式共享訊號處理函式
    • CLONE_THREAD:父子程式放入相同執行緒組
    • CLONE_FS:父子程式共享檔案系統資訊
    • CLONE_FILES:共享開啟的檔案 ...

4.3 核心執行緒

  • 核心執行緒:獨立執行在核心空間的標準程式
  • 和普通程式的區別:沒有獨立的地址空間,只能在核心空間執行
  • 建立只能由其他核心執行緒建立,函式為kernel_thread

4.4 程式終結

釋放資源

  • 程式終結時,核心必須釋放它所佔有的資源,並通知父程式
  • 結束可以是正常,異常,還可以註冊終止清理函式,詳見另一篇文章:關於unix程式和檔案的文章序列
  • 最終結束會呼叫do_exit(kenel/exit.c),完成的工作包括:
    • 將task_struct的標誌成員設定為PF_EXITING
    • 如果程式會計功能開啟,會呼叫acct_process輸出統計資訊
    • 呼叫_exit_mm函式放棄程式佔用的mm_struct,如果沒有被共享就徹底釋放
    • 呼叫sem_exit。如果排隊等待IPC訊號,則離開佇列
    • 呼叫__exit_files:遞減檔案描述符;__exit_fs:遞減檔案系統資料;exit_namespace:名字空間引用計數;exit_sighand:訊號處理函式引用計數,如果某個降為0,則可釋放
    • task_struct的exit_code設定退出程式碼
    • 呼叫exit_notify向程式傳送訊號,父程式修改為其他執行緒或init程式,程式狀態設定為TASK_ZOMBLE(僵死,不被排程)
    • 最後,呼叫schedule切換到其他程式
  • 呼叫完do_exit,與程式相關的所有資源都被釋放了,它所佔有的資源只剩報錯thread_info的核心棧和儲存task_struct的那一小片slab,存在的唯一目的就是向父程式提供資訊。

刪除程式描述符

  • 呼叫do_exit之後,執行緒僵死,但是還保留檔案描述符
  • 父程式獲取到子程式的資訊後,子程式的task_sturct結構才被釋放
  • wait函式呼叫系統函式wait4實現,將掛起呼叫它的程式,直到其中一個子程式退出,函式返回子程式的pid
  • 當最終需要釋放程式描述符時,release_task會被呼叫,執行以下工作:
    • 呼叫free_uid減少該程式擁有者的程式使用計數
    • 呼叫unhash_process從pidhash上刪除該程式,同時從task_list刪除該程式
    • 如果程式正在被ptrace跟蹤,將跟蹤父程式重置
    • 最後,呼叫put_task_struct釋放核心棧和thread_info結構所佔的頁,並釋放task_struct所佔的slab快取記憶體
    • 這時資源和描述符就全部被釋放掉了

孤兒程式的處理

  • 父程式如果在子程式之前退出,必須找到新的父親,否則永遠僵死
  • 尋找父親的函式在do_exit中呼叫的notify_present函式,內部呼叫forget_original_parent,該函式實現具體尋找過程
  • 該函式設定父親為執行緒組內的其他程式,沒有就用init程式

三. 程式排程

1. 概述

  • 排程程式是核心組成部分,它負責選擇下一個要執行的程式
  • 排程程式負責給可執行程式分配處理器時間資源
  • 多工系統可分為:搶佔式任務(linux等現代作業系統的方式)和非搶佔式任務
  • 分配給每個程式的執行時間叫做時間片

2. 排程策略

2.1 cpu密集型和IO密集型

  • cpu密集型:大部分時間執行程式碼
  • IO密集型:大部分時間提交io和等待io,經常可執行,但執行時間極短
  • 從系統響應速度考慮,linux排程策略更傾向於優先排程IO密集型程式

2.2 程式優先順序

  • 排程演算法中最基本的一類:基於優先順序排程,根據程式的價值和其對處理器時間的需求分級的思想
  • 排程程式總是選擇時間片未用完且優先順序最高的程式執行
  • linux實現了一種基於動態優先順序的排程演算法。一開始設定基本優先順序,然後根據需要動態加,減優先順序:如果一個程式IO等待時間多餘執行時間,它屬於IO密集型,會提高優先順序;相反,如果程式時間片一下就別耗盡,屬於cpu密集型,會降低優先順序
  • linux提供兩組獨立的優先順序範圍:
    • nice值:-20~19,預設為0。標準優先順序範圍。值越大,優先順序越低,時間片越短。task_struct的static_prio欄位表示
    • 實時優先順序:0~99

2.3 時間片

  • 表明程式在被搶佔之前能持續執行的時間
  • 排程策略必須規定預設時間片。如果過長,互動式的響應表現欠佳;如果過短,會明顯增大程式切換帶來的處理器耗時
  • 很多系統預設時間片很短:20ms
  • linux提供動態調整優先順序和時間片長度的機制,使得排程效能穩定且強健
  • 程式不一定要一次用完時間片,可分多次使用,儘可能長時間保證可執行

2.4 程式搶佔

  • 當一個程式處於TASK_RUNNING狀態,核心會檢查它的優先順序是否高於正在執行的程式,滿足的話排程程式被喚醒重新選擇程式執行
  • 當一個程式的時間片為0,它會被搶佔,排程程式可以選擇新的程式執行

3. 排程演算法

3.1 概述

  • linux排程程式定義與kernel/sched.c
  • 2.5版本核心重寫排程演算法,和以前版本區別很大,實現以下目標
    • 充分實現O(1)排程,不管多少程式或什麼輸入,每個演算法能在恆定時間內完成
    • 每個處理器擁有自己的鎖和自己的可執行佇列
    • 儘量將同一組任務分配給同一個cpu連續執行,減少在cpu間移動程式
    • 加強互動效能,即使系統負載,也保證系統響應
    • 保證公平。消除飢餓執行緒,減少大量時間片程式

3.2 可執行佇列

  • 可執行佇列資料結構為kernel/sched.c檔案下的runqueue
  • 表示給定處理器上的可執行程式連結串列,每個處理器一個
    linux核心設計與實現
  • 可執行佇列是排程程式核心的資料結構,提供很多巨集獲取該指標
    • cpu_rq(processor):返回給定處理器可執行佇列指標
    • this_rq:當前處理器可執行佇列指標
    • task_rq:給定任務所在佇列指標
  • 對佇列進行操作時,需要鎖住佇列,加鎖函式
    • task_rq_lock
    • task_rq_unlock
    • this_rq_lock
    • this_rq_unlock
    • double_rq_lock
    • double_rq_unlock

3.3 優先順序陣列

  • 每個可執行佇列都有兩個優先順序陣列,一個活躍的和一個過期的
  • 資料結構為kernel/sched.c檔案下的prio_array
  • 能提供O(1)級演算法複雜度的資料結構
  • 優先順序陣列使可執行處理器的每一種優先順序都包含一個相應的佇列,該佇列包含該優先順序上可執行程式連結串列
  • 優先順序陣列還擁有一個優先順序點陣圖,幫助高效查詢最高優先順序可執行程式
    linux核心設計與實現
  • MAX_PRIO:系統擁有的優先順序個數,預設140
  • BITMAP_SISE:優先順序點陣圖陣列大小,unsigned long為32位,要表示140個優先順序,需要5個長整形,總共160位
  • 每個優先順序陣列都要包含一個點陣圖成員。開始時,所有點陣圖為0,當某個優先順序程式開始執行,相應點陣圖變為1,查詢最高優先順序就變為查詢點陣圖為1的第一個值,查詢時間恆定。函式為sched_find_first_bit
  • 要查詢給定優先順序任務,並輪詢時,只需要遍歷某個優先順序連結串列即可
  • nr_active表示優先順序陣列內可執行程式數目

3.4 重新計算時間片

  • 當所有執行緒時間片用完時,老版本linux採用迴圈遍歷計算的方式重新計算時間片
  • 新版本排程程式通過維護兩個優先順序陣列:活動陣列和過期陣列,過期時間耗盡的執行緒放入過期陣列,非同步計算過期陣列的時間片。最後只需要交換兩個陣列即可

3.5 計算優先順序和時間片

動態計算優先順序

  • 靜態優先順序由使用者指定後就不可更改,即前面介紹的nice值
  • 動態優先順序根據靜態優先順序和程式互動性函式關係計算而來
  • effective_prio函式返回一個程式的動態優先順序:該函式以nice值為基數,再根據互動程度加上獎懲值(-5~5)
  • 交換程度判斷:計算程式執行時間和休眠時間。task_struct的sleep_avg變數,預設為10ms,休眠時增加該值(休眠時長),執行時減少該值

動態計算時間片

  • 程式建立時,父子程式平分時間片。防止建立多個程式以搶佔更多cpu時間
  • 任務時間片用完時,根據任務的動態優先順序計算時間片。函式為task_timeslice。時間值根據優先順序值按比例縮放。最高200ms,最低10ms,預設為100ms
  • 如果一個互動性很強的程式(TASK_INTER_ACTIVE巨集),時間片用完後,會被再次放入活動陣列而不是過期陣列。避免出現需要互動卻因為沒有等到兩個陣列交換而執行不了。實現程式碼:scheduler_tick函式

3.6 睡眠和喚醒

  • 處於休眠的執行緒進入等待佇列,裡面儲存所有因等待某些事件發生的程式組成的簡單連結串列
  • 等待佇列的資料結構為wake_queue_head_t
  • 等待佇列的建立:DECLEAR_WAITQUEUE或init_waitqueue_head
  • 加入等待佇列:add_wait_queue
  • 與等待佇列相關的事件發生時,佇列的程式會被喚醒,使用wake_up函式

3.7 負載平衡程式

  • 負載平衡程式針對多處理器系統中可執行程式負載不均衡的情況
  • 函式為kernel/sched.c中的load_balance函式
  • 呼叫時機:可執行佇列為空時,定時呼叫(系統空閒時每隔1ms呼叫,其他情況每隔200ms呼叫)。單處理器不需要此函式。
  • 負載平衡呼叫時需要鎖定當前佇列,並且遮蔽中斷
  • load_balance操作步驟:
    • 呼叫find_busiest_queue,找到最繁忙的佇列。該佇列程式數最多。如果沒有超過當前佇列25%的佇列,直接結束返回
    • 從繁忙佇列中選擇一個優先順序陣列用來抽取程式,最好是過期陣列
    • 定址含有優先順序最高(值最小)的連結串列,把高優先順序的程式分散開
    • 找到連結串列中沒有在執行,且可移動,且不在快取記憶體中的程式,靠用pull_task將程式抽取到當前佇列
    • 只要佇列不平衡就執行以上步驟。最後釋放鎖。

4. 搶佔和上下文切換

4.1 概述

  • 上下文切換是從一個程式切換到另一個程式。
  • 上下文切換定義在kernel/sched.c中的context_switch函式,該函式完成兩項基本工作
    • 呼叫定義在include/asm/menu_context.h中的switch_mm,負責把虛擬記憶體從上一個程式對映切換到新程式中
    • 呼叫定義在include/asm/system.h中的switch_to,負責從上一個程式的處理器狀態切換到新程式的處理器狀態。包括儲存,恢復棧資訊和暫存器資訊
  • 核心提供need_resched標誌表明是否需要重新排程。某個程式用盡時間片時,schedular_tick會設定該標誌;當一個高優先順序程式進入可執行狀態時,try_to_wake_up也會設定該標誌
  • 每個程式都包含need_resched標誌

4.2 使用者搶佔

  • 使用者搶佔:核心返回使用者空間時,如果need_reshed被設定,導致呼叫schedule,會選擇一個更合適的程式執行的情況
  • 使用者搶佔在以下情況時發生:
    • 從系統呼叫返回使用者空間
    • 從中斷處理程式返回使用者空間

4.3 核心搶佔

  • 大部分Unix其他變體和大部分作業系統,不支援核心搶佔,核心程式碼需要一直執行直到完成
  • 2.6版本核心中,新增了核心搶佔。只要重新排程是安全的,就可以核心搶佔
  • 只要沒有持有鎖,重新排程就是安全的,可以核心搶佔
  • 持有鎖使用thread_info中的preempt_count計數器表示,使用鎖時數值加一,釋放鎖時數值減一
  • 核心搶佔在以下情況時發生:
    • 從中斷處理程式返回核心空間時
    • 當核心程式碼再一次具有可搶佔性時
    • 核心中的任務顯示呼叫schedule
    • 核心中的任務阻塞

5. 與排程相關的系統呼叫

linux核心設計與實現

  • sched_setscheduler:設定task_struct的policy和rt_priority值
  • sched_setaffinity:設定task_struct的cpus_allowed這個位掩碼標誌
  • sched_yield:將程式從活動佇列移動到過期佇列,以讓出執行時間

四. 系統呼叫

1. 概述

  • 系統呼叫提供核心和應用程式互動的介面
    linux核心設計與實現
  • 系統呼叫內部,函式宣告中要新增asmlinkage,通知編譯期僅從棧中提取函式引數
  • 系統呼叫在核心中均以sys_作為字首
  • linux中每個系統呼叫都和一個獨一無二的系統呼叫號關聯
  • 核心記錄系統呼叫表所有已註冊過的系統呼叫列表,儲存在sys_call——table中,以體系結構有關
  • linux核心設計優化簡潔,上下文切換時間極快,作業系統執行效率高

2. 系統呼叫處理程式

  • 使用者程式不能直接呼叫核心函式,以防止核心空間安全失控。而是通過中斷通知核心,讓核心代表程式去執行
  • 觸發軟中斷前,將呼叫號裝入eax暫存器
    linux核心設計與實現
  • 引數傳遞:在x86系統上,ebx、ecx、edx、esi、edi按照順序存放前五個引數。返回值通過eax暫存器返回
  • 核心空間和使用者空間資料拷貝函式:copy_to_user,copy_from_user

3. 系統呼叫上下文

  • current指標指向引發當前呼叫的程式
  • 執行系統呼叫時處於程式上下文
  • 程式上下文中,核心可以休眠(呼叫阻塞或schedule)並可以被搶佔

4. 系統呼叫的實現

  • linux不提倡多用途的系統呼叫,每個系統呼叫都應該有明確的用途
  • 介面應該儘量簡潔,引數少。力求穩定不做改動
  • 儘量為將來做考慮,儘量通用,不要做限制。“提供機制而不是策略”
  • 編寫完後要註冊到核心,成為真正可用的系統呼叫
    • 在系統呼叫最好加入一個表項。大部分位於entry.s檔案中。所有支援系統呼叫的硬體體系都要做
    • 定義系統呼叫號到include/asm/unist.h檔案中
    • 函式放入kernel檔案下某個位置,使之編譯進核心映像(不能被編譯稱模組)
  • 使用者空間如何訪問註冊的系統呼叫
    • 通常情況下,使用者通過包含標準標頭檔案,並和底層系統呼叫具體的c實現連結,就可以使用系統呼叫
    • 自定義系統呼叫在標誌標頭檔案中不存在,可以通過linux提供的巨集來呼叫:_syscalln,n代表需要傳遞的引數。該巨集有2+2n個引數,第一個代表返回值型別,第二個代表函式名稱,後續的是n個引數型別和引數名稱
    • 比如:open函式的系統呼叫,系統呼叫號為_NR_open,定義在<asm/unistd.h>中,內部被_syscall3巨集實現,呼叫open時,內部把巨集直接放入應用程式程式碼中

五. 中斷和中斷處理程式

1. 中斷

  • 中斷用於解決計算機處理器與硬體裝置處理速度不匹配的問題,硬體處理好任務後主動傳送訊號給處理器
  • 中斷本質是電訊號,由硬體裝置產生,送入中斷處理器的輸入引腳上。再由中斷控制器向處理器傳送訊號。處理器收到訊號就中斷工作去處理中斷
  • 每個中斷都有唯一都數字標識,稱為中斷請求(IRQ)線。比如:IRQ0是時鐘中斷,IRQ1是鍵盤中斷

2. 中斷處理程式

  • 響應特定中斷時,會執行的函式為中斷處理程式或中斷服務例程
  • 中斷處理程式是裝置驅動程式的一部分,裝置驅動程式是用於對裝置進行管理的核心程式碼
  • 與核心函式的區別:中斷處理程式是被核心呼叫來響應中斷的,執行與中斷上下文中
  • 中斷處理程式必須能快速執行,同時,我們要靠它完成大量的其他工作。這兩個目的是矛盾的。
  • 為了解決上述矛盾,將中斷處理程式切分為兩個半部
    • 上半部:接收到請求就立即執行,但執行少部分工作。中斷應答和硬體復位
    • 下半部:中斷處理程式返回時立即執行

3. 註冊中斷處理程式

linux核心設計與實現

  • irq:要分配的中斷號
  • handler:實際中斷處理程式。接受三個引數,返回irqreturn_t型別引數
  • irqflags:可以為0,或以下標識的位掩碼
    • SA_INTERRUPT:表明是快速中斷程式。主要用於時鐘中斷程式
    • SA_SAMPLE_RANDOM
    • SA_SHIRQ:共享中斷線
  • devname:中斷相關裝置的Ascii文字名稱,如鍵盤中段為“keyboard”
  • dev_id:用於共享中斷線
  • 該函式可能會休眠,不能在中斷上下文或不允許阻塞的程式碼中呼叫

4. 中斷處理機制

linux核心設計與實現

  • 裝置產生中斷,把電訊號傳送給中斷控制器
  • 中斷控制器把訊號發給處理器
  • 處理器中斷程式,跳到記憶體中預定義的位置執行。就是中斷程式入口點
  • 核心呼叫do_IRQ,對中斷進行應答
  • 相關函式位於arch/i386/kernel/entry.s,arch/i386/kernel/irq.c

5 中斷控制

  • linux提供一組介面用於操作機器上的中斷狀態,提供能夠禁止中斷系統或遮蔽中斷線的功能
  • 相關程式碼在<asm/system.h>, <asm/irq.h>中
  • 中斷控制的根源是提供同步,通過禁止中斷,確保中斷程式不會搶佔當前程式碼,也可以禁止核心搶佔
  • 禁止和啟用當前處理器中斷:local_irq_disable,local_irq_enable
  • 禁止(遮蔽)指定中斷線: disable_irq,disable_irq_nosync,enable_irq,synchronize_irq
  • 獲取中斷系統狀態:asm/system.h中的irqs_disable
  • 判斷是否處於中斷上下文中:asm/hardirq.h中的in_interrupt

六. 核心同步

1. 基本概念

  • 臨界區:訪問和操作共享資料的程式碼段
  • 競爭條件:多個執行執行緒處於同一個臨界區
  • 同步:避免併發和防止競爭條件
  • 為什麼需要同步:使用者程式會被排程程式搶佔和重新排程
  • 造成併發的原因有:
    • 中斷
    • 核心搶佔
    • 睡眠及使用者空間的同步
    • 多處理器
  • 同步問題的解決:加鎖
  • 什麼資料需要加鎖
    • 核心全域性變數
    • 共享資料
  • 死鎖;所有執行緒都在等待對方釋放鎖,任何執行緒都無法繼續進行

2. 核心同步方法

2.1 原子操作

  • 原子操作保證指令以原子方式執行
  • 原子操作通常是行內函數,通過內嵌彙編指令完成
  • 原子操作比其他同步方法給系統的開銷小
  • linux核心提供對整數和單獨對位進行的原子操作
  • 整數原子操作相關函式為asm/atomic.h檔案中
  • 位原子操作相關函式為asm/bitops.h

2.2 自旋鎖

  • 自旋鎖(spin lock)最多隻能被一個可執行執行緒持有。如果鎖未被佔用,執行緒立刻可以得到。如果被佔用,會一直迴圈等待鎖可用。
  • 自旋鎖可用於防止多個執行緒同時進入臨界區
  • 自旋時特別浪費cpu,所以不應該被長時間持有
  • 介面定義在<linux/spinlock.h>,具體與體系結構相關的實現在<asm/spinlock.h>
  • linux中自旋鎖不可用遞迴
  • 自旋鎖的使用
    spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;
    //普通請求鎖
    spin_lock(&mr_lock);
    //禁止中斷請求鎖
    spin_lock_irqsave(&mr_lock);
    //確保中斷是啟用的情況可用的方法
    spin_lock_irq(&mr_lock)
    /**臨界區**/
    spin_unlock_irq(&mr_lock)
    spin_unlock_irqrestore(&mr_lock);
    spin_unlock(&mr_lock)
    複製程式碼
  • 自旋鎖可以用在中斷處理程式中(訊號量不行,會導致休眠),在使用鎖之前,要禁止本地中斷,否則會導致死鎖

2.3 讀寫自旋鎖

  • 鎖用途可以明確分為讀鎖和寫鎖。
  • 一個或多個讀任務可以併發的持有讀者鎖
  • 用於寫的鎖只能被一個寫任務持有,且此時不能併發讀
  • 讀寫鎖使用
    rwlock_t mr_rwlock = RW_LOCK_UNLOCKED;
    read_lock(&mr_rwlock);
    /**只讀臨界區**/
    read_unlock(&mr_rwlock)
    
    write_lock(&mr_rwlock)
    /**讀寫臨界區**/
    write_unlock(&mr_rwlock)
    複製程式碼
  • 不能把讀鎖升級為寫鎖,會導致死鎖

2.4 訊號量

  • 訊號量是一種睡眠鎖
  • 同一時刻允許任意數量的鎖持有者
  • 訊號量數量為1時,稱為二值訊號量或互斥訊號量
  • 如果一個任務試圖獲取被佔用的訊號量,訊號量會將其推入等待佇列,讓其睡眠。當持有訊號量的程式釋放後,等待的任務被喚醒,獲得訊號量
  • 訊號量的特點
    • 適合鎖會被長時間持有的情況
    • 鎖被短時間持有,睡眠耗時可能比全部時間還長
    • 會睡眠,不能在中斷中呼叫
    • 佔有訊號量時不能佔用自旋鎖。自旋鎖不允許休眠
  • 訊號量支援兩個原子操作P和V,荷蘭語的探查和增加
  • 訊號量相關檔案:<asm/semaphore.h>
  • 使用訊號量
    //宣告訊號量
    static DECLARE_SEMAPHORE_GENERIC(name, count);
    //宣告互斥量
    static DECLARE_MUTET(name);
    
    //以指標方式初始化訊號量
    sema_init(sem, count);
    //以指標方式初始化互斥量
    init_MUTET(sem);
    
    //試圖獲得訊號量
    down_interruptible(&name)
    //釋放訊號量
    up(&name)
    複製程式碼

2.5 讀寫訊號量

  • 與讀寫鎖一樣
  • 相關檔案:<linux/rwsem.h>

2.6 完全變數

  • 提供代替訊號量的簡單解決方法
  • 相關檔案:<linux/completion>

2.7 Seq鎖

  • 提供一種簡單的機制,用於讀寫共享資料
  • 內部實現主要一個序列計數器
  • 鎖的初始化為0,寫資料時,會得到鎖,且序列值增加。讀資料之前和之後,序列號被讀取,如果相同則沒有被打斷,如果為偶數則沒有寫操作發生

2.8 屏障

  • 屏障(barriers)可以確保指令順序執行,禁止指令重排序
  • 重排序是因為現代處理器為了優化其傳送管道,打亂了分派和提交指令的順序
  • rmb方法提供“讀”記憶體屏障,rmb前面的載入操作不會排在rmb之後,反之亦然
  • wmb方法提供“寫”記憶體屏障,wmb前面的儲存操作不會排在wmb之後。
  • mb方法提供了“讀寫”屏障

七. 記憶體管理

1. 核心對記憶體的管理

1.1 頁

  • 核心把物理頁作為記憶體管理的基本單元。
  • 記憶體管理單元:MMU,管理記憶體,並把虛擬地址轉換為實體地址的硬體
  • 頁大小跟體系結構不同而不同,大多數32為體系支援4KB的頁,64位體系支援8KB的頁
  • 物理頁的資料結構位於<linux/mm.h>中的struct page。page與物理頁相關,而不是虛擬頁
    sturct page{
       unsigned long                flags; // 頁的狀態,是否髒,是否被鎖定。可表示32種狀態。定義與<linux/page-flags.h>
       atomic_t                     count; // 頁的引用計數,被使用了多少次。為0時,新的分配就可以使用它 
       struct list_head             list;
       struct address_space         *mapping; //指向與該頁有關的address_space物件
       unsigned long                index; 
       struct list_head             lru;
       union{
           struct pte_chain *chain;
           pte_addr_t   direct;
       }pte;
       unsigned long                private;
       void                         *virtual; //頁虛擬地址,虛擬記憶體地址
    }
    複製程式碼

1.2 區

  • 由於硬體限制,有些頁位於記憶體中特定的實體地址上,不能用於特定的任務,所以把也劃分為不同的區(zones)。
  • 區對有相似特性的頁進行分組。區的劃分沒有任何物理意義,僅僅是為了管理頁採取的邏輯分組
  • linux使用三種區:
    • ZONE_DMA:能執行DMA(直接記憶體訪問)的區
    • ZONE_NORMAL:能正常對映的區
    • ZONE_HIGHEM:不能永久對映到核心地址空間的“高階記憶體”
  • 區的資料結構位於<linux/mmzone.h>的struct zone

2. 頁相關介面

  • 核心提供了一種請求記憶體的底層機制,提供了訪問介面,介面都以頁為端午分配記憶體,定義與<linx/gfp.h>中
    // 分配2^order個連續的物理頁,並返回指標
    struct page* alloc_pages(unsigned int gfp_mask, unsigned int order)    
    // 將頁轉換為邏輯地址,頁是連續的,其他頁緊隨其後
    unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order)
    
    // 只獲取一頁,高階地址分配需要使用此函式
    struct page* alloc_page(unsigned int gfp_mask)    
    unsigned long get_free_page(unsigned int gfp_mask)
    
    //獲取填充內容為0的頁
    unsigned long get_zeroed_page(unsigned int gfp_mask)
    
    //釋放頁
    void __free_pages(struct page *page, unsigned int order)
    void free_pages(unsigned long addr, unsigned int order)
    void free_page(unsigned int order)
    
    // 與使用者空間的malloc函式類似,最通用的介面。提供用於獲得以位元組為單位的一塊核心記憶體
    // 定義與<linux/slab.h>中
    void *kmalloc(size_t siez, int flags)
    // kmalloc相反函式,釋放空間
    void kfree(const void *ptr)
    
    // 確保分配的頁在實體地址上是連續的
    // 定義與<linux/vmalloc.h>
    void* vmalloc(unsigned long size)
    //釋放空間
    void vfree(void *addr)
    複製程式碼
  • gfp_mask標誌,在<linx/gfp.h>中,分為三類:行為修飾符,區修飾符和型別
    • 行為修飾符:核心如何分配所需的記憶體
    • 區修飾符:從哪兒分配記憶體
    • 型別:組合了行為修飾符和區修飾符

3. slab

  • slab提供通用資料結構快取層的角色,slab會給每個處理器維持一個物件告訴快取(空閒連結串列)
  • 適用於需要建立和銷燬很大的資料結構
  • slab層把不同的物件劃分為所謂告訴快取組,每個告訴快取都存放不同型別的物件(每種物件型別對應一個快取記憶體)
  • 每個slab處於三種狀態之一:慢,部分或空
  • 多個salb組成一個快取記憶體,各種資料結構:
    • slab的:struct slab
    • 快取記憶體:kmem_cache_s
    • 滿連結串列:slabs_full
    • 部分連結串列:slabs_partial
    • 空連結串列:slabs_empty

4. 高階記憶體的對映

  • 高階記憶體的頁不能永久對映到核心地址空間中,所以某種標誌獲得的頁不可能有邏輯地址
  • x86系統上,高階記憶體中的頁被對映到3GB-4GB的邏輯地址
  • 對映相關介面:
    //對映一個給定的page結構到核心地址空間:
    void kmap(sturct page *page)
    
    //解除對映關係
    void kunmap(struct page* page)
    
    //臨時對映
    void *kmap_atomic(sturct page *page, enum km_type type)
    複製程式碼

八. 虛擬檔案系統

1. 基本概念

  • 虛擬檔案系統:VFS,提供檔案系統介面。通過VFS,可以利用標準的unix檔案系統呼叫堆不同介質的不同檔案進行操作
  • linux支援相當多的檔案系統(超過50種):
    • 本地檔案系統: ext2, ext3
    • 網路檔案系統:NFS,Coda
  • VFS中有四個主要的物件型別
    • 超級快物件,代表一個已安裝檔案系統
    • 索引節點物件,代表一個檔案
    • 目錄項物件,代表一個目錄項
    • 檔案物件,代表由程式開啟的檔案

2. 超級塊物件

  • 各種檔案系統都必須實現超級塊,該物件用於儲存特定檔案系統的資訊,通常對應於存放在磁碟特定扇區中的檔案系統控制塊
  • 超級塊資料結構定義與<linux/fs.h>中的super_block。
  • 超級塊中的s_op指向超級快的操作函式表,由super_operation結構體表示。檔案系統對超級塊操作時,會找到相應的操作方法
  • 也就是不同的檔案系統的資訊,通過往super_operation中註冊自己的針對檔案系統操作的方法,提供給VFS使用
  • 超級快相關程式碼位於<fs/super.c>中

3. 索引節點物件

  • 索引節點物件包含了核心在操作檔案或目錄時需要的全部資訊
  • 索引節點物件資料結構位於<linux/fs.h>中的struct inode
  • 代表了檔案系統中的一個檔案(包括普通檔案,管道等等)
  • 索引節點中的inode_operations項也非常重要,定義了操作索引節點物件的所有方法

4. 目錄項物件

  • 目錄項包括執行目錄相關的操作,比如路徑名查詢等
  • 目錄項資料結構位於<linux/dcache.h>中的struct dentry
  • 目錄項狀態包括:被使用,未被使用和負狀態
  • 目錄項還包括目錄項快取,包括:
    • 被使用的目錄項鍊表
    • 最近被使用的雙向連結串列
    • 雜湊表和相應的雜湊函式用來快速將給定路徑解析為相關目錄項物件
  • 目錄項操作定義在dentry_operation結構體中,位於<linux/dcache.h>

5. 檔案物件

  • 檔案物件定義與<linux/fs.h>中的struct file結構體
  • 檔案操作由file_operations結構體表示
  • 具體的檔案系統定義不同的實現

6. 其他資料結構

  • 與檔案系統相關的資料結構:struct file_system_type,描述特定檔案系統型別,如ext3或XFS
  • 安裝檔案系統的例項:vfsmount,<linux/mount.h>
  • 程式描述符的files指向的資料,包含程式相關的開啟的檔案及檔案描述符的等資訊:struct files_struct,<linux/file.h>
  • 程式描述符的fs指向的資料,包含檔案系統和程式資訊:struct fs_struct, <linx/fs_struct.h>
  • 程式描述符的namespace指向的資料,包含名稱空間資訊:struct namespace, <linx/namespace.h>

九. 塊IO層

1. 基本概念

  • 基本裝置型別包括:塊裝置,字元裝置。區別在於是否可以被隨機訪問
  • 塊裝置中最小的可定址單元是扇區,扇區大小一般是2的整數倍,最常見的大小是512位元組
  • 物理磁碟按扇區定址,檔案系統按塊進行訪問,塊是較高層次的抽象
  • 塊包含一個或多個扇區,但大小不超過一頁
  • 塊調入記憶體時,需要先載入到緩衝區,每個緩衝區與一個塊對應
  • 每個緩衝區有一個描述符,用buffer_head結構表示,稱為緩衝區頭。在檔案<linux/buffer_head.h>中。包括緩衝區狀態,使用計數,邏輯塊號,物理頁,塊大小等資訊

2. bio

  • 目前核心中塊IO操作基本容器由bio結構表示,位於<linux/bio.h>
    linux核心設計與實現

3. io排程程式

十. 程式地址空間

1. 基本概念

  • 每個程式都有唯一的地址空間,彼此之間互不干擾
  • 程式只能訪問有效範圍內的記憶體地址
  • 記憶體區域包含各種記憶體物件,包括:
    • 程式碼段:可執行檔案程式碼的記憶體對映
    • 資料段:已初始化全域性變數的記憶體對映
    • BSS段:未初始化全域性變數
    • 其他

2. 記憶體描述符

  • 核心使用記憶體描述符表示程式的地址空間
  • 記憶體描述符資料結構位於<linux/sched.h>中的mm_struct
  • 內部的mmap和mm_rb兩個資料結構表示的內容一樣,此處做了冗餘。前者為連結串列,便於高效遍歷所有;後者為紅黑樹,便於高效搜尋
  • 鎖都有的mm_struct都通過自身的mmlist連結在一個雙向連結串列中,首元素的init程式,init_mm描述符。操作連結串列時,要用mmlist_lock加鎖,鎖位於<kernel/fork.c>
  • 程式描述符中的mm欄位存放記憶體描述符
  • 分配記憶體描述符:copy_mm,內部呼叫allocate_mm巨集從mm_cachep slab快取分配
  • 核心執行緒沒有程式地址空間,mm欄位為空

3. 記憶體區域

  • 記憶體區域由vm_area_struct表示,定義在<linux/mm.h>中,也稱虛擬記憶體區域
  • vm_area_struct描述了指定地址空間內連續空間上的一個獨立記憶體範圍
  • vma包括很多標誌位,標誌了所含頁面的行為和資訊
    • VM_READ:頁面讀許可權
    • VM_WRITE:頁面寫許可權
    • VM_EXEC:頁面可執行許可權
  • vm_area_struct結構體的vm_ops指向與指定記憶體區域相關的操作函式表,由vm_operations_struct表示

十一. 頁快取記憶體和頁回寫

1. 基本概念

  • 頁快取記憶體是linux實現的一種磁碟快取,主要用來減少對磁碟的io操作
  • 通過把磁碟中的資料快取到實體記憶體中,把對磁碟的訪問變為對實體記憶體的訪問
  • 磁碟快取記憶體的意義:
    • 加快訪問速度,記憶體速度大於磁碟
    • 臨時區域性原理:資料一旦被訪問,很有可能在短期內再次被訪問
  • 執行io操作前,核心會檢查資料是否已經在頁快取記憶體中,如果在就可以立馬返回

2. 頁快取記憶體資料結構

  • 頁快取記憶體使用address_space結構體描述頁快取記憶體中的頁面,定義與<linux/fs.h>中
  • 內部的a_ops指向地址空間物件中的操作函式表,由address_space_operations結構體表示
  • 檢查頁是否在快取記憶體中操作必須高效,每個address_struct提供的基樹(radix tree),page_tree的二叉樹,記錄了檔案偏移量。以達到快速檢索的目的。基樹程式碼在<lib/radix-tree.c>

3. 頁回寫

  • pdflush後臺例程負責將記憶體的髒頁寫回磁碟,寫的時機包括
    • 空閒記憶體低於某個值,以釋放記憶體。閥值可配置
    • 髒頁駐留時間超過特定值
    • 週期性回寫,防止系統異常資料丟失
  • pdflush程式碼在<mm/pdflush.c>中。回寫機制程式碼在<mm/page-writeback.c>,<fs/fs-writeback.c>

十二. 其他

定時器和時間管理

  • 週期性時間由系統定時器驅動,是一種可程式設計硬體晶片,能以固定頻率產生中斷(定時器中斷)
  • 實際時間:定義在<kernel/time.c>中。struct timespec xtime; timespec資料結構定義在檔案<linux/time.h>中
  • 定時器:定義在<linux/timer.h>中,由結構time_list

相關文章