C++面試寶典--操作系統

原文鏈接:https://www.nowcoder.com/tutorial/93/2f895548adc24f0b88ffcb01c7973f23

1. 請你說一下進程與線程的概念,以及爲什麼要有進程線程,其中有什麼區別,他們各自又是怎麼同步的

基本概念:
進程是對運行時程序的封裝,是系統進行資源調度和分配的的基本單位,實現了操作系統的併發;
線程是進程的子任務,是CPU調度和分派的基本單位,用於保證程序的實時性,實現進程內部的併發;線程是操作系統可識別的最小執行和調度單位。每個線程都獨自佔用一個虛擬處理器:獨自的寄存器組,指令計數器和處理器狀態。每個線程完成不同的任務,但是共享同一地址空間(也就是同樣的動態內存,映射文件,目標代碼等等),打開的文件隊列和其他內核資源。

區別:
(1)一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。線程依賴於進程而存在。
(2)進程在執行過程中擁有獨立的內存單元,而多個線程共享進程的內存。(資源分配給進程,同一進程的所有線程共享該進程的所有資源。同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量。)
(3)進程是資源分配的最小單位,線程是CPU調度的最小單位;
(4)系統開銷: 由於在創建或撤消進程時,系統都要爲之分配或回收資源,如內存空間、I/o設備等。因此,操作系統所付出的開銷將顯著地大於在創建或撤消線程時的開銷。類似地,在進行進程切換時,涉及到整個當前進程CPU環境的保存以及新被調度運行的進程的CPU環境的設置。而線程切換隻須保存和設置少量寄存器的內容,並不涉及存儲器管理方面的操作。可見,進程切換的開銷也遠大於線程切換的開銷。
(5)通信:由於同一進程中的多個線程具有相同的地址空間,致使它們之間的同步和通信的實現,也變得比較容易。進程間通信IPC,線程間可以直接讀寫進程數據段(如全局變量)來進行通信——需要進程同步和互斥手段的輔助,以保證數據的一致性。在有的系統中,線程的切換、同步和通信都無須操作系統內核的干預
(6)進程編程調試簡單可靠性高,但是創建銷燬開銷大;線程正相反,開銷小,切換速度快,但是編程調試相對複雜。
(7)進程間不會相互影響 ;線程一個線程掛掉將導致整個進程掛掉
(8)進程適應於多核、多機分佈;線程適用於多核

進程間通信的方式:
進程間通信主要包括管道、系統IPC(包括消息隊列、信號量、信號、共享內存等)、以及套接字socket
(1)管道:管道主要包括無名管道和命名管道,管道可用於具有親緣關係的父子進程間的通信,有名管道除了具有管道所具有的功能外,它還允許無親緣關係進程間的通信。
(2)系統IPC:
1)消息隊列:是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標記。 (消息隊列克服了信號傳遞信息少,管道只能承載無格式字節流以及緩衝區大小受限等特點)具有寫權限得進程可以按照一定得規則向消息隊列中添加新信息;對消息隊列有讀權限得進程則可以從消息隊列中讀取信息;
特點:消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級。消息隊列獨立於發送與接收進程。進程終止時,消息隊列及其內容並不會被刪除。消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
2)信號量semaphore:與已經介紹過的 IPC 結構不同,它是一個計數器,可以用來控制多個進程對共享資源的訪問。信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。
特點:信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。信號量基於操作系統的 PV 操作,程序對信號量的操作都是原子操作。每次對信號量的 PV 操作不僅限於對信號量值加 1 或減 1,而且可以加減任意正整數。支持信號量組。
3)信號signal:一種比較複雜的通信方式,用於通知接收進程某個事件已經發生。
4)共享內存(Shared Memory):它使得多個進程可以訪問同一塊內存空間,不同進程可以及時看到對方進程中對共享內存中數據得更新。這種方式需要依靠某種同步操作,如互斥鎖和信號量等。
特點:共享內存是最快的一種IPC,因爲進程是直接對內存進行存取。因爲多個進程可以同時操作,所以需要進行同步。信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。
(3)套接字SOCKET:socket也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同主機之間的進程通信。

線程間通信的方式:
(1)臨界區:通過多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據訪問;
(2)互斥量Synchronized/Lock:採用互斥對象機制,只有擁有互斥對象的線程纔有訪問公共資源的權限。因爲互斥對象只有一個,所以可以保證公共資源不會被多個線程同時訪問
(3)信號量Semphare:爲控制具有有限數量的用戶資源而設計的,它允許多個線程在同一時刻去訪問同一個資源,但一般需要限制同一時刻訪問此資源的最大線程數目。
(4)事件(信號),Wait/Notify:通過通知操作的方式來保持多線程同步,還可以方便的實現多線程優先級的比較操作。

2. 請你說一說Linux虛擬地址空間

爲了防止不同進程同一時刻在物理內存中運行而對物理內存的爭奪和踐踏,採用了虛擬內存。虛擬內存技術使得不同進程在運行過程中,它所看到的是自己獨自佔有了當前系統的4G內存。所有進程共享同一物理內存,每個進程只把自己目前需要的虛擬內存空間映射並存儲到物理內存上。事實上,在每個進程創建加載時,內核只是爲進程“創建”了虛擬內存的佈局,具體就是初始化進程控制表中內存相關的鏈表,實際上並不立即就把虛擬內存對應位置的程序數據和代碼(比如.text .data段)拷貝到物理內存中,只是建立好虛擬內存和磁盤文件之間的映射就好(叫做存儲器映射),等到運行到對應的程序時,纔會通過缺頁異常,來拷貝數據。還有進程運行過程中,要動態分配內存,比如malloc時,也只是分配了虛擬內存,即爲這塊虛擬內存對應的頁表項做相應設置,當進程真正訪問到此數據時,才引發缺頁異常。
請求分頁系統、請求分段系統和請求段頁式系統都是針對虛擬內存的,通過請求實現內存與外存的信息置換。

虛擬內存的好處:
(1)擴大地址空間;
(2)內存保護:每個進程運行在各自的虛擬內存地址空間,互相不能干擾對方。虛存還對特定的內存地址提供寫保護,可以防止代碼或數據被惡意篡改。
(3)公平內存分配。採用了虛存之後,每個進程都相當於有同樣大小的虛存空間。
(4)當進程通信時,可採用虛存共享的方式實現。
(5)當不同的進程使用同樣的代碼時,比如庫文件中的代碼,物理內存中可以只存儲一份這樣的代碼,不同的進程只需要把自己的虛擬內存映射過去就可以了,節省內存。
(6)虛擬內存很適合在多道程序設計系統中使用,許多程序的片段同時保存在內存中。當一個程序等待它的一部分讀入內存時,可以把CPU交給另一個進程使用。在內存中可以保留多個進程,系統併發度提高。
(7)在程序需要分配連續的內存空間的時候,只需要在虛擬內存空間分配連續空間,而不需要實際物理內存的連續空間,可以利用碎片。

