• 正文
    • 一、進程創(chuàng)建
    • 二、進程終止
    • 三、進程等待
    • 四、進程程序替換
  • 相關(guān)推薦
申請入駐 產(chǎn)業(yè)圖譜

深入理解Linux中進程控制(精講)

2023/02/08
686
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

一、進程創(chuàng)建

fork函數(shù)初識

Linux中,fork函數(shù)是非常重要的函數(shù),它從已存在進程中創(chuàng)建一個新進程。新進程為子進程,而原進程為父進程。

返回值:

在子進程中返回0,父進程中返回子進程的PID,子進程創(chuàng)建失敗返回-1。

進程調(diào)用fork,當控制轉(zhuǎn)移到內(nèi)核中的fork代碼后,內(nèi)核做:

分配新的內(nèi)存塊和內(nèi)核數(shù)據(jù)結(jié)構(gòu)給子進程。

將父進程部分數(shù)據(jù)結(jié)構(gòu)內(nèi)容拷貝至子進程。

添加子進程到系統(tǒng)進程列表當中。

fork返回,開始調(diào)度器調(diào)度。

fork之后,父子進程代碼共享。例如:

運行結(jié)果如下:

這里可以看到,Before只輸出了一次,而After輸出了兩次。其中,Before是由父進程打印的,而調(diào)用fork函數(shù)之后打印的兩個After,則分別由父進程和子進程兩個進程執(zhí)行。也就是說,fork之前父進程獨立執(zhí)行,而fork之后父子兩個執(zhí)行流分別執(zhí)行。

注意: fork之后,父進程和子進程誰先執(zhí)行完全由調(diào)度器決定。

fork函數(shù)返回值

fork函數(shù)為什么要給子進程返回0,給父進程返回子進程的PID?

一個父進程可以創(chuàng)建多個子進程,而一個子進程只能有一個父進程。因此,對于子進程來說,父進程是不需要被標識的;而對于父進程來說,子進程是需要被標識的,因為父進程創(chuàng)建子進程的目的是讓其執(zhí)行任務的,父進程只有知道了子進程的PID才能很好的對該子進程指派任務。

為什么fork函數(shù)有兩個返回值?

父進程調(diào)用fork函數(shù)后,為了創(chuàng)建子進程,fork函數(shù)內(nèi)部將會進行一系列操作,包括創(chuàng)建子進程的進程控制塊、創(chuàng)建子進程的進程地址空間、創(chuàng)建子進程對應的頁表等等。子進程創(chuàng)建完畢后,操作系統(tǒng)還需要將子進程的進程控制塊添加到系統(tǒng)進程列表當中,此時子進程便創(chuàng)建完畢了。

也就是說,在fork函數(shù)內(nèi)部執(zhí)行return語句之前,子進程就已經(jīng)創(chuàng)建完畢了,那么之后的return語句不僅父進程需要執(zhí)行,子進程也同樣需要執(zhí)行,這就是fork函數(shù)有兩個返回值的原因。

寫時拷貝

當子進程剛剛被創(chuàng)建時,子進程和父進程的數(shù)據(jù)和代碼是共享的,即父子進程的代碼和數(shù)據(jù)通過頁表映射到物理內(nèi)存的同一塊空間。只有當父進程或子進程需要修改數(shù)據(jù)時,才將父進程的數(shù)據(jù)在內(nèi)存當中拷貝一份,然后再進行修改。

這種在需要進行數(shù)據(jù)修改時再進行拷貝的技術(shù),稱為寫時拷貝技術(shù)。

1、為什么數(shù)據(jù)要進行寫時拷貝?

進程具有獨立性。多進程運行,需要獨享各種資源,多進程運行期間互不干擾,不能讓子進程的修改影響到父進程。

2、為什么不在創(chuàng)建子進程的時候就進行數(shù)據(jù)的拷貝?

子進程不一定會使用父進程的所有數(shù)據(jù),并且在子進程不對數(shù)據(jù)進行寫入的情況下,沒有必要對數(shù)據(jù)進行拷貝,我們應該按需分配,在需要修改數(shù)據(jù)的時候再分配(延時分配),這樣可以高效的使用內(nèi)存空間。

