文章插圖

文章插圖
X86架構
CPU包括3個部分:運算單元,數據單元,控制單元
總線上主要有兩類數據,一類是地址數據(要拿內存中哪個位置的數據),這類總線是地址總線;另一類是真正的數據,這類總線是數據總線
32位CPU包含的寄存器
通用寄存器:EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP段寄存器:CS/DS/ES/FS/GS/SS指令寄存器:EIP標志寄存器:EFLAGS控制寄存器:CR0/CR2/CR3/CR4系統表指針寄存器:IDTR/GDTR/LDTR任務寄存器:TR調試寄存器:DR0-DR7
PS:視頻相關學習文檔,后臺私信(Linux)
主板上的ROM固化了一段初始化程序BIOS(Basic Input and Output System,基本輸入輸出系統)
Grub2是一個linux啟動管理器 ,Grub2把boot.img共512字節安裝到啟動盤的第一個扇區,這個扇區稱為MBR(Master Boot Record,主引導記錄扇區)
主機上電后,CS重置為0xFFFF,IP重置為0x0000,所以第一條指令指向0xFFFF0,正好在ROM范圍內,從這里開始進行硬件檢查將磁盤引導扇區讀入0x7c00處,并從0x7c000開始執行boot模塊:bootsect.s,繼續讀取其它扇區setup模塊:setup.s從實模式切換到保護模式(CR0寄存器最后一位置1)初始化GDT,IDTsystem模塊:許多文件(head.s,main.c …)進入main函數,進入各種初始化操作 INIT_TASK(init_task):系統創建第一個進程,0號進程 。這是唯一一個沒有通過fork或者kernel_thread產生的進程,是進程列表的第一個 trap_init() mm_init() sched_init() rest_init()kernel_thread(kernel_init, NULL, CLONE_FS) 創建第二個進程,這個是1號進程 。1號進程即將運行一個用戶進程從內核態到用戶態ramdisk創建2號進程
0號進程 -> 1號內核進程 -> 1號用戶進程(init進程) -> getty進程 -> shell進程 -> 命令行執行進程 。
加載內核:打開電源后,加載BIOS(只讀,在主板的ROM里),硬件自檢,讀取MBR(主引導記錄),運行Boot Loader,內核就啟動了,這里還有從實模式到保護模式的切換進程:內核啟動后,先啟動第0號進程init_task,然后在內核運行init()函數,1號進程進入用戶態變為init進程,init進程是所有用戶態進程的老祖宗,其配置文件是/etc/inittab,通過該文件設置運行等級:根據/etc/inittab文件設置運行等級,比如有無網絡系統初始化:啟動第一個用戶層文件/etc/rc.d/rc.sysinit,然后激活交換分區、檢查磁盤、加載硬件模塊等建立終端:系統初始化后返回init,這時守護進程也啟動了,init接下來打開終端以供用戶登錄init_task是第0號進程,唯一一個沒有通過fork或kernel_thread產生的進程,是進程列表的第一個init是第1號進程,會成為用戶態,是所有用戶態進程的祖先kthreadd是第2號進程,會成為內核態,是所有內核態進程的祖先系統調用
int指令將CS中的CPL改為0,從而進入內核,這是用戶程序發起調用內核代碼的唯一方式
系統調用核心
用戶程序中包含一段包含int指令的代碼操作系統寫中斷處理,獲取調用程序的編號操作系統根據編號執行相應代碼
Linux的PCB是task_struct結構
任務ID
在Linux系統中,一個線程組中的所有線程使用和該線程組的領頭線程相同的pid,并被存放在tgid成員中 。只有線程組的領頭線程的pid成員才會被設置為與tgid相同的值
注意,getpid()系統調用返回的是當前進程的tgid值而不是pid值
用戶棧和內核棧
內核在創建進程的時候,在創建task_struct的同時,會為進程創建相應的堆棧 。每一個進程都有兩個棧,一個用戶棧,存在于用戶空間;一個內核棧,存在于內核空間
當進程在用戶空間運行時,CPU堆棧指針寄存器里面的內容都是用戶棧地址,使用用戶棧;當進程在內核空間運行時,CPU堆棧指針寄存器里面的內容是內核棧地址,使用內核棧
當進程因為中斷或者系統調用陷入到內核態時,進程所使用的堆棧也要從用戶棧轉到內核棧 。
進程陷入到內核態后,先把用戶態堆棧的地址保存在內核棧之中,然后設置堆棧指針寄存器的內容為內核棧的地址,這樣就完成了用戶棧向內核棧的轉換;當進程從內核態恢復到用戶態時,在內核態之后的最后將保存在內核棧里面的用戶棧的地址恢復到堆棧指針寄存器即可 。這樣就實現了內核棧向用戶棧的轉換
在進程從用戶態轉到內核態的時候,進程的內核??偸强盏?。這是因為當進程在用戶態運行時使用用戶棧,當進程陷入到內核態時,內核保存進程在內核態運行的相關信息,但是一旦進程返回到用戶態后,內核棧中保存的信息全部無效,因此每次進程從用戶態陷入內核的時候得到的內核棧都是空的 。所以在進程陷入內核的時候,直接把內核棧的棧頂地址給堆棧指針寄存器就可以了
union thread_union {struct thread_info thread_info;unsigned long stack[THREAD_SIZE/sizeof(long)];};整個內核棧用union表示,thread_info和stack共用一段存儲空間,thread_info占用低地址 。在pt_regs和STACK_END_MAGIC之間,就是內核代碼的運行棧 。當內核棧增長超過STACK_END_MAGIC就會報內核棧溢出thread_info:存儲內核態運行的一些信息,如指向task_struct的task指針,使得陷入內核態之后仍然能夠找到當前進程的task_struct,還包括是否允許內核中斷的preemt_count開關等等
pt_regs:存儲用戶態的硬件上下文,用戶態進入內核態后,由于使用的棧、內存地址空間、代碼段等都不同,所以用戶態的eip、esp、ebp等需要保存現場,內核態恢復到用戶態時再將棧中的信息恢復到硬件 。由于進程調度一定會在內核態的schedule函數,用戶態的所有硬件信息都保存在pt_regs中了 。SAVE_ALL指令就是將用戶態的cpu寄存器值保存如內核棧,RESTORE_ALL就是將pt_regs中的值恢復到寄存器中,這兩個指令在介紹中斷的時候還會提到
TSS(task state segment):這是intel為上層做進程切換提供的硬件支持,還有一個TR(task register)寄存器專門指向這個內存區域 。當TR指針值變更時,intel會將當前所有寄存器值存放到當前進程的tss中,然后再講切換進程的目標tss值加載后寄存器中
這里很多人都會有疑問,不是有內核棧的pt_regs存儲硬件上下文了嗎,為什么還要有tss?前文說過,進程切換都是在內核態,而pt_regs是保存的用戶態的硬件上下文,tss用于保存內核態的硬件上下文
但是linux并沒有買賬使用tss,因為linux實現進程切換時并不需要所有寄存器都切換一次,如果使用tr去切換tss就必須切換全部寄存器,性能開銷會很高 。這也是intel設計的敗筆,沒有把這個功能做的更加的開放導致linux沒有用 。linux使用的是軟切換,主要使用thread_struct,tss僅使用esp0這個值,用于進程在用戶態 -> 內核態時,硬件會自動將該值填充到esp寄存器 。在初始化時僅為每1個cpu僅綁定一個tss,然后tr指針一直指向這個tss,永不切換 。
thread_struct:一個和硬件體系強相關的結構體,用來存儲內核態切換時的硬件上下文
context_switch執行過程
內存空間切換
將curr_task設置為新進程的task
將cpu寄存器保存到舊進程的thread_struct結構
將新進程的thread_struct的寄存器的值寫入cpu
切換棧頂指針
PS:文章福利后臺私信(Linux)獲取
task_struct {unsigned int policy// 調度策略int prio, static_prio, normal_prio;// 優先級unsigned int rt_priority;const struct sched_class* sched_class;// 調度器類}// 調度策略定義#define SCHED_NORMAL0// 普通進程#define SCHED_FIFO1// 相同優先級先來先服務#define SCHED_RR2// 相同優先級輪流調度#define SCHED_BATCH3// 后臺進程#define SCHED_IDLE5// 空閑進程#define SCHED_DEADLINE 6// 相同優先級電梯算法(deadline距離當前時間點最近)實時進程的優先級范圍是0-99,非實時進程優先級范圍100-139,數值越小,優先級越高實時進程的調度策略:SCHED_FIFO、SCHED_RR、SCHED_DEADLINE非實時進程的調度策略:SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE// 調度器類定義stop_sched_class// 優先級最高的任務會使用這種策略,中斷所有其他線程,且不會被其他任務打斷dl_sched_class// 對應deadline調度策略rt_sched_class// 對應RR算法或者FIFO算法的調度策略,具體調度策略由進程的task_struct->policy指定fair_sched_class// 普通進程的調度策略idle_sched_class// 空閑進程的調度策略CFS調度算法(完全公平調度算法)CFS給每個進程安排一個虛擬運行時間vruntime,正在運行的進程vruntime隨tick不斷增大,沒有運行的進程vruntime不變,vruntime小的會被優先運行對于不同優先級的進程,換算vruntime時優先級高的算少,優先級低的算多,這樣優先級高的進程實際運行時間就變多了調度隊列使用紅黑樹,紅黑樹的節點是調度實體進程發生需要等待IO的系統調用進程主動sleep進程等待占用信號量或mutex(自旋鎖不會觸發調度)
搶占式調度(**先標記為應該被搶占,等到調用__schedule函數時才調度**)
時鐘中斷處理函數當一個進程被喚醒的時候搶占的時機
用戶態的搶占時機
從系統調用返回用戶態從中斷返回用戶態
【陳莉君 linux操作系統原理與應用張玲課后答案 Linux操作系統原理與應用電子版百度網盤】內核態的搶占時機
在內核態的執行中,有的操作是不能被中斷的,在進行這些操作之前,先調用preempt_disable關閉搶占,當再次打開的時候,就是一次內核態被搶占的機會從中斷返內核態fork過程
fork系統調用最終調用_do_fork函數
復制結構copy_process喚醒新進程wake_up_new_task
線程不是一個完全由內核實現的機制,它是由內核態和用戶態合作完成的
調用clone系統調用
創建進程調用的系統調用是fork,在copy_process函數里面,會將五大結構files_struct、fs_struct、sighand_struct、signal_struct、mm_struct都復制一遍,從此父進程和子進程各用各的數據結構
創建線程調用的系統調用是clone,在copy_process函數里面,五大結構僅僅是引用計數加一,即線程共享進程的數據結構
Shell進程在讀取用戶輸入的命令之后會調用fork復制出一個新的Shell進程,然后新的Shell進程調用exec執行新的程序
函數調用堆棧
3個重要的寄存器
EIP指向CPU下次要執行指令ESP棧指針寄存器,永遠指向系統棧最上面一個棧幀的棧頂EBP基址指針寄存器,永遠指向系統棧最上面一個棧幀的棧底
棧幀
每個函數每次調用,都有自己獨立的棧幀,ESP指向當前棧幀的棧頂,EBP指向當前棧幀的棧底
棧從高地址向低地址擴展
push eax; == esp=esp-4;eax->[esp];pop eax; == [esp]->eax;esp=esp+4;在main()里調用A(a, b)push b; 最右邊參數先入棧push a;push eip; 將A函數的EIP壓棧push ebp; 將A函數的EBP壓棧(把main函數的棧幀底部保存起來,不用保存棧幀頂部)mov ebp esp; 將當前的ESP賦值給EBP,相當于EBP從指向main的棧基址指向了A的?;?…當從A函數返回時,ESP移動到棧幀底部(釋放局部變量),然后把main函數的棧幀底部彈出到EBP,再彈出返回地址到EIP上,ESP繼續移動劃過參數,這樣,EBP,ESP就回到了調用函數前的狀態,即恢復了原來的main的棧幀CPU上下文切換
CPU上下文:CPU相關寄存器
CPU上下文切換:保存前一個任務的CPU上下文,加載新任務的CPU上下文,然后運行新任務
根據任務不同,CPU上下文切換分成不同場景
進程上下文切換線程上下文切換中斷上下文切換
進程可以在用戶空間運行,也可以在內核空間運行,用戶態和內核態的轉變通過系統調用完成;
系統調用過程發生了CPU上下文切換
一次系統調用,發生了兩次CPU上下文切換
進程的切換發生在內核態,進程的上下文不僅包括了虛擬內存,棧,全局變量等用戶空間的資源,還包括內核堆棧,寄存器等內核空間的狀態
進程上下文切換比系統調用多了一步
當虛擬內存更新后,TLB也需要更新,內存訪問會隨之變慢,影響所有處理器的進程
線程上下文切換
線程是調度的基本單位前后兩個線程屬于不同進程,切換過程同進程上下文切換前后兩個線程屬于同一進程,切換時虛擬內存等共享資源不動,只切換線程私有數據等非共享資源
中斷上下文只包括內核中斷服務程序執行所必需的的狀態,包括CPU寄存器,內核堆棧,硬件中斷參數等
過多的上下文切換,會把CPU時間消耗在寄存器、內核棧以及虛擬內存等數據的保存和恢復上,從而縮短進程真正運行的時間,導致系統的整體性能大幅下降
# 每隔5s輸出1組數據vmstat 5#cs(context switch)是每秒上下文切換的次數#in(interrupt)則是每秒中斷的次數#r(Running or Runnable)是就緒隊列的長度,也就是正在運行和等待CPU的進程數#b(Blocked)則是處于不可中斷睡眠狀態的進程數# 每隔5s輸出1組數據pidstat -wt 5#cswch 每秒自愿上下文切換的次數#nvcswch 每秒非自愿上下文切換的次數內核線程、輕量級進程、用戶進程Linux下只有一種類型的進程,就是task_struct
一個進程由于其運行空間的不同, 從而有內核線程和用戶進程的區分, 內核線程運行在內核空間, 之所以稱之為線程是因為它沒有虛擬地址空間,只能訪問內核的代碼和數據 。而用戶進程則運行在用戶空間,但是可以通過中斷, 系統調用等方式從用戶態陷入內核態
用戶進程運行在用戶空間上, 而一些通過共享資源實現的一組進程我們稱之為線程組 。linux下內核其實本質上沒有線程的概念,linux下線程其實上是與其他進程共享某些資源的進程而已 。但是我們習慣上還是稱他們為線程或者輕量級進程
進程棧,線程棧,內核棧,中斷棧進程棧就是Linux內存結構的棧,向低地址增長線程棧由mmap獲得,和進程處于同一地址空間,線程棧是事先分配好的,不能動態增長內核棧在地址空間的最高處,當用戶調用系統調用時就進入內核棧,進程間的內核棧是相互獨立的中斷棧用于運行中斷響應程序中斷
中斷導致系統從用戶態轉為內核態
用戶程序調用系統調用引起的軟件中斷由外部設備產生的硬件中斷代碼執行出錯導致的異常
軟件中斷
用戶進程調用系統調用時,會觸發軟件中斷(第128號中斷),該中斷使得系統轉到內核態,并執行相應的中斷處理程序,中斷處理程序根據系統調用號調用對應的系統調用
硬件中斷
CPU收到硬件中斷后,就會通知操作系統,每個硬件中斷有對應一個中斷號,并對應一個中斷處理程序(ISR)
中斷處理程序一般分為兩部分(top half,bottom half),執行前面部分時會禁止所有中斷,做一些有嚴格時限的工作,在執行后面部分時則允許被打斷
Linux2.6之后,每個處理器都擁有一個中斷棧專門用來執行中斷處理程序
中斷處理流程
硬件中斷到達中斷控制器,后者檢查該中斷是否被屏蔽中斷到達CPU系統跳轉到一個預定義的內存位置,執行預定義的指令屏蔽相同的中斷,檢查是否有對應的ISR如果有就執行對應的ISR檢查是否需要調用schedule(),返回被中斷處
用戶態和內核態
系統調用、異常、外圍設備中斷會導致系統進入內核態
系統調用:malloc內存分配
異常:缺頁異常
外圍設備中斷:鼠標鍵盤
從用戶態轉入內核態時,系統需要先保存進程上下文,切換到內核棧,更新寄存器,將權限修改為特權級,轉而去執行相應指令
進程、線程、協程
進程
進程是資源分配的基本單位,它是程序執行時的一個實例 。程序運行時系統就會創建一個進程,并為它分配資源,然后把該進程放入進程就緒隊列,進程調度器選中它的時候就會為它分配CPU時間,程序開始真正運行一個進程收到來自客戶端新的請求時,可以通過fork()復制出一個子進程來處理,父進程只需負責監控請求的到來,這樣就能做到并發處理 。根據寫時拷貝(COW)機制,分為兩個進程繼續運行后面的代碼 。fork分別在父進程和子進程中返回,在子進程永遠返回0,在父進程返回的是子進程的pid
線程
線程是資源調度的基本單位,一個進程可以由很多個線程組成,線程間共享進程的所有資源,每個線程有自己的堆棧和局部變量 。線程由CPU獨立調度執行,在多CPU環境下就允許多個線程同時運行 。同樣多線程也可以實現并發操作,每個請求分配一個線程來處理
線程和進程的區別
進程是資源分配的最小單位,線程是資源調度的最小單位一個進程的所有線程共享地址空間,但每個線程具有獨立的程序計數器,寄存器,棧,CPU切換的代價和創建一個線程的開銷都比進程小進程間通信需要通過IPC進行,線程間通信更方便,但要處理好同步與互斥,多進程程序更健壯,因為進程有自己獨立的地址空間,多線程程序只要有一個線程死掉,整個進程就死掉了
協程
協程是用戶態的輕量級線程,其調度由用戶控制,共享進程的地址空間,協程有自己的棧和寄存器,在切換時需要保存/恢復狀態,切換時減少了切換到內核態的開銷. 一個進程和線程都可以有多個協程. 相比函數調用, 協程可以在遇到IO阻塞時交出控制權
子程序,或者稱為函數,在所有語言中都是層級調用,比如A調用B,B在執行過程中又調用了C,C執行完畢返回,B執行完畢返回,最后是A執行完畢 。所以子程序調用是通過棧實現的,一個線程就是執行一個子程序 。子程序調用總是一個入口,一次返回,調用順序是明確的 。而協程的調用和子程序不同 。協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然后轉而執行別的子程序,在適當的時候再返回來接著執行 。在一個子程序中中斷,去執行其他子程序,不是函數調用,有點類似CPU的中斷協程的特點在于是一個線程執行,那和多線程比,協程有何優勢?最大的優勢就是協程極高的執行效率 。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯 。第二大優勢就是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多 。因為協程是一個線程執行,那怎么利用多核CPU呢?最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能 。Python對協程的支持還非常有限,用在generator中的yield可以一定程度上實現協程 。雖然支持不完全,但已經可以發揮相當大的威力了 。多進程與多線程的使用場景
多進程適用于CPU密集型,或者多機分布式場景中, 易于多機擴展
多線程模型的優勢是線程間切換代價較小,因此適用于I/O密集型的工作場景,因此I/O密集型的工作場景經常會由于I/O阻塞導致頻繁的切換線程 。同時,多線程模型也適用于單機多核分布式場景
協程
協程(coroutine)又叫微線程、纖程,完全位于用戶態,一個程序可以有多個協程
協程的執行過程類似于子例程,允許子例程在特定地方掛起和恢復
協程是一種偽多線程,在一個線程內執行,由用戶切換,由用戶選擇切換時機,沒有進入內核態,只涉及CPU上下文切換,所以切換效率很高
缺點:協程適用于IO密集型,不適用于CPU密集型
進程調度算法RR 時間片輪轉調度算法FCFS 先來先服務調度算法SJF 短作業優先調度算法優先級調度算法多級隊列調度算法多級反饋隊列調度算法高響應比優先調度算法
對于單處理器系統,每次只允許一個進程運行,任何其他進程必須等待,直到CPU空閑能被調度為止,多道程序的目的是在任何時候都有某些進程在運行,以使CPU使用率最大化 。
先到先服務FCFS,用了FIFO隊列,非搶占輪轉法調度Round Robin,就緒隊列作為循環隊列,按時間片切換,一般20ms~50ms最短作業優先調度SJF,先處理短進程,平均等待時間最小,所以是最佳的,但是很難知道每個作業要多少實現,所以不太現實優先級調度,會有饑餓現象,低優先級的進程永遠得不到調度多級隊列調度,根據進程的屬性(如內存大小、類型、優先級)分到特定隊列,不同隊列執行不同的調度算法多級反饋隊列調度,允許進程在隊列之間移動如果進程使用過多的CPU時間,那么它會被移到更低的優先級隊列 。這種方案將I/O密集型和交互進程放在更高優先級隊列上 。此外,在較低優先級隊列中等待過長的進程會被移到更高優先級隊列 。這種形式的老化可阻止饑餓的發生 。
CPU密集轉為IO密集
饑餓(starvation)是什么,如何解決
饑餓是指某進程因為優先級的關系一直得不到CPU的調度,可能永遠處于等待/就緒狀態;定期提升進程的優先級
- oracle配置環境變量 Linux oracle配置環境變量教程
- linux遠程登錄windows命令 linux遠程登錄命令ssh
- class文件記事本打開全亂碼了 linux查看class文件內容亂碼
- Linux服務器修改時間 linux時間服務器設置方法
- Linux常用命令詳解 linux相關命令及用法
- linux卸載javajdk linuxjdk卸載
- ibm服務器配置raid與安裝操作系統 ibm服務器配置raid10
- linux常用命令創建用戶 linux命令行創建用戶
- linux 給用戶賦權 linux給用戶組賦權限
- 中國的linux系統 操作系統linux是什么系統