虛擬內存的代價:
(1)虛存的管理需要建立很多數據結構,這些數據結構要佔用額外的內存。
(2)虛擬地址到物理地址的轉換,增加了指令的執行時間。
(3)頁面的換入換出需要磁盤I/O,這是很耗時的
(4)如果一頁中只有一部分數據,會浪費內存。

3. 請你說一說操作系統中的程序的內存結構

在這裏插入圖片描述

一個程序本質上都是由BSS段、data段、text段三個組成的。可以看到一個可執行程序在存儲(沒有調入內存)時分爲代碼段、數據區和未初始化數據區三部分。

BSS段(未初始化數據區):通常用來存放程序中未初始化的全局變量和靜態變量的一塊內存區域。BSS段屬於靜態分配,程序結束後靜態變量資源由系統自動釋放。
data數據段:存放程序中已初始化的全局變量的一塊內存區域。數據段也屬於靜態內存分配
text代碼段:存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經確定,並且內存區域屬於只讀。在代碼段中,也有可能包含一些只讀的常數變量

text段和data段在編譯時已經分配了空間,而BSS段並不佔用可執行文件的大小,它是由鏈接器來獲取內存的。
bss段(未進行初始化的數據)的內容並不存放在磁盤上的程序文件中。其原因是內核在程序開始運行前將它們設置爲0。需要存放在程序文件中的只有正文段和初始化數據段。
data段(已經初始化的數據)則爲數據分配空間,數據保存到目標文件中。
data數據段包含經過初始化的全局變量以及它們的值。BSS段的大小從可執行文件中得到,然後鏈接器得到這個大小的內存塊,緊跟在數據段的後面。當這個內存進入程序的地址空間後全部清零。包含數據段和BSS段的整個區段此時通常稱爲數據區。

可執行程序在運行時又多出兩個區域:棧區和堆區。
棧區:由編譯器自動釋放,存放函數的參數值、局部變量等。每當一個函數被調用時,該函數的返回類型和一些調用的信息被存放到棧中。然後這個被調用的函數再爲他的自動變量和臨時變量在棧上分配空間。每調用一個函數一個新的棧就會被使用。棧區是從高地址位向低地址位增長的,是一塊連續的內存區域,最大容量是由系統預先定義好的,申請的棧空間超過這個界限時會提示溢出,用戶能從棧中獲取的空間較小。
堆區:用於動態分配內存,位於BSS和棧中間的地址區域。由程序員申請分配和釋放。堆是從低地址位向高地址位增長,採用鏈式存儲結構。頻繁的malloc/free造成內存空間的不連續,產生碎片。當申請堆空間時庫函數是按照一定的算法搜索可用的足夠大的空間。因此堆的效率比棧要低的多。

3. 請你說一說操作系統中的缺頁中斷

malloc()和mmap()等內存分配函數,在分配時只是建立了進程虛擬地址空間,並沒有分配虛擬內存對應的物理內存。當進程訪問這些沒有建立映射關係的虛擬內存時,處理器自動觸發一個缺頁異常。
缺頁中斷:在請求分頁系統中,可以通過查詢頁表中的狀態位來確定所要訪問的頁面是否存在於內存中。每當所要訪問的頁面不在內存是,會產生一次缺頁中斷,此時操作系統會根據頁表中的外存地址在外存中找到所缺的一頁,將其調入內存。

缺頁本身是一種中斷,與一般的中斷一樣,需要經過4個處理步驟:
(1)保護CPU現場
(2)分析中斷原因
(3)轉入缺頁中斷處理程序進行處理
(4)恢復CPU現場,繼續執行

但是缺頁中斷是由於所要訪問的頁面不存在於內存時,由硬件所產生的一種特殊的中斷,因此,與一般的中斷存在區別:
(1)在指令執行期間產生和處理缺頁中斷信號
(2)一條指令在執行期間,可能產生多次缺頁中斷
(3)缺頁中斷返回時,執行產生中斷的一條指令,而一般的中斷返回是,執行下一條指令。

4. fork和vfork的區別

(1)fork( )的子進程拷貝父進程的數據段和代碼段;vfork( )的子進程與父進程共享數據段
(2)fork( )的父子進程的執行次序不確定;vfork( )保證子進程先運行,在調用exec或exit之前與父進程數據是共享的,在它調用exec或exit之後父進程纔可能被調度運行。
(3)vfork( )保證子進程先運行,在它調用exec或exit之後父進程纔可能被調度運行。如果在調用這兩個函數之前子進程依賴於父進程的進一步動作,則會導致死鎖。
(4)當需要改變共享數據段中變量的值,則拷貝父進程。

5. 請問如何修改文件最大句柄數?

linux默認最大文件句柄數是1024個,在linux服務器文件併發量比較大的情況下,系統會報"too many open files"的錯誤。故在linux服務器高併發調優時,往往需要預先調優Linux參數,修改Linux最大文件句柄數。
有兩種方法:
(1)ulimit -n <可以同時打開的文件數>,將當前進程的最大句柄數修改爲指定的參數(注:該方法只針對當前進程有效,重新打開一個shell或者重新開啓一個進程,參數還是之前的值)
(2)對所有進程都有效的方法,修改Linux系統參數
vi /etc/security/limits.conf 添加
*  soft  nofile  65536
*  hard  nofile  65536
將最大句柄數改爲65536
修改以後保存,註銷當前用戶,重新登錄,修改後的參數就生效了

6. 請你說一說併發(concurrency)和並行(parallelism)

併發(concurrency):指宏觀上看起來兩個程序在同時運行,比如說在單核cpu上的多任務。但是從微觀上看兩個程序的指令是交織着運行的,你的指令之間穿插着我的指令,我的指令之間穿插着你的,在單個週期內只運行了一個指令。這種併發並不能提高計算機的性能,只能提高效率。
並行(parallelism):指嚴格物理意義上的同時運行,比如多核cpu,兩個程序分別運行在兩個核上,兩者之間互不影響,單個週期內每個程序都運行了自己的指令,也就是運行了兩條指令。這樣說來並行的確提高了計算機的效率。所以現在的cpu都是往多核方面發展。

7. 請問MySQL的端口號是多少,如何修改這個端口號

查看端口號:
使用命令show global variables like ‘port’;查看端口號 ,mysql的默認端口是3306
(補充:sqlserver默認端口號爲:1433;oracle默認端口號爲:1521;DB2默認端口號爲:5000;PostgreSQL默認端口號爲:5432)
修改端口號:
修改端口號:編輯/etc/my.cnf文件,早期版本有可能是my.conf文件名,增加端口參數,並且設定端口,注意該端口未被使用,保存退出。

8. 請你說一說操作系統中的頁表尋址

頁式內存管理,內存分成固定長度的一個個頁片。操作系統爲每一個進程維護了一個從虛擬地址到物理地址的映射關係的數據結構,叫頁表,頁表的內容就是該進程的虛擬地址到物理地址的一個映射。頁表中的每一項都記錄了這個頁的基地址。通過頁表,由邏輯地址的高位部分先找到邏輯地址對應的頁基地址,再由頁基地址偏移一定長度就得到最後的物理地址,偏移的長度由邏輯地址的低位部分決定。一般情況下,這個過程都可以由硬件完成,所以效率還是比較高的。頁式內存管理的優點就是比較靈活,內存管理以較小的頁爲單位,方便內存換入換出和擴充地址空間。

