目錄
一.馮諾依曼體系結構
一). 軟件運行前為什么要先加載?程序運行之前在哪里?
二).理解數據流動?
二.操作系統OS(Operator System)?
一).概念?
二).設計OS的目的
三).如何理解操作系統OS的“管理”
四).系統調用和庫函數概念?
三.進程
一).進程基本概念與基本操作?
1.描述進程-PCB
2.task_ struct?
3.查看進程
1). 進程的信息可以通過 /proc 系統文件夾查看
2). 大多數進程信息同樣可以使用top和ps這些用戶級工具來獲取
4.通過系統調用獲取進程標示符
5.通過系統調用創建進程-fork初識
1).認識fork?
2).父子進程代碼共享,數據各自開辟空間,私有一份(采用寫時拷貝)?
3).fork有兩個返回值?
4).fork 之后通常要用 if 進行分流??
二).進程狀態
1.Linux內核源代碼怎么說
2.運行 && 阻塞 && 掛起?
3.進程狀態查看
4.Z(zombie)-僵尸進程
5.孤兒進程
三).進程優先級?
1.基本概念
2.查看系統進程
3.查看修改進程優先級
4.補充概念---競爭、獨立、并行、并發
四).進程切換
1.死循環進程如何運行?
2. CPU和寄存器
3.如何切換?
五).Linux真實的調度算法
1.優先級
2.活動隊列
3.過期隊列
4.active指針和expired指針
一.馮諾依曼體系結構
這里我們討論學習的是紅色的數據信號。
我們所認識的計算機,都是由一個個的硬件組件組成
- 輸入單元:包括鍵盤, 鼠標,掃描儀, 寫板,磁盤等
- 中央處理器(CPU):含有運算器和控制器等
- 輸出單元:顯示器,打印機,磁盤等
- 存儲器:內存
關于馮諾依曼,必須強調幾點:
1.這里的存儲器指的是內存。
2.不考慮緩存情況,這里的CPU能且只能對內存進行讀寫,不能訪問外設(輸入或輸出設備)。
3.外設(輸入或輸出設備)要輸入或者輸出數據,也只能寫入內存或者從內存中讀取。
4.一句話,所有設備都只能直接和內存打交道。
那為什么有這些設定呢?速度差距大。
我們設計出不同存儲速度的內存進行存儲。當代計算機是性價比的產物。
我們從兩個問題開始詳細認識這個體系。
一). 軟件運行前為什么要先加載?程序運行之前在哪里?
二).理解數據流動?
請解釋,從你登錄上qq開始和某位朋友聊天開始,數據的流動過程。從你打開窗口,開始給他發消息,到他的到消息之后的數據流動過程。如果是在qq上發送文件呢?
二.操作系統OS(Operator System)?
一).概念?
任何計算機系統都包含一個基本的程序集合,稱為操作系統(OS),操作系統是一款進行軟硬件管理的軟件。籠統的理解,操作系統包括:
? 內核(進程管理,內存管理,文件管理,驅動管理)
? 其他程序(例如函數庫,shell程序等等)
二).設計OS的目的
對下,與硬件交互,管理所有的軟硬件資源(目的)
對上,為用戶程序(應用程序)提供一個良好的執行環境(手段)
對于操作系統我們要認識以下幾點:
1.軟硬件體系結構------>層狀結構
2.訪問操作系統,必須使用系統調用------其實就是函數,不過是系統提供的
3.我們的程序,只要你判斷出它訪問了硬件,那么它必須貫穿整個軟硬件體系結構。比如:printf的本質:是你把你的數據寫到了硬件里。
4.庫可能在底層封裝了系統調用
在整個計算機軟硬件架構中,操作系統的定位是:一款純正的“搞管理”的軟件?
三).如何理解操作系統OS的“管理”
對于OS的管理,我們使用生活中的例子來進行描述。對于一件事情我們分為兩個步驟1.決策2.執行
管理的例子 ----- 學生,輔導員,校長。
加速理解:
1.對于校長管理學生,我們可以知道,我們一學期甚至大學四年都見不到校長幾次?,而對于校長的決策,比如:什么時候舉辦運動會,將全校最高的幾個男生組成籃球隊打比賽,我們都是要去執行的。
2.對于選全校身高最高的學生這個決策如何進行呢?校長一個一個的找同學去問嗎?并不是,而是通過學生的身高數據去篩選。
3.那么,身高數據從哪來?那當然是輔導員統計上交的啦。
那么,我們可以得到幾個結論:
1.要管理,管理者和被管理者,可以不需要見面
?2.管理者怎么管理被管理者,根據“數據”進行管理
3.不需要見面,如何得到數據?由中間層獲取。?
那么校長對于學生數據就可以進行數據建模,使用結構體來描述一個學生的信息
校長進行數據建模的行為,我們可以認為是先描述,再組織。
操作系統對于計算機管理硬件
1. 描述起來,用struct結構體。
2. 組織起來,用鏈表或其他高效的數據結構。
四).系統調用和庫函數概念?
在開發角度,操作系統對外會表現為一個整體,但是會暴露自己的部分接口,供上層開發使用,這部分由操作系統提供的接口,叫做系統調用。
系統調用在使用上,功能比較基礎,對用戶的要求相對也比較高,所以,有心的開發者可以對部分系統調用進行適度封裝,從而形成庫,有了庫,就很有利于更上層用戶或者開發者進行二次開發。
三.進程
操作系統是怎么管理進行進程管理的呢?很簡單,先把進程描述起來,再把進程組織起來!
一).進程基本概念與基本操作?
課本概念:程序的一個執行實例,正在執行的程序等?
內核觀點:擔當分配系統資源(CPU時間,內存)的實體
加速理解:?
我們可以簡單的形容為我們找工作的過程:內存中的代碼和數據就是“我們”,進程控制塊就是“我們的簡歷”,CPU就是“面試官”,進程列表就是“我們的簡歷在排隊”,我們在等待面試官篩選的過程本質上是我們的簡歷在等待被篩選,也就是進程控制塊在被讀取而不是我們的代碼和數據,而進程控制塊上的代碼和數據地址就是“我們的電話號碼和居住地址”。這樣應該就能稍微理解進程了。
1.描述進程-PCB
task_struct ---- PCB的一種
在Linux中描述進程的結構體叫做task_struct。
task_struct是Linux內核的一種數據結構,它會被裝載到RAM(內存)里并且包含著進程的信息
總結:
進程 = 內核數據結構對象? +??自己的代碼和數據?
在這里就是:進程 = PCB(task_struct)? +? 自己的代碼和數據?
2.task_ struct?
?內容分類
- 標示符: 描述本進程的唯一標示符,用來區別其他進程。
- 狀態: 任務狀態,退出代碼,退出信號等。
- 優先級: 相對于其他進程的優先級。
- 程序計數器: 程序中即將被執行的下一條指令的地址。
- 內存指針: 包括程序代碼和進程相關數據的指針,還有和其他進程共享的內存塊的指針。
- 上下文數據: 進程執行時處理器的寄存器中的數據。
- I/O狀態信息: 包括顯示的I/O請求,分配給進程的I/O設備和被進程使用的文件列表。
- 記賬信息: 可能包括處理器時間總和,使用的時鐘數總和,時間限制,記賬號等。
- 其他信息。
組織進程
可以在內核源代碼里找到它。所有運行在系統里的進程都以task_struct鏈表的形式存在內核里
3.查看進程
1). 進程的信息可以通過 /proc 系統文件夾查看
如:要獲取PID為1的進程信息,你需要查看 /proc/1 這個文件夾。?
2). 大多數進程信息同樣可以使用top和ps這些用戶級工具來獲取
我們歷史上執行的所有的指令,工具,自己的程序,運行起來,全部都是進程。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{while(1){sleep(1);printf("我是一個進程pid: %d\n",getpid());}return 0;
}
getpid() 的作用
查看運行程序的進程?ps axj | grep code
添加屬性列:ps axj | head -1;ps axj | grep code
殺掉進程 ctrl + c 或? kill -9 pid
4.通過系統調用獲取進程標示符
- 進程id(PID)
- 父進程id(PPID)
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 5 int main()6 {7 while(1)8 {9 sleep(1);10 printf("我是一個子進程pid: %d,我的父進程:%d\n",getpid(),getppid());11 } 12 }
那么這個135370進程是什么呢??
操作系統會給每一個用戶分配一個bash
查看bash:while :; do ps axj |head -1;ps axj | grep 'bash' | grep -v grep ; sleep 1 ; done
5.通過系統調用創建進程-fork初識
1).認識fork?
運行?man fork?
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 5 int main()6 {7 8 printf("父進程開始運行,pid: %d\n", getpid());9 pid_t id = fork();10 printf("進程開始運行,pid: %d\n", getpid());11 }
2).父子進程代碼共享,數據各自開辟空間,私有一份(采用寫時拷貝)?
3).fork有兩個返回值?
返回值中子進程為0,父進程為子進程的pid?
4).fork 之后通常要用 if 進行分流??
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{printf("父進程開始運行,pid: %d\n", getpid());pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){// childwhile(1){sleep(1);printf("我是一個子進程 !, 我的pid: %d, 我的父進程id: %d\n", getpid(), getppid());}} else{ //fatherwhile(1){ sleep(1);printf("我是一個父進程 !, 我的pid: %d, 我的父進程id: %d\n", getpid(), getppid());} }printf("進程開始運行,pid: %d\n", getpid());
}
那么我們會產生下面幾個問題
1.fork為什么會有兩個返回值?
因為? 父進程 :子進程 ==? 1? :n??父進程可能有多個子進程,父進程需要管理子進程,所以需要得到子進程的pid,所以父進程的返回值為子進程的pid,而子進程只需要管理好自己,子進程pid可以通過getpid得到,所以返回0。
2.為什么一個函數會被返回兩次?
fork語句后創建了子進程且被調度了,父進程執行一次return,子進程也執行一次return
3.一個變量怎么能讓 if 和 else if 同時成立?這個問題需要在后面才能解釋清楚。
首先我們要知道一個前提:進程具有獨立性。也就是父進程掛掉了子進程也無影響。
代碼測試:
#include <stdio.h> #include <unistd.h> #include <sys/types.h>int gval=100; int main() {printf("父進程開始運行,pid: %d\n", getpid());pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){printf("我是一個子進程 !, 我的pid: %d, 我的父進程id: %d, gval: %d\n", getpid(), getppid(), gval);sleep(5);// childwhile(1){sleep(1);printf("子進程修改變量: %d->%d", gval, gval+10);gval+=10; // 修改printf("我是一個子進程 !, 我的pid: %d, 我的父進程id: %d\n", getpid(), getppid());}}else{//fatherwhile(1){sleep(1);printf("我是一個父進程 !, 我的pid: %d, 我的父進程id: %d, gval: %d\n", getpid(), getppid(), gval); }}printf("進程開始運行,pid: %d\n", getpid()); }
二).進程狀態
1.Linux內核源代碼怎么說
為了弄明白正在運行的進程是什么意思,我們需要知道進程的不同狀態。一個進程可以有幾個狀態(在Linux內核里,進程有時候也叫做任務)。
下面的狀態在kernel源代碼里定義:
*
*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 */
}
進程狀態就是一個整數?
2.運行 && 阻塞 && 掛起?
掛起:將進程交換到磁盤的swap分區
運行:進程在調度隊列中,進程的狀態都是running
阻塞:等待某種設備或資源就位(鍵盤,顯示器,網卡....)?
進程狀態的變化表現之一:就是要在不同的隊列中進行流動,本質都是數據結構的增刪查改
3.進程狀態查看
循環查看進程狀態:while :; do ps axj |head -1; ps axj | grep code ;sleep 1; done
a:顯示一個終端所有的進程,包括其他用戶的進程。
x:顯示沒有控制終端的進程,例如后臺運行的守護進程。
j:顯示進程歸屬的進程組ID、會話ID、父進程ID,以及與作業控制相關的信息
u:以用戶為中心的格式顯示進程信息,提供進程的詳細信息,如用戶、CPU和內存使用情況等
我們通過代碼來具體查看進程狀態?
R運行狀態(running): 并不意味著進程一定在運行中,它表明進程要么是在運行中要么在運行隊列里。
S睡眠狀態(sleeping): 意味著進程在等待事件完成(這里的睡眠有時候也叫做可中斷睡眠(interruptible sleep))。
D磁盤休眠狀態(Disk sleep)有時候也叫不可中斷睡眠狀態(uninterruptible sleep),在這個狀態的進程通常會等待IO的結束。
這個狀態很少見,當磁盤老化,內存嚴重不足時才有可能出現
T/t停止狀態(stopped): 可以通過發送 SIGSTOP 信號給進程來停止(T)進程。這個被暫停的進程可以通過發送 SIGCONT 信號讓進程繼續運行。
小 t 狀態,程序被debug,斷點,程序被暫停了
X死亡狀態(dead):這個狀態只是一個返回狀態,你不會在任務列表里看到這個狀態。
4.Z(zombie)-僵尸進程
僵死狀態(Zombies)是一個比較特殊的狀態。當進程退出并且父進程(使用wait()系統調用,后面講)沒有讀取到子進程退出的返回代碼時就會產生僵死(僵尸)進程
僵死進程會以終止狀態保持在進程表中,并且會一直在等待父進程讀取退出狀態代碼。
所以,只要子進程退出,父進程還在運行,但父進程沒有讀取子進程狀態,子進程進入Z狀態
我們使用下面這段代碼查看僵尸狀態
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{pid_t id = fork();if(id == 0){//childint count = 5;while(count) //5次后退出{printf("我是子進程,我正在運行: %d\n", count);sleep(1);count--;}}else {while(1){printf("我是父進程,我正在運行...\n");sleep(1);}}return 0;
}
僵尸進程危害
- 進程的退出狀態必須被維持下去,因為他要告訴關心它的進程(父進程),你交給我的任務,我辦的怎么樣了。可父進程如果一直不讀取,那子進程就一直處于Z狀態?是的!那么就會造成內存泄漏。
- 維護退出狀態本身就是要用數據維護,也屬于進程基本信息,所以保存在task_struct(PCB)中,換句話說,Z狀態一直不退出,PCB一直都要維護?是的!
- 那?個父進程創建了很多子進程,就是不回收,是不是就會造成內存資源的浪費?是的!因為數據結構對象本身就要占用內存。
5.孤兒進程
父進程如果提前退出,那么子進程后退出,進入Z之后,那該如何處理呢?
父進程先退出,子進程就稱之為“孤兒進程”
孤兒進程被1號systemd進程領養,當然要有systemd進程回收嘍。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{pid_t id = fork();if(id == 0){// childwhile(1){printf("我是一個子進程, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}}else{// fatherint cnt = 5;while(cnt){printf("我是一個父進程, pid: %d, ppid: %d\n", getpid(), getppid());cnt--;sleep(1);}} return 0;
}
那么這個1號進程是什么呢??為什么要領養呢?
1號進程就是操作系統。
如果不領養,子進程退出,進入僵尸狀態后就無法回收,造成內存泄漏。
三).進程優先級?
1.基本概念
- cpu資源分配的先后順序,就是指進程的優先權(priority)。
- 優先權高的進程有優先執行權利。配置進程優先權對多任務環境的linux很有用,可以改善系統性能。
- 還可以把進程運行到指定的CPU上,這樣?來,把不重要的進程安排到某個CPU,可以大大改善系統整體性能
當然,這么看還是很陌生,我們從三個方向認識。
是什么?
是進程得到cpu資源的先后順序。
為什么?
目標資源稀缺,導致要通過優先級確認誰先誰后的問題。
怎么辦?
也是一種數字。值越低,優先級越高,反之,優先級越低。基于時間片的分時操作系統,考慮公平性,優先級可能變化,但是變化幅度不能太大。
2.查看系統進程
?ps -al | head -1 && ps -al |grep code
- UID : 代表執行者的身份
- PID : 代表這個進程的代號
- PPID :代表這個進程是由哪個進程發展衍?而來的,亦即父進程的代號
- PRI :代表這個進程可被執行的優先級,其值越小越早被執行
- NI :代表這個進程的nice值
注意:?
1.nice其取值范圍是-20?19,一共40個級別 。
2.當nice值為負值的時候,那么該程序將會優先級值將變小,即其優先級會變高,則其越快被執行。
3.程真實的優先級=PRI(默認優先級80)+NI。
3.查看修改進程優先級
用top命令更改已存在進程的nice:
- top
- 進入top后按“r”?>輸?進程PID?>輸入nice值。注意是輸入nice值。
命令 | 用途 |
---|---|
nice | 啟動時設置Nice值 |
renice | 修改運行中進程的Nice值 |
chrt | 查看/設置實時優先級 |
ps -o pid,ni,pri,rtprio,comm | 查看各種優先級 |
top | 交互式查看和修改優先級 |
注意:優先級設立不合理,會導致優先級低的進程長時間得不到CPU資源,進而導致:進程饑餓
4.補充概念---競爭、獨立、并行、并發
競爭性: 系統進程數目眾多,而CPU資源只有少量,甚至1個,所以進程之間是具有競爭屬性的。為了高效完成任務,更合理競爭相關資源,便具有了優先級。
獨立性: 多進程運行,需要獨享各種資源,多進程運行期間互不干擾。
并行: 多個進程在多個CPU下分別,同時進行運行,這稱之為并行。
并發: 多個進程在?個CPU下采用進程切換的方式,在一段時間之內,讓多個進程都得以推進,稱之為并發。
四).進程切換
CPU上下文切換:其實際含義是任務切換, 或者CPU寄存器切換。當多任務內核決定運行另外的任務時, 它保存正在運行任務的當前狀態, 也就是CPU寄存器中的全部內容。這些內容被保存在任務自己的堆棧中, 入棧工作完成后就把下一個將要運行的任務的當前狀況從該任務的棧中重新裝入CPU寄存器, 并開始下一個任務的運行, 這一過程就是context switch。
當然這么看還是不理解,我們先認識下幾個問題 。
1.死循環進程如何運行?
- ?一旦一個進程占有CPU,會把自己的代碼執行完嗎?不會,只執行一段時間片的東西。
- 死循環進程不會一直占有CPU。
2. CPU和寄存器
3.如何切換?
加速理解:
可以理解為我們進入大學后去服兩年兵役,此時我們需要向導員報告,學校需要保存我們的學籍信息。兩年后,我們回來繼續上學,那么我們要提前通知導員,讓學校恢復我們的學籍信息。我們可以粗略的認為:學校-----CPU;導員-----調度器;我們-----進程;學籍-----進程運行的臨時數據,CPU內寄存器里面的內容(當前進程的上下文數據);保留學籍-----保存進程上下文數據,CPU內寄存器里面的內容保存起來;恢復數據-----恢復進程上下文數據,恢復到CPU內寄存器里;去服兵役-----進程從CPU上剝離下來。
時間片:當代計算機都是分時操作系統,沒有進程都有它合適的時間片(其實就是?個計數器)。時間片到達,進程就被操作系統從CPU中剝離下來。
五).Linux真實的調度算法
圖是Linux2.6內核中進程隊列的數據結構?
1.優先級
- 普通優先級:100?139(我們都是普通的優先級,想想nice值的取值范圍,可與之對應!)
- 實時優先級:0?99(不關心)
2.活動隊列
- 時間片還沒有結束的所有進程都按照優先級放在該隊列
- nr_active: 總共有多少個運行狀態的進程
- queue[140]: 一個元素就是一個進程隊列,相同優先級的進程按照FIFO規則進行排隊調度,所以,數組下標就是優先級!
- 從該結構中,選擇一個最合適的進程,過程是怎么的呢?
1. 從0下表開始遍歷queue[140]
2. 找到第一個非空隊列,該隊列必定為優先級最高的隊列
3. 拿到選中隊列的第一個進程,開始運行,調度完成!
4. 遍歷queue[140]時間復雜度是常數!但還是太低效了 - bitmap[5]:一共140個優先級,一共140個進程隊列,為了提高查找非空隊列的效率,就可以用5*32個比特位表示隊列是否為空,這樣,便可以大大提高查找效率!
3.過期隊列
- 過期隊列和活動隊列結構一模一樣
- 過期隊列上放置的進程,都是時間片耗盡的進程
- 當活動隊列上的進程都被處理完畢之后,對過期隊列的進程進行時間片重新計算
4.active指針和expired指針
- active指針永遠指向活動隊列
- expired指針永遠指向過期隊列
- 可是活動隊列上的進程會越來越少,過期隊列上的進程會越來越多,因為進程時間片到期時?直都存在的。
- 沒關系,在合適的時候,只要能夠交換active指針和expired指針的內容,就相當于有具有了一批新的活動進程!
那么我們理解了這算法,我們也就知道了為什么要有 PRI和NI兩個值了,如果沒有NI,那當我們修改進程的優先級時,是在活動隊列直接將進程的優先級修改然后再排序嗎?不,那會大大增加時間成本;而是將修改的結果計算好,當進程在活動隊列運行結束后進入過期隊列后直接鏈接,不用排序。
- PRI :代表這個進程可被執行的優先級,其值越小越早被執行
- NI :代表這個進程的nice值
總結:?在系統當中查找一個最合適調度的進程的時間復雜度是一個常數,不隨著進程增多而導致時間成本增加,我們稱之為進程調度O(1)算法!