一、概述
1.1 什么是進程?
在 Linux 系統中,進程是程序的一次動態執行過程。程序是靜態的可執行文件,而進程是程序運行時的實例,系統會為其分配內存、CPU 時間片等資源。例如,輸入 ls 命令時,系統創建進程執行 ls 程序來顯示文件列表。進程是資源分配的基本單位,理解進程對掌握 Linux 系統運行機制至關重要。
1.2 查看進程
在 Linux 中,可使用 ps 命令查看系統中當前運行的進程。下面是一些常用的 ps 命令參數組合:
- ps -ef:
-
- 功能:以全格式顯示所有進程的詳細信息。
-
- 步驟:
-
-
- 打開終端。
-
-
-
- 輸入 ps -ef 并回車。
-
-
- 示例輸出:
UID PID PPID C STIME TTY TIME CMDroot 1 0 0 00:00 ? 00:00:01 /sbin/init splashroot 2 0 0 00:00 ? 00:00:00 [kthreadd]
- 參數解釋:
-
- UID:進程所有者的用戶 ID。
-
- PID:進程的 ID 號。
-
- PPID:父進程的 ID 號。
-
- C:CPU 占用率。
-
- STIME:進程啟動時間。
-
- TTY:進程關聯的終端。
-
- TIME:進程使用的 CPU 時間。
-
- CMD:啟動進程的命令。
在 Linux 中,除了 ps -ef,還可使用以下命令查看進程:?
- ps aux:?
- 功能:顯示所有進程的詳細資源使用情況(如內存、CPU 占用率)。?
- 示例輸出:?
TypeScript取消自動換行復制USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND ?root 1 0.0 0.1 24176 4360 ? Ss 00:00 0:01 /sbin/init splash ?
- %CPU:CPU 占用百分比。?
- %MEM:內存占用百分比。?
- STAT:進程狀態(如 S 表示睡眠,R 表示運行)。?
- top 命令:?
- 功能:動態實時顯示進程資源占用情況,類似 Windows 任務管理器。?
- 操作:輸入 top 后,可按 q 退出。
二、進程的創建
2.1 fork 函數
在 C 語言里,fork 函數是創建新進程的關鍵函數。它的作用是復制當前進程,生成一個子進程,原進程則成為父進程。fork 函數的原型如下:
#include <unistd.h>pid_t fork(void);
- 返回值:
-
- 在父進程中,fork 函數返回子進程的 PID(一個正整數)。
-
- 在子進程中,fork 函數返回 0。
-
- 若 fork 失敗,返回 -1。
創建進程的步驟
- 包含必要的頭文件:#include <unistd.h> 和 #include <stdio.h>。
- 調用 fork 函數創建子進程。
- 根據 fork 的返回值判斷當前是父進程還是子進程,并執行相應的代碼。
示例代碼
#include <unistd.h>#include <stdio.h>int main() {pid_t pid;pid = fork();if (pid < 0) {perror("fork 失敗");} else if (pid == 0) {// 子進程printf("我是子進程,我的 PID 是 %d,父進程的 PID 是 %d\n", getpid(), getppid());} else {// 父進程printf("我是父進程,我的 PID 是 %d,子進程的 PID 是 %d\n", getpid(), pid);}return 0;}
編譯和運行步驟
- 把上述代碼保存為 fork_example.c。
- 打開終端,進入代碼所在目錄。
- 使用 gcc 編譯代碼:gcc fork_example.c -o fork_example。
- 運行編譯后的可執行文件:./fork_example。
三、僵尸進程
3.1 形成條件
僵尸進程的形成需要滿足以下三個條件:
- 子進程優先于父進程結束。
- 父進程不結束。
- 父進程不調用 wait 函數。
當子進程結束時,它會向父進程發送一個 SIGCHLD 信號,但如果父進程沒有調用 wait 或 waitpid 函數來回收子進程的資源,子進程就會變成僵尸進程。
3.2 如何避免僵尸進程
方法一:父進程調用 wait 函數
wait 函數的作用是等待任意一個子進程結束,并回收其資源。其原型如下:
#include <sys/types.h>#include <sys/wait.h>pid_t wait(int *status);
- 參數:status 用于存儲子進程的退出狀態。
- 返回值:返回結束的子進程的 PID。
示例代碼
#include <unistd.h>#include <stdio.h>#include <sys/types.h>#include <sys/wait.h>int main() {pid_t pid;pid = fork();if (pid < 0) {perror("fork 失敗");} else if (pid == 0) {// 子進程printf("子進程開始執行,PID 是 %d\n", getpid());sleep(2);printf("子進程結束\n");} else {// 父進程int status;pid_t child_pid = wait(&status);printf("父進程回收了 PID 為 %d 的子進程\n", child_pid);}return 0;}
方法二:使用 signal 函數處理 SIGCHLD 信號
可通過 signal 函數捕獲 SIGCHLD 信號,并在信號處理函數中調用 wait 或 waitpid 函數。
示例代碼
#include <unistd.h>#include <stdio.h>#include <sys/types.h>#include <sys/wait.h>#include <signal.h>void sigchld_handler(int signo) {pid_t pid;int status;while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {printf("回收了 PID 為 %d 的子進程\n", pid);}}int main() {signal(SIGCHLD, sigchld_handler);pid_t pid;pid = fork();if (pid < 0) {perror("fork 失敗");} else if (pid == 0) {// 子進程printf("子進程開始執行,PID 是 %d\n", getpid());sleep(2);printf("子進程結束\n");} else {// 父進程printf("父進程繼續執行\n");sleep(5);}return 0;}
四、孤兒進程
4.1 形成條件
孤兒進程的形成需要滿足以下兩個條件:
- 父進程優先于子進程結束。
- 子進程未結束。
當父進程結束后,子進程就會變成孤兒進程,此時它會被進程 ID 為 1 的 init 進程接管。
4.2 被進程 ID 為 1 的進程接管
init 進程會負責回收孤兒進程的資源,確保系統資源不會被浪費。
示例代碼
#include <unistd.h>#include <stdio.h>int main() {pid_t pid;pid = fork();if (pid < 0) {perror("fork 失敗");} else if (pid == 0) {// 子進程printf("子進程開始執行,父進程 PID 是 %d\n", getppid());sleep(5);printf("子進程繼續執行,父進程 PID 是 %d\n", getppid());} else {// 父進程printf("父進程結束\n");}return 0;}
在這個示例中,父進程會先結束,子進程在睡眠 5 秒后,會發現自己的父進程 ID 變成了 1。
五、守護進程(后臺進程)
5.1 實現過程
守護進程是一種在后臺持續運行的進程,通常在系統啟動時就開始運行,并且不受用戶登錄和注銷的影響。以下是創建守護進程的詳細步驟:
步驟 1:創建子進程,父進程退出
#include <unistd.h>#include <stdio.h>#include <stdlib.h>int main() {pid_t pid;pid = fork();if (pid < 0) {perror("fork 失敗");exit(EXIT_FAILURE);}if (pid > 0) {// 父進程退出exit(EXIT_SUCCESS);}// 子進程繼續執行// 后續步驟...return 0;}
步驟 2:在子進程中創建新會話
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>int main() {pid_t pid;pid = fork();if (pid < 0) {perror("fork 失敗");exit(EXIT_FAILURE);}if (pid > 0) {// 父進程退出exit(EXIT_SUCCESS);}// 子進程創建新會話pid_t sid = setsid();if (sid < 0) {perror("setsid 失敗");exit(EXIT_FAILURE);}// 后續步驟...return 0;}
setsid 函數的作用是創建一個新的會話,使子進程成為新會話的首進程,并且脫離原有的控制終端。
步驟 3:改變工作目錄
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>int main() {pid_t pid;pid = fork();if (pid < 0) {perror("fork 失敗");exit(EXIT_FAILURE);}if (pid > 0) {// 父進程退出exit(EXIT_SUCCESS);}// 子進程創建新會話pid_t sid = setsid();if (sid < 0) {perror("setsid 失敗");exit(EXIT_FAILURE);}// 改變工作目錄if (chdir("/") < 0) {perror("chdir 失敗");exit(EXIT_FAILURE);}// 后續步驟...return 0;}
chdir 函數用于將工作目錄切換到根目錄,避免工作目錄被卸載導致進程無法正常工作。
步驟 4:設置文件權限掩碼
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>int main() {pid_t pid;pid = fork();if (pid < 0) {perror("fork 失敗");exit(EXIT_FAILURE);}if (pid > 0) {// 父進程退出exit(EXIT_SUCCESS);}// 子進程創建新會話pid_t sid = setsid();if (sid < 0) {perror("setsid 失敗");exit(EXIT_FAILURE);}// 改變工作目錄if (chdir("/") < 0) {perror("chdir 失敗");exit(EXIT_FAILURE);}// 設置文件權限掩碼umask(0);// 后續步驟...return 0;}
umask 函數用于設置文件權限掩碼,確保守護進程創建的文件具有預期的權限。
步驟 5:關閉不需要的文件描述符
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>int main() {pid_t pid;pid = fork();if (pid < 0) {perror("fork 失敗");exit(EXIT_FAILURE);}if (pid > 0) {// 父進程退出exit(EXIT_SUCCESS);}// 子進程創建新會話pid_t sid = setsid();if (sid < 0) {perror("setsid 失敗");exit(EXIT_FAILURE);}// 改變工作目錄if (chdir("/") < 0) {perror("chdir 失敗");exit(EXIT_FAILURE);}// 設置文件權限掩碼umask(0);// 關閉不需要的文件描述符close(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO);// 守護進程的主循環while (1) {// 執行守護進程的任務sleep(1);}return 0;}
關閉標準輸入、標準輸出和標準錯誤輸出的文件描述符,防止守護進程與控制終端交互。
編譯和運行步驟
- 把上述代碼保存為 daemon_example.c。
- 打開終端,進入代碼所在目錄。
- 使用 gcc 編譯代碼:gcc daemon_example.c -o daemon_example。
- 運行編譯后的可執行文件:./daemon_example。此時,守護進程會在后臺持續運行。
通過以上步驟,你可以逐步掌握 Linux 多進程的相關知識,包括進程的創建、僵尸進程和孤兒進程的處理,以及守護進程的實現。在實際應用中,多進程編程可以提高程序的并發性能,充分利用多核 CPU 的資源。