Linux最初的兩級頁表機制:
兩級分頁機制將32位的虛擬空間分成三段,低十二位表示頁內偏移,高20分成兩段分別表示兩級頁表的偏移。
PGD(Page Global Directory): 最高10位,全局頁目錄表索引
PTE(Page Table Entry):中間10位,頁表入口索引
當在進行地址轉換時,結合在CR3寄存器中存放的頁目錄(page directory, PGD)的這一頁的物理地址,再加上從虛擬地址中抽出高10位叫做頁目錄表項(內核也稱這爲pgd)的部分作爲偏移, 即定位到可以描述該地址的pgd;從該pgd中可以獲取可以描述該地址的頁表的物理地址,再加上從虛擬地址中抽取中間10位作爲偏移, 即定位到可以描述該地址的pte;在這個pte中即可獲取該地址對應的頁的物理地址, 加上從虛擬地址中抽取的最後12位,即形成該頁的頁內偏移, 即可最終完成從虛擬地址到物理地址的轉換。從上述過程中,可以看出,對虛擬地址的分級解析過程,實際上就是不斷深入頁表層次,逐漸定位到最終地址的過程,所以這一過程被叫做page talbe walk。

Linux的三級頁表機制:
當X86引入物理地址擴展(Pisycal Addrress Extension, PAE)後,可以支持大於4G的物理內存(36位),但虛擬地址依然是32位,原先的頁表項不適用,它實際多4 bytes被擴充到8 bytes,這意味着,每一頁現在能存放的pte數目從1024變成512了(4k/8)。相應地,頁表層級發生了變化,Linus新增加了一個層級,叫做頁中間目錄(page middle directory, PMD), 變成:
字段 描述 位數
cr3 指向一個PDPT crs寄存器存儲
PGD 指向PDPT中4個項中的一個 位31~30
PMD 指向頁目錄中512項中的一個 位29~21
PTE 指向頁表中512項中的一個 位20~12
page offset 4KB頁中的偏移 位11~0
現在就同時存在2級頁表和3級頁表,在代碼管理上肯定不方便。巧妙的是,Linux採取了一種抽象方法:所有架構全部使用3級頁表: 即PGD -> PMD -> PTE。那隻使用2級頁表(如非PAE的X86)怎麼辦?
辦法是針對使用2級頁表的架構,把PMD抽象掉,即虛設一個PMD表項。這樣在page table walk過程中,PGD本直接指向PTE的,現在不了,指向一個虛擬的PMD,然後再由PMD指向PTE。這種抽象保持了代碼結構的統一。

Linux的四級頁表機制:
硬件在發展,3級頁表很快又捉襟見肘了,原因是64位CPU出現了, 比如X86_64, 它的硬件是實實在在支持4級頁表的。它支持48位的虛擬地址空間1。如下:
字段 描述 位數
PML4 指向一個PDPT 位47~39
PGD 指向PDPT中4個項中的一個 位38~30
PMD 指向頁目錄中512項中的一個 位29~21
PTE 指向頁表中512項中的一個 位20~12
page offset 4KB頁中的偏移 位11~0
Linux內核針爲使用原來的3級列表(PGD->PMD->PTE),做了折衷。即採用一個唯一的,共享的頂級層次,叫PML4。這個PML4沒有編碼在地址中,這樣就能套用原來的3級列表方案了。不過代價就是,由於只有唯一的PML4, 尋址空間被侷限在(239=)512G, 而本來PML4段有9位, 可以支持512個PML4表項的。現在爲了使用3級列表方案,只能限制使用一個, 512G的空間很快就又不夠用了,解決方案呼之欲出。

9. 請問單核機器上寫多線程程序,是否需要考慮加鎖,爲什麼?

在單核機器上寫多線程程序,仍然需要線程鎖。因爲線程鎖通常用來實現線程的同步和通信。在單核機器上的多線程程序,仍然存在線程同步的問題。因爲在搶佔式操作系統中,通常爲每個線程分配一個時間片,當某個線程時間片耗盡時,操作系統會將其掛起,然後運行另一個線程。如果這兩個線程共享某些數據,不使用線程鎖的前提下,可能會導致共享數據修改引起衝突。

10. 請問線程需要保存哪些上下文,SP、PC、EAX這些寄存器是幹嘛用的

線程在切換的過程中需要保存當前線程Id、線程狀態、堆棧、寄存器狀態等信息。其中寄存器主要包括SP PC EAX等寄存器,其主要功能如下:
SP:堆棧指針,指向當前棧的棧頂地址
PC:程序計數器,存儲下一條將要執行的指令
EAX:累加寄存器,用於加法乘法的缺省寄存器

11. 遊戲服務器應該爲每個用戶開闢一個線程還是一個進程,爲什麼?

遊戲服務器應該爲每個用戶開闢一個進程。因爲同一進程間的線程會相互影響,一個線程死掉會影響其他線程,從而導致進程崩潰。因此爲了保證不同用戶之間不會相互影響,應該爲每個用戶開闢一個進程。

12. 請你說一說OS缺頁置換算法

當訪問一個內存中不存在的頁,並且內存已滿,則需要從內存中調出一個頁或將數據送至磁盤對換區,替換一個頁,這種現象叫做缺頁置換。當前操作系統最常採用的缺頁置換算法如下:
先進先出(FIFO)算法:置換最先調入內存的頁面,即置換在內存中駐留時間最久的頁面。按照進入內存的先後次序排列成隊列,從隊尾進入,從隊首刪除。
最近最少使用(LRU)算法: 置換最近一段時間以來最長時間未訪問過的頁面。根據程序局部性原理,剛被訪問的頁面,可能馬上又要被訪問;而較長時間內沒有被訪問的頁面,可能最近不會被訪問。
當前最常採用的就是LRU算法。

13. 請你說一說死鎖發生的條件以及如何解決死鎖

死鎖是指兩個或兩個以上進程在執行過程中,因爭奪資源而造成的下相互等待的現象。
死鎖發生的四個必要條件如下:
(1)互斥條件:進程對所分配到的資源不允許其他進程訪問,若其他進程訪問該資源,只能等待,直至佔有該資源的進程使用完成後釋放該資源;
(2)請求和保持條件:進程獲得一定的資源後,又對其他資源發出請求,但是該資源可能被其他進程佔有,此時請求阻塞,但該進程不會釋放自己已經佔有的資源
(3)不可剝奪條件:進程已獲得的資源,在未完成使用之前,不可被剝奪,只能在使用後自己釋放
(4)環路等待條件:進程發生死鎖後,必然存在一個進程-資源之間的環形鏈

