[操作系統] 進程的概念與基礎操作詳解


在現代操作系統中,**進程(Process)**是一個重要的核心概念。它是操作系統管理資源的基本單位,理解進程的概念以及如何操作它是學習操作系統的基礎。本篇文章將深入講解進程的基本概念、結構和一些典型操作。

什么是進程?
前文所提:應用程序從磁盤加載進內存,而操作系統的管理方法是描述 + 組織,所以通過該種管理方法形成的管理對象就是進程。
從用戶的視角來看,進程是一個程序的運行實例;從操作系統的視角來看,進程是一個擁有資源分配能力的實體。
進程 = 內核數據結構對象 + 自己的代碼和數據
在Linux中進程可以看做是PCB(task struct)和自己的代碼和數據組成的。PCB中包含該進程的所有屬性,與代碼以及數據共同組成進程,PCB中存在指向其他進程的指針,通過指針的指向,進程通過雙向鏈表的數據結構來進行鏈接,而進程的管理就是對鏈表的增刪查改。并且每個進程都有獨立的地址空間,以避免相互干擾。
我們所使用的指令、工具以及自己的程序,運行起來,都是進程!

進程控制塊(PCB)
操作系統使用**進程控制塊(Process Control Block, PCB)**來描述和管理進程的所有信息。PCB 是一個重要的數據結構,操作系統通過它來追蹤每個進程的狀態。
在 Linux 操作系統中,PCB 被實現為一個名為 task_struct 的結構體,其主要內容包括:
PCB 的主要內容分類
- 標識符:如進程 ID (PID),用于唯一標識進程。
- 狀態:包括進程當前的運行狀態(運行、就緒、阻塞等)。
- 優先級:用于調度時比較不同進程的重要性。(CPU計算的優先級)
- 程序計數器:存儲下一條將要執行的指令地址。
- 內存指針:指向進程的代碼段、數據段以及共享內存塊。
- 上下文數據:包括處理器寄存器中的數據。
- I/O 狀態信息:描述進程使用的文件和 I/O 設備。
- 記賬信息:記錄進程使用的資源總量和時間。
PCB 的組織結構
在 Linux 內核中,所有進程的 PCB 以鏈表形式組織。通過 task_struct 中的 next 和 prev 指針,形成一個雙向鏈表,對進程進行遍歷和管理。
如下圖所示:



如何查看進程信息
在 Linux 系統中,可以通過 /proc 文件系統以及用戶級工具來查看進程信息:
通過 /proc 文件夾
- 每個進程在
/proc中都有一個對應的文件夾,文件夾名稱是該進程的 PID。 - 數字進程目錄是針對單個進程的詳細信息存儲,字母進程目錄(或文件)是關于系統整體信息的匯總。
- 例如,要查看 PID 為 1 的進程信息,可以訪問
/proc/1。

通過命令行工具
ps** 命令**:顯示進程的詳細信息。

bash就是命令行解釋器,每啟動一個XShell就會有一個bash進程啟動,所以輸入的指令等信息都是通過父進程
bash處理的,所以當使用命令行啟動多個進程后可以發現它們的父進程(PPID)都是bash。
top** 命令**:實時顯示系統運行的進程和資源使用情況。

通過系統調用獲取進程標識符
進程id(PID) :
**<font style="color:rgb(100,106,115);">getpid();</font>**?進程id(PPID):
**<font style="color:rgb(100,106,115);">getppid();</font>**
在sys/types.h包含獲取當前進程ID的函數,比如使用getpid();獲取當前進程的PID:


進程的cwd與exe
- 現在將進程啟動。

- 通過指令查看進程是否存在。

grep作為指令也是進程,所以顯示的時候也會顯示grep的進程信息。
- 查看進程具體信息。

