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