3、代碼會不會進行寫時拷貝?

90%的情況下是不會的,但這并不代表代碼不能進行寫時拷貝,例如在進行進程替換的時候,則需要進行代碼的寫時拷貝。

fork常規(guī)用法

一個進程希望復制自己,使子進程同時執(zhí)行不同的代碼段。例如父進程等待客戶端請求,生成子進程來處理請求。

一個進程要執(zhí)行一個不同的程序。例如子進程從fork返回后,調(diào)用exec函數(shù)。

fork調(diào)用失敗的原因

fork函數(shù)創(chuàng)建子進程也可能會失敗,有以下兩種情況:

系統(tǒng)中有太多的進程,內(nèi)存空間不足,子進程創(chuàng)建失敗。

實際用戶的進程數(shù)超過了限制,子進程創(chuàng)建失敗。

二、進程終止

進程退出場景

進程退出只有三種情況:

代碼運行完畢,結(jié)果正確。

代碼運行完畢,結(jié)果不正確。

代碼異常終止(進程崩潰)。

進程退出碼

我們都知道m(xù)ain函數(shù)是代碼的入口,但實際上main函數(shù)只是用戶級別代碼的入口,main函數(shù)也是被其他函數(shù)調(diào)用的,例如在VS2013當中main函數(shù)就是被一個名為__tmainCRTStartup的函數(shù)所調(diào)用,而__tmainCRTStartup函數(shù)又是通過加載器被操作系統(tǒng)所調(diào)用的,也就是說main函數(shù)是間接性被操作系統(tǒng)所調(diào)用的。

既然main函數(shù)是間接性被操作系統(tǒng)所調(diào)用的,那么當main函數(shù)調(diào)用結(jié)束后就應該給操作系統(tǒng)返回相應的退出信息,而這個所謂的退出信息就是以退出碼的形式作為main函數(shù)的返回值返回,我們一般以0表示代碼成功執(zhí)行完畢,以非0表示代碼執(zhí)行過程中出現(xiàn)錯誤,這就是為什么我們都在main函數(shù)的最后返回0的原因。

當我們的代碼運行起來就變成了進程,當進程結(jié)束后main函數(shù)的返回值實際上就是該進程的進程退出碼,我們可以使用echo $?命令查看最近一次進程退出的退出碼信息。

例如,對于下面這個簡單的代碼:

代碼運行結(jié)束后,我們可以查看該進程的進程退出碼。

[cl@VM-0-15-centos procTermination]$ echo $?

這時便可以確定main函數(shù)是順利執(zhí)行完畢了。

為什么以0表示代碼執(zhí)行成功,以非0表示代碼執(zhí)行錯誤?

因為代碼執(zhí)行成功只有一種情況,成功了就是成功了,而代碼執(zhí)行錯誤卻有多種原因,例如內(nèi)存空間不足、非法訪問以及棧溢出等等,我們就可以用這些非0的數(shù)字分別表示代碼執(zhí)行錯誤的原因。

C語言當中的strerror函數(shù)可以通過錯誤碼,獲取該錯誤碼在C語言當中對應的錯誤信息:

運行代碼后我們就可以看到各個錯誤碼所對應的錯誤信息:

實際上Linux中的ls、pwd等命令都是可執(zhí)行程序,使用這些命令后我們也可以查看其對應的退出碼。

可以看到,這些命令成功執(zhí)行后,其退出碼也是0。

但是命令執(zhí)行錯誤后,其退出碼就是非0的數(shù)字,該數(shù)字具體代表某一錯誤信息。

注意: 退出碼都有對應的字符串含義,幫助用戶確認執(zhí)行失敗的原因,而這些退出碼具體代表什么含義是人為規(guī)定的,不同環(huán)境下相同的退出碼的字符串含義可能不同。

進程正常退出

return退出

在main函數(shù)中使用return退出進程是我們常用的方法。

例如,在main函數(shù)最后使用return退出進程。

運行結(jié)果:

exit函數(shù)

使用exit函數(shù)退出進程也是我們常用的方法,exit函數(shù)可以在代碼中的任何地方退出進程,并且exit函數(shù)在退出進程前會做一系列工作:

