1、c++語言部分html
將讓 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」後面的括號裏的代碼當作C代碼來處理,固然咱們也能夠以單條語句來聲明
這樣就聲明瞭C類型的func和var。不少時候咱們寫一個頭文件聲明瞭一些C語言的函數,而這些函數可能被C和C++代碼調用,當咱們提供給C++代碼調用時,須要在頭文件里加extern 「C」,不然C++編譯的時候會找不到符號,而給C代碼調用時又不能加extern 「C」,由於C是不支持這樣的語法的,常見的處理方式是這樣的,咱們以C的庫函數memset爲例
其中__cplusplus是C++編譯器定義的一個宏,若是這份代碼和C++一塊兒編譯,那麼memset會在extern "C"裏被聲明,若是是和C代碼一塊兒編譯則直接聲明,因爲__cplusplus沒有被定義,因此也不會有語法錯誤。這樣的技巧在系統頭文件裏常常被用到。
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
首先須要明確的是,程序在運行起來,內存訪問的順序和程序員編寫的順序不必定一致,基於這個前提下,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 經常使用的地方,那麼包括:
這裏篇幅有限,若是你做爲程序員,你能夠從 https://www.kernel.org/doc/Documentation/memory-barriers.txt 一文入手研究,若是這個還不能知足你,能夠進一步深刻硬件來研究多 CPU 間內存亂序訪問的問題:http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
我我的也對 Memory barrier 作了一點小研究,主要寫了幾個例子驗證亂序的存在: http://b2e699b3.wiz03.com/share/s/2OVFCP1_wkXs20LtbT1nXNrj0EqwFC1zZAjT2bCeRi3Tzco2
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直接在編譯期就拒絕了這種轉換。
new的功能是在堆區新建一個對象,並返回該對象的指針。
所謂的【新建對象】的意思就是,將調用該類的構造函數,由於若是不構造的話,就不能稱之爲一個對象。
而malloc只是機械的分配一塊內存,若是用mallco在堆區建立一個對象的話,是不會調用構造函數的
我從下面三條說明了編譯器對字節處理的一些原則。固然除了一些特殊的編譯器在處理字節對齊的方式也不同, 這些狀況我未碰到過,就不做說明了。
類型 | 對齊值(字節) |
char | 1 |
short | 2 |
int | 4 |
float | 4 |
double | 4 |
d. 類、結構及成員的有效對齊字節值。有效對齊值=min(類/結構體/成員的自身對齊字節值,指定對齊字節值)。 有效對齊值決定了數據的存放方 式,sizeof 運算符就是根據有效對齊值來計算成員大小的。簡單來講, 有效對齊其實就是要求數據成員存放的地址值能被有效對齊值整除,即:地址值%有效對齊值=0
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。
程序代碼區(code area)
存放函數體的二進制代碼
靜態數據區(data area)
也稱全局數據區,包含的數據類型比較多,如全局變量、靜態變量、通常常量、字符串常量。其中:
注意:靜態數據區的內存在程序結束後由操做系統釋放。
堆區(heap area)
通常由程序員分配和釋放,若程序員不釋放,程序運行結束時由操做系統回收。malloc()、calloc()、free()等函數操做的就是這塊內存。
注意:這裏所說的堆區與數據結構中的堆不是一個概念,堆區的分配方式卻是相似於鏈表。
棧區(stack area)
由系統自動分配釋放,存放函數的參數值、局部變量的值等。其操做方式相似於數據結構中的棧。
命令行參數區
存放命令行參數和環境變量的值,如經過main()函數傳遞的值。
C++語言在C的基礎上添加了面向對象的概念,引入了封裝,繼承,多態。而一個對象的內存佈局就相對於C語言的結構體等在內存的佈局要複雜的多。
在C++中,有兩種數據成員(class data members):static 和nonstatic,以及三種類成員函數(class member functions):static、nonstatic和virtual:
概述:在此模型下,nonstatic 數據成員被置於每個類對象中,而static數據成員被置於類對象以外。static與nonstatic函數也都放在類對象以外,而對於virtual 函數,則經過虛函數表+虛指針來支持,具體以下:
如今咱們有一個類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; };
能夠看到,對一個C++對象來講,它的內存佈局僅有虛表指針和非靜態成員,而其餘的靜態成員,成員函數(靜態,非靜態),虛表等都是佈局在類上的。
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字節。
###########################
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:
unique_ptr
when you want a single pointer to an object that will be reclaimed when that single pointer is destroyed.shared_ptr
when you want multiple pointers to the same resource.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也是對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指針。
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的事件處理,提升效率。
它會顯示例以下面的信息:
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引用下面一段話:
再引用網絡資源的一段話:
五、RST出現緣由
TCP異常終止的常見情形
咱們在實際的工做環境中,致使某一方發送reset報文的情形主要有如下幾種:
1,客戶端嘗試與服務器未對外提供服務的端口創建TCP鏈接,服務器將會直接向客戶端發送reset報文。
2,客戶端和服務器的某一方在交互的過程當中發生異常(如程序崩潰等),該方系統將向對端發送TCP reset報文,告之對方釋放相關的TCP鏈接,以下圖所示:
3,接收端收到TCP報文,可是發現該TCP的報文,並不在其已創建的TCP鏈接列表內(好比server機器直接宕機),則其直接向對端發送reset報文,以下圖所示:
TCP_NODelay
TCP_NODELAYTCP/IP協議中針對TCP默認開啓了 Nagle算法。Nagle算法經過減小須要傳輸的數據包,來優化網絡。關於Nagle算法,@ 郭無意 同窗的答案已經說了很多了。在內核實現中,數據包的發送和接受會先作緩存,分別對應於寫緩存和讀緩存。
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.
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
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 參考答案查詢
歡迎關注個人微信公衆號:九章算法(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);
|
while(pass == 0) pthread_cond_wait(...);
pass = 1; pthread_cond_signal(...)
// 條件測試 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(...);
// 條件測試 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()以後必須得從新獲取。
生產者、消費者模型
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)