天天看點

Unix環境進階程式設計學習筆記(五) 程序控制fork 函數exec系列函數 程序中的User ID 和 Group ID

fork 函數

fork函數用于建立子程序,先看其聲明方式:

pid_t fork(void);           

該函數如果執行成功,則會傳回兩次,對于父程序,傳回其子程序的ID,對于子程序,傳回0。 程序建立成功後,子程序會拷貝父程序的位址空間,包括資料空間,堆和棧。但這在許多情況下會不必要的耗費很多資源,是以現在的實作一般都采用了一種叫做“copy-on-write (COW)”的技術。在這種技術下,父程序和子程序會共享這片空間,核心會保護這片空間為隻讀模式,隻有當有程序要修改時,才進行部分拷貝工作。 對于Posix标準的線程,fork函數建立的程序隻包含目前的調用線程。在前面的學習筆記中,我們知道,每個程序都會有自己的程序表,在fork以後,程序表會被複制,進而父子程序共享打開的檔案。

Unix環境進階程式設計學習筆記(五) 程式控制fork 函數exec系列函數 程式中的User ID 和 Group ID

除了打開的檔案,以下程序屬性也将被子程序繼承:

• Real user ID, real groupID, effective user ID, effective group ID

• Supplementary group IDs

• Process group ID

• Session ID

• Controlling terminal

• The set-user-ID andset-group-ID flags

• Current workingdirectory

• Root directory

• File mode creation mask

• Signal mask anddispositions

• The close-on-exec flagfor any open file descriptors

• Environment

• Attached shared memorysegments

• Memory mappings

• Resource limits

exec系列函數

在前面的學習筆記中,我們已經讨論過exec系列函數了,今天,我們将看到更多關于它的細節問題。我們知道,exec系列函數将使用一個新的程序鏡像替換掉原先的程序鏡像,而這部分包含代碼段,資料段,堆以及棧。為了更好的說明exec函數之間的關系,我們還是回顧一下這六個聲明:

int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv []);
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp []);
int execlp(const char *filename, const char *arg0,... /* (char *)0 */ );
int execvp(const char *filename, char *const argv []);
           

實際上,這六個函數隻有一個execve屬于系統調用,其它都屬于庫函數的定義,它們之間的關系可見下圖:

Unix環境進階程式設計學習筆記(五) 程式控制fork 函數exec系列函數 程式中的User ID 和 Group ID

對于這六個函數的命名方式,其規律其實很容易看出來,exec是共有部分,而l帶表可變參,與之相對的則是v,代表數組。如果有p,則說明使用了PATH環境變量,其參數則是filename,最後的e則代表替換的程序(實際上并沒有建立程序)将使用envp所指定的環境表。 需要着重解釋的是關于filename與pathname的差別,如果是pathname,則它所代表的是可執行檔案的路徑,而filename則分兩種情況,如果包含'/'字首,則其意義與pathname相同,否則,則隻代表檔案名,尋找該可執行檔案時需要使用PATH環境變量中的值作為查找字首。 每一個打開的檔案描述符都有一個close-on-exec辨別,它預設是沒有被設定的,如果該位被設定,則在使用exec系列函數後,該檔案描述符将被關閉。

許多情況下,我們建立程序的目的隻是為了調用exec系列的函數去加載新的程式,而在這種情況下使用fork就太浪費了,因為我們并不需要使用到父程序的位址空間及相關資源,替代的,我們可以使用vfork函數。該函數和fork的功能基本相同,差別在于,子程序并不會複制父程序的位址空間,在子程序調用_exit系列函數或是exec系列函數之前,子程序将在父程序的位址空間中運作,而在此期間父程序将被阻塞(請注意,一旦子程序調用了_exit系列函數或是exec系列函數之後,父程序将可以繼續執行)。 關于vfork函數,還有一點需要特别注意的是,永遠不要再子程序裡進行main函數的傳回或是調用exit函數,因為子程序和父程序共用位址空間,而exit函數會導緻流的關閉以及調用“出口函數”,這将影響到父程序的狀态。至于main函數的傳回,其效果和調用exit函數是一樣的。 盡管vfork函數并不複制位址空間,但程序表之類的東西它仍然是會複制的,也就是說,雖然調用_exit函數會關閉檔案描述符,但這并不影響到父程序。如果父程序先于子程序結束,那麼子程序将被init程序收養。

程序中的User ID 和 Group ID

在之前的學習筆記中,我已經介紹過關于程序ID的部分知識已經檔案權限中set user id 位和set group id 位的作用。今天将更深入的介紹裡面的一些知識,先來看兩個函數聲明:

int setuid(uid_t uid);
int setgid(gid_t gid);
           

這兩個函數是用來設定程序ID以及組ID的,我們着重介紹setuid函數,後者與其類似,它的執行規則如下: 1. 如果程序擁有超級使用者的權限,則setuid函數将real user ID, effective user ID, 以及 saved set-user-ID都設定為指定ID。

2. 如果程序沒有超級使用者的權限,則除非指定ID與real user ID 或則是 saved set-user-ID相同,否則執行不會成功,當執行成功時, effective user ID被設定為指定ID的值。

當我們執行exec系列函數後,如果set  user id位已被設定,則程序的 effective user ID将被設定為執行檔案的屬主ID,而無論該位是否被設定,saved set-user-ID都将被設定為effective user ID的值(這一點非常重要)。

我們來看一下saved set-user-ID的作用,拿man指令來舉例,因為它将使用到許多普通使用者無法使用的檔案,是以它的set user id 是被設定了的,也就是說在執行它的時候,effective user ID将被設定為man使用者。然後,由于,我們可以在man運作的過程中通過指令執行其他程序(例如shell),這樣是非常危險的,因為它很可能把man的權限傳遞給危險的程序。為了防止這種情況的發生,man在執行完它的初始化後,在等待使用者指令之前,它将調用setuid函數将effective user ID設定回real id(saved set-user-ID不變,依然是man),而在需要再次通路man所需要的檔案時,我們再通過setuid提高權限,将effective user ID設定回man(因為saved set-user-ID是man,是以操作合法),是以,我們可以看到man在這裡就起着一個記錄儲存的作用,可以友善安全的進行權限的提高和降低。

繼續閱讀