11.進程控制
文章目錄
- 11.進程控制
- 一、進程創建
- 二、進程終止
- 退出碼
- 進程終止的方式
- 三、進程等待
- 進程等待的方式
- 獲取?進程status
- 小程序
- 阻塞與非阻塞等待
- 四、進程程序替換
- 替換原理
- 進程程序替換的接口——exec替換函數
- 五、總結
一、進程創建
之前學習了fork()函數創建子進程,進程調?fork,當控制轉移到內核中的fork代碼后,內核做:分配新的內存塊和內核數據結構給?進程;將?進程部分數據結構內容拷???進程;添加?進程到系統進程列表當中;fork返回,開始調度器調度 。其中將?進程部分數據結構內容拷???進程的方法是寫時拷貝。下面進行說明:
通常,??代碼共享,??再不寫?時,數據也是共享的,當任意??試圖寫?,便以寫時拷?的?式各??份副本。 因為有寫時拷?技術的存在,所以??進程得以徹底分離!完成了進程獨?性的技術保證! 寫時拷?,是?種延時申請技術,可以提?整機內存的使?率
那么在修改內容之前與修改內容之后,具體做了什么?
在fork時將父進程的所有物理地址權限都改為只讀,之后子進程再拷貝父進程的內容。這樣當子進程或父進程對數據進行寫入操作的時候就會觸發系統錯誤。之后系統會觸發缺頁中斷,之后系統檢測,是真的發生錯誤還是發生寫時拷貝。若判定為寫時拷貝,系統會進行申請內存—>發生拷貝(將原數據段內容拷貝到新申請的內存中)—>修改頁表—>恢復執行,同時將數據區的權限設置為讀寫(父子進程都會改)。
二、進程終止
退出碼
main函數的返回值其實是返回給父進程和系統的。
[lisihan@hcss-ecs-b735 lession15]$ cat code1.cpp
#include<iostream>int main()
{std::cout << "hello linux" << std::endl;return 123;
}[lisihan@hcss-ecs-b735 lession15]$ ./code1
hello linux
[lisihan@hcss-ecs-b735 lession15]$ echo $?
123
[lisihan@hcss-ecs-b735 lession15]$ echo $?
0
-
通過
$?
我們可以查到上一次程序執行的退出碼,這里返回的就是123,并用echo顯示出來,這個退出碼用于表明錯誤原因 -
同樣echo也是一個可執行程序,所以第二次
echo $?
打印的值實際上是上一個echo $?
的退出碼,即為0 -
通常約定:0—成功 非0—失敗
在C語言中提供了一批錯誤碼errno
以及查看錯誤碼對應錯字符串的函數接口strerrno()
[lisihan@hcss-ecs-b735 lession15]$ cat code2.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{printf("before: errno: %d, strerrno: %s\n", errno, strerror(errno));FILE* fp = fopen("./text.c", "r");if(fp == NULL){printf("before: errno: %d, strerrno: %s\n", errno, strerror(errno));return errno;}return 0;
}
[lisihan@hcss-ecs-b735 lession15]$ ./code2
before: errno: 0, strerrno: Success
before: errno: 2, strerrno: No such file or directory
進程終止的方式
-
_exit()函數
這個函數是一個系統調用函數,參數是進程要傳遞的退出碼
-
exit()函數
這個函數是C語言自帶的函數,與_exit()的用法是一樣的
-
return退出
return是?種更常?的退出進程?法。執?return n等同于執?exit(n),因為調?main的運?時函數會將main的返回值當做 exit的參數。
_exit()與exit()的區別:
exit最后也會調?_exit, 但在調?_exit之前,還做了其他?作 :
- 執???通過 atexit或on_exit定義的清理函數
- 關閉所有打開的流,所有的緩存數據均被寫?
- 調?_exit
從這個過程可知:使用exit()退出進程會將緩存區的內容刷新出來,之后再關閉,而_exit()直接關閉進程,沒有上述步驟。
三、進程等待
進程等待的方式
之前演示過當子進程運行結束而父進程沒有回收子進程的話,子進程會一直處于僵尸狀態,進?造成內存泄漏。并且?進程派給?進程的任務完成的如何,我們需要知道。如,?進程運?完成,結果對還是不對,或者是否正常退出。因此?進程需要通過進程等待的?式,回收?進程資源,獲取?進程退出信息。
pid_t wait(int status);*
返回值:成功返回被等待進程pid,失敗返回-1。
參數:輸出型參數,獲取?進程退出狀態,不關?則可以設置成為NULL
*pid_ t waitpid(pid_t pid, int status, int options);
-
返回值:
當正常返回的時候waitpid返回收集到的?進程的進程ID;
如果設置了選項WNOHANG,?調?中waitpid發現沒有已退出的?進程可收集,則返回0;
如果調?中出錯,則返回-1,這時errno會被設置成相應的值以指?錯誤所在;
-
參數:
-
pid:
Pid=-1,等待任?個?進程。與wait等效。
Pid>0.等待其進程ID與pid相等的?進程。 -
status: 輸出型參數
WIFEXITED(status): 若為正常終??進程返回的狀態,則為真。(查看進程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED?零,提取?進程退出碼。(查看進程的退出碼) -
options:
默認為0,表?阻塞等待
WNOHANG: 若pid指定的?進程沒有結束,則waitpid()函數返回0,不予以等待。若正常結束,則返回該?進程的ID。
-
使用要點:
- 如果?進程已經退出,調?wait/waitpid時,wait/waitpid會?即返回,并且釋放資源,獲得?進程退出信息。
- 如果在任意時刻調?wait/waitpid,?進程存在且正常運?,則進程可能阻塞。
- 如果不存在該?進程,則?即出錯返回。
獲取?進程status
- wait和waitpid,都有?個status參數,該參數是?個輸出型參數,由操作系統填充。
- 如果傳遞NULL,表?不關??進程的退出狀態信息。
- 否則,操作系統會根據該參數,將?進程的退出信息反饋給?進程。
- status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16?特位):
解釋:這張圖表示status獲取子進程狀態之后數據的分布情況,可以看到只用到了低16位的bit位,但是分了兩種不同的情況:
- 正常終止:8~15bit位儲存進程退出信息也就是子進程返回的退出碼
- 當子進程被信號所殺:0~7bit的位置儲存終止信號的信息。
舉例:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id < 0){printf("errno : %d, errstring: %s\n", errno, strerror(errno));return errno;}else if(id == 0){int cnt = 5;while(cnt){printf("子進程運行中, pid: %d\n", getpid());cnt--;sleep(1);}exit(123);}else{sleep(3);int status = 0;pid_t rid = waitpid(id, &status, 0); // == waitif(rid > 0){printf("wait sub process success, rid: %d, status code: %d, singal: %d\n", rid, status >> 8 & 0xFF, status & 0x0F );//這里我們可以用這種位運算的方法,也可以使用系統提供的宏WEXITSTATUS(status),這些在前面有//if(WIFEXITED(status))//{// printf("子進程正常推出!\n");//}//else//{// //.....//}}elseperror("waitpid");while(1){printf("我是父進程: pid:%d\n", getpid());sleep(1);}}return 0;
}
運行結果:
#運行窗口
[lisihan@hcss-ecs-b735 lession15]$ ./code3
子進程運行中, pid: 16597
子進程運行中, pid: 16597
子進程運行中, pid: 16597
子進程運行中, pid: 16597
子進程運行中, pid: 16597
wait sub process success, rid: 16597, status code: 123, singal: 0
我是父進程: pid:16596
我是父進程: pid:16596
我是父進程: pid:16596
我是父進程: pid:16596
我是父進程: pid:16596#監視窗口PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND #進程未啟動
14111 16594 16593 14111 pts/2 16593 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND #進程啟動,創建父子進程
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
16596 16597 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16601 16600 14111 pts/2 16600 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
16596 16597 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16606 16605 14111 pts/2 16605 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
16596 16597 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16611 16610 14111 pts/2 16610 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
16596 16597 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16616 16615 14111 pts/2 16615 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
16596 16597 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16621 16620 14111 pts/2 16620 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND #子進程運行結束,父進程回收子進程
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16626 16625 14111 pts/2 16625 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16641 16640 14111 pts/2 16640 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND #進程全部結束14111 16641 16640 14111 pts/2 16640 S+ 1000 0:00 grep --color=auto code3
此時我們可以談談關于進程退出:
- 子進程代碼跑完之后,結果是否正確是用退出碼判定的
- 子進程出現異常,比如說野指針、數據溢出等情況,OS會使用信號直接終止這個進程(有關信號的問題后面再談),進程退出信息中,會記錄自己的退出信號
- 進程的退出碼和退出信號都會在進程的內核數據結構task_struct中維護
小程序
學習了上面的內容,我們可以寫一個小程序,用于定時備份一個vector中的內容:
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <vector>
#include <string>std::vector<int> data;int save()
{pid_t id = fork();if(id == 0)//子進程進行拷貝操作{std::string name = "./backup/"; //拷貝文件放在backup文件夾中name += std::to_string(time(nullptr)); //文件名稱為拷貝的時間#############################################################name += ".backup"; //文件名后綴FILE* fp = fopen(name.c_str(), "w"); //文件創建+打開if(fp == nullptr) return 1;std::string data_str;for(auto d :data) //把data中的數據轉化為字符串儲存在data_str中{data_str += std::to_string(d);data_str += " ";}fputs(data_str.c_str(), fp); //文件寫入fclose(fp); //文件關閉exit(0);}else //父進程阻塞等待子進程拷貝完成{int status;pid_t pid = waitpid(id, &status, 0);if(pid > 0) //等待成功{printf("wait child process success!, exit code: %d\n", WEXITSTATUS(status));}else //等待失敗{printf("wait child process...\n");}}return 0;
}int main()
{int cnt = 0;while(true){data.push_back(cnt++); //data數據寫入sleep(1); if(cnt % 10 == 0)save(); //拷貝操作}return 0;
}
程序運行:
#運行窗口
[lisihan@hcss-ecs-b735 lession15]$ g++ -o code4 code4.cpp -std=c++11
[lisihan@hcss-ecs-b735 lession15]$ ./code4
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0#監視窗口
[lisihan@hcss-ecs-b735 backup]$ ls
1748787298.backup 1748787308.backup 1748787354.backup 1748787364.backup 1748787374.backup
[lisihan@hcss-ecs-b735 backup]$ cat 1748787374.backup
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 [lisihan@hcss-ecs-b735 backup]$ ls
1748787298.backup 1748787364.backup 1748787394.backup 1748787424.backup
1748787308.backup 1748787374.backup 1748787404.backup 1748787434.backup
1748787354.backup 1748787384.backup 1748787414.backup
程序運行正常
阻塞與非阻塞等待
阻塞等待:如果子進程沒有退出或終止,父進程會一直等待直到子進程退出,父進程才會執行之后的代碼
非阻塞等待:即使子進程沒有退出,父進程只會在waitpid函數中判斷一次,不會一直在函數中等待子進程退出,在等待子進程退出的這段時間,父進程可以去做其他事情。
選擇阻塞還是非阻塞等待由waitpid函數中的第三個形參決定的
options:
默認為0,表?阻塞等待
WNOHANG(理解為wait no hang): 若pid指定的?進程沒有結束,則waitpid()函數返回0,不予以等待。若正常結束,則返回該?進程的ID。
具體就不演示了
四、進程程序替換
之前我們使用函數fork()
創建子進程, fork()
之后,??各?執??進程代碼的?部分如果?進程就想執??個全新的程序呢?進程的程序替換來完成這個功能。程序替換是通過特定的接?,加載磁盤上的?個全新的程序(代碼和數據),加載到調?進程的地址空間中!
以execl()
函數為例,這個函數用于進程程序替換,后面會詳細介紹,我們執行下面的代碼:
#include <iostream>
#include <unistd.h>int main()
{execl("/bin/ls", "-a", "-l", nullptr); return 0;
}
運行結果:
[lisihan@hcss-ecs-b735 lession15]$ ./code5
total 108
drwxrwxr-x 2 lisihan lisihan 4096 Jun 1 22:17 backup
-rwxrwxr-x 1 lisihan lisihan 9024 May 31 18:31 code1
-rw-rw-r-- 1 lisihan lisihan 94 May 31 18:29 code1.cpp
-rwxrwxr-x 1 lisihan lisihan 8528 May 31 19:05 code2
-rw-rw-r-- 1 lisihan lisihan 313 May 31 19:04 code2.c
-rwxrwxr-x 1 lisihan lisihan 8784 Jun 1 20:40 code3
-rw-rw-r-- 1 lisihan lisihan 1053 Jun 1 21:37 code3.c
-rwxrwxr-x 1 lisihan lisihan 30040 Jun 1 22:15 code4
-rw-rw-r-- 1 lisihan lisihan 1002 Jun 1 22:17 code4.cpp
-rwxrwxr-x 1 lisihan lisihan 8760 Jun 2 20:48 code5
-rw-rw-r-- 1 lisihan lisihan 116 Jun 2 20:46 code5.cpp
-rw-rw-r-- 1 lisihan lisihan 176 Jun 2 20:47 makefile
[lisihan@hcss-ecs-b735 lession15]$ ls -a -l
total 116
drwxrwxr-x 3 lisihan lisihan 4096 Jun 2 20:48 .
drwx------ 22 lisihan lisihan 4096 May 31 18:27 ..
drwxrwxr-x 2 lisihan lisihan 4096 Jun 1 22:17 backup
-rwxrwxr-x 1 lisihan lisihan 9024 May 31 18:31 code1
-rw-rw-r-- 1 lisihan lisihan 94 May 31 18:29 code1.cpp
-rwxrwxr-x 1 lisihan lisihan 8528 May 31 19:05 code2
-rw-rw-r-- 1 lisihan lisihan 313 May 31 19:04 code2.c
-rwxrwxr-x 1 lisihan lisihan 8784 Jun 1 20:40 code3
-rw-rw-r-- 1 lisihan lisihan 1053 Jun 1 21:37 code3.c
-rwxrwxr-x 1 lisihan lisihan 30040 Jun 1 22:15 code4
-rw-rw-r-- 1 lisihan lisihan 1002 Jun 1 22:17 code4.cpp
-rwxrwxr-x 1 lisihan lisihan 8760 Jun 2 20:48 code5
-rw-rw-r-- 1 lisihan lisihan 116 Jun 2 20:46 code5.cpp
-rw-rw-r-- 1 lisihan lisihan 176 Jun 2 20:47 makefile
結果表明這個函數可以實現程序替換,用我們自己寫的code5來執行ls -a -l
命令。
替換原理
?fork創建?進程后執?的是和?進程相同的程序(但有可能執?不同的代碼分?),?進程往往要調??種exec函數以執?另?個程序。當進程調??種exec函數時,該進程的??空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執?。調?exec并不創建新進程,所以調?exec前后該進程的id并未改變。
重點:
-
進程程序替換沒有創建子進程。只是將這個進程的PCB中的虛擬內存通過頁表映射的物理內存中的代碼段和數據段替換成了另一個程序的代碼和數據。由于進程中原有的代碼和數據都被替換了,所以原來程序后面的代碼就不會再運行了
-
進程程序替換不僅可以執行系統的程序,同樣可以執行我們自己創建的可執行程序
-
調?exec前后該進程的id并未改變,這個同樣我們可以證明一下
//code5.cpp #include <iostream> #include <unistd.h> int main() {printf("my pid: %d, ppid: %d\n", getpid(), getppid());execl("./code3", "code3", nullptr); return 0; } //code3.c #include <stdio.h> #include <string.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() {pid_t id = fork();if(id < 0){printf("errno : %d, errstring: %s\n", errno, strerror(errno));return errno;}else if(id == 0){int cnt = 3;while(cnt){printf("子進程運行中, pid: %d\n", getpid());cnt--;sleep(1);}exit(123);}else{sleep(20);int status = 0;pid_t rid = waitpid(id, &status, 0); // == waitif(rid > 0)printf("wait sub process success, rid: %d, status code: %d, singal: %d\n", rid, status >> 8 & 0xFF, status & 0x0F );elseperror("waitpid");while(1){printf("我是父進程: pid:%d\n", getpid());sleep(1);}}return 0; }
運行結果:
[lisihan@hcss-ecs-b735 lession15]$ ./code5my pid: 29566, ppid: 28461子進程運行中, pid: 29567子進程運行中, pid: 29567子進程運行中, pid: 29567子進程運行中, pid: 29567子進程運行中, pid: 29567wait sub process success, rid: 29567, status code: 123, singal: 0我是父進程: pid:29566我是父進程: pid:29566我是父進程: pid:29566我是父進程: pid:29566我是父進程: pid:29566
如果想要進程程序替換但是自己本身又要執行后面的代碼,我們可以利用
fork()
函數創建一個子進程來進程程序替換,自己的進程繼續運行后續的代碼,具體就不演示了。其實這與我們shell的功能有一些類似,shell也是一個進程,這個進程接收到用戶輸入會fork一個子進程,然后根據用戶輸入execl其他程序 -
從進程的程序替換我們可以知道,進程的數據和代碼也可以發生寫時拷貝,此時進程就是徹底獨立的
進程程序替換的接口——exec替換函數
所有函數:
include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, 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[]);
int execve(const char *path, char *const argv[], char *const envp[]);
這些函數的不同點在于傳參方式的不同,根據函數名可以分為以下傳參方式,這些函數除了execve
是系統調用函數,其他函數都是C語言提供的庫函數,這些庫函數都是復用execve
函數實現的:
l(list) : 表?參數采?列表
v(vector) : 參數?數組
p(path) : 有p?動搜索環境變量PATH
e(env) : 表???維護環境變量
這個表中如果帶路徑表示用戶不需要自己寫路徑,對于環境變量也是一樣的
- 這些函數如果調?成功則加載新的程序從啟動代碼開始執?,不再返回。
- 如果調?出錯則返回-1
- 所以exec函數只有出錯的返回值?沒有成功的返回值。
- 在使用列表類函數的時候傳參的最后一個要加上nullptr