目錄
六、進程管理
1.程序和進程
- 程序
- 程序(program)是存放在磁盤文件中的可執行文件。
- 進程和進程ID
- 程序的執行實例被稱爲進程(program)。
- 每個linux進程都一定有一個唯一的數字標識符,稱爲進程ID(process ID)。進程ID總是一非負整數。
2、linux下的進程結構
- linux系統時一個多進程的系統,進程之間具有並行性、互不干擾的特點。
- linux中進程包含PCB(進程控制塊)、程序以及程序鎖操縱的數據結構集,可分爲代碼段、數據段、堆棧段。
3、進程狀態
參數 | 描述 |
---|---|
R | 運行狀態(TASK_RUNNING) |
S | 可中斷睡眠狀態(TASK_INTERRUPTIBLE) |
D | 不可中斷睡眠狀態(TASK_UNINTERRUPTIBLE) |
T | 暫停狀態(TASK_STOPPED或TASK_TRACED) |
Z | 僵死狀態(TASK_ZOMBLE) |
X | 退出狀態(TASK_DEAD) |
4、進程狀態轉換圖
5、init進程
- 進程ID爲1通常是init進程,在自舉過程結束時由內核調用
- init進程絕不會終止
- 它是一個普通的用戶進程(與交換進程不同,它不是內核中的系統進程),但是它以超級用戶特權運行。
6、獲取進程標識
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); //返回調用進程的進程ID
pid_t getppid(void); //返回:調用進程的父進程I D
uid_t getuid(void); //返回:調用進程的實際用戶I D
uid_t geteuid(void); //返回:調用進程的有效用戶I D
gid_t getgid(void); //返回:調用進程的實際組I D
gid_t getegid(void); //返回:調用進程的有效組I D
7、fork系統調用
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
//返回:子進程中爲0,父進程中爲子進程ID,出錯-1
-
由fork創建的新進程被稱爲子進程(child process)。
-
該函數被調用一次但返回兩次。兩次返回的區別是子進程的返回值是0,而父進程的返回值則是子進程的進程ID。
-
一般來說,在fork之後是父進程先執行還是子進程先執行是不確定的。這取決於內核所使用的調度算法。
-
使用fork函數得到的子進程從父進程的繼承了整個進程的地址空間,包括:
- 進程上下文、進程堆棧、內存信息、打開的文件描述符、信號控制設置、進程優先級、進程組號、當前工作目錄、根目錄、資源限制、控制終端等。
-
子進程與父進程的區別在於:
- 父進程設置的鎖,子進程不繼承
- 各自的進程ID和父進程ID不同
- 子進程的未決告警被清除
- 子進程的未決信號集設置爲空集
fork 系統調用注意點
- fork系統調用之後,父子進程將交替執行。
- 如果父進程先退出,子進程還沒退出那麼子進程的父進程將變爲init進程。(注:任何一個進程都必須有父進程)
- 如果子進程先退出,父進程還沒有退出,那麼子進程必須等到父進程捕獲到了子進程的退出狀態才真正結束,否則這個時候子進程就成爲僵進程。
8、替換一個進程映像(exec)
當進程調用一種exec函數時,該進程完全由新程序代換,而新程序則從其main函數開始執行。因爲調用exec並不創建新進程,所以前後的進程ID並未改變。exec只是用另一個新程序替換了當前進程的正文、數據、堆和棧斷。
-
功能:用exec函數可以把當前進程替換爲一個新進程。exec名下是由多個關聯函數組成的一個完整系列
-
原型
#include <unistd.h> int execl(const char *path,const char *arg,...);//使用參數列表 int execlp(const char *path,const char *arg,...);//使用參數列表 int execle(const char *path,const char *arg,...,char *const envp[]);//將環境變量添加到新建的子進程中 int execv(const char *path,char *const argv[]);//通過指針數組的方式來傳遞參數 int execvp(const char *file,char *const argv[]);//通過指針數組的方式來傳遞參數
-
參數
- path:表示你要啓動程序的名稱包括路徑名
- arg:表示啓動程序所帶的參數
-
返回值
- 成功返回0,失敗返回 -1
示例
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Runnning ps with execlp\n");
execl("bin/ls","ls","-l","/etc/passwd",(char *)0);
execlp("ls","ls","-l","/etc/passwd",(char *)0);// ls -l /etc/passwd
printf("Done.\n");
exit(0);
}
9、啓動新的進程(system)
- 功能:可以讓一個程序在另一個程序的內部運行,也就是說,我們創建了一個新的進程。這個工作可以通過庫函數system來實現。
- 原型:
#include <stdlib.h>
int system(const char *string);
- 參數
- string:你要啓動程序的名稱
- 返回值
- 如果無法啓動shell運行命令,system將返回 “127”;
- 出現不能執行system調用的其他錯誤時返回 “-1”。
- 如果system能夠順利執行,返回那個命令的退出碼。
示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("PID:%d\n",getpid());
printf("Running ps with system\n");
system("ps -ef");
printf("Done\n");
exit(0);
}
-
system函數遠非是啓動其他進程的理想手段,因爲它必須用一個shell來啓動預定的程序。
-
對shell的安裝情況和它所處的環境的依賴也很大
-
效率很低
10、wait和waipid函數
什麼是殭屍進程
- 當一個子進程結束運行時,它與其父進程之間的關聯還會保持到父進程也正常地結束運行或者父進程調用了wait才告終止。
- 進程表中代表子進程的數據項是不會立刻釋放的,雖然不再活躍了,可子進程還停留在系統裏,因爲它的退出碼還需要保存起來以備父進程中後續的wait調用使用。它將稱爲一個“僵進程”。
wait和waipid函數
-
當一個進程正常或異常終止時,內核就向其父進程發送SIGCHLD信號。因爲子進程終止是個異步事件(這可以在父進程運行的任何時候發生),所以這種信號也是內核向父進程發的異步通知。
-
父進程可以忽略該信號,或者提供一個該信號發生時即被調用執行的函數(信號處理程序)。對於這種信號的系統默認動作是忽略它。
-
wait函數用於使父進程阻塞,直到一個子進程結束或者該進程接收到一個指定信號爲止。
wait函數返回說明
調用wait或waitpid的進程可能會:
- 阻塞(如果其所有子進程都還在運行)。
- 帶子進程的終止狀態立即返回(如果一個子進程已終止,正等待父進程存取其終止狀態)。
- 出錯立即返回(如果它沒有任何子進程)。
wait函數
- 功能:使父進程阻塞,直到一個子進程結束或者該進程接收到了一個指定的信號爲止。如果該父進程沒有子進程或者它的子進程已經結束,則wait()函數就會立即返回。
wait返回狀態檢測
宏定義 | 描述 |
---|---|
WIFEXITED(stat_val) | 如果子進程正常結束,返回一個非零值 |
WEXITSTATUS(stat_val) | 如果WIFEXITED非零,返回子進程退出碼 |
WIFSIGNALED(stat_val) | 子進程因未捕獲信號而終止,返回非零值 |
WTERMSIG(stat_val) | 如果WIFSIGNALED非零,返回信號代碼 |
WIFSTOPPED(stat_val) | 如果子進程終止,返回一個非零值 |
WSTOPSIG(stat_val) | 如果WIFSTOPPED非零,返回一個信號代碼 |
waitpid函數
- 功能:waitpid()的作用和wait()一樣,但它並不一定要等待第一個終止的子進程(它可以指定需要等待終止的子進程),它還有若干選項,可以不阻塞父進程。
wait和waitpid的區別
- 在一個子進程終止前, wait 使其調用者阻塞,而waitpid 有一選擇項,可使調用者不阻塞。
- waitpid並不等待第一個終止的子進程—它有若干個選擇項,可以控制它所等待的特定進程。
- 實際上wait函數是waitpid函數的一個特例。
11、exit和_exit
- exit和_exit用於中止進程;
- _exit的作用:直接使進程停止運行,清除其使用的內存空間,並清除其在內核中的數據結構;
- exit與_exit函數不同,exit函數在調用exit系統之前要檢查文件打開情況把文件緩衝區的內容寫回文件中去。如調用printf()函數。
‘\n’ 會立刻把文件緩衝區的內容寫回文件中去。所以在測試exit和_exit的區別時,在printf不要加’\n’。
exit工作原理圖
12、守護進程
什麼是守護進程?
- 守護進程是在後臺運行不受終端控制的進程
- 守護進程能自動轉到後臺並且脫離於終端的聯繫
- Linux系統中一般有很多守護進程在後臺運行,執行不同的管理任務
守護進程的特徵
- 最重要特徵是後臺運行
- 守護進程必須與運行前的環境隔離開來
- 守護進程的啓動方式有其特殊之處
守護進程運行環境
- 包括未關閉的文件描述符,控制終端,會話和進程組,工作目錄以及文件創建掩碼等。
- 這些環境通常是守護進程從執行它的父進程(特別是shell)中繼承下來的。
守護進程啓動方法
- 在系統啓動時很多守護進程都是由系統初始化腳本啓動。
- 這些腳本一般在/etc目錄或以/etc/rc開頭的目錄下,它們的位置和內容依賴於具體的實現
守護進程啓動示例
- 許多網絡服務器是由inetd超級服務器啓動的,inetd是由系統初始化腳本啓動的
- cron守護進程按規則定期執行一些程序,由它啓動的程序也以守護進程的方式運行,cron是由系統初始化腳本啓動
- 不管是在前臺還是在後臺,守護進程也可以在用戶終端上啓動
守護進程消息處理
- 由於守護進程沒有控制終端,在發生問題時它要用一些其他方式以輸出消息。這些消息既有一般的通告消息,也有需要管理員處理的緊急事件消息。
- syslog函數是輸出這些消息的標準方式,它將消息發往syslog守護進程
進程和進程組
- 進程屬於一個進程組
- 進程組號(GID)就是進程組長的進程號(PID)
- 進程組通常是從父進程繼承過來
- 登陸會話可以包含多個進程組
- 所有的進程組共享一個控制終端
守護進程建立流程
後臺運行子進程
-
避免掛起,控制終端將守護進程放入後臺執行。
-
方法是在進程中調用fork使父進程終止,
if(pid=fork())
exit(0);//是父進程,結束父進程,子進程繼續
脫離運行環境
- 控制終端,登錄會話和進程組通常是從父進程繼承下來的
- 我們的目的就是要擺脫它們,使之不受它們的影響。
- 方法是在第1點的基礎上,調用setsid()使進程成爲會話組長。
setsid系統調用
- 創建守護進程最關鍵的一步是調用setsid函數創建一個新的Session,併成爲Session Leader。
- 該函數調用成功時返回新創建的Session的id(其實也就是當前進程的id),出錯返回-1。注意,調用這個函數之前,當前進程不允許是進程組的Leader,否則該函數返回-1。要保證當前進程不是進程組的Leader也很容易,只要先fork再調用setsid就行了。fork創建的子進程和父進程在同一個進程組中,進程組的Leader必然是該組的第一個進程,所以子進程不可能是該組的第一個進程,在子進程中調用setsid就不會有問題了。
禁止進程重新打開控制終端
- 守護進程雖然已經稱爲無終端的會話組長,但是它可能重新申請打開一個新的控制終端
- 如果這個守護進程不是會話組長,它就不能打開新的控制終端。所以我們可以先結束第一個子進程,再創建第二個子進程,這樣第二個子進程就不會是進程組長了
關閉打開的文件描述符
-
進程從創建它的父進程那裏繼承了打開的文件描述符。如不關閉,將會浪費系統資源,造成進程所在的文件系統無法卸下以及引起無法預料的錯誤。
-
可按照如下格式關閉文件描述符
for(i=0;i<打開的文件描述符數;i++)
close(i);
改變當前目錄工作目錄
- 進程活動時,其工作目錄所在的文件系統不能卸下。一般需要將工作目錄改變到根目錄。對於需要轉儲核心,寫運行日誌的進程將工作目錄改變到特定目錄如/tmp
- 使用函數 chdir(const char*);來更改工作目錄
重設文件創建掩碼
- 進程從創建它的父進程那裏繼承了文件創建掩碼
- 可能修改守護進程所創建的文件的存取位。爲防止這一點,將文件創建掩碼清除
- 使用函數 umask(0);清除文件創建掩碼
忽略SIGHUP信號
- 當會話頭(第一次生成的子進程)終止時,該會話中的所有進程(第二次生成的子進程)都會收到SIGHUP信號
- 使用 signal(SIGHUP,SIG_IGN); 來忽略掉這個信號
守護進程示例
#include <unistd.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void init_daemon(void)
{
int pid;
int i;
if(pid = fork())
exit(0);//是父進程,結束父進程
else if(pid < 0)
exit(1);//fork失敗,退出
//是第一子進程,後臺繼續執行
setsid();//第一子進程成爲新的會話組長和進程組長
//並與控制終端分離
if(pid = fork())
exit(0);//是第一子進程,結束第一子進程
else if(pid < 0)
exit(1);//fork失敗,退出
//是第二子進程,繼續
//第二子進程不再是會話組長
for(i = 0;i < NOFILE;i++)
close(i);
chdir("/tmp");//改變工作目錄到/tmp
umask(0);//重設文件創建掩碼
return;
}
int main()
{
FILE *fp;
init_daemon();//初始化爲Daemon
while(1)//每隔2s向test.log報告運行狀態
{
sleep(2);
if(fp = fopen("test.log","a"))
{
time_t t = time(NULL);
fprintf(fp,"I'm here at %s\n",asctime(localtime(&t)));
fclose(fp);
}
}
return 0;
}