UNIX高级环境编程(9)进程控制(Process Control)- fork,vfork,僵尸进程,wait和waitpid...
本章包含內容有:
- 創建新進程
- 程序執行(program execution)
- 進程終止(process termination)
- 進程的各種ID
?
1 進程標識符(Process Identifiers)
每個進程都有一個唯一的標識符,進程ID(process ID)。
進程的ID是可重用的,如果一個進程被終止,那么它的進程ID會被系統回收,但是會延遲使用,防止該進程ID標識的新進程被誤認為是以前的進程。
三個特殊ID的進程:
- Process ID 0:調度者進程,內核進程。
- Process ID 1:init進程,內核引導程序最后啟動,負責啟動Unix系統。對應系統文件/sbin/init。
- Process ID 2:pagedaemon,負責虛擬內存的頁管理。
獲取進程各種ID的相關函數:
函數聲明:
#include <unistd.h>
pid_t getpid(void); ? ? // Returns: process ID of calling process
pid_t getppid(void); ? ? ? ?// Returns: parent process ID of calling process
uid_t getuid(void); ? ? ? ?// Returns: real user ID of calling process
uid_t geteuid(void); ? ? ? // Returns: effective user ID of calling process
gid_t getgid(void); ? ? ? ?// Returns: real group ID of calling process
gid_t getegid(void); ? ? ? ?// Returns: effective group ID of calling process
這里的各種ID在前面第三篇中有說明,http://www.cnblogs.com/suzhou/p/4295535.html
?
2 fork函數
fork函數用于一個已存在的進程創建一個新的進程。
函數聲明:
#include <unistd.h>
pid_t fork(void);
函數細節:
Example:
#include "apue.h"
?
int ? ? globvar = 6;? ? ? ? /* external variable in initialized data */
char? ? buf[] = "a write to stdout\n";
?
int
main(void)
{
? ? int ? ? var;? ? ? ? /* automatic variable on the stack */
? ? pid_t ? pid;
?
? ? var = 88;
? ? if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
? ? ? ? err_sys("write error");
? ? printf("before fork\n");? ? /* we don't flush stdout */
?
? ? if ((pid = fork()) < 0) {
? ? ? ? err_sys("fork error");
? ? } else if (pid == 0) {? ? ? /* child */
? ? ? ? globvar++;? ? ? ? ? ? ? /* modify variables */
? ? ? ? var++;
? ? } else {
? ? ? ? sleep(2); ? ? ? ? ? ? ? /* parent */
? ? }
?
? ? printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
? ? ? var);
? ? exit(0);
}
執行結果:
pid為12291的進程為子進程,對變量glob和var進行了加1。
當把輸出重定向到一個文件時,我們發現結果和直接輸出到終端中不太一樣:
原因:?
- 函數write不使用緩存,所以在系統調用fork之前調用write,結果直接輸出到標準輸出上;
- 而標準輸出如果連接到終端,則是行緩沖(line buffered),否則是全緩沖(full buffered);
- 在第一個例子中,換行導致printf寫入到標準輸出中的數據flush到終端上(行緩沖,換新行,導致前面一行被打印);
- 在第二個例子中,我們將標準輸出重定向到文件,則使用全緩沖,printf的數據被緩存在buffer中沒有被打印,在fork時,buffer同樣被拷貝了一份,這樣父子進程都有了一個標準IO緩存(standard IO buffer);
- 程序中的第二個printf將新的內從append到buffer中已有數據的后面,一同打印出,就看到了第二個例子中打印的結果。
?
文件共享(File Sharing)
當調用fork函數時,父進程的所有打開的文件描述符都會復制一份到子進程中,包括文件偏移量(file offset)。
所以當父子進程同時寫文件時,他們的操作都會更新同一個文件偏移量(file offset),加入子進程向文件中寫入了一部分數據,同時更新了file offset,那么父進程進行寫入操作時,會使用跟新以后的offset,從而避免了覆蓋了子進程寫入的數據。
父子進程共享文件如下圖所示:
我們可以發現,父子進程擁有相同的文件描述符,又沒有其他的同步方式,所以他們的輸出可能會混起來(intermixed)。
fork之后,常見的處理父子進程擁有的文件描述符有兩種方式:
- 父進程等待子進程完成。
- 父子進程各自工作,關閉不需要的文件描述符。
除了打開的文件描述,其他的子進程會繼承自父進程的內容包括:
父子進程不同的地方包括:
- fork的返回值不同
- 進程ID不同
- 進程的父進程ID不同
- 子進程的tms_utime, tms_stime, itms_cutime和itms_cstime值被置為0
- 父進程的文件鎖不會被子進程繼承
- 子進程的pending signals被置空
?
3 vfork
vfork和fork有相同的返回值。
vfork和fork的不同點:
- 函數目的:vfork創建的子進程是為了讓子進程執行一個新的程序
- 復制操作:不復制父進程的地址空間,而是直接運行在父進程的地址空間中,直到子進程調用exec或者exit
- 效率:所以vfork的執行效率比fork要高,因為它沒有copy操作
- 不確定的結果:但是如果子進程修改了數據、調用函數或者沒有調用exec和exit方法,則會造成不確定的結果
- 子進程先運行:vfork保證子進程先運行
?Example:
#include "apue.h"
?
int ? ? globvar = 6;? ? ? ? /* external variable in initialized data */
?
int
main(void)
{
? ? int ? ? var;? ? ? ? /* automatic variable on the stack */
? ? pid_t ? pid;
?
? ? var = 88;
? ? printf("before vfork\n"); ? /* we don't flush stdio */
? ? if ((pid = vfork()) < 0) {
? ? ? ? err_sys("vfork error");
? ? } else if (pid == 0) {? ? ? /* child */
? ? ? ? globvar++;? ? ? ? ? ? ? /* modify parent's variables */
? ? ? ? var++;
? ? ? ? _exit(0); ? ? ? ? ? ? ? /* child terminates */
? ? }
?
? ? /* parent continues here */
? ? printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
? ? ? var);
?
? ? exit(0);
}
運行結果:
?
4 進程退出和僵尸進程
正常退出:三個函數exit,?
如果子進程不正常退出,則內核保證記錄該進程的異常退出狀態,該進程的父進程可以通過調用wait或者waitpid函數獲取該子進程的異常退出狀態。
如果父進程在子進程之前終止,則init進程成為該子進程的父進程。從而保證每個進程都有父進程。
如果子進程先終止(異常終止或者正常退出),內核會保存該子進程的部分信息,包括進程pid,進程終止時的狀態和該進程占用的CPU時間,同時內核會清除該進程占用的內存,關閉所有已經打開的文件描述符。父進程可以通過檢查該信息獲取子進程的終止情況。
如果子進程先終止,而沒有父進程調用waitpid獲取該子進程的信息,那么這種進程被成為僵尸進程。使用ps命令可以看到僵尸進程的相關信息。
如果父進程為init進程,那么子進程異常終止并不會成為僵尸進程,因為init進程會對它的所有子進程調用wait函數獲取子進程的終止狀態。
?
5 wait和waitpid函數
子進程終止,內核會向父進程發送SIGCHLD信號。父進程默認的行為是忽略該信號,父進程也可以設置一個信號處理函數,當捕捉到該信號時,調用該處理函數,在后面的相關章節會介紹信號相關的概念。
本節介紹的wait和waitpid函數的作用是:
- 如果子進程在運行,則阻塞;
- 如果子進程終止,并且子進程的終止狀態被父進程獲取,則該函數立刻返回該終止狀態;
- 如果該進程沒有任何子進程,則返回錯誤。
需要注意的一點是,如果我們在接收到SIGCHLD信號后,調用wait函數,則該函數會立刻返回。在其他情況下調用wait函數,則會阻塞。
函數聲明:
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);?
// Both return: process ID if OK, 0,or -1 on error
兩個函數之間的區別:
- wait函數會阻塞,一直到一個子進程終止;waitpid函數的參數options可以指定不阻塞;
- waitpid函數可以選擇不阻塞,并且可以指定等待某一個子進程終止。
函數細節:
- 如果一個子進程終止并成為了僵尸進程,wait函數立刻返回該子進程的狀態;
- 如果一個進程調用wait()函數并阻塞,并且有多個子進程,則當有一個子進程終止時,wait()函數返回;
- 參數statloc是一個整型指針,如果該參數不為null,則子進程的終止狀態被保存在該參數指向的整型中;如果我們不關心進程的終止狀態,statloc傳入null就行;
返回值檢查:
使用四個宏來檢查wait和waitpid函數來獲取子進程的終止狀態(terminated status),如退出狀態,信號值等信息。
四個宏的具體說明見下表所示:
pid的取值對waitpid函數行為的影響:
- pid == -1:行為和wait相同,等待任意一個子進程終止
- pid > 0:等待進程號為pid的進程終止
- pid ==0:等待進程組號和調用進程的進程組號相同的任意一個子進程終止
- pid < -1:等待進程組號等于pid的任意一個子進程終止
參數option的取值:
waitpid函數提供了三個wait沒有的特性:
- waitpid可以讓我們等待某一個特定的進程;
- waitpid提供了不阻塞版本的wait函數;
- option參數WCONTINUED和WUNTRACED為系統的任務控制(job control)提供了支持。
?
Example:
#include "apue.h"
#include <sys/wait.h>
?
int
main(void)
{
? ? pid_t ? pid;
?
? ? if ((pid = fork()) < 0) {
? ? ? ? err_sys("fork error");
? ? } else if (pid == 0) {? ? ? /* first child */
? ? ? ? if ((pid = fork()) < 0)
? ? ? ? ? ? err_sys("fork error");
? ? ? ? else if (pid > 0)
? ? ? ? {
? ? ? ? ? ? exit(0);? ? /* parent from second fork == first child */
? ? ? ? }
?
? ? ? ? /*
?? ? ? ? * We're the second child; our parent becomes init as soon
?? ? ? ? * as our real parent calls exit() in the statement above.
?? ? ? ? * Here's where we'd continue executing, knowing that when
?? ? ? ? * we're done, init will reap our status.
? ? ? ? ?*/
? ? ? ? sleep(2);
? ? ? ? printf("second child, parent pid = %ld\n", (long)getppid());
? ? ? ? exit(0);
? ? }
?
? ? if (waitpid(pid, NULL, 0) != pid) ? /* wait for first child */
? ? ? ? err_sys("waitpid error");
?
? ? /*
?? ? * We're the parent (the original process); we continue executing,
?? ? * knowing that we're not the parent of the second child.
?? ? */
? ? exit(0);
}
執行結果:
結果分析:
在這里我們fork了兩次,原因是,當我們想fork一個子進程出來,而我們不希望父進程阻塞在wait函數,并且不希望由于父進程沒有調用wait函數先退出導致子進程成為僵尸進程,那么fork兩次,并且退出第一個子進程,可以使得父進程及時退出,并且第二個子進程的父進程變成init進程。
?
小結
本篇主要介紹了fork、vfork、僵尸進程、wait和waitpid函數,這些在unix環境中都是很重要的概念和函數,并且在面試中也經常問到。
下一篇的內容包括:
- 解釋器文件(interpreter files)
- 系統調用(system function)
?
參考資料:
《Advanced Programming in the UNIX Envinronment 3rd》
?
轉載于:https://www.cnblogs.com/suzhou/p/4348978.html
總結
以上是生活随笔為你收集整理的UNIX高级环境编程(9)进程控制(Process Control)- fork,vfork,僵尸进程,wait和waitpid...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【读书笔记-数据挖掘概念与技术】分类:高
- 下一篇: grep命令參数及使用方法