【Linux 進(jìn)程控制】—— 進(jìn)程亦生生不息:起于鴻蒙,守若空谷,歸于太虛

進(jìn)程創(chuàng)建再識(shí)fork函數(shù)

linux中 fork 函數(shù)是非常重要的函數(shù),它從已存在進(jìn)程中創(chuàng)建?個(gè)新進(jìn)程。創(chuàng)建出來的新進(jìn)程叫做子進(jìn)程,而原進(jìn)程則稱為父進(jìn)程。

linux參考手冊(cè)中,fork函數(shù)的原型如下:(man 2 fork 指令查看)

代碼語言:JavaScript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

NAME       fork - create a child processSYNOPSIS       #include <sys/types.h>       #include <unistd.h>       pid_t fork(void);

如上不難看出:

fork 函數(shù)的功能是創(chuàng)建一個(gè)子進(jìn)程頭文件有 參數(shù)為 void ,返回值為 pid_t (實(shí)際上是Linux內(nèi)核中typedef出來的一個(gè)類型)


進(jìn)程調(diào)用 fork,當(dāng)控制轉(zhuǎn)移到內(nèi)核中的 fork 代碼后,內(nèi)核做如下幾件事:

分配新的內(nèi)存塊和內(nèi)核數(shù)據(jù)結(jié)構(gòu)給子進(jìn)程將父進(jìn)程部分?jǐn)?shù)據(jù)結(jié)構(gòu)內(nèi)容拷貝至子進(jìn)程添加子進(jìn)程到系統(tǒng)進(jìn)程列表當(dāng)中fork返回,開始調(diào)度器調(diào)度

【Linux 進(jìn)程控制】—— 進(jìn)程亦生生不息:起于鴻蒙,守若空谷,歸于太虛

當(dāng)?個(gè)進(jìn)程調(diào)用fork之后,就有兩個(gè)?進(jìn)制代碼相同的進(jìn)程。并且它們都運(yùn)行到相同的地方。但每個(gè)進(jìn)程都將可以開始屬于它們自己的旅程,看如下程序:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(void){    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;}

輸出:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

Before: pid is 40176After:pid is 40176, fork return 40177After:pid is 40177, fork return 0

如下圖所示:

【Linux 進(jìn)程控制】—— 進(jìn)程亦生生不息:起于鴻蒙,守若空谷,歸于太虛

所以,fork之前父進(jìn)程獨(dú)立執(zhí)行,fork之后,父子進(jìn)程兩個(gè)執(zhí)行流分別執(zhí)行之后的代碼。值得注意的是,fork之后,誰先執(zhí)行完全由調(diào)度器決定,并沒有明確的先后關(guān)系!


fork函數(shù)返回值

fork創(chuàng)建成功:

子進(jìn)程返回0父進(jìn)程返回的是子進(jìn)程的 pid

為什么給父進(jìn)程返回子進(jìn)程的pid,這個(gè)問題我們之前已經(jīng)討論過:

為什么子進(jìn)程返回0


fork創(chuàng)建失敗:

返回 -1并設(shè)置錯(cuò)誤碼:

當(dāng)系統(tǒng)資源不足(如進(jìn)程數(shù)超限、內(nèi)存耗盡)時(shí),fork() 失敗。

錯(cuò)誤碼:

需檢查 errno 確定具體原因代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

if (pid == -1) {    perror("fork failed"); // 輸出類似 "fork failed: Resource temporarily unavailable"}

常見錯(cuò)誤碼:

EAGAIN:進(jìn)程數(shù)超過限制(RLIMIT_NPROC)或內(nèi)存不足。ENOMEM:內(nèi)核無法分配必要數(shù)據(jù)結(jié)構(gòu)所需內(nèi)存。


寫時(shí)拷貝 copy-On-Write

為什么需要寫時(shí)拷貝?

在傳統(tǒng)的進(jìn)程創(chuàng)建方式中,fork() 會(huì)直接復(fù)制父進(jìn)程的所有內(nèi)存空間給子進(jìn)程。這種方式存在明顯問題:

內(nèi)存浪費(fèi):如果父進(jìn)程占用 1GB 內(nèi)存,子進(jìn)程即使不修改任何數(shù)據(jù),也會(huì)立即消耗額外 1GB 內(nèi)存。性能低下:復(fù)制大量?jī)?nèi)存需要時(shí)間,尤其是對(duì)大型進(jìn)程而言,fork() 會(huì)顯著延遲程序運(yùn)行。

COW 的解決思路:

推遲實(shí)際的內(nèi)存復(fù)制,直到父子進(jìn)程中某一方嘗試修改內(nèi)存頁時(shí),才進(jìn)行真正的拷貝。在此之前,父子進(jìn)程共享同一份物理內(nèi)存。

