一、基本概念
書本上的概念:程序的一個執行實例,正在執行的程序等
基于內核的觀點:擔當分配系統資源(CPU時間,內存)的實體。?
我們知道,我們在寫代碼的時候,你的代碼進行編譯鏈接后生成可執行文件,這個文件就在磁盤當中,當我們雙擊這個文件之后,該文件就被加載到了內存之中,因為只有加載到了內存之中才能被cpu逐語句執行。而加載了內存中的程序,不再是程序而應該是進程。?
二、描述進程——PCB
實際上我們在系統當中有很多的進程,我們可以通過ps aux命令來查看:
我們知道操作系統是第一個被加載到內存的,而操作系統就是做的管理工作的,那么操作系統是怎么做到管理的呢?
這里就用到了之前在談操作系統時候提到的六個字:先描述,再組織。操作系統作為管理者,是不需要與進程直接交互的,當進程到來時,操作系統需要對進程進行描述,那么對進程的管理就變成了對描述的管理。進程的描述信息會被放到一個叫進程描述塊之中,官方稱之為PCB(process control block)。
操作系統將每一個進程都進行描述,形成了一個個的進程控制塊(PCB),并將這些PCB以雙鏈表的形式組織起來:
這樣一來,操作系統只要拿到這個雙鏈表的頭指針,便可以訪問到所有的PCB。此后,操作系統對各個進程的管理就變成了對這條雙鏈表的一系列操作。
例如創建一個進程實際上就是先將該進程的代碼和數據加載到內存,緊接著操作系統對該進程進行描述形成對應的PCB,并將這個PCB插到該雙鏈表當中。而退出一個進程實際上就是先將該進程的PCB從該雙鏈表當中刪除,然后操作系統再將內存當中屬于該進程的代碼和數據進行釋放或是置為無效。
總的來說,操作系統對進程的管理實際上就變成了對該雙鏈表的增、刪、查、改等操作。?
1.task_struct——PCB的一種
因為Linux是拿C語言寫的,那么這里的task_struct其實是一個結構體。
1)在Linux中描述進程的結構體叫做task_struct。
2)task_struct是Linux內核的一種數據結構,它會被裝載到RAM(內存)里并且包含著進程的信息。
2.task_struct的內容分類
主要內容:
- 標示符:描述本進程的唯一標示符,用來區別其他進程
- 狀態: 任務狀態,退出代碼,退出信號等。
- 優先級:相對于其他進程的優先級。
- 程序計數器:程序中即將被執行的下一條指令的地址。
- 內存指針:包括程序代碼和進程相關數據的指針,還有和其他進程共享的內存塊的指針
- 上下文數據: 進程執行時處理器的寄存器中的數據[休學例子,要加圖CPU,寄存器]。
- 1/0狀態信息:包括顯示的I/0請求,分配給進程的!/0設備和被進程使用的文件列表。
- 記賬信息:可能包括處理器時間總和,使用的時鐘數總和,時間限制,記賬號等。
- 其他信息
三、查看進程
1.通過系統目錄查看
我們除了上面的,通過ps aux命令查看以外,還可以在根目錄下找系統文件夾proc。
我們打開文件夾,可以看到一些以數字命名的目錄
這些數字就是我們之前所說的PID,在對應的目錄中記錄了進程的相關信息,比如我們查看1的文件內容:
2.通過ps命令查看
單獨使用ps:
ps aux
ps結合grep可以看到更加標準的進程信息:
四、通過系統調用獲取進程的PID和PPID?
這里我們要用到兩個調用函數,分別是getpid()和getppid()來分別獲取PID和PPID。
我們可以寫個代碼來測試一下:
輸出:?
我們也可以看一看進程的信息是不是和我們調用函數獲取的一致
五、通過系統調用創建進程
1.fork函數創建子進程
fork函數是一個系統調用函數,他可以創建一個子進程:
例如:
運行結果如下:
運行結果中的第一行數據是該進程的PID和PPID,第二行是、fork函數創建的PID和PPID,不難發現fork函數創建的進程的PPID就是proc的PID,也就是說proc進程和fork創建的進程是父子關系。
沒出現一個進程,操作系統都會為其創建PCB,fork也不例外。
我們知道加載到內存的代碼和數據是父進程的,那么fork創建的子進程的代碼和數據是哪里來的呢?
我們來寫個代碼來看看:
運行結果:
實際上,fork函數創建子進程,在fork函數之前的代碼要被父進程執行,而fork()之后的代碼默認是父子進程都可以執行的。
敲黑板:
1)這里雖然是父子進程代碼共享,但是父子進程各自開辟空間(寫時拷貝)。
2)這里面可能大家都會有一個疑問,那就是這里父子進程的執行順序是什么樣的,其實這里執行的順序完全是不確定的,取決于操作系統的調度。
2.使用if來引出問題
我們在之前說了,在fork()函數之后的父子進程共享代碼,那么這樣一來,我們創建子進程就沒有了意義,實際上使用的時候是要使用if來分流的,也就是父子進程去做不同的事情。
fork的返回值:
1.如果子進程創建成功了,在父進程中返回子進程的PID,而在子進程中返回0。
2.如果子進程創建失敗,則父進程中返回。
既然子進程創建的返回值不一樣,那么我們就可以通過這個性質來分流。
代碼如下:
運行結果:
六、Linux的進程狀態
進程從創建開始到被系統清理消亡的這個時間里,有時會占用CPU,有時在等待CPU分配資源,從這里看,進程是不同于程序的,進程有著自己動態變化的狀態。下面的這個圖就是我們待會要說明的。
我們可以看看在Linux中的進程狀態的內容,下面是Linux內核的部分源代碼:
/*
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*/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 */};
敲黑板:
這里的進程狀態實際上是保存在進程控制塊(PCB)中的,而在Linux中就是保存在了task_struct中的。
?在Linux中我們可以使用ps aux和ps axj來查看進程的狀態。
ps aux
ps axj
1.運行狀態——R
這個狀態(Runing)不一定就是進程處在運行當中,也有可能是在運行隊列當中,也就是說系統中可以同時有多個處于R狀態的進程。
敲黑板:
所有處于這個狀態的進程,都是可以被調度的,他們在運行隊列當中,在操作系統需要切換進程時,可以在這個里面直接選取。?
2.淺度睡眠狀態——S?
一個進程處于淺度睡眠狀態(sleeping),意味著該進程在等待事件的完成,這時的進程是可以被隨時喚醒的(和人一樣),也可以被殺亖(由于這個原因,這個狀態也被叫做可中斷睡眠(interruptible sleep)。
我們可以寫個代碼來演示一下:
這里我們讓程序休眠100秒來模擬進程處于淺度睡眠狀態。
ps aux | head -1 && ps aux | grep test | grep -v grep
之前說過處于這個狀態的進程是可以被殺亖的,我們來看看:
3.深度睡眠狀態——D
一個進程處于深度睡眠狀態(disk sleep),表示這個進程是不可以被殺亖的,即使是操作系統也不行,只能該進程自動喚醒才可以恢復。所以這個狀態也可以被叫做是不可中斷睡眠(uninterruptible sleep),處在這個狀態的進程通常需要等待IO的結束。
其實我們通過他的英文也可以知道,他肯定適合磁盤(disk)相關的,具體的場景就是要向硬盤中寫數據,這個過程是不能被殺亖的,因為我們等待磁盤的回復(是否寫入完畢)來做出反應(磁盤處于休眠狀態)。
4.暫停狀態——T
在Linux中我們可以發送SIGSTOP信號讓進程處于暫停狀態(stopped),發送SIGCONT信號讓處于暫停狀態的進程重新運行起來。
例如:
我們發送一個SIGSTOP信號給test讓進程處于暫停狀態。
然后我們再發送一個SIGCONT信號讓進程重新運行,這里運行的時候盡量快一點,因為時間一代程序就結束了:
補充說明:
我們可以使用kill -l命令來列出命令集
kill -l
5.僵尸狀態——Z
一個進程在要退出時,在系統層面,并不是我們想的直接就釋放資源,而是會保存一段時間,來供操作系統或父進程讀取退出信息,如果沒有讀取到相關的退出信息,那么數據也不會被釋放,一個進程在等待數據被釋放的過程就是處于僵尸狀態(zombie)。
我們通過上面的描述也能知道僵尸狀態其實是很必要的,因為進程就是去被指示去做事情的,那么指示方就應該要知道被指示方的完成情況,而僵尸狀態就是指示方來獲取完成情況的。
例如:我們之前一直在寫的return 0;實際上這個0就是返回給給操作系統的,讓操作系統知道我們完成的情況。在Linux中我們可以通過打印$?來獲取最近一個進程的退出碼。
echo $?
敲黑板:
和運行狀態一樣,這個退出碼也是被保存在PCB中的,在Linux中就是在task_struct中。
6.死亡狀態?
死亡狀態就是一個理論上的返回狀態,當一個進程的退出信息被讀取后,該進程申請的資源就被釋放掉了,那么該進程也就不存在了,所以我們不可能在我們列出來的信息中看到死亡狀態。