歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> linux內核系統調用和標准C庫函數的關系分析

linux內核系統調用和標准C庫函數的關系分析

日期:2017/3/3 12:11:54   编辑:Linux內核
今天研究了一下系統調用和標准庫函數的區別和聯系,從網上搜集的資料如下:
資料引用分割線(紅字為自己批注的重點和總結)
《=================================================================================================================================》
1.系統調用是為了方便應用使用操作系統的接口,而庫函數是為了方便人們編寫應用程序而引出的,比如你自己編寫一個函數其實也可以說就是一個庫函數。
2.系統調用可以理解為內核提供給我們在用戶態用的接口函數,可以認為是某種內核的庫函數。
3.read就是系統調用,而fread就是C標准庫函數.
4.很多c函數庫中的函數名與系統調用的名稱一樣是因為該函數本身其實就是調用的系統調用,放到c函數庫就是為了用戶態的使用
5.寫程序直接使用的是庫函數,而庫函數內部可能就是調用的同名系統調用
6.首先,現在的OS內核主要采用兩種模式,整體的單內核模式(linux)和分層的微內核模式(Windows)。單內核
模式的特點就是代碼緊湊,執行速度快,各個模塊之間是直接的調用關系,可以說最後一點既是優點,也是缺
點...有點就是執行速度快,缺點是內核看起來很亂,維護起來困難。
無論是單內核,還是微內核,立體的體系結構從下到上大概都是分成這樣幾層:物理硬件,OS內核,OS服務,
應用程序。這四層結構中,OS內核起到一個“承上啟下”的作用,向下管理物理硬件;向上為OS服務和應用程序
提供接口。主意,這裡的接口實際上是指系統調用(System Call)。
通常OS內核為了考慮實現起來的難度和易於管理,只提供少部分必要的系統調用,這些系統調用通常都是C和
匯編混編來實現的。接口用C定義,實現體用匯編來寫。這樣做的好處是,執行效率高,並且極大的方便了上層的
調用。
再說庫函數(即API)。庫函數可以概括的分為兩類,一類是隨OS提供的,另一類是第三方的。隨系統提供的庫
函數進一步封裝或組合系統調用,實現更多的功能,就像用C語言的許多功能單一的小函數來實現很多很多個功能
復雜的大函數一樣。這樣的API能夠執行一些相對內核來說很復雜的操作,比如,read()函數根據參數,直接就
能讀文件,而背後隱藏的比如文件在硬盤的哪個磁道,哪個扇區,加載到內存的哪個位置等等這些操作,程序員
是不必關心的,這些操作裡面自然也包含了系統調用。而對於第三方的庫,它其實和系統庫一樣,只是它直接利
用系統調用的可能性要小一些,而是利用系統提供的API接口來實現功能。(API的接口是開放的)
7.高級語言庫函數的確是調用系統調用來實現的,所以說系統調用才是真正對硬件操作的。
但是大家可能注意到了為什麼庫函數為什麼是通用的(同名),這是正是高級語言不依賴與特定的硬件。
其實,都是編譯器來負責庫函數到系統調用之間的轉換的。比如說VC可能通過把fopen()對應到windows的打印系統調用XXX(不知道具體是哪個)上去了,而linux的編譯器gcc通過把fopen()對應到linux的系統調用open上去了。
8.系統調用沒有庫函數的效率高是因為與設置的緩沖池大小有關吧,緩沖池(不知是用戶的還是內核的)小的話,系統調用的操作就頻繁,緩沖池(不知是用戶的還是內核)大的的話,庫函數就調用系統調用的次數就少。
9.應用程序可以直接調用庫函數來操作。那麼linux的源代碼C裡是不應該使用庫函數的吧,你想。編譯連接成內核鏡像後,安裝在裸機上,連個運行
環境都沒怎麼系統調用啊。有的人會說,系統調用在編譯連接的時候已經把系統調用弄進去了,那麼不是說現在流行的動態連接嗎,直到運行需要時才去連接嗎。
論壇的人基本就是以上的觀點.
有些混亂,這裡參考了下<>的第五章,關於系統調用的一些內容,如下:
第5章 系統調用
大部分介紹Linux內核的書籍都沒有仔細說明系統調用,這應該算是一個失誤。內核發展到現在,我們實際需要的系統調用現在已經十分完美,從這個意義上來說,再耗費寶貴的時間去研究系統調用的實現是毫無意義的事情。
然而,對於希望能夠對內核有更深理解的我們來說,仔細研究少量系統調用仍是十分值得的。這樣就有機會初步了解一些概念,並可以趁機詳細了解一下內核編程的特點,就像系統調用本身在應用程序和內核間的橋梁作用一樣,學習並理解它也是我們走向內核的一個很好的過渡。
5.1 系統調用概述
一個穩定運行的Linux操作系統需要內核和用戶應用程序之間的完美配合,內核提供各種各樣的服務,然後用戶應用程序通過某種途徑使用這些服務,進而契合用戶的不同需求。
用戶應用程序訪問並使用內核所提供的各種服務的途徑即是系統調用。在內核和用戶應用程序相交界的地方,內核提供了一組系統調用接口,通過這組接口,應用程序可以訪問系統硬件和各種操作系統資源。比如用戶可以通過文件系統相關的系統調用,請求系統打開文件、關閉文件或讀寫文件;可以通過時鐘相關的系統調用,獲得系統時間或設置定時器等。
內核提供的這組系統調用通常也被稱之為系統調用接口層。系統調用接口層作為內核和用戶應用程序之間的中間層,扮演了一個橋梁,或者說中間人的角色。系統調用把應用程序的請求傳達給內核,待內核處理完請求後再將處理結果返回給應用程序。
5.1.1 系統調用、POSIX、C庫、系統命令和內核函數
(1)系統調用和POSIX。
系統調用雖然是內核和用戶應用程序之間的溝通橋梁,是用戶應用程序訪問內核的入口點,但通常情況下,應用程序是通過操作系統提供的應用編程接口(API)而不是直接通過系統調用來編程。
操作系統API的主要作用是把操作系統的功能完全展示出來,提供給應用程序,基於該操作系統,與文件、內存、時鐘、網絡、圖形、各種外設等互操作的能力。此外,操作系統API通常還提供許多工具類的功能,比如操縱字符串、各種數據類型、時間日期等。
在UNIX世界裡,最通用的操作系統API基於POSIX(Portable Operating System Interface of
UNIX,可移植操作系統接口)標准。POSIX的誕生和UNIX的發展密不可分,UNIX於20世紀70年代誕生於Bell
lab,並於20世紀80年代向美各大高校分發V7版的源碼以做研究。UC Berkeley在V7的基礎上開發了BSD UNIX。
後來很多商業廠家意識到UNIX的價值也紛紛以Bell Lab的System V或BSD為基礎來開發自己的UNIX,較著名的有Sun
OS、AIX、VMS等。雖然這帶來了UNIX的繁榮,但由於各廠家對UNIX的開發各自為政,UNIX的版本相當混亂,給軟件的可移植性帶來很大困難,
對UNIX的發展極為不利。
為結束這種局面,IEEE制訂了POSIX標准,目標是提供一套大體上基於UNIX的可移植操作系統標准,提高UNIX環境下應用程序的可移植性。
然而,POSIX並不局限於UNIX。許多其他的操作系統,例如DEC OpenVMS和Microsoft Windows
NT,都支持POSIX標准
POSIX標准定義了"POSIX兼容"的操作系統所必須提供的服務。Linux兼容於POSIX標准,提供了根據POSIX而定義的API函數。
這些API函數和系統調用之間有著直接的關系,一個API函數可以由一個系統調用實現,也可以通過調用多個系統調用來實現,還可以完全不使用任何系統調
用。
(2)系統調用和C庫。
操作系統API通常都以C庫的方式提供,Linux也是如此。C庫提供了POSIX的絕大部分API,同時,內核提供的每個系統調用在C庫中都具有相應的封裝函數。系統調用與其C庫封裝函數的名稱常常相同,比如,read系統調用在C庫中的封裝函數即為read函數。
C庫中的系統調用封裝函數在最終調用到相應系統調用之前,往往不做多少額外的工作。不過,某些情況下會有些例外,比如對於兩個相關的系統調用truncate和truncate64,C庫中的封裝函數truncate函數即需要決定它們中的哪個應該最終被調用。
當然,如圖5.1所示,系統調用和C庫函數之間並不是一一對應的關系。可能幾個不同的函數會調用到同一個系統調用,比如malloc函數和free函數都是通過brk系統調用來擴大或縮小進程的堆棧,execl、execlp、execle、execv、execvp和execve函數都是通過execve系統調用來執行一個可執行文件。
也有可能一個函數調用多個系統調用。更有些函數並不依賴於任何系統調用,比如strcpy函數(復制字符串)和atoi函數(轉換ASCII為整數),因為它們並不需要向內核請求任何服務