執(zhí)行用戶通過atexit或on_exit定義的清理函數(shù)。

關(guān)閉所有打開的流,所有的緩存數(shù)據(jù)均被寫入。

調(diào)用_exit函數(shù)終止進程。

例如,以下代碼中exit終止進程前會將緩沖區(qū)當中的數(shù)據(jù)輸出。

運行結(jié)果:

_exit函數(shù)

使用_exit函數(shù)退出進程的方法我們并不經(jīng)常使用,_exit函數(shù)也可以在代碼中的任何地方退出進程,但是_exit函數(shù)會直接終止進程,并不會在退出進程前會做任何收尾工作。

例如,以下代碼中使用_exit終止進程,則緩沖區(qū)當中的數(shù)據(jù)將不會被輸出。

運行結(jié)果:

return、exit和_exit之間的區(qū)別與聯(lián)系

return、exit和_exit之間的區(qū)別

只有在main函數(shù)當中的return才能起到退出進程的作用,子函數(shù)當中return不能退出進程,而exit函數(shù)和_exit函數(shù)在代碼中的任何地方使用都可以起到退出進程的作用。

使用exit函數(shù)退出進程前,exit函數(shù)會執(zhí)行用戶定義的清理函數(shù)、沖刷緩沖,關(guān)閉流等操作,然后再終止進程,而_exit函數(shù)會直接終止進程,不會做任何收尾工作。

return、exit和_exit之間的聯(lián)系

執(zhí)行return num等同于執(zhí)行exit(num),因為調(diào)用main函數(shù)運行結(jié)束后,會將main函數(shù)的返回值當做exit的參數(shù)來調(diào)用exit函數(shù)。

使用exit函數(shù)退出進程前,exit函數(shù)會先執(zhí)行用戶定義的清理函數(shù)、沖刷緩沖,關(guān)閉流等操作,然后再調(diào)用_exit函數(shù)終止進程。

進程異常退出

情況一:向進程發(fā)生信號導致進程異常退出。

例如,在進程運行過程中向進程發(fā)生kill -9信號使得進程異常退出,或是使用Ctrl+C使得進程異常退出等。

情況二:代碼錯誤導致進程運行時異常退出。

例如,代碼當中存在野指針問題使得進程運行時異常退出,或是出現(xiàn)除0的情況使得進程運行時異常退出等。

三、進程等待

進程等待的必要性

子進程退出,父進程如果不讀取子進程的退出信息,子進程就會變成僵尸進程,進而造成內(nèi)存泄漏。

進程一旦變成僵尸進程,那么就算是kill -9命令也無法將其殺死,因為誰也無法殺死一個已經(jīng)死去的進程。

對于一個進程來說,最關(guān)心自己的就是其父進程,因為父進程需要知道自己派給子進程的任務完成的如何。

父進程需要通過進程等待的方式,回收子進程資源,獲取子進程的退出信息。

獲取子進程status

下面進程等待所使用的兩個函數(shù)wait和waitpid,都有一個status參數(shù),該參數(shù)是一個輸出型參數(shù),由操作系統(tǒng)進行填充。

如果對status參數(shù)傳入NULL,表示不關(guān)心子進程的退出狀態(tài)信息。否則,操作系統(tǒng)會通過該參數(shù),將子進程的退出信息反饋給父進程。

status是一個整型變量,但status不能簡單的當作整型來看待,status的不同比特位所代表的信息不同,具體細節(jié)如下(只研究status低16比特位):

在status的低16比特位當中,高8位表示進程的退出狀態(tài),即退出碼。進程若是被信號所殺,則低7位表示終止信號,而第8位比特位是core dump標志。

我們通過一系列位操作,就可以根據(jù)status得到進程的退出碼和退出信號。

exitCode = (status >> 8) & 0xFF; //退出碼
exitSignal = status & 0x7F; //退出信號

對于此,系統(tǒng)當中提供了兩個宏來獲取退出碼和退出信號。

WIFEXITED(status):用于查看進程是否是正常退出,本質(zhì)是檢查是否收到信號。

WEXITSTATUS(status):用于獲取進程的退出碼。

exitNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status); //獲取退出碼

需要注意的是,當一個進程非正常退出時,說明該進程是被信號所殺,那么該進程的退出碼也就沒有意義了。

