Linux 進程的創建、終止、等待與程序替換函數 保姆級講解

目錄

一、?進程創建

fork函數

二、進程的終止:

1. 想明白:終止是在做什么?

?2.進程終止的3種情況?

a.退出碼是什么?存在原因?為什么int main()return 0?

b.第三種進程終止的情況(異常))-- 代碼執行時,出現了異常,提前退出

? ? ? ? ? ? ? 一個進程退出原因,只需要知道退出碼、退出信號:

三、如何終止?

a. main 函數return ,表示進程終止。

(非main函數,return表示什么?叫做函數結束,不一定代表進程結束)

b. exit函數:引起一個正常的進程終止。

任意位置調用 exit() 都表示進程終止

c. _exit()? ?---> system call(系統調用)

exit() (c庫函數)vs _exit()(系統調用):

進程退出時,內核會執行以下操作:

???釋放流程總結(詳細版)

四.怎么避免成為僵尸進程而導致內存泄漏?

進程等待必要性

wait()

等待的時候解決子進程的僵尸狀態 :

如何理解阻塞等待子進程?

waitpid()

獲取子進程status

可以不使用位操作來提退出信息。

非阻塞等待

五、進程程序替換

替換原理

1. 通過代碼看現象

2. 解釋原理

3. 將代碼改成多進程版

4. 使用所有的替換方法,并且認識函數參數的含義

1.exec ----?l -- list 代表的是命令列表

2.execv -- v ---> vector,類似于c++中所學到的動態數組 vector, 使用的時候就是現將要執行的選項放進數組里,然后再進行傳參:

3.execvp -- p ----> 用戶可以不穿要執行文件的路徑(要傳文件名),直接告訴exec*, 我要執行誰就行,p: 查找這個程序,系統會自動在環境變量PATH中進行查找。

4.execlp ->

程序替換 --- 替換自己寫的程序

調用其他語言的程序

5.execvpe? ?---- e? ----> evironment ---> 環境變量,允許我們自定義

6.execve ---真正的系統調用


一、?進程創建

fork函數

在linux中fork函數時非常重要的函數,它從已存在進程中創建一個新進程。新進程為子進程,而原進程為父進程。

#include <unistd.h>
pid_t fork(void);

進程:內核的相關管理數據結構(tast_struct + mm_struct + 頁表)? + 代碼和數據

進程調用fork,當控制轉移到內核中的fork代碼后,內核做:

1.分配新的內存塊和內核數據結構給子進程
2.將父進程部分數據結構內容拷貝至子進程
3.添加子進程到系統進程列表當中

4.fork返回,開始調度器調度

?寫時拷貝

fork函數具體使用和寫時拷貝,可以查看我的上一篇博客:

linux操作系統內存管理核心解析:地址空間、頁表與進程調度原理-CSDN博客

返回值:子進程中返回0,父進程返回子進程id,出錯返回-1

因為return的本質就是向父進程進行寫入,寫入發生寫時拷貝。

為什么給父進程返回的是子進程的PID,子進程返回的是0?

方便父進程對子進程進行標識,從而對子進程進行管理,通過0值知道子進程創建成功

fork常規用法

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

2.一個進程要執行一個不同的程序。例如子進程從fork返回后,調用exec函數。
fork調用失敗的原因

1.系統中有太多的進程

2.實際用戶的進程數超過了限制

二、進程的終止:

1. 想明白:終止是在做什么?

? ? ? ? 創建進程是先創建好內核的相關管理數據結構還是先創建好代碼和數據??

????????先?創建好內核的相關管理數據結構? 再 創建好代 碼和數據

終止是在釋放曾經的代碼和數據所占據的空間。再釋放內核數據結構


?2.進程終止的3種情況?

a.退出碼是什么?存在原因?為什么int main()return 0?

使用vim編譯器寫一段代碼:

#include<stdio.h>#include<unistd.h>int main()
{printf("i'm a process, pid: %d, ppid: %d\n",getpid(),getppid());sleep(1);return 0;
}

這是我兩次運行打印出的結果:

i'm a process, pid: 562487, ppid: 553748i'm a process, pid: 562539, ppid: 553748

現在將main函數的返回值改為100

再次運行程序的結果:

i'm a process, pid: 563668, ppid: 553748i'm a process, pid: 563689, ppid: 553748

此時在命令行輸入echo $?,輸出結果100

echo $?
100

變量'?'?

在shell中存在一個變量'?'? ? ---->使用‘$’ 查看

echo 是 內建命令,打印的都是bash內部的變量數據。

?? ?---->? 父進程bash獲取到的,最近一個子進程退出的退出碼,退出碼為0表示成功,!0表示失敗。

進程的退出碼是告訴關心方(父進程),任務完成的情況,失敗的原因是什么。

不是說 echo $? 是查看最近一次進程的退出碼嗎?

因為 echo $? 也是一個進程

不同的非0值,一方面表示失敗,一方面表示失敗原因,每一個數字都有對應的錯誤描述(字符串類型)。

運行以下代碼:

#include<stdio.h>#include<unistd.h>#include<string.h>int main()
{       for(int errcode = 0; errcode <= 255; errcode++){printf("errcode=%d, %s\n",errcode, strerror(errcode));}printf("i'm a process, pid: %d, ppid: %d\n",getpid(),getppid());sleep(1);return 0;
}