解決死鎖的方法即破壞上述四個條件之一,主要方法如下:
(1)資源一次性分配,從而剝奪請求和保持條件
(2)可剝奪資源:即當進程新的資源未得到滿足時,釋放已佔有的資源,從而破壞不可剝奪的條件。
(3)資源有序分配法:系統給每類資源賦予一個序號,每個進程按編號遞增的請求資源,釋放則相反,從而破壞環路等待的條件。

14. 請你說一說操作系統中的結構體對齊,字節對齊

(1)原因:
平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。
性能原因:數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
(2)規則
數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset爲0的地方,以後每個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行。
結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。
結構體作爲成員:如果一個結構裏有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存儲。
(3)定義結構體對齊
可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數,其中的n就是指定的“對齊係數”。
(4)舉例
#pragma pack(2)
struct AA {
int a; //長度4 > 2 按2對齊;偏移量爲0;存放位置區間[0,3]
char b; //長度1 < 2 按1對齊;偏移量爲4;存放位置區間[4]
short c; //長度2 = 2 按2對齊;偏移量要提升到2的倍數6;存放位置區間[6,7]
char d; //長度1 < 2 按1對齊;偏移量爲7;存放位置區間[8];共九個字節
};
#pragma pack()

15. 請你講述一下互斥鎖(mutex)機制,以及互斥鎖和讀寫鎖的區別

(1)互斥鎖和讀寫鎖概念:
互斥鎖:mutex,用於保證在任何時刻,都只能有一個線程訪問該對象。當獲取鎖操作失敗時,線程會進入睡眠,等待鎖釋放時被喚醒。
讀寫鎖:rwlock,分爲讀鎖和寫鎖。處於讀操作時,可以允許多個線程同時獲得讀操作。但是同一時刻只能有一個線程可以獲得寫鎖。其它獲取寫鎖失敗的線程都會進入睡眠狀態,直到寫鎖釋放時被喚醒。 注意:寫鎖會阻塞其它讀寫鎖。當有一個線程獲得寫鎖在寫時,讀鎖也不能被其它線程獲取;寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)。適用於讀取數據的頻率遠遠大於寫數據的頻率的場合。
(2)互斥鎖和讀寫鎖的區別:
1)讀寫鎖區分讀者和寫者,而互斥鎖不區分
2)互斥鎖同一時間只允許一個線程訪問該對象,無論讀寫;讀寫鎖同一時間內只允許一個寫者,但是允許多個讀者同時讀對象。

Linux的4種鎖機制:
(1)互斥鎖:mutex,用於保證在任何時刻,都只能有一個線程訪問該對象。當獲取鎖操作失敗時,線程會進入睡眠,等待鎖釋放時被喚醒
(2)讀寫鎖:rwlock,分爲讀鎖和寫鎖。處於讀操作時,可以允許多個線程同時獲得讀操作。但是同一時刻只能有一個線程可以獲得寫鎖。其它獲取寫鎖失敗的線程都會進入睡眠狀態,直到寫鎖釋放時被喚醒。 注意:寫鎖會阻塞其它讀寫鎖。當有一個線程獲得寫鎖在寫時,讀鎖也不能被其它線程獲取;寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)。適用於讀取數據的頻率遠遠大於寫數據的頻率的場合。
(3)自旋鎖:spinlock,在任何時刻同樣只能有一個線程訪問對象。但是當獲取鎖操作失敗時,不會進入睡眠,而是會在原地自旋,直到鎖被釋放。這樣節省了線程從睡眠狀態到被喚醒期間的消耗,在加鎖時間短暫的環境下會極大的提高效率。但如果加鎖時間過長,則會非常浪費CPU資源。
(4)RCU:即read-copy-update,在修改數據時,首先需要讀取數據,然後生成一個副本,對副本進行修改。修改完成後,再將老數據update成新的數據。使用RCU時,讀者幾乎不需要同步開銷,既不需要獲得鎖,也不使用原子指令,不會導致鎖競爭,因此就不用考慮死鎖問題了。而對於寫者的同步開銷較大,它需要複製被修改的數據,還必須使用鎖機制同步並行其它寫者的修改操作。在有大量讀操作,少量寫操作的情況下效率非常高。

16. A* a = new A; a->i = 10;在內核中的內存分配上發生了什麼?

(1)A a:a是一個局部變量,類型爲指針,故而操作系統在程序棧區開闢4/8字節的空間(0x000m),分配給指針a。
(2)new A:通過new動態的在堆區申請類A大小的空間(0x000n)。
(3)a = new A:將指針a的內存區域填入棧中類A申請到的地址的地址。即
(0x000m)=0x000n。
(4)a->i:先找到指針a的地址0x000m,通過a的值0x000n和i在類a中偏移offset,得到a->i的地址0x000n + offset,進行*(0x000n + offset) = 10的賦值操作,即內存0x000n + offset的值是10。

17. 給你一個類,裏面有static,virtual,之類的,來說一說這個類的內存分佈

(1)static修飾符
1)static修飾成員變量
對於非靜態數據成員,每個類對象都有自己的拷貝。而靜態數據成員被當做是類的成員,無論這個類被定義了多少個,靜態數據成員都只有一份拷貝,爲該類型的所有對象所共享(包括其派生類)。所以,靜態數據成員的值對每個對象都是一樣的,它的值可以更新。
因爲靜態數據成員在全局數據區分配內存,屬於本類的所有對象共享,所以它不屬於特定的類對象,在沒有產生類對象前就可以使用。
2)static修飾成員函數
與普通的成員函數相比,靜態成員函數由於不是與任何的對象相聯繫,因此它不具有this指針。從這個意義上來說,它無法訪問屬於類對象的非靜態數據成員,也無法訪問非靜態成員函數,只能調用其他的靜態成員函數。
Static修飾的成員函數,在代碼區分配內存

(2)C++繼承和虛函數
C++多態分爲靜態多態和動態多態。靜態多態是通過重載和模板技術實現,在編譯的時候確定。動態多態通過虛函數和繼承關係來實現,執行動態綁定,在運行的時候確定。
動態多態實現有幾個條件:
1) 虛函數;
2)一個基類的指針或引用指向派生類的對象;
基類指針在調用成員函數(虛函數)時,就會去查找該對象的虛函數表。虛函數表的地址在每個對象的首地址。查找該虛函數表中該函數的指針進行調用。
每個對象中保存的只是一個虛函數表的指針,C++內部爲每一個類維持一個虛函數表,該類的對象的都指向這同一個虛函數表。
虛函數表中爲什麼就能準確查找相應的函數指針呢?因爲在類設計的時候,虛函數表直接從基類也繼承過來,如果覆蓋了其中的某個虛函數,那麼虛函數表的指針就會被替換,因此可以根據指針準確找到該調用哪個函數。

(3)virtual修飾符
如果一個類是局部變量則該類數據存儲在棧區,如果一個類是通過new/malloc動態申請的,則該類數據存儲在堆區。
如果該類是virutal繼承而來的子類,則該類的虛函數表指針和該類其他成員一起存儲。虛函數表指針指向只讀數據段中的類虛函數表,虛函數表中存放着一個個函數指針,函數指針指向代碼段中的具體函數。
如果類中成員是virtual屬性,會隱藏父類對應的屬性。