/proc/[PID]目錄下的cwd和exe是與進程相關的重要符號鏈接,它們分別代表了進程的當前工作目錄和可執行文件路徑。理解這兩個概念對于深入掌握進程的行為和狀態非常有幫助。
cwd(Current Working Directory)
- 定義
cwd是一個符號鏈接,指向進程的當前工作目錄。當前工作目錄是指進程在執行過程中,其相對路徑的基準目錄。就好比你在終端中切換到某個目錄,然后運行一個程序,這個被切換到的目錄就是程序的當前工作目錄。- 例如,假設你在
/home/user/projects目錄下啟動了一個名為my_app的程序,那么/proc/[PID]/cwd就會指向/home/user/projects目錄。 - 使用
chdir可以改變cwd的指向路徑。
- 作用和用途
- 文件訪問基準:當進程嘗試打開一個相對路徑的文件時,這個相對路徑是相對于
cwd來解析的。比如,如果my_app程序嘗試創建data.txt文件,直接使用(./data.txt)而沒有指定絕對路徑,那么系統會直接在/home/user/projects下建立/home/user/projects/data.txt(假設cwd是/home/user/projects)。 - 監控和調試:對于系統管理員和開發者來說,通過查看
cwd可以了解進程是在哪個目錄下運行的,這對于調試程序(特別是當程序試圖訪問文件時出現路徑錯誤等問題)和監控進程行為非常有用。例如,如果一個進程試圖訪問一個不存在的文件并報錯,查看cwd可以幫助確定它試圖訪問文件的完整路徑,從而更容易地找到問題所在。
- 文件訪問基準:當進程嘗試打開一個相對路徑的文件時,這個相對路徑是相對于
exe(Executable)
- 定義
exe是一個符號鏈接,指向啟動該進程的可執行文件的路徑。這個可執行文件是進程運行的主體,包含了程序的機器代碼和資源。- 例如,如果你使用命令
/usr/bin/my_app啟動了一個程序,那么/proc/[PID]/exe就會指向/usr/bin/my_app。
- 作用和用途
- 程序識別:通過
exe鏈接,你可以清楚地知道是哪個可執行文件啟動了這個進程。這對于系統監控工具來說非常重要,因為它們可以根據可執行文件的路徑來識別和分類進程。例如,在一個包含多個不同版本應用程序的系統中,通過exe可以區分是哪個版本的應用程序正在運行。 - 安全和審計:在安全審計方面,
exe可以幫助確定是否有未經授權的程序在運行。如果發現exe指向一個不熟悉或可疑的路徑,這可能是一個安全風險的信號。此外,它也可以用于追蹤軟件的使用情況,比如統計某個特定可執行文件被啟動的次數等。 - 重新啟動和分析:對于開發者來說,如果需要重新啟動進程或對進程進行分析(如性能分析),知道
exe的路徑是非常有用的??梢灾苯油ㄟ^這個路徑來啟動新的進程實例,或者使用調試工具(如gdb)附加到這個可執行文件上進行分析。
- 程序識別:通過
實際應用示例
假設你正在運行一個名為example_app的程序,你可以在終端中使用以下命令來查看其cwd和exe:
pid=$(pgrep example_app) # 獲取example_app進程的PID
ls -l /proc/$pid/cwd # 查看cwd鏈接
ls -l /proc/$pid/exe # 查看exe鏈接
這將輸出類似以下內容:
lrwxrwxrwx 1 user user 0 Jan 1 12:34 /proc/1234/cwd -> /home/user/projects
lrwxrwxrwx 1 user user 0 Jan 1 12:34 /proc/1234/exe -> /usr/local/bin/example_app
從這個輸出中,你可以看到example_app進程的當前工作目錄是/home/user/projects,而其可執行文件位于/usr/local/bin/example_app。這些信息對于理解進程的行為和進行系統管理非常關鍵。

認識fork以及進程的獨立
進程的創建和管理是操作系統的重要功能。在 Linux 中,創建進程主要通過 fork() 系統調用。
通過man查看fork():

- 返回值為
pid_t類型- 包含在頭文件
<unistd.h>
獲取進程和父進程的標識符
可以通過以下代碼獲取進程的 PID 和其父進程的 PPID:
PID:
getpid();PPID:
getppid();
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
如何創建子進程以及父子進程關系的理解
fork() 是 Linux 中用于創建新進程的函數。

得到的運行結果如下:

可以看出,在fork();執行后出現了兩個進程,其中一個進程的pid是fork前的進程的pid,一個是新進程的pid。此時就成功的創建了子進程。但是要如何使用fork()呢?
首先從fork()函數本身開始理解:
以下是一個代碼示例。
printf("父進程開始運行,pid:%d \n", getpid());
pid_t id = fork(); // 父子進程的獨立過程是在調用 fork() 函數時完成,之后父子進程獨立
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
while(1)
{
sleep(1);
printf("我是一個子進程!我的pid:%d,我的父進程id:%d\n",getpid(), getppid());
}
}
else
{
// father
while(1)
{
sleep(1);
printf("我是父進程!我的pid:%d,我的父進程id:%d\n",getpid(), getppid());
}
}
運行結果如下:

