Beyond the Void
BYVoid
Linux C語言編程學習筆記 (1)進程控制入門
本文正體字版由OpenCC轉換

想進行Linux系統開發已經很久了,一直沒有付諸實踐。今日終於開始學習Linux下的C語言編程,研究一天,終於大概弄明白了Linux系統進程管理的一些基本概念和編程方法,總結下來以方便大家學習和自己實踐。

進程系統

Linux是個多任務多用戶的操作系統,系統直接管理的每個任務的最小單位,就是進程(process)。每個進程都有一個惟一的標識符pid,不同的進程pid不相同,在Shell下輸入ps -A,可以顯示當前的所有進程。一個進程不代表一個應用程序(application),因爲一個應用程序可能對應多個進程,也不代表一個可執行文件(executable file),因爲一些可執行文件可以被同時運行多個,它們是互不相干的。

在Linux中,進程不是相互獨立的,每個進程(除了init進程)都有一個父進程(parent process),同時每個進程可以有0個1個或多個子進程(child process)。換句話說,Linux的進程是一個樹形結構,在Shell下輸入pstree可以查看這個樹的形狀。下圖爲pstree返回結果的一部分。

init─┬─NetworkManager─┬─dhclient
│                └─{NetworkManager}
├─SystemToolsBack
├─avahi-daemon───avahi-daemon
├─bonobo-activati───{bonobo-activati}
├─console-kit-dae───63*[{console-kit-dae}]
├─hald───hald-runner─┬─hald-addon-acpi
│                    ├─hald-addon-cpuf
├─pulseaudio─┬─gconf-helper
│            └─2*[{pulseaudio}]
├─rsyslogd───2*[{rsyslogd}]
├─seahorse-daemon
├─telepathy-gabbl
├─telepathy-haze─┬─telepathy-haze
│                └─{telepathy-haze}
├─trashapplet
└─wpa_supplicant

在C語言中,獲得當前進程的pid的函數是pid_t getpid(void);,獲得當前進程的父進程的pid的函數是pid_t getppid(void);,兩者都在unistd.h中聲明。

用戶和權限

因爲Linux是多用戶的系統,所以內核中有着強大的用戶控制,因此每個進程還有一個所有者,即實際用戶ID(uid)。系統uid是一個整數,不同於用戶名。默認情況下進程的uid繼承於父進程。例如我用所有者爲byvoid(uid爲1000)的bash終端啓動了一個進程,那麼這個進程的uid也是1000。用戶uid可以通過uid_t getuid(void);函數獲得。如果權限滿足,程序在運行時可以修改uid,C語言函數爲int setuid(uid_t uid);,如果成功執行返回0,否則返回-1。只有具有root用戶權限的進程可以設置uid。

除此以外,進程還有一個有效用戶ID(euid)。euid是決定進程文件系統權限的身份,一般情況下進程euid和uid是相同的。在C語言中可以通過uid_t geteuid(void);函數獲得進程euid。同樣euid也可以修改,函數爲int seteuid(uid_t uid);僅噹噹前uid和euid中至少有一個爲0(root)時,纔可以設置euid。有一種特殊情況,就是一個二進制可執行文件所有者爲root,並且被chmod +s後,在一般用戶身份下執行,這時產生的進程uid爲一般用戶,而euid爲0(root),這種情況下該進程具有和root一樣高的權限。

進程生成

fork函數

Linux允許用戶創建用戶進程的子進程,在C語言中通過pid_t fork(void);函數實現。fork函數的基本功能是生成一個子進程,並複製當前進程的數據段和堆棧段,子進程和父進程共用代碼段。因爲複製了堆棧段,所以父進程和子進程都停留在fork函數的棧幀中,fork函數要返回兩次,一次在父進程中返回,一次在子進程中返回,但是兩次的返回值是不一樣的。在父進程中,fork函數返回值爲子進程的pid(如果成功調用的話),在子進程中,fork函數的返回值爲0。因此可以根據返回值的不同確定程序的運行流程。父進程和子進程默認情況下是同步執行的,由系統內核調度,哪個先執行是未知的。因爲父子進程的數據段和堆棧段都是獨立的,所以兩者互不干涉,各行其是,內存不能直接共享。

執行程序