18. 請你回答一下軟鏈接和硬鏈接區別

爲了解決文件共享問題,Linux引入了軟鏈接和硬鏈接。除了爲Linux解決文件共享使用,還帶來了隱藏文件路徑、增加權限安全及節省存儲等好處。若1個inode號對應多個文件名,則爲硬鏈接,即硬鏈接就是同一個文件使用了不同的別名,使用ln創建。若文件用戶數據塊中存放的內容是另一個文件的路徑名指向,則該文件是軟連接。軟連接是一個普通文件,有自己獨立的inode,但是其數據塊內容比較特殊。

19. 請問什麼是大端小端以及如何判斷大端小端

大端是指低字節存儲在高地址;小端存儲是指低字節存儲在低地址。我們可以根據聯合體來判斷該系統是大端還是小端。因爲聯合體變量總是從低地址存儲。
int fun1(){
union test{ int i; char c; };
test t;
t.i = 1;
return (t.c == 1);
}
如果是大端,則t.c爲0x00,c!=1返回0,如果是小端,t.c==1返回1。

20. 請你回答一下靜態變量什麼時候初始化

靜態變量存儲在虛擬地址空間的數據段和bss段,C語言中其在代碼執行之前初始化,屬於編譯期初始化。而C++中由於引入對象,對象生成必須調用構造函數,因此C++規定全局或局部靜態對象當且僅當對象首次用到時進行構造。

21. 請你說一說用戶態和內核態區別

用戶態和內核態是操作系統的兩種運行級別,兩者最大的區別就是特權級不同。用戶態擁有最低的特權級,內核態擁有較高的特權級。運行在用戶態的程序不能直接訪問操作系統內核數據結構和程序。內核態和用戶態之間的轉換方式主要包括:系統調用,異常和中斷。

22. 如何設計server,使得能夠接收多個客戶端的請求

多線程,線程池,io複用

23. 死循環+來連接時新建線程的方法效率有點低,怎麼改

提前創建好一個線程池,用生產者消費者模型,創建一個任務隊列,隊列作爲臨界資源,有了新連接,就掛在到任務隊列上,隊列爲空所有線程睡眠。改進死循環:使用select epoll這樣的技術。

24. 怎麼喚醒被阻塞的socket線程?

給阻塞時候缺少的資源

25. 怎樣確定當前線程是繁忙還是阻塞?

使用ps命令查看

26. 請問就緒狀態的進程在等待什麼?

被調度使用cpu的運行權

27. 兩個進程訪問臨界區資源,會不會出現都獲得自旋鎖的情況?

單核cpu,並且開了搶佔可以造成這種情況。

28. windows消息機制知道嗎,請說一說

當用戶有操作(鼠標,鍵盤等)時,系統會將這些時間轉化爲消息。每個打開的進程系統都爲其維護了一個消息隊列,系統會將這些消息放到進程的消息隊列中,而應用程序會循環從消息隊列中取出來消息,完成對應的操作。

29. C++的鎖你知道幾種?

鎖包括:互斥鎖,條件變量,自旋鎖和讀寫鎖。

30. 請你說一說死鎖產生的必要條件?

(1)互斥條件:一個資源每次只能被一個進程使用。
(2)請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
(3)不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
(4)循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

31. 你都使用什麼線程模型?

常用線程模型
(1)Future模型:該模型通常在使用的時候需要結合Callable接口配合使用。
Future是把結果放在將來獲取,當前主線程並不急於獲取處理結果。允許子線程先進行處理一段時間,處理結束之後就把結果保存下來,當主線程需要使用的時候再向子線程索取。
Callable是類似於Runnable的接口,其中call方法類似於run方法,所不同的是run方法不能拋出受檢異常沒有返回值,而call方法則可以拋出受檢異常並可設置返回值。兩者的方法體都是線程執行體。
(2)fork&join模型:該模型包含遞歸思想和回溯思想,遞歸用來拆分任務,回溯用合併結果。可以用來處理一些可以進行拆分的大任務。其主要是把一個大任務逐級拆分爲多個子任務,然後分別在子線程中執行,當每個子線程執行結束之後逐級回溯,返回結果進行彙總合併,最終得出想要的結果。
這裏模擬一個摘蘋果的場景:有100棵蘋果樹,每棵蘋果樹有10個蘋果,現在要把他們摘下來。爲了節約時間,規定每個線程最多隻能摘10棵蘋樹以便於節約時間。各個線程摘完之後彙總計算總蘋果樹。
(3)actor模型:actor模型屬於一種基於消息傳遞機制並行任務處理思想,它以消息的形式來進行線程間數據傳輸,避免了全局變量的使用,進而避免了數據同步錯誤的隱患。actor在接受到消息之後可以自己進行處理,也可以繼續傳遞(分發)給其它actor進行處理。在使用actor模型的時候需要使用第三方Akka提供的框架。
(4)生產者消費者模型
生產者消費者模型都比較熟悉,其核心是使用一個緩存來保存任務。開啓一個/多個線程來生產任務,然後再開啓一個/多個來從緩存中取出任務進行處理。這樣的好處是任務的生成和處理分隔開,生產者不需要處理任務,只負責向生成任務然後保存到緩存。而消費者只需要從緩存中取出任務進行處理。使用的時候可以根據任務的生成情況和處理情況開啓不同的線程來處理。比如,生成的任務速度較快,那麼就可以靈活的多開啓幾個消費者線程進行處理,這樣就可以避免任務的處理響應緩慢的問題。
(5)master-worker模型
master-worker模型類似於任務分發策略,開啓一個master線程接收任務,然後在master中根據任務的具體情況進行分發給其它worker子線程,然後由子線程處理任務。如需返回結果,則worker處理結束之後把處理結果返回給master。

32. 請你來說一說協程

(1)概念:協程,又稱微線程,纖程,英文名Coroutine。協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然後轉而執行別的子程序,在適當的時候再返回來接着執行。
例如:
def A() :
print ‘1’
print ‘2’
print ‘3’
def B() :
print ‘x’
print ‘y’
print ‘z’
由協程運行結果可能是12x3yz。在執行A的過程中,可以隨時中斷,去執行B,B也可能在執行過程中中斷再去執行A。但協程的特點在於是一個線程執行。
(2)協程和線程區別
那和多線程比,協程最大的優勢就是協程極高的執行效率。因爲子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
第二大優勢就是不需要多線程的鎖機制,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。

33. 系統調用是什麼,你用過哪些系統調用

