在計算機科學中,進程是一個至關重要的概念。它是操作系統中最基本的執行單元,也是實現併發和多任務處理的關鍵。《操作系統概念》一書中提到:"進程是正在執行的程序,是程序執行過程中的一次指令、數據的集合,也可以叫做程序的一次執行過程。"然而,要真正理解進程,需要我們跨越硬件和軟件開始,深入探索期底層原理和工作機制。

一.硬件:馮諾依曼體系結構

1.核心框架

馮諾依曼體系結構是現代計算機的核心硬件框架,核心是“存儲程序+程序控制”,以5大組件實現數據與指令的處理:

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_狀態


  • 核心邏輯:程序和數據先存入存儲器,控制器按序讀取指令,驅動運算器執行運算,再通過輸入/輸出設備交互信息。
  • 5大組件:
  • 存儲器:內存(存數據/指令)
  • 輸入設備:輸信息(鍵盤、攝像頭、話筒、網卡、硬盤......)
  • 輸出設備:展結果(顯示器、播放器、網卡、硬盤......)
  • 運算器:對數據計算(算術運算,邏輯運算)
  • 控制器:對計算過程(硬件流程)進行一定控制、

其中,設備都只能和內存打交道(故儲存器處於一個核心地位)輸入設備和輸出設備被稱作外部設備(簡稱: 外設)。各個硬件都是獨立的個體,每個硬件單元必須用“線”連接起來(系統總線,IO總線:暫時不用過多瞭解)。

2.儲存器(內存)的作用

對於這5大組件,我們可以思考一個問題:程序數據是通過輸入設備得到的,運算過程由CPU完成,最後通過輸出設備輸出信息,那麼,內存在其中有什麼作用?我們為什麼不能用寄存器(臨時存儲單元)直接將外設的程序數據直接給CPU?

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_操作系統_02

我們看到,在存儲層繫結構金字塔(存儲金字塔) 當中,按照“訪問速度快->慢,容量小->大,成本高->低” 將不同類型的存儲設備從頂層到底層逐層排開;這時我們應該可以猜測到內存在其中發揮的重要作用:

  • 由於CPU比外設效率(訪問速度快太多)高,而CPU(寄存器)的容量小、價格高;受成本限制,寄存器只有較小的容量,如果沒有內存,我們只能不斷從輸入設備中讀取一小部分數據快速計算,再給輸出設備,這種線性結構將大部分的時間浪費在外設上;
  • 而內存,相當於硬件級別緩存一次性讀取大量數據到內存(容量更大),在不斷給CPU,當CPU處理部分數據的同時,內存能夠繼續從外設中讀取數據,形成並行結構輸出時同理,讓數據在內存暫存,“慢慢”傳給外設,同時不影響CPU不斷地進行計算。

3.馮諾依曼體系結構影響

馮諾依曼這種體系結構的設計,在提高效率的同時,降低了計算機的造價(用更便宜的內存代替成本更高的寄存器),極大促進了計算機在我們生活中的發展和普及。

二.操作系統

1.概念

操作系統(OS)是一款進行管理的軟件,可分成兩部分來看待:

  • 內核(進程/內存/文件/驅動......),是“管理者”,具有計算機最高權限
  • 其他程序(函數庫、shell程序.......),是“使用者”,僅擁有普通權限

什麼叫做最高權限?什麼是普通權限?

  • - 最高權限(內核態/核心態):僅內核擁有,可直接操控CPU、內存、磁盤等所有硬件,能執行最底層系統指令。
  • - 普通權限(用户態):應用程序和普通用户操作的權限,無法直接訪問硬件,需通過“系統調用”向內核申請(root賬號有“用户態”的最高權限,但也需要“系統調用”向內核申請訪問)。
  • - 原因:我們知道:操作系統是為了幫助用户管理好軟硬件資源,給用户提供一個良好(穩定高效)的運行環境;而對於操作系統來説,用户既是日常使用者,也是開發軟件的程序員。然而,為了保證OS內的各種數據安全,OS是不能“信任”任何人的,意味着任何用户都不能直接接觸操作系統內部數據;然而OS又必須滿足程序員開發訪問數據需要,所以操作系統以接口的方式給用户提供了調用入口,以此獲取OS內部數據——自己內部函數調用(如C/C++函數),系統調用(操作系統提供函數)
  • 所有訪問OS的行為,都只能通過系統調用實現;基於系統調用實現功能的行為,被稱作系統編程;

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_操作系統_03

2.如何管理

