深入Linux系統(tǒng)調(diào)用:原理、機(jī)制與實(shí)戰(zhàn)全解析
在 Linux 操作系統(tǒng)中,系統(tǒng)調(diào)用是連接用戶程序與內(nèi)核的關(guān)鍵橋梁。當(dāng)我們?cè)诮K端輸入命令,或是運(yùn)行各類應(yīng)用程序時(shí),背后都離不開系統(tǒng)調(diào)用的支撐。它不僅是內(nèi)核為用戶空間提供服務(wù)的核心接口,更是理解 Linux 運(yùn)行機(jī)制的重要鑰匙。若將操作系統(tǒng)比作精密的工廠,內(nèi)核便是掌控全局的中樞,而系統(tǒng)調(diào)用則是連接生產(chǎn)線(用戶程序)與中樞的通道。用戶程序通過它向內(nèi)核請(qǐng)求服務(wù),無論是文件操作、進(jìn)程管理,還是內(nèi)存分配等任務(wù),系統(tǒng)調(diào)用都發(fā)揮著不可或缺的作用。
從本質(zhì)上看,系統(tǒng)調(diào)用是內(nèi)核提供的接口,常以 C 函數(shù)形式呈現(xiàn),方便開發(fā)者調(diào)用系統(tǒng)功能。在 Linux 系統(tǒng)里,它構(gòu)建了用戶程序安全訪問硬件資源的通道。由于硬件資源由內(nèi)核底層操作,若用戶程序直接訪問,易引發(fā)系統(tǒng)不穩(wěn)定甚至崩潰,而系統(tǒng)調(diào)用就像門衛(wèi),保障了系統(tǒng)的安全與穩(wěn)定。在多任務(wù)管理方面,系統(tǒng)調(diào)用如同工廠調(diào)度員。內(nèi)核借助它合理調(diào)度 CPU 時(shí)間,讓多個(gè)進(jìn)程協(xié)同工作,顯著提升系統(tǒng)并發(fā)處理能力,確保系統(tǒng)流暢運(yùn)行。
對(duì)開發(fā)者和系統(tǒng)管理員而言,掌握系統(tǒng)調(diào)用至關(guān)重要。開發(fā)者編寫應(yīng)用程序時(shí),可通過 open、read 等系統(tǒng)調(diào)用實(shí)現(xiàn)文件操作,用 fork、exec 進(jìn)行進(jìn)程管理;系統(tǒng)管理員則能借助 sysinfo 獲取系統(tǒng)性能信息,使用 ioctl 配置管理設(shè)備。掌握系統(tǒng)調(diào)用,就能更好地與 Linux 交互,釋放系統(tǒng)潛能。接下來,就讓我們一起深入探索 Linux 系統(tǒng)調(diào)用的奇妙世界,層層揭開它神秘的面紗,探尋它的原理與實(shí)現(xiàn)機(jī)制,看看這個(gè)強(qiáng)大的工具是如何在幕后掌控整個(gè)操作系統(tǒng)的運(yùn)行,為我們的日常操作和開發(fā)工作保駕護(hù)航的。
一、Linux 系統(tǒng)調(diào)用是什么?
1.1 系統(tǒng)調(diào)用的定義
系統(tǒng)調(diào)用,顧名思義,說的是操作系統(tǒng)提供給用戶程序調(diào)用的一組“特殊”接口。用戶程序可以通過這組“特殊”接口來獲得操作系統(tǒng)內(nèi)核提供的服務(wù),比如用戶可以通過文件系統(tǒng)相關(guān)的調(diào)用請(qǐng)求系統(tǒng)打開文件、關(guān)閉文件或讀寫文件,可以通過時(shí)鐘相關(guān)的系統(tǒng)調(diào)用獲得系統(tǒng)時(shí)間或設(shè)置定時(shí)器等。
從邏輯上來說,系統(tǒng)調(diào)用可被看成是一個(gè)內(nèi)核與用戶空間程序交互的接口——它好比一個(gè)中間人,把用戶進(jìn)程的請(qǐng)求傳達(dá)給內(nèi)核,待內(nèi)核把請(qǐng)求處理完畢后再將處理結(jié)果送回給用戶空間。系統(tǒng)服務(wù)之所以需要通過系統(tǒng)調(diào)用來提供給用戶空間的根本原因是為了對(duì)系統(tǒng)進(jìn)行“保護(hù)”,因?yàn)槲覀冎繪inux的運(yùn)行空間分為內(nèi)核空間與用戶空間,它們各自運(yùn)行在不同的級(jí)別中,邏輯上相互隔離。所以用戶進(jìn)程在通常情況下不允許訪問內(nèi)核數(shù)據(jù),也無法使用內(nèi)核函數(shù),它們只能在用戶空間操作用戶數(shù)據(jù),調(diào)用用戶空間函數(shù)。
比如我們熟悉的“hello world”程序(執(zhí)行時(shí))就是標(biāo)準(zhǔn)的用戶空間進(jìn)程,它使用的打印函數(shù)printf就屬于用戶空間函數(shù),打印的字符“hello word”字符串也屬于用戶空間數(shù)據(jù)。但是很多情況下,用戶進(jìn)程需要獲得系統(tǒng)服務(wù)(調(diào)用系統(tǒng)程序),這時(shí)就必須利用系統(tǒng)提供給用戶的“特殊接口”——系統(tǒng)調(diào)用了,它的特殊性主要在于規(guī)定了用戶進(jìn)程進(jìn)入內(nèi)核的具體位置;換句話說,用戶訪問內(nèi)核的路徑是事先規(guī)定好的,只能從規(guī)定位置進(jìn)入內(nèi)核,而不準(zhǔn)許肆意跳入內(nèi)核。有了這樣的陷入內(nèi)核的統(tǒng)一訪問路徑限制才能保證內(nèi)核安全無虞。
我們可以形象地描述這種機(jī)制:作為一個(gè)游客,你可以買票要求進(jìn)入野生動(dòng)物園,但你必須老老實(shí)實(shí)地坐在觀光車上,按照規(guī)定的路線觀光游覽。當(dāng)然,不準(zhǔn)下車,因?yàn)槟菢犹kU(xiǎn),不是讓你丟掉小命,就是讓你嚇壞了野生動(dòng)物。
1.2 存在的必要性
系統(tǒng)調(diào)用的存在,有著諸多至關(guān)重要的原因,每一點(diǎn)都與操作系統(tǒng)的穩(wěn)定、安全和高效運(yùn)行息息相關(guān)。
(1)保護(hù)系統(tǒng)資源
操作系統(tǒng)的資源,無論是硬件資源,如 CPU、內(nèi)存、磁盤等,還是軟件資源,像文件、進(jìn)程等,都是極其珍貴且需要嚴(yán)格保護(hù)的。內(nèi)核如同一位嚴(yán)謹(jǐn)?shù)墓芗遥卫握瓶刂@些資源。如果用戶程序能夠隨意直接訪問這些資源,就好比未經(jīng)授權(quán)的人隨意進(jìn)出管家的核心管理區(qū)域,必然會(huì)引發(fā)混亂,導(dǎo)致系統(tǒng)的安全性和穩(wěn)定性遭受嚴(yán)重威脅。
而系統(tǒng)調(diào)用就像是管家設(shè)置的一扇安全門,用戶程序必須通過系統(tǒng)調(diào)用這一正規(guī)途徑向內(nèi)核提出資源訪問請(qǐng)求,內(nèi)核會(huì)依據(jù)一系列嚴(yán)格的規(guī)則,如用戶權(quán)限、資源使用狀態(tài)等,對(duì)請(qǐng)求進(jìn)行細(xì)致的審查和合理的裁決,只有符合條件的請(qǐng)求才會(huì)被批準(zhǔn),從而有效避免了用戶程序?qū)ο到y(tǒng)資源的非法或不當(dāng)訪問,保障了系統(tǒng)的穩(wěn)定和安全。
(2)提供統(tǒng)一接口
在計(jì)算機(jī)系統(tǒng)中,硬件設(shè)備的種類繁雜多樣,不同的硬件設(shè)備有著各自獨(dú)特的操作方式和特性。這就好比一個(gè)大型工廠里有著各種不同類型的機(jī)器設(shè)備,每臺(tái)設(shè)備的操作方法都不一樣。如果沒有一個(gè)統(tǒng)一的接口,用戶程序在訪問不同硬件設(shè)備時(shí),就需要針對(duì)每種設(shè)備編寫復(fù)雜且差異巨大的代碼,這無疑會(huì)極大地增加編程的難度和復(fù)雜性,降低開發(fā)效率。而系統(tǒng)調(diào)用就像是工廠里的統(tǒng)一操作指南,為用戶空間提供了一種簡潔、統(tǒng)一的硬件抽象接口。
無論用戶程序需要訪問何種硬件設(shè)備,都只需通過相應(yīng)的系統(tǒng)調(diào)用,而無需深入了解硬件設(shè)備的底層細(xì)節(jié),這使得編程變得更加簡單、高效,也提高了應(yīng)用程序的可移植性,就像按照統(tǒng)一操作指南,工人可以輕松操作不同的設(shè)備,而無需為每種設(shè)備單獨(dú)學(xué)習(xí)復(fù)雜的操作方法。
(3)實(shí)現(xiàn)多任務(wù)和虛擬內(nèi)存管理
在現(xiàn)代操作系統(tǒng)中,多任務(wù)處理和虛擬內(nèi)存管理是至關(guān)重要的功能。多任務(wù)處理就像一位優(yōu)秀的調(diào)度員同時(shí)安排多個(gè)任務(wù)并行執(zhí)行,讓多個(gè)進(jìn)程能夠在同一時(shí)間內(nèi)有條不紊地運(yùn)行,充分利用系統(tǒng)資源,提高系統(tǒng)的并發(fā)處理能力。虛擬內(nèi)存管理則如同一個(gè)智能的內(nèi)存分配助手,為每個(gè)進(jìn)程分配獨(dú)立的虛擬地址空間,使得進(jìn)程在運(yùn)行時(shí)仿佛擁有了自己獨(dú)立的內(nèi)存空間,互不干擾。而這些功能的實(shí)現(xiàn),都離不開系統(tǒng)調(diào)用的支持。系統(tǒng)調(diào)用能夠讓內(nèi)核清晰地了解每個(gè)進(jìn)程的運(yùn)行狀態(tài)和資源需求,從而合理地調(diào)度 CPU 時(shí)間,精準(zhǔn)地分配內(nèi)存資源。
當(dāng)一個(gè)進(jìn)程通過系統(tǒng)調(diào)用請(qǐng)求創(chuàng)建新進(jìn)程時(shí),內(nèi)核會(huì)根據(jù)系統(tǒng)的資源狀況和調(diào)度策略,為新進(jìn)程分配必要的資源,并將其納入多任務(wù)管理的范疇;在內(nèi)存管理方面,進(jìn)程通過系統(tǒng)調(diào)用請(qǐng)求內(nèi)存分配時(shí),內(nèi)核會(huì)依據(jù)虛擬內(nèi)存管理機(jī)制,為進(jìn)程分配合適的虛擬內(nèi)存空間,并負(fù)責(zé)管理虛擬內(nèi)存與物理內(nèi)存之間的映射關(guān)系,確保進(jìn)程能夠正常訪問內(nèi)存資源,就像調(diào)度員根據(jù)任務(wù)需求合理安排工作時(shí)間,內(nèi)存分配助手根據(jù)進(jìn)程需要分配和管理內(nèi)存空間,保障了系統(tǒng)多任務(wù)和虛擬內(nèi)存管理功能的順利實(shí)現(xiàn)。
1.3為什么需要系統(tǒng)調(diào)用
linux內(nèi)核中設(shè)置了一組用于實(shí)現(xiàn)系統(tǒng)功能的子程序,稱為系統(tǒng)調(diào)用。系統(tǒng)調(diào)用和普通庫函數(shù)調(diào)用非常相似,只是系統(tǒng)調(diào)用由操作系統(tǒng)核心提供,運(yùn)行于內(nèi)核態(tài),而普通的函數(shù)調(diào)用由函數(shù)庫或用戶自己提供,運(yùn)行于用戶態(tài)。
一般的,進(jìn)程是不能訪問內(nèi)核的。它不能訪問內(nèi)核所占內(nèi)存空間也不能調(diào)用內(nèi)核函數(shù)。CPU硬件決定了這些(這就是為什么它被稱作“保護(hù)模式”)。
為了和用戶空間上運(yùn)行的進(jìn)程進(jìn)行交互,內(nèi)核提供了一組接口。透過該接口,應(yīng)用程序可以訪問硬件設(shè)備和其他操作系統(tǒng)資源。這組接口在應(yīng)用程序和內(nèi)核之間扮演了使者的角色,應(yīng)用程序發(fā)送各種請(qǐng)求,而內(nèi)核負(fù)責(zé)滿足這些請(qǐng)求(或者讓應(yīng)用程序暫時(shí)擱置)。實(shí)際上提供這組接口主要是為了保證系統(tǒng)穩(wěn)定可靠,避免應(yīng)用程序肆意妄行,惹出大麻煩。
系統(tǒng)調(diào)用在用戶空間進(jìn)程和硬件設(shè)備之間添加了一個(gè)中間層,該層主要作用有三個(gè):
- 它為用戶空間提供了一種統(tǒng)一的硬件的抽象接口。比如當(dāng)需要讀些文件的時(shí)候,應(yīng)用程序就可以不去管磁盤類型和介質(zhì),甚至不用去管文件所在的文件系統(tǒng)到底是哪種類型。
- 系統(tǒng)調(diào)用保證了系統(tǒng)的穩(wěn)定和安全。作為硬件設(shè)備和應(yīng)用程序之間的中間人,內(nèi)核可以基于權(quán)限和其他一些規(guī)則對(duì)需要進(jìn)行的訪問進(jìn)行裁決。舉例來說,這樣可以避免應(yīng)用程序不正確地使用硬件設(shè)備,竊取其他進(jìn)程的資源,或做出其他什么危害系統(tǒng)的事情。
- 每個(gè)進(jìn)程都運(yùn)行在虛擬系統(tǒng)中,而在用戶空間和系統(tǒng)的其余部分提供這樣一層公共接口,也是出于這種考慮。如果應(yīng)用程序可以隨意訪問硬件而內(nèi)核又對(duì)此一無所知的話,幾乎就沒法實(shí)現(xiàn)多任務(wù)和虛擬內(nèi)存,當(dāng)然也不可能實(shí)現(xiàn)良好的穩(wěn)定性和安全性。在Linux中,系統(tǒng)調(diào)用是用戶空間訪問內(nèi)核的惟一手段;除異常和中斷外,它們是內(nèi)核惟一的合法入口。
1.4 API/POSIX/C庫的區(qū)別與聯(lián)系
一般情況下,應(yīng)用程序通過應(yīng)用編程接口(API)而不是直接通過系統(tǒng)調(diào)用來編程。這點(diǎn)很重要,因?yàn)閼?yīng)用程序使用的這種編程接口實(shí)際上并不需要和內(nèi)核提供的系統(tǒng)調(diào)用一一對(duì)應(yīng)。
一個(gè)API定義了一組應(yīng)用程序使用的編程接口。它們可以實(shí)現(xiàn)成一個(gè)系統(tǒng)調(diào)用,也可以通過調(diào)用多個(gè)系統(tǒng)調(diào)用來實(shí)現(xiàn),而完全不使用任何系統(tǒng)調(diào)用也不存在問題。實(shí)際上,API可以在各種不同的操作系統(tǒng)上實(shí)現(xiàn),給應(yīng)用程序提供完全相同的接口,而它們本身在這些系統(tǒng)上的實(shí)現(xiàn)卻可能迥異。
在Unix世界中,最流行的應(yīng)用編程接口是基于POSIX標(biāo)準(zhǔn)的,其目標(biāo)是提供一套大體上基于Unix的可移植操作系統(tǒng)標(biāo)準(zhǔn)。POSIX是說明API和系統(tǒng)調(diào)用之間關(guān)系的一個(gè)極好例子。在大多數(shù)Unix系統(tǒng)上,根據(jù)POSIX而定義的API函數(shù)和系統(tǒng)調(diào)用之間有著直接關(guān)系。
Linux的系統(tǒng)調(diào)用像大多數(shù)Unix系統(tǒng)一樣,作為C庫的一部分提供如下圖所示。C庫實(shí)現(xiàn)了 Unix系統(tǒng)的主要API,包括標(biāo)準(zhǔn)C庫函數(shù)和系統(tǒng)調(diào)用。所有的C程序都可以使用C庫,而由于C語言本身的特點(diǎn),其他語言也可以很方便地把它們封裝起來使用。
從程序員的角度看,系統(tǒng)調(diào)用無關(guān)緊要,他們只需要跟API打交道就可以了。相反,內(nèi)核只跟系統(tǒng)調(diào)用打交道;庫函數(shù)及應(yīng)用程序是怎么使用系統(tǒng)調(diào)用不是內(nèi)核所關(guān)心的。
關(guān)于Unix的界面設(shè)計(jì)有一句通用的格言“提供機(jī)制而不是策略”。換句話說,Unix的系統(tǒng)調(diào)用抽象出了用于完成某種確定目的的函數(shù)。至干這些函數(shù)怎么用完全不需要內(nèi)核去關(guān)心。區(qū)別對(duì)待機(jī)制(mechanism)和策略(policy)是Unix設(shè)計(jì)中的一大亮點(diǎn)。大部分的編程問題都可以被切割成兩個(gè)部分:“需要提供什么功能”(機(jī)制)和“怎樣實(shí)現(xiàn)這些功能”(策略)。
區(qū)別
api是函數(shù)的定義,規(guī)定了這個(gè)函數(shù)的功能,跟內(nèi)核無直接關(guān)系。而系統(tǒng)調(diào)用是通過中斷向內(nèi)核發(fā)請(qǐng)求,實(shí)現(xiàn)內(nèi)核提供的某些服務(wù)。
聯(lián)系
- 一個(gè)api可能會(huì)需要一個(gè)或多個(gè)系統(tǒng)調(diào)用來完成特定功能。通俗點(diǎn)說就是如果這個(gè)api需要跟內(nèi)核打交道就需要系統(tǒng)調(diào)用,否則不需要。
- 程序員調(diào)用的是API(API函數(shù)),然后通過與系統(tǒng)調(diào)用共同完成函數(shù)的功能。因此,API是一個(gè)提供給應(yīng)用程序的接口,一組函數(shù),是與程序員進(jìn)行直接交互的。系統(tǒng)調(diào)用則不與程序員進(jìn)行交互的,它根據(jù)API函數(shù),通過一個(gè)軟中斷機(jī)制向內(nèi)核提交請(qǐng)求,以獲取內(nèi)核服務(wù)的接口。
- 并不是所有的API函數(shù)都一一對(duì)應(yīng)一個(gè)系統(tǒng)調(diào)用,有時(shí),一個(gè)API函數(shù)會(huì)需要幾個(gè)系統(tǒng)調(diào)用來共同完成函數(shù)的功能,甚至還有一些API函數(shù)不需要調(diào)用相應(yīng)的系統(tǒng)調(diào)用(因此它所完成的不是內(nèi)核提供的服務(wù))
二、系統(tǒng)調(diào)用與用戶程序的交互
系統(tǒng)調(diào)用(system calls),Linux內(nèi)核, GNU C庫(glibc)
在電腦中,系統(tǒng)調(diào)用(英語:system call),指運(yùn)行在用戶空間的程序向操作系統(tǒng)內(nèi)核請(qǐng)求需要更高權(quán)限運(yùn)行的服務(wù)。系統(tǒng)調(diào)用提供用戶程序與操作系統(tǒng)之間的接口。大多數(shù)系統(tǒng)交互式操作需求在內(nèi)核態(tài)執(zhí)行。如設(shè)備IO操作或者進(jìn)程間通信。
用戶空間(用戶態(tài))和內(nèi)核空間(內(nèi)核態(tài))
操作系統(tǒng)的進(jìn)程空間可分為用戶空間和內(nèi)核空間,它們需要不同的執(zhí)行權(quán)限。其中系統(tǒng)調(diào)用運(yùn)行在內(nèi)核空間。
庫函數(shù)
系統(tǒng)調(diào)用和普通庫函數(shù)調(diào)用非常相似,只是系統(tǒng)調(diào)用由操作系統(tǒng)內(nèi)核提供,運(yùn)行于內(nèi)核核心態(tài),而普通的庫函數(shù)調(diào)用由函數(shù)庫或用戶自己提供,運(yùn)行于用戶態(tài)。
典型實(shí)現(xiàn)(Linux)
Linux 在x86上的系統(tǒng)調(diào)用通過 int 80h 實(shí)現(xiàn),用系統(tǒng)調(diào)用號(hào)來區(qū)分入口函數(shù)。操作系統(tǒng)實(shí)現(xiàn)系統(tǒng)調(diào)用的基本過程是:
- 應(yīng)用程序調(diào)用庫函數(shù)(API);
- API 將系統(tǒng)調(diào)用號(hào)存入 EAX,然后通過中斷調(diào)用使系統(tǒng)進(jìn)入內(nèi)核態(tài);
- 內(nèi)核中的中斷處理函數(shù)根據(jù)系統(tǒng)調(diào)用號(hào),調(diào)用對(duì)應(yīng)的內(nèi)核函數(shù)(系統(tǒng)調(diào)用);
- 系統(tǒng)調(diào)用完成相應(yīng)功能,將返回值存入 EAX,返回到中斷處理函數(shù);
- 中斷處理函數(shù)返回到 API 中;
- API 將 EAX 返回給應(yīng)用程序。
應(yīng)用程序調(diào)用系統(tǒng)調(diào)用的過程是:
- 把系統(tǒng)調(diào)用的編號(hào)存入 EAX;
- 把函數(shù)參數(shù)存入其它通用寄存器;
- 觸發(fā) 0x80 號(hào)中斷(int 0x80)。
查看系統(tǒng)調(diào)用號(hào)
- 使用命令cat /usr/include/asm/unistd_32.h來打開32位系統(tǒng)調(diào)用表
- 使用命令cat /usr/include/asm/unistd_64.h來打開32位系統(tǒng)調(diào)用表
簡介幾種系統(tǒng)調(diào)用函數(shù):write、read、open、close、ioctl
在 Linux 中,一切(或幾乎一切)都是文件,因此,文件操作在 Linux 中是十分重要的,為此,Linux 系統(tǒng)直接提供了一些函數(shù)用于對(duì)文件和設(shè)備進(jìn)行訪問和控制,這些函數(shù)被稱為系統(tǒng)調(diào)用(syscall),它們也是通向操作系統(tǒng)本身的接口。
系統(tǒng)調(diào)用工作在內(nèi)核態(tài),實(shí)際上,系統(tǒng)調(diào)用是用戶空間訪問內(nèi)核空間的唯一手段(除異常和陷入外,它們是內(nèi)核唯一的合法入口)。系統(tǒng)調(diào)用的主要作用如下:
- 1)系統(tǒng)調(diào)用為用戶空間提供了一種硬件的抽象接口,這樣,當(dāng)需要讀寫文件時(shí),應(yīng)用程序就可以不用管磁盤類型和介質(zhì),甚至不用去管文件所在的文件系統(tǒng)到底是哪種類型;
- 2)系統(tǒng)調(diào)用保證了系統(tǒng)的穩(wěn)定和安全。作為硬件設(shè)備和應(yīng)用程序之間的中間人,內(nèi)核可以基于權(quán)限、用戶類型和其他一些規(guī)則對(duì)需要進(jìn)行的訪問進(jìn)行判斷;
- 3)系統(tǒng)調(diào)用是實(shí)現(xiàn)多任務(wù)和虛擬內(nèi)存的前提
要訪問系統(tǒng)調(diào)用,通常通過 C 庫中定義的函數(shù)調(diào)用來進(jìn)行。它們通常都需要定義零個(gè)、一個(gè)或幾個(gè)參數(shù)(輸入),而且可能產(chǎn)生一些副作用(會(huì)使系統(tǒng)的狀態(tài)發(fā)生某種變化)。系統(tǒng)調(diào)用還會(huì)通過一個(gè) long 類型的返回值來表示成功或者錯(cuò)誤。通常,用一個(gè)負(fù)的值來表明錯(cuò)誤,0表示成功。系統(tǒng)調(diào)用出現(xiàn)錯(cuò)誤時(shí),C 庫會(huì)把錯(cuò)誤碼寫入 errno 全局變量,通過調(diào)用 perror() 庫函數(shù),可以把該變量翻譯成用戶可理解的錯(cuò)誤字符串。
2.1 用戶程序發(fā)起調(diào)用的方式
(1)write 系統(tǒng)調(diào)用
在 Linux 系統(tǒng)中,用戶程序發(fā)起系統(tǒng)調(diào)用主要有兩種常見方式。一種是通過應(yīng)用程序編程接口(API),這些 API 通常是對(duì)系統(tǒng)調(diào)用的封裝,以更友好、易用的形式呈現(xiàn)給開發(fā)者。另一種則是在一些特定場景下,直接使用 syscall 函數(shù)。
以 write 函數(shù)為例,它是一個(gè)用于向文件描述符寫入數(shù)據(jù)的系統(tǒng)調(diào)用封裝函數(shù),在 C 語言中被廣泛應(yīng)用。當(dāng)我們需要將數(shù)據(jù)寫入文件時(shí),便可以使用 write 函數(shù)。以下是一個(gè)簡單的 C 語言示例代碼,展示了如何使用 write 函數(shù)將字符串寫入標(biāo)準(zhǔn)輸出(通常是終端屏幕):
#include <unistd.h>
#include <stdio.h>
int main() {
char *message = "Hello, World!\n";
int len = 15; // 包括空終止符的長度
// 使用write函數(shù)將字符串寫入stdout
if (write(1, message, len) != len) {
perror("write error");
return 1;
}
return 0;
}在這段代碼中,首先定義了一個(gè)字符串 message,即我們想要輸出的內(nèi)容,以及它的長度 len。然后,調(diào)用 write 函數(shù),其第一個(gè)參數(shù) 1 代表標(biāo)準(zhǔn)輸出的文件描述符,在 Linux 系統(tǒng)中,文件描述符是一個(gè)非負(fù)整數(shù),用于標(biāo)識(shí)打開的文件或設(shè)備,標(biāo)準(zhǔn)輸出的文件描述符通常為 1;第二個(gè)參數(shù) message 是指向要寫入數(shù)據(jù)緩沖區(qū)的指針,也就是我們定義的字符串;第三個(gè)參數(shù) len 則表示要寫入數(shù)據(jù)的字節(jié)數(shù)。如果 write 函數(shù)返回值不等于請(qǐng)求寫入的字節(jié)數(shù) len,說明寫入過程出現(xiàn)了錯(cuò)誤,此時(shí)通過 perror 函數(shù)輸出錯(cuò)誤信息,并返回 1 表示程序異常結(jié)束。如果寫入成功,則正常返回 0 。
在這個(gè)例子中,write 函數(shù)雖然看似只是一個(gè)普通的函數(shù)調(diào)用,但實(shí)際上它是對(duì)底層系統(tǒng)調(diào)用的封裝。當(dāng)程序執(zhí)行 write 函數(shù)時(shí),會(huì)進(jìn)一步觸發(fā)系統(tǒng)調(diào)用機(jī)制,實(shí)現(xiàn)從用戶態(tài)到內(nèi)核態(tài)的切換,進(jìn)而完成實(shí)際的寫入操作。這種通過 API 封裝系統(tǒng)調(diào)用的方式,極大地簡化了開發(fā)者的工作,使得我們無需深入了解底層系統(tǒng)調(diào)用的復(fù)雜細(xì)節(jié),就能輕松實(shí)現(xiàn)文件寫入等功能 。
(2)read 系統(tǒng)調(diào)用
系統(tǒng)調(diào)用 read 的作用是:從文件描述符 fildes 相關(guān)聯(lián)的文件里讀入 nbytes 個(gè)字節(jié)的數(shù)據(jù),并把它們放到數(shù)據(jù)區(qū) buf 中。它返回實(shí)際讀入的字節(jié)數(shù),這可能會(huì)小于請(qǐng)求的字節(jié)數(shù)。如果 read 調(diào)用返回 0,就表示沒有讀入任何數(shù)據(jù),已到達(dá)了文件尾;如果返回 -1,則表示 read 調(diào)用出現(xiàn)了錯(cuò)誤。read 系統(tǒng)調(diào)用的原型如下:
#include <unistd.h>
size_t read(int fildes,void *buf,size_t nbytes);用一段代碼演示一下用法:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
char buffer[30];
size_t x = read(0,buffer,30);
write(1,buffer,x);
exit(0);
}
/* 輸出結(jié)果:
hello ,my name is tongye!
hello ,my name is tongye!
*/這段代碼使用 read 系統(tǒng)調(diào)用函數(shù)從標(biāo)準(zhǔn)輸入讀取 30 個(gè)字節(jié)到緩沖區(qū) buffer 中去(輸出結(jié)果中的第一行是從標(biāo)準(zhǔn)輸入鍵入的),然后使用 write 系統(tǒng)調(diào)用函數(shù)將 buffer 中的字節(jié)寫到標(biāo)準(zhǔn)輸出中去。
(3)open 系統(tǒng)調(diào)用
系統(tǒng)調(diào)用 open 用于創(chuàng)建一個(gè)新的文件描述符。
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int open(const char *path,int oflags);
int open(const char *path,int oflags,mode_t mode); // oflags 標(biāo)志為 O_CREAT 時(shí),使用這種格式open 建立了一條到文件或設(shè)備的訪問路徑。如果調(diào)用成功,它將返回一個(gè)可以被 read、write 和其他系統(tǒng)調(diào)用使用的文件描述符。這個(gè)文件描述符是唯一的,不會(huì)與任何其他運(yùn)行中的進(jìn)程共享。在調(diào)用失敗時(shí),將返回 -1 并設(shè)置全局變量 errno 來指明失敗的原因。
使用 open 系統(tǒng)調(diào)用時(shí),準(zhǔn)備打開的文件或設(shè)備的名字作為參數(shù) path 傳遞給函數(shù),oflags 參數(shù)用于指定打開文件所采取的動(dòng)作。oflags 參數(shù)是通過命令文件訪問模式與其他可選模式相結(jié)合的方式來指定的,open 調(diào)用必須指定以下文件訪問模式之一:
- 1)O_RDONLY:以只讀方式打開;
- 2)O_WRONLY:以只寫方式打開;
- 3)O_RDWR :以讀寫方式打開。
另外,還有以下幾種可選模式的組合( 用按位或 || 來操作 ):
- 4)O_APPEND:把寫入數(shù)據(jù)追加在文件的末尾;
- 5)O_TRUNC:把文件長度設(shè)置為零,丟棄已有的內(nèi)容;
- 6)O_CREAT:如果需要,就按照參數(shù) mode 中給出的訪問模式創(chuàng)建文件;
- 7)O_EXCL:與 O_CREAT 一起使用,確保調(diào)用者創(chuàng)建出文件。使用這個(gè)模式可以防止兩個(gè)程序同時(shí)創(chuàng)建同一個(gè)文件,如果文件已經(jīng)存在,open 調(diào)用將失敗。
當(dāng)使用 O_CREAT 標(biāo)志的 open 調(diào)用來創(chuàng)建文件時(shí),需要使用有 3 個(gè)參數(shù)格式的 open 調(diào)用。其中,第三個(gè)參數(shù) mode 是幾個(gè)標(biāo)志按位或后得到的,這些標(biāo)志在頭文件 sys/stat.h 中定義,如下:
標(biāo)志 | 說明 |
S_IRUSR | 文件屬主可讀 |
S_IWUSR | 文件屬主可寫 |
S_IXUSR | 文件屬主可執(zhí)行 |
S_IRGRP | 文件所在組可讀 |
S_IWGRP | 文件所在組可寫 |
S_IWOTH | 文件所在組可執(zhí)行 |
S_IROTH | 其他用戶可讀 |
S_IWOTH | 其他用戶可寫 |
S_IWOTH | 其他用戶可執(zhí)行 |
用一個(gè)例子說明一下:
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{
open("file",O_CREAT,S_IRUSR | S_IWGRP);
exit(0);
}執(zhí)行這段代碼將在當(dāng)前目錄下創(chuàng)建一個(gè)名為 file 的文件,該文件對(duì)文件屬主可讀,對(duì)文件所在組可寫,用 ls -l 命令查看如下:
[tongye@Iocalhost OpenSysCaII]$ Is -I
total 20
-r---W---- 1 tongye tongye 0 0ct 24 00:55 file
-rw-rw-r-- 1 tongye tongye 96 0ct 24 00:54 Makefile
-rwxrwxr-x 1 tongye tongye 8496 0ct 24 00:57
-rw-rw-r-- 1 tongye tongye 127 0ct 24 00:57 open_sys_calT.c可以看到有一個(gè)名為 file 的文件,該文件就是使用 open 系統(tǒng)調(diào)用創(chuàng)建的,文件的權(quán)限為文件屬主可讀,文件所在組可寫。
(4)close 系統(tǒng)調(diào)用
系統(tǒng)調(diào)用 close 可以用來終止文件描述符 fildes 與其對(duì)應(yīng)文件之間的關(guān)聯(lián)。當(dāng) close 系統(tǒng)調(diào)用成功時(shí),返回 0,文件描述符被釋放并能夠重新使用;調(diào)用出錯(cuò),則返回 -1。
#include <unistd.h>
int close(int fildes);(5)ioctl 系統(tǒng)調(diào)用
系統(tǒng)調(diào)用 ioctl 提供了一個(gè)用于控制設(shè)備及其描述符行為和配置底層服務(wù)的接口。終端、文件描述符、套接字甚至磁帶機(jī)都可以有為它們定義的 ioctl。
#include <unistd.h>
int ioctl(int fildes,int cmd,...);octl 對(duì)描述符 fildes 引用的對(duì)象執(zhí)行 cmd 參數(shù)中給出的操作。
2.2 調(diào)用過程中的狀態(tài)切換
當(dāng)用戶程序發(fā)起系統(tǒng)調(diào)用時(shí),一個(gè)關(guān)鍵的環(huán)節(jié)便是從用戶態(tài)到內(nèi)核態(tài)的狀態(tài)切換。在 Linux 系統(tǒng)中,這種切換是通過軟中斷機(jī)制來實(shí)現(xiàn)的,而軟中斷又是借助中斷向量表來精準(zhǔn)找到對(duì)應(yīng)的中斷處理程序。
在計(jì)算機(jī)系統(tǒng)中,用戶態(tài)和內(nèi)核態(tài)是兩種不同的執(zhí)行模式。用戶態(tài)下運(yùn)行的程序權(quán)限較低,只能訪問用戶空間的內(nèi)存,并且不能直接操作硬件資源,這就像一個(gè)普通員工在公司的特定工作區(qū)域內(nèi)工作,權(quán)限有限,不能隨意進(jìn)入核心管理區(qū)域。而內(nèi)核態(tài)則擁有對(duì)系統(tǒng)所有資源的完全控制權(quán)限,包括硬件訪問、內(nèi)存管理和進(jìn)程調(diào)度等,類似于公司的核心管理層,擁有最高權(quán)限,可以掌控公司的一切資源。
為了實(shí)現(xiàn)從用戶態(tài)到內(nèi)核態(tài)的切換,Linux 使用軟中斷指令。以 x86 架構(gòu)為例,早期使用 int 0x80 指令來觸發(fā)系統(tǒng)調(diào)用軟中斷,后來隨著技術(shù)發(fā)展,引入了更高效的 syscall 指令。當(dāng)用戶程序執(zhí)行到發(fā)起系統(tǒng)調(diào)用的代碼時(shí),如上述的 write 函數(shù)調(diào)用,會(huì)執(zhí)行相應(yīng)的軟中斷指令。這條指令就像一個(gè)特殊的 “通行證”,它會(huì)觸發(fā) CPU 產(chǎn)生一個(gè)軟件中斷信號(hào)。
CPU 接收到這個(gè)軟中斷信號(hào)后,會(huì)暫停當(dāng)前用戶態(tài)程序的執(zhí)行,就像一個(gè)正在專心工作的員工突然被緊急通知停下手中工作。接著,CPU 開始進(jìn)行一系列復(fù)雜的操作來切換到內(nèi)核態(tài)。它首先會(huì)保存當(dāng)前用戶態(tài)程序的上下文信息,包括程序計(jì)數(shù)器(記錄下一條要執(zhí)行的指令地址)、通用寄存器的值等,這些信息就像是員工停下工作時(shí)記錄的工作進(jìn)度和手頭的資料,以便后續(xù)能夠恢復(fù)工作。然后,CPU 根據(jù)中斷向量表來查找與該軟中斷對(duì)應(yīng)的中斷處理程序的入口地址。
中斷向量表是一個(gè)非常重要的數(shù)據(jù)結(jié)構(gòu),它就像一本詳細(xì)的 “指南手冊(cè)”,存儲(chǔ)了所有中斷處理程序的地址。每個(gè)中斷都被賦予一個(gè)唯一的中斷號(hào),這個(gè)中斷號(hào)就像是手冊(cè)中的頁碼,通過它可以快速定位到相應(yīng)中斷處理程序的地址。在系統(tǒng)調(diào)用軟中斷的情況下,CPU 會(huì)根據(jù)軟中斷對(duì)應(yīng)的中斷號(hào),從中斷向量表中找到系統(tǒng)調(diào)用處理程序的入口地址,進(jìn)而跳轉(zhuǎn)到該地址開始執(zhí)行內(nèi)核態(tài)的代碼。
一旦進(jìn)入內(nèi)核態(tài),系統(tǒng)調(diào)用處理程序就會(huì)根據(jù)用戶程序傳遞過來的系統(tǒng)調(diào)用號(hào)和參數(shù),執(zhí)行相應(yīng)的內(nèi)核服務(wù)例程。在 write 函數(shù)對(duì)應(yīng)的系統(tǒng)調(diào)用中,內(nèi)核會(huì)根據(jù)傳遞的文件描述符、數(shù)據(jù)緩沖區(qū)和數(shù)據(jù)長度等參數(shù),在內(nèi)核空間中進(jìn)行實(shí)際的文件寫入操作,訪問底層的硬件設(shè)備(如磁盤)來完成數(shù)據(jù)的存儲(chǔ)。
當(dāng)內(nèi)核完成系統(tǒng)調(diào)用的處理后,會(huì)將結(jié)果返回給用戶程序。此時(shí),CPU 會(huì)再次進(jìn)行上下文切換,恢復(fù)之前保存的用戶態(tài)程序的上下文信息,就像員工重新拿起之前記錄的工作進(jìn)度和資料,繼續(xù)之前被中斷的工作。然后,CPU 返回到用戶態(tài),繼續(xù)執(zhí)行用戶程序中系統(tǒng)調(diào)用之后的代碼 。
從用戶態(tài)到內(nèi)核態(tài)的切換過程是 Linux 系統(tǒng)調(diào)用實(shí)現(xiàn)的關(guān)鍵環(huán)節(jié),它涉及到 CPU、內(nèi)存、中斷向量表等多個(gè)組件的協(xié)同工作,通過軟中斷機(jī)制和中斷向量表的配合,實(shí)現(xiàn)了用戶程序與內(nèi)核之間安全、高效的通信,確保了系統(tǒng)的穩(wěn)定運(yùn)行和資源的合理利用 。
三、系統(tǒng)調(diào)用的實(shí)現(xiàn)原理
3.1 系統(tǒng)調(diào)用號(hào)與系統(tǒng)調(diào)用表
在 Linux 系統(tǒng)調(diào)用的實(shí)現(xiàn)過程中,系統(tǒng)調(diào)用號(hào)與系統(tǒng)調(diào)用表扮演著不可或缺的關(guān)鍵角色。
系統(tǒng)調(diào)用號(hào),簡單來說,是一個(gè)獨(dú)一無二的標(biāo)識(shí)符,就像每個(gè)人都有一個(gè)獨(dú)特的身份證號(hào)碼一樣,每個(gè)系統(tǒng)調(diào)用都被賦予了一個(gè)唯一的系統(tǒng)調(diào)用號(hào)。在 x86 架構(gòu)中,系統(tǒng)調(diào)用號(hào)通常是通過 eax 寄存器傳遞給內(nèi)核的。在用戶空間執(zhí)行系統(tǒng)調(diào)用之前,會(huì)將對(duì)應(yīng)的系統(tǒng)調(diào)用號(hào)存入 eax 寄存器,這樣當(dāng)系統(tǒng)進(jìn)入內(nèi)核態(tài)時(shí),內(nèi)核就能依據(jù)這個(gè)系統(tǒng)調(diào)用號(hào),精準(zhǔn)地知曉用戶程序究竟請(qǐng)求的是哪一個(gè)系統(tǒng)調(diào)用。
以常見的文件操作相關(guān)系統(tǒng)調(diào)用為例,打開文件的系統(tǒng)調(diào)用 open,它擁有特定的系統(tǒng)調(diào)用號(hào),當(dāng)用戶程序需要打開文件時(shí),會(huì)將 open 系統(tǒng)調(diào)用對(duì)應(yīng)的系統(tǒng)調(diào)用號(hào)存入 eax 寄存器,再發(fā)起系統(tǒng)調(diào)用,內(nèi)核就能根據(jù)這個(gè)號(hào)碼識(shí)別出用戶的意圖是打開文件。這種通過唯一編號(hào)來標(biāo)識(shí)系統(tǒng)調(diào)用的方式,極大地提高了系統(tǒng)調(diào)用處理的效率和準(zhǔn)確性,避免了因名稱解析等復(fù)雜操作帶來的性能損耗 。
而系統(tǒng)調(diào)用表,則是一個(gè)存儲(chǔ)著系統(tǒng)調(diào)用函數(shù)指針的數(shù)組,它就像是一本精心編制的索引目錄,數(shù)組的每個(gè)元素都是一個(gè)指向特定系統(tǒng)調(diào)用處理函數(shù)的指針。在 x86 架構(gòu)下,系統(tǒng)調(diào)用表的定義和實(shí)現(xiàn)與具體的內(nèi)核版本和架構(gòu)相關(guān)。
在 64 位系統(tǒng)中,系統(tǒng)調(diào)用表定義在arch/x86/kernel/syscall_64.c文件中 ,其數(shù)組名為sys_call_table,該數(shù)組的大小為__NR_syscall_max + 1,其中__NR_syscall_max是一個(gè)宏,在 64 位模式下,它的值為 542 ,這個(gè)宏定義于include/generated/asm-offsets.h文件,該文件是在 Kbuild 編譯后生成的。系統(tǒng)調(diào)用表中的元素類型為sys_call_ptr_t,這是通過 typedef 定義的函數(shù)指針,它指向的是具體的系統(tǒng)調(diào)用處理函數(shù)。當(dāng)內(nèi)核接收到系統(tǒng)調(diào)用請(qǐng)求,并獲取到系統(tǒng)調(diào)用號(hào)后,就會(huì)以這個(gè)系統(tǒng)調(diào)用號(hào)作為索引,迅速在系統(tǒng)調(diào)用表中找到對(duì)應(yīng)的函數(shù)指針,進(jìn)而調(diào)用相應(yīng)的系統(tǒng)調(diào)用處理函數(shù),執(zhí)行具體的系統(tǒng)調(diào)用操作 。
假設(shè)系統(tǒng)調(diào)用號(hào)為n,那么系統(tǒng)調(diào)用表sys_call_table中第n個(gè)元素sys_call_table[n]就指向了處理該系統(tǒng)調(diào)用的函數(shù)。如果系統(tǒng)調(diào)用號(hào)為 1,對(duì)應(yīng)sys_call_table[1],它指向的就是處理 write 系統(tǒng)調(diào)用的函數(shù),當(dāng)內(nèi)核根據(jù)系統(tǒng)調(diào)用號(hào) 1 在表中找到這個(gè)指針并調(diào)用相應(yīng)函數(shù)時(shí),就能完成實(shí)際的文件寫入操作。系統(tǒng)調(diào)用號(hào)與系統(tǒng)調(diào)用表的緊密配合,構(gòu)成了 Linux 系統(tǒng)調(diào)用實(shí)現(xiàn)的重要基礎(chǔ),它們使得內(nèi)核能夠高效、準(zhǔn)確地響應(yīng)用戶程序的各種系統(tǒng)調(diào)用請(qǐng)求,保障了系統(tǒng)的穩(wěn)定運(yùn)行和高效工作 。
3.2 系統(tǒng)調(diào)用處理程序
系統(tǒng)調(diào)用處理程序是系統(tǒng)調(diào)用實(shí)現(xiàn)過程中的核心環(huán)節(jié),它負(fù)責(zé)處理用戶程序發(fā)起的系統(tǒng)調(diào)用請(qǐng)求,執(zhí)行相應(yīng)的內(nèi)核服務(wù)例程,并返回處理結(jié)果。當(dāng)用戶程序發(fā)起系統(tǒng)調(diào)用時(shí),會(huì)觸發(fā)軟中斷,從而進(jìn)入內(nèi)核態(tài),開始執(zhí)行系統(tǒng)調(diào)用處理程序。
系統(tǒng)調(diào)用處理程序的工作流程嚴(yán)謹(jǐn)而有序。當(dāng) CPU 響應(yīng)軟中斷進(jìn)入內(nèi)核態(tài)后,首先會(huì)保存當(dāng)前用戶程序的寄存器狀態(tài)。這一步至關(guān)重要,因?yàn)榧拇嫫髦写鎯?chǔ)著用戶程序當(dāng)前的執(zhí)行狀態(tài)和相關(guān)數(shù)據(jù),保存這些寄存器狀態(tài)就如同為用戶程序的執(zhí)行進(jìn)度拍了一張 “快照”,以便在系統(tǒng)調(diào)用完成后能夠準(zhǔn)確地恢復(fù)到調(diào)用前的狀態(tài),繼續(xù)執(zhí)行用戶程序。在 x86 架構(gòu)中,通常會(huì)將寄存器的值壓入到核心棧中,這些寄存器包括通用寄存器如 eax、ebx、ecx、edx 等,以及程序計(jì)數(shù)器(記錄下一條要執(zhí)行的指令地址)等關(guān)鍵寄存器。
保存完寄存器狀態(tài)后,系統(tǒng)調(diào)用處理程序會(huì)根據(jù)用戶程序傳遞過來的系統(tǒng)調(diào)用號(hào),在系統(tǒng)調(diào)用表中查找對(duì)應(yīng)的系統(tǒng)調(diào)用處理函數(shù)。這個(gè)查找過程就像是在一本索引清晰的大字典中查找特定的詞條,系統(tǒng)調(diào)用號(hào)就是詞條的索引,通過它能夠快速定位到系統(tǒng)調(diào)用表中對(duì)應(yīng)的函數(shù)指針,進(jìn)而找到真正執(zhí)行系統(tǒng)調(diào)用功能的處理函數(shù)。如果系統(tǒng)調(diào)用號(hào)為 5,表示打開文件的系統(tǒng)調(diào)用,處理程序就會(huì)根據(jù)這個(gè) 5 作為索引,在系統(tǒng)調(diào)用表中找到指向sys_open函數(shù)的指針,這個(gè)sys_open函數(shù)就是專門負(fù)責(zé)處理打開文件系統(tǒng)調(diào)用的函數(shù) 。
找到對(duì)應(yīng)的處理函數(shù)后,系統(tǒng)調(diào)用處理程序就會(huì)調(diào)用該函數(shù),執(zhí)行相應(yīng)的內(nèi)核服務(wù)例程。在執(zhí)行過程中,處理函數(shù)會(huì)根據(jù)系統(tǒng)調(diào)用的具體需求,訪問和操作內(nèi)核資源,完成用戶程序請(qǐng)求的任務(wù)。在執(zhí)行文件寫入的系統(tǒng)調(diào)用時(shí),處理函數(shù)會(huì)根據(jù)傳遞過來的文件描述符、數(shù)據(jù)緩沖區(qū)和數(shù)據(jù)長度等參數(shù),在內(nèi)核空間中進(jìn)行實(shí)際的文件寫入操作,訪問底層的磁盤設(shè)備,將數(shù)據(jù)存儲(chǔ)到指定的文件中 。
當(dāng)內(nèi)核服務(wù)例程執(zhí)行完畢后,系統(tǒng)調(diào)用處理程序會(huì)將執(zhí)行結(jié)果返回給用戶程序。在返回之前,會(huì)先恢復(fù)之前保存的用戶程序寄存器狀態(tài),就像把之前拍的 “快照” 重新還原,讓 CPU 回到系統(tǒng)調(diào)用前的狀態(tài)。然后,CPU 會(huì)從內(nèi)核態(tài)切換回用戶態(tài),繼續(xù)執(zhí)行用戶程序中系統(tǒng)調(diào)用之后的代碼,將系統(tǒng)調(diào)用的執(zhí)行結(jié)果傳遞給用戶程序,用戶程序就可以根據(jù)這個(gè)結(jié)果進(jìn)行后續(xù)的處理 。
系統(tǒng)調(diào)用處理程序的工作流程確保了系統(tǒng)調(diào)用的安全、高效執(zhí)行,它在用戶程序與內(nèi)核之間搭建起了一座可靠的橋梁,使得用戶程序能夠在不直接訪問內(nèi)核資源的情況下,通過系統(tǒng)調(diào)用獲取內(nèi)核提供的各種服務(wù),保障了系統(tǒng)的穩(wěn)定性和安全性 。
3.3 參數(shù)傳遞與返回值處理
在系統(tǒng)調(diào)用過程中,參數(shù)傳遞和返回值處理是兩個(gè)關(guān)鍵環(huán)節(jié),它們確保了用戶程序與內(nèi)核之間能夠準(zhǔn)確、有效地進(jìn)行數(shù)據(jù)交互。
系統(tǒng)調(diào)用的參數(shù)傳遞方式與硬件架構(gòu)密切相關(guān)。以常見的 x86 架構(gòu)為例,在 32 位系統(tǒng)中,當(dāng)用戶程序發(fā)起系統(tǒng)調(diào)用時(shí),參數(shù)通常通過寄存器來傳遞。具體來說,ebx、ecx、edx、esi 和 edi 這幾個(gè)寄存器按照順序存放前五個(gè)參數(shù)。如果系統(tǒng)調(diào)用需要傳遞六個(gè)或更多參數(shù),由于寄存器數(shù)量有限,此時(shí)會(huì)用一個(gè)單獨(dú)的寄存器(通常是 eax)存放指向所有這些參數(shù)在用戶空間地址的指針,然后通過內(nèi)存空間進(jìn)行參數(shù)傳遞。在執(zhí)行一個(gè)需要傳遞多個(gè)參數(shù)的文件寫入系統(tǒng)調(diào)用時(shí),前五個(gè)參數(shù)(如文件描述符、數(shù)據(jù)緩沖區(qū)指針、數(shù)據(jù)長度等)可能分別存放在 ebx、ecx、edx、esi 和 edi 寄存器中,如果還有其他參數(shù),就會(huì)將這些參數(shù)在用戶空間的地址存放在 eax 寄存器中,內(nèi)核可以根據(jù)這個(gè)地址從用戶空間獲取完整的參數(shù) 。
在 64 位的 x86 架構(gòu)系統(tǒng)中,參數(shù)傳遞規(guī)則有所不同。前 6 個(gè)整數(shù)或指針參數(shù)會(huì)在寄存器 RDI、RSI、RDX、RCX、R8、R9 中傳遞,對(duì)于嵌套函數(shù),R10 用作靜態(tài)鏈指針,其他參數(shù)則在堆棧上傳遞。這種參數(shù)傳遞方式充分利用了 64 位架構(gòu)下寄存器數(shù)量增加的優(yōu)勢(shì),提高了參數(shù)傳遞的效率和靈活性 。
關(guān)于系統(tǒng)調(diào)用的返回值,也有著明確的約定。在 Linux 系統(tǒng)中,通常用一個(gè)負(fù)的返回值來表明系統(tǒng)調(diào)用執(zhí)行過程中出現(xiàn)了錯(cuò)誤。返回值為 - 1 可能表示權(quán)限不足,-2 可能表示文件不存在等。不同的負(fù)值對(duì)應(yīng)著不同的錯(cuò)誤類型,這些錯(cuò)誤類型的定義可以在errno.h頭文件中找到。當(dāng)用戶程序接收到負(fù)的返回值時(shí),可以通過查看errno變量的值來確定具體的錯(cuò)誤原因,并且可以調(diào)用perror()庫函數(shù),將errno的值翻譯成用戶可以理解的錯(cuò)誤字符串,以便進(jìn)行錯(cuò)誤處理 。
如果系統(tǒng)調(diào)用執(zhí)行成功,返回值通常為正值或 0。對(duì)于一些返回?cái)?shù)據(jù)的系統(tǒng)調(diào)用,如讀取文件內(nèi)容的系統(tǒng)調(diào)用,返回值可能是實(shí)際讀取到的字節(jié)數(shù);而對(duì)于一些只執(zhí)行操作不返回具體數(shù)據(jù)的系統(tǒng)調(diào)用,成功時(shí)返回值可能為 0,表示操作順利完成。在執(zhí)行讀取文件系統(tǒng)調(diào)用時(shí),如果成功讀取到數(shù)據(jù),返回值就是實(shí)際讀取的字節(jié)數(shù),用戶程序可以根據(jù)這個(gè)返回值來判斷讀取操作是否成功以及獲取到的數(shù)據(jù)量 。
參數(shù)傳遞和返回值處理機(jī)制是系統(tǒng)調(diào)用實(shí)現(xiàn)的重要組成部分,它們確保了用戶程序與內(nèi)核之間能夠準(zhǔn)確地傳遞數(shù)據(jù)和信息,使得系統(tǒng)調(diào)用能夠按照預(yù)期的方式執(zhí)行,并將結(jié)果反饋給用戶程序,為應(yīng)用程序的正確運(yùn)行提供了堅(jiān)實(shí)的保障 。
四、不同架構(gòu)下的系統(tǒng)調(diào)用實(shí)現(xiàn)差異
4.1 x86架構(gòu)
x86 架構(gòu)下系統(tǒng)調(diào)用的實(shí)現(xiàn)方式隨著技術(shù)的發(fā)展不斷演進(jìn),經(jīng)歷了從 int 0x80 到 syscall 指令的重要轉(zhuǎn)變。
在早期,x86 架構(gòu)主要通過 int 0x80 指令來實(shí)現(xiàn)系統(tǒng)調(diào)用。當(dāng)用戶程序需要發(fā)起系統(tǒng)調(diào)用時(shí),會(huì)執(zhí)行 int 0x80 這條軟中斷指令。這一指令就像是一個(gè)特殊的 “信號(hào)彈”,它會(huì)觸發(fā) CPU 產(chǎn)生一個(gè)軟件中斷信號(hào)。CPU 在接收到這個(gè)信號(hào)后,會(huì)暫停當(dāng)前用戶態(tài)程序的執(zhí)行,轉(zhuǎn)而執(zhí)行中斷處理程序。在這個(gè)過程中,系統(tǒng)調(diào)用號(hào)被存放在 eax 寄存器中,參數(shù)則通過 ebx、ecx、edx 等寄存器傳遞。例如,當(dāng)執(zhí)行一個(gè)打開文件的系統(tǒng)調(diào)用時(shí),會(huì)將打開文件系統(tǒng)調(diào)用對(duì)應(yīng)的系統(tǒng)調(diào)用號(hào)存入 eax 寄存器,文件路徑等參數(shù)可能存放在 ebx 等寄存器中 。
與 int 0x80 指令緊密相關(guān)的是 entry_INT80_32 函數(shù),它在系統(tǒng)調(diào)用處理流程中扮演著關(guān)鍵角色。當(dāng) int 0x80 中斷發(fā)生后,CPU 會(huì)跳轉(zhuǎn)到 entry_INT80_32 函數(shù)執(zhí)行。這個(gè)函數(shù)主要負(fù)責(zé)保存用戶態(tài)的寄存器狀態(tài),因?yàn)檫@些寄存器中存儲(chǔ)著用戶程序當(dāng)前的執(zhí)行狀態(tài)和相關(guān)數(shù)據(jù),保存它們就如同為用戶程序的執(zhí)行進(jìn)度拍了一張 “快照”,以便在系統(tǒng)調(diào)用完成后能夠準(zhǔn)確地恢復(fù)到調(diào)用前的狀態(tài),繼續(xù)執(zhí)行用戶程序。在 entry_INT80_32 函數(shù)中,會(huì)將寄存器的值壓入到核心棧中,這些寄存器包括通用寄存器如 eax、ebx、ecx、edx 等,以及程序計(jì)數(shù)器(記錄下一條要執(zhí)行的指令地址)等關(guān)鍵寄存器 。
保存完寄存器狀態(tài)后,entry_INT80_32 函數(shù)會(huì)調(diào)用 do_syscall_32_irqs_on 函數(shù),這個(gè)函數(shù)才是真正處理系統(tǒng)調(diào)用的核心函數(shù)。它會(huì)根據(jù) eax 寄存器中保存的系統(tǒng)調(diào)用號(hào),在系統(tǒng)調(diào)用表中查找對(duì)應(yīng)的系統(tǒng)調(diào)用處理函數(shù)。系統(tǒng)調(diào)用表就像是一本精心編制的索引目錄,數(shù)組的每個(gè)元素都是一個(gè)指向特定系統(tǒng)調(diào)用處理函數(shù)的指針。do_syscall_32_irqs_on 函數(shù)會(huì)以系統(tǒng)調(diào)用號(hào)作為索引,在系統(tǒng)調(diào)用表中找到對(duì)應(yīng)的函數(shù)指針,進(jìn)而調(diào)用相應(yīng)的系統(tǒng)調(diào)用處理函數(shù),執(zhí)行具體的系統(tǒng)調(diào)用操作 。
隨著 x86 架構(gòu)的不斷發(fā)展,為了提高系統(tǒng)調(diào)用的性能,引入了 syscall 指令。syscall 指令相比 int 0x80 指令,減少了一些不必要的操作,使得系統(tǒng)調(diào)用的執(zhí)行更加高效。在使用 syscall 指令時(shí),系統(tǒng)調(diào)用號(hào)被存放在 rax 寄存器中,參數(shù)則通過 rdi、rsi、rdx 等寄存器傳遞。這種方式在一定程度上簡化了參數(shù)傳遞過程,提高了系統(tǒng)調(diào)用的執(zhí)行效率 。
從 int 0x80 到 syscall 指令的演變,體現(xiàn)了 x86 架構(gòu)在系統(tǒng)調(diào)用實(shí)現(xiàn)上不斷追求性能優(yōu)化的過程。雖然具體的實(shí)現(xiàn)細(xì)節(jié)在不同的內(nèi)核版本和架構(gòu)下可能會(huì)有所差異,但總體上都是圍繞著如何更高效、更安全地實(shí)現(xiàn)用戶程序與內(nèi)核之間的通信這一核心目標(biāo)展開的。無論是早期的 int 0x80 指令,還是后來的 syscall 指令,它們都在 x86 架構(gòu)的系統(tǒng)調(diào)用實(shí)現(xiàn)中發(fā)揮了重要作用,為 x86 架構(gòu)下的 Linux 系統(tǒng)提供了穩(wěn)定、高效的系統(tǒng)調(diào)用支持 。
4.2 ARM架構(gòu)
ARM 架構(gòu)下系統(tǒng)調(diào)用的實(shí)現(xiàn)與 x86 架構(gòu)有著顯著的不同,展現(xiàn)出自身獨(dú)特的特點(diǎn)。
在 ARM 架構(gòu)中,系統(tǒng)調(diào)用主要通過 SWI(Software Interrupt)指令來觸發(fā),在 Thumb 指令集下則使用 SVC(Supervisor Call)指令,它們的功能類似,都是用于實(shí)現(xiàn)從用戶態(tài)到內(nèi)核態(tài)的切換,以執(zhí)行系統(tǒng)調(diào)用。當(dāng)用戶程序需要發(fā)起系統(tǒng)調(diào)用時(shí),會(huì)執(zhí)行 SWI 或 SVC 指令,這就如同按下了一個(gè)特殊的 “開關(guān)”,觸發(fā)系統(tǒng)進(jìn)入內(nèi)核態(tài)進(jìn)行系統(tǒng)調(diào)用的處理 。
與 x86 架構(gòu)不同,在 ARM 架構(gòu)中,系統(tǒng)調(diào)用號(hào)通常被存放在 r7 寄存器中。在發(fā)起系統(tǒng)調(diào)用之前,用戶程序會(huì)將對(duì)應(yīng)的系統(tǒng)調(diào)用號(hào)存入 r7 寄存器,同時(shí),參數(shù)會(huì)被放入 r0 - r6 等寄存器中進(jìn)行傳遞。在執(zhí)行一個(gè)讀取文件的系統(tǒng)調(diào)用時(shí),會(huì)將讀取文件系統(tǒng)調(diào)用的系統(tǒng)調(diào)用號(hào)存入 r7 寄存器,文件描述符、數(shù)據(jù)緩沖區(qū)指針、數(shù)據(jù)長度等參數(shù)可能分別存放在 r0、r1、r2 等寄存器中 。
系統(tǒng)調(diào)用號(hào)的定義位置也與 x86 架構(gòu)不同。在 ARM 架構(gòu)中,系統(tǒng)調(diào)用號(hào)的定義通常位于arch/arm/include/asm/unistd.h文件中。在這個(gè)文件中,通過一系列的宏定義來為每個(gè)系統(tǒng)調(diào)用分配唯一的系統(tǒng)調(diào)用號(hào)。這些宏定義就像是一個(gè)編號(hào)分配表,明確地規(guī)定了每個(gè)系統(tǒng)調(diào)用對(duì)應(yīng)的編號(hào),使得內(nèi)核能夠根據(jù)系統(tǒng)調(diào)用號(hào)準(zhǔn)確地識(shí)別用戶程序請(qǐng)求的系統(tǒng)調(diào)用類型 。
在處理流程上,當(dāng) SWI 或 SVC 指令被執(zhí)行后,CPU 會(huì)跳轉(zhuǎn)到相應(yīng)的中斷處理程序。這個(gè)中斷處理程序會(huì)根據(jù) r7 寄存器中的系統(tǒng)調(diào)用號(hào),在系統(tǒng)調(diào)用表中查找對(duì)應(yīng)的處理函數(shù)。與 x86 架構(gòu)類似,系統(tǒng)調(diào)用表中存儲(chǔ)著各個(gè)系統(tǒng)調(diào)用處理函數(shù)的指針,通過系統(tǒng)調(diào)用號(hào)作為索引,能夠快速找到對(duì)應(yīng)的處理函數(shù)并執(zhí)行。在處理函數(shù)執(zhí)行完畢后,會(huì)將結(jié)果返回給用戶程序,同時(shí)恢復(fù)用戶態(tài)的執(zhí)行環(huán)境,使程序繼續(xù)執(zhí)行 。
ARM 架構(gòu)下系統(tǒng)調(diào)用的實(shí)現(xiàn)方式是基于其自身的硬件特點(diǎn)和設(shè)計(jì)理念而形成的。通過 SWI 或 SVC 指令觸發(fā)系統(tǒng)調(diào)用,以及獨(dú)特的系統(tǒng)調(diào)用號(hào)定義和參數(shù)傳遞方式,使得 ARM 架構(gòu)在實(shí)現(xiàn)系統(tǒng)調(diào)用時(shí),能夠充分發(fā)揮其低功耗、高性能的優(yōu)勢(shì),滿足嵌入式系統(tǒng)等應(yīng)用場景對(duì)于系統(tǒng)調(diào)用高效、穩(wěn)定執(zhí)行的需求 。
五、Linux下系統(tǒng)調(diào)用的三種方法
5.1通過 glibc 提供的庫函數(shù)
glibc 是 Linux 下使用的開源的標(biāo)準(zhǔn) C 庫,它是 GNU 發(fā)布的 libc 庫,即運(yùn)行時(shí)庫。glibc 為程序員提供豐富的 API(Application Programming Interface),除了例如字符串處理、數(shù)學(xué)運(yùn)算等用戶態(tài)服務(wù)之外,最重要的是封裝了操作系統(tǒng)提供的系統(tǒng)服務(wù),即系統(tǒng)調(diào)用的封裝。那么glibc提供的系統(tǒng)調(diào)用API與內(nèi)核特定的系統(tǒng)調(diào)用之間的關(guān)系是什么呢?
通常情況,每個(gè)特定的系統(tǒng)調(diào)用對(duì)應(yīng)了至少一個(gè) glibc 封裝的庫函數(shù),如系統(tǒng)提供的打開文件系統(tǒng)調(diào)用 sys_open 對(duì)應(yīng)的是 glibc 中的 open 函數(shù);
其次,glibc 一個(gè)單獨(dú)的 API 可能調(diào)用多個(gè)系統(tǒng)調(diào)用,如 glibc 提供的 printf 函數(shù)就會(huì)調(diào)用如 sys_open、sys_mmap、sys_write、sys_close 等等系統(tǒng)調(diào)用;
另外,多個(gè) API 也可能只對(duì)應(yīng)同一個(gè)系統(tǒng)調(diào)用,如glibc 下實(shí)現(xiàn)的 malloc、calloc、free 等函數(shù)用來分配和釋放內(nèi)存,都利用了內(nèi)核的 sys_brk 的系統(tǒng)調(diào)用。
舉例來說,我們通過 glibc 提供的chmod 函數(shù)來改變文件 etc/passwd 的屬性為 444:
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>
int main()
{
int rc;
rc = chmod("/etc/passwd", 0444);
if (rc == -1)
fprintf(stderr, "chmod failed, errno = %d\n", errno);
else
printf("chmod success!\n");
return 0;
}在普通用戶下編譯運(yùn)用,輸出結(jié)果為:
chmod failed, errno = 1上面系統(tǒng)調(diào)用返回的值為-1,說明系統(tǒng)調(diào)用失敗,錯(cuò)誤碼為1,在 /usr/include/asm-generic/errno-base.h 文件中有如下錯(cuò)誤代碼說明:
#define EPERM 1 /* Operation not permitted */即無權(quán)限進(jìn)行該操作,我們以普通用戶權(quán)限是無法修改 /etc/passwd 文件的屬性的,結(jié)果正確。
5.2使用 syscall 直接調(diào)用
使用上面的方法有很多好處,首先你無須知道更多的細(xì)節(jié),如 chmod 系統(tǒng)調(diào)用號(hào),你只需了解 glibc 提供的 API 的原型;其次,該方法具有更好的移植性,你可以很輕松將該程序移植到其他平臺(tái),或者將 glibc 庫換成其它庫,程序只需做少量改動(dòng)。
但有點(diǎn)不足是,如果 glibc 沒有封裝某個(gè)內(nèi)核提供的系統(tǒng)調(diào)用時(shí),我就沒辦法通過上面的方法來調(diào)用該系統(tǒng)調(diào)用。如我自己通過編譯內(nèi)核增加了一個(gè)系統(tǒng)調(diào)用,這時(shí) glibc 不可能有你新增系統(tǒng)調(diào)用的封裝 API,此時(shí)我們可以利用 glibc 提供的syscall 函數(shù)直接調(diào)用。該函數(shù)定義在 unistd.h 頭文件中,函數(shù)原型如下:
long int syscall (long int sysno, ...)sysno 是系統(tǒng)調(diào)用號(hào),每個(gè)系統(tǒng)調(diào)用都有唯一的系統(tǒng)調(diào)用號(hào)來標(biāo)識(shí)。在 sys/syscall.h 中有所有可能的系統(tǒng)調(diào)用號(hào)的宏定義。
... 為剩余可變長的參數(shù),為系統(tǒng)調(diào)用所帶的參數(shù),根據(jù)系統(tǒng)調(diào)用的不同,可帶0~5個(gè)不等的參數(shù),如果超過特定系統(tǒng)調(diào)用能帶的參數(shù),多余的參數(shù)被忽略。
返回值 該函數(shù)返回值為特定系統(tǒng)調(diào)用的返回值,在系統(tǒng)調(diào)用成功之后你可以將該返回值轉(zhuǎn)化為特定的類型,如果系統(tǒng)調(diào)用失敗則返回 -1,錯(cuò)誤代碼存放在 errno 中。
還以上面修改 /etc/passwd 文件的屬性為例,這次使用 syscall 直接調(diào)用:
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>
int main()
{
int rc;
rc = syscall(SYS_chmod, "/etc/passwd", 0444);
if (rc == -1)
fprintf(stderr, "chmod failed, errno = %d\n", errno);
else
printf("chmod succeess!\n");
return 0;
}在普通用戶下編譯執(zhí)行,輸出的結(jié)果與上例相同。
5.3通過 int 指令陷入
如果我們知道系統(tǒng)調(diào)用的整個(gè)過程的話,應(yīng)該就能知道用戶態(tài)程序通過軟中斷指令int 0x80 來陷入內(nèi)核態(tài)(在Intel Pentium II 又引入了sysenter指令),參數(shù)的傳遞是通過寄存器,eax 傳遞的是系統(tǒng)調(diào)用號(hào),ebx、ecx、edx、esi和edi 來依次傳遞最多五個(gè)參數(shù),當(dāng)系統(tǒng)調(diào)用返回時(shí),返回值存放在 eax 中。
仍然以上面的修改文件屬性為例,將調(diào)用系統(tǒng)調(diào)用那段寫成內(nèi)聯(lián)匯編代碼:
#include <stdio.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <errno.h>
int main()
{
long rc;
char *file_name = "/etc/passwd";
unsigned short mode = 0444;
asm(
"int $0x80"
: "=a" (rc)
: "0" (SYS_chmod), "b" ((long)file_name), "c" ((long)mode)
);
if ((unsigned long)rc >= (unsigned long)-132) {
errno = -rc;
rc = -1;
}
if (rc == -1)
fprintf(stderr, "chmode failed, errno = %d\n", errno);
else
printf("success!\n");
return 0;
}如果 eax 寄存器存放的返回值(存放在變量 rc 中)在 -1~-132 之間,就必須要解釋為出錯(cuò)碼(在/usr/include/asm-generic/errno.h 文件中定義的最大出錯(cuò)碼為 132),這時(shí),將錯(cuò)誤碼寫入 errno 中,置系統(tǒng)調(diào)用返回值為 -1;否則返回的是 eax 中的值。
上面程序在 32位Linux下以普通用戶權(quán)限編譯運(yùn)行結(jié)果與前面兩個(gè)相同!
六、系統(tǒng)調(diào)用的應(yīng)用場景與實(shí)例分析
6.1Linux下系統(tǒng)調(diào)用的實(shí)現(xiàn)
Linux下的系統(tǒng)調(diào)用是通過0x80實(shí)現(xiàn)的,但是我們知道操作系統(tǒng)會(huì)有多個(gè)系統(tǒng)調(diào)用(Linux下有319個(gè)系統(tǒng)調(diào)用),而對(duì)于同一個(gè)中斷號(hào)是如何處理多個(gè)不同的系統(tǒng)調(diào)用的?最簡單的方式是對(duì)于不同的系統(tǒng)調(diào)用采用不同的中斷號(hào),但是中斷號(hào)明顯是一種稀缺資源,Linux顯然不會(huì)這么做;還有一個(gè)問題就是系統(tǒng)調(diào)用是需要提供參數(shù),并且具有返回值的,這些參數(shù)又是怎么傳遞的?也就是說,對(duì)于系統(tǒng)調(diào)用我們要搞清楚兩點(diǎn):
- 1. 系統(tǒng)調(diào)用的函數(shù)名稱轉(zhuǎn)換。
- 2. 系統(tǒng)調(diào)用的參數(shù)傳遞。
首先看第一個(gè)問題。實(shí)際上,Linux中處理系統(tǒng)調(diào)用的方式與中斷類似。每個(gè)系統(tǒng)調(diào)用都有相應(yīng)的系統(tǒng)調(diào)用號(hào)作為唯一的標(biāo)識(shí),內(nèi)核維護(hù)一張系統(tǒng)調(diào)用表,表中的元素是系統(tǒng)調(diào)用函數(shù)的起始地址,而系統(tǒng)調(diào)用號(hào)就是系統(tǒng)調(diào)用在調(diào)用表的偏移量。在進(jìn)行系統(tǒng)調(diào)用是只要指定對(duì)應(yīng)的系統(tǒng)調(diào)用號(hào),就可以明確的要調(diào)用哪個(gè)系統(tǒng)調(diào)用,這就完成了系統(tǒng)調(diào)用的函數(shù)名稱的轉(zhuǎn)換。舉例來說,Linux中fork的調(diào)用號(hào)是2(具體定義,在我的計(jì)算機(jī)上是在/usr/include/asm/unistd_32.h,可以通過find / -name unistd_32.h -print查找)
[cpp] view plain copy
#ifndef _ASM_X86_UNISTD_32_H
#define _ASM_X86_UNISTD_32_H
/*
* This file contains the system call numbers.
*/
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5Linux中是通過寄存器%eax傳遞系統(tǒng)調(diào)用號(hào),所以具體調(diào)用fork的過程是:將2存入%eax中,然后進(jìn)行系統(tǒng)調(diào)用,偽代碼:
[plain] view plain copy
mov eax, 2
int 0x80對(duì)于參數(shù)傳遞,Linux是通過寄存器完成的。Linux最多允許向系統(tǒng)調(diào)用傳遞6個(gè)參數(shù),分別依次由%ebx,%ecx,%edx,%esi,%edi和%ebp這個(gè)6個(gè)寄存器完成。比如,調(diào)用exit(1),偽代碼是:
[plain] view plain copy
mov eax, 2
mov ebx, 1
int 0x80因?yàn)閑xit需要一個(gè)參數(shù)1,所以這里只需要使用ebx。這6個(gè)寄存器可能已經(jīng)被使用,所以在傳參前必須把當(dāng)前寄存器的狀態(tài)保存下來,待系統(tǒng)調(diào)用返回后再恢復(fù),這個(gè)在后面棧切換再具體講。
Linux中,在用戶態(tài)和內(nèi)核態(tài)運(yùn)行的進(jìn)程使用的棧是不同的,分別叫做用戶棧和內(nèi)核棧,兩者各自負(fù)責(zé)相應(yīng)特權(quán)級(jí)別狀態(tài)下的函數(shù)調(diào)用。當(dāng)進(jìn)行系統(tǒng)調(diào)用時(shí),進(jìn)程不僅要從用戶態(tài)切換到內(nèi)核態(tài),同時(shí)也要完成棧切換,這樣處于內(nèi)核態(tài)的系統(tǒng)調(diào)用才能在內(nèi)核棧上完成調(diào)用。系統(tǒng)調(diào)用返回時(shí),還要切換回用戶棧,繼續(xù)完成用戶態(tài)下的函數(shù)調(diào)用。
寄存器%esp(棧指針,指向棧頂)所在的內(nèi)存空間叫做當(dāng)前棧,比如%esp在用戶空間則當(dāng)前棧就是用戶棧,否則是內(nèi)核棧。棧切換主要就是%esp在用戶空間和內(nèi)核空間間的來回賦值。在Linux中,每個(gè)進(jìn)程都有一個(gè)私有的內(nèi)核棧,當(dāng)從用戶棧切換到內(nèi)核棧時(shí),需完成保存%esp以及相關(guān)寄存器的值(%ebx,%ecx...)并將%esp設(shè)置成內(nèi)核棧的相應(yīng)值。
而從內(nèi)核棧切換會(huì)用戶棧時(shí),需要恢復(fù)用戶棧的%esp及相關(guān)寄存器的值以及保存內(nèi)核棧的信息。一個(gè)問題就是用戶棧的%esp和寄存器的值保存到什么地方,以便于恢復(fù)呢?答案就是內(nèi)核棧,在調(diào)用int指令機(jī)型系統(tǒng)調(diào)用后會(huì)把用戶棧的%esp的值及相關(guān)寄存器壓入內(nèi)核棧中,系統(tǒng)調(diào)用通過iret指令返回,在返回之前會(huì)從內(nèi)核棧彈出用戶棧的%esp和寄存器的狀態(tài),然后進(jìn)行恢復(fù)。
相信大家一定聽過說,系統(tǒng)調(diào)用很耗時(shí),要盡量少用。通過上面描述系統(tǒng)調(diào)用的實(shí)現(xiàn)原理,大家也應(yīng)該知道這其中的原因了。
- 第一,系統(tǒng)調(diào)用通過中斷實(shí)現(xiàn),需要完成棧切換。
- 第二,使用寄存器傳參,這需要額外的保存和恢復(fù)的過程。
6.2文件操作相關(guān)系統(tǒng)調(diào)用
在 Linux 系統(tǒng)中,文件操作是日常使用和開發(fā)中極為常見的任務(wù),而 open、read、write、close 等系統(tǒng)調(diào)用則是實(shí)現(xiàn)文件操作的核心工具。
open 系統(tǒng)調(diào)用用于打開或創(chuàng)建一個(gè)文件,它的函數(shù)原型為int open(const char *pathname, int flags, mode_t mode);。其中,pathname是要打開或創(chuàng)建的文件的路徑名;flags參數(shù)用于指定文件的打開方式,比如O_RDONLY表示以只讀方式打開,O_WRONLY表示以只寫方式打開,O_RDWR則表示以讀寫方式打開,還有一些可選的標(biāo)志位,如O_CREAT表示如果文件不存在則創(chuàng)建新文件,O_APPEND表示以追加方式寫入文件等;mode參數(shù)在創(chuàng)建新文件時(shí)用于指定文件的訪問權(quán)限,如S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH表示文件所有者具有讀寫權(quán)限,同組用戶和其他用戶具有讀權(quán)限 。
read 系統(tǒng)調(diào)用用于從文件中讀取數(shù)據(jù),函數(shù)原型是ssize_t read(int fd, void *buf, size_t count);,fd是文件描述符,它是 open 系統(tǒng)調(diào)用成功返回的一個(gè)非負(fù)整數(shù),用于標(biāo)識(shí)打開的文件;buf是用于存儲(chǔ)讀取數(shù)據(jù)的緩沖區(qū)指針;count表示期望讀取的字節(jié)數(shù),該函數(shù)返回實(shí)際讀取到的字節(jié)數(shù) 。
write 系統(tǒng)調(diào)用則用于向文件中寫入數(shù)據(jù),其函數(shù)原型為ssize_t write(int fd, const void *buf, size_t count);,參數(shù)含義與 read 類似,fd為文件描述符,buf是要寫入數(shù)據(jù)的緩沖區(qū)指針,count是要寫入的字節(jié)數(shù),返回值是實(shí)際寫入的字節(jié)數(shù) 。
close 系統(tǒng)調(diào)用用于關(guān)閉一個(gè)打開的文件,函數(shù)原型為int close(int fd);,fd為要關(guān)閉的文件描述符,關(guān)閉成功返回 0,失敗返回 - 1 。
以下是一個(gè)簡單的 C 語言代碼示例,展示了如何使用這些系統(tǒng)調(diào)用實(shí)現(xiàn)文件的讀取和寫入操作:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int source_fd, destination_fd;
ssize_t bytes_read, bytes_written;
char buffer[BUFFER_SIZE];
// 打開源文件,以只讀方式
source_fd = open("source.txt", O_RDONLY);
if (source_fd == -1) {
perror("無法打開源文件");
return 1;
}
// 創(chuàng)建目標(biāo)文件,以讀寫方式,如果文件不存在則創(chuàng)建,權(quán)限設(shè)置為所有者可讀可寫,其他用戶可讀
destination_fd = open("destination.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (destination_fd == -1) {
perror("無法創(chuàng)建目標(biāo)文件");
close(source_fd);
return 1;
}
// 從源文件讀取數(shù)據(jù)并寫入目標(biāo)文件
while ((bytes_read = read(source_fd, buffer, BUFFER_SIZE)) > 0) {
bytes_written = write(destination_fd, buffer, bytes_read);
if (bytes_written != bytes_read) {
perror("寫入目標(biāo)文件失敗");
close(source_fd);
close(destination_fd);
return 1;
}
}
if (bytes_read == -1) {
perror("讀取源文件失敗");
}
// 關(guān)閉文件
close(source_fd);
close(destination_fd);
return 0;
}在這個(gè)示例中,首先使用 open 系統(tǒng)調(diào)用以只讀方式打開名為source.txt的源文件,如果打開失敗,通過perror函數(shù)輸出錯(cuò)誤信息并返回 1。接著,使用 open 系統(tǒng)調(diào)用以讀寫方式創(chuàng)建名為destination.txt的目標(biāo)文件,如果文件已存在則截?cái)辔募?nèi)容,如果創(chuàng)建失敗同樣輸出錯(cuò)誤信息并關(guān)閉已打開的源文件后返回 1 。
然后,通過一個(gè)循環(huán),使用 read 系統(tǒng)調(diào)用從源文件中讀取數(shù)據(jù)到緩沖區(qū)buffer中,每次最多讀取BUFFER_SIZE個(gè)字節(jié)。只要讀取到的數(shù)據(jù)長度大于 0,就使用 write 系統(tǒng)調(diào)用將緩沖區(qū)中的數(shù)據(jù)寫入目標(biāo)文件。如果寫入的字節(jié)數(shù)與讀取的字節(jié)數(shù)不一致,說明寫入失敗,輸出錯(cuò)誤信息并關(guān)閉兩個(gè)文件后返回 1 。
如果在讀取過程中bytes_read等于 - 1,說明讀取失敗,輸出錯(cuò)誤信息。最后,使用 close 系統(tǒng)調(diào)用分別關(guān)閉源文件和目標(biāo)文件,完成文件操作 。
通過這個(gè)示例,我們可以清晰地看到 open、read、write、close 系統(tǒng)調(diào)用在文件讀寫操作中的具體應(yīng)用和執(zhí)行流程,它們相互配合,實(shí)現(xiàn)了高效、準(zhǔn)確的文件數(shù)據(jù)傳輸和管理 。
6.3進(jìn)程管理相關(guān)系統(tǒng)調(diào)用
在 Linux 系統(tǒng)中,進(jìn)程管理是操作系統(tǒng)的核心功能之一,fork、exec、wait 等系統(tǒng)調(diào)用在進(jìn)程的創(chuàng)建、執(zhí)行和等待過程中發(fā)揮著關(guān)鍵作用。
fork 系統(tǒng)調(diào)用用于創(chuàng)建一個(gè)新的進(jìn)程,稱為子進(jìn)程,它的函數(shù)原型為pid_t fork(void);。調(diào)用 fork 后,系統(tǒng)會(huì)創(chuàng)建一個(gè)與原進(jìn)程(父進(jìn)程)幾乎完全相同的子進(jìn)程,子進(jìn)程復(fù)制了父進(jìn)程的代碼段、數(shù)據(jù)段、堆棧段等資源。但父子進(jìn)程也有一些不同之處,它們擁有不同的進(jìn)程 ID(PID),通過getpid()函數(shù)可以獲取當(dāng)前進(jìn)程的 PID,通過getppid()函數(shù)可以獲取父進(jìn)程的 PID 。fork 函數(shù)的返回值非常特殊,在父進(jìn)程中,返回值是新創(chuàng)建子進(jìn)程的 PID;在子進(jìn)程中,返回值為 0;如果創(chuàng)建子進(jìn)程失敗,返回值為 - 1 。
exec 系統(tǒng)調(diào)用并不是一個(gè)單獨(dú)的函數(shù),而是一組函數(shù),如execl、execv、execle、execve等,它們的主要作用是在當(dāng)前進(jìn)程中啟動(dòng)另一個(gè)程序。當(dāng)進(jìn)程調(diào)用 exec 函數(shù)時(shí),會(huì)用新的程序替換當(dāng)前進(jìn)程的正文、數(shù)據(jù)、堆和棧段,也就是說,當(dāng)前進(jìn)程會(huì)被新的程序完全取代,從新程序的main函數(shù)開始執(zhí)行。由于 exec 并不創(chuàng)建新進(jìn)程,所以前后的進(jìn)程 ID 并未改變 。
wait 系統(tǒng)調(diào)用用于等待子進(jìn)程的結(jié)束,并獲取子進(jìn)程的退出狀態(tài),函數(shù)原型為pid_t wait(int *status);。status是一個(gè)指向整數(shù)的指針,用于存儲(chǔ)子進(jìn)程的退出狀態(tài)信息。調(diào)用 wait 后,父進(jìn)程會(huì)阻塞,直到有一個(gè)子進(jìn)程結(jié)束,此時(shí) wait 返回結(jié)束子進(jìn)程的 PID,并將子進(jìn)程的退出狀態(tài)存儲(chǔ)在status指向的變量中 。
下面通過一個(gè)簡單的代碼示例來說明進(jìn)程創(chuàng)建和父子進(jìn)程的執(zhí)行流程:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
pid_t pid;
int status;
// 創(chuàng)建子進(jìn)程
pid = fork();
if (pid == -1) {
perror("fork失敗");
exit(1);
} else if (pid == 0) {
// 子進(jìn)程執(zhí)行的代碼
printf("我是子進(jìn)程,我的PID是 %d,父進(jìn)程的PID是 %d\n", getpid(), getppid());
// 子進(jìn)程執(zhí)行另一個(gè)程序,這里以執(zhí)行l(wèi)s命令為例
execl("/bin/ls", "ls", "-l", NULL);
perror("execl失敗");
exit(1);
} else {
// 父進(jìn)程執(zhí)行的代碼
printf("我是父進(jìn)程,我的PID是 %d,子進(jìn)程的PID是 %d\n", getpid(), pid);
// 父進(jìn)程等待子進(jìn)程結(jié)束
wait(&status);
printf("子進(jìn)程已結(jié)束,退出狀態(tài)為 %d\n", WEXITSTATUS(status));
}
return 0;
}在這個(gè)示例中,首先調(diào)用 fork 系統(tǒng)調(diào)用創(chuàng)建子進(jìn)程。如果 fork 返回 - 1,說明創(chuàng)建子進(jìn)程失敗,通過perror函數(shù)輸出錯(cuò)誤信息并調(diào)用exit函數(shù)退出程序 。
如果 fork 返回 0,說明當(dāng)前是子進(jìn)程,子進(jìn)程打印自己的 PID 和父進(jìn)程的 PID,然后調(diào)用execl函數(shù)執(zhí)行/bin/ls -l命令,列出當(dāng)前目錄下的文件詳細(xì)信息。如果execl執(zhí)行失敗,同樣輸出錯(cuò)誤信息并退出 。
如果 fork 返回一個(gè)大于 0 的值,說明當(dāng)前是父進(jìn)程,父進(jìn)程打印自己的 PID 和子進(jìn)程的 PID,然后調(diào)用 wait 系統(tǒng)調(diào)用等待子進(jìn)程結(jié)束。當(dāng)子進(jìn)程結(jié)束后,wait 返回,父進(jìn)程獲取子進(jìn)程的退出狀態(tài),并打印子進(jìn)程已結(jié)束以及其退出狀態(tài) 。
通過這個(gè)示例,我們可以清楚地看到 fork、exec、wait 系統(tǒng)調(diào)用在進(jìn)程管理中的協(xié)同工作,fork 用于創(chuàng)建新進(jìn)程,exec 用于在子進(jìn)程中執(zhí)行新程序,wait 用于父進(jìn)程等待子進(jìn)程結(jié)束并獲取其退出狀態(tài),它們共同構(gòu)成了 Linux 系統(tǒng)強(qiáng)大的進(jìn)程管理機(jī)制 。

































