引出
我們學(xué)習(xí)了進(jìn)程,是為了去用多進(jìn)程,那么,為什么需要用到多進(jìn)程呢?
1:為了提高效率,支持大用戶量的并發(fā)。
2:一些大型的服務(wù)器程序,是常年補(bǔ)下電的,需要一直運行,而服務(wù)器系統(tǒng)崩潰是很嚴(yán)重的事情,那么,就需要有多進(jìn)程,一個進(jìn)程掛掉了不能影響另外一個進(jìn)程的使用,這樣,對用戶來說,是體會不到的,不影響業(yè)務(wù)的進(jìn)行。
舉例:銀行怎么提高效率?多開幾個窗口,每個窗口都在做業(yè)務(wù)。
舉例:之前在華為的網(wǎng)關(guān)產(chǎn)品項目中使用多進(jìn)程的舉例。
一:進(jìn)程的創(chuàng)建
一個現(xiàn)有進(jìn)程(父進(jìn)程)可以通過調(diào)用fork()函數(shù)來創(chuàng)建一個跟現(xiàn)有進(jìn)程一模一樣的新進(jìn)程(子進(jìn)程)。
頭文件:? #include <unistd.h>
函數(shù)原型:pid_t fork(void);pid_t 實際就是int類型。
返回值:? 如果創(chuàng)建子進(jìn)程成功,則返回給父進(jìn)程的是子進(jìn)程的id,返回給子進(jìn)程的是0。
? ? ? ? ? 如果失敗,則返回給父進(jìn)程的是-1,并置errno。
注意:1:子進(jìn)程創(chuàng)建的過程:會復(fù)制父進(jìn)程的所有資源,包括堆,棧,rodata段,data段,bss段, 緩沖區(qū)。但是系統(tǒng)相關(guān)信息,代碼段共享。
? ? ? 2:fork之后父,子進(jìn)程誰先執(zhí)行是不確定,取決系統(tǒng)的調(diào)度算法。
? ? ? 3:fork之后,父子進(jìn)程都是從fork下一條語句開始執(zhí)行。
? ? ? 4:fork之后,父子進(jìn)程擁有獨立的4G虛擬地址空間?;ハ嗖挥绊?。
? ? ? 5:fork之后,子進(jìn)程會繼承父進(jìn)程的打開的文件描述符集合,共享文件狀態(tài)標(biāo)志位和文件的偏移量。
例如:打開一個文件,在父親進(jìn)程中用文件描述符偏移到文件的中間。如果此時創(chuàng)建子進(jìn)程,則子進(jìn)程也是也是從文件的中間開始的。
步驟:1:int main(){fork();printf("hello\n");}? hello打印了兩遍,說明進(jìn)程已經(jīng)創(chuàng)建。
2:查看返回值,并且在父子進(jìn)程中個分別打印自己的id。
? 3:解釋下邊代碼運行結(jié)果打印兩邊hello的原因。(原因就是fork出來的子進(jìn)程,會完全的復(fù)制父進(jìn)程的所有資源,包括緩沖區(qū))
? ? printf("hello");
? ? pid = fork();

===================================================================
僵尸子進(jìn)程:子進(jìn)程結(jié)束的時候,父進(jìn)程沒有進(jìn)行收尸操作(父進(jìn)程還存在),此時占用資源。
孤兒進(jìn)程:父進(jìn)程結(jié)束了,子進(jìn)程會變成孤兒進(jìn)程,會自動被init進(jìn)程所收養(yǎng)。
===================================================================
我們看到man幫助中,有個copy_on_write這樣的字眼,這個是什么東西?我們來拓展介紹一下。
《寫時拷貝技術(shù)》
<2>vfork
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
功能:創(chuàng)建子進(jìn)程
參數(shù):無
返回值:成功,對父進(jìn)程而言。返回子進(jìn)程的PID好。
? ? ? ? ? ? ? 對子進(jìn)程而言。返回0.
? ? ? ? ? ? ? 錯誤,返回-1。
fork與vfork的區(qū)別:
<1>fork函數(shù)父子進(jìn)程誰先運行不確定,由系統(tǒng)調(diào)度決定。
? vfork函數(shù)子進(jìn)程先運行,此時父進(jìn)程會阻塞,子進(jìn)程會一直運行在父進(jìn)程的地址空間,直到子進(jìn)程調(diào)用exit結(jié)束后才會運行,如果這時子進(jìn)程修改了某個變量,這將影響到父進(jìn)程的變量。
<2>fork 函數(shù)的正文段共享,其他段被子進(jìn)程復(fù)制。
? ? vfork函數(shù)的子進(jìn)程直接共享父進(jìn)程的虛擬地址空間。
<3> 來看一下父子進(jìn)程對同一文件的操作
先open一個文件,再fork,然后分別再父子進(jìn)程中寫入內(nèi)容,結(jié)果是追加寫
先fork,然后分在父子進(jìn)程中打開同一文件,然后分別寫入內(nèi)容,結(jié)果分別寫
為什么?下邊第一個圖是兩個獨立的進(jìn)程打開同一個文件,第二個圖是先open,再fork之后父子進(jìn)程對文件共享的關(guān)系圖。文件表項是在內(nèi)核空間,進(jìn)程間共享的。