回到概念,系統各種數據複雜,OS是如何做到“管理”的?舉個例子:現在,我們要管理一個水果店,假設我們目前不認識任何一種水果,應該怎麼做?首先想到的是“分類”。但由於我們不認識任何一種水果,我們需要從屬性入手(當然,我們對世界的認識也是從屬性開始的,比如讓你形容"什麼是蘋果",我們能夠描述出的都是蘋果的屬性):①{{圓形或橢圓},{一般紅色},{表皮光滑},{頂部有果柄},{底部有凹陷的果臍}} ②{{成長彎月牙狀},{成熟呈黃色},{果菱明顯},{頂端有"把手"},{果柄短}} ;我們先描述外表屬性,根據我們總結出的屬性對水果進行組織(分類以及其它操作)。

以上我們的行為,可以總結為:先描述,再組織

操作系統也是一款“管理“的軟件:管理的本質是對數據作管理,那麼對數據做管理也遵循:先描述,再組織的過程;最終,OS將對對象的管理轉換為對某種數據結構的增刪查改。

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_狀態_04

三.進程概念

講解進程我們為什麼談硬件和操作系統?這些和進程有什麼關係?

許多經典操作系統教材寫進程時都提過這樣一句話:

進程是加載到內存中執行的程序實例

1.程序

先説程序:程序是一組預先編寫好的、能被計算機識別和執行的指令集合,通常以文件形式存儲(如.exe、.py文件),用於完成特定任務(如計算、文本處理、運行軟件等),本質是靜態的代碼和數據

程序有兩種核心狀態:

  • 未運行狀態:程序以靜態文件形式存於磁盤,未佔用CPU、內存等硬件資源,處於“待命”狀態。
  • 運行狀態:程序被加載到內存成為進程,佔用硬件資源並被CPU執行,處於“活動”狀態。

也就是説:一個程序開始運行(被加載到內存)後,我們就稱它為進程。

2.PCB

然而操作系統上不可能只運行一個進程,當多個程序被加載到內存,OS就不得不將它們管理起來,當然,也遵循管理原則:先描述,再組織

  • 描述:定義struct結構體對象。

任何一個進程,被加載到內存形成進程前,OS要先創建描述進程(屬性)的結構體對象,這個struct結構體,就是PCB(Process Control Block)

  • 組織:用鏈表等數據結構將PCB結構體儲存來起,進行增刪查改等管理操作。


[Linux]探索進程的奧秘:從硬件到軟件的全面解析_馮諾依曼體系結構_05

OS管理代碼和數據時,不需要對程序代碼和數據進行過多關注,只需要對鏈表進行操作;如:幾個進程順序執行,只需要讓PCB排隊,而不是代碼和是數據排下去,”單鏈表“中只需要存PCB信息;當我們要結束進程時,只需要將鏈表中對應進程節點刪除,不用去管理代碼/數據的刪除(也意味着系統將不再將其視為一個可調度的進程,而此進程代碼和數據空間將被設為“閒置”,之後被其他程序的代碼代碼和數據覆蓋)

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_狀態_06

所以本質上:程序(自己的代碼和數據)+內核PCB數據結構對象 才組成真正的進程。

3.task_struct PCB在Linux的具體實現

(1)概念

PCB是操作系統理論中的通用概念,用於描述進程狀態、管理進程的“數據結構模板”,定義了所有操作系統管理進程所需的核心信息(如PID、進程狀態、資源佔用、調度信息等),適用於所有支持進程管理的操作系統。

而task_struct:僅存在於Linux系統中,是Linux內核為實現PCB功能而定義的具體數據結構,是PCB概念在Linux中的“實體化”。

(2)源碼核心結構

task_struct  是 Linux 內核中最複雜的數據結構之一,包含進程的"識符、狀態、調度信息、內存映射、文件描述符、信號處理"等所有屬性

struct task_struct {
    // 1. 進程基本標識符
    pid_t pid;                // 進程ID(PID) 進程的編號、標識
    pid_t tgid;               // 線程組ID(主線程PID,線程共享)
    struct task_struct *real_parent; // 真實父進程
    struct task_struct *parent;      // 當前父進程(可能被 ptrace 修改)
    struct list_head children;       // 子進程鏈表
    // 2. 進程狀態與調度
    volatile long state;      // 進程狀態(TASK_RUNNING/TASK_SLEEPING等)
    unsigned int flags;       // 進程標誌(如 PF_KTHREAD 表示內核線程)
    int prio;                 // 動態優先級
    int static_prio;          // 靜態優先級
    struct sched_entity se;   // 調度實體(用於CFS調度器)
    // 3. 內存管理相關
    struct mm_struct *mm;     // 進程地址空間(用户空間內存)
    struct mm_struct *active_mm; // 活躍地址空間(內核線程無mm,複用此)
    // 4. 文件與文件系統
    struct files_struct *files; // 進程打開的文件描述符表
    struct fs_struct *fs;       // 進程的文件系統上下文(當前目錄、根目錄)
    // 5. 信號處理
    struct signal_struct *signal; // 進程信號集合
    struct sighand_struct *sighand; // 信號處理函數集合
    // 6. 線程相關(Linux 中線程是輕量級進程)
    struct thread_struct thread; // 線程上下文(寄存器、棧信息等)
    unsigned long nsproxy_ptr;   // 命名空間代理(PID/網絡/掛載等命名空間)
    // 7. 其他核心屬性
    char comm[TASK_COMM_LEN]; // 進程命令名(如 "bash",長度默認16)
    u64 utime, stime;         // 用户態/內核態運行時間
    struct list_head tasks;   // 全局進程鏈表節點(所有進程通過此鏈表串聯)
};

