Linux進程詳解__Linux
來源:互聯網
上載者:User
本文實際上是 "UNIX環境進階編程" 的讀書筆記. 所以許多細節並沒有表述出來, 想要刨根問底的同學建議再看看原書. 之所以把讀書筆記貼到部落格上, 出於兩個目的: 1. 加深自己的學習效果. 2. 提供一個快速探索的方式.
本文提到的技術在下面的環境中實際驗證過: Linux version 2.6.18-164.el5 x86_64 GNU/Linux (gcc version 4.1.2 20080704 (Red Hat 4.1.2-46))
程式和進程
程式是指磁碟上的可執行檔, 核心使用 exec 函數將程式讀入記憶體, 並使其執行. 程式的執行執行個體稱為進程. 一個程式可以啟動若干個進程.
進程標識符
核心使用一個唯一的非負整數來標識每一個進程, 稱為進程ID (process ID). 當進程終止後, pid 就可以再次使用了, (過一段時間後才會被使用).
#include <unistd.h> pid_t getpid();//進程id pid_t getppid();//父進程id
pid 為 0 的進程通常是調度進程, 也被稱為 交換進程(swapper). 它是系統進程. pid 為 1 的進程通常是 init 進程, 在自舉過程結束時由核心調用. 負責啟動系統. 它是一個普通的使用者進程, 以超級使用者提高權限執行.
進程啟動
C程式總是從 main 函數開始執行, main 函數的原型是: int main(int argc, char *argv[]); 核心將啟動進程的命令列參數通過 main 函數的參數傳遞給進程. 此外, 核心還會通過變數 environ 傳遞環境變數. 通常, C程式不直接使用 environ, 而是通過 getenv 函數來取環境變數的值.
參數表和環境表一樣, 都是一個c字串指標數組. 不同的是, 參數表中參數的個數, 是由一個參數表示的. 環境表不提供環境變數的個數, 它把最後一個環境變數指標設為null來表明環境表結束.
一個進程可以調用 fork 函數建立一個新進程. #include <unistd.h> pid_t fork();//子進程返回0, 父進程返回子進程ID, 出錯返回-1. 通常, fork 之後是父進程先執行還是子進程先執行是不確定的.
父子進程共用本文段, 但是不共用資料區段.
fork 函數會讓子進程複製當前父進程的資料區段和堆棧. vfork 函數也能建立一個新進程, 但是它不會複製父進程的資料區段和堆棧. 而且, 父進程調用 vfork 後會被阻塞, 直到子進程調用exec 或 exit;
進程終止
有八種方式使進程終止, 它們是: 1. 從main函數返回. 2. 調用 exit. 3. 調用 _exit 或 _Exit 4. 最後一個線程從其啟動常式返回. 5. 最後一個線程調用 pthread_exit. 6. 調用 abort. 它產生 SIGABRT 訊號. 7. 收到一個訊號並終止. 8. 最後一個線程對取消請求作出相應. 其中 1-5 是正常退出, 6-8 是異常終止.
有三個函數用於正常退出一個進程. #include <stdlib.h> void exit(int status); void _Exit(int status); #include <unistd.h> void _exit(int status);
_exit 和 _Exit 立即進入核心, exit 則先執行一些清理 (調用執行各終止處理常式, 關閉所有標準IO流等), 然後進入核心. status 是進程的終止狀態.
以下幾種情況, 進程的終止狀態是未定義的. 1. 調用這些函數時不帶終止狀態. 2. main 執行了一個無傳回值的 return. 3. main 沒有聲明傳回型別為整形. 如果main 的傳回型別是整形, 並且最後一條語句沒有提供傳回值, 那麼該進程的終止狀態是0. 在 main 函數中 exit(0) 等價於 return 0;
不管進程如何終止, 最後都會執行核心中的同一段代碼. 這段代碼為相應進程關閉所有開啟的描述符, 釋放它使用的寄存器.
子進程的終止
當進程終止時, 核心會向其父進程發送 SIGCHLD 訊號.
這個訊號系統預設是忽略它. 程式員可以為這個訊號註冊一個訊號處理函數來捕捉這個訊號.
父進程可以用 wait 或 waitpid 函數來獲得子進程的終止狀態.
exit, _Exit, _exit 三個函數會把終止狀態傳遞給父進程. 如果進程是異常終止的, 核心會產生一個指明其原因的終止狀態.
#include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); 成功則返回進程id, 出錯返回-1;
調用 wait 或 waitpid 函數時, 可能發生這些情況: 1. 如果其所有子進程都還在運行, 則阻塞. 2. 如果一個子進程已經終止(僵死進程), 返回該子進程的終止狀態. 3. 如果它沒有任何子進程, 立即出錯返回; 如果進程收到 SIGCHLD 訊號後調用 wait , 可期望 wait 會立即返回. 在其它時候, 進程很可能會阻塞.
對於父進程已經終止的進程, 他們的父進程都將改變為 init 進程.
waitid, wait3, wait4 這幾個函數也可以用來擷取子進程的終止狀態.
僵死進程
當一個子進程終止時, 如果它的父進程還在運行, 核心會為這個終止的子進程保留一定量的資訊. 父進程可以根據這些資訊知道子進程的情況. 直到父進程對其進行了善後處理, 子進程才會完全終止. 在這期間, 這個子進程會成為 僵死進程, 它仍然佔用一定資源.
使用者終止處理常式
按照 ISO C 的規定, 一個進程可以登記多達 32 個函數, 這些函數將由 exit 自動調用. 這些函數稱為終止處理常式. 使用 atexit 函數來登記這些函數.
#include <stdlib.h> int atexit(void (*func)(void));//成功返回0, 出錯返回非0
exit 調用這些函數的順序與登記順序相反. 同一個函數如果被登記多次, 也會被調用多次. 要確定一個平台支援的最大終止處理常式數, 可以使用 sysconf 函數.
進程執行和終止流程
在進程中執行新程式
fork 用來建立子進程, exec 函數用來執行另一個程式. 當進程調用exec後, 一個全新的程式替換了當前進程的本文, 資料, 和堆棧段. 新程式從其main函數開始執行. 新程式的 pid 不會變化, 此外, 原進程的其它一些特徵也會被保留.
有六種不同的exec 函數: execl, execv, execle, execve, execlp, execvp;
進程安全
進程的許可權是由啟動進程的使用者和組id決定的, 有時為了安全考慮, 只給以進程最小許可權. 程式可以通過一些函數動態修改進程所屬的使用者和組id, 以限制進程的許可權, 這些函數包括:
#include <unistd.h> int setuid(uid_t uid); int setgid(gid_t gid); int setreuid(uid_t ruid, uid_t euid); int setregid(gid_t rgid, gid_t egid); 成功返回0, 出錯返回-1
uid_t getuid();//使用者id uid_t geteuid();//有效使用者id gid_t getgid();//實際組id gid_t getegid();//有效組id
進程環境
C程式的儲存空間布局: 本文段. 由CPU執行的機器指令組成. 通常, 本文段由多個進程共用, 並且是唯讀. 初始化資料區段 . 通常稱為資料區段, 包含所有在函數外聲明並明確地賦初值的變數. 非初始化資料區段 . 也被稱為bss(由符號開始的塊)段. 在程式開始執行前, 核心將此段中的資料初始化為0或null 指標. 任何在函數外聲明(但是沒有明確賦初值)的變數都存放在非初始化資料區段中. 棧. 自動變數和函數調用時需要儲存的資訊, 都放在棧中. 堆. 在堆中進行動態記憶體分配. 由於曆史慣例, 堆位於非初始化資料區段和棧之間.
除了上面的段, 還有若干其它類型的段. 比如: 包含符號表的段, 包含調試資訊的段, 以及包含動態共用程式庫連結資料表的段等等. 這些部分並不裝載到進程執行的程式映象中.
size 指令可以顯示程式的本文段, 資料區段 和 bss段的長度:
第四和第五列分別以十進位和十六進位表示三個段的總長度.
資源限制
每個進程都有一組資源限制, 其中的一些可以用 getrlimit 和 setrlimit 函數查詢和更改.
更改資源限制時, 需要遵循下列規則: 1. 任何一個進程都可以將一個軟式節流值更改為小於等於其硬限制值. 2. 任何一個進程都可以降低其硬限制值, 但必須大於或等於其軟式節流值. 這種降低對普通使用者是無法復原的. 3. 只有超級使用者進程才可以提高硬限制值. 常量 RLIM_INFINITY 表明無限享.
下面列出了一些資源類型: RLIMIT_AS 進程可用儲存區的最大位元組數. 影響 sbrk 函數 和 mmap 函數. RLIMIT_CORE core檔案的最大位元組數, 為0則阻止建立core檔案.
RLIMIT_CPU CPU時間的最大量(秒), 超過此軟式節流時, 進程會收到 SIGXCPU 訊號.
RLIMIT_DATA 資料區段的最大位元組長度. 包括初始化資料區段, 非初始化資料區段 和 堆的總和.
RLIMIT_FSIZE 建立的檔案的最大位元組長度. 超過此軟式節流時, 進程會收到 SIGXFSZ 訊號.
RLIMIT_LOCKS 可持有的檔案鎖的最大數. 此數包括Linux特有的檔案租借數.
RLIMIT_MEMLOCK 進程使用 mlock 能夠鎖定在寄存器中的最大位元組長度. RLIMIT_NOFILE 能開啟的最大檔案數. 會影響 sysconf 函數在參數 _SC_OPEN_MAX中的傳回值.
RLIMIT_NPROC 每個實際使用者ID可擁有的最大子進程數. 會影響 sysconf 函數在參數 _SC_CHILD_MAX中的傳回值.
RLIMIT_RSS 最大駐記憶體集(RSS)的位元組長度. 如果實體儲存體器供不應求, 核心將從進程取回超過RSS的部分.
RLIMIT_STACK 棧的最大位元組長度.
資源限制會被子進程繼承. 我們通常使用 shell 的 limit 命令在進程啟動前設定資源限制. 以下代碼用來查看進程的資源限制:
#include <iostream> #include <sys/resource.h> #include <assert.h>
#define PRINT_LIMIT(NAME) \ assert(getrlimit(NAME, &limit)==0);\ printf("%s: soft limit = %d, hard limit = %d\n", \ #NAME, limit.rlim_cur, limit.rlim_max);
int main(){ printf("RLIM_INFINITY = %d\n", RLIM_INFINITY); rlimit limit; PRINT_LIMIT(RLIMIT_AS) PRINT_LIMIT(RLIMIT_CORE) PRINT_LIMIT(RLIMIT_CPU) PRINT_LIMIT(RLIMIT_DATA) PRINT_LIMIT(RLIMIT_FSIZE) PRINT_LIMIT(RLIMIT_LOCKS) PRINT_LIMIT(RLIMIT_MEMLOCK) PRINT_LIMIT(RLIMIT_NOFILE) PRINT_LIMIT(RLIMIT_NPROC) PRINT_LIMIT(RLIMIT_RSS) PRINT_LIMIT(RLIMIT_STACK) return 0; }
執行命令字串
ISO C 定義了 system 函數, 可以很方便的執行一個命令字串. #include <stdlib.h> int system(const char *cmdstring);
例如: system("date > file");//把日期存入檔案.
由於system 在其實現中調用了 fork, exec 和 waitpid, 因此有三種傳回值. 1. 如果fork 失敗或者waitpid 返回除 EINTR 之外的錯, 則返回 -1, 並在 errno 中設定錯誤類型值. 2. 如果 exec 失敗(表示不能執行shell), 其傳回值如同shell執行了 exit(127) 一樣. 3. 如果 fork, exec, waitpid 都執行成功, 傳回值是shell 的終止狀態.
進程組
每個進程都屬於某個進程組. 進程組是一個或多個進程的集合. 通常它們與同一作業相關聯, 可以接收來自同一終端的各種訊號. 每個進程組有一個唯一的進程組ID. #include <unistd.h> pid_t getpgrp();//當前進程的進程組id pid_t getpg