🌟🌟作者主頁:ephemerals__
🌟🌟所屬專欄:Linux
目錄
前言
一、什么是進程
二、task_struct的內容
三、Linux下進程基本操作
四、父進程和子進程
1. 用fork函數創建子進程
五、進程狀態
1. 三種重要狀態
運行狀態
阻塞狀態
掛起狀態
2. 內核鏈表的理解
3. Linux的進程狀態
孤兒進程?
總結
前言
????????在學習 Linux 操作系統的過程中,進程是一個至關重要的概念。無論你是想了解系統的基礎操作,還是深入研究 Linux 內核,進程管理的理解都將為你打下堅實的基礎。進程不僅是操作系統資源管理的核心,也是實現多任務處理的關鍵所在。通過學習進程的創建、調度、同步等機制,你可以更好地掌握操作系統的運行原理,進而優化系統性能和解決實際問題。本文將從基礎知識入手,帶領大家逐步深入探索 Linux 中進程的各個方面,幫助你在 Linux 學習的道路上邁出堅實的第一步。
一、什么是進程
? ? ? ? ?進程有多種描述方式,例如:程序的運行實例、正在執行的程序、操作系統進行資源調配的基本單位等。不過,以上說法都太理論化,我們用程序運行的實際情況來描述進程。
? ? ? ? 一個程序在執行前,其二進制代碼和數據(變量、常量、堆棧數據等)需要加載到內存。當加載完成之后,操作系統就會為這一塊代碼和數據創建一個對應的PCB(也叫做進程控制塊,本質是一個存儲進程相關信息的結構體),其中存在一個內存指針,指向代碼和數據,便于訪問。
? ? ? ? 所以“進程”不僅僅包括了程序的運行實例,它也包括操作系統管理該進程的相關信息。簡而言之,“進程”是指PCB與程序代碼數據的集合。操作系統根據PCB來跟蹤進程的執行狀態,方便對進程進行調度。
????????而當有多個程序需要執行時,操作系統就會為每一個程序的代碼和數據都創建一個對應的PCB(描述過程),再通過容器將所有的PCB串聯起來(組織過程)。此時,操作系統對于進程的管理即為對容器的增刪查改。
需要注意:
在Linux下,PCB(進程控制塊)是一個叫做task_struct的結構體;進程的所有屬性都可以通過task_struct直接或間接地找到。
Linux下的task_struct之間通過雙向鏈表進行連接。
二、task_struct的內容
?task_struct有如下成員,用于表示進程各種狀態信息,以及訪問程序的代碼和數據:
-
進程標識符(PID)--區別其他進程
-
進程狀態信息
-
優先級
-
程序計數器
-
內存指針--指向代碼和數據
-
上下文數據
-
I/O狀態信息
-
記賬信息
-
其他信息
?之后的進程學習當中,我們將圍繞以上成員數據,學習進程的相關概念及操作。
三、Linux下進程基本操作
C語言函數獲取當前進程標識符和父進程的標識符(PID):
getpid(); //返回當前進程標識符,返回值類型是pid_t
getppid(); //返回當前進程的父進程標識符
注意使用以上函數時,需要引頭文件<unistd.h>。
使用指令查看當前所有進程:
ps ajx
ls /proc
根據程序名查看某個進程信息:
ps ajx | head -1 && ps ajx | grep (可執行程序名)
根據標識符查看進程文件:
ll /proc/(標識符)
示例:
我們可以重點關注一下圖中列舉出的兩個文件cwd和exe:
cwd指的是當前進程對應的可執行程序所在目錄;
exe指的是當前進程對應的可執行程序位置。
C語言函數修改當前進程所在路徑:
chdir("(路徑)");
注意使用該函數要引頭文件<unistd.h>。
殺進程的兩種方式:
1. ctrl + c
2. 命令行輸入kill -9? (進程標識符)
四、父進程和子進程
? ? ? ? 一個進程通過系統調用創建出的另一個進程稱之為該進程的子進程,反之該進程稱為其父進程。在Linux下,我們在命令行輸入的命令都是Bash(命令行解釋器)的子進程。
1. 用fork函數創建子進程
? ? ? ? ?fork是一個系統調用,存在于頭文件<unistd.h>中,當執行fork函數之后,當前進程會創建一個子進程,后續的代碼會被父進程和子進程分別執行一次。
代碼示例:
#include <stdio.h>
#include <unistd.h>int main()
{fork();printf("hello world\n");return 0;
}
運行結果:
注意:fork函數創建的子進程沒有自己的代碼和數據,雖然操作系統為其創建了PCB,但是其內存指針指向的還是父進程的代碼和數據。
?子進程在創建成功后,fork函數會給子進程返回0,給父進程返回子進程的PID。為什么會給父子進程不同的返回值呢?因為一個父進程可能會有多個子進程,給父進程返回子進程的PID,更方便父進程對子進程進行管理。而子進程如果想要知道父進程的PID,直接調用getppidh函數即可。另外,返回值不同可以配合分支語句讓父子進程執行不同的代碼。示例如下:
#include <stdio.h>
#include <unistd.h>int main()
{pid_t id = fork();if(id == 0)//子進程{printf("我是子進程,我的pid是%d\n", getpid());}else//父進程{printf("我是父進程,我的pid是%d\n", getpid());}return 0;
}
運行結果:
? ? ? ? 那么,為什么fork函數能夠做到返回兩個值呢?實際上fork函數在執行return語句之前,就已經創建好了子進程,此時就可以通過分支語句來區分給父進程和子進程的返回值。?
注意:雖然fork函數創建的子進程與父進程的代碼是共享的,但如果父子任何一方要修改其中的數據,那么操作系統就會將數據進行拷貝,此時父子就各自維護自己的數據,本質上修改的是拷貝的數據,不會影響另一方。這種狀況叫做寫時拷貝。
五、進程狀態
? ? ? ? 對于不同的操作系統,進程狀態可能略有不同,但常見的大體上的進程狀態有如下幾種:創建、就緒、運行、阻塞、終止、掛起。我們介紹一下其中最重要的三點:運行狀態、阻塞狀態和掛起狀態。
1. 三種重要狀態
運行狀態
? ? ? ? 首先要知道,一般情況下一個CPU維護一個進程調度隊列,該隊列中存放著一個個PCB,等待CPU對它們進行調度。而一個PCB在運行隊列中排隊時,就稱該進程處于運行狀態。
阻塞狀態
? ? ? ? 當一個進程需要等待某種資源或設備(如鼠標、鍵盤等)就緒時,該進程就處于阻塞狀態。阻塞狀態的進程在代碼層面的體現是:PCB從運行隊列中移出,轉而進入設備的等待隊列當中。
此時若設備準備就緒(如按下鍵盤),則操作系統會修改當前設備狀態,然后檢查等待隊列,將等待隊列中的PCB重新移動到運行隊列當中,該進程重新恢復運行狀態。
掛起狀態
? ? ? ? 當一個進程被暫停執行時,稱該進程處于掛起狀態。?那么它的具體體現是什么呢?
????????當內存空間較為吃緊時,操作系統會將一些暫時不需要使用的內存數據(如阻塞狀態的PCB控制的代碼和數據)喚出到磁盤中的swap交換分區。此時等待隊列中的PCB不再維護該進程的代碼和數據,這樣的進程狀態叫做阻塞掛起。
????????此時,若設備準備就緒,則操作系統就將swap交換分區中的代碼和數據重新喚入到內存中,給PCB維護,然后恢復到運行狀態。
????????當內存空間嚴重不足時,操作系統會將運行狀態的PCB控制的代碼和數據也喚出到swap交換分區。此時稱之為運行掛起。
由這三種狀態在代碼層面的一部分具體體現,我們可以得出如下結論:進程狀態的變化表現之一就是PCB在不同的數據結構之間移動,變化本質是操作系統對數據結構的增刪查改。
2. 內核鏈表的理解
? ? ? ? 之前提到,在Linux下,操作系統會使用雙向鏈表將PCB串聯起來,方便進程管理。那么為什么PCB還會出現在CPU維護的調度隊列當中呢?其實task_struct確實是同時出現在兩種數據結構當中的,它基于一種特殊的結構來實現:?
task_struct當中,將用于構成雙向鏈表的指針域封裝成一個結構體list_head,它的指針指向的是其他task_struct的list_head。那么既然指向另一個指針域,如何能訪問到task_struct的其他成員呢?這就需要用到結構體內存對齊的相關知識了:結構體的成員都是按照自身的對齊數進行存儲的,第一個成員變量的地址就是結構體的首地址。通過求出list_head相對于結構體第一個成員的偏移量,就能間接訪問結構體的其他成員。例如,如下表達式就可以表示next指針指向的list_head所在task_struct的首地址(其中links表示list_head的變量):
(struct task_struct*)(next - &((struct task_struct*)0->links))
將0強轉為task_struct*類型,求出成員links的地址,即為links的偏移量,然后用links的地址減去該偏移量,得出task_struct的首地址,再強轉為task_struct*類型,然后就可以訪問其他成員了。
? ? ? ? 而其他指針域也可以通過這種方式訪問task_struct的其余成員,但可以用不同的鏈接方式,形成不同的數據結構,這樣就實現了一個PCB同時存在于多種數據結構的壯舉。
3. Linux的進程狀態
? ? ? ? 相比于之前提到的操作系統大體上的進程狀態,Linux的進程狀態就顯得更加具體化。在Linux下,進程狀態本質是task_struct內的長整型變量,它有以下幾種進程狀態表示:
static const char *const task_state_array[] = {"R (running)", /*0 */"S (sleeping)", /*1 */"D (disk sleep)", /*2 */"T (stopped)", /*4 */"t (tracing stop)", /*8 */"X (dead)", /*16 */"Z (zombie)", /*32 */
};
R:運行狀態
S:休眠狀態(可中斷休眠)
D:深度睡眠狀態(不可中斷休眠)
進程處于深度睡眠狀態時,不可被殺。
T:暫停狀態--用戶手動暫停進程(如Ctrl + z)
t:追蹤狀態--調試過程中執行到斷點處,進程被暫停
x:死亡狀態
z:僵尸狀態--子進程在死亡之后,代碼和數據可以釋放,但其PCB不能直接釋放,需要被父進程讀取信息,讀取信息之前稱之為僵尸狀態。
注意:如果父進程一直都不讀取子進程的信息,那么僵尸狀態就會一直存在,PCB也會一直存在,這就導致了內存泄漏。
孤兒進程?
? ? ? ? 除了以上幾種狀態,進程還有一種特殊情況:孤兒進程。?當父進程先死亡,子進程就會被1號進程領養,成為新的父進程,此時該子進程就被稱作孤兒進程。
注:1 號進程(init 或 systemd) 是 Linux 系統中的第一個用戶態進程,負責初始化系統并管理其他進程。它由內核在系統啟動時創建,PID固定為 1。現代 Linux 主要使用 systemd 作為 1 號進程,提供服務管理、日志收集和系統控制功能,而早期系統則使用 sysvinit 或 upstart。如果 1 號進程崩潰,系統通常會進入不可用狀態,需要重啟。
? ? ? ? 那么為什么子進程會被1號進程領養呢?如果1號進程不領養它,則當子進程死亡后,沒有父進程讀取信息,就會造成內存泄漏。
總結
? ? ? ? 通過本篇文章,我們學習了Linux進程的基礎知識,包括進程概念、task_struct 結構、進程狀態以及父子進程關系,希望這篇文章能幫助你更清晰地理解Linux進程的運行機制。如果你覺得博主講的還不錯,就請留下一個小小的贊在走哦,感謝大家的支持???