前言:
????????上文我們講到了Linux下的第一個程序:進度條? ? ? ?【Linux】LInux下第一個程序:進度條-CSDN博客
? ? ? ? 本文我們來講一講Linux中下一個非常重要的東西:進程
1.馮諾依曼體系結構
我們所見的大部分計算機都是遵循的馮諾依曼體系結構
我們的計算機都是由一個個硬件所組成的
- 輸出設備:顯示器、音響、攝像頭、網卡.......
- 輸入設備:鼠標、鍵盤 、網卡.......
- 中央處理器(CPU):包含運算器、控制其等等等等......
對于馮諾依曼體系結構我們要注意以下幾點:
- 存儲器:其實就是我們所說的內存。相應的外存就是我們說所的磁盤
- 輸入與輸出(Input/Output,IO):輸入與輸出我們要站在內存的角度來看待,外設的數據流出內存叫做輸入,內存將數據交給輸入設備叫做輸出。
- CPU與內存:CPU在數據層面上只能直接訪問內存,并不能直接訪問硬件設備。所以一切軟件的運行都想要先將其加載到內存才行。加載的本質其實是Input,數據從一個設備“拷貝”到另一個設備。拷貝的效率決定了體系結構的效率。
- 軟件運行:軟件的運行是通過CPU執行我們的代碼,訪問我們的數據來得以實現的。
- 理解內存:假設沒有內存,CPU直接從輸入設備中拿去數據,再交由輸出設備。我們知道輸入設備與輸出設備的速度是遠遠的慢與CPU的。這就導致了不論CPU有多快都沒用,CPU始終要等著輸入設備的數據過來才能開始處理,這個設備的效率全部取決于了外設。這顯然是不合理的。
????????而內存的出現解決了,CPU與外設之間運算速度不匹配的弊端。內存會提前將輸入設備中的數據拿過來,盡可能的減少CPU與外設之間的速度差。
- 理解數據的流動:
2.操作系統(Operator System)
2.1基本概念
任何一個計算機都包含一個最基本的程序:OS(操作系統)
操作系統本質是一款用于管理軟硬件的軟件
廣義的操作系統包含:內核(進程管理、文件管理、內存管理、驅動管理)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 其他程序(外殼shell、函數庫等等等等)
狹義的操作系統包含:內核
2.2設計OS的目的是什么
對下:與硬件交互,管理軟件與硬件的資源(手段)
對上:為應用程序提供一個良好的運行環境(目的)
注意:
1.操作系統是封裝起來的任何人都無法訪問其內部,只能通過操作系統給用戶提供的接口(既系統調用)來執行功能
2.計算機上的任何操作都必須訪問操作系統,且只能通過調用系統接口實現。其接口本質就是函數,只不過是系統提供的。
3.軟硬件結構都為層狀結構
4.我們的程序只要是訪問了硬件(比如顯示器,磁盤)那它就必定會貫穿整個軟硬件體系結構
5.我們常用的庫函數:printf,顯示器上打印信息。它也訪問了硬件設置,這也就意味著這個庫函數底層封裝了系統調用
2.3理解操作系統的“管理”
????????在學校的管理體系中,校長是管理層,輔導員是執行層,而學生則是被管理者。校長擁有決策權,而執行校長的決定不可能由校長親自執行,而是輔導員來。
? ? ? ? 在計算機體系中,校長就相當于是操作系統。輔導員相當于是驅動程序。學生則相當于是底層硬件
“校長”應該如何管理?
? ? ? ? 校長要管理學生,但是校長不可能將想要管理的學生一個個都喊到辦公室來。校長與學生不必見面。更合理的做法是校長通過學生冊里面的信息來進行管理,做出的決定交由輔導員來執行。
1.管理者與被管理者不必見面
2.管理者如何進行管理?通過數據進行管理
3.既然不見面,那數據從何而來?通過中間層“輔導員”--->驅動程序獲得
“校長”管理方式進化之路
管理方式1.0:
通過表格來管理學生
管理方式2.0:
校長覺得表格管理太不方便了,于是想使用計算機來管理信息
先通過結構體來“描述”學生的信息,創建一個又一個對象來保存信息。
保存了眾多學生數據,又如何管理這些數據呢?答案是通過數據結構來管理
以上這種管理方式被我們稱作:先描述再組織
那么操作系統是如何管理驅動程序、進程或者其他東西呢?答案也是先描述:使用類先表示屬性,再組織:使用恰當的數據結構來進行管理。
2.4理解系統調用
1.操作系統要對上提供服務
2.操作系統不相信任何人
? ? ? ? 操作系統是為了我們更好的使用計算機。但操作系統是封裝起來的,任何人都不能訪問其內部,想要使用操作系統只能通過操作系統提供的“系統調用”。
? ? ? ? 操作系統就像銀行,你要存錢或者取錢都只能在銀行提供的ATM機或者窗口上辦理,銀行不可能讓你自己進到銀行金庫里取錢或者存錢。
? ? ? ? 不論是Linux、windows還是macOS這些常見的操作系統都是用C語言寫的,所以“系統調用”的本質其實是C函數,只不過是由操作系統提供。通過調用系統提供的C函數讓操作系統執行我們想要的操作。
? ? ? ? 僅僅有“系統調用”對于不太了解操作系統的人來說上手還是太困難了。所以為了我們普通人更好的使用操作系統,就有了我們所說的:庫、shell外殼、指令等等。這些都是在底層封裝了“系統調用”以便于我們更好的使用。這二者是上下層的關系
3.進程
3.1什么是進程?
我就不念叨書上晦澀難懂的定義了,看不懂也沒啥用。直接上圖!
? ? ? ? 上面我們說到了操作系統的管理方式是:先描述再組織。同樣的操作系統管理進程也是先使用結構體“描述”其中包含進程的所有屬性,再使用數據結構“組織”。
? ? ? ? 當我們運行我們的可執行程序時,代碼和數據會加載到內存中。此時操作系統會創建一個結構體,其中包含了test.exe的所有屬性。并且會有對應的指針指向對應的代碼和數據。
? ? ? ? 所謂進程其實就是:包含對應所有屬性的結構體對象+代碼和數據
? ? ? ? 補充:進程的創建規則是由父進程創建子進程
PCB
操作系統創建包含所有屬性的結構體有一個專業的名字叫做:進程控制塊。簡稱為PCB(process control block)? ? ? ??
? ? ? ? Linux操作系統下的PCB是:task_struct。是位于內核的一種數據結構,它會被裝在到RAM(內存)中記載進程信息。
task_struct
我們知道了task_struct是一個結構體包含了進程的所有屬性,那么大概有那些類型的屬性呢?
- 標示符:描述本進程的唯一標志(pid)用于區別其他進程
- 狀態:表示任務狀態,退出代碼,退出信號等等
- 優先級:相對于其他進程的優先級
- 程序計數器:程序中即將被執行的下一條指令的地址
- 內存指針:包含指向代碼和數據的指針,以及和其他進程共享內存塊的指針
- 上下文數據:進程執行時處理器的寄存器中的數據
- I/O狀態信息:包括顯示的I/O請求,分配給進程的I/O設備和被進程使用的文件列表
- 記賬信息:可能包含處理器時間總和,使用的時鐘總和,時間限制,記賬號等等
task_struct在linux系統中的組織方式是:雙向鏈表
3.2初見進程
首先我們先介紹一下我們初見進程認識的第一個系統調用:getpid
????????如圖,getpid的功能是獲得調用getpid函數的進程的標識符(簡稱pid)。包含在<unistd.h>庫中。無需傳參,返回pid_t類型的值(有同學可能比較疑惑pid_t是什么類型,其實這是由操作系統提供的類型與C語言中的int類型一致都是整型)
先編寫一個簡單的代碼和makefile
運行成功,得到具體的pid,證明當前確實是進程?
3.3查看進程
查看進程信息可以通過指令查看,也可以通過系統文件查看
指令查看
- 指令ps、指令top都可以查看進程信息
- ps? ajx可以一鍵查看全部進程信息,但為了看我們想看的進程可以使用管道+grep進行過濾
這樣就看見了我們之前執行的進程信息。這里同學可能會注意到為什么grep進程也被我們查出來了,這里簡單說一下:因為grep指令也是進程,執行test關鍵字過濾的同時gerp進程中也有test關鍵字,所以被一起查出來了。
? ? ? ? 我們看見了一長串的進程信息但是我們不知道這些信息分別代表什么含義。所以我們可以使用:head -1指令,讓其顯示進程信息的第一行內容(&& 或者 ;同時執行左右兩個指令)
- top指令也可以查看進程信息,不過top是實時更新的
文件查看
- 進程信息可以通過?/proc?的系統文件進行查看
可以指定具體的進程查看其詳細內容?
?補充:
在上圖我們看見就兩個高亮的字符:cwd和exe,并且后面都跟了一個地址。
cwd:表示當前工作路徑(current work dir)。進程在啟動時會記錄下自己當前所在的路徑
exe:進程在啟動時會記錄可執行程序所在的路徑
系統調用:chdir可以修改進程的cwd
殺死進程
殺死進程有兩種方式
- ctrl + c:無腦殺死當前臺運行的進程
- kill -9? pid:殺死指定pid的進程
3.4父進程
上面我們講到了getpid,不知道大家有沒有注意到在這個圖里面還有一個函數叫做getppid
? ? ? ? getpid是獲取當前進程的pid,而getppid這是獲取當前這個進程的父進程的pid。
? ? ? ? 注:linux中進程的創建都是由父進程完成的。
? ? ? ? 我們可以通過代碼來看看ppid
????????通過不斷的執行殺死再次執行,我們可以發現,子進程的pid是不斷變換的,而父進程的pid則是一直不變的。我們可以通過查詢父進程的pid來看看父進程到底是什么東西
? ? ? ? 查詢之后我們發現父進程是一個bash
? ? ? ? bash其實就是我們所說的命令行解釋器(補充:os會給每一個登錄用戶都分配一個hash,如果同時登錄3個用戶就會分配3個bash)
? ? ? ? bash是命令行解釋器,那其實也是一個進程,我們執行的命令也是一個進程。從這里我們就可以知道,我們輸入指令執行對應的進程,都是由bash這個父進程來創建的。
4.創建進程
在上面我們知道了,我們輸入指令執行的進程,都是通過hash這個父進程來進行創建的。那么父進程是如何創建子進程的呢?
4.1如何創建進程
創建進程主要通過系統調用:fork來實現
先上代碼,看看具體效果
#include <stdio.h>
#include <unistd.h>int main()
{printf("執行父進程:%d\n",getpid());fork();printf("執行進程:%d\n",getpid());
}
? ? ? ? 我們可以看見出現兩個不同的pid,這說明了子進程確實創建出來了。
? ? ? ? 但是為什么會有3個輸出呢?我們接著往下看。
4.2fork相關問題
進程創建具體邏輯
? ? ? ? 我們知道進程是由PCB+代碼和數據組成的。父進程創建子進程時,會將自己的PCB數據拷貝給子進程,然后子進程在進行相應的修改(所以父子進程的PCB數據大部分都是一樣的)。在沒有新代碼加載進來時,子進程是默認與父進程共享代碼和數據的(我們的代碼就沒有加載新數據)
? ? ? ? 我們的代碼再第一個輸出后創建了子進程,而子進程是和父進程共享代碼的。父進程繼續向下執行第二個printf輸出對應內容,子進程不會執行已經執行過的代碼,只會執行還沒有執行的代碼。所以子進程執行第二個printf輸出對應內容。也就是我們所看到了最后輸出。
fork返回值
RETURN VALUE
? ? ? ?On success, the PID of the child process is returned in the parent, and 0 is returned in the child. ?On failure, -1 is returned in the parent, no child process is ?created, ?and? errno is set appropriately.
? ? ? ? 以上是關于fork返回值的文檔描述。我們可以看到:如果創建成功,將會返回子進程的pid給父進程,返回0給子進程。如果創建失敗則返回-1給父進程。
? ? ? ? 看到這里我們可能會很震驚,fork居然會返回兩個值嗎??但事實上確實是的!我們可以通過代碼來驗證一下。
#include <stdio.h>
#include <unistd.h>int main()
{pid_t pid = fork();if(pid<0){ printf("創建失敗");} else if(pid == 0){ //子進程printf("我是一個子進程:%d,這是我的父進程:%d\n",getpid(),getppid());} else if(pid > 0){ printf("我是一個父進程:%d,這是我的父進程:%d\n",getpid(),getppid());}
}
? ? ? ? 執行代碼,我們可以看到同時輸出了父進程和子進程,這就說明了fork確實是返回了兩個值。
? ? ? ? 但與此同時,我們心中的疑問可能更多了。
為什么fork要給父子進程各自返回不同的值?
? ? ? ? 因為子進程是由父進程創建的,父進程可能同時擁有多個子進程,為了區分不同的子進程,父進程需要子進程的pid。
為什么fork會返回兩個返回值???
? ? ? ? fork函數的功能是創建子進程,執行fork函數時,當子進程創建的一系列動作完成之后,才會返回pid。
? ? ? ? 也就是說返回pid之前子進程就已經創建完成了,并且已經放入調度隊列中開始執行了!上面我們講到過子進程只會執行還沒有執行過的程序,而恰恰return pid就是還沒有執行的程序!所以父進程執行return,子進程也會執行return。
? ? ? ? 這就是為什么fork會返回兩個返回值的原因。
為什么同一個變量既滿足等于0,又滿足大于0?
先說結論:因為進程具有獨立性,互不影響
? ? ? ? 我們可能疑惑雖然說fork返回了兩個值,但是都是返回給變量pid。按我們以前的理解,pid應該會進行覆蓋,最后只會滿足一個條件。但是為什么兩個if條件都滿足呢?
? ? ? ? 首先我們知道,在我們當前這個代碼中,父子進程是共享代碼和數據的。共享代碼自然是好理解的,父子進程對代碼只有讀權限沒有寫權限,是不能修改代碼的。
? ? ? ? 但是數據呢?假設父進程中有變量a為10,但是如果子進程中要對a進行修改的話,豈不是亂套了?
? ? ? ? 所以在不修改數據的情況下,操作系統默認是父子進程共享數據的。但當要修改數據時操作系統就進行“寫時拷貝”。具體是將需要修改的數據在底層拷貝一份,讓目標進程修改拷貝的變量。
? ? ? ? 所以雖然我們這里的父子進程代碼是共享的,但是當fork返回不同的值時,操作系統會進行“寫時拷貝”,形成父子進程獨立的變量pid。父進程執行代碼讀取的數據與子進程執行代碼讀取的數據是不同的。
? ? ? ? 所以最后我們才會看到同時執行了兩個輸出
由此便解決了我們上述代碼中的所有問題