二:進(jìn)程的退出
1:相關(guān)退出函數(shù)
? <1>return? 結(jié)束一個函數(shù)的執(zhí)行。(當(dāng)前程序不一定結(jié)束。)
<2>void exit(int status)[庫函數(shù)]
功能:結(jié)束一個進(jìn)程。結(jié)束之前會刷新緩沖區(qū)。
參數(shù):@status? ? 進(jìn)程狀態(tài)的標(biāo)志。0表示正常結(jié)束,其他表示異常結(jié)束。
<3>void? _exit(int status) [系統(tǒng)調(diào)用]
功能:結(jié)束一個進(jìn)程。結(jié)束之前不會刷新緩沖區(qū)。
參數(shù):@status
2:return和exit的區(qū)別
2.1. return返回函數(shù)值,是關(guān)鍵字;exit是一個函數(shù)。
2.2. return是語言級別的,它表示了調(diào)用堆棧的返回;而exit是系統(tǒng)調(diào)用級別的,它表示了一個進(jìn)程的結(jié)束。
2.3. return是函數(shù)的退出(返回);exit是進(jìn)程的退出。
2.4. return是C語言提供的,exit是操作系統(tǒng)提供的(或者函數(shù)庫中給出的)。
2.5. return用于結(jié)束一個函數(shù)的執(zhí)行,將函數(shù)的執(zhí)行信息傳出個其他調(diào)用函數(shù)使用;exit函數(shù)是退出應(yīng)用程序,刪除進(jìn)程使用的內(nèi)存空間,并將應(yīng)用程序的一個狀態(tài)返回給OS,這個狀態(tài)標(biāo)識了應(yīng)用程序的一些運行信息,這個信息和機(jī)器和操作系統(tǒng)有關(guān),一般是?0 為正常退出,非0 為非正常退出。
2.6. 非主函數(shù)中調(diào)用return和exit效果很明顯,但是在main函數(shù)中調(diào)用return和exit的現(xiàn)象就很模糊,多數(shù)情況下現(xiàn)象都是一致的。
3:exit和_exit的區(qū)別
3.1:exit是庫函數(shù),_exit是系統(tǒng)調(diào)用,exit是基于_exit的實現(xiàn)。
3.2:exit退出進(jìn)程會清理IO緩沖區(qū),_eixt不會。