(1)概念:
在計算機中,系統調用(英語:system call),又稱爲系統呼叫,指運行在使用者空間的程序向操作系統內核請求需要更高權限運行的服務。系統調用提供了用戶程序與操作系統之間的接口(即系統調用是用戶程序和內核交互的接口)。
操作系統中的狀態分爲管態(核心態)和目態(用戶態)。大多數系統交互式操作需求在內核態執行。如設備IO操作或者進程間通信。特權指令:一類只能在核心態下運行而不能在用戶態下運行的特殊指令。不同的操作系統特權指令會有所差異,但是一般來說主要是和硬件相關的一些指令。用戶程序只在用戶態下運行,有時需要訪問系統核心功能,這時通過系統調用接口使用系統調用。
應用程序有時會需要一些危險的、權限很高的指令,如果把這些權限放心地交給用戶程序是很危險的(比如一個進程可能修改另一個進程的內存區,導致其不能運行),但是又不能完全不給這些權限。於是有了系統調用,危險的指令被包裝成系統調用,用戶程序只能調用而無權自己運行那些危險的指令。另外,計算機硬件的資源是有限的,爲了更好的管理這些資源,所有的資源都由操作系統控制,進程只能向操作系統請求這些資源。操作系統是這些資源的唯一入口,這個入口就是系統調用。

34. 請你來手寫一下fork調用示例

(1)概念:Fork:創建一個和當前進程映像一樣的進程可以通過fork( )系統調用:
成功調用fork( )會創建一個新的進程,它幾乎與調用fork( )的進程一模一樣,這兩個進程都會繼續運行。在子進程中,成功的fork( )調用會返回0。在父進程中fork( )返回子進程的pid。如果出現錯誤,fork( )返回一個負值。
最常見的fork( )用法是創建一個新的進程,然後使用exec( )載入二進制映像,替換當前進程的映像。這種情況下,派生(fork)了新的進程,而這個子進程會執行一個新的二進制可執行文件的映像。這種“派生加執行”的方式是很常見的。
在早期的Unix系統中,創建進程比較原始。當調用fork時,內核會把所有的內部數據結構複製一份,複製進程的頁表項,然後把父進程的地址空間中的內容逐頁的複製到子進程的地址空間中。但從內核角度來說,逐頁的複製方式是十分耗時的。現代的Unix系統採取了更多的優化,例如Linux,採用了寫時複製的方法,而不是對父進程空間進程整體複製。
(2)fork實例
int main(void)
{
pid_t pid;
signal(SIGCHLD, SIG_IGN);
printf(“before fork pid:%d\n”, getpid());
int abc = 10;
pid = fork();
if (pid == -1) { //錯誤返回
perror(“tile”);
return -1;
}
if (pid > 0) { //父進程空間
abc++;
printf(“parent:pid:%d \n”, getpid());
printf(“abc:%d \n”, abc);
sleep(20);
}
else if (pid == 0) { //子進程空間
abc++;
printf(“child:%d,parent: %d\n”, getpid(), getppid());
printf(“abc:%d”, abc);
}
printf(“fork after…\n”);
}

35. 請你來說一說用戶態到內核態的轉化原理

用戶態切換到內核態的3種方式
(1)系統調用
這是用戶進程主動要求切換到內核態的一種方式,用戶進程通過系統調用申請操作系統提供的服務程序完成工作。而系統調用的機制其核心還是使用了操作系統爲用戶特別開放的一箇中斷來實現,例如Linux的ine 80h中斷。
(2)異常
當CPU在執行運行在用戶態的程序時,發現了某些事件不可知的異常,這是會觸發由當前運行進程切換到處理此。異常的內核相關程序中,也就到了內核態,比如缺頁異常。
(3)外圍設備的中斷
當外圍設備完成用戶請求的操作之後,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條將要執行的指令,轉而去執行中斷信號的處理程序,如果先執行的指令是用戶態下的程序,那麼這個轉換的過程自然也就發生了有用戶態到內核態的切換。比如硬盤讀寫操作完成,系統會切換到硬盤讀寫的中斷處理程序中執行後續操作等。

切換操作
從出發方式看,可以在認爲存在前述3種不同的類型,但是從最終實際完成由用戶態到內核態的切換操作上來說,涉及的關鍵步驟是完全一樣的,沒有任何區別,都相當於執行了一箇中斷響應的過程,因爲系統調用實際上最終是中斷機制實現的,而異常和中斷處理機制基本上是一樣的,用戶態切換到內核態的步驟主要包括:
(1)從當前進程的描述符中提取其內核棧的ss0及esp0信息。
(2)使用ss0和esp0指向的內核棧將當前進程的cs,eip,eflags,ss,esp信息保存起來,這個過程也完成了由用戶棧找到內核棧的切換過程,同時保存了被暫停執行的程序的下一條指令。
(3)將先前由中斷向量檢索得到的中斷處理程序的cs,eip信息裝入相應的寄存器,開始執行中斷處理程序,這時就轉到了內核態的程序執行了。

36. 請你說一下源碼到可執行文件的過程

<1> 預編譯
主要處理源代碼文件中的以“#”開頭的預編譯指令。處理規則見下
(1)刪除所有的#define,展開所有的宏定義。
(2)處理所有的條件預編譯指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
(3)處理“#include”預編譯指令,將文件內容替換到它的位置,這個過程是遞歸進行的,文件中包含其他文件。
(4)刪除所有的註釋,“//”和“/**/”。
(5)保留所有的#pragma 編譯器指令,編譯器需要用到他們,如:#pragma once 是爲了防止有文件被重複引用。
(6)添加行號和文件標識,便於編譯時編譯器產生調試用的行號信息,和編譯時產生編譯錯誤或警告是能夠顯示行號。

<2>編譯
把預編譯之後生成的xxx.i或xxx.ii文件,進行一系列詞法分析、語法分析、語義分析及優化後,生成相應的彙編代碼文件。
(1)詞法分析:利用類似於“有限狀態機”的算法,將源代碼程序輸入到掃描機中,將其中的字符序列分割成一系列的記號。
(2)語法分析:語法分析器對由掃描器產生的記號,進行語法分析,產生語法樹。由語法分析器輸出的語法樹是一種以表達式爲節點的樹。
(3)語義分析:語法分析器只是完成了對表達式語法層面的分析,語義分析器則對表達式是否有意義進行判斷,其分析的語義是靜態語義——在編譯期能分期的語義,相對應的動態語義是在運行期才能確定的語義。
(4)優化:源代碼級別的一個優化過程。
(5)目標代碼生成:由代碼生成器將中間代碼轉換成目標機器代碼,生成一系列的代碼序列——彙編語言表示。
(6)目標代碼優化:目標代碼優化器對上述的目標機器代碼進行優化:尋找合適的尋址方式、使用位移來替代乘法運算、刪除多餘的指令等。

<3> 彙編
將彙編代碼轉變成機器可以執行的指令(機器碼文件)。 彙編器的彙編過程相對於編譯器來說更簡單,沒有複雜的語法,也沒有語義,更不需要做指令優化,只是根據彙編指令機器指令的對照表一一翻譯過來,彙編過程有彙編器as完成。經彙編之後,產生目標文件(與可執行文件格式幾乎一樣)xxx.o(Windows下)、xxx.obj(Linux下)。