其中的strerror:把錯誤碼轉換成一段描述錯誤信息的字符串。

運行程序:一共有133條錯誤碼的提示。我們發現在進行指令操作或者運行程序失敗后,系統會直接告訴我們錯誤的原因。

因此,進程的退出碼是告訴關心方(父進程),任務完成的情況,失敗的原因是什么。bash是為用戶負責

運行以下代碼:

#include<stdio.h>#include<unistd.h>#include<string.h>int Div(int x, int y)
{if( 0 == y){return -1;}else{return x / y;}
}int main()
{       int result = 0;result = Div(10, 0);printf("result = %d\n", result);sleep(1);return 0;
}

運行結果:

result = -1

雖然我知道代碼是什么,但看到result = -1, 會認為是結果為-1,還是因為錯誤的 -1

再寫以下代碼:

#include<stdio.h>#include<unistd.h>#include<string.h>//自定義枚舉常量
enum
{Success = 0,DIV_ZERO,Mod_Zero,
};
int exit_code = Success;const char* CodeToErrString(int code)
{switch(code){case Success:return "Success!";case DIV_ZERO:return "div zero!";case Mod_Zero:return "mod zero!";default:return "unknown error!";}
}int Div(int x, int y)
{if( 0 == y){exit_code = DIV_ZERO;return -1;}else{return x / y;}
}int main()
{int result = 0;result = Div(10, 5);printf("result = %d [%s]\n", result, CodeToErrString(exit_code));result = Div(10, 0);printf("result = %d [%s]\n", result, CodeToErrString(exit_code));sleep(1);return 0;
}

輸出結果:

result = 2 [Success!]
result = -1 [div zero!]

此時的結果就是正確的

進程終止的前兩種情況:

1.代碼跑完,結果正確

2.代碼跑完,結果不正確

main函數的返回值是給父進程的,這叫做進程的退出碼,用來標明進程退出結果的正確與否

b.第三種進程終止的情況(異常))-- 代碼執行時,出現了異常,提前退出

直接除0并返回:

運行結果:

vs 編譯運行的時候,崩潰了,操作系統發現進程做了不該做的事情,因此殺掉了進程

異常之后,退出碼還是否有意義?

沒有意義。只關心異常的原因

進程出異常,是因為操作系統發現后給進程發送了信號。

當我們只運行以下代碼:

運行結果:正常情況下,一直運行

也可以直接用kill -9 pid 命令,殺掉進程

又運行代碼:

輸出結果:

segmentation fault? ---> 段錯誤,也就是野指針,os提前終止進程。這就是一種異常。

退出信號:

因為,進程出現異常后,退出碼失去了意義,但是還可以看退出信號,判斷進程異常的原因是什么。

? ? ? ? ? ? ? 一個進程退出原因,只需要知道退出碼、退出信號:

退出碼退出信號進程終止情況
00成功
!00沒異常,但結果不對
0!0進程出現異常
!0!0

這些信號告訴父進程(如bash), 一個進程退出后,會將進程的退出碼、退出信號寫入到進程的pcb當中,當一個進程成為了僵尸進程,已經刪釋放了代碼和數據,但是會維持一段時間pcb不釋放,編程z狀態,這就是因為要讓父進程知道進程退出的情況,退出信號、退出碼保存到了進程的pcb中 int sig_code、int exit_code 當中:

看下面幾張圖:

進程的task_struct
進程的task_struct
把所有的進程用一個雙鏈表鏈接在一起
?
tast_struct中的 exit_code、exit_signal?

三、如何終止?

a. main 函數return ,表示進程終止。

(非main函數,return表示什么?叫做函數結束,不一定代表進程結束)

b. exit函數:引起一個正常的進程終止。

頭文件:<stdlib.h>, 參數:status

在代碼中:

運行程序后,echo $?

執行代碼:

運行后結果,此時exit()是寫在非main函數中的,這是函數返回呢?還是進程終止?

任意位置調用 exit() 都表示進程終止

c. _exit()? ?---> system call(系統調用)

運行下面代碼:

運行結果:

?將_exit()寫入函數內部:

運行結果:

作用和exit()類似,區別是??

運行下面代碼:

結果:

去掉\n:

結果:運行程序時前3秒這個還沒有打印出來,運行結束時才打印出來(沒打印出來的時候存在緩沖區)

exit()作用是,在進程退出時,沖刷緩沖區

改成_exit()

運行:

運行了,但是沒打印,退出結果也是3

exit() (c庫函數)vs _exit()(系統調用):

exit()作用是,在進程退出時,幫我們沖刷緩沖區, _exit()不會,因為緩沖區(不是內核緩沖區)不在系統調用和操作系統內部,因此刷新不了。而實際上,exit()在底層上就調用的_exit()。殺掉進程讓進程退出。