這是簡化版CentOS 7中task_struct的核心字段,可以看看大致結構,裏面包含了進程相關的各種屬性信息。

如果想在Linux查看進程,可以使用ps axj | head -1命令,同時可用ls命令在 /proc/ <pid>文件目錄下查看進程編號為pid進程的詳細信息

4.進程PID/PPID

(1)概念

進程 ID(PID)是操作系統為每個進程分配的唯一標識符。它是一個整數,用於在系統中標識和跟蹤進程。

每個進程都有一個獨特的 PID,當進程啓動時,操作系統會為其分配一個 PID,並在進程的整個生命週期中保持不變。

簡單來説:

  • pid(Process ID):進程的編號(很好理解,為了區分和更好管理每個進程,給每個進程都編了號)
  • ppid(Parent Process ID):一個進程父進程的編號

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_馮諾依曼體系結構_07

(2)getpid/getppid

進程編號pid在PCB結構體內部,且PCB結構體由操作系統管理,之前提到“OS是不信任我們的”,那我們如何能在代碼中得到一個進程的pid?

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_狀態_08

兩個系統調用接口,可以讓我們在自己寫程序時,得到進程ID(可在Linux中通過man手冊查詢man getpid)

四.進程創建

先觀察現象:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    printf("begin:進程,pid:%d,ppid:%d\n",getpid(),getppid());
    printf("before:only one line\n");
    sleep(3);
    pid_t id  = fork();//以調用進程為模板創建一個新的進程
    printf("after:only one line\n");

    if(id == 0){
        //子進程
         printf("子進程,pid:%d,ppid:%d\n",getpid(),getppid());
         sleep(1);
    }
    else if(id > 0){
        //父進程    
         printf("父進程,pid:%d,ppid:%d\n",getpid(),getppid());
         sleep(1);
    }
     else{
        //error
        printf("error\n");
    }
    return 0;
}

我們運行代碼會發現兩個現象:① printf("after:only one line\n");這一行代碼被執行了兩次;②分支語句都被執行。為什麼?

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_操作系統_09

這就不得不提fork()這一函數:

fork()  是 Linux/Unix 下的系統調用,核心作用是從當前進程(父進程)複製出一個新進程(子進程),最終得到兩個幾乎完全相同的運行進程。

關鍵特性(3點)

  1.  一次調用,兩次返回:調用  fork()  後,父進程返回子進程的 PID(正數),子進程返回 0,以此區分父子進程。
  2.  數據獨立:子進程複製父進程的內存空間(代碼、數據、堆棧等),後續修改互不影響(寫時複製,初始共享內存,修改時才真正拷貝)。
  3.  執行流並行:父子進程獨立運行,操作系統調度順序不確定(誰先執行由 OS 調度決定)。 

核心用途

常用於創建多進程,實現並行任務(如服務器多進程處理客户端請求、後台任務拆分等)。

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_進程概念_10

對於父進程,fork會返回子進程的ID,這是為了父進程更好的找到子進程(子進程可以通過getppid得到父進程,而父進程不容易得到子進程id);同時,我們創建子進程是為了讓父、子執行不同代碼塊,fork返回值不同可以在之後代碼中更好的區分父子進程。

至於為什麼同一個id能獲得兩個不一樣的值,甚至於 &id 我們都會得到相同的結果,這與另兩個概念:虛擬地址空間和頁表 有關(這部分涉及到的知識比較多,而且不影響理解對進程的基礎概念,所以之後單寫一篇博客講地址空間,這就不細説了)。

上述代碼現象:

用這段代碼在Linux持續顯式某進程信息:

 while :; do ps axj | head -1 && ps axj | grep <進程名> | grep -v grep; sleep 1; echo "-----------------"

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_操作系統_11