進程等待的方法

wait方法

函數(shù)原型:pid_t wait(int* status);

作用:等待任意子進程。

返回值:等待成功返回被等待進程的pid,等待失敗返回-1。

參數(shù):輸出型參數(shù),獲取子進程的退出狀態(tài),不關(guān)心可設(shè)置為NULL。

例如,創(chuàng)建子進程后,父進程可使用wait函數(shù)一直等待子進程,直到子進程退出后讀取子進程的退出信息。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0){
//child
int count = 10;
while(count--){
printf("I am child...PID:%d, PPID:%dn", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = wait(&status);
if(ret > 0){
//wait success
printf("wait child success...n");
if(WIFEXITED(status)){
//exit normal
printf("exit code:%dn", WEXITSTATUS(status));
}
}
sleep(3);
return 0;
}

我們可以使用以下監(jiān)控腳本對進程進行實時監(jiān)控:

[cl@VM-0-15-centos procWait]$ while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done

這時我們可以看到,當子進程退出后,父進程讀取了子進程的退出信息,子進程也就不會變成僵尸進程了。

waitpid方法

函數(shù)原型:pid_t waitpid(pid_t pid, int* status, int options);

作用:等待指定子進程或任意子進程。

返回值:

1、等待成功返回被等待進程的pid。

2、如果設(shè)置了選項WNOHANG,而調(diào)用中waitpid發(fā)現(xiàn)沒有已退出的子進程可收集,則返回0。

3、如果調(diào)用中出錯,則返回-1,這時errno會被設(shè)置成相應的值以指示錯誤所在。

參數(shù):

1、pid:待等待子進程的pid,若設(shè)置為-1,則等待任意子進程。

2、status:輸出型參數(shù),獲取子進程的退出狀態(tài),不關(guān)心可設(shè)置為NULL。

3、options:當設(shè)置為WNOHANG時,若等待的子進程沒有結(jié)束,則waitpid函數(shù)直接返回0,不予以等待。若正常結(jié)束,則返回該子進程的pid。

例如,創(chuàng)建子進程后,父進程可使用waitpid函數(shù)一直等待子進程(此時將waitpid的第三個參數(shù)設(shè)置為0),直到子進程退出后讀取子進程的退出信息。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 10;
while (count--){
printf("I am child...PID:%d, PPID:%dn", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret >= 0){
//wait success
printf("wait child success...n");
if (WIFEXITED(status)){
//exit normal
printf("exit code:%dn", WEXITSTATUS(status));
}
else{
//signal killed
printf("killed by siganl %dn", status & 0x7F);
}
}
sleep(3);
return 0;
}

在父進程運行過程中,我們可以嘗試使用kill -9命令將子進程殺死,這時父進程也能等待子進程成功。

注意: 被信號殺死而退出的進程,其退出碼將沒有意義。

多進程創(chuàng)建以及等待的代碼模型

上面演示的都是父進程創(chuàng)建以及等待一個子進程的例子,實際上我們還可以同時創(chuàng)建多個子進程,然后讓父進程依次等待子進程退出,這叫做多進程創(chuàng)建以及等待的代碼模型。

例如,以下代碼中同時創(chuàng)建了10個子進程,同時將子進程的pid放入到ids數(shù)組當中,并將這10個子進程退出時的退出碼設(shè)置為該子進程pid在數(shù)組ids中的下標,之后父進程再使用waitpid函數(shù)指定等待這10個子進程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t ids[10];
for (int i = 0; i < 10; i++){
pid_t id = fork();
if (id == 0){
//child
printf("child process created successfully...PID:%dn", getpid());
sleep(3);
exit(i); //將子進程的退出碼設(shè)置為該子進程PID在數(shù)組ids中的下標
}
//father
ids[i] = id;
}
for (int i = 0; i < 10; i++){
int status = 0;
pid_t ret = waitpid(ids[i], &status, 0);
if (ret >= 0){
//wait child success
printf("wiat child success..PID:%dn", ids[i]);
if (WIFEXITED(status)){
//exit normal
printf("exit code:%dn", WEXITSTATUS(status));
}
else{
//signal killed
printf("killed by signal %dn", status & 0x7F);
}
}
}
return 0;
}

運行代碼,這時我們便可以看到父進程同時創(chuàng)建多個子進程,當子進程退出后,父進程再依次讀取這些子進程的退出信息。

基于非阻塞接口的輪詢檢測方案

上述所給例子中,當子進程未退出時,父進程都在一直等待子進程退出,在等待期間,父進程不能做任何事情,這種等待叫做阻塞等待。

實際上我們可以讓父進程不要一直等待子進程退出,而是當子進程未退出時父進程可以做一些自己的事情,當子進程退出時再讀取子進程的退出信息,即非阻塞等待。

做法很簡單,向waitpid函數(shù)的第三個參數(shù)potions傳入WNOHANG,這樣一來,等待的子進程若是沒有結(jié)束,那么waitpid函數(shù)將直接返回0,不予以等待。而等待的子進程若是正常結(jié)束,則返回該子進程的pid。

例如,父進程可以隔一段時間調(diào)用一次waitpid函數(shù),若是等待的子進程尚未退出,則父進程可以先去做一些其他事,過一段時間再調(diào)用waitpid函數(shù)讀取子進程的退出信息。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 3;
while (count--){
printf("child do something...PID:%d, PPID:%dn", getpid(), getppid());
sleep(3);
}
exit(0);
}
//father
while (1){
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret > 0){
printf("wait child success...n");
printf("exit code:%dn", WEXITSTATUS(status));
break;
}
else if (ret == 0){
printf("father do other things...n");
sleep(1);
}
else{
printf("waitpid error...n");
break;
}
}
return 0;
}