圖5.1 C庫函數與系統調用
實際上,從用戶的角度看,系統調用和C庫之間的區別並不重要,他們只需通過C庫函數完成所需功能。相反,從內核的角度看,需要考慮的則是提供哪些針對確定目的的系統調用,並不需要關注它們如何被使用。
(3)系統調用與系統命令。
系統命令位於C庫的更上層,是利用C庫實現的可執行程序,比如最為常用的ls、cd等命令。
strace工具可以跟蹤命令的執行,使用希望跟蹤的命令為參數,並顯示出該命令執行過程中所使用到的所有系統調用。比如,如果希望了解在執行pwd命令時都調用了哪些系統調用,可以使用下面的命令:
$strace pwd
結果會產生大量的信息,顯示出pwd命令執行過程中所調用到的各個系統調用:
…… write(1, "/usr/src/linux-2.6.23/n", 22/usr/src/linux-2.6.23) = 22 close(1)                                = 0 munmap(0xb7f5a000, 4096)                = 0 exit_group(0)
(4)系統調用和內核函數。
內核函數與C庫函數的區別僅僅是內核函數在內核實現,因此必須遵守內核編程的規則。
系統調用最終必須具有明確的操作。用戶應用程序通過系統調用進入內核後,會執行各個系統調用對應的內核函數,即系統調用服務例程,比如系統調用getpid的服務例程是內核函數sys_getpid。
系統調用服務例程之外,內核中存在著大量的內核函數。有些局限於某個內核文件自己使用,有些則是export出來供內核其他部分共同使用。對於export出來的內核函數,可以使用ksyms命令或通過/proc/ksyms文件查看。
說白了,系統調用是內核提供的接口,而不管是系統命令還是標准庫函數,都是基於系統調用編寫的應用程序,只不過是一些大牛寫的啦,程序健壯,夠標准,哈哈,我們自己也可以寫用戶程序做為自己的庫函數,自己在內核代碼中添加系統調用,這些都是可以做到的
5.1.2 系統調用表
系統調用表sys_call_table存儲了所有系統調用對應的服務例程的函數地址,在arch/i386/kernel/syscall_table.S文件中被定義:
001 ENTRY(sys_call_table) 002 .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ 003 .long sys_exit 004 .long sys_fork 005 .long sys_read 006 .long sys_write 007 .long sys_open /* 5 */  320 .long sys_getcpu 321 .long sys_epoll_pwait 322 .long sys_utimensat /* 320 */ 323 .long sys_signalfd 324 .long sys_timerfd 325 .long sys_eventfd 326 .long sys_fallocate
從中可發現兩個特別之處。首先,所有系統調用服務例程的命名均遵守一定的規則,即在系統調用名稱之前增加"sys_"前綴,比如open系統調用對應sys_open函數。
其次,內核提供的系統調用數目非常有限,到2.6.23版本的內核也不過才達到僅僅325個,使用"man 2 syscalls"命令即可以浏覽到所有系統調用的添加歷史。這也是系統調用與C庫函數的區別之一:系統調用通常只提供最小的接口,C庫函數則在此基礎之上提供更多復雜的功能。
5.1.3 系統調用號
既然系統調用表集中存放了所有系統調用服務例程的地址,那麼系統調用在內核中的執行就可以轉化為從該表獲取對應的服務例程並執行的過程。
這個過程中一個很重要的環節就是系統調用號。每個系統調用都擁有一個獨一無二的系統調用號,用戶應用通過它,而不是系統調用的名稱,來指明要執行哪個系統調用。
系統調用號的定義在include/asm-i386/unistd.h文件。
#define 
__NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define 
__NR_read 3 #define __NR_write 4 #define __NR_open 5 ...... #define 
__NR_getcpu 318 #define __NR_epoll_pwait 319 #define __NR_utimensat 320 
#define __NR_signalfd 321 #define __NR_timerfd 322 #define __NR_eventfd 
323 #define __NR_fallocate 324
將其與sys_call_table的定義相比較可以發現,每個系統調用號都依次對應了sys_call_table中的某一項。內核正是將系統調用號作為下標去獲取sys_call_table中的服務例程函數地址。
系統調用號與系統調用為相依相生的關系,一旦分配就不能再有任何變更,即使該系統調用被刪除,它所擁有的系統調用號也不能被回收利用。
5.1.4 系統調用服務例程
系統調用最終由系統調用服務例程完成明確的操作。所有的系統調用服務例程集中聲明在include/linux/syscalls.h文件,但分散
定義在很多不同的文件。比如getpid系統調用用於獲取當前進程的PID,它的服務例程sys_getpid在kernel/timer.c文件中定義
為:
asmlinkage long sys_getpid(void) { return current->tgid; }
除了都具有"sys_"前綴之外,所有的系統調用服務例程命名與定義還必須遵守其他的一些規則。首先,函數定義中必須添加asmlinkage標記,通知編譯器僅從堆棧中獲取該函數的參數。
其次,必須返回一個long類型的返回值表示成功或錯誤,通常返回0表示成功,返回負值表示錯誤。當然,getpid系統調用非常簡單,不可能會失敗,通過命令"man 2 getpid"可以查看它的手冊,裡面也明確指出了這一點。
每個系統調用的系統調用號、命名以及操作目的都是固定的,但內核如何去實現並沒有明確規定,不同版本、不同架構的內核實現都有可能會有所變化。
5.1.5 如何使用系統調用
如圖5.2所示,用戶應用可以通過兩種方式使用系統調用。第一種方式是通過C庫函數,包括系統調用在C庫中的封裝函數和其他普通函數。

