Linux 內核學習——系統調用(fork)

    昨天花了一天的時間在看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:我擦,突然發現哈沒吃午飯,哈哈,那就先到這裏吧,如果有跟深入的理解,會出續篇,同時也歡迎大家指正,既然都看到這裏了,給筆者一個贊鼓勵下吧,哈哈


Linux系統中斷處理程序int 0x80實現原理

GCC在C語言中內嵌彙編 call _volasile_
Linux內核——fork()函數創建進程
Linux系統調用的實現技術
系統調用的實現原理

在此非常各位感謝作者的無私奉獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章