手寫一個Pcap捕包工具及效能優化
前言
這是在專案上遇到的問題。因為專案的緣故,需要實現pcap捕包的功能,在使用後發現libpcap自身API使用比較複雜,就手寫了一個API並嘗試進行效能優化。部分程式碼在githuab上。pcapDumper
pcap檔案格式
一個pcap檔案的結構如下:
(圖片來自部落格)
如上圖,一個pcap檔案,有兩個部分組成,一個Pcap 頭部,和一堆包。其中每一個包都有兩個欄位,分別是Packet Head和 Packet Data。它們的資料結構如下:
struct _pcap_file_header {
uint32_t magic;
uint16_t version_major;
uint16_t version_minor;
int32_t thiszone; /* gmt to local correction */
uint32_t sigfigs; /* accuracy of timestamps */
uint32_t snaplen; /* max length saved portion of each pkt */
uint32_t linktype; /* data link type (LINKTYPE_*) */
};
struct _pcap_pkthdr {
struct _timeval ts; /* time stamp using 32 bits fields */
uint32_t caplen; /* length of portion present */
uint32_t len; /* length this packet (off wire) */
};
pcap_file_header
儲存著包結構的頭,magic
欄位是位元組順序。version_major
version_minor
分別是主版本號和次版本號。 thiszone
代表時區, snaplen
是資料包的最大長度,一般設定為65535. linktype
表示鏈路型別。
可以看到pcap_file_header
儲存的是,錄製這個包的時候的網路狀態。
pcap_pkthdr
儲存的是資料包的結構。包括資料包的時間戳和包長。在這個結構之後,緊跟著就是一段長為len
len
同時也指向了下一個資料包的位置,即偏移到len+1
的位置。
在寫入的時候,就是按照上圖所示,一個pcap_file_hdr
,然後pcap_pkthdr1
,pkt_data1
,pcap_pkthdr2
,pkt_data2
,這麼寫入的。
pcap_file_hdr->pcap_pkthdr->pkt_data->pcap_pkthdr->pkt_data->pcap_pkthdr->pkt_data->pcap_pkthdr->pkt_data->...
所以在讀取pcap檔案的時候,實際上是線性的訪問資料包的。只有讀取了第n個數據包,你才能訪問的到底n+1個數據包。
注意的坑
pcap_pkthdr
中的timeval
是32位。它的實際結構應該是這樣的:
struct _timeval {
uint32_t tv_sec; /* seconds */
uint32_t tv_usec; /* microseconds */
};
在64位系統中,timeval
中兩個欄位都是uint64_t
的值。所以如果沒有注意到這一點,會出現欄位錯誤。導致儲存的pcap包無法被wireshark等工具正常識別。
效能優化
在效能上,主要的瓶頸在於寫入硬碟。如果程式在每次拿到一個包就寫入到硬碟中,就會造成頻繁的小檔案的寫入。這樣發揮不了硬碟的最大速度。可以使用一個記憶體buf來把資料先快取在記憶體中,等到了一定大小(比如16M)或者pcap檔案寫入完畢後再寫入硬碟。
架構上的優化
程式上的優化最高能夠讓寫包速度達到100Mbps。而相對的,硬碟的最高速度應該是在1000Mbps以上,網絡卡的效能則可以達到10000Mbps上。所以實際上寫包速率是滿足不了高效能捕包的寫包需求的。這個時候就需要從架構上優化了。
有兩種思路可以從架構上優化,第一種是使用多執行緒並行。每個執行緒都進行捕包->解析包->寫入包->再捕包
的過程。這個過程能夠提升一定的效能。但是會有幾個問題:
1. 頻繁的中斷。
2. 資源爭用。
每個執行緒在寫入一個包的時候,在三個過程,需要申請不同的資源,分別是網絡卡資源,CPU計算資源和硬碟IO資源。系統的中斷和切換資源需要消耗一定的時間。此外,資源爭用的問題也會發生,這主要體現在硬碟的資源上。在錄製包的場景下,硬碟資源是瓶頸資源。這個時候多個執行緒在頻繁請求硬碟資源,會導致硬碟的寫入效能下降。比如我有100的時間片,這100的時間片中,可能只有80的時間片用在了真正的寫入硬碟中,其他的時間片都花費在了資源排程上。這會使每個執行緒的效能不及原來單執行緒的時候,而且硬碟也無法發揮自己百分百的效能。
另一種是使用多執行緒併發。每個執行緒都只幹一件事情。捕包有捕包的執行緒,解析包有解析包的執行緒,寫入包有寫入包的執行緒。這樣線上程間的資源競爭就能夠被避免。在實際的專案中,甚至把寫入包之後的記憶體釋放都單獨分出一個執行緒。就是為了讓硬碟IO的效率達到最大。實際的效果,能夠使程式支援1000Mbps的錄包速度。如果需要再提高效能,就可能需要使用SSD,或者分散式儲存,磁碟陣列等來解決了。