APUE學習:進程控制

進程標識符

進程標識符在系統中是唯一的。Unix採用延遲技術來分配pid。因爲,如果一個進程終止了,馬上把他的pid分配給新的進程,而且這個新進程與舊進程要做一樣的事。那麼別人就不知道這是新進程還是舊進程在做了。(類似於tcp4次揮手的最後一步。)
還有一些特殊的進程,例如pid爲0的進程,這個可以看成是一個內核的一部分,他主要用來調度。還有pid爲1的進程,這個進程就是一個普通的進程了,但是擁有root權限,主要用來當做daemon。1號進程是內核加載完畢之後啓動第一個進程,他可以用來設置用戶的啓動環境,啓動內核模塊等等。而且他是所有進程的父進程(除了0號進程)

fork函數

fork

fork之後,子進程會複製父進程的data space 與statck,但是現代實現中採用copy-on-write,即子進程在改變的時候纔會去擁有自己的進程空間。

注意點:
對於書上Figure 8.1
注意到這兩個輸出的不同:
1.第一個只是輸出了一個 before fork,而第二個輸出了兩個 before fork。
這是因爲在第一個中,我們將標準I/O連接到了終端中,這樣標準I/O就是一個行緩衝。所以在調用printf時,會自動刷新緩衝,這樣在進程中就沒有這個數據了。但是在第二個中,標準I/O被重定向到了文件中,這樣標準I/O就是全緩衝了,也就是說除非緩衝區滿,或者調用fflush,否則我們是不會刷新緩衝區的。之後我們調用fork創建子進程,子進程會複製父進程的data space ,這樣緩衝區中的數據也被複制到子進程中,在子進程退出時因爲會自動調用fflush,所以會再次刷新緩衝,造成再次輸出before fork
2.注意到write的輸出在這兩個中都只出現了一次。
這是因爲write是不帶緩衝的,所以不管寫到哪都會只寫一次。

思考:1.可以在子進程中調用_exit,這樣就不會清空緩衝。
2.printf中不寫\n,這樣即使是連接到終端也會輸出兩個before fork

文件共享

在fork之後,子進程與父進程會共享打開文件表。如圖:
這裏寫圖片描述

說明:
因爲子進程會複製父進程的文件描述符,這樣操作不當會造成輸出的混亂。所以處理文件描述的方法一般有兩個:
1.父進程等待子進程完成。注意子進程會影響到文件偏移。
2.父子進程執行不同的程序段。例如子進程fork之後調用exec。回憶文件描述符的flag中可能會有O_CLOSEXEC,這樣子進程exec之後,對應的文件描述符就被關閉了。這樣父子進程就會使用不衝突的文件描述符。

使用fork的兩種情況

1.父子進程執行不同的代碼段
2.子進程要執行不同的程序,可以通過fork之後調用exec來讓新程序執行

vfork函數

vfork是爲了簡化fork之後調用exec的步驟。vfork保證了子進程先進行,直到子進程執行了exit或者exec。vfork實現中,子進程會借用父進程的地址空間,也就是說如果在子進程中改變一些值,會影響到父進程中的值。所以一般vfork是爲了spawn用的,即fork-exec。

exit函數

不管如何退出,kernel最終都會執行相同的代碼段,即關閉文件描述符,是否內存空間等等。
在一個正常的退出中,是內核,而不是進程來保障終止狀態。

wait和waitpid

#include <sys/wait.h>

pid_t wait(int *statloc);

pid_t waitpid(pid_t pid, int *statloc, int options);

//兩個函數返回值:若成功則返回進程ID,若出錯則返回-1
  • 在一個子進程終止前,wait使其調用者阻塞,而waitpid有一個選項,可使調用者不阻塞。
  • 如果子進程已經終止,並且是一個殭屍進程,則wait立即返回並取得該子進程的狀態。
  • waitpid並不等待在其調用之後的第一個終止的子進程。
  • statloc不是NULL,則終止進程的終止狀態就存放在它所指向的單元內。如果不關心終止狀態,可以把stataloc置爲NULL。
  • 可以使用宏來知道statloc所表示的意思。
  • wait函數會阻塞,直到子進程結束
  • waitpid會等待指定的進程結束
  • 注意如果子進程已經是殭屍進程,那麼 wait會馬上返回。

這裏寫圖片描述

  • 對於waitpid中的pid:
    pid == -1,等待任一子進程
    pid > 0, 等待其進程ID與pid相等的子進程
    pid==0, 等待其組ID等於調用進程組ID的任一子進程
    pid < -1 等待其組ID等於pid絕對值的任一子進程

waitpid的option參數

這裏寫圖片描述

waitid

#include <sys/wait.h>

int waitid(idtype_t idtype, id_t id, 
           siginfor_t *infop, int options);

waitid類似於waitpid
idtype:指出要等待的類型,
id:進程id
infop:有更一步的信息
options:

競爭條件

競爭條件:當有多個進程嘗試使用共享的數據做些事情,並且最後結果依賴於這些進程的執行順序。

exec函數族

exec函數族,主要用於在父進程fork之後,調用exec來執行另一個程序,新的程序從它自己的main函數出開始執行。調用exec並不會改變子進程的pID,exec函數僅僅改變了子進程的text,data, heap and stack。
這裏寫圖片描述

  • 最終都會調用execve()函數。