三:進(jìn)程的替換(exec函數(shù)族)
1.環(huán)境變量
或者稱為全局變量,存在與所有的shell 中,在你登陸系統(tǒng)的時候就已經(jīng)有了相應(yīng)的系統(tǒng)定義的環(huán)境變量了。Linux 的環(huán)境變量具有繼承性,即子shell 會繼承父shell 的環(huán)境變量。
查看當(dāng)前系統(tǒng)的全部環(huán)境變量信息:env命令
修改當(dāng)前系統(tǒng)環(huán)境變量信息:直接修改對應(yīng)的環(huán)境變量的值(臨時的,只再當(dāng)前shell生效)
? ? ? ? ? ? ? ? ? ? ? ? ? 永久修改:修改配置文件(按照層級)? ?
/etc/profile.d->?/etc/bashrc?->~/.bash_profile?->?~/.bashrc?
~/.bash_profile? 用戶登錄時被讀取,其中包含的命令被執(zhí)行。
? ? ~/.bashrc? 啟動新的shell時被讀取,并執(zhí)行。
需要重點關(guān)注的:PATH:決定了shell將到哪些目錄中尋找命令或程序
舉例:a.out的執(zhí)行,不帶./看是否可以?
? ? ? 修改PATH環(huán)境變量,把a(bǔ).out的文件所在路徑加進(jìn)去,然后再次執(zhí)行。
2. exec函數(shù)族
?<1>頭文件
#include <unistd.h>
<2>函數(shù)原型
extern char **environ;
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[]);
返回值:成功返回0,失敗返回-1。
參數(shù):@param path: 可執(zhí)行文件的路徑名
? @param file: 可執(zhí)行文件名,只能搜索環(huán)境變量 PATH 指定的路徑
? @parma arg: 可執(zhí)行文件名以及參數(shù),參數(shù)列表需要以 NULL 結(jié)尾
? @param argv[]: 參數(shù)數(shù)組,可以取代參數(shù)列表
? @param evnp[]: 環(huán)境變量數(shù)組
?應(yīng)用舉例:? ? ?
execl("/bin/ls","ls","-l",NULL);? ./a.out? argv[0]:./a.out? 1 + 2? argv[1]
execlp("ls","ls","-l",NULL);
char * const envp[] = {"USER=root","PATH=/bin",NULL};
execle("./app","./app","1",NULL,envp) ;
char * const argv[] = {"ls","-l",NULL};
execv("/bin/ls",argv);
char * const argv[] = {"ls","-l",NULL};
execvp("ls",argv) ;
char * const argv[] = {"ls","-l",NULL};
char * const envp[] = {"USER=root","PATH=/bin",NULL};
execvpe("ls",argv,envp);
.* 練習(xí)
1.自己寫一個calc.c,要求實現(xiàn)加減乘除功能。gcc calc.c -o calc
./calc 12 + 20
2.自己寫一個execl_home.c,要求使用execl調(diào)用calc打印相應(yīng)的內(nèi)容
參考代碼:calc.c,main.c?
calc.c:./calc 10 - 20? ? 10
execl_home.c:./execl 10 + 20
.*練習(xí)
實現(xiàn)一個簡單的shell。
問題描述參考《myshell的實現(xiàn)》? ?
strtok函數(shù)的使用。
四:進(jìn)程的等待
僵尸進(jìn)程:子進(jìn)程結(jié)束的時候,父進(jìn)程沒有進(jìn)行收尸操作(父進(jìn)程還存在),此時子進(jìn)程還占用資源。這時候的子進(jìn)程就是僵尸進(jìn)程。
父進(jìn)程回收子進(jìn)程資源的時機(jī):(1)父進(jìn)程結(jié)束? (2)處理子進(jìn)程結(jié)束時候發(fā)送的SIGCHILD信號來回收資源。
僵尸進(jìn)程的危害:僵尸態(tài)子進(jìn)程已經(jīng)結(jié)束,它占用大部分資源已經(jīng)釋放,但是仍然保留PID資源。 如果父進(jìn)程一直不退出,就一直不會回收子進(jìn)程的僵尸資源,這樣產(chǎn)生僵尸態(tài)子進(jìn)程過多,會導(dǎo)致PID資源耗盡,創(chuàng)建子進(jìn)程失敗。
那為什么還要設(shè)計僵尸進(jìn)程呢?給進(jìn)程設(shè)置僵尸狀態(tài)的目的是維護(hù)子進(jìn)程的信息,以便父進(jìn)程在以后某個時間獲取。這些信息包括子進(jìn)程的進(jìn)程ID、終止?fàn)顟B(tài)以及資源利用信息(CPU時間,內(nèi)存使用量等等)。
解決辦法:為了防止產(chǎn)生僵尸進(jìn)程,在fork子進(jìn)程之后我們都要wait它們;同時,當(dāng)子進(jìn)程退出的時候,內(nèi)核都會給父進(jìn)程一個SIGCHLD信號,所以我們可以建立一個捕獲SIGCHLD信號的信號處理函數(shù),在函數(shù)體中調(diào)用wait(或waitpid),就可以清理退出的子進(jìn)程以達(dá)到防止僵尸進(jìn)程的目的。
<1>wait的用法
函數(shù)原型:pid_t wait(int? *status)
函數(shù)功能:回收僵尸態(tài)子進(jìn)程,如果沒有僵尸態(tài)的子進(jìn)程則阻塞,如果沒有子進(jìn)程會立即返回。
函數(shù)參數(shù):@status? 是一個整型指針,指向的對象用來保存子進(jìn)程退出時的狀態(tài)
? a. status若是為NULL ,表示忽略子進(jìn)程退出時的狀態(tài)。
? b. status若是不為NULL ,表示保存子進(jìn)程退出時的狀態(tài)。
返回值:成功返回僵尸態(tài)子進(jìn)程的PID,失敗返回-1(沒有子進(jìn)程)。
大部分的時候,我們不需要關(guān)注子進(jìn)程退出時候的狀態(tài),只是想把這個僵尸子進(jìn)程干掉,這個時候,我們就不需要傳遞實際的status獲取狀態(tài),直接傳NULL進(jìn)去就可以了,但是,也有的時候,我們是需要關(guān)注這個狀態(tài)的,比如我確實需要知道子進(jìn)程是不是正常退出的,還是異常退出的。那我們呢就需要知道傳出來的status不同的值代表什么意思。
WIFEXITED(status)? ? 宏返回真表示進(jìn)程正常退出
WEXITSTATUS(status)? 取得子進(jìn)程exit()返回的結(jié)束代碼,一般會先用WIFEXITED來判斷是否正常結(jié)束,然后才使用此宏。
===============================================
WIFSIGNALED(status)? 如果子進(jìn)程是因為信號而結(jié)束則,返回值為非0 。
? ? ? ? ? ? ? ? ? ? 否則,返回值為0。
WTERMSIG(status)? ? 取得子進(jìn)程因信號而中止的信號代碼,一般會先用 WIFSIGNALED來判斷后,然后才使用此宏。
注意:wait函數(shù)是以阻塞(暫停)方式等待子進(jìn)程結(jié)束,等待當(dāng)前父進(jìn)程的任一子進(jìn)程的退出!
? ? ? 如果是多個子進(jìn)程,要實現(xiàn)對所有子進(jìn)程的收尸操作,需要循環(huán)調(diào)用wait來實現(xiàn)!
思考:什么是阻塞,什么是非阻塞呢?
阻塞: 得到調(diào)用的結(jié)果之前。一直等待。直到獲得了結(jié)果再去做其他的事情。
非阻塞:得到調(diào)用的結(jié)果之前。你可以做其它的事情。當(dāng)獲得了結(jié)果告訴我一聲就可以了。
例如:exit(5) 結(jié)束子進(jìn)程。則我們調(diào)用WEXITSTATUS(status)就返回5.
練習(xí):
fork一個子進(jìn)程,子進(jìn)程打印自己的pid,然后死循環(huán),(用信號終止子進(jìn)程)
父進(jìn)程wait子進(jìn)程結(jié)束,要獲得子進(jìn)程終止的信號編號。
<2>waitpid的用法
waitpid 函數(shù)常見用法如下:
1. 使用非阻塞的方式等待特定子進(jìn)程退出
while(waitpid(pid,&status,WNOHANG) == 0)
usleep(50000);
2. 阻塞等待任意子進(jìn)程退出
waitpid(-1,&status,0);====wait(&status)
waitpid(-1,NULL,0);=====wait(NULL);
3. 非阻塞等待任意子進(jìn)程退出
waitpid(-1,&status,WNOHANG);
4.阻塞等待特定子進(jìn)程的退出
waitpid(pid,&status, 0);
.*練習(xí)
父? 子
使用waitpid(pid, NULL,0)指定這個子進(jìn)程,阻塞式的等待它退出
waitpid(-1, &sta, WNOHANG)非阻塞等待任意子進(jìn)程退出,獲取它的退出狀態(tài),正常退出打印退出碼,因為信號退出,打印對應(yīng)的信號值