昨天花了一天的時間在看Linux0.11的內核,當看到main.c中的int()函數的時候被一個系統調用難倒了(其實筆者的c,彙編等基礎並不是很厲害。但是處於某些原因,就入了這個內核的坑,被澆冷水是經常的事情)。於是各種資料各種搜索,但是大家給出的資料都是千篇一律,不是一開始就介紹什麼是系統調用,就是沒有切實的去一句一句的剖析系統調用的原理,反正就是巴拉巴拉說的基本都是一樣,看下來還是什麼都不懂…… ps:可能是筆者真的太渣,沒辦法理解。
於是我便自己看是研究了,首先是從init哪那裏開始,針對fork()這個系統調用開始了深入的研究(其實也不是很深入了,哈哈哈)。跟蹤它實現的過程,我想現在或者以後也會有人遇到跟我類似的問題,再加上想更加鞏固一下掌握的知識,就着手寫了這篇博了,那麼開始正題吧!
首先要說的是,我們現在針對的是Linux0.11版本內核中的fork()系統調用來對系統調用做分析。
當調用fork()函數時,在main.c中的init()函數中就出現了fork()的調用:
// 下面fork()用於創建一個子進程(子任務)。對於被創建的子進程,fork()將返回0 值,
// 對於原(父進程)將返回子進程的進程號。所以180-184 句是子進程執行的內容。該子進程
// 關閉了句柄0(stdin),以只讀方式打開/etc/rc 文件,並執行/bin/sh 程序,所帶參數和
// 環境變量分別由argv_rc 和envp_rc 數組給出。參見後面的描述。
if (!(pid = fork ()))
{
close (0);
if (open ("/etc/rc", O_RDONLY, 0))
_exit (1); // 如果打開文件失敗,則退出(/lib/_exit.c,10)。
execve ("/bin/sh", argv_rc, envp_rc); // 裝入/bin/sh 程序並執行。
_exit (2); // 若execve()執行失敗則退出(出錯碼2,“文件或目錄不存在”)。
}
大家應該都知道這個函數,確切的說應該是系統調用是用來創建一個新進程的。但是大家會發現內核裏面根本找不到這個函數的具體原型,那它到底是怎麼實現的呢?我們可以用編輯器查看的定義,我用的是source Insight 3 ,發現在unistd中有它的函數原型定義:
<span style="font-family: SimHei;">int fork (void);// 對應各系統調用的函數原型定義。在文件下的第235行</span>
但是我們只能找到這個函數原型定義,卻找不到它的源。還是在這個文件中有這樣一些函數定義:
// 以下定義系統調用嵌入式彙編宏函數。
// 不帶參數的系統調用宏函數。type name(void)。
// %0 - eax(__res),%1 - eax(__NR_##name)。其中name 是系統調用的名稱,與 __NR_ 組合形成上面
// 的系統調用符號常數,從而用來對系統調用表中函數指針尋址。
// 返回:如果返回值大於等於0,則返回該值,否則置出錯號errno,並返回-1。
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ( "int $0x80" \ // 調用系統中斷0x80。
:"=a" (__res) \ // 返回值??eax(__res)。
:"" (__NR_
##name)); \ // 輸入爲系統中斷調用號__NR_name。
if (__res >= 0) \ // 如果返回值>=0,則直接返回該值。
return (type) __res; errno = -__res; \ // 否則置出錯號,並返回-1。
return -1;}
剛看到這個嵌入彙編語句的c程序的時候也是一頭霧水,不知所云,但是根據註釋發現這可能跟系統調用有很大的關係,但是就是不清楚這個syscall(後面的0,1,2……表示的是所帶的參數,這裏暫且統稱爲syscall吧)函數和fork函數的關係。後來通過一番死纏爛打的無賴詢問和翻閱書籍發現是這樣一回事:
在使用這些系統調用的文件中會有這樣一些聲明,比如在main中:
/*
* 我們需要下面這些內嵌語句 - 從內核空間創建進程(forking)將導致沒有寫時複製(COPY ON WRITE)!!!
* 直到一個執行execve 調用。這對堆棧可能帶來問題。處理的方法是在fork()調用之後不讓main()使用
* 任何堆棧。因此就不能有函數調用 - 這意味着fork 也要使用內嵌的代碼,否則我們在從fork()退出
* 時就要使用堆棧了。
* 實際上只有pause 和fork 需要使用內嵌方式,以保證從main()中不會弄亂堆棧,但是我們同時還
* 定義了其它一些函數。
*/
static inline
_syscall0 (int, fork) // 是unistd.h 中的內嵌宏代碼。以嵌入彙編的形式調用
// Linux 的系統調用中斷0x80。該中斷是所有系統調用的
// 入口。該條語句實際上是int fork()創建進程系統調用。
// syscall0 名稱中最後的0 表示無參數,1 表示1 個參數。
static inline _syscall0 (int, pause) // int pause()系統調用:暫停進程的執行,直到
// 收到一個信號。
static inline _syscall1 (int, setup, void *, BIOS) // int setup(void * BIOS)系統調用,僅用於
// linux 初始化(僅在這個程序中被調用)。
static inline _syscall0 (int, sync) // int sync()系統調用:更新文件系統。
可以看到,這裏有一個static inline _syscall0(int,fork),這樣的聲明,很明顯這就是fork()和syscall()之間的聯繫了。如果還是不明顯可以這樣,把聲明,宏函數全部按照順序進行展開,把staticinline_syscall0(int,fork)帶到syscall0函數中去,然後把宏替換掉就會變成這樣的一個函數:
void fork(void)
{
long __res;
__asm__ volatile ( "int $0x80" :"=a" (__res) :"" (__NR_fork));
if (__res >= 0)
return (type) __res; errno = -__res;
return -1;
}
哈哈,沒錯這就是fork()函數了,在調用fork之前我們要在當前文件下先進行上面main中的那樣聲明,這就是創建和調用了fork函數(可能這樣做的原因是減少系統代碼量吧,如果每個系統調用都要像函數那樣去聲明的話,如有60個系統調用,那麼就要有60個這樣的函數,如果用這樣的方法的話,那就只有短短几個函數加上一些函數聲明瞭。可能這只是其中之一)。
那這個函數是如何工作的呢?
在上面的內嵌彙編語句中我們可以看到這樣的一些語句,其實上面也有詳細的說明我就不多解釋。系統調用要通過0x80中斷來實現,int $0x80就是調用0x80中斷的意思,返回值是__res,函系統中斷調用號是__NR_和name的鏈接,如果nam是fork的話,那系統調用號就是__NR_fork啦。系統調用號是在sys.h頭文件中(截取):
extern int sys_setup (); // 系統啓動初始化設置函數。 (kernel/blk_drv/hd.c,71)
extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137)
extern int sys_fork (); // 創建進程。 (kernel/system_call.s, 208)
extern int sys_read (); // 讀文件。 (fs/read_write.c, 55)
extern int sys_write (); // 寫文件。 (fs/read_write.c, 83)
extern int sys_open (); // 打開文件。 (fs/open.c, 138)
// 系統調用函數指針表。用於系統調用中斷處理程序(int 0x80),作爲跳轉表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};
其實__NR_fork對應的宏定義是2,也就對應的是fn_ptr
sys_call_table[]中的2號——sys_fork,這樣一來fork系統調用的具體實現也就轉到了sys_fork這個函數中來了。這個函數的位置就是址 = _sys_call_table + %eax * 4。(未查證,其他博文中看到的)。接下來就是在內核狀態下的系統調用具體實現了。
其實最關鍵的還是在理解:
__asm__ volatile ( "int $0x80" :"=a" (__res) :"" (__NR_fork));
這句話上面,而這句話中,最關鍵的就是0x80號中斷,由於涉及到中斷處理和彙編程序較多,但是筆者現在也只是剛剛開始瞭解內核,所以不好多做解釋,大家可以根據以下幾篇博文進行深入瞭解:
ps:我擦,突然發現哈沒吃午飯,哈哈,那就先到這裏吧,如果有跟深入的理解,會出續篇,同時也歡迎大家指正,既然都看到這裏了,給筆者一個贊鼓勵下吧,哈哈
GCC在C語言中內嵌彙編 call _volasile_
Linux內核——fork()函數創建進程
Linux系統調用的實現技術
系統調用的實現原理
在此非常各位感謝作者的無私奉獻