圖5.2 使用系統調用的兩種方式
第二種方式是使用_syscall宏。2.6.18版本之前的內核,在include/asm-i386/unistd.h文件中定義有7個_syscall宏,分別是:
_syscall0(type,name);
 _syscall1(type,name,type1,arg1); 
_syscall2(type,name,type1,arg1,type2,arg2); 
_syscall3(type,name,type1,arg1,type2,arg2,type3,arg3); 
_syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4); 
syscall5type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5);
 
syscall6type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5,type6,arg6);
其中,type表示所生成系統調用的返回值類型,name表示該系統調用的名稱,typeN、argN分別表示第N個參數的類型和名稱,它們的數目
和_syscall後面的數字一樣大。這些宏的作用是創建名為name的函數,_syscall後面跟的數字指明了該函數的參數的個數。
比如sysinfo系統調用用於獲取系統總體統計信息,使用_syscall宏定義為:
_syscall1(int, sysinfo, struct sysinfo *, info);
展開後的形式為:
int sysinfo(struct sysinfo * info) { long __res; __asm__ volatile("int $0x80" : "=a" (__res) : ""(116),"b" ((long)(info))); do { if((unsigned long)(__res) >= (unsigned long)(-(128 + 1))) { errno = -(__res); __res = -1; } return (int) (__res); } while (0); }
可以看出,_syscall1(int, sysinfo, struct sysinfo *, info)展開成一個名為sysinfo的函數,原參數int就是函數的返回類型,原參數struct sysinfo *和info分別構成新函數的參數。
在程序文件裡使用_syscall宏定義需要的系統調用,就可以在接下來的代碼中通過系統調用名稱直接調用該系統調用。下面是一個使用sysinfo系統調用的實例。
代碼清單5.1 sysinfo系統調用使用實例
#include #include #include #include #include /* for struct sysinfo */ _syscall1(int, sysinfo,struct sysinfo *, info); int main(void) { struct sysinfo s_info; int error; error = sysinfo(&s_info); printf("code error = %d/n", error); printf("Uptime = %lds/nLoad: 1 min %lu / 5 min %lu / 15 min %lu/n" "RAM: total %lu / free %lu / shared %lu/n" "Memory in buffers = %lu/nSwap: total %lu / free %lu/n" "Number of processes = %d/n",
 s_info.uptime, s_info.loads[0], s_info.loads[1], s_info.loads[2], 
