目錄
- 一、馮諾依曼體系
- 二、作業系統概念
- 三、行程
- 3.1 行程概念
- 3.2 描述行程-PCB
- 3.3 行程狀態
- 四、行程地址空間
一、馮諾依曼體系
我們先來看一下日常生活中,我們所用的電腦或者公司用的服務器都遵守馮諾依曼體系

由五種組成,分別是輸入設備,存盤器,運算器,控制器和輸出設備
輸入單元:滑鼠,鍵盤,掃描儀,寫板,網卡,磁盤,話筒等
存盤器:也就是物理記憶體,后續講行程地址會詳細講
運算器和控制器:都屬于CPU,運算器在CPU中會做兩件事一是執行算術,二是執行邏輯
輸出單元:顯示幕,網卡,磁盤,音響
輸入和輸出設備統稱為外設,其次CPU并不和外設打交道,同樣的外設只和記憶體打交到
程序:輸入單元輸入的是資料,資料寫入存盤器進行處理(運算器和控制器)然后再傳出輸出設備,因此馮諾依曼規定了硬體層面上的資料流向
那么從馮諾依曼體系的角度來解釋一道題:
小明在廣東給遠在安徽的小紅發了一句在嗎?那么請問發送了這個“在嗎?”資訊經過了哪些步驟?
ans:輸入在嗎?通過輸入設備,然后存盤到存盤器,qq軟體運行的時候也跑在存盤器上,通過加密運算等程序再寫回到存盤器然后qq再把資料重繪出去,到輸出設備,此時的輸出設備叫網卡,通過網路朋友家接收資料的輸入設備此時是網卡,不是鍵盤然后網卡的資料首先也會貫道記憶體里,qq在記憶體中獲得訊息,經過CPU運算解包再寫到存盤器里,然后存盤器再定時的刷到顯示幕上,朋友最終收到了訊息
二、作業系統概念
作業系統是進行軟硬體資源管理的軟體,管理硬體
用struct結構體進行描述,用鏈表或其他高效資料結構進行組織,作業系統包括以下:
- 內核(行程管理,記憶體管理,檔案管理,驅動管理)
- 其他程式(例如函式庫,shell程式等)
為什么會有有作業系統?
3. 可以減少用戶使用計算機的成本
4. 對下管理好所有的軟硬體,對上對用戶提供一個穩定高效的運行環境
三、行程
3.1 行程概念
那么什么是行程?
行程 = 你的程式 + 內核申請的資料結構(PCB),程式加載到記憶體里,作業系統為此創建PCB結構體變數然后里面填上該行程的屬性資訊最終完成行程創建
舉個例子,當我們啟動一個google瀏覽器,或者進行下載等,程式一旦運行就是行程,
行程是資源分配的最小單位,且每個行程擁有獨立的地址空間
那么問題來了!!
如果我們開啟了幾十個行程呢?對于作業系統來說就必須要進行管理
正如上面所提到的我們對于每個行程都遵守先描述再組織的原則,先描述就是為每個行程創建一個行程控制塊PCB,行程資訊被放在行程控制塊的資料結構中,然后一個個的行程由鏈表或其他高效資料結構進行組織
3.2 描述行程-PCB
那么我們就詳細了解一下PCB,可以理解為行程屬性的集合,就好像在自然界中我們看到一個動物會通過它的特征進行描述組織從而精準的判斷,
在Linux作業系統下的描述行程結構體叫做task_struct
task_struct是linux內核中的一種資料結構,被裝在到記憶體里并且包含著對應行程的資訊
- 標示符: 描述本行程的唯一標示符,用來區別其他行程,也就是我們在Linux下看到的PID
我們可以grep一個行程看到
- 狀態: 任務狀態,退出代碼,退出信號等,
- 優先級: 相對于其他行程的優先級,
- 程式計數器: 程式中即將被執行的下一條指令的地址,
- 記憶體指標: 包括程式代碼和行程相關資料的指標,還有和其他行程共享的記憶體塊的指標
- 背景關系資料: 行程執行時處理器的暫存器中的資料[休學例子,要加圖CPU,暫存器],
- I/O狀態資訊: 包括顯示的I/O請求,分配給行程的I/O設備和被行程使用的檔案串列,
- 記賬資訊: 可能包括處理器時間總和,使用的時鐘數總和,時間限制,記賬號等
注意:行程放在CPU上后,不是一直在運行知道行程結束的
一般行程讓出CPU有兩種情況
- 來了一個優先級更高的行程
- 時間片到了
并發和并行的概念:
單CPU:跑起來多個行程,通過行程快速切換的方式,有個時間片,在一段時間內給行程運行一段固定的時間片時間,然后給下一個,從而實作并發
多核CPU:任何時刻,允許多個行程同時執行,并行
行程間切換:
最重要的就是保存背景關系,把臨時資料保存在PCB中,讓出給其他的行程
如果有4個行程被切,是出于運行狀態的,所以我們的作業系統會給每個CPU形成對應的 運行佇列,通過運行佇列把所有的PCB鏈接起來(也是用全域的鏈表連接起來,因為PCB里面包含了大量的指標,和檔案等其他事務關聯),這些佇列是CPU所系結的運行佇列,CPU拿任務時就從這個佇列中去拿任務,狀態都為 R 狀態

