一、進程創建1、fork函數初識
在linux中fork函數是非常重要的函數,它從已存在進程中創建一個新進程。新進程為子進程,而原進程為父進程。
代碼語言:JavaScript代碼運行次數:0運行復制
#include <unistd.h>pid_t fork(void);返回值:?進程中返回0,?進程返回?進程id,出錯返回-1
進程調用fork,當控制轉移到內核中的fork代碼后,內核做:
分配新的內存塊和內核數據結構給子進程將父進程部分數據結構內容拷貝至子進程添加子進程到系統進程列表當中fork返回,開始調度器調度

當一個進程調用fork之后,就有兩個二進制代碼相同的進程。而且它們都運行到相同的地方。但每個進程都將可以開始它們自己的旅程,看如下程序。
代碼語言:javascript代碼運行次數:0運行復制
int main(){pid_t pid;printf("Before: pid is %dn", getpid());if ( (pid=fork()) == -1 ){perror("fork()");exit(1);}printf("After:pid is %d, fork return %dn", getpid(), pid);sleep(1);return 0;} **運?結果**:[root@localhost linux]# ./a.outBefore: pid is 43676After:pid is 43676, fork return 43677After:pid is 43677, fork return 0
這里看到了三行輸出,一行before,兩行after。進程43676先打印before消息,然后它有打印after。另一個after消息有43677打印的。注意到進程43677沒有打印before,為什么呢?如下圖所示

所以,fork之前父進程獨立執行,fork之后,父子兩個執行流分別執行。注意,fork之后,誰先執行完全由調度器決定。
2、fork函數返回值子進程返回0,父進程返回的是子進程的pid。3、寫時拷貝
通常,父子代碼共享,父子在不寫入時,數據也是共享的,當任意一方試圖寫入,便以寫時拷貝的方式各自一份副本。具體見下圖:

因為有寫時拷貝技術的存在,所以父子進程得以徹底分離離!完成了進程獨立性的技術保證! 寫時拷貝,是一種延時申請技術,可以提高整機內存的使用率
4、fork常規用法一個父進程希望復制自己,使父子進程同時執行不同的代碼段。例如,父進程等待客戶端請求,生成子進程來處理請求。一個進程要執行一個不同的程序。例如子進程從fork返回后,調用exec函數。5、fork調用失敗的原因系統中有太多的進程實際用戶的進程數超過了限制二、進程終止
進程終止的本質是釋放系統資源,就是釋放進程申請的相關內核數據結構和對應的數據和代碼。
1、進程退出場景代碼運行完畢,結果正確代碼運行完畢,結果不正確代碼異常終止2、進程常見退出方法
正常終止(可以通過 echo $? 查看進程退出碼):
從main返回調用exit_exit
異常退出: ? ctrl + c,信號終止
2.1、退出碼
退出碼(退出狀態)可以告訴我們最后一次執行的命令的狀態。在命令結束以后,我們可以知道命令是成功完成的還是以錯誤結束的。其基本思想是,程序返回退出代碼 0 時表示執行成功,沒有問題。代碼 1 或 0 以外的任何代碼都被視為不成功。 Linux Shell 中的主要退出碼:

退出碼 0 表示命令執行無誤,這是完成命令的理想狀態。退出碼 1 我們也可以將其解釋為 “不被允許的操作”。例如在沒有 sudo 權限的情況下使用apt;再例如除以 0 等操作也會返回錯誤碼 1 ,對應的命令為 let a=1/0130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等終止信號是非常典型的,它們屬于 128+n 信號,其中 n 代表終止碼??梢允褂?a href="http://m.babyishan.com/tag/strerror">strerror函數來獲取退出碼對應的描述。2.2、_exit函數代碼語言:javascript代碼運行次數:0運行復制
#include <unistd.h>void _exit(int status);參數:status 定義了進程的終?狀態,?進程通過wait來獲取該值
說明:雖然status是int,但是僅有低8位可以被父進程所用。所以_exit(-1)時,在終端執行$?發現返回值是255。2.3、exit函數代碼語言:javascript代碼運行次數:0運行復制
#include <unistd.h>void exit(int status);
exit最后也會調用_exit, 但在調用_exit之前,還做了其他工作:
執行用戶通過 atexit或on_exit定義的清理函數。關閉所有打開的流,所有的緩存數據均被寫入調用_exit

實例:
代碼語言:javascript代碼運行次數:0運行復制
int main(){printf("hello");exit(0);} **運?結果**:[root@localhost linux]# ./a.outhello[root@localhost linux]#//int main(){printf("hello");_exit(0);} **運?結果**:[root@localhost linux]# ./a.out[root@localhost linux]#
2.4、return退出
return是一種更常見的退出進程方法。執行return n等同于執行exit(n),因為調用main的運行時函數會將main的返回值當做 exit的參數。
三、進程等待1、進程等待必要性之前講過,子進程退出,父進程如果不管不顧,就可能造成‘僵尸進程’的問題,進而造成內存泄漏。另外,進程一旦變成僵尸狀態,那就刀槍不入,“殺人不眨眼”的kill -9 也無能為力,因為誰也沒有辦法殺死一個已經死去的進程。最后,父進程派給子進程的任務完成的如何,我們需要知道。如,子進程運行完成,結果對還是不對,或者是否正常退出。父進程通過進程等待的方式,回收子進程資源,獲取子進程退出信息2、進程等待的方法2.1、wait方法代碼語言:javascript代碼運行次數:0運行復制
#include<sys/types.h>#include<sys/wait.h>pid_t wait(int* status);返回值:成功返回被等待進程pid,失敗返回-1。參數:輸出型參數,獲取?進程退出狀態,不關?則可以設置成為NULL
2.2、waitpid方法代碼語言:javascript代碼運行次數:0運行復制
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,子進程存在且正常運行,則進程可能阻塞。如果不存在該子進程,則立即出錯返回。

2.3、獲取子進程statuswait和waitpid,都有一個status參數,該參數是一個輸出型參數,由操作系統填充。如果傳遞NULL,表示不關心子進程的退出狀態信息。否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程。status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16比特位):

