一.進程
1.1.程序和進程的關係
簡單來説,程序是靜止的,就是我們的可執行文件,進程是動態的,就是運行起來的程序。
1.2.並行和併發
1)並行,parallel 強調同一時刻同時執行
2)併發,concurrency 則指的一個時間段內去一起執行
1.3.進程的狀態
在五態模型中,進程分為新建態、終止態,運行態,就緒態,阻塞態,如下圖
1.4.進程各個狀態的切換時機
①TASK_RUNNING(運行態):進程正在被CPU執行。
當一個進程剛被創建時會處於TASK_RUNNABLE,表示己經準備就緒,正等待被調度。
②TASK_INTERRUPTIBLE(可中斷):進程正在睡眠(被阻塞)等待某些條件的達成。
當條件達成,內核就會把進程狀態設置為運行。
處於此狀態的進程會因為接收到信號而提前被喚醒,比如給一個TASK_INTERRUPTIBLE(可中斷)狀態的進程發送SIGKILL信號,這個進程將先被喚醒(進入TASK_RUNNABLE運行狀態),然後再響應SIGKILL信號而退出(變為TASK_ZOMBIE停止狀態),而不是從TASK_INTERRUPTIBLE狀態(可中斷)直接退出。
③TASK_UNINTERRUPTIBLE(不可中斷):處於等待中的進程,待資源滿足時被喚醒,但不可以由其它進程通過信號或中斷喚醒。
由於不接受外來的任何信號,因此無法用kill殺掉這些處於該狀態的進程,這個狀態存在的作用就是因為內核的某些處理流程是不能被打斷的。如果響應異步信號,程序的執行流程中就會被插入一段用於處理異步信號的流程,於是原有的流程就被中斷了,這可能使某些設備陷入不可控的狀態。
另外處於TASK_UNINTERRUPTIBLE狀態一般情況下,是非常短暫的,很難通過ps命令捕捉到。
④TASK_ZOMBIE(僵死):表示進程已經結束了,但是其父進程還沒有調用wait4或waitpid()來釋放進程描述符。為了父進程能夠獲知它的消息,子進程的進程描述符仍然被保留着。一旦父進程調用了wait4(),進程描述符就會被釋放。
⑤TASK_STOPPED(停止):進程停止執行。當進程接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信號的時候,還有在調試的時候接收到任何信號,也會使進程進入這種狀態。當接收到SIGCONT信號,會重新回到TASK_RUNNABLE。
1.5 進程號
進程號PID:每個進程都由一個進程號來標識,其類型為 pid_t(整型),進程號的範圍:0~32767。
父進程號PPID:任何進程( 除 init 進程)都是由另一個進程創建,該進程稱為被創建進程的父進程,對應的進程號稱為父進程號(PPID)。
進程組號PGID:進程組是一個或多個進程的集合。他們之間相互關聯,進程組可以接收同一終端的各種信號,關聯的進程有一個進程組號(PGID)
1.6 進程相關函數
getpid函數
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:
獲取本進程號(PID)
參數:
無
返回值:
本進程號
getppid函數
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
功能:
獲取調用此函數的進程的父進程號(PPID)
參數:
無
返回值:
調用此函數的進程的父進程號(PPID)
getpgid函數
#include <sys/types.h>
#include <unistd.h>
pid_t getpgid(pid_t pid);
功能:
獲取進程組號(PGID)
參數:
pid:進程號
返回值:
參數為 0 時返回當前進程組號,否則返回參數指定的進程的進程組號
二.創建進程
2.1 fork函數
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
功能:
用於從一個已存在的進程中創建一個新進程,新進程稱為子進程,原進程稱為父進程。
參數:
無
返回值:
成功:子進程中返回 0,父進程中返回子進程 ID。pid_t,為整型。
失敗:返回-1。
失敗的兩個主要原因是:
1)當前的進程數已經達到了系統規定的上限,這時 errno 的值被設置為 EAGAIN。
2)系統內存不足,這時 errno 的值被設置為 ENOMEM。
使用 fork() 函數得到的子進程會將父進程複製一份(包括進程上下文、進程堆棧、打開的文件描述符、信號控制設定、進程優先級、進程組號等),
所以使用fork()創建進程開銷是很大的。
那麼如何區分父進程和子進程呢?那就是根據fork()返回的pid值。
fork() 函數被調用一次,但返回兩次,
子進程的返回值是 0,而父進程的返回值則是新子進程的進程 pid。
pid_t pid = fork(); // 根據這個返回值來區分父子進程,如同這個值小於0,那就是表示創建進程失敗。
代碼示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
// 打印創建進程的進程號
printf("創建子進程前的pid:[%d]\n",getpid());
// 創建子進程
pid_t pid = fork();
if(pid < 0){// 創建進程失敗
perror("fork ");
return -1;
}else if(pid >0){ // 父進程執行內容
printf("父進程執行[pid %d][ppid:%d\n]",getpid(),getppid());
}else if(pid == 0){ //子進程執行
printf("子進程執行[pid %d][ppid:%d\n]",getpid(),getppid());
}
printf("創建子進程後執行 pid[%d]\n",getpid());
return 0;
}
輸出結果
創建子進程前的pid:[29871]
父進程執行[pid 29871][ppid:29848
]創建子進程後執行 pid[29871]
子進程執行[pid 29872][ppid:29871
]創建子進程後執行 pid[29872]
父子進程中各自的空間是獨立的,假定全局變量a,在子進程中修改a的值時,並不影響父進程。
2.2 exec 函數族
在運行的進程中,啓動一個外部程序,由內核將這個外部程序讀入內存,使其執行起來成為一個進程,例如,我們執行一個程序,需要調用外部的ls命令,獲取一個目錄的資源信息,這裏使用的就是exec函數族。
exec 函數族,就是一簇函數,在 Linux 中,並不存在 exec() 函數,exec 指的是一組函數,共有6個,如下:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
一般常用的有兩個,execl和excelp。
execl函數一般執行自己寫的程序
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
path: 要執行的程序的路徑
變參arg: 要執行的程序的需要的參數,一般先是程序的名字,之後是程序的執行參數
參數寫完之後: NULL
返回值:若是成功,則不返回,不會再執行exec函數後面的代碼;
若是失敗,會執行execl後面的代碼,可以用perror打印錯誤原因。
參考示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
printf("===============start ls=================");
/*調用系統的ls命令*/
execl("/bin/ls","ls","-l",NULL);
perror("execl");
return 0;
}
我們也可以自己寫一個程序文件,來進行調用
例如我們寫一個示例程序test.c,打印接受的參數
#include <stdio.h>
#include <unistd.h>
int man(int argc, char*argv[]){
for(int i=0;i<argc;i++){
printf("[%s] ",argv[i]);
}
return 0;
}
主程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
printf("===============start ls=================");
execl("./test","test","hello","world",NULL);
perror("execl");
return 0;
}
execlp很是相似,execlp函數一般是執行系統自帶的程序或者是命令.
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
參數説明:
file: 執行命令的名字, 根據PATH環境變量來搜索該命令,
其餘和execl相同。
代碼示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
printf("===============start ls=================");
/*調用系統的ls命令*/
execlp("ls","ls","-l",NULL);
perror("execl");
return 0;
}
3 退出進程
3.1 資源回收
在進程退出的時候,內核會釋放進程所有的資源,但仍然有一些資源不能被釋放,主要指進程控制塊PCB(進程號、退出狀態、運行時間等)。
父進程可以通過調用wait或waitpid得到它的退出狀態同時徹底清除掉這個進程,避免資源的浪費。
3.2 孤兒進程和殭屍進程
1)孤兒進程
父進程運行結束,但它的子進程卻還未結束運行,這個子進程就成了孤兒進程。
內核會把孤兒進程的父進程設置為init ,而init 進程會循環地 wait() 它的已經退出的子進程。
所以孤兒進程並不會造成什麼危害。
2)殭屍進程
若子進程結束運行,父進程還在繼續運行,但是父進程沒有調用wait或waitpid函數完成對子進程的資源回收,那麼這個子進程就成了殭屍進程。
殭屍進程會佔用系統的資源,霸佔進程號,而進程號是有限的,過多的殭屍進程出現,會導致可用進程號減少,最後無法創建新的進程,所以要避免產生殭屍進程。
3.3 回收進程資源的函數wait或waitpid
1)wait
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
功能:
等待任意一個子進程結束,如果任意一個子進程結束了,此函數會回收該子進程的資源。
參數:
status : 進程退出時的狀態信息。
返回值:
成功:已經結束子進程的進程號
失敗: -1
具體説明:
wait()函數的主要功能為回收已經結束子進程的資源,調用 wait() 函數的進程會處於阻塞狀態。
如果調用進程沒有子進程,那函數立即返回;
如果它的子進程已經結束,則函數會立即返回,同時會回收那個該進程的資源。
如果傳入參數 status 的值不是 NULL,那麼wait() 就會把子進程退出時的狀態取出並存入一個整數值(int),表明子進程是正常退出還是異常終止。
這個退出信息在一個 int 中包含了多個字段,我們使用用宏定義取出其中的每個字段,主要有如下三組宏信息
1) WIFEXITED(status) 為非0,表示進程正常結束
WEXITSTATUS(status) 如上宏為真,使用此宏可以獲取進程退出狀態 (exit的參數)
2) WIFSIGNALED(status)為非0,表示進程異常終止
WTERMSIG(status)如上宏為真,使用此宏,取得使進程終止的那個信號的編號。
3) WIFSTOPPED(status)為非0 → 進程處於暫停狀態
WSTOPSIG(status)如上宏為真,使用此宏取得使進程暫停的那個信號的編號。
WIFCONTINUED(status)為非0,意味着進程暫停後已經繼續運行
代碼示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t pid = fork();
if(pid<0){
perror("fork");
return -1;
}else if(pid >0){
// 父進程調用wait 一次wait只能終止一個子進程
int status;
pid_t cpid = wait(&status);
printf("child pid [%d]",cpid);
if(WIFEXITED(status)){
// 正常退出
printf("child process normal exit. status[%d]\n",WEXITSTATUS(status));
}else if(WIFSIGNALED(status)){
// 被信號殺死
printf("child process killed by signal, signo[%d]\n", WTERMSIG(status));
}
}else if(pid == 0){
// 子進程
for(int i=0;i<10;i++){
printf("helloworld\n");
}
return 10;
}
return 0;
}
2)waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
功能:跟wait一樣,等待子進程終止,回收子進程的資源。
參數:
pid : 參數 pid 的值有以下幾種類型:
pid > 0 等待進程 ID 等於 pid 的子進程。
pid = 0 等待同一個進程組中的任何子進程,如果子進程已經加入了別的進程組,waitpid 不會等待它。
pid = -1 等待任一子進程,此時 waitpid 和 wait 作用一樣。
pid < -1 等待指定進程組中的任何子進程,這個進程組的 ID 等於 pid 的絕對值,很少使用。
status : 進程退出時的狀態信息。和 wait() 用法一樣。
options : options 提供了一些額外的選項來控制 waitpid()。
0:同 wait(),阻塞父進程,等待子進程退出。
WNOHANG:沒有任何已經結束的子進程,則立即返回。
WUNTRACED:如果子進程暫停了則此函數馬上返回,並且不予以理會子進程的結束狀態。(由於涉及到一些跟蹤調試方面的知識,加之極少用到)
返回值:
waitpid() 的返回值比 wait() 稍微複雜一些,一共有 3 種情況:
1) 當正常返回的時候,waitpid() 返回收集到的已經回收子進程的進程號;
2) 如果設置了選項 WNOHANG,而調用中 waitpid() 發現沒有已退出的子進程可等待,則返回 0;
3) 如果調用中出錯,則返回-1,這時 errno 會被設置成相應的值以指示錯誤所在,如:當 pid 所對應的子進程不存在,或此進程存在,但不是調用進程的子進程,waitpid() 就會出錯返回,這時 errno 被設置為 ECHILD;
代碼示例(上面代碼簡單修改一下)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t pid = fork();
if(pid<0){
perror("fork");
return -1;
}else if(pid >0){
// 父進程調用wait 一次wait只能終止一個子進程
int status;
pid_t cpid = waitpid(-1,&status,0);
printf("child pid [%d]",cpid);
if(WIFEXITED(status)){
// 正常退出
printf("child process normal exit. status[%d]\n",WEXITSTATUS(status));
}else if(WIFSIGNALED(status)){
// 被信號殺死
printf("child process killed by signal, signo[%d]\n", WTERMSIG(status));
}
}else if(pid == 0){
// 子進程
for(int i=0;i<10;i++){
printf("helloworld\n");
}
return 10;
}
return 0;
}