Linux中要執行一個外部程序,必須生成一個子進程,因爲內核執行程序的命令exec會替換掉當前進程的地址空間的所有內容並繼續執行,執行另一個程序意味着當前程序不再執行。在C語言中,並沒有exec這樣的一個函數,而是有下列一組函數。

int execl (const char * file,const char * arg,...);
int execlp(const char * file,const char * arg,...);
int execle(const char * file,const char * arg,...,NULL,char * const envp[ ]);
int execv (const char * file, char * const argv[ ]);
int execvp(const char *file ,char * const argv []);
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

其中以execl開頭的函數,第一個參數file爲可執行文件名,接下來有若干個參數,分別爲傳入的argv[0],argv[1],argv[2],…,最有以NULL結束。如果file參數爲路徑名(其中包含’/’),execl函數會直接定位到文件並執行,否則僅在當前目錄下尋找文件,而execlp函數遇到文件名則會按照環境變量PATH的順序尋找。execle最後一個參數爲二維字符數組,表示傳遞給程序新的環境變量列表。execv,execvp,execve和前三者用法相似,只不過不以可變參數列表的方式傳遞參數,換以二維字符數組。上述函數執行失敗後會返回-1,如果執行成功的話將會不返回,因爲代碼段已經被新的可執行程序替換。

進程阻塞

wait函數

在實際的應用中,有時候需要讓父進程停下來等待子進程的執行完畢,這時候就需要進行進程阻塞(process blocking)。C語言中使用pid_t wait(int *statloc)函數可以得到子進程的結束信息。調用wait函數的進程會阻塞,直到該進程的任意一個子進程結束,wait函數會返回結束的子進程的pid,結束信息保存在statloc指針指向的內存區域。如果該進程沒有活動的子進程,那麼立即出錯並返回-1。如果statloc指針爲NULL,那麼表示不關心進程結束的狀態。如果有多個子進程,wait函數返回哪個數不確定的,需要通過pid來判斷。

如果我們需要等待特定的一個進程,可以使用pid_t waitpid(pid_t pid,int *statloc,int options)函數。waitpid函數的第一個參數指定了要等待的進程pid,並且有更多的選項。

殭屍進程

當一個子進程退出時,如果沒有被父進程通過wait取得狀態信息,這些信息會一直保留在內核內存中,子進程的pid也不會被消除,直到父進程退出,這時候這些子進程被稱爲殭屍進程(zombie process)。雖然殭屍進程只佔用很少的一點內存,但如果是長期運行的服務器,積累大量的殭屍進程會導致系統進程表被塞滿,以至於無法創建新的進程。產生一個殭屍進程很容易,只需要讓子進程先於父進程退出即可,在父進程退出之前,子進程將會成爲殭屍進程。

孤兒進程

與殭屍進程相反,如果父進程沒有阻塞並先於子進程退出,那麼子進程將會成爲孤兒進程(orphan process)。Linux系統中init進程負責領養所有孤兒進程,也就是說,孤兒進程的父進程會被設爲init進程。init進程作爲系統守護進程(daemon process),會不斷調用wait函數等待領養的孤兒進程退出,不會產生殭屍進程。

利用孤兒進程避免殭屍進程

許多時候我們不能讓父進程阻塞下來等待子進程處理完以後再繼續,例如在多用戶的服務器程序上。這時如果讓子進程處理事務,就會產生大量殭屍進程。避免殭屍進程出現的一個經典方法就是利用孤兒進程,具體方法爲首先用父進程產生一個子進程,然後讓子進程立刻產生一個孫子進程,用孫子進程來處理事務。同時父進程阻塞等待,並讓子進程則立刻退出。這時候由於子進程已經退出,孫子進程就變成了孤兒進程,被init領養。而子進程立刻退出後,父進程收到信號並正確銷燬了子進程,相比之下父進程只阻塞了可以忽略不計的一瞬間。下面程序是一個例子避免殭屍進程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
	if(fork() == 0) //啓動一個子進程
	{
		printf("the child\n");
		if(fork() == 0) //啓動一個孫子進程
		{
			printf("do something you want\n");
			sleep(5);
			printf("done\n");
			exit(0);
		}
		else //子進程立刻退出
			exit(0);
	}
	else
	{ //父進程立即阻塞
		wait(NULL);
		printf("the parent\n");
		sleep(10);
	}
	return 0;
}

BYVoid 原創作品


上次修改時間 2017-05-22

相關日誌