系統軟件工程師面試題

1、c++語言部分html

1. extern c

將讓 C++ 中的函數名具有 C-linkage 性質,目的是讓 C 代碼在調用這個函數時,能正確的連接到具體的地址。前端

C調用C++,使用extern "C"則是告訴編譯器依照C的方式來編譯封裝接口,固然接口函數裏面的C++語法仍是按C++方式編譯。java

而C++調用C,extern "C" 的做用是:讓C++鏈接器找調用函數的符號時採用C的方式node

函數的具體定義可有可無,仍舊使用 C++ 編譯react

------------- 額外的廢話linux

C++ 中函數有重載,使用函數名 + 參數信息做爲連接時的惟一 ID。ios

C 中函數沒有重載,只使用函數名做爲連接時的惟一 ID。c++

C編譯器編譯代碼生成的obj文件的符號表內,函數名稱保持原樣,好比int add(int,int)函數在符號表內就叫作add;C++編譯器編譯C++代碼生成的obj文件符號表內,由於有overload的存在,函數名稱的符號再也不是原來的好比add,而是相似_Z3addii這樣的(這是個人g++結果)。git

那麼,一個C程序須要使用某個C++庫內的add函數時,C程序這邊指望的是add,但C++庫內是_Z3addii這樣的,不匹配嘛對不對,因此連接階段要報錯,說找不到add這個函數。程序員

一樣,一個C++程序須要使用某個C庫內的add函數,C++程序這邊指望的是_Z3addii,但C庫內是add這樣的,一樣不匹配,連接階段也是報錯,此次是說找不到_Z3addii。

extern "C"的意思,是讓C++編譯器(不是C編譯器,並且是編譯階段,不是連接階段)在編譯C++代碼時,爲被extern 「C」所修飾的函數在符號表中按C語言方式產生符號名(好比前面的add),而不是按C++那樣的增長了參數類型和數目信息的名稱(_Z3addii)。

展開來細說,就是:

若是是C調用C++函數,在C++一側對函數聲明加了extern "C"後符號表內就是add這樣的名稱,C程序就能正常找到add來調用;若是是C++調用C函數,在C++一側在聲明這個外部函數時,加上extern "C"後,C++產生的obj文件符號表內就也是標記爲它須要一個名爲add的外部函數,這樣配合C庫,就一切都好。

總結:

無論是C代碼調用C++編譯器生成的庫函數,仍是C++代碼調用C編譯器生成的庫函數,都須要在C++代碼一側對相應的函數進行extern 「C」申明。

複製代碼 代碼以下:

extern "C"  
{  
    int func(int);  
    int var;  
}  


它的意思就是告訴編譯器將extern 「C」後面的括號裏的代碼當作C代碼來處理,固然咱們也能夠以單條語句來聲明

複製代碼 代碼以下:

extern "C" int func(int);  
extern "C" int var;  

這樣就聲明瞭C類型的func和var。不少時候咱們寫一個頭文件聲明瞭一些C語言的函數,而這些函數可能被C和C++代碼調用,當咱們提供給C++代碼調用時,須要在頭文件里加extern 「C」,不然C++編譯的時候會找不到符號,而給C代碼調用時又不能加extern 「C」,由於C是不支持這樣的語法的,常見的處理方式是這樣的,咱們以C的庫函數memset爲例

複製代碼 代碼以下:

#ifdef __cplusplus  
extern "C" {  
#endif  
  
void *memset(void*, int, size_t);  
  
#ifdef __cplusplus  
}  
#endif  


其中__cplusplus是C++編譯器定義的一個宏,若是這份代碼和C++一塊兒編譯,那麼memset會在extern "C"裏被聲明,若是是和C代碼一塊兒編譯則直接聲明,因爲__cplusplus沒有被定義,因此也不會有語法錯誤。這樣的技巧在系統頭文件裏常常被用到。

2. volatile/memory barriar

做者:Gomo Psivarh
連接:https://www.zhihu.com/question/31459750/answer/52069135
來源:知乎

C/C++多線程編程中不要使用volatile。
(注:這裏的意思指的是期望volatile解決多線程競爭問題是有很大風險的,除非所用的環境系統不可靠纔會爲了保險加上volatile,或者是從極限效率考慮來實現很底層的接口。這要求編寫者對程序邏輯走向很清楚才行,否則就會出錯)

C++11標準中明確指出解決多線程的數據競爭問題應該使用原子操做或者互斥鎖。
C和C++中的volatile並非用來解決多線程競爭問題的,而是用來修飾一些由於程序不可控因素致使變化的變量,好比訪問底層硬件設備的變量,以提醒編譯器不要對該變量的訪問擅自進行優化。

多線程場景下能夠參考《Programming with POSIX threads》的做者Dave Butenhof對
Why don't I need to declare shared variables VOLATILE?
這個問題的解釋:
comp.programming.threads FAQ

簡單的來講,對訪問共享數據的代碼塊加鎖,已經足夠保證數據訪問的同步性,再加volatile徹底是畫蛇添足。
若是光對共享變量使用volatile修飾而在可能存在競爭的操做中不加鎖或使用原子操做對解決多線程競爭沒有任何卵用,由於volatile並不能保證操做的原子性,在讀取、寫入變量的過程當中仍然可能被其餘線程打斷致使意外結果發生。

 

做者:Name5566
連接:https://www.zhihu.com/question/20228202/answer/24959876
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

首先須要明確的是,程序在運行起來,內存訪問的順序和程序員編寫的順序不必定一致,基於這個前提下,Memory barrier 就有存在的必要了。看一個例子:

x = r; y = 1; 

這裏,y = 1 在實際運行中可能先於 x = r 進行。實際上,在單線程環境中,這兩句誰先執行誰後執行都沒有任何關係,它們之間不存在依賴關係,可是若是在多線程中 x 和 y 的賦值存在隱式依賴時:

// thread 1 while (!x); // memory barrier assert(y == r); // thread 2 y = r; // memory barrier x = 1; 

此代碼斷言就可能失敗。

Memory barrier 可以保證其以前的內存訪問操做先於其後的完成。若是說到 Memory barrier 經常使用的地方,那麼包括:

  1. 實現鎖機制
  2. 用於驅動程序
  3. 編寫無鎖的代碼

這裏篇幅有限,若是你做爲程序員,你能夠從 一文入手研究,若是這個還不能知足你,能夠進一步深刻硬件來研究多 CPU 間內存亂序訪問的問題:

我我的也對 Memory barrier 作了一點小研究,主要寫了幾個例子驗證亂序的存在:

 

3. dynamic cast

reinterpret_cast運算符是用來處理無關類型之間的轉換;它會產生一個新的值,這個值會有與原始參數(expressoin)有徹底相同的比特位

reinterpret_cast用在任意指針(或引用)類型之間的轉換;以及指針與足夠大的整數類型之間的轉換;從整數類型(包括枚舉類型)到指針類型,無視大小。

MSDN的Visual C++ Developer Center 給出了它的使用價值:用來輔助哈希函數。下邊是MSNDN上的例子:

                // expre_reinterpret_cast_Operator.cpp // compile with: /EHsc #include <iostream> // Returns a hash code based on an address unsigned short Hash( void *p ) { unsigned int val = reinterpret_cast<unsigned int>( p ); return ( unsigned short )( val ^ (val >> 16)); } using namespace std; int main() { int a[20]; for ( int i = 0; i < 20; i++ ) cout << Hash( a + i ) << endl; }