運行結(jié)果就是,父進程每隔一段時間就去查看子進程是否退出,若未退出,則父進程先去忙自己的事情,過一段時間再來查看,直到子進程退出后讀取子進程的退出信息。

四、進程程序替換

替換原理

用fork創(chuàng)建子進程后,子進程執(zhí)行的是和父進程相同的程序(但有可能執(zhí)行不同的代碼分支),若想讓子進程執(zhí)行另一個程序,往往需要調(diào)用一種exec函數(shù)。

當進程調(diào)用一種exec函數(shù)時,該進程的用戶空間代碼和數(shù)據(jù)完全被新程序替換,并從新程序的啟動例程開始執(zhí)行。

當進行進程程序替換時,有沒有創(chuàng)建新的進程?

進程程序替換之后,該進程對應的PCB、進程地址空間以及頁表等數(shù)據(jù)結(jié)構(gòu)都沒有發(fā)生改變,只是進程在物理內(nèi)存當中的數(shù)據(jù)和代碼發(fā)生了改變,所以并沒有創(chuàng)建新的進程,而且進程程序替換前后該進程的pid并沒有改變。

子進程進行進程程序替換后,會影響父進程的代碼和數(shù)據(jù)嗎?

子進程剛被創(chuàng)建時,與父進程共享代碼和數(shù)據(jù),但當子進程需要進行進程程序替換時,也就意味著子進程需要對其數(shù)據(jù)和代碼進行寫入操作,這時便需要將父子進程共享的代碼和數(shù)據(jù)進行寫時拷貝,此后父子進程的代碼和數(shù)據(jù)也就分離了,因此子進程進行程序替換后不會影響父進程的代碼和數(shù)據(jù)。

替換函數(shù)

替換函數(shù)有六種以exec開頭的函數(shù),它們統(tǒng)稱為exec函數(shù):

一、int execl(const char *path, const char *arg, ...);

第一個參數(shù)是要執(zhí)行程序的路徑,第二個參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個程序,并以NULL結(jié)尾。

例如,要執(zhí)行的是ls程序。

execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);

二、int execlp(const char *file, const char *arg, ...);

第一個參數(shù)是要執(zhí)行程序的名字,第二個參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個程序,并以NULL結(jié)尾。

例如,要執(zhí)行的是ls程序。

execlp("ls", "ls", "-a", "-i", "-l", NULL);

三、int execle(const char *path, const char *arg, ..., char *const envp[]);

第一個參數(shù)是要執(zhí)行程序的路徑,第二個參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個程序,并以NULL結(jié)尾,第三個參數(shù)是你自己設(shè)置的環(huán)境變量。