在
fork()執行后創建了子進程,并且同上文所講相同,父進程的父進程是bash進程。
什么是 fork()?
fork() 是用于創建進程的系統調用。
- 它會從當前運行的進程(稱為父進程)中復制出一個幾乎完全相同的新進程(稱為子進程)。
- 父子進程幾乎完全獨立,但共享相同的代碼段。
- 父子進程擁有不同的內存空間,彼此之間不影響。
fork() 的返回值
fork() 返回兩個值,因為它在兩個進程中執行,分別是:
- 在父進程中,
fork()返回子進程的 PID(進程 ID),這是一個正整數(> 0)。 - 在子進程中,
fork()返回0。 - 創建子進程失敗返回-1。
為什么 **fork()** 有兩個返回值?
操作系統在執行 fork() 時,會基于當前父進程的狀態,創建一個幾乎完全相同的子進程。
fork() 會把當前的程序和運行環境復制一份,創建一個新的進程。在fork()函數內,return也是代碼語句,所以也會作為拷貝的代碼,申請新的PCB,拷貝父進程的PCB給子進程。在fork中通過區分父子進程后,通過return返回兩個返回值,兩個返回值都對id進行修改,對變量進行修改,觸發了寫時拷貝,因此系統會進行空間及數據的分配。這就是為什么返回兩個返回值的原因,下文會對該過程進行詳細講解。
- **父進程調用 **
**fork()**,操作系統知道它是父進程,所以返回子進程的 PID,方便父進程管理。 - **子進程調用 **
**fork()**,它的視角是:我是子進程,我沒有子進程,所以返回0。
注意:
fork()的執行結果是兩套完全獨立的運行環境。fork()的返回值是區分父進程和子進程的關鍵。
進程獨立的過程詳解
父子進程的獨立過程是在調用 **fork()** 函數時完成的。具體地說,當 fork() 被調用時,操作系統會執行以下步驟,從而使父進程和子進程完全獨立:
進程復制的時機
**fork()**** 的調用時刻**:操作系統在執行fork()時,會基于當前父進程的狀態,創建一個幾乎完全相同的子進程。
進程復制的內容:
- 進程控制塊(PCB):
- 操作系統為子進程分配新的 PCB,記錄子進程的狀態信息(如進程號 PID、父進程號 PPID 等)。
- 子進程的 PCB 是從父進程的 PCB 復制的,因此子進程最初看起來與父進程完全相同。
- 地址空間:
- 操作系統復制父進程的內存結構給子進程,形成一份幾乎完全相同的內存空間。這包括:
- 代碼段:子進程共享父進程的代碼段(只讀)。
- 數據段:父進程中的全局變量和靜態變量會被復制到子進程。
- 堆和棧:子進程的堆和棧也被復制,但它們的內存分配是獨立的。
- 操作系統復制父進程的內存結構給子進程,形成一份幾乎完全相同的內存空間。這包括:
- 文件描述符:
- 父進程打開的所有文件描述符會被子進程繼承,兩者對同一文件的操作是共享的(文件偏移量同步)。
父子進程何時獨立?
一旦 fork() 返回,父子進程開始獨立運行:
- 子進程的內存空間是父進程的副本,但它與父進程完全分離,修改變量不會相互影響。
- 子進程和父進程的執行流從
fork()的返回值處分叉:- 父進程繼續運行時,
fork()返回子進程的 PID。 - 子進程繼續運行時,
fork()返回0。
- 父進程繼續運行時,
父子進程的獨立性體現在以下幾點:
- 內存空間獨立:
- 雖然子進程初始時與父進程的內存內容相同,但它的地址空間是獨立的,修改子進程的內存不會影響父進程。
- PID 和資源獨立:
- 子進程有自己的 PID,調度策略也可能不同。
- 子進程的狀態和運行不會直接影響父進程。
- 文件描述符共享但獨立操作:
- 父子進程共享文件描述符,但可以獨立關閉或操作文件。
獨立的實現機制:寫時復制(Copy-on-Write, COW)
現代操作系統使用了一種優化機制,叫做 寫時復制(COW),以減少不必要的資源浪費:
- 在
**fork()**剛返回時,父子進程共享相同的物理內存頁(只讀),因此復制過程很快。 - 當父進程或子進程試圖修改內存時:
- 操作系統會為需要修改的部分分配新的物理內存。
- 修改后的內存空間對父子進程來說是獨立的。
因此,只有在需要時,內存的獨立性才真正實現,也就是需要對對內存中數據進行修改的時候,但邏輯上,父子進程從 fork() 返回后就已經被視為完全獨立了。
流程圖
調用 fork() 后,父子進程的分離流程可以表示如下:
父進程:
ret = fork(); // 返回子進程 PID (> 0)
------------------------------
| 父進程邏輯 |
| printf("父進程部分"); |
| 獨立運行,繼續父進程代碼 |
------------------------------
子進程:
ret = fork(); // 返回 0
------------------------------
| 子進程邏輯 |
| printf("子進程部分"); |
| 獨立運行,繼續子進程代碼 |
------------------------------
寫時拷貝修改ret內容,進程獨立。
總結:操作系統完成進程獨立的過程
**fork()**** 是操作系統分離父子進程的起點**。- 通過資源復制、地址空間分離和調度機制,父子進程實現了完全獨立。
- 父子進程雖然共享代碼和部分資源,但內存、PID 和運行狀態是互相獨立的,確保了它們可以并發執行,互不干擾。
- 寫時拷貝:當父子進程嘗試修改共享數據時,操作系統會將數據復制到獨立空間。
基本的獨立靠的是
**struct task_struct(PCB)**獨立。當父子進程任何一方進行數據修改的時候觸發寫時拷貝,操作系統就把修改的數據在底層拷貝一份,讓整個目標進程修改這個拷貝,脫離代碼共享,實現完全獨立。

本文來自博客園,作者:DevKevin,轉載請注明原文鏈接:http://www.rzrgm.cn/kevinbee/p/18678186

,每啟動一個XShell就會有一個bash進程啟動,所以輸入的指令等信息都是通過父進程
浙公網安備 33010602011771號