s_info.totalram, s_info.freeram, s_info.sharedram, s_info.bufferram, 
s_info.totalswap, s_info.freeswap, s_info.procs); exit(EXIT_SUCCESS); }
但是自2.6.19版本開始,_syscall宏被廢除,我們需要使用syscall函數,通過指定系統調用號和一組參數來調用系統調用。
syscall函數原型為:
int syscall(int number, ...);
其中number是系統調用號,number後面應順序接上該系統調用的所有參數。下面是gettid系統調用的調用實例。
代碼清單5.2 gettid系統調用使用實例
#include #include #include #define __NR_gettid 224 int main(int argc, char *argv[]) { pid_t tid;tid = syscall(__NR_gettid); }
大部分系統調用都包括了一個SYS_符號常量來指定自己到系統調用號的映射,因此上面紫色的部分可重寫為:
tid = syscall(SYS_gettid);
5.1.6 為什麼需要系統調用
為什麼需要系統調用?主要有以下兩方面原因。
(1)系統調用可以為用戶空間提供訪問硬件資源的統一接口,以至於應用程序不必去關注具體的硬件訪問操作。比如,讀寫文件時,應用程序不用去管磁盤類型,甚至於不用關心是哪種文件系統。
(2)系統調用可以對系統進行保護,保證系統的穩定和安全。系統調用的存在規定了用戶進程進入內核的具體方式,換句話說,用戶訪問內核的路徑是事先規定好的,只能從規定位置進入內核,而不准許肆意跳入內核。有了這樣的進入內核的統一訪問路徑限制才能保證內核的安全。
我們可以形象地描述這種機制:作為一個游客,你可以買票要求進入野生動物園,但你必須老老實實地坐在觀光車上,按照規定的路線觀光游覽。當然,不准下車,因為那樣太危險,不是讓你丟掉小命,就是讓你嚇壞了野生動物。
5.2 系統調用執行過程
系統調用的執行過程主要包括如圖5.3與圖5.4所示的兩個階段:用戶空間到內核空間的轉換階段,以及系統調用處理程序system_call函數到系統調用服務例程的階段。

圖5.3 用戶空間到內核空間

