Linux操作系統-進程和信號(2)
分類:Linux環境編程
進程的標識
有一些函數可以返回進程的標識符
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); //返回調用進程的進程ID
pid_t getppid(void); //返回調用進程的父進程
uid_t getuid(void); //返回調用進程的實際用戶ID
uid_t geteuid(void); //返回調用進程的有效用戶ID
gid_t getgid(void); //返回調用進程的實際組ID
git_t getegid(void); //返回調用進程的有效組ID
進程的狀態
- 運行狀態:指正在CPU中運行或者就緒的狀態,包括:內核運行態,用戶運行態,就緒態。Linux內核並不對此三種狀態進行區分
- 可中斷睡眠狀態:當進程處於可中斷等待狀態時,系統不會調度該進程執行。當系統產生一箇中斷或者釋放了進程正在等待的資源,或者進程收到一個信號,都可以喚醒進程轉換到就緒狀態
- 不可中斷睡眠狀態:不可中斷,指的不是CPU不響應外部硬件中斷,而是指進程不響應異步信號。處於該狀態的進程不響應信號,只有使用wake_up()函數明確喚醒才能轉換到就緒狀態。該狀態被設計用於保護內核的某些流程不被打斷,或者在進行某些I/O操作時避免進程與設備交互的過程中被打斷,造成設備陷入不可控狀態。
- 暫停狀態:當進程收到信號SIGSTOP,SIGTSTP,SIGTTIN或SIGTTOU時就會進入暫停狀態。可向其發送SIGCONT信號讓進程轉換到可運行狀態。
- 僵死狀態(TASK_ZOMBIE):當進程已停止運行,但其父進程還沒有詢問其狀態時,則該進程處於僵死狀態
常見的狀態字符有:
STAT字符 | 說明 |
---|---|
S | 睡眠。通常是在等待某個事件的發生 |
R | 運行/可運行,即在運行隊列中,處於正在運行或即將運行狀態 |
D | 不可中斷的睡眠(等待,不響應異步信號)。通常是在等待輸入或輸出完成 |
T | 停止 |
Z | 殭屍進程 |
N | 低優先級任務 |
s | 進程是回話期首進程 |
+ | 進程屬於前臺進程組 |
l | 進程是多線程的 |
< | 高優先級任務 |
進程的控制
system函數
在進程中執行另一個程序的一個簡單方法是調用標準庫函數system,原型如下:
#include <stdlib.h>
int system(const char *command);
解釋:system函數運行command命令並等待該命令完成,
本質是(執行“/bin/sh -c command”)。system函數調用成功時返回相應命令的退出狀態碼,如果無法啓動shell則返回127,發生其它錯誤時返回-1
注意:使用system函數並非啓動其它進程的理想手段,因其必須啓動一個shell,再使用shell執行相應的命令。下面有一個小例子:
//command.c
#include <stdio.h>
int main(int argc, char **argv)
{
printf("這條命令是由調用system函數執行的。\n");
return 0;
}
將該段代碼編譯成command可執行文件。
//system.c
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char **argv)
{
printf("接下來將要調用system函數來執行一個名爲command的命令。\n");
system("./command");
printf("Done.\n");
return 0;
}
編譯運行
biantiao@lazybone1994-ThinkPad-E430:~/Linux$ gcc -o system system.c
biantiao@lazybone1994-ThinkPad-E430:~/Linux$ ./system
接下來將要調用system函數來執行一個名爲command的命令。
這條命令是由調用system函數執行的。
Done.
biantiao@lazybone1994-ThinkPad-E430:~/Linux$
exec函數
調用exec系列函數可以執行另外一個程序。這些函數的原型如下:
#include <unistd.h>
extern char **evniron;
int execl(const char *path, const char *arg, ...);
int execlp(cosnt char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, const char *argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[]);
int execve(cosnt char *path, char *const argv[], char *const envp[]);
解釋:當一個進程調用一種exec函數時,該進程將完全由新程序替換當前進程的正文,數據,堆和棧段,所以調用exec前後進程ID並未改變。
exec系列函數各參數和返回值的含義如下
- path:待運行的程序全路徑名
- file:待運行的程序名,通過PATH環境變量搜索其路徑
- arg:命令參數
- …:可選的一到多個命令參數,要求最後一個必須是NULL
- argv:命令參數指針數組
- envp:傳遞給待運行程序的環境變量指針數組
- 返回值:成功時不返回,出錯時返回-1
這幾個函數之間的區別
函數名包含字母p(表示path)的execlp, execvp和execvpe函數取文件名file作爲第一個參數,其它函數則取路徑名path作爲第一個參數。當指定file作爲參數時:如果file中包含/,則就將其視爲路徑名,否則就按PATH環境變量的設定,在相關目錄中搜尋可執行文件。
函數名字中包含字母l的execl,execlp和execle要求將新程序的每個命令行參數都作爲一個單獨的參數,然後在最後一個參數後附加一個空指針參數結尾。
函數名中包含字母v的另外三個函數execv,execvp和execve則要求先構造一個指向各參數的指針數組,數組的最後一個元素也必須是空指針,然後以該數組地址作爲這三個函數的參數。
函數以字母e結尾的三個函數execle,execvpe和execve可以傳遞一個指向環境字符串指針數組的指針,該指針數組也必須以空指針作爲最後一個元素
fork函數
一個現存進程創建一個新進程的唯一方法是調用fork或vfork函數
#include <unistd.h>
pid_t fork(void);
由fork創建的新進程被稱爲子進程。該函數被調用一次,但返回兩次。兩次返回的區別是子進程的返回值是0,而父進程的返回值是子進程的PID。
在調用fork之後,子進程和父進程繼續執行fork之後的指令。
一般來說,在fork之後是父進程先執行還是子進程先執行是不確定的,父進程和子進程是完全獨立運行的。如果父,子進程之間相互同步,則要求採用某種形式的進程之間的通信機制。
子進程是父進程的複製品。例如:子進程獲得父進程數據空間,堆和棧的複製品。注意,這是子進程所擁有的拷貝,父子進程並不共享這些儲存空間。如果正文段是隻讀的,則父,子進程共享正文段。
Linux下的fork函數並不對父進程的數據段,堆和棧進行完全拷貝,而是使用了寫時複製的技術,讓父,子進程共享這些區域,而且內核將它們的存取許可權變爲只讀的。當有進程試圖修改這些區域時,才由內核爲有關部分做一個拷貝。
fork函數有以下兩種用法
- 一個父進程希望複製自己,使父,子進程執行不同的代碼段。這在網絡服務進程中是常見的——父進程等待委託者的服務請求,當請求達到時,父進程調用fork,使子進程處理此請求,父進程則繼續等待下一個服務請求。
- 一個進程要執行一個不同的程序,這對shell是常見的。在這種情況下,子進程在從fork返回後立即調用exec。當然了,子進程在fork和exec之間可以更改自己的屬性。
vfork函數
vfork函數與fork函數最大的一個區別是,vfork函數創建子進程後會阻塞父進程,其原型如下:
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
vfork和fork一樣都創建一個子進程,但是它並不會將父進程的地址空間完全複製到子進程中,因爲子進程會立即調用exec或exit,於是也就不會訪問該地址空間。但在子進程調用exec或exit之前,它在父進程的空間中運行。
vfork和fork之間的另一個區別是:vfork保證子進程先運行,在子進程調用exec或exit之後父進程纔可能被調度運行
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t pid; /* 用於保存PID */
char *message; /* 用於保存消息字符串 */
int n = 2; /* 技術變量 */
printf("fork program starting\n");
pid = vfork(); /* 創建子進程 */
switch (pid){
case -1:
perror("fork failed.\n");
exit(1);
case 0:
message = "This is the child.";
n = 5;
break;
default:
message = "This is the parent.";
n++;
break;
}
for (; n > 0; n--){
puts(message);
sleep(1);
}
exit(0);
}
進程的終止狀態
對於正常終止的情況,傳向exit或 _exit的參數,或main函數的返回值,指明瞭它們的退出狀態,內核以該“退出狀態”作爲進程的“終止狀態”。在異常終止的情況下,內核(不是進程本身)會產生一個指示其異常終止原因的終止狀態(termination status)。終止進程的父進程可使用wait或waitpid函數取得其終止狀態。
如果父進程在子進程之前終止,則它們的父進程都將改爲init進程,這種處理方法保證每一個進程都有父進程。
如果子進程在父進程之前終止,那麼父進程如何得到子進程的終止狀態呢?答案是,當進程終止的時候,內核並未馬上釋放其進程控制塊(PCB),而是在其中保留了一定量的信息,所以當終止進程的父進程調用wait或waitpid時,可以得到這些信息。這些信息至少包括進程ID,該進程的終止狀態,以及該進程使用的CPU時間總量。
一個已經終止,但是其父進程尚未對其進行善後處理(獲取終止子進程的有關信息,釋放它扔佔用的資源)的進程被稱爲殭屍進程(zombie)
wait和waitpid函數
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
wait函數等待任一子進程的結束,waitpid函數則可等待指定的子進程的結束。
在父進程中調用wait或者waitpid可能發生如下情況:
- 阻塞(如果其所有子進程都還在運行)
- 帶子進程的終止狀態立即返回
- 出錯立即返回(如果它沒有子進程)
當一個進程終止時,內核會向其父進程發送SIGCHLD信號。父進程默認處理是忽略該信號,但也可以設置一個信號發生時即被調用的回調函數以捕獲該信號。如果父進程在捕獲SIGCHLD信號的回調函數中調用wait,則可期望wait會立即返回。但是在一個任意時刻調用wait,則進程可能會阻塞