例如,你設(shè)置了MYVAL環(huán)境變量,在mycmd程序內(nèi)部就可以使用該環(huán)境變量。

char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);

四、int execv(const char *path, char *const argv[]);

第一個參數(shù)是要執(zhí)行程序的路徑,第二個參數(shù)是一個指針數(shù)組,數(shù)組當中的內(nèi)容表示你要如何執(zhí)行這個程序,數(shù)組以NULL結(jié)尾。

例如,要執(zhí)行的是ls程序。

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);

五、int execvp(const char *file, char *const argv[]);

第一個參數(shù)是要執(zhí)行程序的名字,第二個參數(shù)是一個指針數(shù)組,數(shù)組當中的內(nèi)容表示你要如何執(zhí)行這個程序,數(shù)組以NULL結(jié)尾。

例如,要執(zhí)行的是ls程序。

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);

六、int execve(const char *path, char *const argv[], char *const envp[]);

第一個參數(shù)是要執(zhí)行程序的路徑,第二個參數(shù)是一個指針數(shù)組,數(shù)組當中的內(nèi)容表示你要如何執(zhí)行這個程序,數(shù)組以NULL結(jié)尾,第三個參數(shù)是你自己設(shè)置的環(huán)境變量。

例如,你設(shè)置了MYVAL環(huán)境變量,在mycmd程序內(nèi)部就可以使用該環(huán)境變量。

char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2021", NULL };
execve("./mycmd", myargv, myenvp);

函數(shù)解釋

這些函數(shù)如果調(diào)用成功,則加載指定的程序并從啟動代碼開始執(zhí)行,不再返回。

如果調(diào)用出錯,則返回-1。

也就是說,exec系列函數(shù)只要返回了,就意味著調(diào)用失敗。

命名理解

這六個exec系列函數(shù)的函數(shù)名都以exec開頭,其后綴的含義如下:

l(list):表示參數(shù)采用列表的形式,一一列出。

v(vector):表示參數(shù)采用數(shù)組的形式。

p(path):表示能自動搜索環(huán)境變量PATH,進行程序查找。

e(env):表示可以傳入自己設(shè)置的環(huán)境變量。

事實上,只有execve才是真正的系統(tǒng)調(diào)用,其它五個函數(shù)最終都是調(diào)用的execve,所以execve在man手冊的第2節(jié),而其它五個函數(shù)在man手冊的第3節(jié),也就是說其他五個函數(shù)實際上是對系統(tǒng)調(diào)用execve進行了封裝,以滿足不同用戶的不同調(diào)用場景的。

下圖為exec系列函數(shù)族之間的關(guān)系:

做一個簡易的shell

shell也就是命令行解釋器,其運行原理就是:當有命令需要執(zhí)行時,shell創(chuàng)建子進程,讓子進程執(zhí)行命令,而shell只需等待子進程退出即可。

其實shell需要執(zhí)行的邏輯非常簡單,其只需循環(huán)執(zhí)行以下步驟:

獲取命令行。

解析命令行。

創(chuàng)建子進程。

替換子進程。

等待子進程退出。

其中,創(chuàng)建子進程使用fork函數(shù),替換子進程使用exec系列函數(shù),等待子進程使用wait或者waitpid函數(shù)。

于是我們可以很容易實現(xiàn)一個簡易的shell,代碼如下:

#include <stdio.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEN 1024 //命令最大長度
#define NUM 32 //命令拆分后的最大個數(shù)
int main()
{
char cmd[LEN]; //存儲命令
char* myargv[NUM]; //存儲命令拆分后的結(jié)果
char hostname[32]; //主機名
char pwd[128]; //當前目錄
while (1){
//獲取命令提示信息
struct passwd* pass = getpwuid(getuid());
gethostname(hostname, sizeof(hostname)-1);
getcwd(pwd, sizeof(pwd)-1);
int len = strlen(pwd);
char* p = pwd + len - 1;
while (*p != '/'){
p--;
}
p++;
//打印命令提示信息
printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
//讀取命令
fgets(cmd, LEN, stdin);
cmd[strlen(cmd) - 1] = '