總結

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_馮諾依曼體系結構_12

當父子進程被創建好,誰先運行是不確定的,由調度器決定操作系統負責決定哪一個程序需要被運行,調度器決定哪個進程該被調度(我們有大量進程,而CPU等資源有限,所以説,各進程之間是競爭關係,調度器是為了保證公平調度各進程的運行)

五.進程狀態

之前我們説,操作系統在管理進程管理的是進程的PCB;進程順序執行,只需要讓PCB在“鏈表”中排隊;但我們又提到,調度器管理哪個進程被調度;二者什麼關係?

操作系統(通過內核)決定進程狀態;調度器(OS內核的一部分)基於進程狀態調度程序,針對“就緒程序”的進程,按規則(優先級,時間片)決定其運行先後順序,然後將選中的進程分配給CPU執行。

進程狀態

進程狀態是操作系統內核對進程生命週期階段的劃分,核心目的是高效管理CPU和資源。

1.運行態

程正在佔用CPU,執行代碼指令的狀態;在單核CPU系統中,任何時刻最多隻有一個進程處於運行態;

但是,為什麼在我們看來可以有很多個程序同時被運行?

CPU在多個進程中以極快的速度切換,我們可以稱一個進程運行的極短時間為“時間片”當進程運行完它的時間片後,調度器會從“準備就緒”的進程中,找到下一個被運行的進程;

這些“準備就緒”的進程,都被放在運行隊列裏排隊!

運行隊列

運行隊列是操作系統中就緒狀態的進程集合,這裏的每個進程都已具備運行條件,等待CPU調度執行,進入運行狀態。

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_馮諾依曼體系結構_13

操作系統管理運行隊列,調度程序運行。

2.阻塞態

可以説:運行態進程以及具備運行條件的進程被放到運行隊列當中;同理,阻塞狀態的進程將被放於另一個隊列——等待隊列。

比如,從鍵盤(外設)中讀數據時,就需要把進程投入到等待隊列,因為我們無法預知外設何時返回數據,此時若進程繼續佔用CPU,會因無法推進進程而浪費CPU資源。

但是,硬件或外部設備有很多,我們怎麼知道一個進程等待的是哪個設備?

等待隊列

在操作系統(Linux內核)中,每個外部設備通常有描述該設備的結構體,結構體中會包含指向該設備等待隊列的指針。

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_優先級_14

當有一個進程需要等待外部設備(或其他資源)時,回到該資源對應的等待隊列去排隊當等待資源/事件就緒後,會觸發“喚醒進程+恢復執行”,簡單來説:就是將進程狀態改成“R”運行態,再把PCB放入運行隊列

3.掛起狀態

當內存資源嚴重不足時會觸發另一種狀態“掛起狀態”:在保證正常的前提下,操作系統會將一部分(低優先級或者長時間未活躍)進程的代碼和數據放入磁盤(不再留在內存),只讓進程的PCB保留在內存——這樣省出來的內存資源就會給高優先級或者必須使用的進程使用。

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_操作系統_15

1. 交換分區(Swap Partition)

是硬盤上專門劃分的一塊區域,作用是臨時替代內存,存放暫時不用的進程數據,緩解內存不足問題。 

2. 換出(Swap Out)

當內存不足時,OS會將暫時不用的進程數據(如後台閒置進程)從內存轉移到交換分區,釋放內存空間給急需內存的進程(如前台運行的程序)。

3. 換入(Swap In)

當被換出的進程需要再次執行時(如用户重新打開後台軟件),OS會將該進程數據從交換分區重新加載回內存,使其能被CPU調度執行。

~ 三者關係:交換分區是“倉庫”,換出是“把暫時不用的東西存進倉庫”,換入是“把需要用的東西從倉庫取回來”。

Linux中進程的狀態維護

  • 運行狀態“R”(run):進程要麼正在 CPU 上執行,要麼在運行隊列中等待 CPU 調度。
  • 可中斷睡眠“S”(sleep阻塞狀態):進程因等待某資源就緒,內核會喚醒進程(隨時響應外部變化)進入就緒態。
  • 深度睡眠“D”(disk sleep):進程同樣因等待資源暫停,但不能被信號打斷(如磁盤 I/O 關鍵操作時進程不響應任何其他需求),強制中斷可能導致數據損壞,需等待資源就緒後自動喚醒。
  • 暫停進程"T"(stop):進程暫停(如kill -19 進程PID可暫停進程),需通過 kill -18 進程PID恢復為運行。
  • 暫停狀態“t”:也是暫停,如:斷點暫停。
  • 終止態"X"(dead): 釋放清除資源,進程結束。
  • 殭屍態"Z"(zombine): 在“X”dead之前會出現“Z”狀態<defunct>——失效進程,是一個進程在死之前OS先把"Z"維持一段時間,直到父進程去查看它(主動回收退出碼等進程信息);這段時間內,進程相關資源(task_struct)不會被釋放。
