Linux 系統調用解析:用戶與內核交互的紐帶
在Linux操作系統的廣袤天地里,系統調用宛如一座連接用戶程序與內核的關鍵橋梁。當你在終端敲下指令,或是啟動各類應用程序,背后都有系統調用忙碌的身影。它不僅是內核為用戶空間提供服務的核心接口,更是深入理解 Linux 運行機制的一把重要鑰匙。將操作系統比作一座精密運轉的工廠,內核無疑是掌控全局的中樞,而系統調用則是連接生產線(用戶程序)與中樞的通道。用戶程序借助它向內核發起服務請求,無論是文件操作、進程管理,還是內存分配等復雜任務,系統調用都扮演著無可替代的角色。
從本質而言,系統調用是內核精心提供的接口,常以 C 函數的形式呈現,極大地方便了開發者調用系統功能。在 Linux 系統中,它更是構建起用戶程序安全訪問硬件資源的關鍵通道。由于硬件資源由內核底層操作,若用戶程序直接訪問,極易引發系統不穩定甚至崩潰。而系統調用如同忠誠的門衛,有效保障了系統的安全與穩定。內核借助它合理調度 CPU 時間,讓多個進程能夠和諧協同工作,顯著提升系統并發處理能力,確保系統始終流暢運行??梢哉f,掌握系統調用,就能更好地與 Linux 交互,充分釋放系統潛能。
Part1Linux 系統調用是什么?
1.1系統調用的定義
在 Linux 操作系統中,系統調用是用戶程序與內核交互的關鍵接口,是用戶程序訪問內核服務的唯一合法途徑。內核作為操作系統的核心,負責管理系統的硬件資源和提供各種基本服務,如進程管理、內存管理、文件系統管理等。然而,用戶程序不能直接訪問內核的資源和服務,必須通過系統調用這個橋梁來請求內核執行特定的操作。
可以將系統調用類比為一個公司的客戶服務熱線。用戶程序就像是公司的客戶,有各種需求;而內核則像是公司內部的核心業務部門,負責處理各種關鍵事務??蛻舨荒苤苯舆M入公司內部指揮業務部門工作,而是需要撥打客戶服務熱線(系統調用),向接線員(內核接口)提出請求,接線員再將請求傳達給相應的業務部門(內核服務)進行處理 。
1.2系統調用的重要性
系統調用在操作系統中占據著舉足輕重的地位,是操作系統提供服務和實現功能的基礎機制,在資源管理、進程控制、文件操作、設備管理等眾多方面發揮著關鍵作用。
在資源管理方面,系統調用允許用戶程序請求內核分配和管理系統資源,如內存、CPU 時間片、文件描述符等。通過系統調用,內核可以根據系統的整體狀態和資源使用情況,合理地分配資源,確保各個進程能夠公平、有效地使用資源,避免資源沖突和浪費。比如,當一個程序需要使用內存時,它會通過系統調用向內核請求分配一定大小的內存空間,內核會在內存管理模塊中進行相應的處理,為程序分配合適的內存塊,并返回一個指向該內存塊的指針,供程序使用。
進程控制是操作系統的核心功能之一,系統調用為進程的創建、執行、終止和等待等操作提供了接口。例如,fork () 系統調用用于創建一個新的進程,它是一個與父進程幾乎完全相同的子進程,擁有自己獨立的進程標識符(PID)。父進程可以通過 wait () 或 waitpid () 系統調用等待子進程結束,并獲取子進程的退出狀態,以便進行后續的處理。這些系統調用使得開發者能夠方便地控制進程的生命周期,實現多進程編程,提高程序的并發處理能力。
文件操作是日常計算機使用中最常見的任務之一,系統調用為文件的打開、關閉、讀取、寫入、定位等操作提供了統一的接口。應用程序通過 open () 系統調用打開一個文件,并獲得一個文件描述符,后續對文件的所有操作都通過這個文件描述符進行。write () 系統調用用于向文件中寫入數據,read () 系統調用則用于從文件中讀取數據。這些系統調用屏蔽了底層文件系統的復雜性,使得開發者可以專注于文件操作的邏輯,而不必關心具體的文件存儲和訪問方式。
在設備管理方面,系統調用為應用程序提供了訪問硬件設備的能力。無論是磁盤、打印機、網絡接口等設備,都可以通過相應的系統調用來進行控制和交互。例如,ioctl () 系統調用常用于對設備進行控制操作,如設置設備的參數、獲取設備的狀態信息等。通過系統調用,應用程序可以與各種硬件設備進行通信,實現設備的驅動和管理,使得操作系統能夠充分發揮硬件設備的功能。
1.3為什么需要系統調用
linux內核中設置了一組用于實現系統功能的子程序,稱為系統調用。系統調用和普通庫函數調用非常相似,只是系統調用由操作系統核心提供,運行于內核態,而普通的函數調用由函數庫或用戶自己提供,運行于用戶態。
一般的,進程是不能訪問內核的。它不能訪問內核所占內存空間也不能調用內核函數。CPU硬件決定了這些(這就是為什么它被稱作“保護模式”)。
為了和用戶空間上運行的進程進行交互,內核提供了一組接口。透過該接口,應用程序可以訪問硬件設備和其他操作系統資源。這組接口在應用程序和內核之間扮演了使者的角色,應用程序發送各種請求,而內核負責滿足這些請求(或者讓應用程序暫時擱置)。實際上提供這組接口主要是為了保證系統穩定可靠,避免應用程序肆意妄行,惹出大麻煩。
系統調用在用戶空間進程和硬件設備之間添加了一個中間層,該層主要作用有三個:
- 它為用戶空間提供了一種統一的硬件的抽象接口。比如當需要讀些文件的時候,應用程序就可以不去管磁盤類型和介質,甚至不用去管文件所在的文件系統到底是哪種類型。
- 系統調用保證了系統的穩定和安全。作為硬件設備和應用程序之間的中間人,內核可以基于權限和其他一些規則對需要進行的訪問進行裁決。舉例來說,這樣可以避免應用程序不正確地使用硬件設備,竊取其他進程的資源,或做出其他什么危害系統的事情。
- 每個進程都運行在虛擬系統中,而在用戶空間和系統的其余部分提供這樣一層公共接口,也是出于這種考慮。如果應用程序可以隨意訪問硬件而內核又對此一無所知的話,幾乎就沒法實現多任務和虛擬內存,當然也不可能實現良好的穩定性和安全性。在Linux中,系統調用是用戶空間訪問內核的惟一手段;除異常和中斷外,它們是內核惟一的合法入口。
Part2用戶空間與內核空間
對 32 位操作系統而言,它的尋址空間(虛擬地址空間,或叫線性地址空間)為 4G(2的32次方)。也就是說一個進程的最大地址空間為 4G。操作系統的核心是內核(kernel),它獨立于普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證內核的安全,現在的操作系統一般都強制用戶進程不能直接操作內核。具體的實現方式基本都是由操作系統將虛擬地址空間劃分為兩部分,一部分為內核空間,另一部分為用戶空間。針對 Linux 操作系統而言,最高的 1G 字節(從虛擬地址 0xC0000000 到 0xFFFFFFFF)由內核使用,稱為內核空間。而較低的 3G 字節(從虛擬地址 0x00000000 到 0xBFFFFFFF)由各個進程使用,稱為用戶空間。
圖片
2.1用戶空間
用戶空間是應用程序運行的區域,它為應用程序提供了一個相對獨立和受保護的執行環境。在 32 位的 Linux 系統中,通常將虛擬地址空間的低 3GB 劃分為用戶空間,每個用戶進程都擁有自己獨立的用戶空間,這使得不同進程之間的地址空間相互隔離,一個進程的崩潰不會影響其他進程的正常運行,就像一個個獨立的小房間,每個房間里的活動互不干擾 。
用戶空間的應用程序具有受限的訪問權限,它們無法直接操作硬件資源,如CPU、內存、I/O設備等。這是因為硬件資源的直接操作需要較高的權限,而用戶空間的應用程序運行在較低的特權級別(如 x86 架構中的 Ring 3),以防止應用程序對系統造成破壞。如果把操作系統比作一個大型工廠,硬件資源就是工廠里的核心生產設備,而用戶空間的應用程序就像是工廠里的普通工人,他們不能隨意操作核心生產設備,必須通過特定的流程(系統調用)向管理層(內核)提出申請 。
2.2內核空間
內核空間是操作系統內核的執行區域,是操作系統的核心部分,負責管理系統的硬件資源和提供各種基本服務。在 32 位的 Linux 系統中,虛擬地址空間的高 1GB 通常被劃分為內核空間,所有進程共享這部分內核空間。內核空間就像是工廠的管理層,擁有最高的權限,可以直接訪問和控制所有硬件資源,執行特權指令。
內核空間擁有完全控制硬件資源的權限,它可以直接操作 CPU、內存、I/O 設備等,負責進程調度、內存管理、文件系統管理、設備驅動等關鍵功能。例如,在內核空間中,內核可以根據進程的優先級和資源需求,合理地分配 CPU 時間片,確保各個進程能夠公平地使用 CPU 資源;內核還負責管理物理內存和虛擬內存之間的映射關系,為進程分配和回收內存;此外,內核還通過設備驅動程序與各種硬件設備進行交互,實現對硬件設備的控制和管理。
2.3為什么需要區分內核空間與用戶空間
在 CPU 的所有指令中,有些指令是非常危險的,如果錯用,將導致系統崩潰,比如清內存、設置時鐘等。如果允許所有的程序都可以使用這些指令,那么系統崩潰的概率將大大增加。所以,CPU 將指令分為特權指令和非特權指令,對于那些危險的指令,只允許操作系統及其相關模塊使用,普通應用程序只能使用那些不會造成災難的指令。比如 Intel 的 CPU 將特權等級分為 4 個級別:Ring0~Ring3。
其實 Linux 系統只使用了 Ring0 和 Ring3 兩個運行級別(Windows 系統也是一樣的)。當進程運行在 Ring3 級別時被稱為運行在用戶態,而運行在 Ring0 級別時被稱為運行在內核態。
當進程運行在內核空間時就處于內核態,而進程運行在用戶空間時則處于用戶態。
在內核態下,進程運行在內核地址空間中,此時 CPU 可以執行任何指令。運行的代碼也不受任何的限制,可以自由地訪問任何有效地址,也可以直接進行端口的訪問。
在用戶態下,進程運行在用戶地址空間中,被執行的代碼要受到 CPU 的諸多檢查,它們只能訪問映射其地址空間的頁表項中規定的在用戶態下可訪問頁面的虛擬地址,且只能對任務狀態段(TSS)中 I/O 許可位圖(I/O Permission Bitmap)中規定的可訪問端口進行直接訪問。
對于以前的 DOS 操作系統來說,是沒有內核空間、用戶空間以及內核態、用戶態這些概念的。可以認為所有的代碼都是運行在內核態的,因而用戶編寫的應用程序代碼可以很容易的讓操作系統崩潰掉。
對于 Linux 來說,通過區分內核空間和用戶空間的設計,隔離了操作系統代碼(操作系統的代碼要比應用程序的代碼健壯很多)與應用程序代碼。即便是單個應用程序出現錯誤也不會影響到操作系統的穩定性,這樣其它的程序還可以正常的運行(Linux 可是個多任務系統啊!)。
所以,區分內核空間和用戶空間本質上是要提高操作系統的穩定性及可用性
Part3系統調用的執行過程
Linux 系統調用的執行過程涉及多個關鍵步驟,從用戶程序發起調用,到內核進行處理,再到最終返回結果給用戶程序,每一步都至關重要。接下來,我們將詳細探討 Linux 系統調用的執行過程。
3.1準備階段
在用戶程序調用系統調用之前,首先需要設置相關參數,這些參數包括系統調用號以及其他與具體系統調用相關的參數。系統調用號是一個唯一的標識符,用于標識不同的系統調用。每個系統調用都被分配了一個特定的編號,就像圖書館里的每一本書都有一個唯一的編號一樣,方便快速查找和識別 。
在 x86 架構中,系統調用號通常通過 EAX 寄存器傳遞給內核。例如,對于常見的 read 系統調用,其系統調用號為 3(在不同架構和內核版本中可能會有所不同)。除了系統調用號,還需要傳遞其他參數,如 read 系統調用需要傳遞文件描述符(通過 EBX 寄存器傳遞)、緩沖區指針(通過 ECX 寄存器傳遞)和讀取的字節數(通過 EDX 寄存器傳遞) 。
3.2觸發系統調用
用戶程序設置好參數后,需要通過特定的指令來觸發系統調用,從而進入內核態執行相應的操作。在 x86 架構中,傳統上常用的指令是 int 0x80,這是一條軟中斷指令。當 CPU 執行到 int 0x80 指令時,會產生一個軟件中斷,從而觸發系統調用機制。隨著技術的發展,x86 架構從內核 2.6.11 版本開始引入了 syscall 指令,它比 int 0x80 指令具有更高的性能。syscall 指令通過特定的寄存器(如 RAX、RDI、RSI 等)來傳遞系統調用號和參數,能夠更高效地實現從用戶態到內核態的切換 。
除了 x86 架構,其他架構也有各自對應的觸發系統調用的方式。在 ARM 架構中,通常使用 svc(Supervisor Call)指令來觸發系統調用,該指令會導致處理器進入特權模式,從而執行內核中的系統調用處理程序 。
3.3進入內核態
當用戶程序觸發系統調用后,CPU 會從用戶態切換到內核態,這是一個關鍵的狀態轉換過程。在這個過程中,CPU 會保存當前用戶程序的上下文信息,包括寄存器的值、程序計數器(PC)等,以便在系統調用完成后能夠恢復用戶程序的執行。
CPU 通過中斷向量表來確定系統調用處理程序的入口地址。中斷向量表是一個存儲中斷處理程序入口地址的表格,每個中斷向量對應一個特定的中斷類型。對于系統調用,會有一個特定的中斷向量(如 x86 架構中 int 0x80 對應的中斷向量),CPU 根據這個中斷向量在中斷向量表中查找對應的系統調用處理程序的入口地址,然后跳轉到該地址開始執行內核代碼 。
3.4參數檢查與處理
在內核開始執行系統調用之前,需要對用戶程序傳遞過來的參數進行合法性檢查,這是確保系統安全性和穩定性的重要環節。內核會檢查參數的類型、范圍、有效性等,以防止用戶程序傳遞非法參數導致系統崩潰或出現安全漏洞。
對于涉及指針的參數,內核會檢查指針是否指向合法的內存地址,并且該內存地址是否屬于用戶程序的地址空間,防止用戶程序通過傳遞非法指針來訪問內核空間或其他進程的內存空間。對于文件描述符參數,內核會檢查文件描述符是否有效,是否指向一個已經打開的文件,以及當前進程是否具有對該文件的相應操作權限 。
3.5執行系統調用函數
內核完成參數檢查后,會根據系統調用號從系統調用表中找到對應的系統調用函數,并執行該函數。系統調用表是一個存儲系統調用函數指針的數組,每個系統調用號對應數組中的一個元素,該元素指向相應的系統調用函數。
在 x86 架構的 32 位系統中,系統調用表位于 arch/x86/kernel/syscall_table_32.S 文件中;在 64 位系統中,系統調用表位于 arch/x86/entry/syscalls/syscall_64.tbl 文件中。例如,對于 read 系統調用,內核會在系統調用表中找到對應的函數指針,然后調用該函數來執行實際的讀取操作。在執行過程中,內核會根據用戶傳遞的參數,如文件描述符、緩沖區指針和讀取字節數,來完成文件讀取任務,并將讀取的數據存儲到用戶指定的緩沖區中 。
3.6返回用戶態
當系統調用函數執行完成后,內核會將結果返回給用戶程序,并將 CPU 從內核態切換回用戶態。內核將系統調用的返回值存儲在特定的寄存器中(如 x86 架構中通過 EAX 寄存器返回),然后恢復之前保存的用戶程序的上下文信息,包括寄存器的值、程序計數器等。
CPU 根據恢復的程序計數器的值,跳轉到用戶程序中系統調用指令的下一條指令處繼續執行,從而完成系統調用的整個過程。如果系統調用執行過程中發生錯誤,內核會返回一個錯誤碼,用戶程序可以通過檢查返回值來判斷系統調用是否成功,并根據錯誤碼進行相應的錯誤處理 。
Part4Linux下系統調用的三種方法
4.1通過 glibc 提供的庫函數
glibc 是 Linux 下使用的開源的標準 C 庫,它是 GNU 發布的 libc 庫,即運行時庫。glibc 為程序員提供豐富的 API(Application Programming Interface),除了例如字符串處理、數學運算等用戶態服務之外,最重要的是封裝了操作系統提供的系統服務,即系統調用的封裝。那么glibc提供的系統調用API與內核特定的系統調用之間的關系是什么呢?
通常情況,每個特定的系統調用對應了至少一個 glibc 封裝的庫函數,如系統提供的打開文件系統調用 sys_open 對應的是 glibc 中的 open 函數;
其次,glibc 一個單獨的 API 可能調用多個系統調用,如 glibc 提供的 printf 函數就會調用如 sys_open、sys_mmap、sys_write、sys_close 等等系統調用;
另外,多個 API 也可能只對應同一個系統調用,如glibc 下實現的 malloc、calloc、free 等函數用來分配和釋放內存,都利用了內核的 sys_brk 的系統調用。
舉例來說,我們通過 glibc 提供的chmod 函數來改變文件 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;
}在普通用戶下編譯運用,輸出結果為:
chmod failed, errno = 1上面系統調用返回的值為-1,說明系統調用失敗,錯誤碼為1,在 /usr/include/asm-generic/errno-base.h 文件中有如下錯誤代碼說明:
#define EPERM 1 /* Operation not permitted */即無權限進行該操作,我們以普通用戶權限是無法修改 /etc/passwd 文件的屬性的,結果正確。
4.2使用 syscall 直接調用
使用上面的方法有很多好處,首先你無須知道更多的細節,如 chmod 系統調用號,你只需了解 glibc 提供的 API 的原型;其次,該方法具有更好的移植性,你可以很輕松將該程序移植到其他平臺,或者將 glibc 庫換成其它庫,程序只需做少量改動。
但有點不足是,如果 glibc 沒有封裝某個內核提供的系統調用時,我就沒辦法通過上面的方法來調用該系統調用。如我自己通過編譯內核增加了一個系統調用,這時 glibc 不可能有你新增系統調用的封裝 API,此時我們可以利用 glibc 提供的syscall 函數直接調用。該函數定義在 unistd.h 頭文件中,函數原型如下:
long int syscall (long int sysno, ...)sysno 是系統調用號,每個系統調用都有唯一的系統調用號來標識。在 sys/syscall.h 中有所有可能的系統調用號的宏定義。
... 為剩余可變長的參數,為系統調用所帶的參數,根據系統調用的不同,可帶0~5個不等的參數,如果超過特定系統調用能帶的參數,多余的參數被忽略。
返回值 該函數返回值為特定系統調用的返回值,在系統調用成功之后你可以將該返回值轉化為特定的類型,如果系統調用失敗則返回 -1,錯誤代碼存放在 errno 中。
還以上面修改 /etc/passwd 文件的屬性為例,這次使用 syscall 直接調用:
#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;
}在普通用戶下編譯執行,輸出的結果與上例相同。
4.3通過 int 指令陷入
如果我們知道系統調用的整個過程的話,應該就能知道用戶態程序通過軟中斷指令int 0x80 來陷入內核態(在Intel Pentium II 又引入了sysenter指令),參數的傳遞是通過寄存器,eax 傳遞的是系統調用號,ebx、ecx、edx、esi和edi 來依次傳遞最多五個參數,當系統調用返回時,返回值存放在 eax 中。
仍然以上面的修改文件屬性為例,將調用系統調用那段寫成內聯匯編代碼:
#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 之間,就必須要解釋為出錯碼(在/usr/include/asm-generic/errno.h 文件中定義的最大出錯碼為 132),這時,將錯誤碼寫入 errno 中,置系統調用返回值為 -1;否則返回的是 eax 中的值。
上面程序在 32位Linux下以普通用戶權限編譯運行結果與前面兩個相同!
Part5系統調用的案例分析
5.1Linux下系統調用的實現
Linux下的系統調用是通過0x80實現的,但是我們知道操作系統會有多個系統調用(Linux下有319個系統調用),而對于同一個中斷號是如何處理多個不同的系統調用的?最簡單的方式是對于不同的系統調用采用不同的中斷號,但是中斷號明顯是一種稀缺資源,Linux顯然不會這么做;還有一個問題就是系統調用是需要提供參數,并且具有返回值的,這些參數又是怎么傳遞的?也就是說,對于系統調用我們要搞清楚兩點:
- 1. 系統調用的函數名稱轉換。
- 2. 系統調用的參數傳遞。
首先看第一個問題。實際上,Linux中處理系統調用的方式與中斷類似。每個系統調用都有相應的系統調用號作為唯一的標識,內核維護一張系統調用表,表中的元素是系統調用函數的起始地址,而系統調用號就是系統調用在調用表的偏移量。在進行系統調用是只要指定對應的系統調用號,就可以明確的要調用哪個系統調用,這就完成了系統調用的函數名稱的轉換。舉例來說,Linux中fork的調用號是2(具體定義,在我的計算機上是在/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傳遞系統調用號,所以具體調用fork的過程是:將2存入%eax中,然后進行系統調用,偽代碼:
[plain] view plain copy
mov eax, 2
int 0x80對于參數傳遞,Linux是通過寄存器完成的。Linux最多允許向系統調用傳遞6個參數,分別依次由%ebx,%ecx,%edx,%esi,%edi和%ebp這個6個寄存器完成。比如,調用exit(1),偽代碼是:
[plain] view plain copy
mov eax, 2
mov ebx, 1
int 0x80因為exit需要一個參數1,所以這里只需要使用ebx。這6個寄存器可能已經被使用,所以在傳參前必須把當前寄存器的狀態保存下來,待系統調用返回后再恢復,這個在后面棧切換再具體講。
Linux中,在用戶態和內核態運行的進程使用的棧是不同的,分別叫做用戶棧和內核棧,兩者各自負責相應特權級別狀態下的函數調用。當進行系統調用時,進程不僅要從用戶態切換到內核態,同時也要完成棧切換,這樣處于內核態的系統調用才能在內核棧上完成調用。系統調用返回時,還要切換回用戶棧,繼續完成用戶態下的函數調用。
寄存器%esp(棧指針,指向棧頂)所在的內存空間叫做當前棧,比如%esp在用戶空間則當前棧就是用戶棧,否則是內核棧。棧切換主要就是%esp在用戶空間和內核空間間的來回賦值。在Linux中,每個進程都有一個私有的內核棧,當從用戶棧切換到內核棧時,需完成保存%esp以及相關寄存器的值(%ebx,%ecx...)并將%esp設置成內核棧的相應值。
而從內核棧切換會用戶棧時,需要恢復用戶棧的%esp及相關寄存器的值以及保存內核棧的信息。一個問題就是用戶棧的%esp和寄存器的值保存到什么地方,以便于恢復呢?答案就是內核棧,在調用int指令機型系統調用后會把用戶棧的%esp的值及相關寄存器壓入內核棧中,系統調用通過iret指令返回,在返回之前會從內核棧彈出用戶棧的%esp和寄存器的狀態,然后進行恢復。
相信大家一定聽過說,系統調用很耗時,要盡量少用。通過上面描述系統調用的實現原理,大家也應該知道這其中的原因了。
- 第一,系統調用通過中斷實現,需要完成棧切換。
- 第二,使用寄存器傳參,這需要額外的保存和恢復的過程。
5.2文件操作相關系統調用
在 Linux 系統中,文件操作是日常使用和開發中極為常見的任務,而 open、read、write、close 等系統調用則是實現文件操作的核心工具。
open 系統調用用于打開或創建一個文件,它的函數原型為int open(const char *pathname, int flags, mode_t mode);。其中,pathname是要打開或創建的文件的路徑名;flags參數用于指定文件的打開方式,比如O_RDONLY表示以只讀方式打開,O_WRONLY表示以只寫方式打開,O_RDWR則表示以讀寫方式打開,還有一些可選的標志位,如O_CREAT表示如果文件不存在則創建新文件,O_APPEND表示以追加方式寫入文件等;mode參數在創建新文件時用于指定文件的訪問權限,如S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH表示文件所有者具有讀寫權限,同組用戶和其他用戶具有讀權限 。
read 系統調用用于從文件中讀取數據,函數原型是ssize_t read(int fd, void *buf, size_t count);,fd是文件描述符,它是 open 系統調用成功返回的一個非負整數,用于標識打開的文件;buf是用于存儲讀取數據的緩沖區指針;count表示期望讀取的字節數,該函數返回實際讀取到的字節數 。
write 系統調用則用于向文件中寫入數據,其函數原型為ssize_t write(int fd, const void *buf, size_t count);,參數含義與 read 類似,fd為文件描述符,buf是要寫入數據的緩沖區指針,count是要寫入的字節數,返回值是實際寫入的字節數 。
close 系統調用用于關閉一個打開的文件,函數原型為int close(int fd);,fd為要關閉的文件描述符,關閉成功返回 0,失敗返回 - 1 。
以下是一個簡單的 C 語言代碼示例,展示了如何使用這些系統調用實現文件的讀取和寫入操作:
#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;
}
// 創建目標文件,以讀寫方式,如果文件不存在則創建,權限設置為所有者可讀可寫,其他用戶可讀
destination_fd = open("destination.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (destination_fd == -1) {
perror("無法創建目標文件");
close(source_fd);
return 1;
}
// 從源文件讀取數據并寫入目標文件
while ((bytes_read = read(source_fd, buffer, BUFFER_SIZE)) > 0) {
bytes_written = write(destination_fd, buffer, bytes_read);
if (bytes_written != bytes_read) {
perror("寫入目標文件失敗");
close(source_fd);
close(destination_fd);
return 1;
}
}
if (bytes_read == -1) {
perror("讀取源文件失敗");
}
// 關閉文件
close(source_fd);
close(destination_fd);
return 0;
}在這個示例中,首先使用 open 系統調用以只讀方式打開名為source.txt的源文件,如果打開失敗,通過perror函數輸出錯誤信息并返回 1。接著,使用 open 系統調用以讀寫方式創建名為destination.txt的目標文件,如果文件已存在則截斷文件內容,如果創建失敗同樣輸出錯誤信息并關閉已打開的源文件后返回 1 。
然后,通過一個循環,使用 read 系統調用從源文件中讀取數據到緩沖區buffer中,每次最多讀取BUFFER_SIZE個字節。只要讀取到的數據長度大于 0,就使用 write 系統調用將緩沖區中的數據寫入目標文件。如果寫入的字節數與讀取的字節數不一致,說明寫入失敗,輸出錯誤信息并關閉兩個文件后返回 1 。
如果在讀取過程中bytes_read等于 - 1,說明讀取失敗,輸出錯誤信息。最后,使用 close 系統調用分別關閉源文件和目標文件,完成文件操作 。
通過這個示例,我們可以清晰地看到 open、read、write、close 系統調用在文件讀寫操作中的具體應用和執行流程,它們相互配合,實現了高效、準確的文件數據傳輸和管理 。
5.3進程管理相關系統調用
在 Linux 系統中,進程管理是操作系統的核心功能之一,fork、exec、wait 等系統調用在進程的創建、執行和等待過程中發揮著關鍵作用。
fork 系統調用用于創建一個新的進程,稱為子進程,它的函數原型為pid_t fork(void);。調用 fork 后,系統會創建一個與原進程(父進程)幾乎完全相同的子進程,子進程復制了父進程的代碼段、數據段、堆棧段等資源。但父子進程也有一些不同之處,它們擁有不同的進程 ID(PID),通過getpid()函數可以獲取當前進程的 PID,通過getppid()函數可以獲取父進程的 PID 。fork 函數的返回值非常特殊,在父進程中,返回值是新創建子進程的 PID;在子進程中,返回值為 0;如果創建子進程失敗,返回值為 - 1 。
exec 系統調用并不是一個單獨的函數,而是一組函數,如execl、execv、execle、execve等,它們的主要作用是在當前進程中啟動另一個程序。當進程調用 exec 函數時,會用新的程序替換當前進程的正文、數據、堆和棧段,也就是說,當前進程會被新的程序完全取代,從新程序的main函數開始執行。由于 exec 并不創建新進程,所以前后的進程 ID 并未改變 。
wait 系統調用用于等待子進程的結束,并獲取子進程的退出狀態,函數原型為pid_t wait(int *status);。status是一個指向整數的指針,用于存儲子進程的退出狀態信息。調用 wait 后,父進程會阻塞,直到有一個子進程結束,此時 wait 返回結束子進程的 PID,并將子進程的退出狀態存儲在status指向的變量中 。
下面通過一個簡單的代碼示例來說明進程創建和父子進程的執行流程:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
pid_t pid;
int status;
// 創建子進程
pid = fork();
if (pid == -1) {
perror("fork失敗");
exit(1);
} else if (pid == 0) {
// 子進程執行的代碼
printf("我是子進程,我的PID是 %d,父進程的PID是 %d\n", getpid(), getppid());
// 子進程執行另一個程序,這里以執行ls命令為例
execl("/bin/ls", "ls", "-l", NULL);
perror("execl失敗");
exit(1);
} else {
// 父進程執行的代碼
printf("我是父進程,我的PID是 %d,子進程的PID是 %d\n", getpid(), pid);
// 父進程等待子進程結束
wait(&status);
printf("子進程已結束,退出狀態為 %d\n", WEXITSTATUS(status));
}
return 0;
}在這個示例中,首先調用 fork 系統調用創建子進程。如果 fork 返回 - 1,說明創建子進程失敗,通過perror函數輸出錯誤信息并調用exit函數退出程序 。
如果 fork 返回 0,說明當前是子進程,子進程打印自己的 PID 和父進程的 PID,然后調用execl函數執行/bin/ls -l命令,列出當前目錄下的文件詳細信息。如果execl執行失敗,同樣輸出錯誤信息并退出 。
如果 fork 返回一個大于 0 的值,說明當前是父進程,父進程打印自己的 PID 和子進程的 PID,然后調用 wait 系統調用等待子進程結束。當子進程結束后,wait 返回,父進程獲取子進程的退出狀態,并打印子進程已結束以及其退出狀態 。
通過這個示例,我們可以清楚地看到 fork、exec、wait 系統調用在進程管理中的協同工作,fork 用于創建新進程,exec 用于在子進程中執行新程序,wait 用于父進程等待子進程結束并獲取其退出狀態,它們共同構成了 Linux 系統強大的進程管理機制 。


























