本文編寫過程有些長,通過反覆折磨,可能看着有些凌亂,像系統學習的朋友,請先食用過推薦的視頻課程以後再來看文章,文章內容也就是個總結性東西,看着和課堂筆記差很少,字數多了實在太卡了,我拆成多篇文章了~java
操做系統我以爲是軟件領域最難學的部分了,絕對沒有之一,太多的概念和知識點,要理清楚期中脈絡,相互關係,而後串起來造成一個技能樹,這也一個及其困難的事。更使人絕望的是絕大部分學習資料都不合格,我找了好久,有半年吧,終於找到了O看的資料,看完這些能幫你把整個操做系統學明白,因此輕珍惜此次的資料推薦還mysql
有一點,操做系統雖然學完了你感受沒什麼大用,能直接幫助你的不多,可是請不要所以請示對於其的學習,操做系統的內容是貫穿咱們整個職業生涯的,全部的代碼,軟件,都是運行在操做系統上的,職業生涯中確定會是否是碰到涉及操做系統的時候,這個時候你不會,相關的點你就吸取很差,乾脆看不懂,很耽誤你事。你要是遇到一點學一點,你會崩潰的,扯出泥巴帶着跟,跟還連着別的苗,那學習效果懶得1Blinux
android 面試通常估計不會問到這些,可是不要忽視操做系統的內容,操做系統裏的不少內容實際上是咱們精進技術必須瞭解的基礎知識點,操做系統不熟後面你看好多深刻C層的技術會根天書同樣,尤爲是騰訊的 MMKV 了,看得懂嗎,能理解嗎,即使對着你講你也會懵逼的吧android
主力資料:程序員
《Linux內核設計與實現》
,容易上手輔助資料:面試
資料食用教程:redis
Y4NGY
的課程,最好的方式了,沒有其它操做系統簡單能夠分紅3部分:封裝、抽象硬件的內核
,給用戶程序提供服務的外層
,UI層
算法
Linux 不是一個完整的操做系統,Linux 僅僅是一個開源的內核罷了,有好多基於 Linux 內核開發的操做系統:ubuntu/centos/opensuse/redhat
這些sql
怎麼理解,這些所謂的系統都是在 Linux 內核的基礎上,添加了本身的一層UI界面和一些服務,還有軟件源(相似於應用市場)罷了,包括 android 都是這樣shell
Linux 的學習是很困難的一件事
因此說學習 Linux 是一件耗時很長的事
操做系統:封裝一切對硬件的操做、交互,在軟件開發層面屏蔽硬件操做,只需關心代碼邏輯便可。按馬老師的說法,操做系統這東西就是一種特殊的軟件,對上服務咱們的程序,對下管理硬件
操做系統是60年代開始出現的,進化到如今也是經歷了許許多多的。在沒有操做系統的時代,咱們寫程序可不會像如今同樣簡單,只要專一於業務就好了
全部的操做咱們都要本身去作關聯,咱們要本身去和硬件打交道,咱們要本身控制內存如何存儲數據,控制內存地址的變遷,往顯卡寫入數據,須要知道顯卡的端口號,往顯示器輸出圖像,須要知道顯示器什麼制式的,用打印機打印須要知道打印機是什麼牌子的,每一家不一樣牌子的打印機支持的機器指令都是不一樣的
這樣寫程序自己是一件及其費時費力的事,並且寫出來的程序只能在這個型號上的機器跑,換個硬件這個程序就不能用了,程序沒有一點移植性可言,放到如今這是不可想象的,可是當年就是這樣
後來人們發現這樣不行啊,硬件一更新程序就要從新寫,這樣太費事了,根本不是程序應該有的樣子。因而聰明的人想到應該把那些和硬件有關的操做都統一塊兒來,屏蔽掉這些繁瑣的和硬件之間操做,使用統一協議規範硬件之間的交互、指令,在程序開發時不用再考慮和硬件之間的操做,把這些交給上面咱們封裝好的和硬件交互的代碼,因此操做系統誕生了
我這個解釋估計不是很好,可是操做系統雖然是個複雜的哦東西,可是本質上不復雜,就是把全部根硬件的操做都封裝起來,最後就變成了操做系統這個龐然大物。最合適的理解其實應該是早期的 DOS 系統
可能第一次接觸的朋友仍是不怎麼明白,簡單的說從啊作系統分2層:一層是封裝硬件操做的內核;一層是給應用進程提供功能的外層,你們這麼理解就好了,下面就說到內核了。要是還不理解,那麼就記住操做系統是硬件的一層抽象
按照我新找到的學習資料來講,操做系統扮演的是一個 interface 接口的角色,軟硬件之間的接口分3層:
硬件 —> 硬件之間的接口:
典型的 USB 接口就是,使用總線相聯,硬件提供中斷命令和驅動給操做系統實現硬件的響應、使用、調度硬件 -> 軟件之間的接口:
軟件 -> 軟件之間的接口:
操做系統向下提供硬件->軟件的接口,以實現軟件操做硬件的可能性;向上提供軟件->軟件的接口,已實現用戶程序對硬件操做的可能性和安全、權限管理
通常操做系統都有這麼一個內核,內核裏面管理硬件,在內核周邊運行着一些服務,來管理應用程序
操做系統分2層:內核態、用戶態,這個內核指的就是操做系統內核了,內核的東西就是上面說的封裝的那些對硬件的操做。這些和硬件的操做不少,除了內核的核心以外,基於封裝思想還有5大功能模塊:
內存管理模塊
cpu調度模塊
其餘硬件設備管理模塊
文件系統管理模塊
進程調度模塊
操做系統內核就是一個程序,而這5大功能又能夠當作5大程序,固然操做系統內核仍是有本身的核心的,一些基礎的、雜七雜八的內容仍是放在覈心中的,核心中是操做系統最爲核心的東西,核心和5大功能程序共同組成和操做系統內核,5個功能模塊能夠當作單獨分離出來的核心的小弟,受核心管理,核心是老大,帶着5個小弟,這個社團叫操做系統內核
嚴格摳字眼的話我這裏應該不怎麼對,可是你們要是不熟悉,以前沒研究過的話,這麼理解是最好的,對於作 Android 的來講,我麼拿過來學習操做系統的部分不是爲了開發操做系統的,就是爲了夯實下基礎的知識點,能理解就好了
什麼是宏內核
,看字面意思,宏是大的意思,5大功能模塊和核心必須安裝在一塊兒共同組成系統內核,這個就是宏內核,固然這樣的內核會很大,會佔用不少系統資源。PC,手機都是宏內核,win 啓動後你們看看吃多少內存走就知道這種宏內核設計很是耗費系統資源了
什麼是微內核
,微就是微小,能夠把系統內核作的很小,目的是減小資源消耗,微內核只有核心和進程管理2個組件。其餘功能組件能夠安裝在系統內核以外的地方,這樣系統內核運行時仍是會去尋找相應的功能組件,有點像分佈式系統
形象理解下:
宏內核 -
內核這團大哥和小弟必須坐在一塊兒辦公微內核 -
社團本部有大哥和進程調度這個管錢的小弟就好了,其餘人能夠外派出去,也能夠在本部呆着2者的優缺點:
對於微內核來講,除了進程調度這個模塊必須在打在內核中,其餘的模塊你想用就掛到內核中,甚至能夠作成分佈式的,掛在別的設備、芯片上
華爲退出的鴻蒙
就是微內核的,智能家居能夠當作微內核應用,一屋子的設備,冰箱、電視、空調、掃地機器人、洗碗機,這些有一個總的控制器,這個總得控制器能夠當作內核核心,其餘設備能夠當作不一樣的功能組件,用到哪一個去找哪一個就行,總體系統能夠隨時擴容或者瘦身
你說一張 SIM 卡能有多大性能,宏內核系統跑得起來嗎,也只能是微內核這種系統啦,這種小微設備還有不少,偏偏這些小微設備就是物聯網的基礎
微內核的特性必然在物聯網時代中大紅大紫,微內核系統自己又和硬件尤爲是芯片緊密相關,說不定物聯網的時代華爲麒麟+鴻蒙
會佔半壁江山也不說不定,如今頭部公司都在大力推動、後進公司也在佈局這方面
PC時代:inter + window
移動時代:ARM + Android/IOS
我很期待物聯網 IOT 時代是:麒麟+鴻蒙
的,很期待
外核
也是一種核心,只不過是應用在科研領域,市面上的商業項目是沒有的。起特色是能夠根據具體場景,生成最適合這個場景運行的系統內核
像阿里正在研發的 JVM,內存分配再也不是根據對象爲基礎分區,分代來了,針對高併發這種場景,每個 request 進來,JVM 都會給這個 request 分配一塊內存,request 結束時回收這塊內存。不用再去遍歷對象樹,不用再去判斷對象是否是死了,是否是要升級,最簡單的就是性能最好的
還有阿里研發的多租戶,這個不詳說了
這個瞭解就行,頂尖大學裏可能能看見這東西
VMM
能夠當作是一個虛擬層,VMM 又專用的應用場景:資源極端富裕
。像有的公司只是泡泡通常的簡單程序,可是服務器配置賊高,128個CPU,每一個CPU8個核,內存幾T,你說這樣資源浪費不浪費。因此就又了 VMM 這個東西,能夠在同一套硬件資源上運行多個操做系統,VMM 就是介於硬件核操做系統之間的虛擬核心
這個瞭解便可
這個是重點
早在 DOS 系統時代,一個程序想幹什麼就幹什麼,想控制哪一個硬件就控制哪一個硬件,想訪問哪塊內存就訪問哪塊內存,這個時代也是病毒天堂的時代,計算機是極度不安全的
爲了系統安全,爲了系統穩定運行
內核態,用戶態
內核空間,用戶空間
用戶程序是不能訪問內核空間的,可是內核能夠訪問用戶程序內存空間
目前,在硬件層面就能夠實現對指令分級,inter CPU 上吧機器指令分紅4個級別:ring0,ring1,ring2,ring3
,Linux 系統只使用了 ring0,ring3
這2個權限級別,具體解釋就是:
用戶程序程序只能使用 ring3 級別的指令,而內核態程序就能使用 ring0 級別的指令
用戶程序想用網卡讀取數據,那麼首先向系統內核申請 ring0 指令使用受權,操做系統內核使用 ring0 指令讀到數據後,再使用 ring3 指令把數據交給用戶程序。對於硬件來講是指令在 ring0/ring3 之間不斷切換
再好比用戶程序計算2+3,用戶程序使用 ring3 指令生成2核3,而後向內核申請 add 計算這樣的 ring0 指令的使用權限,系統內核使用 ring0 指定 add 以後把數據寫回內存,用戶程序用 ring3 指令就能讀取到結果了
ring0 能夠訪問全部的內存,ring3 只能訪問屬於本身程序的那塊內存
系統內核的功能都是經過內核函數
對外暴露出來的,Linux 系統內核指令很少,就200多個,像 java 中 socket 操做,其實是調系統內核的操做。建立線程,用戶程序是幹不了的,只能去找內核作操做,內核操做完了再通知你。JVM 什麼級別,站在操做系統的角度,你JVM就是一個普通程序
咱們繼續深化理解操做系統的層級結構,上面說了操做系統簡單的就分2層:內核和外層服務
,不直觀,很差理解,雖然上面咱們看了系統內核,可是這裏咱們仍是要結合圖示進一步看看
有必要再強調一下系統 API 和系統調用的關係,這2個概念必須理解清晰才行:
系統調用
提供了訪問和使用操做系統提供的服務的接口,這一層級的實現是操做系統級別的系統API
是指名了參數和返回值的一組函數,應用app開發人員經過API間接訪問系統調用系統調用也是函數,只不過是操做系統級別的函數,能夠理解爲系統內核中的函數,這些方法由於安全和權限考慮不直接對外提供訪問服務,而是經過通過考慮的、再次封裝過的、能夠對外提供訪問服務的系統API來間接調用。系統API這一層的方法就不在內核中了,而是在內核外部
好比標準函數庫裏提供的 API:printf
,能夠顯示器輸出字符串,這個函數內部就是使用了系統調用 wirte,是一個從用戶態到內核態,再從內核態切換回用戶態的過程
還有幾張有意思的圖來講這個問題:
理解到圖中的這些內容就好了,更深刻的有需求再去看,通常 app 層開發是用不到了
早期不是每個系統調用都有對應的中斷編號的,而是用統一用一箇中斷編號:0x80
,80中斷就是這麼來的,用 0x80 表明系統調用,軟中斷
0x80 這個中斷在中斷向量表裏保存了系統調用派發程序的入口,去系統調用表裏根據調用編號找處處理函數入口
後來爲了優化系統調用的性能,改成經過特殊指令觸發系統調用,X86的 sysenter
、AMD64的 syscall
,有個專用寄存器保存派發入口,不用再去中斷向量表裏查了
到如今:
0x80
這個中斷編號,確切的說這個纔是咱們說的軟中斷。軟中斷的函數參數中也能夠執行你要執行哪一個方法。操做系統內核中預約義了不少處理函數,好比 IO 操做相關的函數經常被2個概念搞的頭大~
虛擬地址:
這是說內存尋址的虛擬內存:
這來源於 WIN 系統,說的是用硬盤來擴展內存的大小,把一部分硬盤當內存使這2個概念不要亂,咱們常常說的實際上是虛擬地址這個東西,這個概念不少人都說,可是能講清除的甚少啊,推薦你們看看B站佩雨小姐姐的這2個視頻:
簡單易懂,我算是看這個視頻真正理解了虛擬地址
首先你們必須明確地址是幹啥的,物理內存中每個內存位都有在矩陣中,有本身的座標,咱們經過這個座標來找到數據,座標就是地址
物理地址就是內存位真實的物理存儲位置
虛擬內存是操做系統內核爲了對進程地址空間進行管理(process address space management)而精心設計的一個邏輯意義上的內存空間概念。咱們程序中的指針其實都是這個虛擬內存空間中的地址。
早期,那時程序都很小,咱們都是直接把程序自己所有加載在內存中的。好比1個程序在硬盤中是2M大小,咱們運行這個程序會把2M的代碼全局一次性加載進內存,此時咱們適用物理內存地址來訪問內存
就行這樣->
後面咱們發現了其中嚴重的問題:
進程不隔離帶來的安全問題:
典型的 DOS 系統,病毒能夠隨意幹什麼,病毒進程能夠隨意修改其餘進程的數據。由於進程間內存是不隔離的,爲何不隔離呢,由於你們用的都是真實的物理內存地址,咱們能夠訪問其餘進程的地址上的數據使用效率低:
要是運行的程序須要的內存大小超過了物理內存大小呢,系統會把部份內存數據寫入硬盤,把硬盤當作內存的次級緩存,把節省出來的內存分配給須要的進程,這樣會形成內存隔離,進程用的都是內存碎片,內存碎片會帶來性能問題正是由於上面直接使用真實物理內存地址帶來的種種問題,咱們不得不給真實內存地址上套一層,使用一個相互之間不通用的別名來代替真實內存地址,這個虛假的內存地址別名就是虛擬地址了
我就不畫圖了,你們腦補下,就好像咱們給電報加密,進程A使用本身加密方式去使用內存地址,病毒進程即使拿到進程A的內存地址也沒法定位到真實的內存地址
虛擬內存使用:分段、分頁
技術進一步優化內存使用,具體由操做系統和CPU硬件中的MMU單元來管理
在計算機系統中,映射的工做是由硬件和軟件共同來完成的。承擔這個任務的硬件部分叫作存儲管理單元MMU,軟件部分就是操做系統的內存管理模塊了
爲了不內存碎片的誕生,咱們直接給進程分配一段連續的真實內存,而後使用虛擬內存映射到真實物理內存上
經過分段技術,實現了進程間內存隔離,進程之間不能訪問其餘進程的內存了,由於每一個進程虛擬地址都是獨一份,單獨維護的,單獨和物理內存映射的,其餘進程拿到也沒用,沒有虛擬內存映射關係你是找不到真實物理地址的
分段技術遠遠不夠,內存對於電腦來講永遠是不夠的,咱們不能說進程你要多少內存就給你多少物理內存,一個電腦上同時運行的進程數有好幾十,這點內存怎麼夠分,即使幾T都不必定夠分,因此咱們使用了分頁
這個技術,實現按需分配
進程A你要60M內存,OK 虛擬內存層面給你,可是物理內存先不給你,等你運行時,須要一點內存我就給你分配一點內存,作到按需分配,這樣就能實現內存的高效應用了
分頁中的頁指的是內存管理單元把內存安頁這個基本的單位分配,一頁是4K大小,虛擬內存中的頁叫頁,物理內存中的頁叫頁框,記錄頁於頁框之間映射關係的叫頁表
頁的分配原則是按需分配,進程A告訴操做系統我須要60M內存,那麼操做系統就先給了進程A60M虛擬內存,可是沒給物理內存。在進程運行時計算真的須要1M內存,此時才分配1M物理內存給進程A,實現進程A虛擬內存於這1M物理內存的映射,等不夠用了再分配物理內存,可是總量不能超過進程A啓動時申請的60M虛擬內存這個閥值
頁的地址由:頁碼+偏移量
組成
頁碼
- 虛擬內存中頁的位置,其實就是排序數,虛擬內存劃分的最小單位就是頁,假如說虛擬內存分10000個頁,那麼0x23這個頁碼數就是說第0x23個頁偏移量
- 數據位於該頁中的位置,一頁是4K的大小,能夠裝好多對象了,內存又是順序分配,因此這個偏移量就是數據在這個頁中內存位置的首地址,頁和頁框中的偏移量其實都是同樣的,你們想啊都是4K大小,在這4K中的位置能有區別嘛每一個進程都有本身獨立的虛擬地址空間,這些地址空間須要經過頁表映射到不一樣的物理地址
頁表記錄頁於頁框的映射關係,頁和頁框地址的偏移量,頁表核心的就是記錄頁碼和頁框碼了,看下圖就是這個意思
最終CPU處理虛擬內存到物理內存就是下圖:
你們想一想要是能在2個進程中,要是都是指向相同的物理內存上,是否是就能實現跨進程內存共享啦,內存共享是進程間通訊的一種方式
swap 這個概念一直都很差理解,簡單來講就是實現虛擬內存->硬盤的映射,下面是我找到的比較明白的解釋
虛擬內存經過缺頁中斷爲進程分配物理內存,內存老是有限的,若是全部的物理內存都被佔用了怎麼辦呢?
Linux 提出 SWAP 的概念,Linux 中可使用 SWAP 分區,在分配物理內存,但可用內存不足時,將暫時不用的內存數據先放到磁盤上,讓有須要的進程先使用,等進程再須要使用這些數據時,再將這些數據加載到內存中,經過這種」交換」技術,Linux 可讓進程使用更多的內存
另外一個物理內存管理要處理的事情就是頁面的換出。每一個進程都有本身的虛擬地址空間,虛擬地址空間都很是大,而不可能有這麼多的物理內存。因此對於一些長時間不使用的頁面,將其換出到磁盤,等到要使用的時候,將其換入到內存中,以此提升物理內存的使用率
固然,也存在這樣的狀況:在請頁成功以後,內存中已沒有空閒物理頁框了。這是,系統必須啓動所謂地「交換」機制,即調用相應的內核操做函數,在物理頁框中尋找一個當前再也不使用或者近期可能不會用到的頁面所佔據的頁框。找到後,就把其中的頁移出,以裝載新的頁面。對移出頁面根據兩種狀況來處理:若是該頁未被修改過,則刪除它;若是該頁曾經被修改過,則系統必須將該頁寫回輔存
爲了公平地選擇將要從系統中拋棄的頁面,Linux系統使用最近最少使用(LRU)頁面的衰老算法。這種策略根據系統中每一個頁面被訪問的頻率,爲物理頁框中的頁面設置了一個叫作年齡的屬性。頁面被訪問的次數越多,則頁面的年齡最小;相反,則越大。而年齡較大的頁面就是待換出頁面的最佳候選者
最後要注意 Swap 和 mmap 的區別:Swap 是操做系統自動的內存到文件的映射,mmap 是用戶主動的內存到文件的映射,後面會詳說 mmap
Swap:表示非mmap內存(也叫anonymous memory,好比malloc動態分配出來的內存)因爲物理內存不足被swap到交換空間的大小
你們想啊,頁表自己也是存儲在內存中的,爲了訪問一個內存中的數據,要經歷2次內存訪問:1-> MMU 訪問內存中進程的頁表,獲取對一個的物理內存地址,2-> 經過物理地址訪問變量
減小 CPU 訪問內存的次數是系統優化的重點,這裏天然就有優化的點,要是 MMU 能直接計算出進程虛擬內存對用的物理地址,那就能減小一次訪問內存的操做了,因此 CPU 結構中專門有一個寄存器會存儲處於運行狀態的進程、進程的物理內存首地址,這個寄存器就是:PTBR
後來物理內存分塊分配,光有 PTBR
寄存器也很差使了,物理內存都是按塊分配,再也不是連續分配了,這時候就要在 CPU 中緩存進程頁表了,因而又誕生了一個寄存器:TLB
,該寄存器會保存進程部分頁表,爲了提升 TLB 的命中還有其餘一些算法,這裏就不說了,知道這個東西就好了,線程切換,進程切換,TLB 緩存也會跟着失效
進程你們熟悉這個東西,不少人以爲知道是什麼個東西就好了, 我知道它的特性啊,這些就夠了呀,可是我仍是要說請你們仔仔細細的把進程的全部學習一遍,這回解釋不少模糊的地方,及其有學習意義
程序、進程、線程,這2個概念,面試的時候老是愛問,除了應付面試以外,咱們其實也是應該能把這3個概念說清楚的
程序:
程序就是一個可執行文件,是存儲再硬盤上的一列列指令,就像 win 系統裏的 .exe
文件,這是一個可執行的安裝文件,解壓縮咱們能夠看到好多好多的代碼,只是這些代碼如今都是靜態的,都尚未運行起來進程:
當一個可執行文件被加載進內存,程序就變成了進程。進程就是已經被加載進內存的一系列相互關聯的課執行指令。進程把第一條指令加載進內存,而後按順序一條條的執行指令。進程本質就是程序計數器+運行時數據程序線程:
CPU 執行的任務就是一個個線程,線程中有棧幀,棧幀就是一個個將要運行的方法和臨時數據面試時這樣回答:進程是資源分配、保護和調度的基本單位
,線程是CPU調度的基本單位
在繼續深刻進程以前,先把併發並行
這倆概念搞清楚,頗有必要
並行:
多個進程在多個CPU核心中一塊兒執行,執行時機是固定的,是一塊兒執行的,相互之間沒有資源的搶佔和衝突併發:
多個進程在多個CPU核心中執行,能夠是同時執行,也能夠是你前我後,我後你前,執行時機是順機的,相互之間有資源的搶佔和衝突,好比對於CPU時間片的搶奪得益於上面咱們已經說多了虛擬內存的部分,你們知道了用戶態和內核態,全部操做硬件的指令都必需要在內核態中執行,這裏咱們就好說多了
無論物理內存有多少,linux 系統都會給每個進程分配4G
的虛擬內存也叫邏輯內存
0-3G的低位內存分配給用戶態
3-4G的高維內存分配給內核態
用戶態的3G內存就是進程本身的,別的進程訪問不了,可是這1G的內核態內存都會同一映射到物理內存中的內核內存部分
物理內存中,通常有1/4,最少1G的起始內存是分配給操做系統內核專門使用的,用戶進程是無法映射到內核所屬的物理內存的,可是操做系統內核卻能夠訪問所有的物理內存地址,進程間通訊就是經過內核映射的物理內存作中轉的
從3G-4G空間爲內核空間,存放內核代碼和數據,只有內核態進程可以直接訪問,用戶態進程不能直接訪問,只能經過系統調用和中斷進入內核空間,而這時就要進行的指令權限切換
操做系統中的全部進程中的這1G內核內存映射到的都是同一段物理內存,也就是全部進程共享內核所屬的物理內存,這點必須明確
最後咱們整體的看一下這4G邏輯內存的結構:
內核態內存先不說,咱們來詳細看看屬於進程本身的這3G用戶態內存,結構如圖:
text:
這是代碼段內存,保存就是每一個程序指令的首地址data、bss:
統稱數據段,保存已初始化、未初始化的全局和靜態變量heap:
堆內存,存放的是運行時分配的內存,好比用 malloc 函數申請的內存塊就是保存在堆中stack:
C函數運行使用的內存shared libs
這是共享內存部分,共享函數庫,mmap 內存到文件的映射用的就是這塊,位於堆和棧內存的中間注意:
heap 堆分配內存是從下往上分配,從低地址開始
stack 方向是反過來的
結合代碼來看看:
int a = 100;
void f(int b,int c){
int* p = malloc(100);
}
void g(int d){
f(d,d+1);
}
int main(){
static int e = 10;
}
複製代碼
a
是全局變量,保存在 data 裏p
是malloc函數分配的內存塊,保存在 heap 裏d+1
是函數運行時產生的,保存在 stack 裏e
是靜態變量,保存在 data 裏你們能夠對比 java 的內存模型看看,其實很像的,java 就是用本身的方式跑的C
進程都有本身的狀態的,這部分也是有專門的內存塊來保存的,這塊內存就叫作:PCB
,結構以下:
PCB 進程控制塊很重要的,要理解的,後面立刻就用到,PCB+用戶態內存合成進程的上下文
再加一點,PCB 在 Linux 系統用是 task_struct 這個屬性,看代碼的時候要能反應過來,task_struct 描述的是進程的數據結構
mm:
描述進程的內存資源fs:
描述文件系統資源,就是本進程的代碼在磁盤哪裏filel:
進程運行過程當中打開了哪些文件signal:
信號處理函數PID 的數量,進程的個數不是無限支持的,32位系統中最多支持 32768個進程,文件在:cat/proc/sys/kernel/pif_max
裏
進程狀態有2種說法:5狀態、7狀態,這裏先說5的,理解了以後再說7的,7的就是在5上細分出來的
你們千萬別和線程的裝唉搞混了,雖然看着很像啊
暫停狀態:
就是字面意思,該進程被暫停了,而不是 warting 去了,暫停狀態下只有咱們再次喚醒進程才能繼續運行殭屍狀態:
進程死了,可是還留有一具屍體,只有父進程主動使用 wart方法回收屍體,進程屍體纔會消失,不然進程屍體一直就在,kill 9 也殺不沒。殭屍狀態的進程全部資源都釋放了,只有進程的PCB task_struct 還留存,其目的是告訴父進程子進程死亡的緣由。建立進程時傳進去的 state,父進程能夠經過這個參數拿到子進程死亡的緣由。再說一次殭屍狀態資源都已經釋放了,是系統主動釋放的,絕對不會存在內存泄露的問題哈深度隨眠:
通常系統調用都是深度睡眠,只有進程在 warting 的那個中斷信號完事了,進程才能重回 ready 去排隊執行淺度睡眠:
任何信號都能喚醒 warting 的進程,通常驅動程序都用的是淺睡眠睡眠能夠當作一種阻塞,結合後面進程調度的內容,不一樣狀態,睡眠的進程都有本身的 warting 隊列
看圖,說的就是殭屍狀態 state 的使用 ->
R:
TASK_RUNNING,可執行狀態S:
TASK_INTERRUPTIBLE,淺睡眠狀態D:
TASK_UNINTERRUPTIBLE,深度睡眠狀態T:
TASK_STOPPED or TASK_TRACED,暫停狀態Z:
TASK_DEAD - EXIT_ZOMBIE,殭屍狀態X:
TASK_DEAD - EXIT_DEAD,退出狀態,即將被銷燬進程被殺死後其實4G用戶空間中,高位的內核態部分並無被回收,依然殘留有一些關鍵信息,好比 PCB 依然還存在沒有被回收
父進程能夠拿到進程的殘留的 PCB,能夠知道子進程的死因等一系列信息,父進程回收子進程是指父進程把子進程高位內核態地址所有回收,此時 PCB 會被銷燬
什麼叫進程切換,就是進程是去了 CPU,這裏你們先不考慮同一個進程內多線程的情況,這個後面到線程時再說。形成線程切換的惟一緣由就是中斷了
中斷就是一個信號,每一箇中斷源都有編號,內核在接受到中斷後,會看看中斷號,就能找到對應須要執行的任務,根據不一樣的中斷源來選擇handle處理。內核中有一箇中斷向量表,存的就是中斷的對應任務,外部硬件的中斷都是依賴驅動程序註冊到系統內核中的,要不繫統怎麼知道你這個硬件要幹啥啊
中斷是用戶態和內核態之間切換的惟一緣由
中斷是指程序執行過程當中,當發生一個事件時,會當即終止在CPU上執行的進程,而後立刻執行這個事件對應的任務,該任務結束後再恢復這個進程繼續執行程序
中斷類型
內部中斷:
來自 CPU 外部的中斷,這個又叫:硬件中斷,注意單次是:interrupt,中斷來源:
外部中斷:
來自 CPU 內部的中斷,注意使用的單詞時:Exception,系統異常其實都是一個個事件,這和 error 是不一樣的,是系統在運行過程當中本身發出的事件,中斷來源:
軟件中斷:
由軟件程序發出來的中斷,前面2個都是硬件設備發出的中斷信號,可是軟件一樣也有這樣的需求
0x80
,這就是常說的80中斷,具體解釋後面有內部中斷也叫等待資源,外部中斷也叫等待信號,有的地方說進程隊列中等資源、等信號啥的你們要能反應過來,也許這樣說不怎麼正確,可是請這麼理解。你們想啊,CPU 之外的硬件不就是系統的硬件資源嘛,這樣想具理解了
你們回憶上上面說的系統內核,內核中的一個功能模塊就是進程管理,因此對進程的任何變化都必須在內核態中執行,也就是說操做進程的指令都是 ring0 級別的
內核進程管理模塊使用隊列來管理進程,不一樣狀態的進程分別在不一樣的隊列中排隊,每一箇中斷源都有本身專屬的進程排隊隊列,好比進程A和進程B都由於要操做IO設備而觸發了IO中斷信號,在IO處理完以前,A和B都淂在IO中斷隊列裏排隊,看下圖,比較形象了
具體A和B用戶態到內核態的切換過程你們再去上面系統調用哪裏看看,每一個系統調用都會發出對應的中斷信號的 Ψ( ̄∀ ̄)Ψ
你們很差奇隊列裏存的時什麼嗎,不賣關子,隊列裏存的是進程的 PCB,經過 PCB 就能表明一個進程了,就能找到一個進程的位置,內存數據,因此不必把進程全部數據都裝進來
進程在排隊結束後不必定會按照進入的順序再獲取 CPU 資源執行本身的任務,具體的要看操做系統採用哪一種進程調度策略,搶佔式你們都熟悉吧,完事了有資格的進程去搶CPU時間片
PCB內存的做用再看下圖:
再次重申一遍,中斷是用戶態和內核態之間切換的惟一緣由。先不考慮多線程的問題,這個以後說
進程的切換是進程丟失 CPU 再獲取 CPU 的過程,也是從用戶態切換到內核態,再從內核態切換回用戶態的過程,這個過程值得仔細看看
還記得上面說的進程上下文嗎,回憶一下,還有 PCB 這裏就用到了
切換過程:
保存被中斷的進程的上下文信息
修改被中斷進程的狀態
把被中斷的進程加入對應的中斷隊列
執行中斷任務
中斷任務執行完,調度一個新的進程並恢復它的上下文
啊,又是上下文切換,線程切換有上下文切換,進程切換也有上下文切換,進程的上下文就是 PCB+用戶態內存。進程保存在主內存中,得到CPU執行任務要把相應的方法和數據寫入CPU緩存中的,當中斷信號來了,無論是主動的仍是被動的,都淂讓出CPU給別的進程使用,這時候咱們要保存進程當前執行的位置、線程,以實現以後搶到CPU再回到如今的點繼續執行任務
注意這5個過程都是在內核態中執行的,進程上下文的保存和切換都是在內核態由內核代碼執行的。操做系統由一個 load PSW 指令就是專門恢復進程現場的,從新加載進程上下文
這個過程不是一瞬間就完成的,也是耗時的,進程要是總是切來切去的,同樣會浪費大量性能,因此減小進程的切換也是一個優化性能的重點
fork 函數是建立子進程的,這裏強調這個函數是由於對於後面學習很是有意思
PCB 裏有2個參數 ->
PID:
進程IDPPID:
父進程IDPID = fork();
fork 函數是有返回值的,返回的是子進程的PID ->
-1:
子進程建立失敗0:
建立出來的子進程尚未子進程,因此這個數是0非0:
這個就是子進程的PID了經過這個方法咱們通常能夠肯定進程的父子關係
fork 函數的特色:新建立一個空白進程出來,而後分配資源,把父進程的全部數據完徹底全的拷貝一份放到子進程內存中,父進程 fork() 以後的代碼,子進程同樣會在這裏開始繼續開始運行
看到 fork 這裏你們驚訝不驚訝,會把進程的內存打包複製一份給子進程,作 android 的朋友們注意來,android 是大量用到 fork 了的,明白 fork 對於理解 android 很重要的
main{
int* a = 1;
fork();
printf("AA")
}
複製代碼
就這段代碼,fork 以後生成的子進程會繼續執行 fork() 以後的代碼,結合到這裏就是 AA 打印了2次
main{
int* a = 1;
int* PID = fork();
wait(PID);
printf("AA")
}
複製代碼
上面代碼加上一個 wait() 函數,wait 和 java 裏的同樣,其實應該說 java 的 wait 就是用的 C 的 wait,加上 wait 以後父進程會等待子進程執行完成後再執行本身,這個就是一個深度睡眠了,父進程在 wait 這個調度隊列裏一直等着,等着 wait 這個特定的中斷完成再把本身調度回 ready 隊列
父進程裏面啓動了一個子進程,而後這2個進程併發執行,系統回傾向於先執行父進程,固然也能夠設置傾向於執行子進程,這樣就有一個問題,要是在子進程執行完以前,父進程先結束了,那麼子進程的 PPID 就有問題了
Linux 裏面進程都是父子關係的,進程不能沒有爹,你爹要是掛了,系統回再給你動態的找個爹 ┗|`O′|┛ 嗷~~ ,這樣 PPID 也會跟着變
copy on wirte 說的就是 fork 子進程的事,咱們說了 fork 出來的子線程把父進程的全部數據的都拷貝餓了一份,進程調度隊列中存的是什麼,是 PCB 啊,PCB 就能夠表示一個進程,fork 的過程就是複製了 P1 的 PCB 給 P2,此時 P1 和 P2 的 PCB 是徹底同樣啊,看 PCB 圖:
系統會複製 P1 DE PCB 給 P2,mm 段表示內存,那此時 P1 P2 的內存地址都是同樣的,那麼操做都是相同的數據了,copy on wirte 發揮做用的就在這裏
子進程建立出來後,使用的仍是父進程的頁表,不過系統會把父進程頁表改爲 RD-ONLY (只讀)的,當子進程修改數據時,系統看到 RD-ONLY 會觸發缺頁中斷,把新的頁分配給子進程,把父進程的數據 copy 一份給子進程。雖然父子進程之間的虛擬內存頁表地址都同樣,可是指向的倒是不一樣的物理地址,新的內存頁就有寫權限了
fork 內部使用的是 clone(),clone 這個函數,這個函數很是靈活,能夠選擇把 PCB 的哪些數據段複製給 P2,哪些數據端共享
總結下:P1 把 task_struct 對拷一份給 P2,一開始是同樣的,可是隻要 P2 改了就變了,P2 是在本身的那份上改的,內存最難對拷,只要 P2 修改資源了,就從新分配內存頁給 P2,那P2就是本身的一份了,誰先寫誰獲得新的物理內存地址,父進程先寫,那父進程就得到新的物理內存地址
vfork() 函數使用 clone() 函數,PCB 其餘的數據段都複製一份,mm 內存段則共享,這樣一來子進程操做的就是父進程的數據了
fork 的點到這利就差很少了,知道怎麼回事就行,又不是作 Linux 開發的,不必深究~
還有一點,沒有 MMU 單元的 CPU,無法使用 fork() 函數
main{
......
}
複製代碼
Linux 進程中默認都會有一個主線程,這個線程不用你們本身去new,系統在建立進程時一塊iu建立出來了,這個線程就是主線程,看見main函數就表明主線程了。
再建立的其餘線程都叫子線程,有個特色必定要知道:一旦進程中的主線程結束了,無論這個進程還有多少子線程,這些子線程是否是還在執行任務,這些子線程都會跟着你一塊結束
。也就是說進程中主線程的結束表明這進程的死亡,因此進程的主線程通常都是設計成循環遍歷的,空閒時會阻塞
android MainThread 裏的 main 函數熟悉不熟悉,Linux 這塊你沒學過,你能理解到精髓碼,你能真的看得懂嗎,多半都是猜吧,猜就難免會有疑惑、顧慮,這就不叫學明白,你們要清楚這點
線程你們耳熟能詳了吧,不過多說概念了,Linux 中線程是基本的調度單位,面試說這個就好了
每一個進程都有本身的主線程,在進程建立時系統默認就會把主線程建立出來,再建立的線程都是子線程,對於進程來講,線程就是進程內的多個執行流
線程共享進程數據、資源,PCB 進程控制塊能夠找到進程全部的資源,PCB 就能夠表明一個進程,對於線程來講既然咱們要共享繼承的資源,咱們怎麼作最簡單,把 PCB 複製一份就好了,寄存器的值就存線程本身的就好了。複製出來的 PCB 能夠做爲線程的控制單元,更名叫:TCB
線程私有的資源也就是線程棧和寄存器臨時數值了,線程棧在用戶內存中,寄存器臨時數值在內核空間中,PCB 也是在內存中,也就是說複製一份 PCB 出來,生成 TCB,TCB 的 registers 重置一下就好了,PCB 和 TCB 在 Linux 中都是 task_sturct 結構體
代碼上覆制 PCB 最方便的方式就是 lone() 函數了,clone() 函數必會複製一份 PCB 出來,Linux 建立進程的函數 pthread_create() 用的就是 clone() 函數
TCB 中的數據基本都是複製了一份 PCB,可是注意啊,mm
是和 PCB 共享的,也就是用的是同一份數據,而沒有選擇複製,TCB 之間 mm 也會是用的同一份,看圖:
其實咱們所說的線程指的是 用戶線程
,也就是這個線程是運行在用戶空間中的,可是咱們要是須要訪問硬件資源怎麼辦,必需要切換到系統內核也就是內核空間中啊,系統提供了對應的 內核線程
這個東西,下面咱們說的線程建立模式就是用戶線程和內核線程的相互關係
多個用戶線程對應一個內核線程: 蛋疼,要是哪一個用戶線程耗時太長,那別的用戶線程就別想執行了,這顯然是不行的,因此也沒有操做系統喲尼姑這種線程模式
多個用戶線程對應一個內核線程,幾個對幾個就不固定了: 這個問題就是實現起來比較複雜,目前也沒有操做系統用這個線程模型
有1個用戶線程就有1個內核線程: 性能和複雜度的中和,目前基本用的都是這個線程模型
Thread Library 是爲程序眼建立、管理用戶線程服務的,不一樣操做系統有不一樣的線程庫
POSIX Pthreads:
這是 linux 的線程庫 API,能夠建立出用戶線程和內核線程Windows Threads:
win 平臺的java Threads:
java 由於要跨平臺嘛,因此具體要看目標的操做系統了PID:
進程的ID,其實就是線程所屬進程的 PCB 號TID:
線程本身的 TCD ID 號getpid()
函數獲取 PID,gettid()
函數獲取 TID
內核調度的是什麼,就是CPU啊,內核調度器決定哪一個任務執行,哪一個任務排隊,無論是單核心,仍是多核心都是依靠內核調度器來調度計算任務的
還記得面試時咱們對於線程的回答嗎:線程是系統調度的基本單元
,這裏展開一下
對於內核調度器來講,沒有什麼進程線程的概念,只有 task_sturct
,也就是 PCB、TCB 這東西,內核調度器遇到 task_sturct
就能夠去調度
TCB 咱們知道它表明資源,那怎麼理解 PCB,它但是表明的進程啊,最小調度單元不是線程嘛,幹 PCB 什麼事。你們還記得不,Linux 進程一建立,系統會立刻建立給進程建立出一個默認的線程出來,這個線程就是主線程,主線程有 TCB 嗎,沒有,PCB 就是 Linux 進程主線程的 TCB,也許這麼解釋不是很正確,可是我以爲這麼理解就好了
因此上面講的進程切換的東西在這裏都適用~
涉及到的幾個方面:
cpu 指令切換
上下文切換
進程切換
cache miss
CPU性能損失 ->
Linux 採用了2種機器指令權限範圍,ring0,ring3 內核態可使用,ring3 用戶態可使用。CPU 有本身的指令緩存寄存器、高速緩存的,平時都會緩存你這個操做級別對應的及其指令的,任何用戶態和內核態的切換都會形成指令寄存器和緩存的無效,要從新加載,這裏有一點性能損失
上下文切換性能損失 ->
用戶線程運行在用戶空間,內核線程運行內核空間,線程的切換必要要在內核中進行,這樣不光用戶線程切換帶來性能損失,內核線程切換同樣會帶來性能損失。線程有本身數據,在內核調度器中就是 PCB、TCB,這些數據是要加載到 CPU 緩存中的,CPU 一切換任務,這些數據都要離開 CPU 緩存,把新線程的數據加載進來
進程切換 ->
你 CPU 先後切換的線程要是分屬不一樣進程,那會還會形成進程切換,上下文切換的範圍更大,CPU 緩存內進程的頁目錄要切換,TLB 緩存會失效
cache miss性能損失 ->
cache miss 是什麼,是緩存命中無效啊。爲了減小 CPU 等待內存讀取數據的等待時間,有個緩存命中的技術,會有把相關的數據都加載進來,有時候一整頁 4K 的數據都會加載進來,L三、L二、L1 都會有,你一切換線程,這些爲了緩存命中加載家來的數據都無效了,這叫緩存命中無效或丟失,還得從新加載一次
你的線程要是沒事切來切去,這些性能損失也不小了,通常會佔 CPU 時間片的 1% 甚至更高
這裏我先說一個參數和2種任務類型 ->
相應時間:
從提交任務到第一次相應的時間CPU 密集型任務:
像學科計算這種須要佔用不少 CPU 時間的任務IO 密集型任務:
像訪問資源這種不怎麼須要佔用 CPU 時間,可是須要大量等待訪問資源時間的任務這3個東西是放在一塊兒說的,好比像鼠標操做,他是不佔 CPU 時間的,須要的就是操做系統及時反應咱們按鍵盤就好了,要是響應時間太長了,那就是卡頓了,像有大量用戶交互的系統,響應時間是最重要的指標
那磁盤操做這種 IO 任務,也須要及時響應,及時去放訪問資源就好了,而後我等着唄,這個過程當中能夠釋放對 CPU 資源的佔用
要是你們都排隊執行的話,一個 IO 型任務長時間佔用 CPU 可是不用,這對 cpu 型任何是無法接收的。要是按照誰快誰來這樣排隊的話,那 IO 任務永遠沒有執行的機會了
因此 ARM 平臺針對這2種類型的任務,專門推出了 big.LITTLE
型 CPU 架構,說白了就是大小核設計,大核計算能強,小核功耗小。系統會把 CPU 型任務度放到大核中執行,系統會把 IO 型任務放到小核中執行
這種針對任務類型設計的 CPU 架構須要系統架構同步的去這樣設計,效果就是實現用 4個核心的功耗實現7個核心的計算能力,這樣帶來的成本壓縮、功耗降低對於移動平臺來講相當重要
android RXjava,kotlin 的協程都設計有 cpu密集型任務線程池和 IO密集型線程池,也是爲了響應硬件上的設計思路,因此你們看看國外程序眼的眼界多高,代碼均可以迎合硬件架構思路作優化,這個點太 NB 了,我太佩服了
說白了就是排隊,排在前面的先執行,排在後面的後執行,只有前面的執行完了,後面的才能執行,不能插隊 問題也很明顯,前面的執行太慢,後面的任務響應時間就無法預測了,對於鼠標鍵盤來講, 這種策略是不能接受的
這個你們就熟悉了,限定每次 CPU 執行的時間,你們仍是按照順序排隊,CPU 時間用完了就換下一個,而後本身到隊尾接着排隊 好處是公平了,可是問題時對於鍵盤鼠標仍是不能接受,我鍵盤鼠標要是長時間連着操做呢,不能一會一卡吧。通常時間片選擇在 10ms-100ms 之間
這個就是預判誰的任務執行時間最短,誰就執行,而後比較下一個 思路還能夠,可是問題是這個時間執行時間怎麼預測啊,不可預知的東西太多了,如今也沒有成熟的算法,因此這個目前也是沒人用,但這是目前研究的一個方向,如今的成果是:記錄線程以前平均運行時長來做爲參考
優先級高的先執行唄~
SCS/PCS 是簡寫,是2種線程調度模式
SCS:
PTHREAD_SCOPE_SYSTEM,全部的能夠公平的去競爭全部 CPUPCS:
PTHREAD_SCOPE_PROCESS,進程先去競爭 CPU,而後該進程內部的線程再去競爭linux 使用的 SCS 模式,Thread.scope 參數表示的就是這個,緣由很簡單,Linux 採用 1:1 線程模型啊,每一個用戶線程都有本身對應的內核線程,內核線程能夠去搶 CPU 的呀
Linux 中 把優先級分紅 [0-139]
,數字越小,優先級越高,下面說的任務和線程是一回事
Thread 裏有個 Scheduling prlicy 參數,這個就是線程策略,Linux 系統根據線程要求響應的不一樣分紅2大的調度策略:
Real-time Schduling:
實時調度策略,通常內核線程都是這種調度策略,內部使用 FIFO+優先級的思路,每一個優先級都有一個隊列,優先級高的隊列先執行,相同優先級的按照順序排隊運行。如果有個高優先級的來了,就得讓給這個後來的優先級高的線程
SCHED_FIFO
默認是這個SCHED_RR
Normal Schduling
通常任務,用戶線程都是這個級別的,使用 RR+優先級的思路,不過就不是優先級高的運行完了才能等到優先級低的,而是能夠同時槍,區別是優先級高的運行時間長了,就把你優先級調低,優先級低的必定時間輪不到你,就把你的優先級往上調,這個幅度通常是 +-5。這樣作的目的就是爲了你們都能輪到執行,不會說你優先級低就等到最後
SCHED_OTHER
默認是這個SCHED_IDLE
SCHED_BATCH
0-99 對應是 Real-time Schduling 實時線程,100-139 對應的 Normal Schduling 普通線程,Linux 早期,100是-20,139是19,0是-139,具體看你的 Linux 版本
對於100-139的普通線程來講,優先級高的線程對優先級低的線程不具備絕對的優點,100的線程比110的線程,就是執行時間更長,在從 wrating 到 ready 的時候,100的線程能搶到時間片
普通線程的優先級還有 Nice 這個參數,用來動態調節優先級的,nice 的取值範圍:[-20,19],nice 越大優先級越小,nice 算是一個懲罰機制,你運行的時間太長了,把你 nice 值調大,你優先級就下降了,留出機會給其餘線程
普通線程也是有 IO型任務的,IO型任務響應必定要快,Linux 系統自己就會照顧 IO 型的任務,因此就誕生了 nice 這個值,沒有 nice,怎麼實現照顧 IO型任務,儘可能讓 IO 型任務獲得及時響應呀
後來 Linux 出了一個 RT
補丁包,能夠設置實時線程一段時間內佔用 CPU 時間的最大值,好比 1000 個時間片,經過 RT 能夠設置實時任務最多佔900個,剩下的留給普通任務。你們想啊,這個設計也是合理的,內核任務你要是跑起來沒完沒了,後面的普通任務,用戶程序的任務怎麼執行,不能一直都卡在那裏吧,要不用戶體驗就糟糕死了
RT 補丁包了對於普通線程還添加了一個調度算法:CFS 徹底公平調度策略
,CFS 會計算出一個虛擬時間,誰的虛擬時間小,誰執行,採用紅黑樹的數據結構
計算公式:
累計運行時間/權重
權重和優先級轉換:
在最求虛擬時間相等的前提下,權重越小,虛擬時間最大,想要虛擬時間小,就得累計運行時間長,也就是獲得 CPU 時間片的機會更多
cgroup 可讓咱們給線程劃分羣組,可讓該羣組運行在某個核心上,或者某幾個核心上,或者該羣組的線程優先級更高,得到 CPU 的機會更大
android 系統上分了2個羣組:
apps:
前臺 appbg_non_interactive:
背景非交互的,app 再也不前臺了都是這個apps:
cpu.share = 1024bg_non_interactive:
cpu.share = 52數越大權重越高,獲取 cpu 的機會越大,在前前臺運行的 app 可以更大的搶到 CPU
查詢步奏:
root@XXXX:/proc/6566 # ps | grep -i "video"
adb shell進入已經root的Android設備終端,得到進程的pidadb shell cat proc/6566/cgroup
結果:
cpu:/(前臺進程)
cpu:/bg_non_interactive(後臺非交互進程)