測試代碼:
代碼語言:javascript代碼運行次數:0運行復制
#include <sys/wait.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>int main( void ){pid_t pid;if ( (pid=fork()) == -1 )perror("fork"),exit(1);if ( pid == 0 ){sleep(20);exit(10);} else {int st;int ret = wait(&st);if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 正常退出printf("child exit code:%dn", (st>>8)&0XFF);} else if( ret > 0 ) { // 異常退出printf("sig code : %dn", st&0X7F );}}} **測試結果**:# ./a.out #等20秒退出child exit code:10# ./a.out #在其他終端kill掉sig code : 9
2.4、阻塞與非阻塞等待進程的阻塞等待方式:代碼語言:javascript代碼運行次數:0運行復制
int main(){pid_t pid;pid = fork();if(pid < 0){printf("%s fork errorn",__FUNCTION__);return 1;} else if( pid == 0 ){ //childprintf("child is run, pid is : %dn",getpid());sleep(5);exit(257);} else{int status = 0;pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5Sprintf("this is test for waitn");if( WIFEXITED(status) && ret == pid ){printf("wait child 5s success, child return code is:%d.n",WEXITSTATUS(status));}else{printf("wait child failed, return.n");return 1;}} return 0;} **運?結果**:[root@localhost linux]# ./a.outchild is run, pid is : 45110this is test for waitwait child 5s success, child return code is :1.
進程的非阻塞等待方式:代碼語言:javascript代碼運行次數:0運行復制
#include <stdio.h>#include <stdlib.h>#include <sys/wait.h>#include <unistd.h>#include <vector>typedef void (*handler_t)(); // 函數指針類型std::vector<handler_t> handlers; // 函數指針數組void fun_one() {printf("這是?個臨時任務1n");} void fun_two() {printf("這是?個臨時任務2n");} void Load() {handlers.push_back(fun_one);handlers.push_back(fun_two);} void handler() {if (handlers.empty())Load();for (auto iter : handlers)iter();} int main() {pid_t pid;pid = fork();if (pid < 0) {printf("%s fork errorn", __FUNCTION__);return 1;} else if (pid == 0) { // childprintf("child is run, pid is : %dn", getpid());sleep(5);exit(1);} else {int status = 0;pid_t ret = 0;do {ret = waitpid(-1, &status, WNOHANG); // ?阻塞式等待if (ret == 0) {printf("child is runningn");} handler();} while (ret == 0);if (WIFEXITED(status) && ret == pid) {printf("wait child 5s success, child return code is :%d.n",WEXITSTATUS(status));} else {printf("wait child failed, return.n");return 1;}} return 0;}
四、進程程序替換
fork() 之后,父子各自執行父進程代碼的一部分如果子進程就想執行一個全新的程序呢?進程的程序替換來完成這個功能! 程序替換是通過特定的接口,加載磁盤上的一個全新的程序(代碼和數據),加載到調用進程的地址空間中!
1、替換原理
用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分?),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec并不創建新進程,所以調用exec前后該進程的id并未改變。

2、替換函數
其實有六種以exec開頭的函數,統稱exec函數: #include
2.1、函數解釋這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回。如果調用出錯則返回-1所以exec函數只有出錯的返回值而沒有成功的返回值。2.2、命名理解
這些函數原型看起來很容易混,但只要掌握了規律就很好記。
l(list) : 表示參數采用列表v(vector) : 參數用數組p(path) : 有p自動搜索環境變量PATHe(env) : 表示自己維護環境變量
函數名
參數格式
是否帶路徑
是否使用當前環境變量
execl
列表
不是
是
execlp
列表
是
是
execle
列表
不是
不是,須自己組裝環境變量
execv
數組
不是
是
execvp
數組
是
是
execve
數組
不是
不是,須自己組裝環境變量
exec調用舉例如下:
代碼語言:javascript代碼運行次數:0運行復制
#include <unistd.h>int main(){char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/bin/ps", "ps", "-ef", NULL);// 帶p的,可以使?環境變量PATH,?需寫全路徑execlp("ps", "ps", "-ef", NULL);// 帶e的,需要??組裝環境變量execle("ps", "ps", "-ef", NULL, envp);execv("/bin/ps", argv);// 帶p的,可以使?環境變量PATH,?需寫全路徑execvp("ps", argv);// 帶e的,需要??組裝環境變量execve("/bin/ps", argv, envp);exit(0);}
事實上,只有execve是真正的系統調用,其它五個函數最終都調用execve,所以execve在man手冊 第2節,其它函數在man手冊第3節。這些函數之間的關系如下圖所示。 下圖exec函數簇 一個完整的例子:

五、自主Shell命令行解釋器1、目標要能處理普通命令要能處理內建命令要能幫助我們理解內建命令/本地變量/環境變量這些概念要能幫助我們理解shell的允許原理2、實現原理
考慮下面這個與shell典型的互動:
代碼語言:javascript代碼運行次數:0運行復制
[root@localhost epoll]# lsclient.cpp readme.md server.cpp utility.h[root@localhost epoll]# ps PID TTY TIME CMD3451 pts/0 00:00:00 bash3514 pts/0 00:00:00 ps 1 2 3 4 5 6
用下圖的時間軸來表示事件的發生次序。其中時間從左向右。shell由標識為sh的方塊代表,它隨著時間的流逝從左向右移動。shell從用戶讀入字符串”ls”。shell建立一個新的進程,然后在那個進程中運行ls程序并等待那個進程結束。

然后shell讀取新的一行輸入,建立一個新的進程,在這個進程中運行程序 并等待這個進程結束。 所以要寫一個shell,需要循環以下過程:
獲取命令行解析命令行建立一個子進程(fork)替換子進程(execvp)父進程等待子進程退出(wait)
根據這些思路,和我們前面的學的技術,就可以自己來實現一個shell了。
3、shell源碼
實現代碼(點擊跳轉);
4、總結
思考函數和進程之間的相似性 exec/exit就像call/return
一個C程序有很多函數組成。一個函數可以調用另外一個函數,同時傳遞給它一些參數。被調用的函數執行一定的操作,然后返回一個值。每個函數都有他的局部變量,不同的函數通過call/return系統進行通信。
這種通過參數和返回值在擁有私有數據的函數間通信的模式是結構化程序設計的基礎。Linux?勵將這種應用于程序之內的模式擴展到程序之間。如下圖

一個C程序可以fork/exec另一個程序,并傳給它一些參數。這個被調用的程序執行一定的操作,然后通過exit(n)來返回值。調用它的進程可以通過wait(&ret)來獲取exit的返回值。