作業系統進行行程調度,CPU執行
總結:
- 我們的行程進行切換時,因為某些原因如時間片到了,或者遇到了一個優先級更高的行程,當前正在運行的行程需要保存背景關系資訊,然后讓出CPU,另一個新到來的行程需要將自己的資料放入暫存器中,當被切走的行程回來時,也需要首先做恢復作業,這就叫背景關系的保存與恢復
- 每一個行程都隸屬于某一個運行佇列,CPU直接從佇列中調度
代碼中用fork進行理解


- 程式員角度:父子共享用戶代碼,而用戶資料各自私有一份,其中父子共享用戶代碼是只讀的,不可修改和寫入, 為什么要私有?作業系統中,所有行程具有獨立性,如一個app掛掉而不影響另外一個行程的運行, 所以私有一份,不讓行程相互干擾所以是只讀的不能修改
- 內核角度:fork后,系統多了一個行程,創建子行程,通常以父行程為模版其中子行程默認使用的是父行程的代碼和資料(寫時拷貝)
為什么給子行程回傳的是0,父行程回傳子行程的pid?
兒子 -> 唯一父親 , 因此兒子找父親是特別簡單的,是0
父親 -> 多個子女 ,而一個父親找特定的兒子是特別難的,所以要用不同ID區分
3.3 行程狀態

通過task state array定義可以看出一共有七種狀態

- R狀態可以時正在運行,但是R狀態有時候也并不代表是在CPU上面運行,行程在佇列中等待被調度時,或者說行程準備就緒時也是R狀態

- S狀態,也叫淺度休眠,對外部事件可以做出反應,如ctrl+c 可以停止休眠
- D狀態,也叫深度休眠,不可以被kill掉,即便是作業系統通常在訪問磁盤進行資料拷貝的關鍵步驟上是需要將行程設定為Deep sleep的,只能等待D狀態行程自動醒來,或是關機重啟
- 小T狀態,是一種等待狀態


-18讓行程又開始跑起來
-
大T狀態,tracing stop用于打斷點,行程就不跑了

-
Z狀態,僵尸狀態
行程退出時會將自己退出的相關資訊寫入行程的PCB中,供OS或者父行程來進行讀取,其中我們把一個行程退出了但還沒有被讀取的時間點,我們稱該行程來僵尸狀態,
注意:這里Z狀態一直不退出,PCB要一直維護,如果父行程創建了很多子行程就是不會收,就造成了記憶體資源浪費,因為資料結構物件本身就要占用記憶體,換言之造成記憶體泄漏 -
X狀態,行程結束
讀取成功后,該行程才算真正死亡!為X狀態
舉個僵尸狀態的例子:


當子行程執行完自己的事情后退出了,父行程還在S狀態,但父行程不回收子行程,這時候子行程都會處于僵尸狀態
僵尸行程本質上是為了維護一種臨時狀態,讓我們父行程對子行程進行讀取,進而回收資源,以及釋放對應行程所占的資源
??注意:Z狀態已經掛掉,是不能被Kill掉的,plus 深度休眠狀態也是不能kill掉的
- 孤兒行程
先來看一組代碼
下面的代碼父行程首先fork后,父行程先退出了,那么就不會去讀取子行程的回傳代碼了
那么這個老爹退出了之后,子行程還在S狀態,需要被領養,也就是被PPID 為1作業系統給領養
那么這個被領養的行程就叫孤兒行程,非常的形象生動吧!

我們給下命令> top
查看 一下1號行程