具體見下圖:

【Linux 進(jìn)程控制】—— 進(jìn)程亦生生不息:起于鴻蒙,守若空谷,歸于太虛

因?yàn)橛袑憰r(shí)拷貝技術(shù)的存在,所以父子進(jìn)程得以徹底分離!完成了進(jìn)程獨(dú)立性的技術(shù)保證! 寫時(shí)拷貝,是?種延時(shí)申請(qǐng)技術(shù),可以提高整機(jī)內(nèi)存的使用率。

寫時(shí)拷貝的工作流程

1、 fork() 調(diào)用時(shí)

共享內(nèi)存頁:內(nèi)核僅為子進(jìn)程創(chuàng)建虛擬內(nèi)存結(jié)構(gòu)(頁表),但物理內(nèi)存頁仍與父進(jìn)程共享。標(biāo)記為只讀:內(nèi)核將共享的物理內(nèi)存頁標(biāo)記為只讀(即使父進(jìn)程原本可寫)。

2、進(jìn)程嘗試寫入內(nèi)存

觸發(fā)頁錯(cuò)誤:當(dāng)父進(jìn)程或子進(jìn)程嘗試修改某個(gè)共享內(nèi)存頁時(shí),由于頁被標(biāo)記為只讀,CPU 會(huì)觸發(fā)頁錯(cuò)誤(Page Fault)。

內(nèi)核介入處理:操作系統(tǒng)會(huì)由用戶態(tài)陷入內(nèi)核態(tài)處理異常

分配新的物理內(nèi)存頁,復(fù)制原頁內(nèi)容到新頁。修改觸發(fā)寫入的進(jìn)程的頁表,使其指向新頁。將新頁標(biāo)記為可寫,恢復(fù)進(jìn)程執(zhí)行。

3、后續(xù)操作

修改后的進(jìn)程獨(dú)享新內(nèi)存頁,另一進(jìn)程仍使用原頁。未修改的內(nèi)存頁繼續(xù)共享,不做復(fù)制,操作系統(tǒng)不做任何無意義的事情。


進(jìn)程等待

之前我們?cè)谥v進(jìn)程概念的時(shí)候講過,如果父進(jìn)程創(chuàng)建出子進(jìn)程后,如果子進(jìn)程已經(jīng)退出,父進(jìn)程卻沒有對(duì)子進(jìn)程回收,那么就子進(jìn)程就會(huì)變成 “僵尸進(jìn)程” ,造成內(nèi)存泄露等問題。

進(jìn)程等待的必要性

僵尸進(jìn)程問題:

子進(jìn)程終止后,其退出狀態(tài)會(huì)保留在進(jìn)程表中,直到父進(jìn)程讀取。若父進(jìn)程未處理,子進(jìn)程將保持僵尸狀態(tài)(Zombie),占用系統(tǒng)資源。狀態(tài)收集:父進(jìn)程需知曉子進(jìn)程的執(zhí)行結(jié)果(成功、錯(cuò)誤代碼、信號(hào)終止等)。資源回收:內(nèi)核釋放子進(jìn)程占用的內(nèi)存、文件描述符等資源。進(jìn)程等待的方法wait代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

#include<sys/types.h>#include<sys/wait.h>pid_t wait(int* status);

具體功能:

阻塞父進(jìn)程,直到等待到任意一個(gè)子進(jìn)程終止。

參數(shù):

status:輸出型參數(shù),用來存儲(chǔ)子進(jìn)程退出狀態(tài)的指針(可為 NULL,表示不關(guān)心狀態(tài))。

返回值:

成功:返回終止的子進(jìn)程PID。失敗:返回-1(如無子進(jìn)程)。


waitpid代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);

功能:更靈活的等待方式,可指定子進(jìn)程或非阻塞等待模式。

參數(shù):

pid:

>0:等待指定 PID 的子進(jìn)程。-1:等待任意子進(jìn)程(等效于 wait)。0:等待同一進(jìn)程組的子進(jìn)程。

status:同 wait,輸出型參數(shù),表明子進(jìn)程的退出狀態(tài)。

options: 默認(rèn)為0,表示阻塞等待

WNOHANG:非阻塞模式,無子進(jìn)程終止時(shí)立即返回 0。WUNTRACED:報(bào)告已停止的子進(jìn)程(如被信號(hào)暫停)。

返回值:

成功:返回子進(jìn)程PID。WNOHANG 且無子進(jìn)程終止:返回0。失敗:返回-1。

做個(gè)總結(jié):

