Linux——进程管理
先看一下進程在課本里的標準定義:“進程是可并發(fā)執(zhí)行的程序在一個數據集合上的運行過程。”這個定義非常嚴謹,而且難懂,如果你沒有一下子理解這句話,就不妨看看并不嚴謹的解釋。我們大家都知道,硬盤上的一個可執(zhí)行文件經常被稱作程序,在Linux系統(tǒng)中,當一個程序開始執(zhí)行后,在開始執(zhí)行到執(zhí)行完畢退出這段時間里,它在內存中的部分就被稱作一個進程。
當然,這個解釋并不完善,但好處是容易理解,在以下的文章中,我們將會對進程作一些更全面的認識。
1.1 Linux進程簡介
Linux是一個多任務的操作系統(tǒng),也就是說,在同一個時間內,可以有多個進程同時執(zhí)行。如果讀者對計算機硬件體系有一定了解的話,會知道我們大家常用的單CPU計算機實際上在一個時間片斷內只能執(zhí)行一條指令,那么Linux是如何實現多進程同時執(zhí)行的呢?原來Linux使用了一種稱為“進程調度(process scheduling)”的手段,首先,為每個進程指派一定的運行時間,這個時間通常很短,短到以毫秒為單位,然后依照某種規(guī)則,從眾多進程中挑選一個投入運行,其他的進程暫時等待,當正在運行的那個進程時間耗盡,或執(zhí)行完畢退出,或因某種原因暫停,Linux就會重新進行調度,挑選下一個進程投入運行。因為每個進程占用的時間片都很短,在我們使用者的角度來看,就好像多個進程同時運行一樣了。
在Linux中,每個進程在創(chuàng)建時都會被分配一個數據結構,稱為進程控制塊(Process Control Block,簡稱PCB)。PCB中包含了很多重要的信息,供系統(tǒng)調度和進程本身執(zhí)行使用,其中最重要的莫過于進程ID(process ID)了,進程ID也被稱作進程標識符,是一個非負的整數,在Linux操作系統(tǒng)中唯一地標志一個進程,其實從進程ID的名字就可以看出,它就是進程的身份證號碼,每個人的身份證號碼都不會相同,每個進程的進程ID也不會相同。
1.2 getpid
#include<sys/types.h> /* 提供類型pid_t的定義 */ #include<unistd.h> /* 提供函數的定義 */ pid_t getpid(void);getpid的作用很簡單,就是返回當前進程的進程ID,請大家看以下的例子:
/* getpid_test.c */ #include<unistd.h> main() {printf("The current process ID is %d\n",getpid()); }編譯并運行程序getpid_test.c:
$gcc getpid_test.c -o getpid_test $./getpid_test The current process ID is 19801.3 fork
#include<sys/types.h> /* 提供類型pid_t的定義 */ #include<unistd.h> /* 提供函數的定義 */ pid_t fork(void);只看fork的名字,可能難得有幾個人可以猜到它是做什么用的。fork系統(tǒng)調用的作用是復制一個進程。當一個進程調用它,完成后就出現兩個幾乎一模一樣的進程,我們也由此得到了一個新進程。據說fork的名字就是來源于這個與叉子的形狀頗有幾分相似的工作流程。
在Linux中,創(chuàng)造新進程的方法只有一個,就是我們正在介紹的fork。其他一些庫函數,如system(),看起來似乎它們也能創(chuàng)建新的進程,如果能看一下它們的源碼就會明白,它們實際上也在內部調用了fork。包括我們在命令行下運行應用程序,新的進程也是由shell調用fork制造出來的。fork有一些很有意思的特征,下面就讓我們通過一個小程序來對它有更多的了解
/* fork_test.c */ #include<sys/types.h> #inlcude<unistd.h> 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\n",getpid());elseprintf("I am the parent process, my process ID is %d\n",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()之前,只有一個進程在執(zhí)行這段代碼,但在這條語句之后,就變成兩個進程在執(zhí)行了,這兩個進程的代碼部分完全相同,將要執(zhí)行的下一條語句都是if(pid<0)……。
兩個進程中,原先就存在的那個被稱作“父進程”,新出現的那個被稱作“子進程”。父子進程的區(qū)別除了進程標志符(process ID)不同外,變量pid的值也不相同,pid存放的是fork的返回值。fork調用的一個奇妙之處就是它僅僅被調用一次,卻能夠返回兩次,它可能有三種不同的返回值:
1.在父進程中,fork返回新創(chuàng)建子進程的進程ID;
2.在子進程中,fork返回0;
3.如果出現錯誤,fork返回一個負值;
fork出錯可能有兩種原因:(1)當前的進程數已經達到了系統(tǒng)規(guī)定的上限;(2)系統(tǒng)內存不足。
fork系統(tǒng)調用出錯的可能性很小,而且如果出錯,一般都為第一種錯誤。如果出現第二種錯誤,說明系統(tǒng)已經沒有可分配的內存,正處于崩潰的邊緣,這種情況對Linux來說是很罕見的。
剩下的代碼,如果pid小于0,說明出現了錯誤;pid==0,就說明fork返回了0,也就說明當前進程是子進程,就去執(zhí)行printf("I am the child!"),否則(else),當前進程就是父進程,執(zhí)行printf("I am the parent!")。
說到這里,可能有些讀者還有疑問:如果fork后子進程和父進程幾乎完全一樣,而系統(tǒng)中產生新進程唯一的方法就是fork,那豈不是系統(tǒng)中所有的進程都要一模一樣嗎?那我們要執(zhí)行新的應用程序時候怎么辦呢?從對Linux系統(tǒng)的經驗中,我們知道這種問題并不存在。至于采用了什么方法,我們到后面具體討論。
1.4 exit
#include<stdlib.h> void exit(int status);不像fork那么難理解,從exit的名字就能看出,這個系統(tǒng)調用是用來終止一個進程的。無論在程序中的什么位置,只要執(zhí)行到exit系統(tǒng)調用,進程就會停止剩下的所有操作,清除包括PCB在內的各種數據結構,并終止本進程的運行。請看下面的程序:
/* exit_test1.c */ #include<stdlib.h> main() {printf("this process will exit!\n");exit(0);printf("never be displayed!\n"); }編譯后運行:
$gcc exit_test1.c -o exit_test1 $./exit_test1 this process will exit!我們可以看到,程序并沒有打印后面的"never be displayed!\n",因為在此之前,在執(zhí)行到exit(0)時,進程就已經終止了。
exit系統(tǒng)調用帶有一個整數類型的參數status,我們可以利用這個參數傳遞進程結束時的狀態(tài),比如說,該進程是正常結束的,還是出現某種意外而結束的,一般來說,0表示沒有意外的正常結束;其他的數值表示出現了錯誤,進程非正常結束。我們在實際編程時,可以用wait系統(tǒng)調用接收子進程的返回值,從而針對不同的情況進行不同的處理。
在一個進程調用了exit之后,該進程并非馬上就消失掉,而是留下一個稱為僵尸進程(Zombie)的數據結構。在Linux進程的5種狀態(tài)中,僵尸進程是非常特殊的一種,它已經放棄了幾乎所有內存空間,沒有任何可執(zhí)行代碼,也不能被調度,僅僅在進程列表中保留一個位置,記載該進程的退出狀態(tài)等信息供其他進程收集,除此之外,僵尸進程不再占有任何內存空間。從這點來看,僵尸進程雖然有一個很酷的名字,但它的影響力遠遠抵不上那些真正的僵尸兄弟,真正的僵尸總能令人感到恐怖,而僵尸進程卻除了留下一些供人憑吊的信息,對系統(tǒng)毫無作用。
也許讀者們還對這個新概念比較好奇,那就讓我們來看一眼Linux里的僵尸進程究竟長什么樣子。
當一個進程已退出,但其父進程還沒有調用系統(tǒng)調用wait(稍后介紹)對其進行收集之前的這段時間里,它會一直保持僵尸狀態(tài),利用這個特點,我們來寫一個簡單的小程序:
/* zombie.c */ #include <sys/types.h> #include <unistd.h> main() {pid_t pid;pid=fork();if(pid<0) /* 如果出錯 */printf("error occurred!\n");else if(pid==0) /* 如果是子進程 */exit(0);else /* 如果是父進程 */sleep(60); /* 休眠60秒,這段時間里,父進程什么也干不了 */wait(NULL); /* 收集僵尸進程 */ }sleep的作用是讓進程休眠指定的秒數,在這60秒內,子進程已經退出,而父進程正忙著睡覺,不可能對它進行收集,這樣,我們就能保持子進程60秒的僵尸狀態(tài)。
編譯這個程序:
$ cc zombie.c -o zombie后臺運行程序,以使我們能夠執(zhí)行下一條命令
$ ./zombie &[1] 1577列一下系統(tǒng)內的進程
$ ps -ax... ... 1177 pts/0 S 0:00 -bash1577 pts/0 S 0:00 ./zombie1578 pts/0 Z 0:00 [zombie <defunct>]1579 pts/0 R 0:00 ps -ax看到中間的"Z"了嗎?那就是僵尸進程的標志,它表示1578號進程現在就是一個僵尸進程。
我們已經學習了系統(tǒng)調用exit,它的作用是使進程退出,但也僅僅限于將一個正常的進程變成一個僵尸進程,并不能將其完全銷毀。僵尸進程雖然對其他進程幾乎沒有什么影響,不占用CPU時間,消耗的內存也幾乎可以忽略不計,但有它在那里呆著,還是讓人覺得心里很不舒服。而且Linux系統(tǒng)中進程數目是有限制的,在一些特殊的情況下,如果存在太多的僵尸進程,也會影響到新進程的產生。那么,我們該如何來消滅這些僵尸進程呢?
先來了解一下僵尸進程的來由,我們知道,Linux和UNIX總有著剪不斷理還亂的親緣關系,僵尸進程的概念也是從UNIX上繼承來的,而UNIX的先驅們設計這個東西并非是因為閑來無聊想煩煩其他的程序員。僵尸進程中保存著很多對程序員和系統(tǒng)管理員非常重要的信息,首先,這個進程是怎么死亡的?是正常退出呢,還是出現了錯誤,還是被其它進程強迫退出的?其次,這個進程占用的總系統(tǒng)CPU時間和總用戶CPU時間分別是多少?發(fā)生頁錯誤的數目和收到信號的數目。這些信息都被存儲在僵尸進程中,試想如果沒有僵尸進程,進程一退出,所有與之相關的信息都立刻歸于無形,而此時程序員或系統(tǒng)管理員需要用到,就只好干瞪眼了。
那么,我們如何收集這些信息,并終結這些僵尸進程呢?就要靠我們下面要講到的wait調用。wait調用的作用是收集僵尸進程留下的信息,同時使這個進程徹底消失。下面就對這個調用作詳細介紹。
1.5 wait
1.5.1 簡介
#include <sys/types.h> /* 提供類型pid_t的定義 */ #include <sys/wait.h> pid_t wait(int *status)進程一旦調用了wait,就立即阻塞自己,由wait自動分析是否當前進程的某個子進程已經退出,如果讓它找到了這樣一個已經變成僵尸的子進程,wait就會收集這個子進程的信息,并把它徹底銷毀后返回;如果沒有找到這樣一個子進程,wait就會一直阻塞在這里,直到有一個出現為止。
參數status用來保存被收集進程退出時的一些狀態(tài),它是一個指向int類型的指針。但如果我們對這個子進程是如何死掉的毫不在意,只想把這個僵尸進程消滅掉,(事實上絕大多數情況下,我們都會這樣想),我們就可以設定這個參數為NULL,就象下面這樣:
pid = wait(NULL);如果成功,wait會返回被收集的子進程的進程ID,如果調用進程沒有子進程,調用就會失敗,此時wait返回-1。
1.5.2 實戰(zhàn)
下面就讓我們用一個例子來實戰(zhàn)應用一下wait調用,程序中用到了系統(tǒng)調用fork。
/* wait1.c */ #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h>main() {pid_t pc,pr;pc=fork();if(pc<0) /* 如果出錯 */printf("error ocurred!\n");else if(pc==0){ /* 如果是子進程 */ printf("This is child process with pid of %d\n",getpid());sleep(10); /* 睡眠10秒鐘 */}else{ /* 如果是父進程 */pr=wait(NULL); /* 在這里等待 */printf("I catched a child process with pid of %d\n"),pr);} exit(0); }編譯并運行:
$ cc wait1.c -o wait1 $ ./wait1 This is child process with pid of 1508 I catched a child process with pid of 1508可以明顯注意到,在第2行結果打印出來前有10秒鐘的等待時間,這就是我們設定的讓子進程睡眠的時間,只有子進程從睡眠中蘇醒過來,它才能正常退出,也就才能被父進程捕捉到。
1.5.3 進程同步
有時候,父進程要求子進程的運算結果進行下一步的運算,或者子進程的功能是為父進程提供了下一步執(zhí)行的先決條件(如:子進程建立文件,而父進程寫入數據),此時父進程就必須在某一個位置停下來,等待子進程運行結束,而如果父進程不等待而直接執(zhí)行下去的話,可以想見,會出現極大的混亂。這種情況稱為進程之間的同步,更準確地說,這是進程同步的一種特例。進程同步就是要協調好2個以上的進程,使之以安排好地次序依次執(zhí)行。解決進程同步問題有更通用的方法,我們將在以后介紹,但對于我們假設的這種情況,則完全可以用wait系統(tǒng)調用簡單的予以解決。請看下面這段程序:
#include <sys/types.h> #include <sys/wait.h>main() {pid_t pc, pr;int status;pc=fork();if(pc<0)printf("Error occured on forking.\n");else if(pc==0){/* 子進程的工作 */exit(0);}else{/* 父進程的工作 */pr=wait(&status);/* 利用子進程的結果 */}這段程序只是個例子,不能真正拿來執(zhí)行,但它卻說明了一些問題,首先,當fork調用成功后,父子進程各做各的事情,但當父進程的工作告一段落,需要用到子進程的結果時,它就停下來調用wait,一直等到子進程運行結束,然后利用子進程的結果繼續(xù)執(zhí)行,這樣就圓滿地解決了我們提出的進程同步問題。
1.6 exec
也許有同學從fork開始讀,一直到這里還有一個很大的疑惑:既然所有新進程都是由fork產生的,而且由fork產生的子進程和父進程幾乎完全一樣,那豈不是意味著系統(tǒng)中所有的進程都應該一模一樣了嗎?而且,就我們的常識來說,當我們執(zhí)行一個程序的時候,新產生的進程的內容應就是程序的內容才對。是我們理解錯了嗎?顯然不是,要解決這些疑惑,就必須提到我們下面要介紹的exec系統(tǒng)調用。
exec函數族的作用是根據指定的文件名找到可執(zhí)行文件,并用它來取代調用進程的內容,換句話說,就是在調用進程內部執(zhí)行一個可執(zhí)行文件。這里的可執(zhí)行文件既可以是二進制文件,也可以是任何Linux下可執(zhí)行的腳本文件。
與一般情況不同,exec函數族的函數執(zhí)行成功后不會返回,因為調用進程的實體,包括代碼段,數據段和堆棧等都已經被新的內容取代,只留下進程ID等一些表面上的信息仍保持原樣,頗有些神似"三十六計"中的"金蟬脫殼"。看上去還是舊的軀殼,卻已經注入了新的靈魂。只有調用失敗了,它們才會返回一個-1,從原程序的調用點接著往下執(zhí)行。
現在我們應該明白了,Linux下是如何執(zhí)行新程序的,每當有進程認為自己不能為系統(tǒng)和用戶做出任何貢獻了,他就可以發(fā)揮最后一點余熱,調用任何一個exec,讓自己以新的面貌重生;或者,更普遍的情況是,如果一個進程想執(zhí)行另一個程序,它就可以fork出一個新進程,然后調用任何一個exec,這樣看起來就好像通過執(zhí)行應用程序而產生了一個新進程一樣。
事實上第二種情況被應用得如此普遍,以至于Linux專門為其作了優(yōu)化,我們已經知道,fork會將調用進程的所有內容原封不動的拷貝到新產生的子進程中去,這些拷貝的動作很消耗時間,而如果fork完之后我們馬上就調用exec,這些辛辛苦苦拷貝來的東西又會被立刻抹掉,這看起來非常不劃算,于是人們設計了一種"寫時拷貝(copy-on-write)"技術,使得fork結束后并不立刻復制父進程的內容,而是到了真正實用的時候才復制,這樣如果下一條語句是exec,它就不會白白作無用功了,也就提高了效率。
int execve(const char *path, char *const argv[ ], char *const envp[ ]);execve第1個參數path是被執(zhí)行應用程序的完整路徑,第2個參數argv就是傳給被執(zhí)行應用程序的命令行參數,第3個參數envp是傳給被執(zhí)行應用程序的環(huán)境變量。
因為課程的學時有限,我們對于exec系統(tǒng)調用的使用就不再深入了,又興趣的同學可查看《UNIX進程間通信》等一些關于linux編程的書。
源程序
。。。。參考文章
浙江理工大學——操作系統(tǒng)實驗指導?(2020.10)
?
總結
以上是生活随笔為你收集整理的Linux——进程管理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux——Linux C语言编程基础
- 下一篇: 《操作系统》实验报告——熟悉Linux基