C++中的類型轉換分爲兩種:

1.隱式類型轉換;
2.顯式類型轉換。

而對於隱式變換,就是標準的轉換,在不少時候,不經意間就發生了,好比int類型和float類型相加時,int類型就會被隱式的轉換位float類型,而後再進行相加運算。而關於隱式轉換不是今天總結的重點,重點是顯式轉換。在標準C++中有四個類型轉換符:static_cast、dynamic_cast、const_cast和reinterpret_cast;下面將對它們一一的進行總結。

static_cast

static_cast的轉換格式:static_cast <type-id> (expression)

將expression轉換爲type-id類型,主要用於非多態類型之間的轉換,不提供運行時的檢查來確保轉換的安全性。主要在如下幾種場合中使用:

1.用於類層次結構中,基類和子類之間指針和引用的轉換;
當進行上行轉換,也就是把子類的指針或引用轉換成父類表示,這種轉換是安全的;
當進行下行轉換,也就是把父類的指針或引用轉換成子類表示,這種轉換是不安全的,也須要程序員來保證;

2.用於基本數據類型之間的轉換,如把int轉換成char,把int轉換成enum等等,這種轉換的安全性須要程序員來保證;

3.把void指針轉換成目標類型的指針,是及其不安全的;

注:static_cast不能轉換掉expression的const、volatile和__unaligned屬性。

dynamic_cast

dynamic_cast的轉換格式:dynamic_cast <type-id> (expression)

將expression轉換爲type-id類型,type-id必須是類的指針、類的引用或者是void *;若是type-id是指針類型,那麼expression也必須是一個指針;若是type-id是一個引用,那麼expression也必須是一個引用。

在C++的面對對象思想中,虛函數起到了很關鍵的做用,當一個類中擁有至少一個虛函數,那麼編譯器就會構建出一個虛函數表(virtual method table)來指示這些函數的地址,假如繼承該類的子類定義並實現了一個同名並具備一樣函數簽名(function siguature)的方法重寫了基類中的方法,那麼虛函數表會將該函數指向新的地址。此時多態性就體現出來了:當咱們將基類的指針或引用指向子類的對象的時候,調用方法時,就會順着虛函數表找到對應子類的方法而非基類的方法。

固然虛函數表的存在對於效率上會有必定的影響,首先構建虛函數表須要時間,根據虛函數表尋到到函數也須要時間。

由於這個緣由若是沒有繼承的須要,通常沒必要在類中定義虛函數。可是對於繼承來講,虛函數就變得很重要了,這不只僅是實現多態性的一個重要標誌,同時也是dynamic_cast轉換可以進行的前提條件。

假如去掉上個例子中Stranger類析構函數前的virtual,那麼語句
Children* child_r = dynamic_cast<Children*> (stranger_r);

在編譯期就會直接報出錯誤,具體緣由不是很清楚,我猜想多是由於當類沒有虛函數表的時候,dynamic_cast就不能用RTTI來肯定類的具體類型,因而就直接不經過編譯。

對於從子類到基類的指針轉換,static_cast和dynamic_cast都是成功而且正確的(所謂成功是說轉換沒有編譯錯誤或者運行異常;所謂正確是指方法的調用和數據的訪問輸出是指望的結果),這是面向對象多態性的完美體現。

從基類到子類的轉換,static_cast和dynamic_cast都是成功的,可是正確性方面,我對二者的結果都先進行了是否非空的判別:dynamic_cast的結果顯示是空指針,而static_cast則是非空指針。但很顯然,static_cast的結果應該算是錯誤的,子類指針實際所指的是基類的對象,而基類對象並不具備子類的Study()方法(除非媽媽又想去接受個"繼續教育")。

對於沒有關係的兩個類之間的轉換,輸出結果代表,dynamic_cast依然是返回一個空指針以表示轉換是不成立的;static_cast直接在編譯期就拒絕了這種轉換。

 

四、malloc/ new 

new的功能是在堆區新建一個對象,並返回該對象的指針。

所謂的【新建對象】的意思就是,將調用該類的構造函數,由於若是不構造的話,就不能稱之爲一個對象。

而malloc只是機械的分配一塊內存,若是用mallco在堆區建立一個對象的話,是不會調用構造函數的

 

linux採用的是glibc中堆內存管理ptmalloc實現,虛擬內存的佈局規定了malloc申請位置以及大小, malloc一次性能申請小內存(小於128KB),分配的是在堆區(heap),用sbrk()進行對齊生長,而 malloc一次性申請大內存(大於128KB時)分配到的是在映射區,而不是在堆區,採用的mmap()系統調用進行映射。固然虛擬地址只是規定了一種最理想的狀態,實際分配仍是要考慮到物理內存加交換內存總量的限制,由於每次分配,特別是大內存分配採用mmap()映射內存須要記錄物理內存加交換內存地址,全部物理內存加交換內存限制了malloc實際分配。
malloc的實現與物理內存天然是無關的,內核爲每一個進程維護一張頁表,頁表存儲進程空間內每頁的虛擬地址,頁表項中有的虛擬內存頁對應着某個物理內存頁面,也有的虛擬內存頁沒有實際的物理頁面對應。不管malloc經過sbrk仍是mmap實現, 分配到的內存只是虛擬內存,並且只是虛擬內存的頁號,表明這塊空間進程能夠用,實際上尚未分配到實際的物理頁面。等你的進程訪問到這個 新分配的內存空間的時候,若是其尚未對應的物理頁面分配,就會產生缺頁中斷,內核這個時候會給進程分配實際的物理頁面,以與這個未被映射的虛擬頁面對應起來

連接:https://www.zhihu.com/question/20220583/answer/28490955

五、內存對齊

  2.爲何要字節對齊  
  爲何呢?簡單點說:爲了提升存取效率。字節是內存空間分配的最小單位, 在程序中,咱們定義的變量能夠放在任何位置。其實不一樣架構 的CPU在訪問特定類型變量時是有規律的,好比有的CPU訪問int型變量時,會從偶數地址開始讀取的,int類型佔用4個字節(windows平臺)。 0X0000,0X0004,0X0008.....這樣只須要讀一次就能夠讀出Int類型變量的值。相反地,則須要讀取二次,再把高低字節相拼才能獲得 int類型的值,這樣子看的話,存取效率固然提升了。  一般寫程序的時候,不須要考慮這些狀況,編譯都會爲咱們考慮這些狀況,除非針對那些特別架構的 CPU編程的時候的則須要考慮 。固然用戶也能夠手工控制對齊方式。
 3.編譯器對字節對齊的一些規則    

  我從下面三條說明了編譯器對字節處理的一些原則。固然除了一些特殊的編譯器在處理字節對齊的方式也不同, 這些狀況我未碰到過,就不做說明了。

  a. 關於數據類型自身的對齊值,不一樣類型會按不一樣的字節來對齊。
類型 對齊值(字節)
char 1
short 2
int 4
float 4
double 4
      b. 類、結構體的自身對齊字節值。對於結構體類型與類對象的對齊原則:使用成員當中最大的對齊字節來對齊。好比在Struct A中,int a的對齊字節爲4,比char,short都大,因此A的對齊字節爲4
     c. 指定對齊字節值。意思是指使用了宏 #pragma pack(n)來指定的對齊值

     d. 類、結構及成員的有效對齊字節值。有效對齊值=min(類/結構體/成員的自身對齊字節值,指定對齊字節值)。   有效對齊值決定了數據的存放方 式,sizeof 運算符就是根據有效對齊值來計算成員大小的。簡單來講, 有效對齊其實就是要求數據成員存放的地址值能被有效對齊值整除,即:地址值%有效對齊值=0

 

六、stl

vector/set/map/unorderedmap, 
vector/list區別: 

1.vector數據結構
vector和數組相似,擁有一段連續的內存空間,而且起始地址不變。
所以能高效的進行隨機存取,時間複雜度爲o(1);
但由於內存空間是連續的,因此在進行插入和刪除操做時,會形成內存塊的拷貝,時間複雜度爲o(n)。
另外,當數組中內存空間不夠時,會從新申請一塊內存空間並進行內存拷貝。

2.list數據結構
list是由雙向鏈表實現的,所以內存空間是不連續的。
只能經過指針訪問數據,因此list的隨機存取很是沒有效率,時間複雜度爲o(n);
但因爲鏈表的特色,能高效地進行插入和刪除。

1.說說std::vector的底層(存儲)機制。

 vector就是一個動態數組,裏面有一個指針指向一片連續的內存空間,當空間不夠裝下數據時,會自動申請另外一片更大的空間(通常是增長當前容量的100%),而後把原來的數據拷貝過去,接着釋放原來的那片空間;當釋放或者刪除裏面的數據時,其存儲空間不釋放,僅僅是清空了裏面的數據。

2.std::vector的自增加機制。

當已經分配的空間不夠裝下數據時,分配雙倍於當前容量的存儲區,把當前的值拷貝到新分配的內存中,並釋放原來的內存。

3.說說std::list的底層(存儲)機制。

以結點爲單位存放數據,結點的地址在內存中不必定連續,每次插入或刪除一個元素,就配置或釋放一個元素空間

4.什麼狀況下用vector,什麼狀況下用list。

vector能夠隨機存儲元素(便可以經過公式直接計算出元素地址,而不須要挨個查找),但在非尾部插入刪除數據時,效率很低,適合對象簡單,對象數量變化不大,隨機訪問頻繁。

list不支持隨機存儲,適用於對象大,對象數量變化頻繁,插入和刪除頻繁。

 

說說std::map底層機制。

map以RB-TREE爲底層機制。RB-TREE是一種平衡二叉搜索樹,自動排序效果不錯。

經過map的迭代器不能修改其鍵值,只能修改其實值。因此map的迭代器既不是const也不是mutable。

七、c++內存佈局, 虛表

C語言的內存模型
 
C語言的內存模型

程序代碼區(code area)

存放函數體的二進制代碼

靜態數據區(data area)

也稱全局數據區,包含的數據類型比較多,如全局變量、靜態變量、通常常量、字符串常量。其中:

  • 全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另外一塊區域。
  • 常量數據(通常常量、字符串常量)存放在另外一個區域。

注意:靜態數據區的內存在程序結束後由操做系統釋放。

堆區(heap area)

通常由程序員分配和釋放,若程序員不釋放,程序運行結束時由操做系統回收。malloc()、calloc()、free()等函數操做的就是這塊內存。

注意:這裏所說的堆區與數據結構中的堆不是一個概念,堆區的分配方式卻是相似於鏈表。

棧區(stack area)

由系統自動分配釋放,存放函數的參數值、局部變量的值等。其操做方式相似於數據結構中的棧。

命令行參數區

存放命令行參數和環境變量的值,如經過main()函數傳遞的值。

 
C語句的個部分會出如今哪些段中

C++對象的內存佈局

C++語言在C的基礎上添加了面向對象的概念,引入了封裝,繼承,多態。而一個對象的內存佈局就相對於C語言的結構體等在內存的佈局要複雜的多。
在C++中,有兩種數據成員(class data members):static 和nonstatic,以及三種類成員函數(class member functions):static、nonstatic和virtual:

非繼承下的C++對象模型

概述:在此模型下,nonstatic 數據成員被置於每個類對象中,而static數據成員被置於類對象以外。static與nonstatic函數也都放在類對象以外,而對於virtual 函數,則經過虛函數表+虛指針來支持,具體以下:

    • 每一個類生成一個表格,稱爲虛表(virtual table,簡稱vtbl)。虛表中存放着一堆指針,這些指針指向該類每個虛函數。虛表中的函數地址將按聲明時的順序排列,不過當子類有多個重載函數時例外,後面會討論。
    • 每一個類對象都擁有一個虛表指針(vptr),由編譯器爲其生成。虛表指針的設定與重置皆由類的複製控制(也便是構造函數、析構函數、賦值操做符)來完成。vptr的位置爲編譯器決定,傳統上它被放在全部顯示聲明的成員以後,不過如今許多編譯器把vptr放在一個類對象的最前端。關於數據成員佈局的內容,在後面會詳細分析。
      另外,虛函數表的前面設置了一個指向type_info的指針,用以支持RTTI(Run Time Type Identification,運行時類型識別)。RTTI是爲多態而生成的信息,包括對象繼承關係,對象自己的描述等,只有具備虛函數的對象在會生成。
 
C++數據成員及成員函數類型

如今咱們有一個類Base,它包含了上面這5中類型的數據或函數:

class Base { public: Base(int i) :baseI(i){}; int getI(){ return baseI; } static void countI(){}; virtual void print(void){ cout << "Base::print()"; } virtual ~Base(){} private: int baseI; static int baseS; }; 
 
Base類圖
 
Base內存佈局

能夠看到,對一個C++對象來講,它的內存佈局僅有虛表指針和非靜態成員,而其餘的靜態成員,成員函數(靜態,非靜態),虛表等都是佈局在類上的。


##################
A. tips

Aclass* ptra=new Bclass;
 98    int ** ptrvf=(int**)(ptra);
 99    RTTICompleteObjectLocator str=
100        *((RTTICompleteObjectLocator*)(*((int*)ptrvf[0]-1)));

能夠明顯看到,虛表地址減1以後才獲得類型信息。

結論:vptr指向的第一個位置是第一個虛函數的地址,不是type_info。

B. tips

1. 空類
class A
{
};
 
void main()
{
    printf("sizeof(A): %d\n", sizeof(A));
    getchar();
}
 獲得結果爲:1。
 類的實例化就是給每一個實例在內存中分配一塊地址。空類被實例化時,會由編譯器隱含的添加一個字節。因此空類的size爲1。

2.虛函數
class A
{
    virtual void FuncA();<br>        virtual void FuncB(); 
};
 獲得結果:4
當C++ 類中有虛函數的時候,會有一個指向虛函數表的指針(vptr),在32位系統分配指針大小爲4字節。因此size爲4.

3.靜態數據成員
class A
{
  int a;
  static int b;
  virtual void FuncA();
};
 獲得結果:8
靜態數據成員被編譯器放在程序的一個global data members中,它是類的一個數據成員.可是它不影響類的大小,無論這個類實際產生了多少實例,仍是派生了多少新的類,靜態成員數據在類中永遠只有一個實體存在。

而類的非靜態數據成員只有被實例化的時候,他們才存在.可是類的靜態數據成員一旦被聲明,不管類是否被實例化,它都已存在.能夠這麼說,類的靜態數據成員是一種特殊的全局變量.
因此該類的size爲:int a型4字節加上虛函數表指針4字節,等於8字節。

4.普通成員函數
class A
{
          void FuncA();
}
 結果:1
類的大小與它的構造函數、析構函數和其餘成員函數無關,只已它的數據成員有關。

5.普通繼承
class A
{
    int a;
};
class B
{
  int b;
};
class C : public A, public B
{
  int c;
};
 結果爲:sizeof(C) =12.
可見普通的繼承,就是基類的大小,加上派生類自身成員的大小。

6.虛擬繼承

class C : virtual public A, virtual public B
{
  int c;
};
 結果:16.

當存在虛擬繼承時,派生類中會有一個指向虛基類表的指針。因此其大小應爲普通繼承的大小(12字節),再加上虛基類表的指針大小(4個字節),共16字節。

###########################


做者:啓發禪悟
連接:https://www.jianshu.com/p/0c10b662ef09
來源:簡書
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

八、shared_ptr

When using unique_ptr, there can be at most one unique_ptr pointing at any one resource. When that unique_ptr is destroyed, the resource is automatically reclaimed. Because there can only be one unique_ptr to any resource, any attempt to make a copy of a unique_ptr will cause a compile-time error. For example, this code is illegal:

unique_ptr<T> myPtr(new T); // Okay unique_ptr<T> myOtherPtr = myPtr; // Error: Can't copy unique_ptr

However, unique_ptr can be moved using the new move semantics:

unique_ptr<T> myPtr(new T); // Okay unique_ptr<T> myOtherPtr = std::move(myPtr); // Okay, resource now stored in myOtherPtr

Similarly, you can do something like this:

unique_ptr<T> MyFunction() { unique_ptr<T> myPtr(/* ... */); /* ... */ return myPtr; }

This idiom means "I'm returning a managed resource to you. If you don't explicitly capture the return value, then the resource will be cleaned up. If you do, then you now have exclusive ownership of that resource." In this way, you can think of unique_ptr as a safer, better replacement for auto_ptr.

shared_ptr, on the other hand, allows for multiple pointers to point at a given resource. When the very last shared_ptr to a resource is destroyed, the resource will be deallocated. For example, this code is perfectly legal:

shared_ptr<T> myPtr(new T); // Okay shared_ptr<T> myOtherPtr = myPtr; // Sure! Now have two pointers to the resource.

Internally, shared_ptr uses reference counting to track how many pointers refer to a resource, so you need to be careful not to introduce any reference cycles.

In short:

  1. Use unique_ptr when you want a single pointer to an object that will be reclaimed when that single pointer is destroyed.
  2. Use shared_ptr when you want multiple pointers to the same resource.

unique_ptr

unique_ptr 這個類的關鍵點在於這個定義:

unique_ptr(const unique_ptr&) = delete;

它把拷貝構造函數幹掉了,這樣的話,就不能直接這樣用了:

unique_ptr<A> pa(new A());
    unique_ptr<A> pb = pa;

這樣也挺好,既然auto_ptr是由於多個變量持有同一個指針引發的,那麼我儘可能避免這種拷貝就行了。

唉,但這是C++啊,不留點口子確定不是C++的風格,因此unique_ptr還留下了move賦值這種東西,這個咱們不去看了。只知道有這麼一回事就好了。咱們今天的重點是shared_ptr

shared_ptr

shared_ptr也是對auto_ptr的一種改進,它的思路是,使用引用計數來管理指針。若是一個指針被屢次使用了,那麼引用計數就加一,若是減小一次使用,引用計數就減一。當引用計數變爲0,那就能夠真正地刪除指針了。先看一下基本用法:

#include <iostream>
#include <memory>
using namespace std;

class A { 
private:
    int a;
public:
    A() {
        cout << "create object of A" << endl;
        a = 1;
    }   

    ~A() {
        cout << "destroy an object A" << endl;
    }   

    void print() {
        cout << "a is " << a << endl;
    }   
};

int main() {
    shared_ptr<A> pa(new A());
    shared_ptr<A> pb = pa;
    return 0;
}

你們能夠與上節課的auto_ptr比較一下,就發現它們的區別了,固然了,這樣寫仍是不行:

int main() { A * a = new A(); shared_ptr<A> pa(a); shared_ptr<A> pb(a); return 0; } 

這種寫法仍是會讓指針被 delete 兩次。

它的基本原理是在智能指針中引入一個引用計數,在拷貝構造中對引用計數加一,在析構函數中,對引用計數減一。我寫一個簡單的例子模擬shared_ptr以下:

template <typename V>
class SmartPtr {
private:
    int * refcnt;
    V * v;
public:
    SmartPtr(V* ptr): v(ptr) {
        refcnt = new int(1);
    }   

    SmartPtr(const SmartPtr& ptr) {
        this->v = ptr.v;
        this->refcnt = ptr.refcnt;
        *refcnt += 1;
    }   

    ~SmartPtr() {
        cout << "to delete a smart pointer" << endl;
        *refcnt -= 1;

        if (*refcnt == 0) {
            delete v;
           delete refcnt;
       }
    }
};

int main() {
    A * ptrA = new A();
    SmartPtr<A> sp1(ptrA);
    SmartPtr<A> sp2 = sp1;

    return 0;
}

這個例子中中須要注意的點是引用計數是全部管理同一個指針的智能指針所共享的,因此在這個例子中,sp1和sp2的引用計數指向的是相同的一個整數。

咱們看一下這個例子的輸出:

# g++ -o smart myShare.cpp 
# ./smart 
create object of A
to delete a smart pointer
to delete a smart pointer
destroy an object A

能夠看到,這個和shared_ptr同樣能夠正確地delete指針。

 

 

網絡部分

一、tcp/udp區別

二、tcp 三次握手/ connect/ accept 關係, read返回0

三、select/ epoll

ET/LT

在一個非阻塞的socket上調用read/write函數, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK)
從字面上看, 意思是:EAGAIN: 再試一次,EWOULDBLOCK: 若是這是一個阻塞socket, 操做將被block,perror輸出: Resource temporarily unavailable

總結:
這個錯誤表示資源暫時不夠,能read時,讀緩衝區沒有數據,或者write時,寫緩衝區滿了。遇到這種狀況,若是是阻塞socket,read/write就要阻塞掉。而若是是非阻塞socket,read/write當即返回-1, 同時errno設置爲EAGAIN。
因此,對於阻塞socket,read/write返回-1表明網絡出錯了。但對於非阻塞socket,read/write返回-1不必定網絡真的出錯了。多是Resource temporarily unavailable。這時你應該再試,直到Resource available。

綜上,對於non-blocking的socket,正確的讀寫操做爲:
讀:忽略掉errno = EAGAIN的錯誤,下次繼續讀
寫:忽略掉errno = EAGAIN的錯誤,下次繼續寫

對於select和epoll的LT模式,這種讀寫方式是沒有問題的。但對於epoll的ET模式,這種方式還有漏洞。

 

epoll的兩種模式LT和ET
兩者的差別在於level-trigger模式下只要某個socket處於readable/writable狀態,不管何時進行epoll_wait都會返回該socket;而edge-trigger模式下只有某個socket從unreadable變爲readable或從unwritable變爲writable時,epoll_wait纔會返回該socket。

因此,在epoll的ET模式下,正確的讀寫方式爲:
讀:只要可讀,就一直讀,直到返回0,或者 errno = EAGAIN
寫:只要可寫,就一直寫,直到數據發送完,或者 errno = EAGAIN

正確的讀

n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {
    n += nread;
}
if (nread == -1 && errno != EAGAIN) {
    perror("read error");
}

 

正確的寫

int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0) {
    nwrite = write(fd, buf + data_size - n, n);
    if (nwrite < n) {
        if (nwrite == -1 && errno != EAGAIN) {
            perror("write error");
        }
        break;
    }
    n -= nwrite;
}

 

正確的accept,accept 要考慮 2 個問題
(1) 阻塞模式 accept 存在的問題
考慮這種狀況:TCP鏈接被客戶端夭折,即在服務器調用accept以前,客戶端主動發送RST終止鏈接,致使剛剛創建的鏈接從就緒隊列中移出,若是套接口被設置成阻塞模式,服務器就會一直阻塞在accept調用上,直到其餘某個客戶創建一個新的鏈接爲止。可是在此期間,服務器單純地阻塞在accept調用上,就緒隊列中的其餘描述符都得不處處理。

解決辦法是把監聽套接口設置爲非阻塞,當客戶在服務器調用accept以前停止某個鏈接時,accept調用能夠當即返回-1,這時源自Berkeley的實現會在內核中處理該事件,並不會將該事件通知給epool,而其餘實現把errno設置爲ECONNABORTED或者EPROTO錯誤,咱們應該忽略這兩個錯誤。

(2)ET模式下accept存在的問題
考慮這種狀況:多個鏈接同時到達,服務器的TCP就緒隊列瞬間積累多個就緒鏈接,因爲是邊緣觸發模式,epoll只會通知一次,accept只處理一個鏈接,致使TCP就緒隊列中剩下的鏈接都得不處處理。

解決辦法是用while循環抱住accept調用,處理完TCP就緒隊列中的全部鏈接後再退出循環。如何知道是否處理完就緒隊列中的全部鏈接呢?accept返回-1而且errno設置爲EAGAIN就表示全部鏈接都處理完。

綜合以上兩種狀況,服務器應該使用非阻塞地accept,accept在ET模式下的正確使用方式爲:

while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) {
    handle_client(conn_sock);
}
if (conn_sock == -1) {
    if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
    perror("accept");
}

 

一道騰訊後臺開發的面試題
使用Linuxepoll模型,水平觸發模式;當socket可寫時,會不停的觸發socket可寫的事件,如何處理?

第一種最廣泛的方式:
須要向socket寫數據的時候才把socket加入epoll,等待可寫事件。
接受到可寫事件後,調用write或者send發送數據。
當全部數據都寫完後,把socket移出epoll。

這種方式的缺點是,即便發送不多的數據,也要把socket加入epoll,寫完後在移出epoll,有必定操做代價。

一種改進的方式:
開始不把socket加入epoll,須要向socket寫數據的時候,直接調用write或者send發送數據。若是返回EAGAIN,把socket加入epoll,在epoll的驅動下寫數據,所有數據發送完畢後,再移出epoll。

這種方式的優勢是:數據很少的時候能夠避免epoll的事件處理,提升效率。

四、timeout wait過多, 2MSL

  1. netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'    

它會顯示例以下面的信息:

TIME_WAIT 814
CLOSE_WAIT 1
FIN_WAIT1 1
ESTABLISHED 634
SYN_RECV 2
LAST_ACK 1

經常使用的三個狀態是:ESTABLISHED 表示正在通訊,TIME_WAIT 表示主動關閉,CLOSE_WAIT 表示被動關閉。

若是服務器出了異常,百分之八九十都是下面兩種狀況:

1.服務器保持了大量TIME_WAIT狀態

2.服務器保持了大量CLOSE_WAIT狀態

由於linux分配給一個用戶的文件句柄是有限的(能夠參考:http://blog.csdn.net/shootyou/article/details/6579139),而TIME_WAIT和CLOSE_WAIT兩種狀態若是一直被保持,那麼意味着對應數目的通道就一直被佔着,並且是「佔着茅坑不使勁」,一旦達到句柄數上限,新的請求就沒法被處理了,接着就是大量Too Many Open Files異常,

 

1.服務器保持了大量TIME_WAIT狀態

這種狀況比較常見,一些爬蟲服務器或者WEB服務器(若是網管在安裝的時候沒有作內核參數優化的話)上常常會遇到這個問題,這個問題是怎麼產生的呢?

從 上面的示意圖能夠看得出來,TIME_WAIT是主動關閉鏈接的一方保持的狀態,對於爬蟲服務器來講他自己就是「客戶端」,在完成一個爬取任務以後,他就 會發起主動關閉鏈接,從而進入TIME_WAIT的狀態,而後在保持這個狀態2MSL(max segment lifetime)時間以後,完全關閉回收資源。爲何要這麼作?明明就已經主動關閉鏈接了爲啥還要保持資源一段時間呢?這個是TCP/IP的設計者規定 的,主要出於如下兩個方面的考慮:

1.防止上一次鏈接中的包,迷路後從新出現,影響新鏈接(通過2MSL,上一次鏈接中全部的重複包都會消失)
2. 可靠的關閉TCP鏈接。在主動關閉方發送的最後一個 ack(fin) ,有可能丟失,這時被動方會從新發fin, 若是這時主動方處於 CLOSED 狀態 ,就會響應 rst 而不是 ack。因此主動方要處於 TIME_WAIT 狀態,而不能是 CLOSED 。另外這麼設計TIME_WAIT 會定時的回收資源,並不會佔用很大資源的,除非短期內接受大量請求或者受到攻擊。

關於MSL引用下面一段話:

[plain]  view plain copy print ?
  1. MSL 為 一個 TCP Segment (某一塊 TCP 網路封包) 從來源送到目的之間可續存的時間 (也就是一個網路封包在網路上傳輸時能存活的時間),由 於 RFC 793 TCP 傳輸協定是在 1981 年定義的,當時的網路速度不像現在的網際網路那樣發達,你能夠想像你從瀏覽器輸入網址等到第一 個 byte 出現要等 4 分鐘嗎?在現在的網路環境下幾乎不可能有這種事情發生,所以我們大可將 TIME_WAIT 狀態的續存時間大幅調低,好 讓 連線埠 (Ports) 能更快空出來給其餘連線使用。  

再引用網絡資源的一段話:

[plain]  view plain copy print ?
  1. 值 得一說的是,對於基於TCP的HTTP協議,關閉TCP鏈接的是Server端,這樣,Server端會進入TIME_WAIT狀態,可 想而知,對於訪 問量大的Web Server,會存在大量的TIME_WAIT狀態,假如server一秒鐘接收1000個請求,那麼就會積壓 240*1000=240,000個 TIME_WAIT的記錄,維護這些狀態給Server帶來負擔。固然現代操做系統都會用快速的查找算法來管理這些 TIME_WAIT,因此對於新的 TCP鏈接請求,判斷是否hit中一個TIME_WAIT不會太費時間,可是有這麼多狀態要維護老是很差。  
  2. HTTP協議1.1版規定default行爲是Keep-Alive,也就是會重用TCP鏈接傳輸多個 request/response,一個主要緣由就是發現了這個問題。  

也就是說HTTP的交互跟上面畫的那個圖是不同的,關閉鏈接的不是客戶端,而是服務器,因此web服務器也是會出現大量的TIME_WAIT的狀況的。
 
如今來講如何來解決這個問題。
 
解決思路很簡單,就是讓服務器可以快速回收和重用那些TIME_WAIT的資源。
 
下面來看一下咱們網管對/etc/sysctl.conf文件的修改:
[plain]  view plain copy print ?
  1. #對於一個新建鏈接,內核要發送多少個 SYN 鏈接請求才決定放棄,不該該大於255,默認值是5,對應於180秒左右時間   
  2. net.ipv4.tcp_syn_retries=2  
  3. #net.ipv4.tcp_synack_retries=2  
  4. #表示當keepalive起用的時候,TCP發送keepalive消息的頻度。缺省是2小時,改成300秒  
  5. net.ipv4.tcp_keepalive_time=1200  
  6. net.ipv4.tcp_orphan_retries=3  
  7. #表示若是套接字由本端要求關閉,這個參數決定了它保持在FIN-WAIT-2狀態的時間  
  8. net.ipv4.tcp_fin_timeout=30    
  9. #表示SYN隊列的長度,默認爲1024,加大隊列長度爲8192,能夠容納更多等待鏈接的網絡鏈接數。  
  10. net.ipv4.tcp_max_syn_backlog = 4096  
  11. #表示開啓SYN Cookies。當出現SYN等待隊列溢出時,啓用cookies來處理,可防範少許SYN攻擊,默認爲0,表示關閉  
  12. net.ipv4.tcp_syncookies = 1  
  13.   
  14. #表示開啓重用。容許將TIME-WAIT sockets從新用於新的TCP鏈接,默認爲0,表示關閉  
  15. net.ipv4.tcp_tw_reuse = 1  
  16. #表示開啓TCP鏈接中TIME-WAIT sockets的快速回收,默認爲0,表示關閉  
  17. net.ipv4.tcp_tw_recycle = 1  
  18.   
  19. ##減小超時前的探測次數   
  20. net.ipv4.tcp_keepalive_probes=5   
  21. ##優化網絡設備接收隊列   
  22. net.core.netdev_max_backlog=3000   
[plain]  view plain copy print ?
  1.   
修改完以後執行/sbin/sysctl -p讓參數生效。
 
這裏頭主要注意到的是net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_recycle
net.ipv4.tcp_fin_timeout
net.ipv4.tcp_keepalive_*
這幾個參數。
 
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的開啓都是爲了回收處於TIME_WAIT狀態的資源。
net.ipv4.tcp_fin_timeout這個時間能夠減小在異常狀況下服務器從FIN-WAIT-2轉到TIME_WAIT的時間。
net.ipv4.tcp_keepalive_*一系列參數,是用來設置服務器檢測鏈接存活的相關配置。
 
2.服務器保持了大量CLOSE_WAIT狀態
休息一下,喘口氣,一開始只是打算說說TIME_WAIT和CLOSE_WAIT的區別,沒想到越挖越深,這也是寫博客總結的好處,總能夠有意外的收穫。
 
TIME_WAIT狀態能夠經過優化服務器參數獲得解決,由於發生TIME_WAIT的狀況是服務器本身可控的,要麼就是對方鏈接的異常,要麼就是本身沒有迅速回收資源,總之不是因爲本身程序錯誤致使的。
但 是CLOSE_WAIT就不同了,從上面的圖能夠看出來,若是一直保持在CLOSE_WAIT狀態,那麼只有一種狀況,就是在對方關閉鏈接以後服務器程 序本身沒有進一步發出ack信號。換句話說,就是在對方鏈接關閉以後,程序裏沒有檢測到,或者程序壓根就忘記了這個時候須要關閉鏈接,因而這個資源就一直 被程序佔着。我的以爲這種狀況,經過服務器內核參數也沒辦法解決,服務器對於程序搶佔的資源沒有主動回收的權利,除非終止程序運行。
 
若是你使用的是HttpClient而且你遇到了大量CLOSE_WAIT的狀況,那麼這篇日誌也許對你有用: http://blog.csdn.net/shootyou/article/details/6615051
在那邊日誌裏頭我舉了個場景,來講明CLOSE_WAIT和TIME_WAIT的區別,這裏從新描述一下:
服 務器A是一臺爬蟲服務器,它使用簡單的HttpClient去請求資源服務器B上面的apache獲取文件資源,正常狀況下,若是請求成功,那麼在抓取完 資源後,服務器A會主動發出關閉鏈接的請求,這個時候就是主動關閉鏈接,服務器A的鏈接狀態咱們能夠看到是TIME_WAIT。若是一旦發生異常呢?假設 請求的資源服務器B上並不存在,那麼這個時候就會由服務器B發出關閉鏈接的請求,服務器A就是被動的關閉了鏈接,若是服務器A被動關閉鏈接以後程序員忘了 讓HttpClient釋放鏈接,那就會形成CLOSE_WAIT的狀態了。
 
因此若是將大量CLOSE_WAIT的解決辦法總結爲一句話那就是:查代碼。由於問題出在服務器程序裏頭啊。

五、RST出現緣由

TCP異常終止的常見情形

咱們在實際的工做環境中,致使某一方發送reset報文的情形主要有如下幾種:

1,客戶端嘗試與服務器未對外提供服務的端口創建TCP鏈接,服務器將會直接向客戶端發送reset報文。

 

 

2,客戶端和服務器的某一方在交互的過程當中發生異常(如程序崩潰等),該方系統將向對端發送TCP reset報文,告之對方釋放相關的TCP鏈接,以下圖所示:

 

 

3,接收端收到TCP報文,可是發現該TCP的報文,並不在其已創建的TCP鏈接列表內(好比server機器直接宕機),則其直接向對端發送reset報文,以下圖所示:

 

TCP_NODelay

做者:Pengcheng Zeng
連接:https://www.zhihu.com/question/42308970/answer/123620051
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

參考 tcp(7): TCP protocol
TCP_NODELAY
If set, disable the Nagle algorithm. This means that segments are always sent as soon as possible, even if there is only a small amount of data. When not set, data is buffered until there is a sufficient amount to send out, thereby avoiding the frequent sending of small packets, which results in poor utilization of the network. This option is overridden by TCP_CORK; however, setting this option forces an explicit flush of pending output, even if TCP_CORK is currently set.
TCP/IP協議中針對TCP默認開啓了 Nagle算法。Nagle算法經過減小須要傳輸的數據包,來優化網絡。關於Nagle算法,@ 郭無意 同窗的答案已經說了很多了。在內核實現中,數據包的發送和接受會先作緩存,分別對應於寫緩存和讀緩存。
那麼針對題主的問題,咱們來分析一下。
啓動TCP_NODELAY,就意味着禁用了Nagle算法,容許小包的發送。對於延時敏感型,同時數據傳輸量比較小的應用,開啓TCP_NODELAY選項無疑是一個正確的選擇。好比,對於SSH會話,用戶在遠程敲擊鍵盤發出指令的速度相對於網絡帶寬能力來講,絕對不是在一個量級上的,因此數據傳輸很是少;而又要求用戶的輸入可以及時得到返回,有較低的延時。若是開啓了Nagle算法,就極可能出現頻繁的延時,致使用戶體驗極差。固然,你也能夠選擇在應用層進行buffer,好比使用java中的buffered stream,儘量地將大包寫入到內核的寫緩存進行發送;vectored I/O(writev接口)也是個不錯的選擇。
對於關閉TCP_NODELAY,則是應用了Nagle算法。數據只有在寫緩存中累積到必定量以後,纔會被髮送出去,這樣明顯提升了網絡利用率(實際傳輸數據payload與協議頭的比例大大提升)。可是這由不可避免地增長了延時;與TCP delayed ack這個特性結合,這個問題會更加顯著,延時基本在40ms左右。固然這個問題只有在連續進行兩次寫操做的時候,纔會暴露出來。
咱們看一下摘自Wikipedia的Nagle算法的僞碼實現:
if there is new data to send
  if the window size >= MSS and available data is >= MSS
    send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
      enqueue data in the buffer until an acknowledge is received
    else
      send data immediately
    end if
  end if
end if
經過這段僞碼,很容易發現連續兩次寫操做出現問題的緣由。而對於讀-寫-讀-寫這種模式下的操做,關閉TCP_NODELAY並不會有太大問題。
The user-level solution is to avoid write-write-read sequences on sockets. write-read-write-read is fine. write-write-write is fine. But write-write-read is a killer. So, if you can, buffer up your little writes to TCP and send them all at once. Using the standard UNIX I/O package and flushing write before each read usually works.

連續進行屢次對小數據包的寫操做,而後進行讀操做,自己就不是一個好的網絡編程模式;在應用層就應該進行優化。
對於既要求低延時,又有大量小數據傳輸,還同時想提升網絡利用率的應用,大概只能用UDP本身在應用層來實現可靠性保證了。好像企鵝家就是這麼幹的。

 

算法部分

一、數組中兩個數A,B之和等於第三個數C,求最大的C

二、兩個有序數組求中位數

從程序員面試角度來講,經典的問題包括如下內容:

算法部分

二分搜索 Binary Search 
分治 Divide Conquer 
寬度優先搜索 Breadth First Search 
深度優先搜索 Depth First Search
回溯法 Backtracking 
雙指針 Two Pointers 
動態規劃 Dynamic Programming 
掃描線 Scan-line algorithm
快排 Quick Sort

數據結構部分

棧 Stack
隊列 Queue
鏈表 Linked List 
數組 Array 
哈希表 Hash Table
二叉樹 Binary Tree  
堆 Heap
並查集 Union Find
字典樹 Trie

根據2017年校招的狀況,我整理了2017校招的常考算法類型,以及對應的典型題目。

另附參考答案地址:LINTCODE / LEETCODE 參考答案查詢

數學

尾部的零
斐波納契數列
x的平方根
x的平方根 2
大整數乘法
骰子求和
最多有多少個點在一條直線上
超級醜數

比特位操做

將整數A轉換爲B更新二進制位
二進制表示
O(1)時間檢測2的冪次
二進制中有多少個1

動態規劃

編輯距離正則表達式匹配
交叉字符串
乘積最大子序列
二叉樹中的最大路徑和
不一樣的路徑
通配符匹配

滑動窗口的中位數數據流中位數
最高頻的K個單詞
接雨水
堆化
排序矩陣中的從小到大第k個數

二叉樹

二叉樹中序遍歷二叉樹的序列化和反序列化
子樹
最近公共祖先
二叉樹的層次遍歷
將二叉樹拆成鏈表
在二叉查找樹中插入節點

二分法

經典二分查找問題二分查找
兩數組的交
區間最小數
尋找旋轉排序數組中的最小值
搜索排序區間
尋找峯值

分治法

快速冪兩個排序數組的中位數
合併K個排序鏈表

哈希表

變形詞子串哈希函數
短網址
複製帶隨機指針的鏈表
最小子串覆蓋

矩陣

搜索二維矩陣旋轉圖像
島嶼的個數
螺旋矩陣

寬度優先搜索

克隆圖被圍繞的區域
拓撲排序
單詞接龍

鏈表

實現一個鏈表的反轉鏈表求和 II
刪除鏈表中的元素
LRU緩存策略
合併兩個排序鏈表
兩個鏈表的交叉
翻轉鏈表 II
複製帶隨機指針的鏈表
帶環鏈表

枚舉法

統計數字名人確認
最長連續上升子序列
最大子數組差
最長公共前綴

排序

快排擺動排序
最大間距
最接近零的子數組和
最大數
四數之和
數組劃分
第K大元素
排顏色

深度優先搜索

N皇后問題圖是不是樹
帶重複元素的排列
分割回文串

數組

數組劃分逆序對
合併區間
搜索旋轉排序數組
最大子數組
刪除排序數組中的重複數字
第二大的數組
先遞增後遞減數組中的最大值
兩數和 - 輸入的數據是有序的
兩個排序數組的中位數
在大數組中查找
顏色分類
合併排序數組
無序數組K小元素
中位數
奇偶分割數組

貪心

主元素尋找缺失的數
買賣股票最佳時機
加油站
刪除數字
落單的數
最大子數組差

線段樹

線段樹查詢線段樹的構造
線段樹的修改
區間求和
統計比給定整數小的數的個數

帶最小值操做的棧用棧實現隊列
有效的括號序列
簡化路徑

整數

反轉整數將整數A轉換爲B
整數排序

字符串處理

羅馬數字轉整數迴文數
亂序字符串
有效迴文串
翻轉字符串
最長無重複字符的子串
字符串壓縮
比較字符串編輯距離II

歡迎關注個人微信公衆號:九章算法(ninechapter),幫助你瞭解IT技術前沿,經過面試、拿到offer、找到好工做!

操做系統

一、進程、線程

進程概念 
  進程是表示資源分配的基本單位,又是調度運行的基本單位。例如,用戶運行本身的程序,系統就建立一個進程,併爲它分配資源,包括各類表格、內存空間、磁盤空間、I/O設備等。而後,把該進程放人進程的就緒隊列。進程調度程序選中它,爲它分配CPU以及其它有關資源,該進程才真正運行。因此,進程是系統中的併發執行的單位。 
  在Mac、Windows NT等採用微內核結構的操做系統中,進程的功能發生了變化:它只是資源分配的單位,而再也不是調度運行的單位。在微內核系統中,真正調度運行的基本單位是線程。所以,實現併發功能的單位是線程。
線程概念 
  線程是進程中執行運算的最小單位,亦即執行處理機調度的基本單位。若是把進程理解爲在邏輯上操做系統所完成的任務,那麼線程表示完成該任務的許多可能的子任務之一。例如,假設用戶啓動了一個窗口中的數據庫應用程序,操做系統就將對數據庫的調用表示爲一個進程。假設用戶要從數據庫中產生一份工資單報表,並傳到一個文件中,這是一個子任務;在產生工資單報表的過程當中,用戶又能夠輸人數據庫查詢請求,這又是一個子任務。這樣,操做系統則把每個請求――工資單報表和新輸人的數據查詢表示爲數據庫進程中的獨立的線程。線程能夠在處理器上獨立調度執行,這樣,在多處理器環境下就容許幾個線程各自在單獨處理器上進行。操做系統提供線程就是爲了方便而有效地實現這種併發性 
引入線程的好處 
(1)易於調度。 
(2)提升併發性。經過線程可方便有效地實現併發性。進程可建立多個線程來執行同一程序的不一樣部分。 
(3)開銷少。建立線程比建立進程要快,所需開銷不多。。 
(4)利於充分發揮多處理器的功能。經過建立多線程進程(即一個進程可具備兩個或更多個線程),每一個線程在一個處理器上運行,從而實現應用程序的併發性,使每一個處理器都獲得充分運行。 
進程和線程的關係 
(1)一個線程只能屬於一個進程,而一個進程能夠有多個線程,但至少有一個線程。 
(2)資源分配給進程,同一進程的全部線程共享該進程的全部資源。 
(3)處理機分給線程,即真正在處理機上運行的是線程。 
(4)線程在執行過程當中,須要協做同步。不一樣進程的線程間要利用消息通訊的辦法實現同步。

二、進程間通訊的方式?

    (1)管道(pipe)及有名管道(named pipe):管道可用於具備親緣關係的父子進程間的通訊,有名管道除了具備管道所具備的功能外,它還容許無親緣關係進程間的通訊。

    (2)信號(signal):信號是在軟件層次上對中斷機制的一種模擬,它是比較複雜的通訊方式,用於通知進程有某事件發生,一個進程收到一個信號與處理器收到一箇中斷請求效果上能夠說是一致的。

    (3)消息隊列(message queue):消息隊列是消息的連接表,它克服了上兩種通訊方式中信號量有限的缺點,具備寫權限得進程能夠按照必定得規則向消息隊列中添加新信息;對消息隊列有讀權限得進程則能夠從消息隊列中讀取信息。

    (4)共享內存(shared memory):能夠說這是最有用的進程間通訊方式。它使得多個進程能夠訪問同一塊內存空間,不一樣進程能夠及時看到對方進程中對共享內存中數據得更新。這種方式須要依靠某種同步操做,如互斥鎖和信號量等。

    (5)信號量(semaphore):主要做爲進程之間及同一種進程的不一樣線程之間得同步和互斥手段。

    (6)套接字(socket):這是一種更爲通常得進程間通訊機制,它可用於網絡中不一樣機器之間的進程間通訊,應用很是普遍。

三、線程同步

多線程的同步

      有了上面的基本函數還不足以完成本題的要求,爲何呢?由於題目要求按照ABCABC...的方式打印,而3個線程卻在搶佔資源,因此沒法控制排列順序。這時就須要用到多線程編程中的同步技術。

      對於多線程編程來講,同步就是同一時間只容許一個線程訪問資源,而其餘線程不能訪問。多線程有3種同步方式:

  • 互斥鎖
  • 條件變量
  • 讀寫鎖

1.互斥鎖

      互斥鎖是最基本的同步方式,它用來保護一個「臨界區」,保證任什麼時候刻只由一個線程在執行其中的代碼。這個「臨界區」一般是線程的共享數據

      下面三個函數給一個互斥鎖上鎖和解鎖:

int  pthread_mutex_lock(pthread_mutex_t *mptr);
 
int  pthread_mutex_trylock(pthread_mutex_t *mptr);
 
int  pthread_mutex_unlock(pthread_mutex_t *mptr);

  假設線程2要給已經被線程1鎖住的互斥鎖(mutex)上鎖(即執行pthread_mutex_lock(mutex)),那麼它將一直阻塞直到到線程1解鎖爲止(即釋放mutex)。

      若是互斥鎖變量時靜態分配的,一般初始化爲常值PTHREAD_MUTEX_INITIALIZER,若是互斥鎖是動態分配的,那麼在運行時調用pthread_mutex_init函數來初始化。

2.條件變量

      互斥鎖用於上鎖,而條件變量則用於等待,一般它都會跟互斥鎖一塊兒使用。

int  pthread_cond_wait(pthread_cond_t *cptr,pthread_mutex_t *mptr);
int  pthread_cond_signal(pthread_cond_t *cptr);

  一般pthread_cond_signal只喚醒等待在相應條件變量上的一個線程,如有多個線程須要被喚醒呢,這就要使用下面的函數了:

int  pthread_cond_broadcast(pthread_cond_t *cptr);

3.讀寫鎖

      互斥鎖將試圖進入連你姐去的其餘簡稱阻塞住,而讀寫鎖是將讀和寫做了區分,讀寫鎖的分配規則以下:

      (1)只要沒有線程持有某個給定的讀寫鎖用於寫,那麼任意數目的線程能夠持有該讀寫鎖用於讀;

      (2)僅當沒有線程持有某個給定的讀寫鎖用於讀或用於寫時,才能分配該讀寫鎖用於寫。

int  pthread_rwlock_rdlock(pthread_relock_t *rwptr);
int  pthread_rwlock_wrlock(pthread_relock_t *rwptr);
int  pthread_rwlock_unlock(pthread_relock_t *rwptr);
 
 

pthread_cond_wait 爲何須要傳遞 mutex 參數

做者:吳志強
連接:https://www.zhihu.com/question/24116967/answer/26747608
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

首先須要明白兩點:
  • wait()操做一般伴隨着條件檢測,如:
    while(pass == 0) pthread_cond_wait(...); 
  • signal*()函數一般伴隨着條件改變,如:
    pass = 1; pthread_cond_signal(...) 
因爲此兩處都涉及到變量pass,因此爲了防止Race Condition,必須得加鎖。因此代碼會變成下面這樣:
// 條件測試 pthread_mutex_lock(mtx); while(pass == 0) pthread_cond_wait(...); pthread_mutex_unlock(mtx); // 條件發生修改,對應的signal代碼 pthread_mutex_lock(mtx); pass = 1; pthread_mutex_unlock(mtx); pthread_cond_signal(...); 

而後,咱們假設wait()操做不會自動釋放、獲取鎖,那麼代碼會變成這樣:
// 條件測試 pthread_mutex_lock(mtx); while(pass == 0) { pthread_mutex_unlock(mtx); pthread_cond_just_wait(cv); pthread_mutex_lock(mtx); } pthread_mutex_unlock(mtx); // 條件發生修改,對應的signal代碼 pthread_mutex_lock(mtx); pass = 1; pthread_mutex_unlock(mtx); pthread_cond_signal(cv); 

長此以往,程序員發現unlock, just_wait, lock這三個操做始終得在一塊兒。因而就提供了一個pthread_cond_wait()函數來同時完成這三個函數。

另一個證據是,signal()函數是不須要傳遞mutex參數的,因此關於mutex參數是用於同步wait()和signal()函數的說法更加站不住腳。

因此個人結論是:傳遞的mutex並非爲了防止wait()函數內部的Race Condition!而是由於調用wait()以前你老是得到了某個mutex(例如用於解決此處pass變量的Race Condition的mutex),而且這個mutex在你調用wait()以前必須得釋放掉,調用wait()以後必須得從新獲取。


因此,pthread_cond_wait()函數不是一個細粒度的函數,倒是一個實用的函數。

 

生產者、消費者模型

Producer:

While(TRUE)

Mutex_Lock(mutex_p)

if (item_size < FULL)

  PREDOCE

Mutex_UnLock(mutex_p)

 

Mutex_Lock(mutex_c)

if (item_size == 0)

      Cond_Signal(cond)

item_size++;

Mutex_UnLock(mutex_c)

 

Consumer:

Mutex_Lock(mutex_c)

while (item_size == 0)

  Cond_Wait(mutex_c, cond)

 item_size--;

Mutex_UnLock(mutex_c)

相關文章
相關標籤/搜索