如果子進(jìn)程已經(jīng)退出,調(diào)用 wait / waitpid 時(shí),wait / waitpid 會(huì)立即返回,并且釋放資源,獲得子進(jìn)程退出信息。如果在任意時(shí)刻調(diào)用 wait / waitpid,子進(jìn)程存在且正常運(yùn)行,則進(jìn)程可能阻塞。 如果不存在該子進(jìn)程,則立即出錯(cuò)返回。

【Linux 進(jìn)程控制】—— 進(jìn)程亦生生不息:起于鴻蒙,守若空谷,歸于太虛

獲取子進(jìn)程 status

wait 和 waitpid,都有?個(gè) status 參數(shù),該參數(shù)是?個(gè)輸出型參數(shù),由操作系統(tǒng)填充。

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

status 不能簡(jiǎn)單的當(dāng)作整形來看待,可以當(dāng)作位圖來看待,具體細(xì)節(jié)如下圖 (只研究 status 低16比特位):

【Linux 進(jìn)程控制】—— 進(jìn)程亦生生不息:起于鴻蒙,守若空谷,歸于太虛

如何理解呢? 子進(jìn)程的退出分為兩種情況:

正常終止

高 8 位(第 8 ~ 15 位):保存子進(jìn)程的退出狀態(tài)(退出碼)(即 exit(code) 或 return code 中的 code 值)。

第 7 位:通常為 0,表示正常終止。

示例:

若子進(jìn)程調(diào)用 exit(5),表明子進(jìn)程是正常退出,則 status 的高 8 位為 00000101(即十進(jìn)制 5)。

被信號(hào)所殺導(dǎo)致終止

低 7 位(第 0 ~ 6 位):保存導(dǎo)致子進(jìn)程終止的信號(hào)編號(hào)。

第 7 位:若為 1,表示子進(jìn)程在終止時(shí)生成了 core dump 文件(用于調(diào)試)。有關(guān) core dump 文件,后面會(huì)講,大家這里先了解一下即可。

第 8 ~ 15 位:未使用(通常為 0)。

示例:

若子進(jìn)程因 SIGKILL(信號(hào)編號(hào) 9)終止,則 status 的低 7 位為 0001001(即十進(jìn)制 9)。

做個(gè)小總結(jié):代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

低 16 位結(jié)構(gòu):| 15 14 13 12 11 10 9 8 | 7 | 6 5 4 3 2 1 0 |---------------------------------------------正常終止 → [ 退出狀態(tài)(高8位) ]  0  [ 未使用 ]被信號(hào)終止 → [   未使用(全0)   ] c  [ 信號(hào)編號(hào) ]

如何解析 status?

使用宏定義檢查 status 的值:

功能

WIFEXITED(status)

若子進(jìn)程正常終止(exit 或 return)返回真。

WEXITSTATUS(status)

若 WIFEXITED 為真,返回子進(jìn)程的退出碼(exit 的參數(shù)或 return 的值)。

WIFSIGNALED(status)

若子進(jìn)程因信號(hào)終止返回真。

WTERMSIG(status)

若 WIFSIGNALED 為真,返回導(dǎo)致終止的信號(hào)編號(hào)。

WCOREDUMP(status)

若子進(jìn)程生成了核心轉(zhuǎn)儲(chǔ)文件返回真。

常用的兩個(gè)宏:

WIFEXITED(status): 若為正常終止子進(jìn)程返回的狀態(tài),則為真。(查看進(jìn)程是 否是正常退出)WEXITSTATUS(status): 若WIFEXITED非零,提取子進(jìn)程退出碼。(查看進(jìn)程的 退出碼)