說明:

  • 對於含有filename的exec函數,
    1.如果filename包含一個絕對路徑,那麼就把文件中的內容當做一個pathname
    2.否則就在PATH環境變量中找可執行文件
  • 如果execlp or execvp函數在PATH環境變量中找到了filename,但是filename不是一個可執行文件,那麼就加上他是一個腳本文件,傳遞給/bin/sh
  • 這七個函數的一個區別在與argument採用list還是vector。注意採用list,那麼就要在最有一個argument後面加上(char *) 0表示結束。
  • 如果父進程設置了FD_CLOEXEC,那麼在子進程中調用exec就會關閉這個文件。否則還可以使用這個文件。
  • 對於打開的directory streams,在調用exec後就要關閉
  • 在執行exec後,進程的ruid, rgid是不會變的,但是euid與egid根據exec所調用的那個程序是否設置了set-uid與set-gid有關。如果exec的那個程序文件set-uid,那麼exec後的進程的euid就是該程序文件的所有者,否則euid還是原來的euid。
    對於set-gid是同理。

改變uid與gid

setuid,setgid

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

說明:

  • setuid與setgid的使用條件:
    1.如果當前進程有root權限,那麼就把ruid,euid,suid設置爲uid
    2.如果當前進程沒有root權限,但是uid等於當前進程的ruid,suid中的一個,那麼setuid就將euid設置爲uid
    3.否則設置errno爲EPERM,並返回-1

  • 1.只有root權限的進程可以改變ruid。通常ruid在登錄的時候由登錄程序設定好了,並且一般不會改變。
    2.當程序文件的set uid位被設置了,那麼在用exec函數之後,euid就會被設置爲程序文件的所有者id。如果set uid位沒有被設置,那麼exec函數就不會去懂euid的值。
    3.對於suid,通常是有exec程序由euid複製得到的。如果程序文件的set uid位被設置了,那麼suid就會被複製成euid的值,即程序文件所有者的id。否則就是ruid

  • 改變3個用戶id的辦法
    這裏寫圖片描述

seteuid, setegid

int seteuid(uid_t uid);
int setegid(gid_t gid);

說明:
對於非root權限進程可以設置他的euid爲ruid或者suid。
對於root權限進程只是設置euid爲uid

解釋器文件

注意使用exec函數一個解釋器文件的情況
在exec一個解釋器文件的時候,exec的arg[0]會被解釋器文件的路徑名替代,並且exec的開始的參數會變爲解釋器文件中的那一行,然後exec中原有的參數會一次右移幾位。
解釋器文件主要用於腳本中。

system函數

使用system的優勢在於他處理了所有的錯誤,並且所有的信號處理。

說明:
對於Figure 8.25的理解:
注意理解,這裏我們的用戶shell的ruid與euid都是205,然後運行tsys,這時因爲tsys設置了set uid,所以此時我們進程的euid變爲了0,然後又去調用了system函數,而system函數又使用了printuids,在printuids我們的權限是ruid = 205, euid = 0。但是我們執行printuids並不用這麼高的權限,我們只需要205的euid權限就可以執行了。
所以,在使用system要注意的是,不要去在一個設置了set uid的程序中調用system,這樣會造成system調用的那個函數也具有比較高的權限。這種情況要使用fork與exec,即fork之後,使用setXXX等函數降低權限,再去exec。

進程調度

nice函數

int nice(int incr);

說明:
只有root權限的進程可以root來提高自己的優先級,其他權限的用戶只能用來降低自己的優先級。

進程時間

這裏寫圖片描述
times函數獲得進程的相關時間。注意times返回的就是牆上時鐘時間,所以tms結構體中沒有牆上時鐘時間。

說明:
三種時間:

  • 時鐘時間(牆上時鐘時間wall clock time):從進程從開始運行到結束,時鐘走過的時間,這其中包含了進程在阻塞和等待狀態的時間。
  • 用戶CPU時間:就是用戶的進程獲得了CPU資源以後,在用戶態執行的時間。
  • 系統CPU時間:用戶進程獲得了CPU資源以後,在內核態的執行時間。

進程的三種狀態爲阻塞、就緒、運行。
時鐘時間 = 阻塞時間 + 就緒時間 +運行時間
用戶CPU時間 = 運行狀態下用戶空間的時間
系統CPU時間 = 運行狀態下系統空間的時間。
用戶CPU時間+系統CPU時間=運行時間。

總結

Linux中進程權限這一塊的主要思想是:最小權限思想。也就是說我要幹一件事,那麼要用能做成這件事的最小權限去做。所以有setuid,seteuid函數,就是讓我們在fork程序之後,調用這些函數,給子進程適當的權限,然後再去exec。這也是system的一個缺陷,如果在一個設置了setuid的程序中調用system,我們是不可以給system調用的那個函數以適當的權限去做事的。
綜上:記住Linux中文件的權限,進程的權限以及最小權限思想。同時記住Linux中一切皆文件的思想。

思考題

1.在一個function中調用vfork,並且返回。
首先在一個function中使用vfork之後,產生一個子進程,這個子進程共享了父進程的stack,包括函數調用棧。由於vfork首先執行子進程,所以會返回子進程的那個值,然後父進程返回時由於子進程已經把調用棧返回了,會出現segment fault錯誤。
還有一種情況,子進程可以將stack空間都寫上自己的值,然後從function中返回,這是main函數棧後面的數據都被改寫,也就是說父進程想要在function調用後返回,但是這個時候棧空間被子進程重寫了,他就找不到要回到哪裏去了,有可能跑到別的地方去了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章