Lec 04 系統呼叫
(參考來源:上海交通大學並行與分散式系統研究所+作業系統課程ppt)
Creative Commons Attribution 4.0 License
Contents
4.1 系統呼叫
- 硬體提供了一對指令svc/eret指令在使用者態/核心態間切換
- 系統呼叫
(1) 使用者與作業系統之間,類似於過程呼叫的介面
(2) 透過受限的方式訪問核心提供的服務
4.1.2 AArch64下常見的Linux系統呼叫
4.1.3 系統呼叫舉例
int main()
{
write(1, "hello, world\n", 13);
_exit(0);
}
轉換成組合語言
.section .rodata
.LC0:
.string "hello, world\n"
.text
.align 2
.global main
.type main, %function
main:
// First, call write(1, "hello, world\n", 13)!
movq x8, #0x40 // write is system call 64
movq x0, #0x1 // Arg1:stdout has descriptor 1
adrp x3, .LC0
add x1,x3,:lo12:.LC0 // Arg2:Hello world string
movq x2, #0xd // Arg3:string length
svc // Make the system call
// Next, call exit(0)
movq x8, #0x5d // _exit is system call 93
movq x0, #0x0 // Arg1:exit status is 0
svc // Make the system call
4.1.3 系統呼叫的引數傳遞(常見軟體的約定)
- 最多允許8個引數
(1) x0-x7暫存器
(2) x8用於存放系統呼叫編號 - 返回值存放於x0暫存器中
4.1.4 系統呼叫返回值與errno
-
系統呼叫透過暫存器嚮應用傳遞返回值
(1) 一般設定為 -errno
(2) 庫對系統呼叫的 wrapper code 會將系統呼叫的返回值轉換為庫函式形式的返回值 -
暫存器放不下,只能透過記憶體傳參
(1) 將引數放在記憶體中,將指標放在暫存器中傳給核心
(2) 核心透過指標訪問相關引數
(3) 存在安全的隱患(後續課程會進一步介紹)
4.1.4 如何跟蹤系統呼叫
4.1.5 系統呼叫流程圖
4.2 系統呼叫最佳化(Virtual Dynamic Shared Object)
核心將一部分資料透過只讀的形式共享給應用,允許應用直接讀取。
4.2.1 動機
- 系統呼叫的時延不可忽略
(1) 尤其是呼叫非常頻繁的情況
(2) 系統呼叫實際執行邏輯很簡單 - 如何降低系統呼叫的時延?
(1) 特權級切換造成的時間開銷
(2) 如果沒有特權級切換,那麼就不需要儲存恢復狀態
4.2.2 舉例:gettimeofday
- 核心定義
(1) 在編譯時作為核心的一部分 - 使用者態執行
(2) 將gettimeofday的程式碼載入到一塊與應用共享的記憶體頁
(3) 這個頁稱為:vDSO
-- Virtual Dynamic Shared Object
(4) Time 的值同樣對映到使用者態空間(只讀)
-- 只有在核心態才能更新這個值
4.2.3 Linux的vDSO
4.3 系統呼叫最佳化:FLEX-SC
4.3.1 動機
- 如何進一步降低系統呼叫的時延?
(1) 不僅僅是 gettimeofday() - "時間都去哪兒了?"
(1) 大部分是用來做狀態的切換
(2) 儲存和恢復狀態 + 許可權的切換
(3) Cache pollution - 是否有可能在不切換狀態的情況下實現系統呼叫?
4.3.2 Flexible System Call
- 一種新的syscall機制
(1) 引入 system call page ,由 user & kernel 共享
(2) 使用者程序可以將系統呼叫的請求 push 到 system call page
(3) 核心會從system call page poll system call 請求 - Exception-less syscall
(1) 將系統呼叫的呼叫和執行解耦,可分佈到不同的CPU核
4.3.3 exception less system call
舉例
Kernel 填充syscall的返回值
4.3.4 單核上的單執行緒/多執行緒系統呼叫
- 單核心單執行緒
(1) 使用者應用將多個系統呼叫推至系統呼叫頁表
(2) 切換到核心執行緒,核心執行緒從系統呼叫頁表拉取系統呼叫。
(3) 執行完系統呼叫後切換到應用態 - 單核心多執行緒
(1) 執行緒1將系統呼叫推至系統呼叫頁表,並且切換到下一個執行緒,直到所有執行緒推送結束。
(2) 下陷到核心態。核心拉取頁表中的system call。
(3) 執行完所有系統呼叫後返回核心態。
4.4 從應用視角看作業系統抽象
4.4.1 程序
1. 分時複用有限的CPU資源
(1) CPU核心數量少於應用程式數量,如何執行?
(2) 單個CPU核心如何執行多個應用程式?
- 分時複用CPU
(1) 讓多個應用程式輪流使用處理器核心
(2) 何時切換:作業系統決定
-- 執行時間片(例如100ms)
(3) 高頻切換:看起來是多個應用“同時”執行
2. 應用程式與程序
- 通常一個應用程式對應一個程序
(1) 在shell中輸入可執行檔案的名稱
-- shell建立新程序,可執行檔案在新程序中執行
(2) 在圖形介面雙擊應用圖示 - 多程序程式:應用程式亦可自行建立新程序
(1) 建立新程序,在新程序中執行其他應用程式或與自己一樣的程式
3. 程序在作業系統中的實現:狀態資料
- 作業系統提供程序的抽象用於管理應用程式
(1) 程序標識號(Process ID, PID)
(2) 執行狀態
-- 處理器上下文(CPU Context)
(3) 地址空間
(4) 開啟的檔案
4. 程序展示效果
- 程序抽象為應用程式提供了“獨佔CPU”的假象
(1) 程式開發不用考慮如何與其他程式共享CPU
(2) 簡化程式設計 - 程序相關的系統呼叫
(1) 建立程序
(2) 讓程序執行指定的程式
(3) 退出程序
(4) 程序間通訊
4.5 程序切換
4.5.1 處理器上下文(CPU Context)
- 作業系統為每個程序維護處理器上下文
(1) 包含恢復程序執行所需要的狀態
(2) 思考:程序A執行到main函式任意一條指令,切換到程序B執行,一段時間後,再切回到程序A執行
-- 為完成此過程,有哪些狀態需要儲存?
(3) 具體包括:
-- PC暫存器值,棧暫存器值,通用暫存器值,狀態暫存器值
4.5.2 程序切換的時機
-
異常導致的上下文切換
(1) Timer中斷(如基於時間片的多工排程) -
使用者執行系統呼叫並進入核心
(1) 如:read/sleep等會導致程序阻塞的系統呼叫
(2) 即使系統呼叫不阻塞執行,核心也可以決定執行上下文切換,而不是將控制權返回給呼叫程序
舉例1
舉例2
4.6 程序相關介面
4.6.1 獲取程序ID
- 程序 ID
(1) 每個程序都有唯一的正數PID - Getpid()
(1) 返回撥用程序的PID - Getppid()
(1) 返回撥用程序父程序的PID
(2) 父程序:建立該程序的程序
4.6.2 Exit 函式
exit函式終止程序並帶上一個status狀態
4.6.3 Fork 函式
- 呼叫一次
(1) 在父程序中 - 返回兩次
(1) 在父程序中,返回子程序的PID
(2) 在子程序中,返回0 - 返回值提供了唯一明確地區分父程序和子程序執行的方法
4.6.4 execve 函式
- 載入和執行
(1) filename:可執行檔名;argv:引數列表,envp:環境變數列表 - execve 只呼叫一次,且永遠不會返回
(1) 僅僅在執行報錯的時候,返回撥用程式
(2) 例:找不到filename標識的檔案
4.6.5 Linux下的殭屍程序
- 程序終止後,核心不會立刻銷燬該程序
(1) 不再執行,但仍然佔用記憶體資源 - 程序以終止態存在,等待父程序回收
- 當父程序回收終止的子程序
(2) 核心把子程序的exit狀態傳遞給父程序
(3) 核心移除子程序,此時子程序才被真正回收 - 終止狀態下還未被回收的程序就是殭屍程序
- 如果父程序在自己終止前沒有回收殭屍子程序
(4) 核心會安排init程序回收這些子程序 - init程序
(5) PID為1
(6) 在系統初始化時由核心建立
4.6.6 waitpid函式
返回值:成功返回子程序 PID,出錯返回 -1
- pid>0:等待集合中只有pid子程序
- pid=-1:等待集合包括所有子程序
- 如果沒有子程序:返回-1,errno = ECHILD
- 如果等待被中斷:返回-1,errno = EINTR
- options=0
(1) 掛起呼叫程序,等待集合中任意子程序終止
(2) 如果等待集合中有子程序在函式呼叫前已經終止,立刻返回
(3) 返回值是導致函式返回的終止子程序pid
(4) 該終止子程序被核心回收 - options=WNOHANG
(1) 如果等待集合中沒有終止子程序,立刻返回0 - options=WUNTRACED
(2) 除了返回終止子程序的資訊外,還返回因訊號而停止的子程序資訊 - options=WNOHANG|WUNTRACED
(1) 帶回被回收子執行緒的exit狀態
(2) status指標不為NULL
(3) status包含導致子程序進入終止狀態的資訊
(4) wait.h檔案包含了若干宏定義,用於解釋status
4.6.7 小結
4.7 記憶體
4.7.1 虛擬記憶體
-
虛擬地址空間
(1) 應用程序使用虛擬地址訪問記憶體
(2) 所有應用程序的虛擬地址空間都是統一的(方便開發) -
地址翻譯
(1) CPU按照OS配置的規則把虛擬地址翻譯成實體地址
(2) 翻譯對於應用程序是不可見的(無需關心)
4.7.2 虛擬記憶體和實體記憶體
虛擬記憶體具有獨立而統一的地址空間
5. ELF 檔案格式
5.1 目標檔案
- 可執行目標檔案
- 可重定位目標檔案(.o)
- 共享目標檔案(.so)
(1) 特殊的可重定位目標檔案
(2) 可以載入到記憶體中
(3) 支援動態連結:載入時或執行時
5.2 ELF:可執行可連結格式
- 目標檔案的標準二進位制格式
統一格式,又被稱為ELF二進位制檔案 - 用於BSD Unix和Linux
最早用於AT&T System
5.3 ELF格式:可重定位目標檔案
(1) ELF 頭部(ELF Header)
- ELF檔案的第一個部分
- 通常用於存後設資料
-- Magic number (‘0x7f’ ‘E’ ‘L’ ‘F’)
-- 型別(.o, .so, 可執行)
-- 機器架構
-- 位元組順序(大小端)
-- 節頭部表的位置(檔案內偏移)
(2) 節頭部表(Section Header Table)
- 節(section)
-- ELF檔案中除了頭部和頭部表劃分為若干區域
-- 每一個節在檔案中時一塊連續的位元組(可能為空)
-- 互不重疊 - 每一個節都有一個節頭部描述
-- 節頭部的一項
- sh_name
-- 節名稱(在.strtab節中的偏移) - sh_addr
-- 節在載入到記憶體後,在記憶體的起始地址 - sh_offset
-- 節在檔案中的偏移(位元組數) - sh_size
-- 節的大小(位元組數) - sh_addralign
-- 對齊要求
(3) ELF字串表(.strtab)
- 記錄一系列C風格字串
-- 以'\0’結尾 - 表示符號名或節名
-- 使用時記錄字串在表中的偏移(index)
(4) 用以除錯的節
- .debug
-- 除錯符號表,包括變數(全域性、區域性)、typedef、C原始檔
-- gcc –g生成 - .line
-- C原始檔的行數與.text節中指令的對映
(5) 程式碼與資料節
- .text:程式碼
- .rodata:只讀資料
- .data:初始化的全域性變數和靜態變數
- .bss:未初始化的全域性變數和靜態變數
-- “Block Started by Symbol”,“Better Save Space”
-- 不佔檔案空間,但是在節頭部表中有記錄
-- 執行時分配記憶體,預設為0
5.4 可執行目標檔案
- ELF頭部(ELF header)
-- 整體資訊
-- 程式入口 (e_entry)
-- 程式頭部表資訊- 在檔案中的起始位置(e_phoff)
- 大小(e_ehsize)
- 每個條目的大小(e_phentsize)
- 條目數量(e_phnum)
程式頭部表
- Program (Segment) Header Table
p_type - PT_LOAD (1): 可載入段
p_flags
-- 執行時許可權 (rwx) - p_offset
-- 段在檔案中的起始偏移 - p_filesz
-- 段在檔案中的大小( 段在記憶體中的大小p_memsz) - p_vaddr
-- 段在記憶體中的起始(虛擬)地址 - p_paddr (不常用,x86、ARM 下不用)
- p_memsz
-- 段在記憶體中的大小(p_filesz) - p_align
-- 段起始地址的對齊要求
通常為2^12 (4K) 或 2^21 (2M)
6. 檔案
6.1 UNIX 檔案
- Unix 檔案是一串位元組序列。
-- \(B_0,B_1,\ldots,B_k,\ldots,B_m\) - 所有的IO裝置都被抽象成檔案。
-- 如:網路裝置,硬碟,終端
-- Unix提供一個基於檔案的底層應用介面,即UNIX I/O - 所有輸入,輸出都是透過讀,寫檔案完成
-- 所有的輸入輸出都具有統一的表現形式
6.2 檔案型別1
- 普通檔案(regular file):包含任意資料
-- 從應用程式的角度來看有兩種- 文字檔案:僅包含ASCII和UNICODE字元
- 二進位制檔案:除了文字檔案以外的所有
-- 從核心的角度來看沒有區別
6.3 檔案型別2
- 目錄(directory)也是一個檔案
-- 由有連結(links)構成
-- 每個連結將一個檔名對映到一個檔案(或目錄)
-- 每個目錄至少有兩個連結:- . (dot):到資料夾本身的連結
- . . (dot-dot):到上一層資料夾的連結
-- 目錄相關指令:mkdir、ls、rmdir等
6.4 目錄層級
- Linux核心使用層次化目錄來組織所有檔案
-- /:代表根目錄
-- 每個檔案都是根目錄直接或間接的後代
6.5 檔案型別3
- 套接字(Socket)也是檔案,用於跨網路程序互動
- 其他檔案型別包括:
-- 命名管道(named pipes)
-- 符號連線(symbolic links)
-- 字元/塊裝置(character/block devices)
...
6.6 開啟檔案
- 應用準備訪問一個IO裝置
-- 核心開啟相關檔案,並返回一個非負整數,作為檔案識別符號(file descriptor, fd)- fd代表該檔案,用於之後對檔案進行操作
-- 核心跟蹤記錄每個程序的所有開啟檔案的資訊 - 對於每個開啟檔案,維護一個檔案內偏移k
- 應用可以透過seek函式,顯式改變當前檔案內偏移k
- 應用只需要記錄核心返回的檔案識別符號
- fd代表該檔案,用於之後對檔案進行操作
6.7 關閉檔案
- 不再需要訪問檔案
- 核心操作如下
-- 釋放在檔案開啟時建立的資料結構
-- 把檔案識別符號返回到可用的識別符號池 - 程序終止時的預設行為
-- 核心關閉所有開啟的檔案
-- 核心釋放記憶體資源
6.8 讀寫檔案
- 讀操作:
-- 從檔案中複製m>0個位元組到記憶體中- 從當前檔案的位置k開始,並更新k+=m
-- 如果從k開始到檔案末尾的長度小於m,觸發條件end-of-file(EOF) - EOF可以被應用檢測
- 但是檔案末尾實際上不存在EOF字元
- 從當前檔案的位置k開始,並更新k+=m
6.9 檔案:對所有裝置的抽象
- 儲存裝置
-- File - 網路裝置
-- socket - 其他裝置
-- 同樣是fd