示例一:子進(jìn)程正常退出

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    pid_t pid = fork();    if (pid == 0)    { // 子進(jìn)程        printf("子進(jìn)程運(yùn)行中... PID=%dn", getpid());        // 1. 正常退出:調(diào)用 exit(42)        exit(42);    }    else    { // 父進(jìn)程        int status;        waitpid(pid, &status, 0); // 等待子進(jìn)程結(jié)束        if (WIFEXITED(status))        { // 正常退出            printf("子進(jìn)程正常退出,退出碼: %dn", WEXITSTATUS(status));        }        else if (WIFSIGNALED(status))        { // 被信號(hào)終止            printf("子進(jìn)程被信號(hào)終止,信號(hào)編號(hào): %dn", WTERMSIG(status));        }    }    return 0;}

輸出:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

子進(jìn)程運(yùn)行中... PID=56153子進(jìn)程正常退出,退出碼: 42

示例二:子進(jìn)程被信號(hào)終止

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    pid_t pid = fork();    if (pid == 0)    { // 子進(jìn)程        printf("子進(jìn)程運(yùn)行中... PID=%dn", getpid());        int *p = NULL;        *p = 100;  // 對(duì)空指針解引用,觸發(fā) SIGSEGV 被信號(hào)終止    }    else    { // 父進(jìn)程        int status;        waitpid(pid, &status, 0); // 等待子進(jìn)程結(jié)束        if (WIFEXITED(status))        { // 正常退出            printf("子進(jìn)程正常退出,退出碼: %dn", WEXITSTATUS(status));        }        else if (WIFSIGNALED(status))        { // 被信號(hào)終止            printf("子進(jìn)程被信號(hào)終止,信號(hào)編號(hào): %dn", WTERMSIG(status));        }    }    return 0;}

輸出:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

子進(jìn)程運(yùn)行中... PID=56203子進(jìn)程被信號(hào)終止,信號(hào)編號(hào): 11

阻塞等待與非阻塞等待阻塞等待(Blocking Wait)代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

pid_t waitpid(pid_t pid, int *status, 0);  // options 參數(shù)為 0

示例:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    int status;    pid_t child_pid = fork();    if (child_pid == 0)    {        // 子進(jìn)程執(zhí)行任務(wù)        exit(10);    }    else    {        // 父進(jìn)程阻塞等待子進(jìn)程結(jié)束        waitpid(child_pid, &status, 0);        if (WIFEXITED(status))        {            printf("子進(jìn)程退出碼: %dn", WEXITSTATUS(status));        }    }}

非阻塞等待(Non-blocking Wait)

關(guān)鍵選項(xiàng):宏 WNOHANG(定義在 中)。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

pid_t waitpid(pid_t pid, int *status, WNOHANG);

示例:非阻塞輪詢方式

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    int status;    pid_t child_pid = fork();    if (child_pid == 0)    {        sleep(3); // 子進(jìn)程休眠 3 秒后退出        exit(10);    }    else    {        while (1)        {            pid_t ret = waitpid(child_pid, &status, WNOHANG);            if (ret == -1)            {                perror("waitpid");                break;            }            else if (ret == 0)            {                printf("子進(jìn)程未結(jié)束,父進(jìn)程繼續(xù)工作...n");                sleep(1); // 避免頻繁輪詢消耗 CPU            }            else            {                if (WIFEXITED(status))                {                    printf("子進(jìn)程退出碼: %dn", WEXITSTATUS(status));                }                break;            }        }    }}

阻塞等待和非阻塞等待的對(duì)比:

場(chǎng)景

阻塞等待

非阻塞等待

父進(jìn)程任務(wù)優(yōu)先級(jí)

必須立即處理子進(jìn)程結(jié)果

需同時(shí)處理其他任務(wù)

子進(jìn)程執(zhí)行時(shí)間

較短或確定

較長(zhǎng)或不確定

資源消耗

CPU 空閑,無額外開銷

需輪詢,可能占用更多 CPU

典型應(yīng)用

簡(jiǎn)單腳本、單任務(wù)場(chǎng)景

多進(jìn)程管理、事件驅(qū)動(dòng)程序


進(jìn)程終止

進(jìn)程= 內(nèi)核數(shù)據(jù)結(jié)構(gòu) + 進(jìn)程自己的代碼和數(shù)據(jù)

進(jìn)程退出場(chǎng)景代碼運(yùn)行完畢,結(jié)果正確代碼運(yùn)行完畢,結(jié)果不正確代碼異常終止

如何理解這三種進(jìn)程退出的場(chǎng)景呢?舉個(gè)例子

代碼運(yùn)行完畢,結(jié)果正確

程序完整執(zhí)行了所有邏輯,未觸發(fā)任何錯(cuò)誤或異常。輸出結(jié)果與預(yù)期完全一致,符合功能需求或算法目標(biāo)。代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int sum(int a, int b){    return a + b;}int main(){    int result = sum(3, 5);    printf("Result: %dn", result); // 輸出 8,結(jié)果正確    return 0;}

輸出:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

Result: 8

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

程序正常結(jié)束(無崩潰或異常),但輸出結(jié)果與預(yù)期不符。通常由邏輯錯(cuò)誤、算法錯(cuò)誤或數(shù)據(jù)處理錯(cuò)誤導(dǎo)致。

例如:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

// 錯(cuò)誤實(shí)現(xiàn):本應(yīng)計(jì)算階乘,但初始值錯(cuò)誤int factorial(int n){    int result = 0; // 錯(cuò)誤!應(yīng)為 result = 1    for (int i = 1; i <= n; i++)    {        result *= i;    }    return result;}int main(){    printf("5! = %dn", factorial(5)); // 輸出 0,結(jié)果錯(cuò)誤    return 0;}

代碼未執(zhí)行完畢,異常終止

程序未執(zhí)行完畢就中途崩潰或被強(qiáng)制終止。通常由運(yùn)行時(shí)錯(cuò)誤、資源限制或外部信號(hào)觸發(fā)。比如除零錯(cuò)誤,對(duì)空指針解引用等異常

例如:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    int *ptr = NULL;    *ptr = 42;  // 對(duì)空指針解引用,觸發(fā)段錯(cuò)誤    printf("Value: %dn", *ptr);    return 0;}

段錯(cuò)誤:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

Segmentation fault

再比如:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    int a = 10;    int b = a / 0; // 程序除零異常    printf("Value: %dn", b);    return 0;}

浮點(diǎn)數(shù)異常:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

Floating point exception

進(jìn)程常見退出方法

正常終止(可以通過 echo $? 查看進(jìn)程退出碼)

從main返回調(diào)用exit_exit

異常退出:

ctrl + c,信號(hào)終止


進(jìn)程退出碼

退出碼是一個(gè) 8 位無符號(hào)整數(shù)(8-bit unsigned Integer),因此取值范圍為 2^8=256 個(gè)值。

Linux Shell 中的常見退出碼:

【Linux 進(jìn)程控制】—— 進(jìn)程亦生生不息:起于鴻蒙,守若空谷,歸于太虛

退出碼 0 表示命令執(zhí)行有誤,這是完成命令的理想狀態(tài)。退出碼 1 我們也可以將其解釋為 “不被允許的操作”。例如在沒有 sudo 權(quán)限的情況下使用 yum;130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等終止信號(hào)是非常典型的,它們屬于 128+n 信號(hào),其中 n 代表信號(hào)編號(hào)。


這里需要補(bǔ)充一點(diǎn):

進(jìn)程退出碼和錯(cuò)誤碼是兩個(gè)完全不同的概念,不要混為一談!

錯(cuò)誤碼

要理解錯(cuò)誤碼,首先要認(rèn)識(shí)全局變量 error

例如:fork函數(shù)調(diào)用失敗后,會(huì)立刻返回-1,并設(shè)置全局變量 error

【Linux 進(jìn)程控制】—— 進(jìn)程亦生生不息:起于鴻蒙,守若空谷,歸于太虛

定義:errno 是一個(gè)線程安全的整型變量,用于存儲(chǔ)最近一次系統(tǒng)調(diào)用或庫函數(shù)調(diào)用失敗的錯(cuò)誤碼。

特性:

成功調(diào)用不會(huì)重置 errno,因此必須在調(diào)用后立即檢查其值。每個(gè)線程有獨(dú)立的 errno 副本(多線程安全)。

頭文件:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

#include <errno.h>

與之對(duì)應(yīng)的是 strerror 函數(shù),該函數(shù)可以將對(duì)應(yīng)的錯(cuò)誤碼轉(zhuǎn)化成字符串描述的錯(cuò)誤信息打印出來,方便程序員調(diào)試代碼。

實(shí)際上,我們可以通過 for 循環(huán)來打印查看Linux系統(tǒng)下所有的錯(cuò)誤碼以及其錯(cuò)誤信息:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    for (int i = 0; i < 135; ++i)    {        printf("%d-> %sn", i, strerror(i));    }    return 0;}

不難看出,在Linux系統(tǒng)下,一共有 0 ~ 133 總共134個(gè)錯(cuò)誤碼,其中 0 表示 success ,即程序運(yùn)行成功, 1 ~ 133 則分別對(duì)應(yīng)一個(gè)錯(cuò)誤信息。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

0-> Success1-> Operation not permitted2-> No such file or directory3-> No such process4-> Interrupted system call5-> Input/output error6-> No such device or address7-> Argument list too long8-> Exec format error9-> Bad file descriptor10-> No child processes11-> Resource temporarily unavailable12-> Cannot allocate memory13-> Permission denied14-> Bad address15-> Block device required16-> Device or resource busy17-> File exists18-> Invalid cross-device link19-> No such device20-> Not a directory21-> Is a directory22-> Invalid argument23-> Too many open files in system24-> Too many open files25-> Inappropriate ioctl for device26-> Text file busy27-> File too large28-> No space left on device29-> Illegal seek30-> Read-only file system31-> Too many links32-> Broken pipe33-> Numerical argument out of domain34-> Numerical result out of range35-> Resource deadlock avoided36-> File name too long37-> No locks available38-> Function not implemented39-> Directory not empty40-> Too many levels of symbolic links41-> Unknown error 4142-> No message of desired type43-> Identifier removed44-> Channel number out of range45-> Level 2 not synchronized46-> Level 3 halted47-> Level 3 reset48-> Link number out of range49-> Protocol driver not attached50-> No CSI structure available51-> Level 2 halted52-> Invalid exchange53-> Invalid request descriptor54-> Exchange full55-> No anode56-> Invalid request code57-> Invalid slot58-> Unknown error 5859-> Bad font file format60-> Device not a stream61-> No data available62-> Timer expired63-> Out of streams resources64-> Machine is not on the network65-> Package not installed66-> Object is remote67-> Link has been severed68-> Advertise error69-> Srmount error70-> Communication error on send71-> Protocol error72-> Multihop attempted73-> RFS specific error74-> Bad message75-> Value too large for defined data type76-> Name not unique on network77-> File descriptor in bad state78-> Remote address changed79-> Can not Access a needed shared library80-> Accessing a corrupted shared library81-> .lib section in a.out corrupted82-> Attempting to link in too many shared libraries83-> Cannot exec a shared library directly84-> Invalid or incomplete multibyte or wide character85-> Interrupted system call should be restarted86-> Streams pipe error87-> Too many users88-> Socket operation on non-socket89-> Destination address required90-> Message too long91-> Protocol wrong type for socket92-> Protocol not available93-> Protocol not supported94-> Socket type not supported95-> Operation not supported96-> Protocol family not supported97-> Address family not supported by protocol98-> Address already in use99-> Cannot assign requested address100-> Network is down101-> Network is unreachable102-> Network dropped connection on reset103-> Software caused connection abort104-> Connection reset by peer105-> No buffer space available106-> Transport endpoint is already connected107-> Transport endpoint is not connected108-> Cannot send after transport endpoint shutdown109-> Too many references: cannot splice110-> Connection timed out111-> Connection refused112-> Host is down113-> No route to host114-> Operation already in progress115-> Operation now in progress116-> Stale file handle117-> Structure needs cleaning118-> Not a XENIX named type file119-> No XENIX semaphores available120-> Is a named type file121-> Remote I/O error122-> Disk quota exceeded123-> No medium found124-> Wrong medium type125-> Operation canceled126-> Required key not available127-> Key has expired128-> Key has been revoked129-> Key was rejected by service130-> Owner died131-> State not recoverable132-> Operation not possible due to RF-kill133-> Memory page has hardware error134-> Unknown error 134

錯(cuò)誤碼的應(yīng)用:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    FILE *fp = fopen("invalid.txt", "r");//以只讀方式打開不存在的文件會(huì)出錯(cuò)    if (fp == NULL)    {        // 使用 strerror 獲取錯(cuò)誤描述        printf("%d->%sn", errno,strerror(errno));                    return 1; //退出碼設(shè)為1    }    return 0;}

輸出:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

2->No such file or directory

使用錯(cuò)誤碼和對(duì)應(yīng)的錯(cuò)誤信息可以幫助程序員快速定位錯(cuò)誤模塊,調(diào)試程序,掌握錯(cuò)誤碼的使用與調(diào)試技巧,是提升 Linux 編程效率和系統(tǒng)可靠性的關(guān)鍵。


_exit函數(shù)和exit函數(shù)

_exit函數(shù)

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

#include <unistd.h>void _exit(int status);

參數(shù) status:進(jìn)程的退出狀態(tài)碼,范圍是 0~255。父進(jìn)程可以通過 wait() 或 waitpid() 獲取該狀態(tài)碼。返回值:無(進(jìn)程直接終止,不會(huì)返回調(diào)用者)。

當(dāng)前進(jìn)程調(diào)用 _exit() 后,操作系統(tǒng)會(huì)立即介入,會(huì)從用戶態(tài)陷入內(nèi)核態(tài),執(zhí)行以下操作:

關(guān)閉所有文件描述符:內(nèi)核會(huì)關(guān)閉進(jìn)程打開的文件、套接字、管道等資源,但不會(huì)刷新標(biāo)準(zhǔn) I/O 庫(如 stdio)的緩沖區(qū)。釋放用戶空間內(nèi)存:回收進(jìn)程的代碼段、數(shù)據(jù)段、等內(nèi)存資源。發(fā)送 SIGCHLD 信號(hào): 通知父進(jìn)程子進(jìn)程已終止,并傳遞退出狀態(tài)碼 status。終止進(jìn)程:進(jìn)程的狀態(tài)變?yōu)?ZOMBIE(僵尸進(jìn)程),直到父進(jìn)程通過 wait() 回收其資源。

本質(zhì)上,_exit() 最終會(huì)調(diào)用 Linux 內(nèi)核的 exit_group 系統(tǒng)調(diào)用(sys_exit_group),終止整個(gè)進(jìn)程及其所有線程。其內(nèi)核處理流程如下:

釋放進(jìn)程資源:

關(guān)閉所有文件描述符。釋放內(nèi)存映射(mmap)和虛擬內(nèi)存區(qū)域。解除信號(hào)處理程序綁定。

更新進(jìn)程狀態(tài):

將進(jìn)程狀態(tài)設(shè)為 TASK_DEAD向父進(jìn)程發(fā)送 SIGCHLD 信號(hào)。

調(diào)度器介入:

從運(yùn)行隊(duì)列中移除進(jìn)程。切換到下一個(gè)進(jìn)程執(zhí)行。


exit函數(shù)

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

#include <stdlib.h>void exit(int status);  // C #include <cstdlib>void exit(int status);  // c++ 

參數(shù) status:進(jìn)程的退出狀態(tài)碼,范圍 0~255(0 通常表示成功,非零表示異常)。返回值:無(進(jìn)程終止,不會(huì)返回調(diào)用者)。

進(jìn)程調(diào)用 exit 時(shí),按以下順序執(zhí)行操作:

調(diào)用 atexit 注冊(cè)的函數(shù):按注冊(cè)的逆序執(zhí)行所有通過 atexit 或 at_quick_exit(若使用quick_exit)注冊(cè)的函數(shù)。刷新所有標(biāo)準(zhǔn) I/O 緩沖區(qū):清空 stdout、stderr 等流的緩沖區(qū)。 注意: stderr 默認(rèn)無緩沖,stdout 在交互式設(shè)備上是行緩沖。關(guān)閉所有打開的文件流:調(diào)用 fclose 關(guān)閉所有通過 fopen 打開的文件。 注意:不會(huì)關(guān)閉底層文件描述符(需手動(dòng) close)。刪除臨時(shí)文件:刪除由 tmpfile 創(chuàng)建的臨時(shí)文件。終止進(jìn)程:向操作系統(tǒng)返回狀態(tài)碼 status。父進(jìn)程可通過 wait 或 waitpid 獲取該狀態(tài)碼。


其實(shí)本質(zhì)上,exit 是一個(gè)標(biāo)準(zhǔn)庫函數(shù),最后也會(huì)調(diào)用_exit,但是在這之前,exit還做了其他的清理工作:

【Linux 進(jìn)程控制】—— 進(jìn)程亦生生不息:起于鴻蒙,守若空谷,歸于太虛

我們舉個(gè)例子,幫大家直觀的感受一下這兩者的區(qū)別

示例一:使用 exit 函數(shù)

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    printf("hello");    exit(0);}

輸出:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

[root@localhost linux]# ./a.outhello[root@localhost linux]#

示例二:使用 _exit 函數(shù)

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    printf("hello");    _exit(0);}

輸出:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

[root@localhost linux]# ./a.out[root@localhost linux]#

return 退出

狀態(tài)碼傳遞:

main函數(shù)中的 return 語句返回一個(gè)整數(shù)值(通常稱為退出狀態(tài)碼),表示程序的執(zhí)行結(jié)果:

0:表示程序成功執(zhí)行。非0:表示程序異常終止(具體數(shù)值由程序員定義)。

return與exit()的關(guān)系

隱式調(diào)用exit():

在 main 函數(shù)中使用 return 時(shí),C/C++運(yùn)行時(shí)會(huì)自動(dòng)調(diào)用 exit() 函數(shù),并將返回值作為參數(shù)傳遞給它。代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

int main(){    return 42;  // 等價(jià)于 exit(42);}

return的執(zhí)行流程

當(dāng)在main函數(shù)中執(zhí)行return時(shí),程序會(huì)做以下幾件事:

返回值傳遞:將返回值傳遞給運(yùn)行時(shí)環(huán)境。

清理操作:

調(diào)用局部對(duì)象析構(gòu)函數(shù)(按照創(chuàng)建順序的逆序)。調(diào)用全局對(duì)象的析構(gòu)函數(shù)(同樣逆序)。

調(diào)用exit():運(yùn)行時(shí)調(diào)用exit(),執(zhí)行以下操作:

刷新所有I/O緩沖區(qū)(如 std::cout)。關(guān)閉通過 fopen 打開的文件流。執(zhí)行通過 atexit() 注冊(cè)的函數(shù)。

終止進(jìn)程:將控制權(quán)交還給操作系統(tǒng)。

值得注意的一點(diǎn)是:在非main函數(shù)的其他函數(shù)中使用 return 僅退出當(dāng)前函數(shù),返回到調(diào)用者,不會(huì)終止進(jìn)程。


_exit、exit 和 return 對(duì)比

以下是一個(gè)詳細(xì)的表格供大家理解參考

特性

_exit() (系統(tǒng)調(diào)用)

exit() (標(biāo)準(zhǔn)庫函數(shù))

return (在 main 中)

所屬標(biāo)準(zhǔn)

POSIX 系統(tǒng)調(diào)用

C/C++ 標(biāo)準(zhǔn)庫函數(shù)

C/C++ 語言關(guān)鍵字

頭文件

(C)、(C++)

無(語言內(nèi)置)

執(zhí)行流程

立即終止進(jìn)程,不執(zhí)行任何用戶空間清理。

1. 調(diào)用 atexit 注冊(cè)的函數(shù)2. 刷新 I/O 緩沖區(qū)3. 關(guān)閉文件流

1. 調(diào)用 C++ 局部對(duì)象析構(gòu)函數(shù)2. 隱式調(diào)用 exit() 完成后續(xù)清理

清理操作

內(nèi)核自動(dòng)回收進(jìn)程資源(內(nèi)存、文件描述符),不刷新緩沖區(qū)、不調(diào)用析構(gòu)函數(shù)

清理標(biāo)準(zhǔn)庫資源(刷新緩沖區(qū)、關(guān)閉文件流),但不調(diào)用 C++ 局部對(duì)象析構(gòu)函數(shù)

調(diào)用 C++ 局部和全局對(duì)象析構(gòu)函數(shù),并觸發(fā) exit() 的清理邏輯

多線程行為

立即終止所有線程,可能導(dǎo)致資源泄漏

終止整個(gè)進(jìn)程,但可能跳過部分線程資源釋放(如線程局部存儲(chǔ))

同 exit(),但在 C++ 中會(huì)正確析構(gòu)主線程的局部對(duì)象

C++ 析構(gòu)函數(shù)調(diào)用

? 不調(diào)用任何對(duì)象的析構(gòu)函數(shù)(包括全局對(duì)象)

? 不調(diào)用局部對(duì)象析構(gòu)函數(shù)? 調(diào)用全局對(duì)象析構(gòu)函數(shù)(C++)

? 調(diào)用局部和全局對(duì)象析構(gòu)函數(shù)(C++)

緩沖區(qū)處理

? 不刷新 stdio 緩沖區(qū)(如 printf 的輸出可能丟失)

? 刷新所有 stdio 緩沖區(qū)

? 通過隱式調(diào)用 exit() 刷新緩沖區(qū)

適用場(chǎng)景

1. 子進(jìn)程退出(避免重復(fù)刷新緩沖區(qū))2. 需要立即終止進(jìn)程(繞過清理邏輯)

1. 非 main 函數(shù)的程序終止2. 需要執(zhí)行注冊(cè)的清理函數(shù)(如日志收尾)

1. 在 main 函數(shù)中正常退出2. 需要確保 C++ 對(duì)象析構(gòu)(RAII 資源管理)

錯(cuò)誤處理

直接傳遞狀態(tài)碼給操作系統(tǒng),無錯(cuò)誤反饋機(jī)制

可通過 atexit 注冊(cè)錯(cuò)誤處理函數(shù),但無法捕獲局部對(duì)象析構(gòu)異常

可通過 C++ 異常機(jī)制處理錯(cuò)誤(需在 main 中捕獲)

信號(hào)安全

? 可在信號(hào)處理函數(shù)中安全調(diào)用(如 SIGINT)

? 不可在信號(hào)處理函數(shù)中調(diào)用(可能死鎖)

? 不可在信號(hào)處理函數(shù)中使用(僅限 main 函數(shù)流程)

資源泄漏風(fēng)險(xiǎn)

高(臨時(shí)文件、未釋放的手動(dòng)內(nèi)存等需內(nèi)核回收)

中(未關(guān)閉的文件描述符、手動(dòng)內(nèi)存需提前處理)

低(依賴 RAII 自動(dòng)釋放資源)

底層實(shí)現(xiàn)

直接調(diào)用內(nèi)核的 exit_group 系統(tǒng)調(diào)用

調(diào)用 C 標(biāo)準(zhǔn)庫的清理邏輯后,最終調(diào)用 _exit()

編譯器生成代碼調(diào)用析構(gòu)函數(shù),并跳轉(zhuǎn)到 main 結(jié)尾觸發(fā) exit()

最后總結(jié)下:

_exit():最底層的終止方式,適合需要繞過所有用戶空間清理的場(chǎng)景(如子進(jìn)程退出)。exit():平衡安全與效率,適合非 main 函數(shù)的程序終止,但需注意 C++ 對(duì)象析構(gòu)問題。return:C++ 中最安全的退出方式,優(yōu)先在 main 函數(shù)中使用,確保資源自動(dòng)釋放。

以上就是【Linux 進(jìn)程控制】—— 進(jìn)程亦生生不息:起于

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊13 分享