歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux綜合 >> Linux資訊 >> 更多Linux

Linux系統調用跟我學(2)――進程管理

作者:雷鎮     本文介紹了Linux下的進程概念,並著重講解了與Linux進程管理相關的4個重要系統調用getpid,fork,exit和_exit,輔助一些例程說明了它們的特點和使用方法。       關於進程的一些必要知識      先看一下進程在大學課本裡的標准定義:“進程是可並發執行的程序在一個數據集合上的運行過程。”這個定義非常嚴謹,而且難懂,如果你沒有一下子理解這句話,就不妨看看筆者自己的並不嚴謹的解釋。我們大家都知道,硬盤上的一個可執行文件經常被稱作程序,在Linux系統中,當一個程序開始執行後,在開始執行到執行完畢退出這段時間裡,它在內存中的部分就被稱作一個進程。     當然,這個解釋並不完善,但好處是容易理解,在以下的文章中,我們將會對進程作一些更全面的認識。     Linux進程簡介     Linux是一個多任務的操作系統,也就是說,在同一個時間內,可以有多個進程同時執行。如果讀者對計算機硬件體系有一定了解的話,會知道我們大家常用的單CPU計算機實際上在一個時間片斷內只能執行一條指令,那麼Linux是如何實現多進程同時執行的呢?原來Linux使用了一種稱為“進程調度(process scheduling)”的手段,首先,為每個進程指派一定的運行時間,這個時間通常很短,短到以毫秒為單位,然後依照某種規則,從眾多進程中挑選一個投入運行,其他的進程暫時等待,當正在運行的那個進程時間耗盡,或執行完畢退出,或因某種原因暫停,Linux就會重新進行調度,挑選下一個進程投入運行。因為每個進程占用的時間片都很短,在我們使用者的角度來看,就好像多個進程同時運行一樣了。     在Linux中,每個進程在創建時都會被分配一個數據結構,稱為進程控制塊(Process Control Block,簡稱PCB)。PCB中包含了很多重要的信息,供系統調度和進程本身執行使用,其中最重要的莫過於進程ID(process ID)了,進程ID也被稱作進程標識符,是一個非負的整數,在Linux操作系統中唯一地標志一個進程,在我們最常使用的I386架構(即PC使用的架構)上,一個非負的整數的變化范圍是0-32767,這也是我們所有可能取到的進程ID。其實從進程ID的名字就可以看出,它就是進程的身份證號碼,每個人的身份證號碼都不會相同,每個進程的進程ID也不會相同。     一個或多個進程可以合起來構成一個進程組(process group),一個或多個進程組可以合起來構成一個會話(session)。這樣我們就有了對進程進行批量操作的能力,比如通過向某個進程組發送信號來實現向該組中的每個進程發送信號。     最後,讓我們通過ps命令親眼看一看自己的系統中目前有多少進程在運行:     $ps -aux(以下是在我的計算機上的運行結果,你的結果很可能與這不同。)  USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND  root 1 0.1 0.4 1412 520 ? S May15 0:04 init [3]  root 2 0.0 0.0 0 0 ? SW May15 0:00 [keventd]  root 3 0.0 0.0 0 0 ? SW May15 0:00 [kapm-idled]  root 4 0.0 0.0 0 0 ? SWN May15 0:00 [ksoftirqd_CPU0]  root 5 0.0 0.0 0 0 ? SW May15 0:00 [kswapd]  root 6 0.0 0.0 0 0 ? SW May15 0:00 [kreclaimd]  root 7 0.0 0.0 0 0 ? SW May15 0:00 [bdflush]  root 8 0.0 0.0 0 0 ? SW May15 0:00 [kupdated]  root 9 0.0 0.0 0 0 ? SW< May15 0:00 [mdrecoveryd]  root 13 0.0 0.0 0 0 ? SW May15 0:00 [kjournald]  root 132 0.0 0.0 0 0 ? SW May15 0:00 [kjournald]  root 673 0.0 0.4 1472 592 ? S May15 0:00 syslogd -m 0  root 678 0.0 0.8 2084 1116 ? S May15 0:00 klogd -2  rpc 698 0.0 0.4 1552 588 ? S May15 0:00 portmap  rpcuser 726 0.0 0.6 1596 764 ? S May15 0:00 rpc.statd  root 839 0.0 0.4 1396 524 ? S May15 0:00 /usr/sbin/apmd -p  root 908 0.0 0.7 2264 1000 ? S May15 0:00 xinetd -stayalive  root 948 0.0 1.5 5296 1984 ? S May15 0:00 sendmail: accepti  root 967 0.0 0.3 1440 484 ? S May15 0:00 gpm -t ps/2 -m /d  wnn 987 0.0 2.7 4732 3440 ? S May15 0:00 /usr/bin/cserver  root 1005 0.0 0.5 1584 660 ? S May15 0:00 crond  wnn 1025 0.0 1.9 3720 2488 ? S May15 0:00 /usr/bin/tserver  xfs 1079 0.0 2.5 4592 3216 ? S May15 0:00 xfs -droppriv -da  daemon 1115 0.0 0.4 1444 568 ? S May15 0:00 /usr/sbin/atd  root 1130 0.0 0.3 1384 448 tty1 S May15 0:00 /sbin/mingetty tt  root 1131 0.0 0.3 1384 448 tty2 S May15 0:00 /sbin/mingetty tt  root 1132 0.0 0.3 1384 448 tty3 S May15 0:00 /sbin/mingetty tt  root 1133 0.0 0.3 1384 448 tty4 S May15 0:00 /sbin/mingetty tt  root 1134 0.0 0.3 1384 448 tty5 S May15 0:00 /sbin/mingetty tt  root 1135 0.0 0.3 1384 448 tty6 S May15 0:00 /sbin/mingetty tt  root 8769 0.0 0.6 1744 812 ? S 00:08 0:00 in.telnetd: 192.1  root 8770 0.0 0.9 2336 1184 pts/0 S 00:08 0:00 login -- lei  lei 8771 0.1 0.9 2432 1264 pts/0 S 00:08 0:00 -bash  lei 8809 0.0 0.6 2764 808 pts/0 R 00:09 0:00 ps -aux         以上除標題外,每一行都代表一個進程。在各列中,PID一列代表了各進程的進程ID,COMMAND一列代表了進程的名稱或在Shell中調用的命令行,對其他列的具體含義,我就不再作解釋,有興趣的讀者可以去參考相關書籍。     getpid     在2.4.4版內核中,getpid是第20號系統調用,其在Linux函數庫中的原型是:     #include /* 提供類型pid_t的定義 */  #include /* 提供函數的定義 */  pid_t getpid(void);         getpid的作用很簡單,就是返回當前進程的進程ID,請大家看以下的例子:     /* getpid_test.c */  #include  main()  {   printf("The current process ID is %d  ",getpid());  }         細心的讀者可能注意到了,這個程序的定義裡並沒有包含頭文件sys/types.h,這是因為我們在程序中沒有用到pid_t類型,pid_t類型即為進程ID的類型。事實上,在i386架構上(就是我們一般PC計算機的架構),pid_t類型是和int類型完全兼容的,我們可以用處理整形數的方法去處理pid_t類型的數據,比如,用"%d"把它打印出來。     編譯並運行程序getpid_test.c:     $gcc getpid_test.c -o getpid_test  $./getpid_test  The current process ID is 1980  (你自己的運行結果很可能與這個數字不一樣,這是很正常的。)         再運行一遍:     $./getpid_test  The current process ID is 1981         正如我們所見,盡管是同一個應用程序,每一次運行的時候,所分配的進程標識符都不相同。     fork     在2.4.4版內核中,fork是第2號系統調用,其在Linux函數庫中的原型是:     #include /* 提供類型pid_t的定義 */   #include /* 提供函數的定義 */   pid_t fork(void);         只看fork的名字,可能難得有幾個人可以猜到它是做什麼用的。fork系統調用的作用是復制一個進程。當一個進程調用它,完成後就出現兩個幾乎一模一樣的進程,我們也由此得到了一個新進程。據說fork的名字就是來源於這個與叉子的形狀頗有幾分相似的工作流程。     在Linux中,創造新進程的方法只有一個,就是我們正在介紹的fork。其他一些庫函數,如system(),看起來似乎它們也能創建新的進程,如果能看一下它們的源碼就會明白,它們實際上也在內部調用了fork。包括我們在命令行下運行應用程序,新的進程也是由shell調用fork制造出來的。fork有一些很有意思的特征,下面就讓我們通過一個小程序來對它有更多的了解。     /* fork_test.c */  #include  #inlcude  main()  {   pid_t pid;   /*此時僅有一個進程*/   pid=fork();   /*此時已經有兩個進程在同時運行*/   if(pid<0)   printf("error in fork!");   else if(pid==0)   printf("I am the child process, my process ID is %d  ",getpid());   else   printf("I am the parent process, my process ID is %d  ",getpid());  }         編譯並運行:     $gcc fork_test.c -o fork_test  $./fork_test  I am the parent process, my process ID is 1991  I am the child process, my process ID is 1992         看這個程序的時候,頭腦中必須首先了解一個概念:在語句pid=fork()之前,只有一個進程在執行這段代碼,但在這條語句之後,就變成兩個進程在執行了,這兩個進程的代碼部分完全相同,將要執行的下一條語句都是if(pid==0)……。     兩個進程中,原先就存在的那個被稱作“父進程”,新出現的那個被稱作“子進程”。父子進程的區別除了進程標志符(process ID)不同外,變量pid的值也不相同,pid存放的是fork的返回值。fork調用的一個奇妙之處就是它僅僅被調用一次,卻能夠返回兩次,它可能有三種不同的返回值:     在父進程中,fork返回新創建子進程的進程ID;     在子進程中,fork返回0;     如果出現錯誤,fork返回一個負值;     fork出錯可能有兩種原因:     (1)當前的進程數已經達到了系統規定的上限,這時errno的值被設置為EAGAIN。(2)系統內存不足,這時errno的值被設置為ENOMEM。(關於errno的意義,請參考本系列的第一篇文章。)     fork系統調用出錯的可能性很小,而且如果出錯,一般都為第一種錯誤。如果出現第二種錯誤,說明系統已經沒有可分配的內存,正處於崩潰的邊緣,這種情況對Linux來說是很罕見的。     說到這裡,聰明的讀者可能已經完全看懂剩下的代碼了,如果pid小於0,說明出現了錯誤;pid==0,就說明fork返回了0,也就說明當前進程是子進程,就去執行printf("I am the child!"),否則(else),當前進程就是父進程,執行printf("I am the parent!")。完美主義者會覺得這很冗余,因為兩個進程裡都各有一條它們永遠執行不到的語句。不必過於為此耿耿於懷,畢竟很多年以前,UNIX的鼻祖們在當時內存小得無法想象的計算機上就是這樣寫程序的,以我們如今的“海量”內存,完全可以把這幾個字節的顧慮拋到九霄雲外。     說到這裡,可能有些讀者還有疑問:如果fork後子進程和父進程幾乎完全一樣,而系統中產生新進程唯一的方法就是fork,那豈不是系統中所有的進程都要一模一樣嗎?那我們要執行新的應用程序時候怎麼辦呢?從對Linux系統的經驗中,我們知道這種問題並不存在。至於采用了什麼方法,我們把這個問題留到後面具體討論。     exit     在2.4.4版內核中,exit是第1號調用,其在Linux函數庫中的原型是:     #include   void exit(int status);         不像fork那麼難理解,從exit的名字就能看出,這個系統調用是用來終止一個進程的。無論在程序中的什麼位置,只要執行到exit系統調用,進程就會停止剩下的所有操作,清除包括PCB在內的各種數據結構,並終止本進程的運行。請看下面的程序:     /* exit_test1.c */  #include  main()  {   printf("this process will exit!  ");   exit(0);   printf("never be displayed!  ");  }         編譯後運行:     $gcc exit_test1.c -o exit_test1  $./exit_test1  this process will exit!         我們可以看到,程序並沒有打印後面的“never be displayed! ”,因為在此之前,在執行到exit(0)時,進程就已經終止了。     exit系統調用帶有一個整數類型的參數status,我們可以利用這個參數傳遞進程結束時的狀態,比如說,該進程是正常結束的,還是出現某種意外而結束的,一般來說,0表示沒有意外的正常結束;其他的數值表示出現了錯誤,進程非正常結束。我們在實際編程時,可以用wait系統調用接收子進程的返回值,從而針對不同的情況進行不同的處理。關於wait的詳細情況,我們將在以後的篇幅中進行介紹。     exit和_exit     作為系統調用而言,_exit和exit是一對孿生兄弟,它們究竟相似到什麼程度,我們可以從Linux的源碼中找到答案:     #define __NR__exit __NR_exit /* 摘自文件include/asm-i386/unistd.h第334行 */         “__NR_”是在Linux的源碼中為每個系統調用加上的前綴,請注意第一個exit前有2條下劃線,第二個exit前只有1條下劃線。     這時隨便一個懂得C語言並且頭腦清醒的人都會說,_exit和exit沒有任何區別,但我們還要講一下這兩者之間的區別,這種區別主要體現在它們在函數庫中的定義。_exit在Linux函數庫中的原型是:     #include   void _exit(int status);         和exit比較一下,exit()函數定義在stdlib.h中,而_exit()定義在unistd.h中,從名字上看,stdlib.h似乎比unistd.h高級一點,那麼,它們之間到底有什麼區別呢?讓我們先來看流程圖,通過下圖,我們會對這兩個系統調用的執行過程產生一個較為直觀的認識。             從圖中可以看出,_exit()函數的作用最為簡單:直接使進程停止運行,清除其使用的內存空間,並銷毀其在內核中的各種數據結構;exit()函數則在這些基礎上作了一些包裝,在執行退出之前加了若干道工序,也是因為這個原因,有些人認為exit已經不能算是純粹的系統調用。     exit()函數與_exit()函數最大的區別就在於exit()函數在調用exit系統調用之前要檢查文件的打開情況,把文件緩沖區中的內容寫回文件,就是圖中的“清理I/O緩沖”一項。     在Linux的標准函數庫中,有一套稱作“高級I/O”的函數,我們熟知的printf()、fopen()、fread()、fwrite()都在此列,它們也被稱作“緩沖I/O(buffered I/O)”,其特征是對應每一個打開的文件,在內存中都有一片緩沖區,每次讀文件時,會多讀出若干條記錄,這樣下次讀文件時就可以直接從內存的緩沖區中讀取,每次寫文件的時候,也僅僅是寫入內存中的緩沖區,等滿足了一定的條件(達到一定數量,或遇到特定字符,如換行符和文件結束符EOF),再將緩沖區中的內容一次性寫入文件,這樣就大大增加了文件讀寫的速度,但也為我們編程帶來了一點點麻煩。如果有一些數據,我們認為已經寫入了文件,實際上因為沒有滿足特定的條件,它們還只是保存在緩沖區內,這時我們用_exit()函數直接將進程關閉,緩沖區中的數據就會丟失,反之,如果想保證數據的完整性,就一定要使用exit()函數。     請看以下例程:     /* exit2.c */  #include  main()  {   printf("output begin  ");   printf("content in buffer");   exit(0);  }         編譯並運行:     $gcc exit2.c -o exit2  $./exit2  output begin  content in buffer  /* _exit1.c */  #include  main()  {   printf("output begin  ");   printf("content in buffer");   _exit(0);  }         編譯並運行:     $gcc _exit1.c -o _exit1  $./_exit1  output begin         在Linux中,標准輸入和標准輸出都是作為文件處理的,雖然是一類特殊的文件,但從程序員的角度來看,它們和硬盤上存儲數據的普通文件並沒有任何區別。與所有其他文件一樣,它們在打開後也有自己的緩沖區。     請讀者結合前面的敘述,思考一下為什麼這兩個程序會得出不同的結果。相信如果您理解了我前面所講的內容,會很容易的得出結論。     在這篇文章中,我們對Linux的進程管理作了初步的了解,並在此基礎上學習了getpid、fork、exit和_exit四個系統調用。在下一篇文章中,我們將學習與Linux進程管理相關的其他系統調用,並將作一些更深入的探討。   




 



Copyright © Linux教程網 All Rights Reserved