<4> 鏈接
將不同的源文件產生的目標文件進行鏈接,從而形成一個可以執行的程序。鏈接分爲靜態鏈接和動態鏈接:
(1)靜態鏈接:函數和數據被編譯進一個二進制文件。在使用靜態庫的情況下,在編譯鏈接可執行文件時,鏈接器從庫中複製這些函數和數據並把它們和應用程序的其它模塊組合起來創建最終的可執行文件。
空間浪費:因爲每個可執行程序中對所有需要的目標文件都要有一份副本,所以如果多個程序對同一個目標文件都有依賴,會出現同一個目標文件都在內存存在多個副本;
更新困難:每當庫函數的代碼修改了,這個時候就需要重新進行編譯鏈接形成可執行程序。
運行速度快:但是靜態鏈接的優點就是,在可執行程序中已經具備了所有執行程序所需要的任何東西,在執行的時候運行速度快。
(2)動態鏈接:
動態鏈接的基本思想是把程序按照模塊拆分成各個相對獨立部分,在程序運行時纔將它們鏈接在一起形成一個完整的程序,而不是像靜態鏈接一樣把所有程序模塊都鏈接成一個單獨的可執行文件。
共享庫:就是即使需要每個程序都依賴同一個庫,但是該庫不會像靜態鏈接那樣在內存中存在多分,副本,而是這多個程序在執行時共享同一份副本;
更新方便:更新時只需要替換原來的目標文件,而無需將所有的程序再重新鏈接一遍。當程序下一次運行時,新版本的目標文件會被自動加載到內存並且鏈接起來,程序就完成了升級的目標。
性能損耗:因爲把鏈接推遲到了程序運行時,所以每次執行程序都需要進行鏈接,所以性能會有一定損失。

37. 請你來說一下微內核與宏內核

宏內核:除了最基本的進程、線程管理、內存管理外,將文件系統,驅動,網絡協議等等都集成在內核裏面,例如linux內核。
優點:效率高。
缺點:穩定性差,開發過程中的bug經常會導致整個系統掛掉。

微內核:內核中只有最基本的調度、內存管理。驅動、文件系統等都是用戶態的守護進程去實現的。
優點:穩定,驅動等的錯誤只會導致相應進程死掉,不會導致整個系統都崩潰
缺點:效率低。典型代表QNX,QNX的文件系統是跑在用戶態的進程,稱爲resmgr的東西,是訂閱發佈機制,文件系統的錯誤只會導致這個守護進程掛掉。不過數據吞吐量就比較不樂觀了。

38. 請你說一下殭屍進程

正常進程
正常情況下,子進程是通過父進程創建的,子進程再創建新的進程。子進程的結束和父進程的運行是一個異步過程,即父進程永遠無法預測子進程到底什麼時候結束。 當一個進程完成它的工作終止之後,它的父進程需要調用wait()或者waitpid()系統調用取得子進程的終止狀態。
unix提供了一種機制可以保證只要父進程想知道子進程結束時的狀態信息, 就可以得到:在每個進程退出的時候,內核釋放該進程所有的資源,包括打開的文件,佔用的內存等。 但是仍然爲其保留一定的信息,直到父進程通過wait / waitpid來取時才釋放。保存信息包括:
(1)進程號the process ID
(2)退出狀態the termination status of the process
(3)運行時間the amount of CPU time taken by the process等

孤兒進程
一個父進程退出,而它的一個或多個子進程還在運行,那麼那些子進程將成爲孤兒進程。孤兒進程將被init進程(進程號爲1)所收養,並由init進程對它們完成狀態收集工作。

殭屍進程
一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那麼子進程的進程描述符仍然保存在系統中。這種進程稱之爲殭屍進程。
殭屍進程是一個進程必然會經過的過程:這是每個子進程在結束時都要經過的階段。
如果子進程在exit()之後,父進程沒有來得及處理,這時用ps命令就能看到子進程的狀態是“Z”。如果父進程能及時處理,可能用ps命令就來不及看到子進程的殭屍狀態,但這並不等於子進程不經過殭屍狀態。
如果父進程在子進程結束之前退出,則子進程將由init接管。init將會以父進程的身份對殭屍狀態的子進程進行處理。
危害:
如果進程不調用wait / waitpid的話, 那麼保留的那段信息就不會釋放,其進程號就會一直被佔用,但是系統所能使用的進程號是有限的,如果大量的產生僵死進程,將因爲沒有可用的進程號而導致系統不能產生新的進程。
外部消滅:
通過kill發送SIGTERM或者SIGKILL信號消滅產生殭屍進程的進程,它產生的僵死進程就變成了孤兒進程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們佔用的系統進程表中的資源
內部解決:
(1)子進程退出時向父進程發送SIGCHILD信號,父進程處理SIGCHILD信號。在信號處理函數中調用wait進行處理殭屍進程。
(2)fork兩次,原理是將子進程成爲孤兒進程,從而其的父進程變爲init進程,通過init進程可以處理殭屍進程。

39. 請問GDB調試用過嗎,什麼是條件斷點

GDB調試:GDB 是自由軟件基金會(Free Software Foundation)的軟件工具之一。它的作用是協助程序員找到代碼中的錯誤。如果沒有GDB的幫助,程序員要想跟蹤代碼的執行流程,唯一的辦法就是添加大量的語句來產生特定的輸出。但這一手段本身就可能會引入新的錯誤,從而也就無法對那些導致程序崩潰的錯誤代碼進行分析。
GDB的出現減輕了開發人員的負擔,他們可以在程序運行的時候單步跟蹤自己的代碼,或者通過斷點暫時中止程序的執行。此外,他們還能夠隨時察看變量和內存的當前狀態,並監視關鍵的數據結構是如何影響代碼運行的。

條件斷點
條件斷點是當滿足條件就中斷程序運行,命令:break line-or-function if expr。
例如:(gdb)break 666 if testsize==100

40. 請你來介紹一下5種IO模型

(1)阻塞IO:調用者調用了某個函數,等待這個函數返回,期間什麼也不做,不停的去檢查這個函數有沒有返回,必須等這個函數返回才能進行下一步動作
(2)非阻塞IO:非阻塞等待,每隔一段時間就去檢測IO事件是否就緒。沒有就緒就可以做其他事。
(3)信號驅動IO:linux用套接口進行信號驅動IO,安裝一個信號處理函數,進程繼續運行並不阻塞,當IO時間就緒,進程收到SIGIO信號。然後處理IO事件。
(4)IO複用/多路轉接IO:linux用select/poll函數實現IO複用模型,這兩個函數也會使進程阻塞,但是和阻塞IO所不同的是這兩個函數可以同時阻塞多個IO操作。而且可以同時對多個讀操作、寫操作的IO函數進行檢測。知道有數據可讀或可寫時,才真正調用IO操作函數
(5)異步IO:linux中,可以調用aio_read函數告訴內核描述字緩衝區指針和緩衝區的大小、文件偏移及通知的方式,然後立即返回,當內核將數據拷貝到緩衝區後,再通知應用程序。

41. 請你說一說異步編程的事件循環

