Linux:程序控制(fork/vfork)(程序終止:exit/_exit)(程序等待:wait/waitpid/status)(程序替換:exec函式/shell實現)
目錄
程序建立
fork原理
在Linux中fork函式非常重要,它從已經存在的程序中建立一個新的子程序。
新程序為子程序,原程序為父程序。(以父程序為模板來建立子程序)
(pcb,記憶體指標,程式計數器,上下文資料)從下一句程式碼開始執行
每個程序都有自己的虛擬地址空間,子程序複製的只是資料,虛擬地址空間父子程序各一份
,因此父子程序資料的是獨有的,但是程式碼共享。
#include<unistd.h>
pid_t fork(void);
//返回值:子程序中返回0,父程序返回子程序的id,出錯返回-1
程序呼叫fork,當控制轉移到核心中的fork程式碼之後,核心做:
- 分配新的記憶體塊和核心資料結構給子程序
- 將父程序部分資料結構內容拷貝至子程序
- 新增子程序到系統程序列表中
- fork返回,開始排程器排程
當一個程序呼叫fork之後,就有兩個二進位制程式碼相同的程序,而且執行到相同的地方
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<errno.h> 4 int main() 5 { 6 pid_t pid; 7 printf("before:pid : %d\n",getpid()); 8 pid = fork(); 9 if(pid == -1) 10 { 11 perror("fork"); 12 } 13 printf("after:pid :%d and fork return %d\n",getpid(),pid); 14 sleep(1); 15 return 0; 16 }
- 有三行輸出,一行before,兩行after程序3545先列印before還有after,另一個只有after
- 因為子程序是從fork建立之後開始執行,而不是從main函式開始執行,所以不會列印before
- fork之後父子程序兩個執行流分別執行,誰先完全由排程器決定
fork函式返回值
- 子程序返回0
- 父程序返回子程序的id
為什麼不能反過來?
系統提供了getpid()和getppid()可以獲取當前程序和的、當前程序的父程序的id,但是無法獲得子程序的id,所以父程序必須返回子程序id(才能知道子程序id)
寫時拷貝:通常,父子程式碼共享,父子不在寫入時,資料也是共享的,當任意一方試圖寫入時,便以寫時拷貝的方式各自有一份副本
(修改時更新頁表,指向新的記憶體區域)
fork用法和呼叫失敗的原因
用法
- 一個父程序希望複製自己,使父子程序實現不同的程式碼段(例如:父程序等待客戶端請求,生成子程序取處理請求)
- 一個程序要執行一個不同的程式(例如:子程序從fork返回後,呼叫exec函式)
呼叫失敗原因
- 系統中有太多程序
- 實際使用者的程序超過了限制
vfork函式
同樣用來建立子程序,但是
- vfork用於建立一個子程序,而子程序和父程序共享地址空間(fork的子程序有獨立的地址空間)
- vfork保證子程序先執行,在它呼叫exec或exit之後父程序才可能被排程執行
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<errno.h>
4 #include<stdlib.h>
5 int i = 100;
6 int main()
7 {
8 pid_t pid = vfork();
9 if(pid == -1)
10 {
11 perror("vfork");
12 return -1;
13 }
14 else if(pid == 0)
15 {
16 i = 200;
17 printf("child: i = %d pid:%d\n",i,getpid());
18 exit(0);
19 }
20 else
21 {
22 printf("parent: i = %d pid:%d\n",i,getpid());
23 }
24
25 return 0;
26 }
- 父子程序輸出都是200,可見子程序改變了父程序的變數值,因為子程序在父程序的地址空間中執行
- 子程序沒有執行其他程式或者退出之前,父程序阻塞在vfork處不返回(vfork建立子程序先執行,直到exit(0)退出,父程序接著後面程式碼執行)
- 如果不加exit(0),則子程序一直跑完程式碼,return返回,但是由於父子程序共享所以資源以及釋放,導致父程序死迴圈(呼叫棧以及混亂)
總結
1. fork ():子程序拷貝父程序的資料段,程式碼段 ,子程序有自己的虛擬地址空間,父子程序資料獨有(寫時拷貝)程式碼共享
vfork ( ):子程序與父程序共享資料段 (共享虛擬地址空間)
2. fork ()父子程序的執行次序不確定
vfork ()保證子程序先執行,在呼叫exec 或exit 之前與父程序資料是共享的,在它呼叫exec
或exit 之後父程序才可能被排程執行。
3. vfork ()保證子程序先執行,在它呼叫exec 或exit 之後父程序才可能被排程執行。如果在
呼叫這兩個函式之前子程序依賴於父程序的進一步動作,則會導致死鎖。
程序終止
程序退出場景:
- 程式碼執行完畢,結果正確
- 程式碼執行完畢,結果不正確
- 程式碼異常終止
程序常見退出方法
- 從main返回
- 呼叫exit
- _exit
- 異常退出(ctrl + c)
return退出
return 是一種最常見的退出方式,執行return n 相當於exit(n),因為呼叫main函式時將會呼叫main的返回值當exit的引數
_exit函式
#include<unistd.h>
void _exit(int status);
//引數:status 定義了程序的終止狀態,父程序通過wait來獲取該值
直接釋放資源(粗暴)
說明:雖然status是int,但是僅有低8位可以被父程序使用,所以_exit(-1),在終端執行時返回255
echo $? 可以獲取狀態返回碼(0--255)僅有低8位!
exit函式
#include<unistd.h>
void exit(int status);
- exit(逐步釋放資源)最後也會呼叫_exit,但在這之前,會做其他工作
- 先執行使用者通過atexit或on_exit定義的清理函式
- 關閉所有開啟的流,所有快取資料均被寫入
- 呼叫_exit
具體區別如下:逐步釋放和粗暴釋放
總結:
return:只有在main中執行還會退出程序,在main中return跟呼叫exit效果一樣
exit:在任意位置都可以退出程序,退出前會重新整理緩衝,關閉檔案,做很多操作
_exit:粗暴的退出程序,什麼也不幹,直接釋放資源
程序等待
程序等待的重要性
- 子程序退出,父程序不管不顧可能會造成殭屍程序的問題,而造成記憶體洩漏
- 一旦變成殭屍狀態,就很難殺死,kill -9也不行,因為誰也不能殺死一個死去的程序
- 父程序給子程序的任務完成的如何,我們需要指定
- 父程序通過等待的方式,回收子程序資源,獲取子程序資訊
- 為什麼要等待:一個是因為程序之間有競爭性!,另一個是避免產生殭屍程序
程序等待的方法
阻塞:為了完成一個功能發起函式呼叫,然後沒有完成這個功能則一直掛起等待,直到完成才返回。
非阻塞:為了完成一個功能發起一個函式,如果現在不具備完成的條件,則立即返回不等待。
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
// 返回值:成功返回被等待程序pid,失敗返回-1
//引數:輸出型引數,獲取子程序的退出狀態,不關心則設定為NULL
功能:等待子程序退出(阻塞式呼叫:如果沒有子程序退出,就一直等待不返回,直到子程序退出)
waitpid方法
pid_t waitpid(pit_t pid,int *status,int options);
//返回值:
正常時候waitpid返回子程序的程序id
如果設定了選擇WNOHANG,而呼叫waitpid發現沒有已退出的子程序可收集,則返回0
如果調用出錯,則返回-1,這時候errno會被設定成相應的值以只是錯誤
//引數:
pid:
pid = -1,等待一個子程序,與wait等效
pid > 0, 等待程序id == pid 的子程序退出
status:
用於獲取子程序退出狀態碼,不關心則設定為NULL
options:
選項引數(WNOHANG:如果沒有子程序退出,則立即報錯返回(非阻塞式呼叫),
如果有則回收資源)
功能:預設等待子程序退出
- 如果子程序已經退出,呼叫wait或waitpid時,wait或waitpid會立即返回,並且釋放資源,獲得子程序退出狀態
- 如果任意時刻呼叫wait或waitpid,子程序存在其正常執行,則父程序可能阻塞
- 如果不存在該子程序,立即出錯返回
總結
wait:等待任意一個程序,若沒有子程序退出則一直等待(阻塞式,必須等到一個子程序退出後獲取退出狀態釋放資源才返回)
waitpid:可以等待指定的子程序退出,也可以等待一個任意的子程序退出(可以設定為非阻塞WNOHNAG)
獲取子程序status
- wait和waitpid,都有一個status引數,該引數是一個輸出型引數,由系統填充
- 如果傳遞NULL,表示不關心子程序的退出狀態資訊
- 否則,作業系統會根據該引數,將子程序的退出資訊反饋給父程序
- status不能當做整形來看待,可以當做點陣圖來看待(只研究低16位)
測試程式碼:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<errno.h>
4 #include<string.h>
5 #include<sys/wait.h>
6 int main()
7 {
8 pid_t pid;
9 pid = fork();
10 if(pid == -1)
11 {
12 perror("fork");
13 exit(1);
14 }
15 else if(pid = 0)
16 {
17 sleep(20);
18 exit(10);
19 }
20 else
21 {
22 sleep(20);
23 int st;
24 int ret = wait(&st);
25 if(ret > 0 && (st & 0x7F) == 0)//st&0x7F:與0x7F相當於取後7位,其餘值0
26 {
27 //正常退出:高8位代表退出狀態
28 printf("child exit code:%d\n",(st>>8)&0x7F);
29 }
30 else if(ret >0)
31 {
32 //異常退出:低8位代表異常退出訊號
33 printf("sig code:%d\n",st&0x7F);
34 }
35 }
36 return 0;
37 }
重開一個終端kill這個程序,就會產生假的異常退出。
kil -l 可以檢視訊號種類
總結:
- 雖然退出狀態用了4個位元組來獲取,但是實際只用了低16位的2個位元組儲存有用的資訊
- 在低16位中,高8位儲存子程序的退出碼,只有子程序執行完畢正常退出才有,低8位為0
- 低8位儲存引起異常退出的訊號值(第8位儲存core dump標誌),只有子程序異常退出是才有,這時高8位為0
- statu & 0x7 判斷是否正常退出,並獲取退出訊號
- statu >> 8 獲取退出碼
- WIFEXIT(是否正常退出,如果是返回ture)
- WEXITSTATUS可以來獲取WIFEXIT的退出碼
程序程式替換
替換原理
替換的是程式碼段所指向的程式碼實體記憶體區域
建立一個子程序大多時候,並不希望子程序做跟父程序相同的事情,而是希望執行一些其他的程式碼程式,這時候就用到了程序替換,程式替換隻是替換了程式碼段,初始化了資料區域,因此程式替換不會重新建立虛擬地址空間和頁表,只是替換了其中的內容,並且替換後子程序這個程序將從入口函式開始執行。
因為程式碼段被替換,因此在替換之後的原始碼不會再被執行,因為程式碼段中已經沒有這些程式碼
用fork建立子程序後執行的是和父程序相同的程式(但有可能是不同的程式碼分支),子程序往往呼叫一種exec函式以執行另一個程式,
當程序呼叫一種exec函式時,該程序的使用者空間程式碼和資料完全被新程式替換,從新程式的啟動例程開始執行。呼叫exec不建立新的程序,
所以前後程序的id並未改變。
替換函式
- 有6種以exec開頭的函式,統稱為exec函式
- execl
- execlp
- execle
- execv
- execvp
- execve
函式解釋與命名原理
- l(list):表示引數採用列表
- v(vector):引數用陣列
- p(path):有p自動搜尋環境變數PATH(不需要告訴路徑,只需要告訴檔名)
- e(env):表示自己維護環境變數
- 這些函式如果呼叫成功則載入新的程式從啟動程式碼開始執行,不在返回
- 如果調用出錯則返回-1
- 所以exec函式只有出錯的返回值而沒有成功的返回值
- 標頭檔案:<unistd.h>
總結
execl與execv區別:引數如何賦予(引數平鋪,指標陣列)
execl與execlp區別:
excl需要告訴作業系統這個程式的檔案全路徑
exclp不需要告訴路徑,只需要告訴檔名,會自動到PATH尋找
execl與excle區別:
execl繼承於父程序的環境變數
execle自己由我們使用者來組織環境變數
1 #include<unistd.h>
2
3 int main()
4 {
5 char *const argv[] = {"ps","-ef",NULL};
6 char *const envp[] = {"PATH=/bin:/usr/bin","TERM=console",NULL};
7
8 execl("/bin/ps","ps","-ef",NULL);
9
10 execlp("ps","ps","-ef",NULL,envp);
11
12 execv("/bin/ps",argv);
13
14 execvp("ps",argv);
15
16 execve("/bin/ps",argv,envp);
17 return 0;
18 }
事實上,只有execve值真正的系統呼叫,其他五個函式最終都呼叫execve,所以execve在man手冊第2節,其他函式在man第3節
關係如下圖所示:
簡單的shell
程式碼:
shell程式碼非常簡單隻能完成最基本的命令
優化程式碼:
實現一個簡單的shell - W_J_F_的部落格 - CSDN部落格 https://blog.csdn.net/W_J_F_/article/details/83618383
實現了
1:解決的無法識別多空格的問題(ls -l)
2:解決的無任何輸入時回車死迴圈的問題
shell程式碼非常簡單隻能完成最基本的命令
1 //自己實現一個簡單的shell
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<stdlib.h>
5 #include<errno.h>
6 #include<string.h>
7 //1:獲取終端輸入
8 //2:解析輸入(按空格解析到一個一個的命令引數)
9 //3:建立一個子程序:在子程序中級進行程式替換,讓子程序執行命令
10 //4:等待子程序執行完畢,收屍,獲取退出狀態碼
11
12 int argc;
13 char *argv[32];
14 int param_parse(char *buff)
15 {
16 if(buff == NULL)
17 return -1;
18 char *ptr = buff;
19 char *tmp = ptr;
20
21 argc = 0;
22 while((*ptr) != '\0')
23 {
24 if((*ptr) == ' '&& *(ptr + 1) != ' ')
25 {
26 //當遇見空格且下一個位置不是空格
27 //將空格置為‘\0’
28 //但是我們使用argv[argc]來儲存這個字串位置
29 *ptr = '\0';
30 argv[argc] = tmp;
31 tmp = ptr + 1;
32 argc++;
33 }
34 ptr++;
35 }
36 argv[argc++] = tmp;
37 argv[argc] = NULL;
38 return 0;
39 }
40 int do_exec()
41 {
42 int pid = 0;
43 pid = fork();
44 if(pid < 0)
45 {
46 perror("fork");
47 return -1;
48 }
49 else if (pid == 0)
50 {
51 execvp(argv[0],argv);
52 exit(0);
53 }
54 //父程序在這裡必須等待子程序退出,來觀察子程序為什麼會退出
55 //是否出現了什麼錯誤,通過獲取狀態碼,並且轉換退出碼所對應
56 //的錯誤資訊進行列印
57 int statu;
58 wait(&statu);
59 //判斷子程序是否程式碼執行完畢退出
60 if(WIFEXITED(statu))
61 {
62 //獲取到子程序的退出碼,轉換為文字資訊
63 printf("%s",strerror(WEXITSTATUS(statu)));
64 }
65 return 0;
66 }
67
68 int main()
69 {
70 while(1)
71 {
72 printf("shell> ");
73 char buff[1024] = {0};
74 scanf("%[^\n]%*c",buff);
75 //%[^\n] 獲取資料直到遇見\n為止
76 //%*c 清空緩衝區,資料都不要(不然還存有是上一個\n)
77 printf("%s\n",buff);
78 param_parse(buff);
79 do_exec();
80 }
81 return 0;
82 }
圖解
時間軸表示發生次序,shell從使用者讀入字串‘ls’,shell建立一個新的程序,然後在那個程序中執行ls並等待那個程序結束
然後shell讀取新的一行輸入,建立一個新的程序,在這個程序中執行程式,並等待這個程式結束
所以寫一個shell,需要迴圈一下過程
- 獲取命令
- 解析命令
- 建立一個子程序fork,並進行程序替換execvp
- 父程序等待子程序的退出wait
總結
一個從程式有很多函式組成,一個函式具有呼叫另一個函式,同時傳遞給它一些引數。
被呼叫的函式執行一定的操作,然後返回一個值,每個函式都有他的區域性變數,不同的是
函式通過call/return系統進行通訊,這種通過引數和返回值在擁有私有資料的函式之間通訊的模式
是結構化程式設計的基礎。如下圖:
一個c程式可以fork/exec另一個程式,並給它傳一些引數,這個被呼叫的程式執行一定的操作
然後通過exit(n)產生返回值,呼叫它的程序可以通過wait(&ret)來獲取exit的返回值