殭屍態

關於殭屍態,我們要注意它可能引發一個問題:內存泄漏!

當一個進程處於"Z"狀態,它本質上已經終止了,僅殘留PCB;所以它無法用命令直接殺掉(如kill -9 進程PID無法起作用)。如果父進程始終不讀取它的退出信息,它的PCB會一直佔用內核內存,無法回收。

分兩種情況看:

子進程先結束
  1. 父進程可以通過wait()系列(之後文章進程控制裏詳細介紹),讀取子進程退出狀態,徹底回收PID和PCB,結束殭屍進程。
  2. 當父進程沒有wait系列函數,但是父進程很快會結束,當父進程結束後,殭屍子進程會被init(或systemd)進程(PID=1)接管;而init會自動調用wait系列函數。
int main()
{
    printf("begin:進程,pid:%d,ppid:%d\n",getpid(),getppid());
    printf("before:only one line\n");
    pid_t id  = fork();
    printf("after:only one line\n");

    if(id == 0){//子進程
         printf("子進程,pid:%d,ppid:%d\n",getpid(),getppid());
         sleep(1);
    }
    else if(id > 0){//父進程  
        //我們讓父進程循環,模擬子進程先結束情況
        while(1){
        	printf("父進程,pid:%d,ppid:%d\n",getpid(),getppid());
         	sleep(1);
        }
    }
     else{
        //error
        printf("error\n");
    }
    return 0;
}

子進程要一直處於“Z”狀態等待父進程回收退出信息:

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_進程概念_16

父進程先結束

父進程先退出(bash回收),這時的子進程被稱作“孤兒進程”;但由於bash進程只創建了父進程,所以bash只有針對父進程的回收邏輯,並沒有子進程的,所以,子進程只能被OS(init進程)接收"領養";

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_優先級_17

還要注意一個點:如果父進程結束後,子進程轉到後台運行;不同於前台運行程序我們可以通過ctrl+c結束, 結束後台運行程序需要使用命令kill -9 進程PID

六.進程優先級

概念

進程優先級是用來表示對資源訪問(有權訪問的)誰先誰後的。

作用

CPU等資源是有限的,而進程有很多,如果一個進程長時間得不到CPU資源,進程就會長時間無法推進,引發進程的飢餓問題;所以操作系統必須保證”進程的競爭是良性”的,那麼久需要確立優先級。

設置優先級

1.查看優先級

命令ps -al

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_馮諾依曼體系結構_18

2.調整優先級

Linux 調整進程優先級,核心是修改 Nice 值([-20,19],默認 0),那如何修改Nice值?介紹三種方法:

  1.  nice  
  1. 格式: nice -n [Nice值] [命令]  
  2. 示例:nice -n 5 top 設置top命令的nice值為5
  1.  renice  
  1. - 格式: renice [新Nice值] -p [進程PID]  
  2. 示例(PID 1234): renice 5 -p 1234將PID=1234的進程nice值設為5
  1. top
  1. 使用方法:使用top命令,輸r,再輸入進程PID,最後改NI值

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_馮諾依曼體系結構_19

當我們再次ps -al查看時:

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_狀態_20

優先級實現原理

現在我們知道了可以通過改變NI進而改變進程PRI,從而改變進程的優先級;但是,這樣一來:我們可能有很多個優先級不同的進程,我們要分先後的讓它們分別運行,那麼之前優先隊列的結構就不夠用了(只有兩個指針指向一個鏈表,所有優先級混在裏面,我們無法區分哪個優先級高,哪個低)。

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_優先級_21

擴展

操作系統中,有大量的數據需要我們去控制,也一定存在大量數據結構;那麼對於一個task_struct節點而言,它不一定屬於一種數據結構——可以既是鏈表節點,也是樹節點。

[Linux]探索進程的奧秘:從硬件到軟件的全面解析_馮諾依曼體系結構_22

關於進程內容還有很多,之後還會繼續更新相關博客,但這篇文章已經能夠讓我們對進程是什麼有了基本瞭解。 

簡言之,進程是程序的“運行態化身”,也是操作系統管理資源、實現多任務的核心。理解它,就看懂了電腦高效運轉的關鍵一環。

同時,進程是操作系統與硬件之間的“橋樑”——它讓程序能跑起來,也讓資源能用得高效。搞懂進程,是我們入門操作系統的第一步。