一、進程基本概念
1、進程與程序的區別
- 程序:靜態的可執行文件(如電腦中的vs2022安裝程序)。
- 進程:程序的動態執行過程(如啟動后的vs2022實例),是操作系統分配資源的單位(如 CPU 時間、內存)。
- 特點:同一程序可啟動多個進程(如多個 vs2022?窗口),進程關閉后程序仍存在。
2、進程的組成
- 進程控制塊(PCB):操作系統為每個進程創建的唯一標識,包含進程狀態、資源信息等。
- 程序段(代碼):進程執行的代碼邏輯。
- 數據集(數據):進程操作的數據。
二、進程控制塊(PCB)
1、核心字段
- 進程號(PID):32 位無符號整數(Linux 最大為 32767),通過
getpid()
獲取當前進程號。 - 進程狀態:
- R:可執行狀態。
- S:可中斷睡眠。
- D:不可中斷睡眠。
- T:暫停或跟蹤狀態。
- Z:僵尸進程(已退出但未釋放資源)。
- X:即將銷毀的進程。
- 查看命令:
ps -eo stat,pid,user,cmd
(顯示狀態、PID、用戶、命令)。
2、其他字段
- 優先級:決定 CPU 調度順序。
- CPU 現場信息:保存進程暫停時的 CPU 狀態,以便恢復。
- 資源清單:內存、I/O 設備等分配情況。
- 隊列指針:鏈接同一狀態的進程(如就緒隊列、等待隊列)。
三、進程 PID 文件
1、存儲位置:
- 位于
/var/run
目錄,文件名通常為進程名.pid
,內容為單行的進程號。 - 注意:需程序自行創建,系統不會自動生成。
2、作用:
- 防止程序重復啟動(通過文件鎖機制實現)。
- 示例代碼:通過
fcntl
加鎖判斷進程是否已運行。#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/file.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h>// PID文件路徑,用于存儲當前運行實例的進程ID #define PID_FILE "/var/run/test.pid"/*** 檢查程序是否已在運行,確保只有一個實例** 返回值:* 0 - 程序未運行,當前是第一個實例* -1 - 程序已在運行,當前實例退出*/ int checkAlone(void) {int fd; // 文件描述符char buf[16]; // 存儲PID的緩沖區// 打開或創建PID文件,使用O_CREAT標志確保文件存在fd = open(PID_FILE, O_RDWR | O_CREAT, 0666);if (fd < 0) {perror("open pid failed"); // 打印錯誤信息exit(1); // 打開失敗時終止程序}// 定義文件鎖結構,準備加寫鎖struct flock fl;fl.l_type = F_WRLCK; // 寫鎖類型fl.l_start = 0; // 從文件開始位置fl.l_whence = SEEK_SET; // 以文件起始為基準fl.l_len = 0; // 鎖定整個文件// 嘗試加非阻塞寫鎖// 返回值:0-成功,-1-失敗(文件已被鎖定)int ret = fcntl(fd, F_SETLK, &fl);if (ret < 0) {close(fd); // 關閉文件描述符printf("Had run.\n"); // 提示已有實例在運行return -1; // 返回錯誤碼}// 將當前進程ID寫入PID文件sprintf(buf, "%ld", (long)getpid());write(fd, buf, strlen(buf) + 1);printf("first running.\n"); // 提示首次運行return 0; // 返回成功 }int main(void) {// 檢查程序是否已在運行if (checkAlone() < 0) {return -1; // 已有實例在運行,退出當前進程}// 主程序邏輯:循環執行任務while (1) {printf("working...\n"); // 輸出工作狀態sleep(1); // 休眠1秒}return 0; }
?3、開啟兩個終端:
第一次運行程序(第一個終端):
第二次運行程序(第二個終端) :
四、進程的創建
1、fork () 函數
- 功能:創建子進程,返回兩次(父進程返回子進程 PID,子進程返回 0)。
- 特點:子進程復制父進程的內存空間(不共享內存),繼承打開的文件描述符等資源。
- 示例1:
#include <stdio.h> #include <stdlib.h> #include <unistd.h>int main() {int count = 0; // 用于父子進程各自計數的變量printf("準備創建子進程...\n");// 創建子進程:fork調用會返回兩次// 父進程返回子進程的PID(正值)// 子進程返回0// 返回負值表示創建失敗pid_t pid = fork();// 錯誤處理:創建子進程失敗if (pid < 0) {printf("創建子進程失敗");exit(1); // 終止程序并返回錯誤碼1}// 子進程執行分支else if (pid == 0) {// 子進程中fork返回0// getpid()返回子進程自身的PIDprintf("我是子進程,pid=%d, 進程號=%d\n", pid, getpid());count++; // 子進程的count加1}// 父進程執行分支else {// 父進程中fork返回子進程的PID// getpid()返回父進程自身的PIDprintf("我是父進程, pid=%d, 進程號=%d\n", pid, getpid());count++; // 父進程的count加1}// 父子進程都會執行此語句// 通過判斷pid值區分當前是哪個進程printf("我是%s, count=%d\n", pid == 0 ? "子進程" : "父進程", count);return 0; }
說明1:
由 fork 創建的新進程被稱為子進程,原來的進程,稱為“父進程”
該函數被調用一次,但返回兩次(在父進程中返回 1 次,子進程中返回 1 次)
1)子進程的返回值是 0
2)而父進程的返回值是子進程的 PID
fork 執行完之后,子進程和父進程繼續執行 fork 之后的指令。
父進程和子進程幾乎是等同的,它們具有相同的變量值(但變量內存并不共享),
打開的文件也都相同,還有其他一些相同屬性。
如果父進程改變了變量的值,子進程將不會看到這個變化。
實際上, 子進程是父進程的一個復制(拷貝),但它們并不共享內存。
父進程改變了變量的值,子進程中對應的變量不會有任何影響。 -
示例2:
#include <stdio.h> #include <stdlib.h> #include <unistd.h>int main() {int count = 0; // 父子進程各自的計數器printf("準備創建子進程...\n");pid_t pid;// 循環兩次創建子進程,每次fork會產生父子兩個分支for (int i = 0; i < 2; i++) {pid = fork(); // 關鍵系統調用:創建新進程if (pid < 0) {perror("fork失敗"); // 輸出系統錯誤信息exit(1); // 異常退出}else if (pid == 0) {// 子進程分支:pid為0,getpid()返回子進程IDprintf("[新的子進程]我是子進程,pid=%d, 進程號=%d\n", pid,getpid());count++; // 子進程計數器加1}else {// 父進程分支:pid為子進程ID,getpid()返回父進程IDprintf("我是父進程, pid=%d, 進程號=%d\n", pid, getpid());count++; // 父進程計數器加1}}// 父子進程最終都會執行此語句// 通過最后一次fork的返回值判斷當前是父進程還是子進程printf("我是%s, count=%d\n", pid == 0 ? "子進程" : "父進程", count);return 0; }
for 循環了 2 次,實際上創建了 3 個子進程,而不是兩個。
2、exec 系列函數
- 功能:用指定程序替換當前進程(成功后原進程代碼不再執行)。
- 接口差異:
l
:參數以列表形式傳遞(如execl
)。p
:從 PATH 環境變量查找程序(如execlp
)。v
:參數通過指針數組傳遞(如execv
)。e
:傳遞自定義環境變量(如execle
)。
- 示例:
無參的:
?#include <cstdio> #include <unistd.h>int main(int argc, char* argv[]) {// 使用execl函數執行外部程序,替換當前進程映像// 參數1: 要執行的程序路徑// 參數2開始: 傳遞給程序的命令行參數,必須以NULL結尾execl("/bin/pwd", // 指定要執行的程序路徑(絕對路徑)"pwd", // 命令行參數列表的第一個參數,通常是程序名(可自定義)NULL); // 參數列表結束標記,必須為NULL// 如果execl調用成功,當前進程會被完全替換,不會執行到這里// 如果執行到這里,說明execl調用失敗perror("execl failed"); // 打印系統錯誤信息return 1; // 返回錯誤退出碼 }
帶參的:#include <unistd.h> int main(int argc, char* argv[]) {// 相當于執行: ls -l /tmpexecl("/bin/ls", "ls", "-l", "/tmp", NULL);return 0; }
3、fork 與 exec 結合
- 場景:父進程創建子進程后,子進程執行新程序(如 Shell 命令解析)。
- 示例邏輯:
pid_t pid = fork(); if (pid == 0) { execv("/path/to/program", argv); } // 子進程執行新程序
?
五、進程的分類
1、前臺進程
- 定義:需與用戶交互的進程(如終端運行的程序),默認啟動即為前臺。
- 查看命令:
ps -e | grep 進程名
。
2、后臺進程
- 定義:無需交互,在后臺運行(如服務器程序)。
- 啟動方式:命令后加
&
(如./a.out &
)。 - 終止命令:
killall 進程名
。
3、守護進程
- 定義:特殊后臺進程,獨立于終端(如
sshd
、httpd
),用于長期運行任務。 - 特點:
- 不依附終端,終端關閉后仍運行。
- 父進程通常為
systemd
(PID 1)。
- 創建步驟:
fork
后退出父進程,子進程成為孤兒進程。setsid
創建新會話,脫離原終端。chdir("/")
切換工作目錄至根目錄。umask(0)
清除文件權限掩碼。- 關閉默認文件描述符(0、1、2)。
- 示例代碼:通過信號處理實現日志寫入的守護進程。
- 查看命令:
ps axj
(顯示 PPID、會話信息等)。