事件循環就是不停循環等待事件的發生,然後將這個事件的所有處理器,以及他們訂閱這個事件的時間順序依次依次執行。當這個事件的所有處理器都被執行完畢之後,事件循環就會開始繼續等待下一個事件的觸發,不斷往復。當同時併發地處理多個請求時,以上的概念也是正確的,可以這樣理解:在單個的線程中,事件處理器是一個一個按順序執行的。即如果某個事件綁定了兩個處理器,那麼第二個處理器會在第一個處理器執行完畢後,纔開始執行。在這個事件的所有處理器都執行完畢之前,事件循環不會去檢查是否有新的事件觸發。在單個線程中,一切都是有順序地一個一個地執行的!

42. 請你回答一下操作系統爲什麼要分內核態和用戶態

爲了安全性。在cpu的一些指令中,有的指令如果用錯,將會導致整個系統崩潰。分了內核態和用戶態後,當用戶需要操作這些指令時候,內核爲其提供了API,可以通過系統調用陷入內核,讓內核去執行這些操作。

43. 請你回答一下爲什麼要有page cache,操作系統怎麼設計的page cache

加快從磁盤讀取文件的速率。page cache中有一部分磁盤文件的緩存,因爲從磁盤中讀取文件比較慢,所以讀取文件先去page cache中去查找,如果命中,則不需要去磁盤中讀取,大大加快讀取速度。在 Linux 內核中,文件的每個數據塊最多隻能對應一個 Page Cache 項,它通過兩個數據結構來管理這些 Cache項,一個是radix tree,另一個是雙向鏈表。Radix tree 是一種搜索樹,Linux內核利用這個數據結構來通過文件內偏移快速定位Cache 項。

44. server端監聽端口,但還沒有客戶端連接進來,此時進程處於什麼狀態?

這個需要看服務端的編程模型,如果如上一個問題的回答描述的這樣,則處於阻塞狀態,如果使用了epoll,select等這樣的io複用情況下,處於運行狀態。

45. 請問如何設計server,使得能夠接收多個客戶端的請求

多線程,線程池,io複用(select、poll)

46. 請問怎麼實現線程池

(1)設置一個生產者消費者隊列,作爲臨界資源
(2)初始化n個線程,並讓其運行起來,加鎖去隊列取任務運行
(3)當任務隊列爲空的時候,所有線程阻塞
(4)當生產者隊列來了一個任務後,先對隊列加鎖,把任務掛在到隊列上,然後使用條件變量去通知阻塞中的一個線程

47. Linux下怎麼得到一個文件的100到200行

sed -n ‘100,200p’ inputfile
awk ‘NR>=100&&NR<=200{print}’ inputfile
head -200 inputfile|tail -100

48. 請你來說一下awk的使用

(1)作用:樣式掃描和處理語言。它允許創建簡短的程序,這些程序讀取輸入文件、爲數據排序、處理數據、對輸入執行計算以及生成報表,還有無數其他的功能。
(2)用法:
awk [-F field-separator] ‘commands’ input-file(s)
(3)內置變量
ARGC:命令行參數個數
ARGV:命令行參數排列
ENVIRON:支持隊列中系統環境變量的使用
FILENAME:awk瀏覽的文件名
FNR:瀏覽文件的記錄數
FS:設置輸入域分隔符,等價於命令行 -F選項
NF:瀏覽記錄的域的個數
NR:已讀的記錄數
OFS:輸出域分隔符
ORS:輸出記錄分隔符
RS:控制記錄分隔符
(4)實例:找到當前文件夾下所有的文件和子文件夾,並顯示文件大小
ls -l | awk ‘{print $5 “\t” $9}’

49. 請你來說一下linux內核中的Timer 定時器機制

低精度時鐘
Linux 2.6.16之前,內核只支持低精度時鐘,內核定時器的工作方式:
(1)系統啓動後,會讀取時鐘源設備(RTC, HPET,PIT…),初始化當前系統時間。
(2)內核會根據HZ(系統定時器頻率,節拍率)參數值,設置時鐘事件設備,啓動tick(節拍)中斷。HZ表示1秒種產生多少個時鐘硬件中斷,tick就表示連續兩個中斷的間隔時間。
(3)設置時鐘事件設備後,時鐘事件設備會定時產生一個tick中斷,觸發時鐘中斷處理函數,更新系統時鐘,並檢測timer wheel,進行超時事件的處理。
在上面工作方式下,Linux 2.6.16 之前,內核軟件定時器採用timer wheel多級時間輪的實現機制,維護操作系統的所有定時事件。timer wheel的觸發是基於系統tick週期性中斷。
所以說這之前,linux只能支持ms級別的時鐘,隨着時鐘源硬件設備的精度提高和軟件高精度計時的需求,有了高精度時鐘的內核設計。

高精度時鐘
Linux 2.6.16 ,內核支持了高精度的時鐘,內核採用新的定時器hrtimer,其實現邏輯和Linux 2.6.16 之前定時器邏輯區別:
hrtimer採用紅黑樹進行高精度定時器的管理,而不是時間輪;
高精度時鐘定時器不在依賴系統的tick中斷,而是基於事件觸發。
舊內核的定時器實現依賴於系統定時器硬件定期的tick,基於該tick,內核會掃描timer wheel處理超時事件,會更新jiffies,wall time(牆上時間,現實時間),process的使用時間等等工作。
新的內核不再會直接支持週期性的tick,新內核定時器框架採用了基於事件觸發,而不是以前的週期性觸發。新內核實現了hrtimer(high resolution timer):於事件觸發。
hrtimer的工作原理:
通過將高精度時鐘硬件的下次中斷觸發時間設置爲紅黑樹中最早到期的Timer 的時間,時鐘到期後從紅黑樹中得到下一個 Timer 的到期時間,並設置硬件,如此循環反覆。
在高精度時鐘模式下,操作系統內核仍然需要週期性的tick中斷,以便刷新內核的一些任務。hrtimer是基於事件的,不會週期性出發tick中斷,所以爲了實現週期性的tick中斷(dynamic tick):系統創建了一個模擬 tick 時鐘的特殊 hrtimer,將其超時時間設置爲一個tick時長,在超時回來後,完成對應的工作,然後再次設置下一個tick的超時時間,以此達到週期性tick中斷的需求。
引入了dynamic tick,是爲了能夠在使用高精度時鐘的同時節約能源,這樣會產生tickless 情況下,會跳過一些 tick。
新內核對相關的時間硬件設備進行了統一的封裝,定義了主要有下面兩個結構:
時鐘源設備(closk source device):抽象那些能夠提供計時功能的系統硬件,比如 RTC(Real Time Clock)、TSC(Time Stamp Counter),HPET,ACPI PM-Timer,PIT等。不同時鐘源提供的精度不一樣,現在pc大都是支持高精度模式(high-resolution mode)也支持低精度模式(low-resolution mode)。
時鐘事件設備(clock event device):系統中可以觸發 one-shot(單次)或者週期性中斷的設備都可以作爲時鐘事件設備。
當前內核同時存在新舊timer wheel 和 hrtimer兩套timer的實現,內核啓動後會進行從低精度模式到高精度時鐘模式的切換,hrtimer模擬的tick中斷將驅動傳統的低精度定時器系統(基於時間輪)和內核進程調度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章