目錄
前言
什么是進程
如何管理進程
描述進程
組織進程
如何查看進程
通過 ps 命令查看進程?
通過 ls / proc 命令查看進程
通過系統調用 獲取進程標示符
前言
在學習了【Linux系統編程】中的 ??操作系統??和?馮·諾依曼體系結構??之后,我們已經對系統應該有了不錯的了解,接下里我們將繼續深入的了解操作系統最重要的的功能之一:進程
什么是進程
首先我們來看一個問題:
操作系統能不能一次運行多個程序呢?
答案是當然可以的!!因為運行的程序有很多,所以 OS 需要將這些運行的程序管理起來。
我們將這些正在運行的程序稱之為進程。(注意:是正在運行的程序叫進程,而不是程序本身)
【課本概念】:程序的一個執行實例,正在執行的程序等
【內核觀點】:擔當分配系統資源(CPU時間,內存)的實體
對于課本中的觀點大家可能會覺得難以理解,為何正在執行的程序就是一個進程呢。我們可以在Windows下按[Ctrl + shift?+ ESC]
打開任務管理器查看一下:
這也就表明了在一個操作系統中不僅只能運行一個進程,還可以運行多個進程?
但是呢,進程不僅僅可以像上面這樣去理解。我們來思考一個問題
程序是文件嗎?
相信讀者肯定很清楚,文件是存放在磁盤中的,磁盤呢則是屬于外設。這一塊我們在 馮·諾依曼體系結構?有講得很清楚,對于CPU來說,它是只會和內存打交到的,所以磁盤中的數據需要先加載到內存中才可以被執行
那么,當可執行文件被加載到內存中時,該程序就成為了一個【進程】
總結:進程 = 程序(代碼 + 數據) + 內核申請的與該進程對應的數據結構(PCB)。
如何管理進程
你說正在運行的程序叫做進程,那進程可以同時進行嗎?
- 我們可以發現當你在聽網易云的時候你也可以登錄微信聊天,也可以刷抖音 所以進程是可以同時進行的。是因為操作系統把這些進程給管理起來了。也就是先描述再組織
- 操作系統會創建一個描述和控制該進程的結構體。這個結構體稱之為進程控制塊(PCB,Processing Control Block),里面包含了該進程幾乎所有的屬性信息,同時通過進程控制塊也可以找到該進程的代碼和數據。
- 在 Linux 中,進程控制塊就是 struct task_struct 結構體。
- 描述好所有進程了,還需要將所有進程的 PCB 給組織起來(通過雙鏈表的方式),此時操作系統只需要拿到雙鏈表的頭指針,就可以找到所有進程的 PCB。
- OS 把對進程的管理就轉換成了,對數據結構中 PCB 的管理,即對雙鏈表的增刪查改操作。
假設這里有一個可執行程序?test
,它存儲在磁盤上,就是一個普通文件,當我們?./test
?運行此程序,操作系統會做以下事情:將該程序從磁盤加載到內存中,并為該程序創建對應的進程,申請進程控制塊(PCB)。?
描述進程
先思考:人是如何辨別事物或者對象的??
比如你在放學路上見到一個女生一見鐘情,于是你記住了他的樣貌,開始像別人打聽這個女生,因為你不認識這個女生,所以你會對他進行描述,比如說長得很漂亮、水靈的眼睛、瓜子臉……當你提供的特征越來越多的時候,認識她的人或許就能通過你的描述找到這個女生。而這個過程中,這個女生的各種特征其實就是他的屬性,所以我們可以得出一個結論:人是通過屬性去辨別事物和對象的,當屬性足夠多的時候,這一堆屬性的集合,就是目標對象!!
?
所以我們推斷出,任何一個進程加載到內存時,OS需要創建一個描述進程的結構體對象——PCB(process ctrl block進程控制塊)?,而他的本質就是對進程屬性集合的描述!!
- 課本中的叫法是:PCB(Process Control Block)
- Linux操作系統下的PCB是:task_struct
這個結構體呢就是組織了各種各樣的屬性,才可以去很好地描述一個進程
task_struct的內容:
標示符: 描述本進程的唯一標示符,用來區別其他進程。(有點類似學校里每個學生的學號,是一個唯一標識,方便我們通過標示符來管理進程)
狀態: 任務狀態,退出代碼,退出信號等。(OS中同時存在多個進程,所以可能有的進程正在運行、有的正在休眠、有的在正在待定、有的即將銷毀……也就是說每個進程當前可能都處于某一種狀態)
優先級: 相對于其他進程的優先級。(OS中有多個進程,所以先執行誰肯定是要有一個標準的,所以進程之間可能存在對應的優先級關系)
程序計數器: 程序中即將被執行的下一條指令的地址。(以前我們在學習函數棧幀的時候,我們知道代碼是從上往下運行的,但是這個過程中可能會遇到出現某個函數需要我們進行跳轉,這個時候當前的棧幀會暫時保存著,然后當跳轉過去的相關代碼執行結束后再返回之前棧幀的位置繼續運行。但是由于OS中不僅僅只有一個進程,所有有可能這個進程在執行的時候可能會被一些切換給中斷,轉而去執行別的進程,然后該進程可能會進入休眠模式,而后期我們可能還會去喚醒這個進程,這個時候由于之間的棧幀被銷毀了,所以已經不記得執行到哪句代碼了,因此程序計數器存在的意義就是幫助沒我們記住即將被執行的下一條指令的地址!舉個更好理解的例子就是,比方說你正在數一堆書,當你數到50的時候,這個時候突然一個電話告訴你外賣到了,為了不讓外賣員等太久,你需要暫停當前的工作馬上下去,但是你又怕你數過的數字忘記了,所以你就把他記在本子上,當你取完外賣后,你就可以通過從本子上的數字繼續往下數!)
內存指針: 包括程序代碼和進程相關數據的指針,還有和其他進程共享的內存塊的指針(我們一個可執行程序要運行還需要有對應的數據和代碼,所以PCB對象必然需要有一個指針指向這塊空間,當進程響應的時候能夠及時找到,另一方面可能會存在多種數據類型的指針,為了滿足不同場景下的需求——通過數據結構和算法)
上下文數據: 進程執行時處理器的寄存器中的數據[休學例子,要加圖CPU,寄存器]。
I/O狀態信息: 包括顯示的I/O請求,分配給進程的I/O設備和被進程使用的文件列表。
記賬信息: 可能包括處理器時間總和,使用的時鐘數總和,時間限制,記賬號等。(可能會包含進程的一些運行時間,其實對進程的調度來說是有作用的,因為在多個進程的情況下,只有一個CPU,所以先將哪個進程放到CPU里其實是由調度器決定的,而調度器除了考慮進程狀態和一些優先級之外,他會盡可能秉持著公平的原則,比如說有盡可能地優先讓執行時間短的進程優先去調度。)
其他信息
轉換為代碼形式的話就可以是下面這樣
struct PCB{進程的編號進程的狀態進程的優先級...相關的指針信息
};
既然知道了如何去描述一個PCB結構體,我們就要來知道操作系統對一個進程總共會做哪些事情
- 為該進程創建對應的PCB對象
- 將該進程的代碼和數據加載到內存中
所以,很多教科書在介紹進程的時候只會說它在計算機內部是一個PCB對象,其實對于一個進程來說:應該是由操作系統為其創建出來的?PCB對象 + 其數據代碼?組成的
組織進程
我們知道?進程=內核PCB數據結構對象+你自己的代碼和數據。
- 但是OS本質上是對PCB做管理,他并不關心你的代碼和數據,因為他只要能找到PCB,就可以通過他里面的一個相關的指針去找到對應的代碼和數據,然后再交給CPU去運行!!
- 舉個例子就比如HR對人才的管理本質上就是對簡歷進行管理,然后安排面試的時候再通過簡歷來找到你的相關信息。
但是PCB特別多,所以我們需要想辦法管理起來。
其實在我們的Linux中task_struct主要是以雙鏈表的形式組織起來。
??
?你可能會疑惑,使用一個順序表來存儲不是更好嗎??其實在OS內部對于進程的管理方式并沒有像我們以前學的數據結構那么純粹,他的場景會更加復雜,也就是說該進程可能會需要根據不同的需求被存儲在隊列中、雙鏈表中、二叉樹中、棧中……
所以將進程按照節點的方式鏈接起來其實會更方便我們將這個進程放在不同的數據結構中,然后我們可以通過對應的指針信息來講他們更好地管理起來。
舉個例子,比如說我在當前進程中有一個隊列指針。因為在OS中可能會有一些存儲進程指針的運行隊列和等待隊列,如果你想讓這個進程去哪個隊列,你就可以通過修改隊列指針的鏈接隊形做到,從而實現更加靈活的管理。?
所以對進程管理工作取決于你把他放入哪個正在被組織的數據結構中,因為不同的數據結構有不同的特點,所以背后對應的就是不同的算法,而不同的算法對應的就是不同的應用場景。
如何查看進程
現在我們明白了操作系統如何去描述并組織進程,接下去我們就切身地來看一看進程長什么樣吧
下面呢是我們要進行測試的代碼:
#include <stdio.h>2 #include <stdlib.h>3 #include <unistd.h>4 #include <sys/types.h>5 int main()6 {7 // 死循環8 while(1)9 {10 pid_t id = getpid();11 //pid_t fid = getppid();12 //printf("I am a process! pid : %d, ppid : %d\n",id,fid);13 printf("I am a process! pid : %d\n",id);//程序停留一秒在輸出 14 sleep(1);15 }16 17 return 0;18 }
通過 ps 命令查看進程?
??
查看當前我們正在運行的進程
??
此時我們來查看以下當前運行程序的進程
ps ajx | head -1 && ps ajx | grep mytest
?ps ajx
?—— 查看當前系統中所有進程head -1
?—— 獲取第一行grep mytest
?—— 過濾只帶【mytest】的進程?
??
那有同學可能會問:為什么在過濾進程的時候會有 grep --color=auto mytest 這個東西呢?
當grep
在進行過濾的時候自己也要變成一個進程,也可以看到他們使用grep
命令的時候也帶【mytest】關鍵字的,所以在過濾的時候把自己也過濾出來了。
這也側面證明了所有指令在運行的時候都是進程
但如果我們不想看到這個也是有辦法的,那就是在?grep?命令后面加個-v grep
把其過濾掉即可
ps ajx | head -1 && ps ajx | grep mytest | grep -v grep
通過 ls / proc 命令查看進程
我們都清楚根目錄下有很多的路徑:
注意上面的proc目錄,它是一個內存文件系統,里面放的是當前系統實時的進程信息。我們進入此目錄看看:
但是呢,上面這些呢是全部的進程,若我們只是想要查看某個進程的話就要根據其PID
值去進行對應的查找。這個PID
呢就是我們在上面在介紹task_struct
這個Linux下的PCB結構體的時候所講到的【標識符】這個東西,它是?描述本進程的唯一標示符,用來區別其他進程
但是這么看不夠清晰,我們以列表的形式來進行查看。這里我們主要關注兩個:一個是cwd目錄,另一個則是exe
首先我們來看到的是這個【exe】,很明顯它是一個可執行文件,那就是我們在當前目錄下的mytest
這個可執行文件
接下去的話就是這個【cwd】了,其意思為current work directory
當前進程的工作目錄
接下來,我們來詳細說一這個??cwd
看以下的代碼:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main(){printf("我的PID:%d\n",getpid()); FILE* fp = fopen("log.txt","w"); if(fp==NULL){return 1;}fclose(fp);printf("新建文件完成\n");sleep(50);return 0;}
在當前的 proc.c 程序中,創建一個log.txt 的文件,創建好后,大家覺得這個文件會出現在哪里呢? 我們運行以下看看
我們發現在?proc.c ?程序中創建的 log.txt ?文件出現在當前目錄下,這是為什么呢?
我們來根據程序中的進程標識符來檢查一個這個進程
此時我們發現進程中的存有cwd---當前工作目錄,所以導致在此程序中創建的任何文件,在沒有指定放在哪里的時候,常見的文件會默認放在進程指定的cwd----當前工作目錄下
通過系統調用 獲取進程標示符
上面我們有講到了這個PID
進程標示符,是通過ps
這個命令來查看的,那我們能否直接獲取這個PID
呢?
- 在上面我們使用
ps ajx
查看到了當前進程所對應的?PID,但是呢這相當于是遍歷操作,如果我沒有加grep mytest
的話出來的進程數就會很多了
- 那現在我們所要做到的就是對一個單獨的進程去獲取其 PID,此時我們能想到的就是通過庫函數來實現。在之前的文章中我們又說到過對于操作系統而言它是不會相信任何人的,所以會提供給用戶一些系統調用(庫函數),那我們只需要通過這個系統調用即可獲取到當前進程的 PID 值
- 那首先呢,我們先要去查詢一下這個
getpid()
怎么使用,那還是使用到我們的老朋友man
man 2 getpid
?進去之后看到,有兩個庫函數,那如果要使用這兩個庫函數的話就需要引入對應的頭文件
下面我給出一段命令,它可以實時監控當前系統的進程
while :;do ps ajx | head -1 && ps ajx | grep mytest | grep -v grep; echo "------------------------------------------------------------"; sleep 1; done;
- 然后就讓我們來觀察一下其是否真的可以獲取到當前進程的PID,首先運行上面的這段指令,我們看到了當前系統中并不存在有關mytest的進程,但是呢在我們把mytest這個可執行程序運行起來的時候,右側就突然就多出了一條進程的相關信息
- 后一核對相關的PID值就發現確實是當前運行起來的這個進程
- 但是呢當我在將當前這個進程給結束之后再去把它起起來的時候,就發現當前這個進程的
PID
值發生了變化
?其實的話,這個現象是很正常的,每次重新啟動進程其 PID 值是會出現不同的情況
舉個很簡單的例子來說吧,小王在高考結束完后上了一所不是很理想的大學🏫,在開學前兩天時學習為其分配了對應的學號。但是呢小王卻并不滿意自己所待的這個學校,所以就退了學繼續參加高考,在又一次的高考結束后他還是被原來的這所學校給錄取。但是呢我們可以知道,即使你進了一個學校兩次,但是學號卻不一定是一樣的
這也就是為什么一個進程在啟動兩次后會出現不同PID值的原因
?剛才我們在通過【man】手冊查看getpid()
這個函數的時候,還看到了getppid()
這個函數,它是獲取當前進程的父進程的 PID
- 這個
PPID
呢就在PID
的左邊
下面是改進的測試代碼
printf("I am a process, my id is: %d, parent is: %d\n", getpid(), getppid());
- 馬上來看一下是否真的可以獲取到
- 接下去我們再來觀察一下現象:通過3次結束子進程,我們觀察到了子進程確實每次都會發生變化,但是呢對于父進程而言卻不會發生任何的變化,這是為什么呢?
- 我們可以先去查看一下這個父進程到底是什么鬼…(((m -__-)m
ps ajx | head -1 && ps ajx | grep 18866
原因解析:
每次在登錄XShell的時候,系統會為我們單獨再創建一個Bash進程,即命令行解釋的進程,幫我們在顯示器中打印出對話框終端
[XAS@iZf8z3lh8un7rc5rk1ney3Z lesson13]$ ----- 【父進程----ppid】
我們在命令行中輸入的所有指令都是Bash進程的子進程,Bash進程只負責命令行的解釋,具體執行出問題的時候只會影響它的子進程
ls /proc/18866 -- 子進程【PID】
上面這樣解釋可能還是比較抽象,一樣來舉個例子
- 還記得,我們在講解shell運行原理的時候曾經說到過王婆是一位資本家,她為了不損壞自己的名聲呢,在別人找她說媒的時候會派遣一些實習生去,即使實習生出了問題她的名譽也不會受到影響
?這就可以對照到父進程與多個子進程,可以有多個子進程在這個父進程上面運行,即使某一個子進程突然出問題終止了,還有其他子進程在運行,此時父進程也是有保障的,所以子進程每次都會發生變化,但是父進程永遠都不會變化