UNIX/Linux-进程控制(实例入门篇)
UNIX進程
?
進程標(biāo)識符
要想對進程控制,必須得獲取進程的標(biāo)識。每個進程都有一個非負整數(shù)表示的唯一進程ID,雖然是唯一的,但是進程ID可以重用。當(dāng)一個進程終止后,其進程ID就可以再次使用了。
系統(tǒng)中有一些專用的進程。
ID為0的進程通常是調(diào)度進程(常常被稱為交換進程swapper)。該進程是內(nèi)核的一部分,它不執(zhí)行任何磁盤上的程序。
進程ID1通常是init進程。此進程負責(zé)在自舉內(nèi)核后啟動一個UNIX系統(tǒng)。init通常讀與系統(tǒng)有關(guān)的初始化文件,并將系統(tǒng)引導(dǎo)到一個狀態(tài)。init進程絕不會終止,它是一個普通的用戶進程,但是它以超級用戶特權(quán)運行。
#include <unistd.h>
pid_t? getpid(void) ;???????? //獲取調(diào)用進程的進程ID
pid_t? getppid(void) ;??????? //獲取調(diào)用進程的父進程ID
uid_t? getuid(void) ;???????? //獲取調(diào)用進程的實際用戶ID
gid_t? getgid(void) ;???????? //獲取調(diào)用進程的實際組ID
?
進程創(chuàng)建
#include <unistd.h>
pid_t? fork(void) ;
一個現(xiàn)有進程可以調(diào)用fork函數(shù)創(chuàng)建一個新進程。由fork創(chuàng)建的新進程被稱為子進程。
fork函數(shù)被調(diào)用一次,但返回兩次。兩次返回的唯一區(qū)別是子進程的返回值是0,而父進程的返回值則是新子進程的進程ID。
子進程是父進程的副本。子進程獲得父進程數(shù)據(jù)空間、堆和棧的副本。父、子進程并不共享這些存儲空間部分。父、子進程共享正文段。
?
由于在fork之后經(jīng)常跟隨著exec,所以現(xiàn)在的很多實現(xiàn)并不執(zhí)行一個父進程數(shù)據(jù)段、棧和堆的完全復(fù)制。而是使用了寫時復(fù)制(Copy-On-Write, COW)技術(shù)。這些區(qū)域由父、子進程共享,而且內(nèi)核將它們的訪問權(quán)限改變?yōu)橹蛔x的。如果父、子進程中的任一個試圖修改這些區(qū)域,則內(nèi)核只為修改區(qū)域的那塊內(nèi)存制作一個副本,通常是虛擬存儲器系統(tǒng)中的一”頁”。
?
fork有下面兩種用法:
1、一個父進程希望復(fù)制自己,使父子進程同時執(zhí)行不同的代碼段。(開始時只有一個進程,后來fork出了兩個)
2、一個進程要執(zhí)行一個不同的程序。在這種情況下,子進程從fork返回后立即調(diào)用exec(創(chuàng)建了一個全新進程)子進程在fork和exec之間可以更改自己的屬性。例如I/O重定向,用戶ID、信號安排等。
//fork函數(shù)示例 //fork就是分支的起點 //之前是一個進程,遇到fork之后便一分為二,成兩個進程。 #include <unistd.h> #include <stdio.h> #include <errno.h> #include <stdlib.h>int glob = 6 ; char buf[] = "a write to stdout\n" ;int main(int argc, char** argv) {int var ;pid_t pid ;var = 123;if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)perror("write error") ;printf("before fork\n") ;if ((pid = fork()) < 0)perror("fork error") ;else if (pid == 0) //子進程{glob++ ;var++ ;}else //父進程{sleep(3) ; //掛起3秒,讓子進程先運行}//父子進程都有的 相同的程序正文printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var) ;exit(0) ; }【在fork進程時,注意標(biāo)準I/O的緩沖問題】
write函數(shù)不帶緩沖的,但標(biāo)準I/O庫是帶緩沖的。如果標(biāo)準輸出連到終端設(shè)備,則它是行緩沖的(由換行符沖洗),否則它是全緩沖的
?
若把上面程序的輸出重定向到文件:./a.out > test.txt? 則"before fork\n"會輸出兩次
原因是當(dāng)將標(biāo)準輸出重定向到一個文件時,標(biāo)準I/O是全緩沖的。在fork之前調(diào)用了printf一次,但當(dāng)調(diào)用fork時,該行數(shù)據(jù)仍在緩沖區(qū)中,然后將父進程數(shù)據(jù)空間復(fù)制到子進程中時,該緩沖區(qū)也被復(fù)制到子進程中。于是那時父、子進程各自有了帶該行內(nèi)容的標(biāo)準I/O緩沖區(qū)。當(dāng)每個進程終止時,最終會沖洗其緩沖區(qū)中的副本。
?
文件共享
在重定向父進程的標(biāo)準輸出時,子進程的標(biāo)準輸出也被重定向。fork的一個特性是父進程的所有打開文件描述符都被復(fù)制到子進程中。父、子進程的每個相同的打開描述符共享一個文件表項。(因為子進程獲取了父進程文件指針的副本)
這種共享文件的方式使父、子進程對同一文件使用了一個文件偏移量。如果父、子進程寫到同一描述符文件,但又沒有任何形式的同步,那么它們的輸出就會相互混合。
?
進程終止
exit函數(shù)
進程有下面五種正常終止方式:
1、? 執(zhí)行return語句。(這等效于調(diào)用exit)
2、? 調(diào)用exit函數(shù)。(其操作包括調(diào)用各終止處理程序,然后關(guān)閉所有標(biāo)準I/O流等。)
3、? 調(diào)用_exit或_Exit函數(shù)。(立即進入內(nèi)核。此二者為進程提供一種無需運行終止處理程序或信號處理程序而終止的方法。)
4、? 進程的最后一個線程在其啟動例程中返回。
5、? 進程的最后一個線程調(diào)用pthread_exit函數(shù)。
三種異常終止方式:
1、調(diào)用abort。(它產(chǎn)生SIGABRT信號)
2、當(dāng)進程接收到某些信號時。(比如終止信號)
3、最后一個線程對取消請求作出響應(yīng)。
?
【不管進程如何終止,最后都會執(zhí)行內(nèi)核中的同一段代碼。這段代碼為相應(yīng)進程關(guān)閉所有打開描述符,釋放它所使用的存儲空間。】
?
在任意一種情況下,該終止進程的父進程都能用wait或waitpid函數(shù)取得其終止?fàn)顟B(tài)。
?
①?? 若父進程在子進程之前終止,則子進程的父進程都改變?yōu)?span style="font-family:Calibri;">init進程。我們稱之為由init進程領(lǐng)養(yǎng)。(在一個進程終止時,內(nèi)核逐個檢查所有活動進程,看它是否還有活的子進程,如果有,則將它子進程的父進程ID更改為1,即init進程的ID)
②?? 若子進程在父進程之前終止,則當(dāng)父進程調(diào)用wait或waitpid函數(shù)時,可以獲得子進程的終止?fàn)顟B(tài)信息。(內(nèi)核為每個終止子進程保存了一定量的信息)
?
僵死進程:一個已經(jīng)終止,但是其父進程尚未對其進行善后處理(獲取終止子進程的終止?fàn)顟B(tài)信息,釋放它占用的資源)的進程被稱為僵死進程(zombie)。[即:已死,但無人收尸]
由init領(lǐng)養(yǎng)的進程不會變成僵死進程。因為init被編寫成無論何時只要有一個子進程終止,init就會調(diào)用一個wait函數(shù)取得其終止?fàn)顟B(tài)。這也就防止了系統(tǒng)中有很多僵死進程。
(這只能做到父進程先死,子進程不會變僵死進程。若子進程先死,則防止僵死進程的責(zé)任就交給我們了。---內(nèi)核在父進程終止時只檢查其活著的子進程。)
?
wait和waitpid函數(shù)
【當(dāng)一個進程正常或異常終止時,內(nèi)核就向其父進程發(fā)送SIGCHLD信號。】
對于這種信號,系統(tǒng)的默認動作是忽略,當(dāng)然,我們也可以設(shè)置為捕捉,并提供一個信號處理函數(shù)。
#include <sys/wait.h>
pid_t? wait(int *statloc) ;??????????? // statloc為返回的終止?fàn)顟B(tài)存放處
pid_t? waitpid(pid_t pid,? int * statloc,? int options) ;
父進程調(diào)用這兩個函數(shù),只要一有子進程終止,則此函數(shù)就取得該子進程的終止?fàn)顟B(tài)立即返回。否則一直阻塞。(若它沒有任何子進程,則立即出錯返回)
?
這兩個函數(shù)的區(qū)別:
①?? 在一個子進程終止前,wait使其調(diào)用者阻塞,而waitpid則有一個選項,可使調(diào)用者不阻塞。(options設(shè)置為WNOHANG)
②?? wait只獲取在其調(diào)用之后的第一個終止子進程,而waitpid則有參數(shù),可控制它所等待的進程。(pid設(shè)置為不同的值,有不同的含義。)
?
防止僵死進程
若在父進程中調(diào)用waitpid函數(shù),則它只能獲取第一個終止的子進程狀態(tài),其他子進程可能變?yōu)榻┧肋M程。若在調(diào)用waitpid之前就有子進程結(jié)束,則更糟。
若在SIGCHLD的信號處理函數(shù)中調(diào)用waitpid,則效果好一些,但也可能會產(chǎn)生僵死進程。因為若在信號處理函數(shù)執(zhí)行期間,又有多個子進程結(jié)束,發(fā)出SIGCHLD信號,UNIX系統(tǒng)只投遞一次信號。這樣會有子進程的終止?fàn)顟B(tài)得不到獲取。
有效方式1:父進程調(diào)用sigaction函數(shù)綁定信號SIGCHLD的信號處理函數(shù)時,把其選項字段設(shè)置為SA_NOCLDWAIT,則可防止僵死子進程。(子進程終止后,內(nèi)核自動把其終止?fàn)顟B(tài)信息丟棄)父進程可隨時結(jié)束,不必等到所有子進程終止。? ?詳情見UNIX 信號博文
有效方式2:調(diào)用fork兩次以避免僵死進程。
?
//調(diào)用fork兩次,以避免僵死進程。 #include <unistd.h> #include <stdio.h> #include <errno.h> #include <stdlib.h>int main(void) {pid_t pid ;if ((pid = fork()) < 0)perror("fork error") ;else if (pid == 0) //子進程的作用就是創(chuàng)建孫進程,然后把它托付給init進程{if ((pid = fork()) < 0)perror("fork error") ;else if (pid == 0) //以下就是實際做事的 孫進程1代碼段{sleep(2) ; //要讓子進程先運行完 終止//打印出其父進程IDprintf("grandchild 1, parent pid = %d\n", getppid()) ;exit(0) ;}if ((pid = fork()) < 0)perror("fork error") ;else if (pid == 0) //以下就是實際做事的 孫進程2代碼段{sleep(2) ;//打印出其父進程IDprintf("grandchild 2, parent pid = %d\n", getppid()) ;exit(0) ;}//終止自己,這樣init就領(lǐng)養(yǎng)了各孫進程exit(0) ;}//以下是父進程代碼段//父進程需要等待子進程(防止子進程變zombie)但這種等待時間極短(子進程很快便終止了)if (waitpid(pid, NULL, 0) != pid)perror("waitpid error") ;exit(0) ; }?
?
一般的父進程要寫個循環(huán)輪詢wait是否出錯返回(即輪詢所有的子進程是否都已終止),這樣父進程必須在所有子進程終止之后才能終止。
而此法:
父進程只需等待一個子進程結(jié)束(它會很快終止),而實際工作的進程由子進程fork,然后子進程終止 這些孫進程就被init接管了,init可避免它們變?yōu)榻┧肋M程。
但需要注意的是:各孫進程在運行前要sleep一下,以便讓子進程先終止。(若孫進程先終止,則變zombie)
?
執(zhí)行程序
exec函數(shù)族
當(dāng)進程調(diào)用一種exec函數(shù)時,該進程執(zhí)行的程序完全替換為新程序。因為調(diào)用exec并不創(chuàng)建新進程,所以前后的進程ID并未改變。exec只是用一個全新的程序替換了當(dāng)前進程的正文、數(shù)據(jù)、堆和棧段。
?
#include <unistd.h>
int? execl (const char* pathname,? const char* arg0, ………/*(char*)0*/) ;
int? execv (const char* pathname, char* const argv[]) ;
還有execle、execve、execlp、execvp的詳細介紹,略。
函數(shù)execl和execv的區(qū)別與參數(shù)表的傳遞有關(guān)(l表示list,v表示vector)
execl要求將新程序的每個命令行參數(shù)都說明為一個單獨的參數(shù),這種參數(shù)表以空格指針結(jié)尾。
execv則先構(gòu)造一個指向各參數(shù)的指針數(shù)組,然后將該數(shù)組地址作為這個函數(shù)的參數(shù)。
?
system函數(shù)
在程序中執(zhí)行一個命令字符串很方便。
ISO C定義了system函數(shù),但其對操作系統(tǒng)的依賴很強。
#include <stdlib.h>
int system(const char * cmdstring) ;
(其效果相當(dāng)于在控制臺輸入命令,這樣,可以讓我們在程序中用到shell命令)
?
進程時間
時間值(UNIX系統(tǒng)一直使用兩種不同的時間值)
①?? 日歷時間
該值是自1970年1月1日以來國際標(biāo)準時間(UTC)所經(jīng)過的秒數(shù)累計值。這些時間值可以用于記錄文件的最近一次的修改時間等。(其計時粒度較大,以秒為單位)
②?? 進程時間
也被稱為CPU時間,用以度量進程使用的中央處理器資源。進程時間以時鐘滴答計算。(取每秒鐘為50、60或100個滴答。)可用sysconf函數(shù)得到每秒鐘滴答數(shù)。
UNIX使用三個進程時間值:
墻上時鐘時間:它是進程運行的時間總量,其值與系統(tǒng)中同時運行的進程數(shù)有關(guān)。(進程可能被切換,掛起)
用戶CPU時間:它是執(zhí)行用戶指令所用的時間。
系統(tǒng)CPU時間:它是該進程中執(zhí)行內(nèi)核程序所經(jīng)歷的時間。例如read或write。
用戶CPU時間和系統(tǒng)CPU時間之和被稱為CPU時間。(它們都是占用CPU的時間,不包括進程被掛起等待的時間。而墻上時鐘時間進程生命期的所有時間)
?
任一進程都可調(diào)用times函數(shù)以獲得它自己及已終止子進程的上述值。
#include <sys/times.h>
clock_t? times(struct tms * buf) ;
//返回流逝的墻上時鐘時間(單位:時鐘滴答數(shù))此值是相對于過去的某一時刻測量的,所以不能用其絕對值,要用兩個時間點的差值。
times函數(shù)還把用戶CPU時間和系統(tǒng)CPU時間填在了buf指向的結(jié)構(gòu)中。
sysconf(_SC_CLK_TCK)返回每秒時鐘滴答數(shù)。
?
進程同步
可用信號實現(xiàn)(見本博客后續(xù)文章)
可用管道實現(xiàn)(見本博客后續(xù)文章)
?
小結(jié)
進程控制原語
fork 可創(chuàng)建新進程。
exec 可以執(zhí)行新程序。
exit? 處理終止
wait? 等待終止。
?
?
?
轉(zhuǎn)載于:https://www.cnblogs.com/riasky/p/3481695.html
總結(jié)
以上是生活随笔為你收集整理的UNIX/Linux-进程控制(实例入门篇)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ThreadLocal到底有没有内存泄漏
- 下一篇: 31寸Aoc显示器写代码真香!包邮送一台