文章目錄
- Linux | 進程控制 — 進程終止 & 進程等待
- 1、進程終止
- 進程常見退出方法
- 1.1退出碼
- 基本概念
- 獲取退出碼的方式
- 常見退出碼約定
- 使用場景
- 1.2 strerror函數 & errno宏
- 1.3 _exit函數
- 1.4_exit和exit的區別
- 1.4.1 所屬頭文件與函數原型
- 1.4.2 執行過程差異
- **結合現象分析**:
- 2、進程等待
- 2.1 進程等待的作用
- 2.2 僵尸進程(Zombie Process)
- 2.3 `wait()` 系統調用
- 2.4 `waitpid()` 系統調用
- 2.5 示例代碼
- 3、進程程序替換
- 3.1 `exec` 系列函數
- 3.1.1 `execl()`
- 3.1.2 `execlp()`
- 3.1.3 `execle()`
- 3.1.4 `execv()`
- 3.1.5 `execvp()`
- 3.1.6 `execvpe()`
- 3.2 `exec` 系列函數的特點
- 3.3 示例代碼
Linux | 進程控制 — 進程終止 & 進程等待
1、進程終止
進程常見退出方法
進程退出場景
- 代碼運行完畢,結果正確
- 代碼運行完畢,結果不正確
- 代碼異常終止
正常終止(可以通過echo $?
查看進程退出碼)
1.從
main
返回2.調用exit
3._exit
異常退出:
- ctrl + c,信號終止
1.1退出碼
在 Linux 系統中,進程的退出碼(也稱為返回值)是進程結束時返回給其父進程或系統的值,用于表示進程執行的結果。下面從基本概念、獲取方式、常見約定、使用場景等方面詳細講解。
基本概念
- 進程的退出碼是一個整數值,范圍通常是 0 - 255。在 C 語言編寫的程序中,通常通過
main
函數的return
語句或者exit()
函數來設置退出碼。例如:
#include <stdio.h>
#include <stdlib.h>int main() {// 使用 return 語句設置退出碼return 0; // 或者使用 exit() 函數// exit(0);
}
獲取退出碼的方式
- 父進程獲取子進程退出碼:父進程可以使用
wait()
或waitpid()
等系統調用獲取子進程的退出狀態信息,然后通過一些宏來提取退出碼。示例如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid == 0) {// 子進程return 42;} else if (pid > 0) {// 父進程int status;waitpid(pid, &status, 0);if (WIFEXITED(status)) {int exit_code = WEXITSTATUS(status);printf("子進程退出碼: %d\n", exit_code);}}return 0;
}
- shell 中獲取進程退出碼:在 shell 里,可以使用
$?
變量獲取上一個執行命令的退出碼。例如:
./my_program
echo $?
常見退出碼約定
雖然 Linux 沒有強制統一所有程序使用特定的退出碼,但存在一些被廣泛遵循的約定:
- 退出碼 0:表示進程正常結束,任務成功完成。這是最常見的退出碼,表示程序按預期執行完畢。
- 退出碼 1:一般代表通用的錯誤,當程序遇到一些未明確分類的錯誤時,常返回這個退出碼。
- 退出碼 2:通常意味著程序在使用命令行參數時出現了錯誤,例如參數數量不對、參數格式錯誤等。
- 退出碼 126:表示命令雖然找到了,但由于權限問題或其他原因無法執行。
- 退出碼 127:表明命令未找到,可能是因為命令拼寫錯誤或者該命令不在系統的搜索路徑中。
- 退出碼 128 + N:其中
N
是信號編號。當進程因接收到信號而終止時,退出碼通常是128 + 信號編號
。例如,進程因接收到SIGTERM
(信號編號 15)而終止,退出碼就是 143(128 + 15)。
使用場景
- 錯誤處理與調試*:開發人員可以根據退出碼快速定位程序出現問題的大致原因。例如,如果程序返回退出碼 2,就可以先檢查命令行參數的處理邏輯。
- 腳本流程控制:在 shell 腳本中,根據命令的退出碼決定后續的操作。比如,如果某個依賴程序執行失敗(返回非零退出碼),腳本可以選擇終止執行或者嘗試其他替代方案。
./dependency_program
if [ $? -ne 0 ]; thenecho "依賴程序執行失敗,腳本終止"exit 1
fi
- 系統監控:系統監控工具可以根據進程的退出碼判斷進程是否正常運行。如果進程頻繁以非零退出碼結束,可能表示系統存在潛在問題,需要進一步排查。
1.2 strerror函數 & errno宏
strerror
頭文件:#include<string.h>
返回值:指向描述error errnum
的錯誤字符串的指針,簡單來說可以將退出碼和對應的錯誤對應上。
舉例:
#include<string.h>
int main()
{for(int i=0;i<10;i++){printf("%d: %s\n",strerror(i));}return 0;
}
效果如下,后面輸出的就是退出碼對應的錯誤描述:
errno
頭文件:#include<errno.h>
簡單的說,errno會返回最后的一次錯誤碼,使用errno可以獲得退出碼,通過返回退出碼,在多進程中也可以讓父進程知道子進程的狀況。
注意:但是當進程異常退出的時候,本質可能就是代碼沒有跑完,那么進程的退出碼就無意義了,所以應該要先看進程退出的時候,如果要關心進程的推出情況,要先關心退出時后有沒有出異常,如果沒有異常,再看結果是否正確,然后關心退出碼。
- 父進程關心子進程的退出,只需要確定:
- 父進程是否收到來自子進程的信號,若沒有,說明沒有異常,代碼正常跑完
- 查看退出結果:0表示成功,非0表示錯誤,對應各自的原因
舉例:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>int main()
{int ret = 0;char *p = (char*)malloc(1000*1000*1000*4);if(p==NULL){printf("malloc error, %d: %s\n".errno,strerror(errno));ret = errno;} else{printf("malloc success\n");} return ret;
}
1.3 _exit函數
頭文件:#include<unistd.h>
函數格式:void _exit(int status);
exit函數最后也會調用_exit,但是在調用exit之前,還會做以下工作:
1、執行用戶通過
atexit
或on_exit
定義的清理函數2、關閉所有打開的流,所有的緩存數據均被寫入
3、調用_exit
例1:
int main()
{printf("hello linux\n");exit(12);
}
或者:
int main()
{printf("hello linux\n");return 12;
}
編譯執行完后再使用echo $?
查詢退出碼,效果均如下:
區別在于:exit在任意地方被調用,都表示調用進程直接退出,如果調用的是return,只表示當前函數返回,原進程繼續運行,如果調用一個含exit或者return的函數,就可以明顯觀察到。
1.4_exit和exit的區別
在 Linux 系統中,exit
和 _exit
都用于終止進程,但它們在功能實現、調用過程以及使用場景等方面存在明顯區別,下面為你詳細介紹:
1.4.1 所屬頭文件與函數原型
exit
:它是標準 C 庫中的函數,其原型定義在<stdlib.h>
頭文件中,函數原型為void exit(int status);
。這里的status
是進程的退出狀態碼,通常 0 表示正常退出,非零表示異常退出。_exit
:這是一個系統調用,其原型定義在<unistd.h>
頭文件中,函數原型為void _exit(int status);
,status
的含義與exit
中的相同。
1.4.2 執行過程差異
-
exit
:- 在調用
exit
時,它會先執行一些清理工作。首先,會調用所有通過atexit
函數注冊的清理函數,這些函數可以用于釋放資源、關閉文件描述符等操作。 - 接著,會刷新所有打開的標準 I/O 流緩沖區,將緩沖區中的數據寫入對應的文件或設備。
- 最后,調用
_exit
系統調用來真正終止進程,并將status
作為退出狀態返回給父進程。
- 在調用
-
_exit
:_exit
是一個底層的系統調用,它會直接終止進程,不會執行任何清理工作。也就是說,它不會調用atexit
注冊的函數,也不會刷新標準 I/O 流緩沖區。
結合現象分析:
int main()
{printf("hello world");sleep(1);//使用sleep能夠觀察到一些現象,下文會提及exit(11);
}
運行完畢后再調用echo $?
查看退出碼,效果如下:
但是將exit
改為_exit
后:
int main()
{printf("hello world");sleep(1);_exit(11);
}
運行完畢后再調用echo $?
查看退出碼,效果如下:
原因:
當代碼中輸出的內容以\n結尾時,當代碼運行到printf這條語句時,程序會直接輸出內容,但是如果沒有以\n結尾,那么就會先將內容存到緩沖區中,當程序結束前會沖刷緩沖,關閉流,然后就有打印輸出的效果,也正因此會發現運行的時候是先等待了一秒鐘,輸出句子后程序馬上結束,而不是先輸出句子,等待一秒鐘再結束程序。
結合下圖
- 調用
exit()
后會先執行用戶定義的清理函數,再沖刷緩沖,關閉流等,因此會有打印字符串的效果,最后再調用_exit系統調用_exit()
是一個系統調用接口,調用_exit()
后,其會在操作系統內部直接終止進程,對應緩沖區的數據不做刷新。
2、進程等待
2.1 進程等待的作用
- 之前在Linux | 進程狀態一文中有提及過僵尸進程的問題,如果子進程退出,父進程沒有反應,可能造成僵尸進程的問題,導致內存泄漏
- 當進程一旦變成僵尸狀態,即使使用Kill -9也無法結束進程,需要通過進程等待來結束它,進而解決內存泄漏的問題
- 需要通過進程等待,獲得子進程的退出情況和父進程給子進程分配的任務完成的情況,例如子進程執行程序完畢后結果是否正確,或者是否正常退出
- 父進程通過進程等待的方式,回收子進程資源,獲取子進程退出信息。
在Linux中,進程等待是指一個進程(通常是父進程)等待其子進程終止并獲取其退出狀態。這是通過系統調用 wait()
或 waitpid()
來實現的。進程等待的主要目的是防止子進程成為“僵尸進程”(Zombie Process),并確保父進程能夠獲取子進程的退出狀態。
2.2 僵尸進程(Zombie Process)
當一個子進程終止時,它的退出狀態需要被父進程讀取。如果父進程沒有讀取子進程的退出狀態,子進程的進程描述符仍然保留在系統中,這種進程稱為“僵尸進程”。僵尸進程不占用CPU資源,但會占用進程表中的條目,如果系統中存在大量僵尸進程,可能會導致進程表耗盡,無法創建新的進程。
2.3 wait()
系統調用
wait()
系統調用會使父進程阻塞,直到它的任意一個子進程終止。如果已經有子進程終止,wait()
會立即返回。
#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int *status);
-
參數:
status
: 一個指向整數的指針,用于存儲子進程的退出狀態。可以通過宏(如WIFEXITED(status)
、WEXITSTATUS(status)
等)來解析這個狀態。
-
返回值:
- 成功時返回終止的子進程的PID。
- 如果沒有子進程,返回-1,并設置
errno
為ECHILD
。
2.4 waitpid()
系統調用
waitpid()
提供了比 wait()
更靈活的控制,允許父進程等待特定的子進程,并且可以指定是否阻塞。
#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);
-
參數:
pid
: 指定要等待的子進程的PID。pid > 0
: 等待進程ID等于pid
的子進程。pid = -1
: 等待任意子進程,與wait()
類似。pid = 0
: 等待與調用進程屬于同一個進程組的任意子進程。pid < -1
: 等待進程組ID等于pid
絕對值的任意子進程。
status
: 與wait()
中的status
參數相同,用于存儲子進程的退出狀態。options
: 控制waitpid()
的行為,常用的選項有:WNOHANG
: 如果沒有子進程退出,立即返回,不阻塞。WUNTRACED
: 如果子進程被暫停(例如通過SIGSTOP
信號),也返回。
-
返回值:
- 成功時返回終止的子進程的PID。
- 如果指定了
WNOHANG
且沒有子進程退出,返回0。 - 如果出錯,返回-1,并設置
errno
。
2.5 示例代碼
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed");exit(1);} else if (pid == 0) {// 子進程printf("Child process is running\n");sleep(2);printf("Child process is exiting\n");exit(42);} else {// 父進程int status;printf("Parent process is waiting for child\n");wait(&status);if (WIFEXITED(status)) {printf("Child exited with status %d\n", WEXITSTATUS(status));} else {printf("Child did not exit normally\n");}}return 0;
}
3、進程程序替換
在Linux中,進程程序替換是指將一個進程當前執行的程序替換為另一個全新的程序。這個過程是通過 exec
系列函數來實現的。進程程序替換后,進程的PID、父進程、文件描述符等信息保持不變,但進程的代碼段、數據段、堆棧等會被新程序的內容替換。
exec
系列函數是Linux系統調用的一部分,它們的作用是加載并執行一個新的程序,替換當前進程的地址空間。執行成功后,原程序的代碼將不再運行,而是由新程序從頭開始執行。
3.1 exec
系列函數
exec
系列函數有多個變體,它們的核心功能相同,但在參數傳遞方式和行為上略有不同。exec系列函數只有失敗返回值(-1),沒有成功返回值
以下是常用的 exec
函數:
3.1.1 execl()
int execl(const char *path, const char *arg, ..., (char *) NULL);
-
功能: 加載并執行指定路徑的程序。
-
參數:
path
: 要執行的程序的完整路徑。arg
: 程序的命令行參數,第一個參數通常是程序名,最后一個參數必須是NULL
。
-
示例:
execl("/bin/ls", "ls", "-l", NULL);
這行代碼會執行
/bin/ls
程序,并傳遞-l
參數。
3.1.2 execlp()
int execlp(const char *file, const char *arg, ..., (char *) NULL);
- 功能: 類似于
execl()
,但會在PATH
環境變量中查找可執行文件。 - 參數:
file
: 要執行的程序名(不需要完整路徑)。arg
: 程序的命令行參數,最后一個參數必須是NULL
。
- 示例:
這行代碼會在execlp("ls", "ls", "-l", NULL);
PATH
中查找ls
并執行。
3.1.3 execle()
int execle(const char *path, const char *arg, ..., (char *) NULL, char *const envp[]);
- 功能: 加載并執行指定路徑的程序,并允許指定環境變量。
- 參數:
path
: 要執行的程序的完整路徑。arg
: 程序的命令行參數,最后一個參數必須是NULL
。envp
: 自定義的環境變量數組,必須以NULL
結尾。
- 示例:
char *envp[] = {"USER=test", "PATH=/bin", NULL}; execle("/bin/ls", "ls", "-l", NULL, envp);
3.1.4 execv()
int execv(const char *path, char *const argv[]);
- 功能: 加載并執行指定路徑的程序,參數通過數組傳遞。
- 參數:
path
: 要執行的程序的完整路徑。argv
: 命令行參數數組,必須以NULL
結尾。
- 示例:
char *argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv);
3.1.5 execvp()
int execvp(const char *file, char *const argv[]);
- 功能: 類似于
execv()
,但會在PATH
環境變量中查找可執行文件。 - 參數:
file
: 要執行的程序名(不需要完整路徑)。argv
: 命令行參數數組,必須以NULL
結尾。
- 示例:
char *argv[] = {"ls", "-l", NULL}; execvp("ls", argv);
3.1.6 execvpe()
int execvpe(const char *file, char *const argv[], char *const envp[]);
- 功能: 類似于
execvp()
,但允許指定環境變量。 - 參數:
file
: 要執行的程序名(不需要完整路徑)。argv
: 命令行參數數組,必須以NULL
結尾。envp
: 自定義的環境變量數組,必須以NULL
結尾。
- 示例:
char *argv[] = {"ls", "-l", NULL}; char *envp[] = {"USER=test", "PATH=/bin", NULL}; execvpe("ls", argv, envp);
3.2 exec
系列函數的特點
-
替換當前進程:
exec
系列函數會用新程序替換當前進程的地址空間,包括代碼段、數據段、堆棧等。- 進程的PID、父進程、文件描述符等信息保持不變。
-
不創建新進程:
exec
不會創建新進程,它只是替換當前進程的內容。
-
成功時不返回:
- 如果
exec
執行成功,它不會返回,因為原程序的代碼已經被替換。 - 如果
exec
失敗,它會返回-1
,并設置errno
。
- 如果
-
文件描述符的繼承:
- 默認情況下,
exec
會保留進程打開的文件描述符(除非顯式設置FD_CLOEXEC
標志)。
- 默認情況下,
3.3 示例代碼
- 實例1:
? 以下是一個完整的示例,展示如何使用 fork()
和 exec()
創建子進程并替換程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed");exit(1);} else if (pid == 0) // 子進程{printf("子進程正在執行\n");// 替換為 ls 程序char *argv[] = {"ls", "-l", NULL};execvp("ls", argv);//如果進程替換成功,則下面的代碼不會執行,如果進程替換失敗(例如命令錯誤、路徑錯誤等原因導致錯誤),則會執行下面的語句perror("hello world\n");exit(1);} else // 父進程{int status;wait(&status); // 等待子進程結束printf("父進程檢測到子進程退出\n");}return 0;
}
程序運行結果:
-
實例2:
test1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sys/types.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed");exit(1);} else if (pid == 0) // 子進程{ printf("子進程正在執行\n");// 替換為 ls 程序//char *argv[] = {"ls", "-l", NULL};//execvp("ls", argv);//execl("/usr/bin/ls","/usr/bin/ls","-ln","-a",NULL);execl("./test2","test2",NULL);//如果進程替換成功,則下面的代碼不會執行perror("hello world\n");exit(1);} else // 父進程{ int status;wait(&status); // 等待子進程結束printf("父進程檢測到子進程退出\n");} return 0;
}
test2.c
#include<stdio.h>int main(){printf("這是test2\n");return 0;
}
將test1.c編譯成可執行文件后,執行結果如下: