01.02 C++服務器後臺開發面試題總結

總結;linux C/C++服務器後臺開發面試題總結。C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,RTSP RTMP HLS 流媒體 ffmpeg,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等,需要資料的後臺私聊;資料;免費領取


基礎語言知識方面:

1、使用struct關鍵字和class關鍵字定義類以及在類的繼承方面有啥區別?

(1)定義類的差別:

C語言中的struct 關鍵字也可以實現類,用class 關鍵字和struct 關鍵字定義類的唯一差別就在於默認訪問級別不同:

默認情況下,struct 成員的訪問級別為public,而class 成員的訪問級別是private 。語法使用方面都是相同,直接將class 換成struct 即可。

(2)類的繼承的差別:

使用class 關鍵字定義的類,它的派生類默認具有private 繼承,而使用struct 關鍵字定義的類,它的派生類默認具有public 繼承。其他的方面沒有區別。

因此,主要就兩個區別:默認的訪問級別和默認的繼承級別:class 都是private的,struct 都是public的。

2、派生類和虛函數的概述?

(1)基類中的虛函數被派生類繼承過去之後,是希望派生類根據自己的實際需求進行重新定義,以實現特定的功能。如果派生類沒有重新定義基類中的某個虛函數,則在調用的時候會使用基類中定義的版本。

(2)派生類中函數的聲明必須和基類中定義的方式完全匹配。

(3)基類中被聲明為虛函數,在派生類中也依然是虛函數。

3、虛函數和純虛函數的區別?

(1)帶有純虛函數的類被稱之為虛基類,也叫做抽象基類,這種類型的類是不能直接生成對象的,只能被繼承。

繼承之後,在派生類中對純虛函數進行重新定義,然後這個派生類才是我們常見的正常的類。如果在派生類中沒有對這個純虛函數重新定義,那麼這個派生類也將成為虛基類。

(2)虛函數在派生類中是可以不重新被定義的,但是純虛函數在派生類中必須得被重新定義。

4、深拷貝和淺拷貝的區別?

(1)舉例來說就是:

淺拷貝:


linux C/C++服務器後臺開發面試題總結

深拷貝:

linux C/C++服務器後臺開發面試題總結

解釋就是:

淺拷貝只是對指針的拷貝,拷貝之後,兩個指針同時指向同一個內存。

深拷貝不但對指針進行拷貝,還對指針所指向的內容進行拷貝,源指針和經過深拷貝之後的指針是指向兩個不同的地址的。

(2)淺拷貝可能出現的問題:

淺拷貝只是拷貝了指針而已,使得兩個指針同時指向同一個內存地址,這樣在對象結束調用析構函數時,會造成同一個資源被釋放兩次,造成程序崩潰。

淺拷貝使得兩個指針都指向同一塊內存,任何一方的變動都會對另一方造成影響。

5、STL各個容器的實現原理(必考查)?

(1)vector,指的是順序容器,它是一個動態數組,支持元素的隨機插入、刪除、查找等操作。

vector 在內存中是一塊連續存儲的內存空間,當在舊內存空間不夠用的情況下,它會自動分配另一個大小是舊內存空間 2倍 的新內存空間,然後把舊內存空間中的所有數據都拷貝進新內存空間中去,之後再在新內存空間中的原數據的後面繼續進行構造新元素,並且同時釋放舊內存空間,並且,由於vector 空間的重新配置,導致舊vector 的所有迭代器都失效了。

vector 中數據的隨機存取效率很高,O(1)的時間的複雜度,但是在vector 中隨機插入元素,需要移動的元素數量較多,效率比較低下。

(2)map,指的是關聯容器,它是以“鍵值對”的形式進行存儲的,方便根據關鍵字來迅速查找其對應的值。

關鍵字起到索引的作用,值則表示與索引相關聯的數據。它的底層實現結構是紅黑樹,插入元素、刪除元素等操作都在O(logN)的時間複雜度。

(3)set,指的是關聯容器,set 中存放的是關鍵字,也即值,也就是說在set 中,關鍵字就是值,兩者融為一體。

set 的底層實現結構也是紅黑樹,它同樣支持高效的元素插入、元素刪除等操作。另外,set支持高校的關鍵字檢查是否在set 中。

6、STL有7 種主要的容器:分別是:vector、list、deque、map、multimap、set、multiset。

7、C++的特點是什麼?多態實現的機制是什麼?多臺作用是什麼?兩個必要條件是什麼?

(1)C++ 中的多態機制主要體現在兩個方面:一個是函數的重載,一個是接口的重寫。

函數重載,體現的是靜態多態。

接口多態,指的是“一個接口多種形態”的意思。每一個對象內部都有一個虛函數表指針,該虛函數表指針指向該類的虛函數表,所以在程序中,不管你的對象類型如何轉換,但是該對象內部的虛函數表指針的指向始終是固定的,所以,才能實現動態的對象函數調用,這就是C++ 多態實現的原理。

多態的基礎是繼承,需要類中虛函數的支持,派生類繼承基類的大部分資源,但是不能繼承基類的構造函數、析構函數、拷貝構造函數、operator=函數、友元函數等。

(2)多態作用:

a、隱藏了函數的實現細節,代碼能夠模塊化。b、函數接口重用:為了類在繼承和派生的時候正確使用。

(3)必要條件:

一個是基類中要有虛函數,另一個是基類指針或基類引用要指向派生類的對象。

8、類的多重繼承有什麼問題?怎麼樣才能消除多重繼承中的二義性?

(1)增加了程序的複雜度,使得程序的編寫和維護比較困難,很容易出現。

(2)多重繼承,使得派生類和基類中的同名函數產生二義性問題,對於同名函數的調用,不知道調用的是派生類自己的還是基類的,要是基類的,是哪一個基類的,這是引發的問題。C++ 中使用虛繼承來消除這個二義性問題。或者使用成員限定符“作用域運算符::”來避免這個問題。

9、求兩個數的乘積和商數,怎麼用宏定義來實現?


linux C/C++服務器後臺開發面試題總結


10、什麼叫做靜態關聯,什麼叫做動態關聯?

多態中,靜態關聯指的是:程序在編譯階段,就能確定實際執行的動作,比如你是用類的對象來調用類的函數成員。

動態關聯指的是:程序運行階段才能確定執行的動作,比如多態的使用。

11、什麼叫做智能指針?常用的智能指針有哪些?智能指針的實現是怎樣的?

(1)C++11新標準中,引入了智能指針的概念。智能指針,是一個存儲指向動態分配(堆)對象指針的類,構造函數傳入普通指針,析構函數釋放指針。棧上分配,函數或程序結束後自動釋放,防止內存洩露。

(2)使用引用計數器,類與指向的對象相關聯,引用計數跟蹤該類有多少個對象共享同一指針。

創建類的新對象時,初始化指針並將引用計數置為1;

當對象作為另一對象的副本而創建時,引用計數 +1;

對一個對象進行賦值時,引用計數 -1(當引用計數減至 0 時,則刪除基礎對象),並增加右操作數所指對象的引用計數 +1;

調用析構函數時,構造函數減少引用計數,當引用計數減至 0 時,則刪除基礎對象。

(3)智能指針如下:

std::auto_ptr,不支持複製(拷貝構造函數)和賦值(operator =),編譯不會提示出錯。

unique_ptr, 不支持複製和賦值,但比 auto_ptr 好,直接賦值會編譯出錯。

shared_ptr,基於引用計數的智能指針。可隨意賦值,直到內存的引用計數為 0 的時候這個內存會被釋放。

12、析構函數可以拋出異常麼?為什麼不能拋出異常?除了資源洩漏,還有其他需要考慮的麼?

(1)C++ 標準明確規定,類的析構函數不能拋出異常、也不應該拋出異常。

(2)如果對象在運行期間出現了異常,C++ 異常處理機制則有責任去清除那些由於出現異常而導致已經失效了的對象,並釋放對象原來所分配的資源,這其實就是調用對象的析構函數來完成資源的釋放任務,所以從這個意義上來講,析構函數已經變成了異常處理機制中的一部分。


linux C/C++服務器後臺開發面試題總結

13、拷貝構造函數的作用及其用途(拷貝構造函數什麼時候會被調用?)?什麼時候需要自定義拷貝構造函數?

(1)拷貝構造函數原型的一般形式:類名(const 類名 &形參對象名) { };

構造函數的形參是本類對象的引用而不是本類的對象,是為了防止引起拷貝構造函數無休止地遞歸調用。

拷貝構造函數在如下三種情況下會被調用:


linux C/C++服務器後臺開發面試題總結

(2)如下所示:如果在類的構造函數中存在動態內存分配,那麼則必須定義“拷貝構造函數”,否則會導致兩個對象成員指向同一個地址,出現“指針懸掛”問題。(指針懸掛:指的是,如果兩個指針同時指向同一個內存地址,若我們通過其中一個指針釋放掉了該內存單元,則該內存便無效了,然而此時還有另外一個指針指向這個無效的內存單元,我們就說這個指針是懸掛指針,其指向的內存單元是不可預期的。)


linux C/C++服務器後臺開發面試題總結

(2)作用:將算法和具體的對象分離開,與類型無關,比較通用,節省精力。

17、內存的靜態分配和動態分配有什麼區別?

(1)時間不同。靜態分配是發生在程序編譯期間的。動態分配則是發生在程序執行期間的。

(2)空間不同。堆區域是動態分配的,沒有靜態分配的堆區域。棧區域有 2種 分配方式:靜態分配棧和動態分配棧。

靜態分配棧,是由編譯器完成的,比如函數局部變量的分配、比如函數形式參數的分配。

allocate類 可以從棧裡分配動態內存,不用擔心內存洩漏問題,當函數返回時,通過allocate 申請的內存就會被自動釋放。

18、深入談談堆和棧?

(1) 分配和管理方式不同。

堆內存是動態分配的,其空間的分配和釋放都是由程序員手動完成的。

棧內存是由編譯器管理的。棧有 2 種分配方式:靜態分配和動態分配。 靜態分配是由編譯器完成,比如函數局部變量的分配、比如函數形式參數的分配。 allocate類 可以從棧裡分配動態內存,不用擔心內存洩漏問題,當函數返回時,通過allocate 申請的內存就會被自動釋放。但是,這裡要注意:棧的動態分配和堆內存的動態分配是不同的,它的動態分配是由編譯器釋放的,無需手工控制。

(2)產生內存碎片不同。

對於堆內存,頻繁的new/delete 運算符和malloc/free 函數調用,勢必會造成內存空間的不連續,造成大量的內存碎片,使程序效率降低。

對於棧內存,則不存在內存碎片問題,這是因為棧這種數據結構是先進後出的,不可能有一個內存塊從棧的中間位置彈出。

(3)生長方向不同。


linux C/C++服務器後臺開發面試題總結


linux C/C++服務器後臺開發面試題總結

19、explicit 關鍵字 是幹什麼用的?

(0)參考博客:http://www.cnblogs.com/ymy124/p/3632634.html

(1)首先,C++ 中的 explicit 關鍵字只能用於修飾只有一個參數的類構造函數,它的作用是表明該構造函數是顯示的,而非隱式的。 跟它相對應的另一個關鍵字是 implicit,意思是該構造函數是隱藏的。類構造函數默認情況下即聲明為implicit( 隱式 )。

(2)explicit 關鍵字的作用就是防止 類構造函數的隱式自動轉換。

上面也已經說過了,explicit 關鍵字只對有一個參數的類構造函數有效,如果類構造函數參數大於或等於兩個時, 是不會產生隱式轉換的,所以explicit關鍵字也就無效了。

但是,也有一個例外,就是當除了第一個參數以外的其他參數都有默認值的時候,explicit關鍵字依然有效,此時,當調用構造函數時只傳入一個參數,等效於只有一個參數的類構造函數。

(3)上面的代碼中, "CxString string2 = 10;" 這句為什麼是可以的呢?


linux C/C++服務器後臺開發面試題總結

20、內聯函數和宏定義的區別?

(1)內聯函數是指在普通函數的前面加一個關鍵字 inline 來標識。對於函數調用而言,每一次函數調用都會消耗時間,所以,對於對於語句比較短小的函數,若是被頻繁調用,這時所花費的時間會遠大於“把該函數直接寫進程序執行流中,而不是去調用它”所花費的時間,因此這是很有益的。

(2)宏定義,不檢查函數參數、返回值什麼的,只是簡單的進行宏展開操作;相對來說,內聯函數會檢查參數類型,所有更安全。

(3)宏是由預處理器進行宏展開,函數內聯是通過編譯器來控制實現。

21、內存對齊的原則是什麼?

(1)結構體的整體空間大小是佔用空間最大的成員(的類型)所佔用空間字節數的整數倍。

(2)數據對齊原則:內存按照結構體成員的先後順序排列,當排列到該成員時,前面已經擺放的空間大小必須是該成員類型大小的整數倍,如果不夠則補齊之後然後再擺放該成員。


22、C 語言同意一些令人震驚的結構,下面的結構合法嗎?如果合法,則它會做些什麼?

int a=5, b=7, c;

c= a+++b;

等同於:c=a++ +b;

則執行之後:a=6,b=7,c=12。

23、string 的實現?(必須會!)


linux C/C++服務器後臺開發面試題總結


linux C/C++服務器後臺開發面試題總結


linux C/C++服務器後臺開發面試題總結

24、用變量 p 給出如下定義:

一個有10個指針的數組,每個指針都指向一個函數,該函數有一個整形參數並且返回一個整數:

int ( *p[10] ) ( int );

25、使用預處理指令#define聲明一個常數:用來表明一年有多少秒?

#define SECOND_PERYEAR 365*24*60*60(UL)

26、頭文件中的ifndef/define/endif 幹什麼用的?

(1)預處理模塊時使用。可以防止頭文件被重複使用。

extern c 的作用?

(1)告訴編譯器該段代碼使用C語言進行編譯。

27、volatile 是幹什麼用的?使用實例有哪些?( 必須將CPU 的寄存器緩存機制回答的很透徹。考察!)

(1)訪問寄存器比訪問內存單元要快,編譯器會優化減少內存的讀取,聲明變量為volatile 類型的,編譯器不會再對訪問該變量的代碼優化,仍然從內存讀取,使訪問穩定。

volatile 關鍵字,會影響編譯器編譯的結果,用 volatile 關鍵字聲明的變量便是該變量隨時可能發生變化,與該變量有關的運算,不再進行編譯優化,以免出錯。

28、

29、

30、

數據結構和算法方面:

1、100 萬個32位的整數,如何最快的找到中位數?能夠保證每個數都是唯一的,如何實現O(n)的算法?

(0)中位數定義:數字排序之後,位於中間的那個數。比如將100億個數字進行排序,排序之後,位於第50億個位置的那個數 就是中位數。

(1)內存足夠的時候,可以使用快速排序法,時間複雜度是O(NlongN),排完序之後直接找到那個處於中間位置的數字即可。

(2)內存不足的時候,可以使用分桶法。

假設100萬 個數字保存在一個大文件中,首先依次讀一部分文件到內存(不超過內存的限制),將每個數字用二進制表示,比較二進制的最高位( 第32位,符號位,0是正,1是負 ),如果數字的最高位為 0,則將這個數字寫入 file_0 文件中;如果最高位為 1,則將該數字寫入 file_1 文件中。

從而將 100萬 個數字分成了兩個文件:假設 file_0文件中有 60萬 個數字,file_1文件中有 40萬 個數字。 那麼中位數就在 file_0 文件中,並且是 file_0 文件中所有數字排序之後的第 10萬 個數字。(這是因為: file_1 中的數都是負數,file_0 中的數都是正數,也即這裡一共只有 40萬 個負數,那麼排序之後的第 50萬 個數一定位於 file_0 中。)

現在,我們只需要處理 file_0 文件就可以了(不需要再考慮file_1文件)。 對於 file_0 文件,同樣採取上面的措施處理:將file_0 文件依次讀一部分到內存( 不超內存限制 ),

將每個數字用二進制表示比較二進制的 次高位(第31位),如果數字的次高位為 0,寫入 file_0_0 文件中;如果次高位為 1,寫入 file_0_1 文件 中。


  • 現假設 file_0_0 文件中有 30萬 個數字,file_0_1中也有 30萬 個數字,則中位數就是:file_0_0 文件中的數字從小到大排序之後的第 10萬 個數字。

拋棄 file_0_1 文件,繼續對 file_0_0 文件 根據 次次高位( 第30位 ) 劃分,假設此次劃分的兩個文件為:file_0_0_0 中有5萬個數字,file_0_0_1中有25萬個數字,那麼中位數就是 file_0_0_1 文件中的所有數字排序之後的 第 5萬 個數。

按照上述思路,直到劃分的文件可直接加載進內存時,就可以直接對數字進行快速排序,找出中位數了。

2、

3、

4、

5、

6、

7、

8、


服務器編程方面:

1、多線程和多進程的區別(考察!)

(從CPU調度、上下文切換、數據共享、多核CPU利用率、資源佔用等方面回答。然後,有一個問題必須會被問到:哪些東西是一個線程私有的?答案中一定得包含寄存器。)

(1)進程與進程之間的數據空間是分開的,如果要在進程之間進行通信,需要使用特殊的IPC機制,比如管道、信號量、共享內存、消息隊列。

線程是存在於進程內的,線程之間共享進程的堆區間、全局靜態存儲區,而各自享有自己獨立的棧空間。線程之間共享數據比較簡單,但是線程之間的同步比較複雜,線程同步方法比如說使用互斥量mutex、信號量semaphore。

(2)進程的創建、銷燬、切換複雜,速度比較慢。線程的創建、銷燬、切換簡單,速度快。

(3)進程佔用內存多,CPU利用率較低。線程佔用內存少,CPU利用率高。

(4)進程之間不會相互影響。進程的一個線程掛掉則會導致整個進程掛掉。

(5)進程適應於多核多機分佈,線程適應於多核。

線程所私有的內容:

線程ID號、寄存器的值、棧內存、線程的調度策略、線程的私有數據、信號屏蔽字、errno 變量。

2、多線程的鎖的種類有哪些?

(1)互斥鎖mutex、自旋鎖spin lock、讀寫鎖read/write。

3、自旋鎖和互斥鎖的區別是什麼?(考察!)

(1)自旋鎖的定義和優缺點:

自旋鎖,它不會引起調用者睡眠,如果自旋鎖已經被別的某一線程保持,那麼該調用者不會進入睡眠狀態,而是一直循環在那裡看著 該自旋鎖的保持者是否釋放了該自旋鎖。它的作用就是為了某項資源的互斥使用。因為自旋鎖不會引起調用者進入睡眠狀態,所以自旋鎖的效率遠高於互斥鎖。雖然自旋鎖的效率比較高,但是它仍然有一些不足之處:

自旋鎖的調用者在未獲得鎖的情況下還一直在“自旋運行”、佔用CPU,如果不能在短時間內獲得所需要的“鎖”,這無疑會使CPU的利用率下降。所以,自旋鎖適用於鎖使用者保持鎖時間比較短的情況下。

使用自旋鎖時,在遞歸調用的時候有可能造成死鎖。

(2)兩種鎖的加鎖原理:

互斥鎖:線程會從sleep(加鎖)——>running(解鎖),過程中有上下文的切換、CPU的搶佔、信號的發送等開銷。

自旋鎖:線程一直是running(加鎖——>解鎖)、死循環檢測鎖的標誌位、機制不復雜。

(3)閒等鎖和忙等鎖:

互斥鎖屬於 sleep-waiting 類型的鎖。(閒等待的鎖)

自旋鎖是屬於 busy-waiting 類型的鎖。(忙等待的鎖)

(要能說出來這個例子!)

例如,在一個雙核的機器上有兩個線程 ( 線程A和線程B ),它們分別運行在 內核Core0 和 內核Core1上。

現在假設,線程A 想要通過 pthread_mutex_lock 操作 去得到一個臨界區的鎖,而此時這個鎖正被線程B 所持有,那麼線程A 就會被阻塞 (blocking),內核Core0 會在此時進行上下文切換 ( Context Switch ) 將線程A 置於等待隊列中,此時 內核Core0 就可以運行其他的任務 ( 例如另一個線程C ) 而不必進行忙等待。

但是,自旋鎖則不然,它是屬於 busy-waiting 類型的鎖(忙等待的鎖)。如果線程A 是使用 pthread_spin_lock 操作 去請求鎖,那麼線程A 就會一直在 內核Core0 上進行忙等待並不停的進行鎖請求,直到得到這個鎖為止。

(4)兩種鎖的應用場景:

自旋鎖:主要就是用在臨界區持鎖時間非常短、而且CPU資源不緊張的情況下,自旋鎖一般用於多核的服務。

互斥鎖:主要用於臨界區持鎖時間比較長的操作,比如下面這些情況都可以考慮:1)臨界區有IO操作。2)臨界區循環量比較大。3)單核處理器。

另:

線程同步是並行編程中非常重要的通訊手段,其中最典型的應用就是用

Pthreads 提供的鎖機制(lock)來對多個線程之間的共享臨界區(Critical Section)進行保護。

Pthreads提供了多種鎖機制:


linux C/C++服務器後臺開發面試題總結


4、進程之間通信的方式有哪些?線程之間通信的方式有哪些?(考察!)

7 種進程間的通信方式:

(1) 管道(pipe):管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有血緣關係的進程間使用。進程的血緣關係通常指父子進程關係。

(2)有名管道(named pipe):有名管道也是半雙工的通信方式,但是它允許無親緣關係進程間通信。

(3)信號量(semophore):信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它通常作為一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作為進程間以及同一進程內不同線程之間的同步手段。

(4)消息隊列(message queue):消息隊列是由消息組成的鏈表,存放在內核中 並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少,管道只能承載無格式字節流以及緩衝區大小受限等缺點。

(5)信號處理機制(signal):信號是一種比較複雜的通信方式,用於通知接收進程某一事件已經發生。

(6)共享內存(shared memory):共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問,共享內存是最快的IPC方式,它是針對其他進程間的通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號量配合使用,來實現進程間的同步和通信。

(7)套接字(socket):套接字也是一種進程間的通信機制,與其他通信機制不同的是它可以用於不同及其間的進程通信。

3種 線程間的通信機制:

(1)鎖機制:

1.1 互斥鎖 mutex:提供了以排它方式阻止數據結構被併發修改的方法。

1.2 讀寫鎖 read/write:允許多個線程同時讀共享數據,而對寫操作互斥。

1.3 條件變量 condtion variable:可以以原子的方式阻塞進程,直到某個特定條件為真為止。對條件測試是在互斥鎖mutex 的保護下進行的。條件變量始終與互斥鎖一起使用。

(2)信號量機制:比如常用的二進制信號量0和1。

(3)信號處理器機制:類似於進程間的信號處理。

線程之間的通信,主要目的是用於線程之間的同步,所以,線程沒有象進程通信中用於數據交換的通信機制。

5、進程和線程的區別?(考察!)

(1)定義:

進程,它是操作系統進行資源分配的一個獨立單位,它是具有一定獨立功能的程序關於某個數據集合上的一次執行過程。

線程,它是進程內部的一個實體,是CPU 調度的基本單位,它是比進程更小的能獨立運行的基本單位。線程自己基本上不擁有系統資源,它只擁有一點在運行期間必不可少的資源(如程序計數器PC,一組寄存器和棧等)。但是,它可以與同屬於一個進程的其他線程 共享進程所擁有的資源。

(2)關係:

一個線程可以創建和撤銷另一個線程。同一個進程內部的多個線程之間可以併發執行。

相對進程而言,線程是一個更加接近於代碼執行體的概念,它可以與同進程中的其他線程共享數據,但擁有自己獨立的棧空間,擁有獨立的執行序列。

(3)區別:

進程和線程的主要差別在於,它們是不同的操作系統資源管理方式。

進程具有獨立的地址空間,一個進程崩潰後,在保護模式下不會對其它進程產生影響。

而線程,它只是一個進程內部的不同代碼執行流。線程有自己的棧空間(存儲局部變量),但是,線程之間沒有單獨的地址空間,一個線程死掉就等於整個進程死掉。

所以,多進程的程序要比多線程的程序更健壯。但是在不同的進程之間切換時,耗費資源較大,效率要差一些。

對於一些要求同時執行並且又要共享某些變量的併發操作,只能用線程,不能用進程。

1)簡而言之,一個程序至少有一個進程,一個進程至少有一個線程。

2)線程的劃分尺度小於進程,使得多線程程序的併發性高。

3)另外,進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率。

4)線程在執行過程中與進程還是有區別的。每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。

5)從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分可以同時執行。但操作系統並沒有將多個線程看做多個獨立的應用,來實現進程的調度和管理以及資源分配。這就是進程和線程的重要區別。

6、多線程程序架構,線程數量應該如何設置?

(1)在多線程程序架構中,線程的數量應該和主機CPU 的核數相等,或者應該等於CPU核數+1 的個數。

另:

在進行進一步深入討論之前,先以提問的方式就一些共性認知達成一致。

提問:工作線程數是不是設置的越大越好?

回答:不是的。

1)服務器的CPU核數 有限,同時併發的線程數也是有限的,1核CPU設置10000個工作線程沒有意義。

2)線程之間的切換是有開銷的,如果線程切換過於頻繁,反而會使性能降低。

提問:調用sleep() 函數的時候,線程是否一直佔用CPU?

回答:不佔用,調用sleep() 等待時會把CPU 讓出來,給其他需要CPU 資源的線程使用。

不止調用sleep() 函數,在進行一些阻塞調用,例如網絡編程中的阻塞 accept()【等待客戶端連接】和 阻塞 recv()【等待下游回包】也不佔用CPU資源。

提問:如果CPU是單核,設置多線程有意義麼,能提高併發性能麼?

回答:即使是單核,使用多線程也是有意義的。

1)多線程編碼可以讓我們的服務/代碼更加清晰:有些IO線程收發包,有些Worker線程進行任務處理,有些Timeout線程進行超時檢測。

2)如果有一個任務一直佔用CPU資源在進行計算,那麼此時增加線程並不能增加併發。

例如這樣的一個代碼:

while(1) { i++; }

該代碼一直不停的佔用CPU資源進行計算,會使CPU佔用率達到100%。

3)通常來說,Worker線程一般不會一直佔用CPU進行計算,此時即使CPU是單核,增加Worker線程也能夠提高併發,因為這個線程在休息的時候,其他的線程可以繼續工作。

7、socket 套接字編程中,如果客戶端client 突然斷電了,那麼服務器如何快速的知道呢?

(1)使用定時器(適合有數據流動的情況)。

(2)使用socket選項SO_KEEPALIVE(適合沒有數據流動的情況)。

心跳包的發送,通常有兩種技術:

心跳包技術:心跳包之所以叫心跳包是因為:它像心跳一樣每隔固定時間發一次,以此來告訴服務器,這個客戶端還活著。事實上這是為了保持長連接,至於這個包的內容,是沒有什麼特別規定的,不過一般都是很小的包,或者只包含包頭的一個空包。

方法1:應用層自己實現的心跳包。

由應用程序自己發送心跳包來檢測連接是否正常。

大致的方法是:服務器端在一個 定時事件中 定時向客戶端發送一個短小的數據包,然後啟動一個線程,在該線程當中不斷檢測客戶端的ACK應答包。如果在定時時間內收到了客戶端的ACK應答包,說明客戶端與服務器端的TCP連接仍然是可用的。但是,如果定時器已經超時、而服務器仍然沒有收到客戶端的ACK應答包,即可以認為客戶端已經斷開。

同樣道理,如果客戶端在一定時間內沒有收到服務器的心跳包,則也會認為改TCP連接不可用了。

方法2:TCP協議的KeepAlive保活機制。

因為要考慮到一個服務器通常會連接很多個客戶端,因此,由用戶在應用層自己實現心跳包,代碼較多而且稍顯複雜。

而利用TCP/IP協議層的內置的KeepAlive功能來實現心跳功能則簡單得多。不論是服務器端還是客戶端,只要一端開啟KeepAlive功能後,就會自動的在規定時間內向對端發送心跳包, 而另一端在收到心跳包後就會自動回覆,以告訴對端主機我仍然在線。