進程退出時,內核會執行以下操作:

  1. 釋放用戶空間資源:

    • 代碼段、數據段、堆、棧等內存空間。

    • 用戶空間的緩沖區(由?exit()?處理,_exit()?不處理)。

  2. 釋放內核空間資源:

    • 關閉所有打開的文件描述符(由內核自動處理,無論是否調用?exit())。

    • 釋放進程的頁表、信號處理結構、定時器等內核數據結構。

    • PCB(進程控制塊):內核會保留 PCB 直到父進程通過?wait()?讀取子進程的退出狀態(ZOMBIE?狀態)。之后 PCB 才會被徹底釋放。

  3. 通知父進程:通過?SIGCHLD?信號和退出狀態碼。

  • task_struct(進程控制塊,PCB)作用:描述進程的所有信息,是內核管理進程的核心結構。

  • 包含內容

    • 進程狀態(運行、就緒、僵尸等)。

    • 進程標識符(PID、PPID)。

    • 調度信息(優先級、時間片、調度策略)。

    • 內存管理指針(指向?mm_struct(描述進程的虛擬內存空間(代碼段、堆、棧等))。

    • 打開的文件描述符表(指向?files_struct(管理進程打開的文件)。

    • 信號處理表、資源限制、父子進程關系等。

???釋放流程總結(詳細版)

  • ?用戶空間資源釋放

    • 代碼、數據、堆棧內存通過釋放?mm_struct?和?vm_area_struct?(描述進程的每一段虛擬內存(如代碼段、堆、棧、內存映射文件等)回收。

  • ?內核資源釋放

    • 關閉文件描述符(釋放?files_struct?和?file?對象)。

    • 取消信號處理和定時器。

  • ?保留 PCB(task_struct

    • 等待父進程調用?wait()?后釋放。

  • ?緩存和共享資源

    • dentryinode?等由內核緩存機制延遲釋放。

進程等待:

任何子進程,在退出的情況下,一般必須要由父進程等待,進程在退出時。父進程不管,就會變成Z(僵尸進程)一直占用系統內核內存 ---> 內存泄漏。

為什么進程等待?


四.怎么避免成為僵尸進程而導致內存泄漏?

進程等待必要性

之前講過,子進程退出,父進程如果不管不顧,就可能造成‘僵尸進程’的問題,進而造成內存泄漏。另外,進程一旦變成僵尸狀態,那就刀槍不入,“殺人不眨眼”的kill -9 也無能為力,因為誰也沒有辦法殺死一個已經死去的進程。

最后,父進程派給子進程的任務完成的如何,我們需要知道。如,子進程運行完成,結果對還是不對,或者是否正常退出。

父進程通過進程等待的方式:

1.回收子進程資源

2. 獲取子進程退出信息。

wait()

pid_t wait(int* status);

wait函數成功后,會返回進程的pid,等待父進程中,任意一個子進程退出。

如果子進程已經退出,調用wait/waitpid時,wait/waitpid會立即返回,并且釋放資源,獲得子進程退出信息。

如果在任意時刻調用wait/waitpid,子進程存在且正常運行,則進程可能阻塞。

如果不存在該子進程,則立即出錯返回。

等待的時候解決子進程的僵尸狀態 :

在vim編譯器中寫入以下代碼:
?????等子進程運行完畢并且進程退出,父進程休眠10s,再對子進程進行回收,讓我們能短暫的看到子進程退出之后,所形成的僵尸狀態,當父進程進行回收的時候,子進程的僵尸狀態消失,3秒之后父進程退出。

#include<stdio.h>#include<unistd.h>#include<string.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>void ChildRun
{int cnt = 5;while(cnt){printf("i'm child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid());sleep(2);cnt--;}
}int main()
{printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id == 0){//childChildRun();printf("child quit ...\n");exit(0);}//fathersleep(10);pid_t rit = wait(NULL);if(rid > 0){printf("wait Success, rit: %d\n", rit);}sleep(3);return 0;
}

每隔一秒:顯示進程列表的表頭(列名),查找名為?myprocess?的進程,并排除?grep?自身

while : ; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep; sleep 1; done

運行:

就算沒有sleep(10)(只是為了便于我們觀察), 如果子進程沒有退出,父進程其實一直在進行阻塞等待。在這個過程中子進程本身就是軟件,父進程本質是在等待某種軟件條件繼續。

如何理解阻塞等待子進程?

把父進程的狀態設置為非運行狀態,將父進程的pcb鏈入到子進程當中,當子進程執行完畢,再喚醒父進程。


waitpid()

此時這樣寫所表示的作用和wait(NULL)完全相同,所表示的是:等待任何一個子進程退出,那個子進程退出,就返回這個子進程所對應的rid返回

此時所表示的是等待的是指定id的子進程(這就是為什么需要給父進程返回子進程id的原因):

運行代碼

void ChildRun()
{int cnt = 5;while(cnt){printf("i'm child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);sleep(2);cnt--;}
}int main()
{printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id == 0){//childChildRun();printf("child quit ...\n");exit(0);}//fathersleep(15);//pid_t rid = wait(NULL);//pid_t waitpid(pid_t pid, int* wstatus, int options) ;   pid_t rid = waitpid(id, NULL, 0);if(rid > 0){printf("wait Success, rid: %d\n", rid);}sleep(3);printf("father  quit ...\n");return 0;
}

運行結果:

等待失敗的案例:寫一個不存在的子進程id

運行結果:


獲取子進程status

如何獲得子進程退出的信息? -- 參數status

status --> 輸出型參數? --> 表示的是子進程的退出信息?

修改代碼 :?

運行結果:status = 256?

子進程退出的信息需要什么?

進程退出碼 + 進程退出信號

只能使用waitpid()的方式來拿到子進程的退出信號:

難道不能使用全局變量,exit_code, exit_signal?? 不能

在子進程退出的時候將這兩個全局變量設置成退出碼和退出信號就能讓父進程拿到嗎?不可以?

設置全局變量,子進程退出的時候,值也設置了,但是父進程根本就看不到,因為父子進程具有獨立性,在子進程退出的時候臨時修改,os都會發生寫時拷貝。

想要看到退出碼和退出信號,需要使用正確的查看方式,status是有自己的特殊格式的:

status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16比特位)

????????????????次低八位:退出狀態? ? ? ? 低七位:退出時,如果發生異常所收到信號

打印出位圖:status按位與的時候不會改變其值:?

運行結果:子進程退出碼為1,退出信號為0,符合我們的預期。

再將子進程寫成死循環:

父進程只能一直阻塞等待:

使用? kill -9 殺掉子進程,得到退出碼 0,退出信號 9

當子進程有異常

運行:退出碼 :0 ,退出信號:11

(11:是因為野指針退出的)

可以不使用位操作來提退出信息。

status:

????????WIFEXITED(status): 若為正常終止子進程返回的狀態,則為真。(查看進程是否是正常退出)

????????WEXITSTATUS(status): 若WIFEXITED非零,提取子進程退出碼。(查看進程的退出碼)

options:

????????WNOHANG: 若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待。若正常結束,則返回該子進程的ID。

修改代碼:

#include<stdio.h>#include<unistd.h>#include<string.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>void ChildRun()
{int cnt = 5;while(cnt){printf("i'm child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);sleep(2);cnt--;}
}int main()
{printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id == 0){//childChildRun();printf("child quit ...\n");exit(1);}//fathersleep(15);//pid_t rid = wait(NULL);//pid_t waitpid(pid_t pid, int* wstatus, int options) ;   int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){if(WIFEXITED(status)){printf("child  quit success, child quit code: %d\n", WEXITSTATUS(status));}else{printf("child quit unnormal\n");}printf("wait Success, rid: %d\n", rid);}else{printf("wait fail!\n");}sleep(3);
}

運行結果:子進程退出碼是1

現在寫一個異常代碼:

運行結果:子進程非正常退出

父進程等待子進程是必須的,獲取子進程退出信息不是必須的

現在讓父進程不sleep(15)了:

如果子進程沒有退出,二父進程在執行waitpid進行等待。

阻塞等待? ?----> 進程阻塞了 --->? 進程狀態從 r 狀態 變成非 r 狀態。在等待某種條件發生(例如今上文說的子進程退出)。

如何不阻塞等待,因為此時父進程除了等待,沒做其他事

非阻塞等待

pid_t waitpid(pid_t pid, int *status, int options) --->當 options = 0就默認是阻塞等待

宏:WNOHANG? --->? 以非阻塞等待? ? ,HANG --> hang(服務器卡住了,服務器hang住了)

服務器hang很久,計算機掛掉? ----> 服務器宕機了。WNOHANG -->wait no hang,等待的時候不要hang住。

非阻塞等待的特點(優點):

1. 檢測子進程狀態的變化,是否就緒

阻塞等待

pid_t > 0 : 等待成功,子進程退出了,并且父進程回收成功,并退出

pid_t < 0 :等待失敗了

非阻塞等待:

pid_t? ==? 0:檢測是成功的,只不過子進程還沒有退出,需要你下一次進行重復等待。

非阻塞等待接口 + 循環? =? 非阻塞輪詢方案

2. 在非阻塞輪詢的時候允許父進程做其他的事情

實現以上描述的代碼:? 子進程開始進行,父進程檢測子進程狀態變化,當子進程還沒有結束時,父進程會再次檢查子進程狀態,直到id>0即子進程退出,父進程再檢測子進程退出狀態,檢測完成后,父進程退出。

void ChildRun()
{int cnt = 5;while(cnt){printf("i'm child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);sleep(3);cnt--;}
}int main()
{printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id == 0){//childChildRun();printf("child quit ...\n");exit(123);}//father//循環對子進程的狀態進行訪問while (1) {int status = 0;pid_t rid = waitpid(id, &status, WNOHANG);if (rid == 0) {printf("child is running, father check next time!\n");sleep(1); // 避免高頻輪詢,適當休眠} else if (rid == id) {  // 或 rid > 0if (WIFEXITED(status)) {printf("child quit success, code: %d\n", WEXITSTATUS(status));} else {printf("child quit abnormally\n");}break;} else {  // rid == -1,出錯printf("waitpid failed!\n");break;}}
}

運行結果:

實際狀況下,父進程的檢測更為頻繁。可以自己使用這個速度來觀測一下。

當前我們設計一個父進程在檢測子進程狀態時,去做其他任務的場景,這個代碼加入到上面的代碼中

typedef void(*func_t)();//定義一個函數指針類型//想讓父進程完成的任務
#define N 3
func_t task[N] = {NULL};//加載任務
void LoadTask()
{
}//處理凡是在任務列表中的任務
void HanderlTask()
{
}//做除了等待子進程外的其他任務
void DoOtherThing()
{HandlerTask();
}

再創建兩個文件:作為我們的任務

task.c

#include"task.h"void PrintLog()
{printf("開始打印我的 LOG...\n");}void Download()
{printf("開始下載軟件...\n");}
//數據庫同步void MysqlDataSync()
{printf("數據正在同步...\n");}

task.h

#pragma once#include<stdio.h>void PrintLog();void Download();void MysqlDataSync();

然后在myprocess.c中:

#include<stdio.h>#include<unistd.h>#include<string.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>#include"task.h"typedef void(*func_t)();//定義一個函數指針類型//想讓父進程完成的任務
#define N 3
func_t task[N] = {NULL};//加載任務
void LoadTask() //函數名就是函數的地址,此時將地址交給這個函數指針,此時相當于有一張函數表
{task[0] = PrintLog;task[1] = Download;task[2] =  MysqlDataSync;
}//處理凡是在任務列表中的任務
void HandlerTask()
{for(int i = 0; i < N; i++){task[i]();}
}//做除了等待子進程外的其他任務
void DoOtherThing()
{HandlerTask();
}void ChildRun()
{//int* p = NULL;int cnt = 5;while(cnt){printf("i'm child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);sleep(1);cnt--;// *p = 100;}
}int main()
{printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id == 0){//childChildRun();printf("child quit ...\n");exit(123);}//啟動父進程之前,先加載任務:LoadTask();//father//循環對子進程的狀態進行訪問while(1){int status = 0;pid_t rid = waitpid(id, &status, WNOHANG);//非阻塞 non block//查詢到子進程狀態,但是子進程還沒有退出if(rid == 0){usleep(1000);printf("child is running, father check next time!\n");DoOtherThing();}//等待子進程退出成功else if( rid == id){//檢測是否是正常退出if(WIFEXITED(status)){printf("child  quit success, child quit code: %d\n", WEXITSTATUS(status));}else{printf("child quit unnormal\n");}break;}else{printf("waitpid failed!\n");}}

運行程序:上面省略

child is running, father check next time!
開始打印我的 LOG...
開始下載軟件...
數據正在同步...
child is running, father check next time!
開始打印我的 LOG...
開始下載軟件...
數據正在同步...
child is running, father check next time!
開始打印我的 LOG...
開始下載軟件...
數據正在同步...
child is running, father check next time!
開始打印我的 LOG...
開始下載軟件...
數據正在同步...
child quit ...
child is running, father check next time!
開始打印我的 LOG...
開始下載軟件...
數據正在同步...
child  quit success, child quit code: 123

這是為了證明在非阻塞狀態,當父進程在進行輪詢檢測時還能夠做其他的事情

編譯文件之前記得修改makefile

從此以后我們可以直接修改task.c中的代碼,來讓父進程做任何事情,基于函數指針級別的對于父進程完成任務進行解耦。有了多進程將來就可以創建很多進程,將來可以讓不同子進程進行不同的任務,讓不同的程序來跑其他的任務。


五、進程程序替換

替換原理

用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec并不創建新進程,所以調用exec前后該進程的id并未改變。

1. 通過代碼看現象

替換函數

其實有六種以exec開頭的函數,統稱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[])

參數中的 ‘...’ 表示可變參數

這里的execl就類似于printf函數。

如何快捷的在vim編譯器中修改替代一個字符串:

%s/要修改的字符串/修改后的字符串/g,如修改makefile文件經常使用

代碼:

運行結果:

?請注意ls命令就在 usr/bin/ls 處,這里也沒有打印出我寫的"testexec ... end!"這個字符串

(這個代碼已經被替換)

通過:

file /usr/bin/ls 

實際上ls就是用c語言寫的一個可執行二進制程序:

exec*函數的作用就是讓我們(我們寫的程序):執行起來新的程序來替換我們寫的程序

2. 解釋原理

進程 = 內核數據結構 + 代碼和數據

程序替換:在執行進程的時候,保留原有的內核數據結構,結構本身不變(部分屬性值更改),再將新程序的代碼和數據和原有的在物理內存上的代碼和數據進行替換覆蓋,取代原有的代碼和數據。

在進行替換的時候,沒有創建新的進程。

?查看此圖:

程序運行之前,會先被加載到內存里,根據馮諾依曼定理,我的程序最總是會被cpu訪問的,cpu只能就近訪問內存,是通過 exec* 函數(類似于一種Linux上的加載函數)加載的。

exec* 函數的返回值不需要關心,只要替代成功就不會運行后面的代碼,反之,只要后面的代碼運行了,就沒有替換成功,因此不需要通過返回值來判斷是否替換成功。


想要進行程序替換但是不想要影響父進程本身:

3. 將代碼改成多進程版

檢測一次失敗的替換:?

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>int main()
{printf("testexec ... begin\n");pid_t id = fork();if(id == 0){//childsleep(2);execl("/usr/bin/lsss", "lsss", "-all", NULL);exit(1);}//fatherint status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id){printf("father wait success, child exit code : %d\n", WEXITSTATUS(status));}printf("testexec ... end\n");return 0;
}

運行結果:

修改代碼后:ls的名字的修改

創建子進程,讓子進程完成任務:

1.讓子進程執行父進程代碼的一部分

2.讓子進程執行一個全新的程序

3.在子進程要執行新程序的時候,進行程序替換的時候,不僅數據發生了寫時拷貝,代碼也發生了寫時拷貝,不再是僅僅是共享而不能寫入。至此父子進程在數據結構和代碼層面徹底分開。因此不會對父進程產生影響。


4. 使用所有的替換方法,并且認識函數參數的含義

程序替換的接口都是什么意思:

int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);int execlp(const char *file, const char *arg, .../* (char  *) NULL */);int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);int execv(const char *pathname, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[],char *const envp[]);

1.exec ----?l -- list 代表的是命令列表

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

path:要執行的程序,需帶路徑。函數需要知道怎么找到程序,需要告訴函數

arg ----> 選項 ,在命令行中怎么執行就怎么傳參,例如命令后的選項,帶多少個都可以

ls -a -l -i -n

標準寫法:以NULL結尾,可以省略,但是建議不省略

int execv(const char *pathname, char *const argv[]);

2.execv -- v ---> vector,類似于c++中所學到的動態數組 vector, 使用的時候就是現將要執行的選項放進數組里,然后再進行傳參:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>int main()
{printf("testexec ... begin\n");pid_t id = fork();if(id == 0){//childchar* const argv[] = {"ls","-l","-a","--color",NULL};sleep(2);// execl("/usr/bin/ls", "ls", "-all", NULL);execv("/usr/bin/ls", argv);exit(1);}//fatherint status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id){printf("father wait success, child exit code : %d\n", WEXITSTATUS(status));}printf("testexec ... end\n");return 0;
}

3.execvp -- p ----> 用戶可以不穿要執行文件的路徑(要傳文件名),直接告訴exec*, 我要執行誰就行,p: 查找這個程序,系統會自動在環境變量PATH中進行查找。

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

使用方法:

execvp("ls", argv);

4.execlp ->

int execlp(const char *file, const char *arg, .../* (char  *) NULL */);

使用方法:

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

程序替換 --- 替換自己寫的程序

上面的程序替換,我們替換的都是系統命令,可不可以替換成我們自己寫的程序呢?當讓可以。

提供一個新文件:mypragma.cc

#include<iostream>using namespace std;int main()
{cout << "hello c++, i'm c++ pragma!" << endl;cout << "hello c++, i'm c++ pragma!" << endl;cout << "hello c++, i'm c++ pragma!" << endl;cout << "hello c++, i'm c++ pragma!" << endl;cout << "hello c++, i'm c++ pragma!" << endl;cout << "hello c++, i'm c++ pragma!" << endl;return 0;
}

修改makefile文件讓他能一次性執行兩個程序:

運行后,卻只執行了mypragma,因為makefile在執行程序時,從上至下匹配時,只會默認形成第一個目標文件所對應的可執行程序,推導他的依賴關系。

解決辦法以及實現原理:

不能讓其中的一個成為第一個目標文件,使用.PHONY,定義一個偽目標all,all所依賴的是這兩個目標文件,有依賴關系,不寫依賴方法,makefile在從上往下掃描時,先掃描到的是all,這個all 就成為第一個目標文件,其中并沒有依賴方法,makefile就會推all其中mypragma,testexec的依賴關系:

.PHONY:all
all:mypragma testexecmypragma:mypragma.ccg++ -o $@ $^ -std=c++11testexec:testexec.cgcc -o $@ $^
.PHONY:clean
clean:rm -f testexec mypragma

現在學習如何替換成自己寫的程序:

1.使用execl做測試:

"./mypragma" ---> 通過相對路徑找到要執行的文件程序,mypragma? ---> 在命令行需要輸入的命令,因為已經找到程序了,就不用再寫 "./"?

此時執行程序:自己寫的程序被調度起來了

細節提及:在之前我們有說過,在程序替換的時候,并沒有形成新的進程,是代碼和數據被寫時拷貝到原來的內核數據結構當中了。

驗證:

1.先在testexec.c代碼中添加打印子進程pid:

2.再在mypragma.cc需執行的替換文件中添加打印子進程pid,驗證兩pid是否相等,來看是否是同一個進程:

#include<iostream>
#include<unistd.h>
using namespace std;int main()
{cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;return 0;
}

運行結果:

驗證成功,兩者的子進程pid并沒有發生變換。

調用其他語言的程序

驗證除了替換c++語言程序,還是否能替換其他語言的程序呢?

首先驗證一下ubuntu系統中是否有python:

出現<<<后使用quit();退出

創建一個python文件test.py,?

成功運行:

或者是shell腳本語言都可以:

#!/usr/bin/bashcnt=0
while [ $cnt -le 10 ]  # 在 10 和 ] 之間添加空格
doecho "hello shell, cnt: ${cnt}"let cnt++
done

運行成功:

進行替換,同理:

運行結果:

python也同理。

所有的語言在運行的時候在系統中都會變成進程,因此就可以被調用。

想使用 “./” 去運行其他語言的程序可以給他們加入可執行權限:

5.execvpe? ?---- e? ----> evironment ---> 環境變量,允許我們自定義

int execvpe(const char *file, char *const argv[],char *const envp[]);

在環境變量的參數位置寫NULL

程序雖然有個報錯但是依然能執行(請忽略這個報錯):

添加環境變量:

此時將參數顯示傳入main函數中,并打印傳入的命令行參數和環境變量 :

#include<iostream>
#include<unistd.h>
using namespace std;int main(int argc, char *argv[], char *env[])
{int i = 0;//打印命令行參數for(; argv[i]; i++){printf("argv[%d] : %s\n", i, argv[i]);}printf("------------------------\n");//打印環境變量for(i = 0; env[i]; i++){printf("env[%d] : %s\n", i, env[i]);} printf("------------------------\n");cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;cout << "hello c++, i'm c++ pragma!, mypid: " << getpid() << endl;return 0;
}

運行結果:

選項也能傳:

運行結果:

環境變量是父進程給子進程的,父進程現有一張命令行參數和環境變量的表再傳給子進程。

那么子進程的父進程的父進程是誰? --- bash,父進程本身就有一批環境變量。

實際上可以不傳自定義的環境變量,直接傳bash本身有的環境變量給傳入:

運行結果:

而當我們傳的是自定義的環境變量的時候只會出現我們自定義的環境變量,這種現象,表明了我們整體替換了所有的環境變量。

使用老的環境變量記得定義

extern char **environ


或者想要適當修改(新增環境變量到舊的環境變量當中):

誰調用這個函數,就會導出一個新的環境變量

這樣就會直接在環境變量的表里面直接新增,而不是替換整個環境變量的表

最后運行結果:

6.execve ---真正的系統調用

事實上,只有execve是真正的系統調用,其它五個函數最終都調用 execve,所以execve在man手冊 第2節,其它函數在man手冊第3節。這些函數之間的關系如下圖所示

上圖所說的所有接口都是最終系統調用execve()的封裝用來支持不同的應用場景


結語:

? ? ? ?隨著這篇關于題目解析的博客接近尾聲,我衷心希望我所分享的內容能為你帶來一些啟發和幫助。學習和理解的過程往往充滿挑戰,但正是這些挑戰讓我們不斷成長和進步。我在準備這篇文章時,也深刻體會到了學習與分享的樂趣。 ? ?

? ? ? ? ?在此,我要特別感謝每一位閱讀到這里的你。是你的關注和支持,給予了我持續寫作和分享的動力。我深知,無論我在某個領域有多少見解,都離不開大家的鼓勵與指正。因此,如果你在閱讀過程中有任何疑問、建議或是發現了文章中的不足之處,都歡迎你慷慨賜教。 ? ? ? ? ? ? ??

? ? ? ? 你的每一條反饋都是我前進路上的寶貴財富。同時,我也非常期待能夠得到你的點贊、收藏,關注這將是對我莫大的支持和鼓勵。當然,我更期待的是能夠持續為你帶來有價值的內容,讓我們在知識的道路上共同前行。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/898187.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/898187.shtml
英文地址,請注明出處:http://en.pswp.cn/news/898187.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

深入了解Linux —— git三板斧

版本控制器git 為了我們方便管理不同版本的文件&#xff0c;就有了版本控制器&#xff1b; 所謂的版本控制器&#xff0c;就是能夠了解到一個文件的歷史記錄&#xff08;修改記錄&#xff09;&#xff1b;簡單來說就是記錄每一次的改動和版本迭代的一個管理系統&#xff0c;同…

STM32---FreeRTOS事件標志組

一、簡介 事件標志位&#xff1a;用一個位&#xff0c;來表示事件是否發生 事件標志組&#xff1a;一組事件標志位的集合&#xff0c;可以簡單的理解時間標志組&#xff0c;就是一個整體。 事件標志租的特點&#xff1a; 它的每一個位表示一個時間&#xff08;高8位不算&…

在centOS Linux系統搭建自動化構建工具Jenkins

前言 在工作中發現公司使用Jenkins實現自動化部署項目方案&#xff0c;于是閑著自己也搗鼓一下&#xff0c;網上查閱相關部署資料&#xff0c;順便記錄操作步驟&#xff0c;所以有了下面這篇的文章。 部署完之后&#xff0c;安裝前端項目所需環境&#xff0c;比如node環境&am…

Git下載安裝(保姆教程)

目錄 1、Git下載 2、Git安裝&#xff08;windows版&#xff09; &#xff08;1&#xff09;啟動安裝程序 &#xff08;2&#xff09;閱讀許可協議 &#xff08;3&#xff09;選擇安裝路徑 &#xff08;4&#xff09;選擇組件 &#xff08;5&#xff09;選擇開始菜單文件夾…

深入理解嵌入式開發中的三個重要工具:零長度數組、container_of 和 typeof

在嵌入式開發中,內核開發者經常需要處理復雜的數據結構和動態內存分配。零長度數組、container_of 宏和 typeof 是內核開發中三個非常重要的工具,它們在結構體管理、內存操作和類型處理中發揮著關鍵作用。本文將詳細探討這三個工具的功能、應用場景及其在內核開發中的重要性。…

【react】react中的<></>和React Fragment的用法及區別詳解

目錄 1、<>是什么 2、為什么要使用<>&#xff1f; 3、如何使用<>&#xff1f; 基本用法 需要傳遞屬性時&#xff08;如key&#xff09; 使用效果 注意事項 總結 4、React Fragment 與空標簽&#xff08;<>&#xff09;詳解 1. Fragment 的用…

【人工智能】使用Python實現時間序列異常檢測:從基礎到深度學習模型的全方位探索

《Python OpenCV從菜鳥到高手》帶你進入圖像處理與計算機視覺的大門! 解鎖Python編程的無限可能:《奇妙的Python》帶你漫游代碼世界 時間序列異常檢測是數據分析領域中的重要課題,廣泛應用于金融、醫療、工業監控等多個行業。本篇文章深入探討了時間序列異常檢測的基本技術…

Keytool常見問題全解析:從環境配置到公鑰提取

引言 在Android開發、跨平臺應用構建&#xff08;如UniApp&#xff09;或服務端證書管理中&#xff0c;keytool 是一個不可或缺的工具。然而&#xff0c;許多開發者在使用 keytool 時&#xff0c;常因環境配置、路徑權限、密碼問題等導致操作失敗。本文基于真實問題場景&#…

TSB - AD 解讀 — 邁向可靠、透明的 TSAD 任務

目錄 一 文章動機 二 TSAD 領域內的兩類缺陷 三 數據集的構建 四 實驗結果及結論 項目宣傳鏈接&#xff1a;TSB-AD 代碼鏈接&#xff1a; TheDatumOrg/TSB-AD: TSB-AD: Towards A Reliable Time-Series Anomaly Detection Benchmark 原作者解讀&#xff1a;NeurIPS 2…

DNS主從服務器

1.1環境準備 作用系統IP主機名web 服務器redhat9.5192.168.33.8webDNS 主服務器redhat9.5192.168.33.18dns1DNS 從服務器redhat9.5192.168.33.28dns2客戶端redhat9.5192.168.33.7client 1.2修改主機名和IP地址 web服務器 [rootweb-8 ~]# hostnamectl hostname web [rootweb-8…

遙感數據獲取、處理、分析到模型搭建全流程學習!DeepSeek、Python、OpenCV驅動空天地遙感數據分析

【扔進數據&#xff0c;直接出結果】在科技飛速發展的時代&#xff0c;遙感數據的精準分析已經成為推動各行業智能決策的關鍵工具。從無人機監測農田到衛星數據支持氣候研究&#xff0c;空天地遙感數據正以前所未有的方式為科研和商業帶來深刻變革。然而&#xff0c;對于許多專…

第一個vue項目

項目目錄 啟動vue項目 npm run serve 1.vue.config.js文件 (CLI通過vue-cli-serve啟動項目&#xff0c;解析配置配置文件vue-condig-js&#xff09; // vue.config.js //引入path板塊&#xff0c;這是Node.js的一個內置模塊&#xff0c;用于處理文件路徑&#xff0c;這里引用…

QT中讀取QSetting文件

1.ini文件的格式 頭文件 #include <QSettings> #include <QStringList> #include <QtCore> #include <QDebug>2.讀文件 //ini文件的讀取 void iniTest::readIniFile(QString filePath) {//1.打開ini文件QSettings m_iniFile(filePath, QSettings::I…

卷積神經網絡 - 一維卷積、二維卷積

卷積(Convolution)&#xff0c;也叫褶積&#xff0c;是分析數學中一種重要的運算。在信號處理或圖像處理中&#xff0c;經常使用一維或二維卷積&#xff0c;本博文我們來學習一維卷積和二維卷積。 理解一維卷積和二維卷積的核心在于把握維度對特征提取方式的影響。我們從數學定…

java學習總結(六)Spring IOC

一、Spring框架介紹 Spring優點&#xff1a; 1、方便解耦&#xff0c;簡化開發,IOC控制反轉 Spring 就是一個大工廠&#xff0c;可以將所有對象創建和依賴關系維護交給Spring 2、AOP 編程的支持 Spring 提供面向切編程&#xff0c;可以方便的實現對序進行權限攔截、運監控等…

大模型推理:LM Studio在Mac上部署Deepseek-R1模型

LM Studio LM Studio是一款支持離線大模型部署的推理服務框架&#xff0c;提供了易用的大模型部署web框架&#xff0c;支持Linux、Mac、Windows等平臺&#xff0c;并提供了OpenAI兼容的SDK接口&#xff0c;主要使用LLama.cpp和MLX推理后端&#xff0c;在Mac上部署時選擇MLX推理…

AI技術學習筆記系列004:GPU常識

顯卡架構是GPU設計的核心&#xff0c;不同廠商有其獨特的架構演進。以下是主要廠商的顯卡架構概述&#xff1a; 一、NVIDIA Tesla&#xff08;2006-2010&#xff09; 代表產品&#xff1a;GeForce 8000系列&#xff08;G80&#xff09;。特點&#xff1a;首款統一著色架構&…

實驗- 分片上傳 VS 直接上傳

分片上傳和直接上傳是兩種常見的文件上傳方式。分片上傳將文件分成多個小塊&#xff0c;每次上傳一個小塊&#xff0c;可以并行處理多個分片&#xff0c;適用于大文件上傳&#xff0c;減少了單個請求的大小&#xff0c;能有效避免因網絡波動或上傳中斷導致的失敗&#xff0c;并…

Android視頻渲染SurfaceView強制全屏與原始比例切換

1.創建UI添加強制全屏與播放按鈕 2.SurfaceView控件設置全屏顯示 3.全屏點擊事件處理實現 4.播放點擊事件處理 5.使用接口更新強制全屏與原始比例文字 強制全屏/原始比例 點擊實現

數據結構——串、數組和廣義表

串、數組和廣義表 1. 串 1.1 串的定義 串(string)是由零個或多個字符組成的有限序列。一般記為 S a 1 a 2 . . . a n ( n ≥ 0 ) Sa_1a_2...a_n(n\geq0) Sa1?a2?...an?(n≥0) 其中&#xff0c;S是串名&#xff0c;單引號括起來的字符序列是串的值&#xff0c; a i a_i a…