linux內核設計與實現

一. 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

2.2 進程描述符如何分配

  • linux經過slab分配其分配task_struct結構,這樣能達到對象複用和緩存着色
  • 經過預先分配和重複使用task_struct,避免動態分配和釋放帶來的性能損耗,這也是爲何建立進程快的緣由
  • task_struct放在內核棧的尾端,爲了讓寄存器少的硬件體系只經過棧指針就能算出位置,避免使用額外寄存器存儲
  • slab分配器在內核棧的尾部建立新的struct thread_info,內部的task指向實際的task_struct。thread_info位置:<asm/thread_info.h>

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:中止。進程中止執行
  • 狀態變遷圖
  • 設置當前進程: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
  • 表示給定處理器上的可執行進程鏈表,每一個處理器一個
  • 可執行隊列是調度程序核心的數據結構,提供不少宏獲取該指針
    • 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)級算法複雜度的數據結構
  • 優先級數組使可運行處理器的每一種優先級都包含一個相應的隊列,該隊列包含該優先級上可執行進程鏈表
  • 優先級數組還擁有一個優先級位圖,幫助高效查詢最高優先級可執行進程
  • 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. 與調度相關的系統調用

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

四. 系統調用

1. 概述

  • 系統調用提供內核和應用程序交互的接口
  • 系統調用內部,函數聲明中要添加asmlinkage,通知編譯期僅從棧中提取函數參數
  • 系統調用在內核中均以sys_做爲前綴
  • linux中每一個系統調用都和一個獨一無二的系統調用號關聯
  • 內核記錄系統調用表全部已註冊過的系統調用列表,存儲在sys_call——table中,以體系結構有關
  • linux內核設計優化簡潔,上下文切換時間極快,操做系統執行效率高

2. 系統調用處理程序

  • 用戶程序不能直接調用內核函數,以防止內核空間安全失控。而是經過中斷通知內核,讓內核表明程序去執行
  • 觸發軟中斷前,將調用號裝入eax寄存器
  • 參數傳遞:在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. 註冊中斷處理程序

  • irq:要分配的中斷號
  • handler:實際中斷處理程序。接受三個參數,返回irqreturn_t類型參數
  • irqflags:能夠爲0,或如下標識的位掩碼
    • SA_INTERRUPT:代表是快速中斷程序。主要用於時鐘中斷程序
    • SA_SAMPLE_RANDOM
    • SA_SHIRQ:共享中斷線
  • devname:中斷相關設備的Ascii文本名稱,如鍵盤中段爲「keyboard」
  • dev_id:用於共享中斷線
  • 該函數可能會休眠,不能在中斷上下文或不容許阻塞的代碼中調用

4. 中斷處理機制

  • 設備產生中斷,把電信號發送給中斷控制器
  • 中斷控制器把信號發給處理器
  • 處理器中斷程序,跳到內存中預約義的位置執行。就是中斷程序入口點
  • 內核調用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>

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
相關文章
相關標籤/搜索