因為開啟KeepAlive功能需要消耗額外的寬帶和流量,所以TCP協議層默認是不開啟KeepAlive功能的。儘管這微不足道,但是在按流量計費的環境下增加了費用,另一方面,KeepAlive設置不合理的話有可能會 因為短暫的網絡波動而斷開健康的TCP連接。

並且,默認的KeepAlive超時需要即2小時,探測次數為5次。對於很多服務端應用程序來說,2小時的空閒時間太長。因此,我們需要手工開啟KeepAlive功能並設置合理的KeepAlive參數。

8、基於UDP協議的服務器端和客戶端通信,調用connect 函數有什麼作用?

(1)因為UDP協議可以一對一通信、一對多通信或者多對一通信,所以在每次調用數據IO函數 sendto( )或者recvfrom( ) 的時候都必須要指定目標主機的IP地址和端口號。

通過調用connect( )函數來建立一個端到端的UDP連接,就可以像TCP一樣使用send( )或者recv( ) 傳遞數據了,而不需要每次都指定目標主機的IP地址和端口號。但是它和TCP不同的是它沒有三次握手的過程。

(2)可以通過在已經建立連接的UDP套接字上調用connect( ) 函數來實現指定新的IP地址和端口號,來建立新的UDP連接。

9、socket 套接字在什麼情況下是可讀的?

(1)首先,先來介紹幾個概念:每個套接字有一個接收緩衝區低水位和一個發送緩衝區低水位。他們是由select函數使用!

用於讀的 接收緩衝區低水位標記:是讓select函數返回"可讀"時套接字接收緩衝區當中所需要的數據量。對於TCP而言,其默認值為1字節。

通俗講:套接字接收緩衝區的作用就是,接收對端發送過來的數據存放在緩衝區當中,供應用程序讀。當然了,只有當緩衝區可讀的數據量到達一定程度(接收低水位標記:eg: 1)的時候,我們才能讀到數據,不然就不能夠從緩衝區當中讀到數據。

用於寫的 發送緩衝區低水位標記: 是讓select函數返回"可寫"時套接字發送緩衝區當中所需要的可用空間大小。對於TCP而言,其默認值常為2048字節。

通俗講:套接字發送緩衝區的作用就是,發送應用程序的數據到發送緩衝區當中,然後一起發給對端。當然了,只有當緩衝區剩餘的空間大小達到一定程度(發送低水位標記:eg: 2048)的時候,你才能寫數據進去,不然可能導致寫空間不夠而出現問題。

(2)下列四個條件中的任何一個滿足時,socket準備好讀:

socket套接字的接收緩衝區當中的數據量大於等於該套接字的接收緩衝區低水位標記的當前大小時。對這樣的socket的讀操作將不阻塞、並返回一個大於0的值(也就是返回準備好讀入的數據)。

該連接的“讀功能”這一條線被關閉(也就是接收了FIN數據包的TCP連接)。對這樣的socket的讀操作將不阻塞、並返回0。

socket是一個監聽套接字,並且已經完成的連接數為非0。這樣的監聽套接字處於可讀狀態,是因為socket收到了對方的connect請求,執行了三次握手中的第一步:對方發送SYN請求過來,使監聽套接字處於可讀狀態。

有一個socket有異常錯誤條件待處理。 對於這樣的socket的讀操作將不會阻塞,並且會返回一個錯誤(-1),errno全局變量則設置成明確的錯誤條件。這些待處理的錯誤也可以通過指定socket選項SO_ERROR調用getsockopt來取得並清除。

(3)下列三個條件中的任何一個滿足時,socket準備好寫:

socket的發送緩衝區當中的數據量大於等於該socket的發送緩衝區低水位標記的當前大小時。對這樣的socket的寫操作將不阻塞、並返回一個大於0的值(也就是返回準備好寫入的數據量)。對於TCP和UDP的socket而言,其缺省值為2048。

該連接的“寫功能”這一條線被關閉。對這樣的socket的寫操作將產生SIGPIPE信號,該信號的缺省行為是終止進程。

有一個socket異常錯誤條件待處理。 對於這樣的socket的寫操作將不會阻塞並且返回一個錯誤(-1),errno則設置成明確的錯誤條件。這些待處理的錯誤也可以通過指定socket選項SO_ERROR調用getsockopt函數來取得並清除。

10、有一個計數器,多個線程都需要更新,會遇到什麼情況,原因是什麼,應該如何做呢?

(1)某有可能一個線程更新的數據已經被另外一個線程更新過了,更新的數據就會出現異常。

(2)方法:可以對這個計數器的操作代碼加鎖,保證計數器的更新只會被一個線程完成。

11、什麼是原子操作?

(1)原子操作,指的是不會被線程調度機制打斷的操作。這種操作一旦開始,就會一直運行下去直到結束,中間是不會有任何的上下文切換的。

(2)如果原子操作過程中出現了異常,那麼之前所做的操作全部都原樣撤回,撤回到執行這次原子操作之前的初始狀態。

12、網絡編程設計模式,reactor模式、proactor模式的區別?(考察!)

(1)reactor模式,是一種同步IO模式。proactor模式,指的是異步IO模式。

Reactor模式和Proactor模式,最主要的區別就是:真正的讀取和寫入操作是由誰來完成的。

Reactor模式是需要應用程序自己讀取數據或者寫入數據的。

Proactor模式中,應用程序是不需要進行實際的讀寫操作的,直接可以獲得讀寫操作的結果。

(2)Reactor模式:

主線程向epoll 例程當中註冊套接字socket 讀請求事件,然後主線程調用epoll_wait 函數來等待這個讀請求事件的發生。當某一時刻套接字socket 上有可讀數據時,主線程便把套接字socket 上的這個可讀事件放入服務器端的請求隊列中。然後,睡眠在請求隊列上的某個工作線程被喚醒,由這個被喚醒的工作線程來處理客戶端請求,然後再向epoll 例程當中註冊這個套接字socket 寫請求事件,之後主線程會調用epoll_wait 函數來等待這個寫請求事件的發生。當有事件可寫的時候,主線程便把套接字socket 上的可寫事件也放入請求隊列中,之後睡眠在請求隊列上的某個工作線程被喚醒,由它來處理客戶端的請求。

(3)Proactor模式:

主線程調用aio_read 函數向epoll 例程當中註冊socket 讀完成事件,並告訴內核用戶讀緩衝區的位置、以及讀完成之後如何通知應用程序的方式,之後,主線程則繼續處理其他邏輯。一旦當socket 上的數據完全被讀入用戶緩衝區之後,通過信號告訴應用程序數據已經讀取完成、可以使用了。應用程序則直接使用這個結果。

應用程序預先定義好的信號處理函數選擇一個工作線程來處理客戶請求。工作線程處理完客戶請求之後調用aio_write 函數向epoll 例程當中註冊socket 寫完成事件,並告訴內核寫緩衝區的位置、以及寫完成時如何通知應用程序的方式,之後,主線程則處理其他邏輯。一旦當用戶緩衝區的數據被寫入socket 之後,內核便嚮應用程序發送一個信號,來通知應用程序數據已經發送完成。應用程序預先定義的數據處理函數就會完成工作。

(4)半同步半異步模式:

上層的任務(比如數據文件的傳輸)使用同步IO模式,簡化了編寫並行程序的難度。

底層的任務(比如網絡控制器的中斷處理)使用異步IO模式,提高了執行效率。

13、阻塞IO、非阻塞同步IO、非阻塞異步IO的區別?(考察!)

(1)系統IO操作可分為 阻塞IO、 非阻塞同步IO、非阻塞異步型IO 三種。

(2)阻塞IO,意味著控制權直到調用操作結束之後才會回到調用者手裡。 當調用函數時,調用者被阻塞了, 這段時間內調用者做不了任何其它事情。更鬱悶的是,在等待IO結果的這段時間裡,調用者所在線程此時也無法騰出手來去響應其它客戶端請求,比較浪費資源。

比如系統調用read( )操作:調用此函數的代碼會一直僵在此處,直到它所讀的socket緩存中有數據到來為止。

(3)非阻塞同步IO,是會立即返回控制權給調用者的。 調用者不需要等待調用操作結束這麼一個過程,它可以立即從調用的函數那裡獲取兩種結果:要麼此次系統調用成功進行了,返回成功的結果;要麼系統調用返回錯誤標識errno 來告訴調用者當前資源不可用。

比如系統調用read( )操作:如果當前socket有數據可讀,則調用成功了,直接返回讀取數據的字節數。但是,如果當前socket無數據可讀,則會立即返回EAGIN信號,告訴調用者“數據還沒有準備好,請稍後再試”。

(4)非阻塞異步IO,與非阻塞同步IO稍有不同。系統調用在立即返回的時候,它還告訴調用者,這次請求已經開始了。系統便會使用另外的資源或者線程來完成這次請求操作,並在完成的時候告訴調用者(比如通過回調函數的方式),這次請求已經完成,來讀取你的調用結果吧。

比如說對於aio_read( )函數,調用者調用該函數之後,該函數會立即返回,操作系統便會在後臺同時開始讀操作,並且會在讀操作結束的時候通知調用者來取調用結果。

在以上三種IO形式中,非阻塞異步是性能最高、伸縮性最好的。

總結;linux C/C++服務器後臺開發面試題總結。C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,RTSP RTMP HLS 流媒體 ffmpeg,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等,需要資料的後臺私聊;資料;免費領取


分享到:


相關文章: