1.進程概述
進程和程序的區別
程序:程序是存儲在存儲介質上的一個可執行文件---靜態的
進程:進程是程序的執行實例。可以說進程就是正在執行的程序。
程序是一些指令的集合,而進程是程序的執行過程,這個過程的狀態是變化的,包括進程的創建、調度和消亡。
單道程序和多道程序
單道程序:所有進程一個一個排隊執行。若 A 阻塞,B 只能等待,即使 CPU 處于空閑狀態。而在人機交互時阻塞的出現是必然的。所有這種模型在系統資源利用上及其不合理,在計算機發展歷史上存在不久,大部分便被淘汰了。以前的dos操作系統就是單道程序系統
多道程序:在計算機內存中同時存放幾道相互獨立的程序,它們在管理程序控制之下,相互穿插的運行(并行和并發)。多道程序設計必須有硬件基礎作為保證。
并行和并發
并行:同一時間內,多個程序或多條指令同時運行(一定是多核)
并發:宏觀上的并行,多個進程使用同一塊資源(CPU),但是他們快速的交叉使用CPU資源,給人一種假象,像是多個程序同時執行。
進程控制塊(PCB-process contrl block)
進程在內存中運行時,內核為每個進程分配一個 PCB(進程控制塊),維護進程相關的信息(包括數據、代碼存放點位置燈),Linux 內核的進程控制塊是 task_struct 結構體。相當于人的身份證。
PCB是操作系統中最重要的記錄型數據結構。
2.進程的狀態
三種狀態
就緒態:進程已經具備執行的一切條件,正在等待CPU的處理時間
執行態:該進程正在占用CPU運行
等待態:進程因不具備某些執行條件而暫時無法繼續執行的狀態
ps指令:用于顯示當前進程的狀態,類似于windows的任務管理器
參數:
ps 的參數非常多, 在此僅列出幾個常用的參數并大略介紹含義
-A 列出所有的進程
-w 顯示加寬可以顯示較多的資訊
-au 顯示較詳細的資訊
-aux 顯示所有包含其他使用者的進程
-ajx?更詳細、更具層次關系
USER:進程擁有者
PID:process id
%CPU:占用CPU使用率
%NAME:占用的記憶體使用率
VSZ:占用的虛擬記憶體大小
RSS:占用的記憶體大小
TTY:終端的次要裝置號碼
STAT:該進程的狀態
START:進程開始時間
TIME:執行的時間
COMMAND:執行的命令
stat狀態:
3.進程號
每個進程都有一個進程號來進標識,其類型是pid_t,進程號的范圍是0~32767(不同的版本可能有區別)。進程號總是唯一的,但是可以重用,一個進程終止后,這個進程號可以再次被使用。
- linux中的進程號從0開始
- 進程號0和1由內核創建
- 0進程:調度進程,常被稱為交換進程
- 1進程:init進程
- 除調度進程外,所有進程都由1進程進行直接或者間接調用
- 進程號0和1由內核創建
PID:進程號,是一個非負整數
PPID:父進程,任何進程都由另一個進程創建,高金城稱為被創建進程的父進程。對應的進程號稱為父進程號(PPID);
PGID:進程組,一個或多個進程的集合。他們之間相互關聯,進程組可以接受同一終端的各種信號,關聯的進程有一個進程組號。
獲取進程函數
#include <sys/types.h>
#include <unistd.h>//功能:獲取本進程號
//返回值:當前進程進程號
pid_t getpid(void);//功能:獲取調度此函數的進程的父進程號
pid_t getppid(void);//功能:獲取進程組好,參數為0是返回當前PGID,否則返回指定的進程的PGID
pid_t getpgid(pid_t pid);
????????
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main(int argc, char const *argv[])
{printf("該進程號:%d\n",getpid());printf("該進程的父進程號:%d\n",getppid());printf("該進程的進程組號:%d\n",getpgid(0));return 0;
}
4.創建進程
fork函數(創建的子進程是復制一份父進程的內容,到一個新的空間)
#include <sys/types.h>
#include <unistd.h>
//創建一個新進程
pid_t fork(void)
功能:
/*fork()函數用于從一個已存在的進程中創建一個新進程,新進程稱為子進程,原進
程稱為父進程。
返回值:
成功:子進程中返回 0,父進程中返回子進程 ID。 非常重要!!!!!
失敗:返回-1*/
注意:子進程是將父進程里面的內容全部拷貝了一份,但是子進程里的fork不會再執行,會從fork的下一句開始執行,即將得到的id賦值給pid,所以fork函數在父進程里返回值為子進程id,在子進程里返回值為0(系統自動處理,避免一直執行fork,子子孫孫無窮盡也)
因為父子進程同時再運行,所以才會有兩個結果(否則 if與elseif是互斥的,不可能有兩個結果)
#include <sys/types.h>#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>int main(int argc, char **argv)
{pid_t pd = fork();if (pd < 0){perror("fork");return 1;}else if (pd == 0){while (1){printf("子進程ID-->%d,父進程ID-->%d\n", getpid(), getppid());sleep(5);}}else if (pd > 0){while (1){printf("父進程ID-->%d\n", getpid());sleep(2);}}return 0;
}
sleep函數
是將進程暫時掛起一段時間,在后續資源回收詳細介紹,可以在此處理解為延時一段時間
#include<unistd.h>
unsigned int sleep(unsigned int sec); //進程掛起一段時間,即一直處在等待態
功能:
進程掛起指定的描述,知道指定的時間用完或者收到信號才解除掛起
返回值:
若進程掛起到sec指定的時間,則返回0,若有信號中斷,則返回剩余的秒數
注意:
進程掛起指定的秒數后程序不會立即執行,系統知識將此進程切換到就緒態
父子進程的復制
使用 fork 函數得到的子進程是父進程的一個復制品,它從父進程處繼承了整個進程的地址空間。
地址空間:包括進程上下文、進程堆棧、打開的文件描述符、信號控制設定、進程優先級、進程組號等。子進程所獨有的只有它的進程號,計時器等。因此,使用 fork 函數的代價是很大的。
進程之間都是相互獨立的。
vfork函數(子進程和父進程共享一塊內存空間)
#include <sys/types.h>
#include <unistd.h>//pid_t vfork(void);創建一個新進程,但是vfork在創建進程的時候,先創建子進程,在創建父進程
/*功能:vfork()函數用于從一個已存在的進程中創建一個新進程,新進程稱為子進程,原進程稱為父進程,子進程需要退出之后(exit()),父進程才可以執行。
返回值:成功:子進程中返回 0,父進程中返回子進程 ID。失敗:返回-1。*/
pid_t vfork(void);
先創建子進程,再創建父進程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main(int argc, char const *argv[])
{pid_t pd = vfork();if (pd<0){perror("vfork");}else if (pd > 0){while (1){sleep(1);printf("當前進程id:%d\n",getpid());_exit(1); //退出,結束當前進程}}else if (pd == 0){while (1){sleep(1);printf("當前進程id:%d 當前進程父進程:%d\n",getpid(),getppid());_exit(1); //退出,結束當前進程} }return 0;
}
子進程父進程共享同樣的內存
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main(int argc, char const *argv[])
{int num = 0;pid_t pd = vfork();if (pd < 0){perror("vfork");}else if (pd == 0){while (1){sleep(1);printf("當前進程id:%d 當前進程父進程:%d\n", getpid(), getppid());num = 1000;printf("num:%d\n", num);_exit(1); // 退出,結束當前進程}}else if (pd > 0){while (1){sleep(1);printf("當前進程id:%d\n", getpid());printf("num:%d\n", num);_exit(1); // 退出,結束當前進程}}return 0;
}
5.進程資源回收(等待)
當進程結束后,系統可以回收進程資源,但是關于進程中的例如ID號,內存地址,進程名等,系統不會回收。父子進程有序需要簡單的進程間同步,比如父進程等待子進程結束。
一般指的是父進程回收子進程。
wait函數:
#include <sys/types.h> #include <sys/wait.h>/* 功能:等待子進程終止,如果子進程終止了,那么此函數會回收子進程的資源。調用wait函數的進程會掛起(阻塞),因為要等待子進程結束。如果有多個子進程,需要調用多個wait函數 參數:status,子進程退出時的狀態,需要提前定義,后續會把狀態存儲到這里 返回值: 成功:子進程的進程號 失敗:-1 */ pid_t wait(int *wstatus);
案例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{pid_t pd = fork();if (pd < 0){perror("fork");}else if (pd == 0){int i = 0;for ( i = 5; i > 0; i--){printf("子進程%u剩余生命:%d\n",getpid(),i);sleep(1);}exit(-1);}else if (pd > 0){int status = 0 ;printf("父進程%u正在等待子進程%u結束\n",getpid(),pd);wait(&status);printf("子進程%u結束.\n",pd);printf("退出狀態為%d\n",status);}return 0;
}
exit函數
#include <stdlib.h>
void exit(int status); //退出進程,這是庫函數寫法,等價于_exit(int status)
wait參數status詳解
1.取出子進程的退出信息 WIFEXITED(status) 如果子進程是正常終止的,取出的字
段值非零。2.?WEXITSTATUS(status) 返回子進程的退出狀態,退出狀態保存在status 變量的 8~16 位。在用此宏前應先用宏 WIFEXITED 判斷子進程是否正常退出,正常退出才可以使用此宏。
3.此 status 是個 wait 的參數指向的整型變量
else if (pd > 0){int status = 0;printf("父進程%u正在等待子進程%u結束\n", getpid(), pd);pid_t son = wait(&status); //保存退出的那個pidif (WIFEXITED(status)) //如果正常退出{printf("子進程%u結束.\n", son);printf("退出狀態為%d\n", WEXITSTATUS(status)); //打印退出狀態}}
waitpid函數
#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);
函數文檔注釋:
功能:等待子進程終止,如果子進程終止了,此函數會回收子進程的資源。
參數:
? ??????pid : 參數 pid 的值有以下幾種類型:
pid > 0 等待進程 ID 等于 pid 的子進程。
pid = 0 等待同一個進程組中的任何子進程,如果子進程已經加入了別的進程
組,waitpid 不會等待它。
pid = -1 等待任一子進程,此時 waitpid 和 wait 作用一樣。
pid < -1 等待指定進程組中的任何子進程,這個進程組的 ID 等于 pid 的絕對值。例如傳入-5555,對應的作用是監控進程組號為5555的每一個pid
status : 進程退出時的狀態信息。和 wait() 用法一樣。
options : options 提供了一些額外的選項來控制 waitpid()。
0:同 wait(),阻塞父進程,等待子進程退出。
WNOHANG:沒有任何已經結束的子進程,則立即返回(非阻塞)。
WUNTRACED:如果子進程暫停了則此函數馬上返回,并且不予以理會子進程
的結束狀態。(由于涉及到一些跟蹤調試方面的知識,加之極少用到)返回值:?waitpid() 的返回值比 wait() 稍微復雜一些,一共有 3 種情況:
1) 當正常返回的時候,waitpid() 返回收集到的已經回收子進程的進程號;
2) 如果設置了選項 WNOHANG,而調用中 waitpid() 的進程發現有子進程在運行,而且沒有已退出的子進程,則返回0;如果父進程所有子進程都結束,則返回-1;如果>0,則等到了一個子進程退出,這個返回值就是退出的那個子進程
可等待
3) 如果調用中出錯,則返回-1,這時 errno 會被設置成相應的值以指示錯誤
所在。
代碼案例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{pid_t pd = fork();if (pd < 0){perror("fork");}else if (pd == 0){int i = 0;for (i = 5; i > 0; i--){printf("子進程%u剩余生命:%d\n", getpid(), i);sleep(1);}exit(0);}else if (pd > 0){int status = 0;printf("父進程%u正在等待子進程%u結束\n", getpid(), pd);sleep(6);pid_t son = waitpid(-1,&status,WNOHANG); //6s后檢測是否有子進程退出,如果有,則返回對應進程idprintf("son %u\n",son);if (WIFEXITED(status)){printf("子進程%u結束.\n", son);printf("退出狀態為%d\n", WEXITSTATUS(status));}}return 0;
}
6.特殊進程
6.1僵尸進程
進程已經結束,但是對應的資源沒有回收,這樣的進程稱之為僵尸進程。例如:子進程已退出,但是父進程未回收子進程資源(wait,waitpid),那么此子進程就成為僵尸進程。
有危害:因為子進程id被占用,但是系統的pid數量有限制
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{pid_t pd = fork();if (pd < 0){perror("fork");}else if (pd == 0){int i = 0;for (i = 5; i > 0; i--){printf("子進程%u剩余生命:%d\n", getpid(), i);sleep(1);}exit(0);}else if (pd > 0){//子進程結束,父進程不回收資源while (1);//父進程不能結束,否則他們會一起被回收}return 0;
}
6.2孤兒進程
父進程先結束,但是子進程未運行結束的子進程。
子進程被1號進程接管,當子進程結束時,由1號進程進行資源回收
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{pid_t pd = fork();if (pd < 0){perror("fork");}else if (pd == 0){while (1);int i = 0;for (i = 5; i > 0; i--){printf("子進程%u剩余生命:%d\n", getpid(), i);sleep(1);}exit(0);}else if (pd > 0){printf("父進程%u結束\n",getpid());}return 0;
}
6.3守護進程(精靈進程)
脫離終端的孤兒進程
暫時不寫,后續再寫。繼續往后學下去吧
7.fork創建多個子進程
創建2個子進程
1.錯誤的寫法
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{for (int i = 0; i < 2; i++){pid_t pid = fork();}while(1);return 0 ;
}
有4個a.out相關進程
實際是1個父進程,2個子進程,還有1個子進程的子進程,一共4個。
這樣會有問題,不知道哪個進程是子進程
2.正確的進程
起始上面的代碼只要能保證子進程不會再創建子進程即可
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{for (int i = 0; i < 2; i++){pid_t pid = fork();if (pid == 0) //如果是在子進程里,就不再往下運行{break;}}while(1);return 0 ;
}
3.創建多進程并且回收資源
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{int i = 0;for (i = 0; i < 2; i++){pid_t pid = fork();if (pid == 0){break;}}if (i == 0){printf("第1個子進程id:%u\n",getpid());sleep(3);exit(1);}else if(i == 1){printf("第2個子進程id:%u\n",getpid());sleep(4);exit(1);}else if(i == 2){int ret = 0;printf("父進程Id:%u\n",getpid());while(1){ret = waitpid(-1,NULL,WNOHANG);if (ret == 0){continue;}else if (ret > 0){printf("進程%u退出\n",ret);}else if(ret < 0){break;}}}return 0 ;
}
8.終端
在 UNIX 系統中,用戶通過終端登錄系統后得到一個 Shell 進程,這個終端成為Shell 進程的控制終端(Controlling Terminal),進程中,控制終端是保存在 PCB中的信息,而 fork 會復制 PCB 中的信息,因此由 Shell 進程啟動的其它進程的控制終端也是這個終端。
舉個例子:
int num = 0;
scanf("%d",&num);
printf("%d\n",num);
while(1);
在終端中,得到一個shell進程,這個shell進程的PCB中保存著終端控制信息,因此他可以控制終端。
當檢測到a.out的時候,會fork一個子進程來完成a.out的執行,fork的子進程里面不會再有./a.out這個執行執行
子進程中沒有./a.out,也會執行,這是由exec族函數完成的,后續會講。此時shell會把中斷控制權交給a.out,所以在a.out執行的時候,輸入任何命令都不起作用。a.out結束后,終端控制權移交給shell進程
此時,如果a.out也創建一個子進程呢?
代碼案例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{pid_t pd = fork();if (pd == 0){int num = 0;sleep(1);scanf("%d",&num);printf("子進程%u中num=%d,所屬終端名為%s\n",getpid(),num,ttyname(0));}else if (pd > 0){int num = 0;sleep(3);scanf("%d",&num);printf("父進程%u中num=%d,所屬終端名為%s\n",getpid(),num,ttyname(0));}return 0 ;
}
ttyname函數
/*獲取終端名稱
返回值:
成功:終端名
失敗:NULL
參數:文件描述符*/
char *ttyname(int fd);
進程組
進程組是包含一個或多個進程的集合,屬于一個回話,fork不會改變進程組。
進程組ID
每個進程組都有唯一的進程組ID(整數),進程組由進程組ID來唯一標識,除了PID,進程組ID也是一個進程的必備屬性之一。
進程組ID一般是當前進程組中的第一個進程的ID(組長)
getpgid();//獲得進程組IDint setpgid(pid_t pid,pid_t pgid);//將pid的進程組id設置為pgid。創建一個新進程組或者假如一個已經存在的pgid
會話
多個進程組的集合叫做會話。
沒打開一個終端,必打開一個會話。
如果進程ID=進程組ID=會話ID,那么該進程為會話首進程(會長)
會話是一個或多個進程組的集合。 一個會話可以有一個控制終端。這通常是終端設備或偽終端設備; 建立與控制終端連接的會話首進程被稱為控制進程; 一個會話中的幾個進程組可分為一個前臺進程組以及一個或多個后臺進程組; 如果一個會話有一個控制終端,則它有一個前臺進程組,其它進程組為后臺進程組; 如果終端接口檢測到斷開連接,則將掛斷信號發送至控制進程(會話首進程)。
前臺進程組:該進程組中的進程能夠向終端設備進行讀、寫操作的進程組。
后臺進程組:只能向終端寫的進程組。
創建會話
創建會話的函數:
#include <sys/types.h>
#include <unistd.h>
/*
函數功能:創建一個會話,并以自己的 ID 設置進程組 ID,同時也是新會話的 ID。
返回值:
成功:返回調用進程的會話 ID
失敗:-1
*/
pid_t setsid(void);
注意事項
1) 調用進程不能是進程組組長,該進程變成新會話首進程(session header)
2) 該調用進程是組長進程,則出錯返回
3) 該進程成為一個新進程組的組長進程
4) 需有 root 權限(ubuntu 不需要)
5) 新會話丟棄原有的控制終端,該會話沒有控制終端
6) 建立新會話時,先調用 fork, 父進程終止,子進程調用 setsid
代碼:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{pid_t pd = fork();if (pd == 0){printf("%u\n",getsid(0));setsid();while(1){}}else if (pd > 0){exit(1);}return 0 ;
}
9.exec函數族
在 Windows 平臺下,我們可以通過雙擊運行可執行程序,讓這個可執行程序成為一個進程;而在 Linux 平臺,我們可以通過 ./ 運行,讓一個可執行程序成為一個進程。 但是,如果我們本來就運行著一個程序(進程),我們如何在這個進程內部啟動一個外部程序,由內核將這個外部程序讀入內存,使其執行起來成為一個進程呢?這里我們通過 exec 函數族實現。(即可以實現從一個運行的終端啟動另一個程序)。
exec 指的是一組函數,一共有 6個
核心特點:
核心特點總結(四大特點)
“換魂不換殼”
不換:進程ID(PID)、父進程、打開的文件描述符、信號設置、環境變量等全部保留原樣。
全換:進程的代碼段、數據段、堆棧等被徹底替換為指定的新程序。
一山不容二虎
exec
?調用成功后沒有返回值,因為原來的程序代碼已經被完全覆蓋了,執行邏輯永遠跳不到?exec
?之后的代碼。如果調用失敗(如找不到指定程序),則會返回?
-1
,并設置?errno
,然后繼續執行原程序的后續代碼。一族六將,功能各異
這是一個函數族,包含多個函數(如?execl
,?execv
,?execle
,?execve
,?execlp
,?execvp
),它們最終都調用同一個系統調用?execve。
#include <unistd.h>
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file,cconst 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 execve(const char *filename, char *const argv[], char *const envp[]);
一個進程調用 exec 后,除了進程 ID,進程還保留了下列特征不變: 父進程號 進程組號 控制終端 根目錄 當前工作目錄 進程信號屏蔽集 未處理信號 ...
六個exec函數只有execve是真正的系統調用函數,其他的都是在此基礎上封裝的庫函數。
代碼案例1:execl執行ls指令
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{execl("/bin/ls","-a","-l",NULL);printf("hello world\n");return 0 ;
}
exclp執行ls命令(p:即path,env下面的path都包含)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{//execl("/bin/ls","-a","-l",NULL);execlp("ls","-a","-l",NULL);printf("hello world\n");return 0 ;
}
execv實現:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{//execl("/bin/ls","-a","-l",NULL);char *temp[]={"-a","-l",NULL};execv("/bin/ls",temp);printf("hello world\n");return 0 ;
}
execve實現
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{//execl("/bin/ls","-a","-l",NULL);extern char **environ;char *temp[]={"-a","-l",NULL};execve("/bin/ls",temp,environ);printf("hello world\n");return 0 ;
}
system函數
#include <stdlib.h>
/*system函數會調用fork函數,產生子進程,子進程會調用exec啟動/bin/sh -c string來執行參數string所代表的命令,命令執行完畢后返回原調用進程
參數:要執行的字符串
返回值:
如果command為null,則返回非0,一般為1
如果system()在調用/bin/sh時候失敗,返回127,其他原因返回-1
注意:system調用成功才會返回對應的返回值,如果連fork都調用失敗,肯定不會成功,一般返回0為成功
*/
int system(const char *command);
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{int status = system("ls -al");if (WIFEXITED(status)){printf("the exit status is %d\n",status);}else{printf("非正常退出\n");}return 0 ;
}