四、行程地址空間
前面的都是開胃菜,這里才是重頭戲!
首先看一幅圖
行程地址空間從下到上為低地址到高地址
printf("Lowest area: code Address : %p \n", main); //main是函式,一定是屬于代碼的一部分,所以可以充當代碼段
const char* p = "I'm in const or you can call me static area"; //堆疊區定義一個p變數,p變數保存的是常量區的字串
printf("Read Only : %p \n", p); //p就是保存該常量的起始地址, 如果是static int,也在這里(生命周期全域)
printf("global initAlready : %p \n", &g_InitAlready);
printf("global Uninitalized : %p \n", &g_unInit);
char *q = (char*)malloc(10);
printf("Heap Addr : %p \n", q); //注意是q,而不是&q,如果是&q則是保持在堆疊區的變數名地址,而不是q內容
printf("Stack Addr p : %p \n", &p); //堆疊區向下增長,p先入堆疊,因此地址會比q的要大
printf("Stack Addr q : %p \n", &q);
printf("args Addr : %p \n", argv[0]);
printf("argv Addr : %p \n", argv[argc-1]); //帶一些選項就不一樣了
printf("Env Addr : %p \n", env[0]);

上圖論證了各個區域所存放的地址,從代碼段到環境變數區域由低向高走
行程地址空間,不是記憶體!是虛擬出來的,后面會講到到每一個行程都會認為自己有一塊這么大的空間,
行程地址空間,會在行程的整個生命周期一直存在,直到行程退出! 這就是為什么定義的全域變數在為初始化,初始化,靜態區一直存在了
我們可以論證這樣一個觀念:
假設記憶體是4GB,創建出來的每個行程都會認為自己有一塊這么大的空間
val = 0的情況
如果一個行程創建了一個child后sleep 3秒鐘,child先跑,對val進行修改后為10,父親再讀取這個val,child列印出來的val=10,而父val=0,但是發現他們的地址盡然一樣!,這也從側面論證了我們看到的不是實實在在的物理地址,而行程地址空間本質上就是一種虛擬地址,他們的值可以不一樣是因為行程間是相互獨立的,代碼雖然是共享,資料是各自私有的,當發生寫的動作時就是寫時拷貝
行程地址空間的資料結構:

地址空間是在行程和物理空間之間的一個軟體層,通過mm_struct來模擬 讓作業系統給每個行程畫大餅,讓每個行程都認為自己有整個地址,或者說物理記憶體,這樣我們的每個行程就可以根據自己的地址空間來劃分自己的代碼
地址空間和物理記憶體之間的關系:
- 把行程的代碼和資料加載到記憶體中,并給行程創建行程地址空間
- 給每個行程創建一個頁表結構,
頁表構建的就是從行程地址空間出來的虛擬地址到物理地址當中的映射
因此我們就可以說父子行程列印出來的地址可以是完全一樣,而值不同
頁表有權利去控制某段資料有沒有權限寫進物理記憶體,也就是說,地址空間中的字符常量區和代碼區給到頁表是只讀權限的,我們創建的一個指標變數p,在堆疊中保存,指向的資料在常量區,不能進行修改,報segmentaltion fault
那么問題來了:
為什么不把輸入設備的值直接放入物理地址不就完事了嗎?還每個行程額外虛擬出來一塊地址空間干嘛呢?
如果直接訪問物理記憶體,行程一多就有可能發生指標越界,那么行程的獨立性就無法得到保證,因為物理記憶體暴露,可能有惡意程式直接對記憶體資料行程篡改或者讀取了
有了地址空間的好處:
- 保護物理記憶體,不收到任何行程內的地址的直接訪問,方便進行合法性檢測
- 將記憶體管理和行程管理進行解構( 比如創建行程,只需在頁表中向系統申請記憶體,當行程釋放只需通過頁表釋放記憶體(記憶體管理只需知道哪些記憶體區域(Page頁表)是無效的,哪些是有效的),但如果沒有頁表行程和物理記憶體之間是強耦合的
- 讓每個行程,以同樣的方式,來看待代碼和資料,想象一下當每個可執行程式都已經在執行前被劃分好了一塊塊區域,通過頁表,物理地址可以進行高效訪問
創作不易,如果文章對你幫助的話,點贊三連哦:)
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/375955.html
標籤:其他
上一篇:《演算法零基礎100講》(第48講) 位運算 (左移)
下一篇:除了微服務,我們還有其他選擇嗎?



