一、知識背景
學習Linux 程序控制理論無非就是學習程序的幾個方面:建立、退出、等待其他程序結束、執行新的程式。但是在學習這些理論知識以及
具體實作方法之前,有必要了解一下程序其他的基本知識點
1、Linux 系統以程序為基本機關配置設定資源,以線程為基本機關進行排程;
2、程序擁有自己的位址空間,程序相關所有的資訊都存放在程序的位址空間裡,換句話說,程序所能通路到的位址空間的集合就是程序的位址空間;
3、Linux 核心所有的程序都有一個基本結構:程序控制塊(PCB),也叫做任務結構(task_struct),這些結構通過雙向循環連結清單組成一個程序連結清單,即每個程序都會在程序表裡占據一個表項;程序控制塊裡包含了程序相關的所有資訊,如堆棧段、代碼段、打開的檔案描述符、所屬的使用者ID以及組ID等等。
4、每個程序都會有一個程序 描述符:PID,不同的程序所擁有的描述符一定不同,程序結束後它的描述符可以再次被使用;
5、PID為0的是排程程式,PID為1的是init程序;
6、程序的結構
一個Linux 程序由三段組成:代碼段、資料段、堆棧段,這三個段就是一個可執行檔案的必備組成結構。顧名思義,代碼段存儲的是可執行代碼,資料段存放的是全局變 量、靜态變量等,堆棧段存放了臨時變量和動态申請到的資源。具體哪個段存放哪些資料,戳這裡檢視:Linux 程式位址空間分布
二、建立程序
1、建立程序的時機
在Linux系統中建立程序的時機是可以總結出來的:
1)、系統初始化
在Linux 系統啟動的過程中,Linux核心會建立許多必要的程序以完成整個初始化過程,比如init程序就是第一個使用者态的程序。
2)、一個已存在的程序調用系統調用建立一個新的程序
這是Linux 系統中最常見的一個建立程序的方式,由一個程序在代碼裡直接調用系統調用建立一個新的程序,再在新的程序裡執行其他代碼。這也是本文描述的對象。
3)、使用者請求建立以額新的程序
這種方法就比較像windous系統和桌面版Linux 系統了,使用者通過滑鼠或者指令行建立一個程序。
4)、一個批處理作業的初始化
使用者送出批處理作業,作業系統檢查完資源之後會建立一個程序,以運作批處理作業隊列的下一個作業。
從技術上講,這些建立新程序的方式中,本質上都是:由一個已經存在的程序執行了建立新程序的系統調用後完成。
2、程序建立函數
1)、fork()函數
該函數建立一個與調用程序一模一樣的程序,新程序叫子程序,調用函數的程序叫父程序。也就是說,當父程序調用了一個fork()系統調用時,fork()便會建立一個新的位址 空間,并且把父程序的位址空間的内容大部分的複制到子程序的位址空間上。 沒錯,是大部分而不是全部複制。例如父子程序的PID肯定不同對吧,且父子程序的父程序 ID也不可能相同,除此之外父程序設定的檔案鎖也不會被子程序繼承,不同的内容還有其他。當然相同内容更多,比如代碼是共享的,即隻有一份,但是堆棧段和資料段 都有兩個副本,以及打開的檔案描述符,使用者ID、組ID控制終端等都兩個副本且内容相同。
fork()函數還有一個很有趣的特點便是:一次調用兩次傳回,傳回0給子程序,傳回子程序的PID給父程序。為什麼要這樣做呢?想想也是,fork的結果是建立了一個新的進 程,并且兩個程序的位址空間的内容都差不多,那要區分這兩個程序還真不好辦,但是“傳回兩次”這個特性就為我們區分父、子程序提供了方法。我們可以通過一個判斷返 回值的内容來區分父、子程序。以下是基本代碼架構:
int main(int argc, char *argv[])
{
pid_t pid;
printf("Before fork\n");
if((pid=fork())<0)
{
printf("Fork error!\n");
exit(0);
}
if(pid==0)//child process
{
execl("/bin/ls","ls","/home",NULL);
}
else if(pid>0)
{
printf("This is father process\n");
exit(0);
}
}
2)、vfork()函數
vfork() 函數的功能和fork()異曲同工,除了以下幾點:a、fork()函數并不能確定在fork()函數傳回後是哪個程序先運作(由排程算法決定),但是vfork()函數傳回後一定是 子程序先運作;b、vfork() 函數建立的子程序并不會複制父程序的資料段和堆棧段,即父子程序共享代碼段、資料段、堆棧段。
三、程序的終止
1、終止時機
1)、正常退出(自願)
程序完成自己的工作後便自動的交出CPU的控制權,通常程序執行exit()函數或者return語句後便結束,這種屬于正常的退出。
2)、出錯退出(自願)
程序在發現自己需要完成的工作沒法完成時便會退出,但是這種退出仍然屬于自願、非強制性的,比如編譯程序執行:cc funct.c ,但是funct.c 并不存在,是以編譯程序便 退出,并且傳回出錯資訊,諸如此類的退出都叫做出錯退出。
3)、嚴重錯誤(非自願)
程序發現了一些緻命的錯誤,如執行了非法指令、引用非法位址、除書是零等,它便會通知作業系統,作業系統會給程序發出一個信号。
4)、被其他程序殺死(非自願)
在Linux 中,一個程序通過調用一個系統調用通知作業系統殺死其他程序。
2、程序終止的方法
1)、正常終止的方法包括:a、在主函數調用了return 語句;b、調用了exit函數;事實上在主函數調用了return語句其效果等于調用了exit函數,不同的是如果在其他函數 裡調用了exit函數,則程序直接結束;c、程序的最後一個線程在代碼裡執行了return語句;d、程序的最後一個線程調用了pthread_exit函數。
2)、異常終止的方法包括:a、調用abort,産生SIGABRT信号;b、程序接收到某些信号,這些信号使得程序退出;c、最後一個線程對“cancellation”請求作出響應。
正常退出裡的a、b情況是本文描述的重點,關于信号和線程将在後續的文章裡較長的描述。
2、終止程序的函數
exit(int status) 函數将直接終止調用程序,并且通過status參數将本程序的退出狀态傳回給父程序,父程序通過wait或者waitpid函數可以獲得子程序的退出狀态。需要注意的是exit()函數是不會傳回的,想想便了解,如果有傳回那麼應該傳回哪裡?調用這個函數的程序已經退出了,更暴力一點的說法就是,程序的位址空間已經被釋放了,它已經找不到”家“了,還如何傳回?這裡還有一個奇妙的問題:既然退出狀态是傳回給父程序的,如果父程序在子程序終止之前就已經退出了,那麼子程序的退出狀态将傳回給誰呢?答案是:所有程序在退出時,如果其子程序還沒有退出,則這些子程序都将變成init的子程序。事實上,在一個程序要退出時,核心逐個檢查每個活動程序,如果他們是正要退出的程序的子程序,則直接将這些程序的父程序ID改成1(init 程序的PID),這樣就保證了每一個程序都有一個父程序,也說明了其實init 可以是每個程序的父程序。
無論程序如何終止,最後都會執行核心裡的同一段代碼,這段代碼處理程序的收尾工作,如關閉打開的檔案描述符、釋放程序所使用的存儲器等等。
四、程序的等待
父程序在建立了一個子程序後便和子程序“分道揚镳”,各自運作在自己的位址空間,是以父程序要擷取子程序的退出狀态,隻能通過核心。核心會實時監控子程序,如果子程序退出,核心就會給父程序發出一個SIGCHLD的信号,獲得信号以後,父程序可以選擇忽略它(預設動作)或者提供一個處理該信号的函數。通常情況下,父程序需要等待子程序的退出信号,這時我們可以像辦法讓父程序進入阻塞态。
wait()函數和waitpid()函數就提供了這個功能。
wait()函數使得父程序進入阻塞态,并且等待子程序退出,如果任何一個子程序退出,則程序從這個函數傳回(當然中間還有程序排程的問題),并且此時已經獲得子程序的退出狀态。當然調用這個函數的程序可能沒有子程序,或者所有子程序都已經結束了,那麼調用這個函數就不一定會阻塞程序,具體的後果視情況而定。如果程序在接到SIGCHLD後調用這個函數,則會立即傳回。
waitpid()函數功能和wait()函數差不多,并且有所加強,前者能夠指定等待的程序,并且能夠指定調用程序在調用後不進入阻塞态。
注意:兩個函數都擁有一個指向整型資料的指針,如果不指定為NULL ,則可以傳回子程序的終止狀态,如果指定為NULL ,則終止轉态将不傳回。如果不關注終止狀态可以指定為NULL。
五、執行新的程式
exec函數族:
exec 是execute的縮寫,是一類函數的總稱,在Linux 系統裡,這些函數将在現有的程序空間裡裝入指定的可執行代碼。
當fork()函數建立一個子程序以後,子程序的位址空間已經填充了一些由父程序複制過來的内容,包括代碼段、資料段、堆棧段,是以子程序在fork()傳回以後會立即執行fork() 後面的代碼,直到遇到exec函數的其中一個。在很多情況下我們不需要子程序擁有和父程序同樣的可執行代碼,而是希望子程序能夠裝入其他的代碼以執行。exec函數族就完成這樣的功能。
當程序調用exec 函數以後,這個程序的代碼段和資料段以及堆棧段都将被新程式的内容所覆寫。其他位址空間裡的内容不一定都改變,其中PID、UID、GID、工作目錄、終端等都不變,而檔案描述符是否關閉就要看程序中描述符的“執行時關閉标志”。
六、代碼舉例
其實代碼可以直接使用程序建立章節裡的架構。稍加修改就可以加上前面所有的函數,以實作一個比較完整的功能:父程序建立一個子程序,然後等待子程序的退出,子程序退出後父程序列印消息;子程序執行一個新的程式,這裡執行"/bin/ls"。代碼如下:
int main(int argc, char *argv[])
{
pid_t pid;
printf("\nBefore fork\n");
if((pid=fork())<0)
{
printf("Fork error!\n");
exit(0);
}
if(pid==0)//child process
{
printf("\nThis is the child process\n");
printf("executing new program\n");
execl("/bin/ls","ls","/home",NULL);
}
else if(pid>0) //father process
{
wait(NULL);
printf("\nThis is the father process\n\n");
exit(0);
}
}