圖5.4 system_call函數到系統調用服務例程
(1)用戶空間到內核空間。
如圖5.3所示,系統調用的執行需要一個用戶空間到內核空間的狀態轉換,不同的平台具有不同的指令可以完成這種轉換,這種指令也被稱作操作系統陷入(operating system trap)指令。
Linux通過軟中斷來實現這種陷入,具體對於X86架構來說,是軟中斷0x80,也即int $0x80匯編指令。軟中斷和我們常說的中斷(硬件中斷)不同之處在於-它由軟件指令觸發而並非由硬件外設引發。
int 0x80指令被封裝在C庫中,對於用戶應用來說,基於可移植性的考慮,不應該直接調用int $0x80指令。陷入指令的平台依賴性,也正是系統調用需要在C庫進行封裝的原因之一。
通過軟中斷0x80,系統會跳轉到一個預設的內核空間地址,它指向了系統調用處理程序(不要和系統調用服務例程相混淆),即在arch/i386/kernel/entry.S文件中使用匯編語言編寫的system_call函數。
(2)system_call函數到系統調用服務例程。
很顯然,所有的系統調用都會統一跳轉到這個地址進而執行system_call函數,但正如前面所述,到2.6.23版為止,內核提供的系統調用已經達到了325個,那麼system_call函數又該如何派發它們到各自的服務例程呢?
軟中斷指令int
0x80執行時,系統調用號會被放入eax寄存器,同時,sys_call_table每一項占用4個字節。這樣,如圖5.5所
示,system_call函數可以讀取eax寄存器獲得當前系統調用的系統調用號,將其乘以4生成偏移地址,然後以sys_call_table為基
址,基址加上偏移地址所指向的內容即是應該執行的系統調用服務例程的地址。
另外,除了傳遞系統調用號到eax寄存器,如果需要,還會傳遞一些參數到內核,比如write系統調用的服務例程原型為:
sys_write(unsigned int fd, const char * buf, size_t count);
調用write系統調用時就需要傳遞文件描述符fd、要寫入的內容buf以及寫入字節數count等幾個內容到內核。ebx、ecx、edx、esi以及edi寄存器可以用於傳遞這些額外的參數。
正如之前所述,系統調用服務例程定義中的asmlinkage標記表示,編譯器僅從堆棧中獲取該函數的參數,而不需要從寄存器中獲得任何參數。進入
system_call函數前,用戶應用將參數存放到對應寄存器中,system_call函數執行時會首先將這些寄存器壓入堆棧。
對於系統調用服務例程,可以直接從system_call函數壓入的堆棧中獲得參數,對參數的修改也可以一直在堆棧中進行。在system_call函數退出後,用戶應用可以直接從寄存器中獲得被修改過的參數。
並不是所有的系統調用服務例程都有實際的內容,有一個服務例程sys_ni_syscall除了返回-ENOSYS外不做任何其他工作,在kernel/sys_ni.c文件中定義。
asmlinkage long sys_ni_syscall(void) { return -ENOSYS; }
sys_ni_syscall的確是最簡單的系統調用服務例程,表面上看,它可能並沒有什麼用處,但是,它在sys_call_table中占據了很多位置。多數位置上的sys_ni_syscal都代表了那些已經被內核中淘汰的系統調用,比如:
.long sys_ni_syscall /* old stty syscall holder */ .long sys_ni_syscall /* old gtty syscall holder */
就分別代替了已經廢棄的stty和gtty系統調用。如果一個系統調用被淘汰,它所對應的服務例程就要被指定為sys_ni_syscall。
我們並不能將它們的位置分配給其他的系統調用,因為一些老的代碼可能還會使用到它們。否則,如果某個用戶應用試圖調用這些已經被淘汰的系統調用,所得到的結果,比如打開了一個文件,就會與預期完全不同,這將令人感到非常奇怪。
其實,sys_ni_syscall中的"ni"即表示"not implemented(沒有實現)"。
系統調用通過軟中斷0x80陷入內核,跳轉到系統調用處理程序system_call函數,並執行相應的服務例程,但由於是代表用戶進程,所以這個執行過程並不屬於中斷上下文,而是處於進程上下文。
因此,系統調用執行過程中,可以訪問用戶進程的許多信息,可以被其他進程搶占(因為新的進程可能使用相同的系統調用,所以必須保證系統調用可重入),可以休眠(比如在系統調用阻塞時或顯式調用schedule函數時)。
這些特點涉及進程調度的問題,在此不做深究,讀者只需要理解當系統調用完成後,把控制權交回到發起調用的用戶進程前,內核會有一次調度。如果發現有優先級更高的進程或當前進程的時間片用完,那麼就會選擇高優先級的進程或重新選擇進程運行。
5.3 系統調用示例
本節通過對幾個系統調用的剖析來講解它們的工作方式。
5.3.1 sys_dup
dup系統調用的服務例程為sys_dup函數,在fs/fcntl.c文件中定義如下。
代碼清單5.3 dup系統調用的服務例程
asmlinkage long sys_dup(unsigned int fildes) 193 { int ret = -EBADF; struct file * file = fget(fildes); if (file) ret = dupfd(file, 0); return ret; }
除了sys_ni_call()以外,sys_dup()稱得上是最簡單的服務例程之一,但是它卻是Linux輸入/輸出重定向的基礎。
在Linux中,執行一個shell命令時通常會自動打開3個標准文件:標准輸入文件(stdin),通常對應終端的鍵盤;標准輸出文件
(stdout)和
標准錯誤輸出文件(stderr),通常對應終端的屏幕。shell命令從標准輸入文件中得到輸入數據,將輸出數據輸出到標准輸出文件,而將錯誤信息輸出
到標准錯誤文件中。
比如下面的命令:
$cat /proc/cpuinfo
將把cpuinfo文件的內容顯示到屏幕上,但是如果cat命令不帶參數,則會從stdin中讀取數據,並將其輸出到stdout,比如:
$cat Hello! Hello!
用戶輸入的每一行都將立刻被輸出到屏幕上。
輸入重定向是指把命令的標准輸入重定向到指定的文件中,即輸入可以不來自鍵盤,而來自一個指定的文件。所以說,輸入重定向主要用於改變一個命令的輸入源。
輸出重定向是指把命令的標准輸出或標准錯誤輸出重新定向到指定文件中。這樣,該命令的輸出就不顯示在屏幕上,而是寫入到指定文件中。我們經常會利用輸出重定向將程序或命令的log保存到指定的文件中。
那麼sys_dup()又是如何完成輸入/輸出的重定向呢?下面通過一個例子進行說明。
當我們在shell終端下輸入"echo
hello"命令時,將會要求shell進程執行一個可執行文件echo,參數為"hello"。當shell進程接收到命令之後,先在/bin目錄下找
到echo文件(我們可以使用which命令獲得命令所在的位置),然後創建一個子進程去執行/bin/echo,並將參數傳遞給它,而這個子進程從
shell進程繼承了3個標准輸入/輸出文件,即stdin、stdout和stderr,文件號分別為0、1、2。它的工作很簡單,就是將參
數"hello"寫到stdout文件中,通常都是我們的屏幕上。
但是如果我們將命令改成"echo hello > txt",則在執行時輸出將會被重定向到磁盤文件txt中。假定之前該shell進程只有上述3個標准文件打開,則該命令將按如下序列執行。
(1)打開或創建文件txt,如果txt中原來有內容,則清除原來的內容,其文件號為3。
(2)通過dup系統調用復制文件stdout的相關數據結構到文件號4。
(3)關閉stdout,但是由於4號文件也同時引用stdout,所以stdout文件並未真正關閉,只是騰出1號文件號位置。
(4)通過dup系統調用,復制3號文件(即文件txt),由於1號文件關閉,其位置空缺,故3號文件被復制到1號,即進程中原來指向stdout的指針指向了txt。
(5)通過系統調用fork和exec創建子進程並執行echo,子進程在執行cat前關閉3號和4號文件,只留下0、1、2三個文件,請注意,這
時的1號文件已經不是stdout而是文件txt了。當cat想向stdout文件寫入"hello"時自然就寫入到了txt中。
(6)回到shell進程後,關閉指向txt的1號與3號文件文件,再用dup和close系統調用將2號恢復至stdout,這樣shell就恢復了0、1、2三個標准輸入/輸出文件。
5.3.2 sys_reboot
Linux下有關關機與重啟的命令主要有shutdown、reboot、halt、poweroff、telinit和init。它們都可以達到關機或重啟的目的,但是每個命令的工作流程並不一樣。
這些命令並不都是互相獨立的,比如,poweroff、reboot即是halt的符號鏈接,但是它們最終都是通過reboot系統調用來完成關機或重啟操作。
reboot系統調用的服務例程為sys_reboot函數,在kernel/sys.c文件中定義如下。
代碼清單5.4 reboot系統調用的服務例程
asmlinkage long sys_reboot(int magic1, int magic2, unsigned int cmd, void __user * arg) { charbuffer[256]; /* We only trust the superuser with rebooting the system. */ if(!capable(CAP_SYS_BOOT)) return -EPERM; /* For safety, we require "magic" arguments. */ if (magic1
 != LINUX_REBOOT_MAGIC1 ||(magic2 != LINUX_REBOOT_MAGIC2 && 
magic2 != LINUX_REBOOT_MAGIC2A && magic2 != LINUX_REBOOT_MAGIC2B
 && magic2 != LINUX_REBOOT_MAGIC2C)) return -EINVAL; /* Instead of trying to make the power_off code look like * halt when pm_power_off is not set do it the easy way.*/ if((cmd == LINUX_REBOOT_CMD_POWER_OFF) && !pm_power_off)	 cmd = LINUX_REBOOT_CMD_HALT; lock_kernel();switch (cmd) { case LINUX_REBOOT_CMD_RESTART: kernel_restart(NULL); break; caseLINUX_REBOOT_CMD_CAD_ON: C_A_D = 1; break; case LINUX_REBOOT_CMD_CAD_OFF: C_A_D = 0; break; caseLINUX_REBOOT_CMD_HALT: kernel_halt(); unlock_kernel(); do_exit(0); break; caseLINUX_REBOOT_CMD_POWER_OFF: kernel_power_off(); unlock_kernel(); do_exit(0); break; caseLINUX_REBOOT_CMD_RESTART2: if (strncpy_from_user(&buffer[0], arg,sizeof(buffer) - 1) < 0) { unlock_kernel(); return -EFAULT; } buffer[sizeof(buffer) - 1] = '/0'; kernel_restart(buffer);break; case LINUX_REBOOT_CMD_KEXEC: kernel_kexec(); unlock_kernel(); return -EINVAL; #ifdef CONFIG_HIBERNATION case LINUX_REBOOT_CMD_SW_SUSPEND: { int ret = hibernate(); unlock_kernel();return ret; } #endif default: unlock_kernel(); return -EINVAL; } unlock_kernel(); return 0; }
顧名思義,reboot系統調用可以用於重新啟動系統,但根據所提供的參數不同,它還能夠完成關機、掛起系統、允許或禁止使用
Ctrl+Alt+Del組合鍵重啟等不同的操作。我們還要特別注意內核裡對sys_reboot()的注釋,在使用它之前首先要使用sync命令同步磁
盤,否則磁盤上的文件系統可能會有所損壞。
第901行檢查調用者是否有合法權限。capable函數用於檢查是否有操作指定資源的權限,如果它返回非零值,則調用者有權進行操作,否則無權操作。比如,這一行的capable(CAP_SYS_BOOT)即檢查調用者是否有權限使用reboot系統調用。
第905行~第910行通過對兩個參數magic1和magic2的檢測,判斷reboot系統調用是不是被偶然調用到的。如果reboot系統調用是被偶然調用的,那麼參數magic1和magic2幾乎不可能同時滿足預定義的這幾個數字的集合。
從第919行開始,sys_reboot()對調用者的各種使用情況進行區分。為LINUX_REBOOT_CMD_RESTART
時,kernel_restart()將打印出"Restarting
system."消息,然後調用machine_restart函數重新啟動系統。
為LINUX_REBOOT_CMD_CAD_ON或LINUX_REBOOT_CMD_CAD_OFF時,分別允許或禁止
Ctrl+Alt+Del組合鍵。我們還可以在/etc/inittab文件指定是否可以使用Ctrl+Alt+Del組合鍵來關閉並重啟系統。如果希望
完全禁止這個功能,需要將/etc/inittab文件中的下面一行注釋掉。
ca:12345:ctrlaltdel:/sbin/shutdown -t1 -a -r now
為LINUX_REBOOT_CMD_HALT時,打印出"System halted."消息,和LINUX_REBOOT_CMD_RESTART情況下類似,但只是暫停系統而不是將其重新啟動。
為LINUX_REBOOT_CMD_POWER_OFF時,打印出"Power down."消息,然後關閉機器電源。
為LINUX_REBOOT_CMD_RESTART2時,接收命令字符串,該字符串說明了系統應該如何關閉。
最後,LINUX_REBOOT_CMD_SW_SUSPEND用於使系統休眠。
5.4 系統調用的實現
一個系統調用的實現並不需要去關心如何從用戶空間轉換到內核空間,以及系統調用處理程序如何去執行,你需要做的只是遵循幾個固定的步驟。
5.4.1 如何實現一個新的系統調用
為Linux添加新的系統調用是件相對容易的事情,主要包括有4個步驟:編寫系統調用服務例程;添加系統調用號;修改系統調用表;重新編譯內核並測試新添加的系統調用。
下面以一個並無實際用處的hello系統調用為例,來演示上述幾個步驟。
(1)編寫系統調用服務例程。
遵循前面所述的幾個原則,hello系統調用的服務例程實現為:
asmlinkage long sys_hello(void) { printk("Hello!/n"); return 0; }
通常,應該為新的系統調用服務例程創建一個新的文件進行存放,但也可以將其定義在其他文件之中並加上注釋做必要說明。同時,還要在include/linux/syscalls.h文件中添加原型聲明:
asmlinkage long sys_hello(void);
sys_hello函數非常簡單,僅僅打印一條語句,並沒有使用任何參數。如果我們希望hello系統調用不僅能打印"hello!"歡迎信息,還能夠打印出我們傳遞過去的名稱,或者其他的一些描述信息,則sys_hello函數可以實現為:
asmlinkage long sys_hello(const char __user *_name) 02 { char *name; long ret; name = strndup_user(_name, PAGE_SIZE); 07 if (IS_ERR(name)) { ret = PTR_ERR(name); goto error; } printk("Hello, %s!/n", name); return 0; error: return ret; }
第二個sys_hello函數使用了一個參數,在這種有參數傳遞發生的情況下,編寫系統調用服務例程時必須仔細檢查所有的參數是否合法有效。因為系統調用在內核空間執行,如果不加限制任由用戶應用傳遞輸入進入內核,則系統的安全與穩定將受到影響。
參數檢查中最重要的一項就是檢查用戶應用提供的用戶空間指針是否有效。比如上述sys_hello函數參數為char類型指針,並且使用了__user標記進行修飾。__user標記表示所修飾的指針為用戶空間指針,不能在內核空間直接引用,原因主要如下。
用戶空間指針在內核空間可能是無效的。
用戶空間的內存是分頁的,可能引起頁錯誤。
如果直接引用能夠成功,就相當於用戶空間可以直接訪問內核空間,產生安全問題。
因此,為了能夠完成必須的檢查,以及在用戶空間和內核空間之間安全地傳送數據,就需要使用內核提供的函數。比如在sys_hello函數的第6行,就使用了內核提供的strndup_user函數(在mm/util.c文件中定義)從用戶空間復制字符串name的內容。
(2)添加系統調用號。
每個系統調用都會擁有一個獨一無二的系統調用號,所以接下來需要更新include/asm-i386/unistd.h文件,為hello系統調用添加一個系統調用號。
#define 
__NR_utimensat 320 329 #define __NR_signalfd 321 330 #define 
__NR_timerfd 322 331 #define __NR_eventfd 323 332 #define __NR_fallocate
 324 333 #define __NR_hello 325 /*分配hello系統調用號為325*/   #ifdef __KERNEL__
 #define NR_syscalls 326 /*將系統調用數目加1修改為326*/
(3)修改系統調用表。
為了讓系統調用處理程序system_call函數能夠找到hello系統調用,我們還需要修改系統調用表sys_call_table,放入服務例程sys_hello函數的地址。
.long sys_utimensat /* 320 */ .long sys_signalfd 324 . .long sys_timerfd 325 .long sys_eventfd 326 .long sys_fallocate 327 .long sys_hello /*hello系統調用服務例程*/
新的系統調用hello的服務例程被添加到了sys_call_table的末尾。我們可以注意到,sys_call_table每隔5個表項就會有一個注釋,表明該項的系統調用號,這個好習慣可以在查找系統調用對應的系統調用號時提供方便。
(4)重新編譯內核並測試。
為了能夠使用新添加的系統調用,需要重新編譯內核,並使用新內核重新引導系統。然後,我們還需要編寫測試程序對新的系統調用進行測試。針對hello系統調用的測試程序如下:
#include #include #include #define __NR_hello 325 int main(int argc, char *argv[]) { syscall(__NR_hello); return 0; }
然後使用gcc編譯並執行:
$gcc -o hello hello.c $./hello Hello!
由執行結果可見,系統調用添加成功。
5.4.2 什麼時候需要添加新的系統調用
雖說添加一個新的系統調用非常簡單,但這並不意味著用戶有必要這麼做。添加系統調用需要修改內核源代碼、重新編譯內核,如果更進一步希望自己添加的
系統調用能夠得到廣泛的應用,就需要得到官方的認可並分配一個固定的系統調用號,還需要將該系統調用在每個需要支持的體系結構上實現。因此我們最好使用其
他替代方法和內核交換信息,如下所示。
使用設備驅動程序。創建一個設備節點,通過read和write函數進行讀寫訪問,使用ioctl函數進行設置操作和獲取特定信息。這種方法最大的好處在於可以模塊式加載卸載,避免了編譯內核等過程,而且調用接口固定,容易操作。
使用proc虛擬文件系統。利用proc接口獲取系統運行信息和修訂系統狀態是一種很常見的手段,比如讀取/proc/cpuinfo可以獲得當前系統的CPU信息,通過設備驅動提供的proc接口還可以設置硬件寄存器。
sysfs文件系統。sysfs文件系統在2.6內核被引入,是一個類似於proc文件系統的特殊文件系統,用於對系統的設備進行管理,它把實際連接到系統上的設備和總線組織成層次結構,並向用戶提供詳細的內核數據結構信息,用戶可以利用這些信息以實現和內核的交互。
上面說道的用戶空間跟內核空間交互的方法加上系統調用有4種,這些都非常重要。

《UNIX 環境高級編程》一書中如此說:
所有操作系統都提供多種服務的入口點,由此程序向系統內核請求服務。各種版本的Unix都提供經良好定義的有限數目的入口點,經過這些入口點進入系統內
核,這些入口點被稱之為系統調用(system call),系統調用是我們不能更改的一種Unix特征。Unix版本7提供了約50個系統調用,4
3+BSD提供了約110個,而SVR4則提供了約120個。
系統調用界面總是在Unix程序員手冊的第二部分中說明。其定義也包括在C語言中。這與很多較早期的操作系統是不同的,這些系統按傳統都在機器的匯編語言中定義系統核入口點。
Unix所使用的技術是為每條系統調用在標准C庫中設置一個具有同樣名字的函數。用戶進程用標准C調用序列來調用這些函數,然後,函數用系統所要求的技術調用相應的系統核服務。例如函數可將一個或幾個C參數送入通用寄存器,然後執行某個產生軟中斷進入系統核的機器指令。從應用角度考慮,我們可將系統調用視作為C函數。
Unix程序員手冊的第三部分定義了程序員可以使用的通用函數。雖然這些函數可能會調用一個或幾個系統核的系統調用,但是它們並不是系統核的入口
點。例如,printf函數會調用write系統調用以進行輸出操作,但函數strcpy(復制一字符串)和atoi(變換ASCII為整數)並不使用任
何系統調用。
從實施者的角度,系統調用和庫函數之間有重大區別,但從用戶角度其區別並不非常重要。從本書的目的出發,系統調用和庫函數在本書中都以正常的C函數
的形式出現。兩者都對應用程序提供服務,但是,我們應當理解,如果希望的話,我們可以代換庫函數,但是通常我們卻不能代換系統服務。
以存儲器分配函數malloc為例。有多種方法可以進行存儲器分配及與其相關的無用區收集操作(最佳適應,首次適應等),並不存在對所有程序都最佳
的一種技術。Unix系統調用中處理存儲器分配的是sbrk(2),它不是一個通用的存儲器管理器。它增加或減少指定字節數的進程地址空間。如何管理該地
址空間卻取決於進程。存儲器分配函數malloc(3)實現一種特定類型的分配。如果我們不喜歡其操作方式,則我們可以定義自己的malloc函數,極其
可能,它還是要調用sbrk系統調用。事實上,有很多軟件包,它們實現自己的存儲器分配算法,但仍使用sbrk系統調用。圖1.1顯示了應用程序、
malloc函數以及sbrk系統調用之間的關系。

圖1.2〓malloc函數和sbrk系統調用
從中可見,兩者職責不同,相互分開,要核中的系統調用分配另外一塊空間給進程,而庫函數malloc則管理這種空間。
另一個可說明系統調用和庫函數之間的差別的例子是,Unix提供決定當前時間和日期的界面。某些操作系統提供一個系統調用以返回時間,而另一個則返
回日期。任何特殊的處理,例如正常時制和日光節約時制之間的轉換,由系統核處理或要求人的干予。Unix則不同,它只提供一條系統調用,該系統調用返回國
際標准時公元一九七年一月一日午夜來所以經過的秒數。對該值的任何解釋,例如將其變換成人們可讀的,使用本地時區的時間和日期,都留給用戶進程運行。在
標准C庫中,提供了若干例程以處理大多數情況。這些庫函數處理各種細節,例如各種日光節約時算法。
應用程序可以或者調用系統調用,或者庫函數,而很多庫函數則會調用系統調用。這在圖1.3中顯示。
圖1.3〓C庫函數和系統調用之間的差別

另一個系統調用和庫函數之間的差別是:系統調用通常提供一種最小接口,而庫函數通常提供比較復雜的功能。我們從sbrk系統調用和malloc庫函
數之間的差別中看到了這一點,在以後當比較不帶緩存的I/O庫數(第3章)以及標准I/O標准(在第5章)時,我們還將看到這種差別。
進程控制系統調用(fork,exec和wait)通常由用戶的應用程序直接調用。(請回憶程序1.5中的基本shell)但是為了簡化某些常見的
情況,UNIX系統也提供了一些庫函數;例如system和popen。在8.12節中,我們將說明system函數的一種實現,它使用基本的進程控制系
統調用。在10.18中,我們還將強化這一實例以正確地處理信號。
為使讀者了解大多數程序員應用的Unix系統界面,我們不得不既說明系統調用,又介紹某些庫函數。例如若我們只說明sbrk系統調用,那麼就會忽略很多應用程序使用的malloc庫函數。
在本書中,除了一定要將兩者相區分時,我們都將使用術語"函數"來涉及系統調用和庫函數兩者。
《The Linux Kernel Module Programming Guide》書中如此描述:
庫函數是高層的,完全運行在用戶空間, 為程序員提供調用真正的在幕後完成實際事務的系統調用的更方便的接口。系統調用在內核態運行並且由內核自己提供。標准C庫函數
printf()
可以被看做是一個通用的輸出語句,但它實際做的是將數據轉化為符合格式的字符串並且調用系統調用
write()
輸出這些字符串。
Copyright © Linux教程網 All Rights Reserved