libpcap原始碼分析_PACKET_MMAP機制
阿新 • • 發佈:2018-12-09
使用PACKET_MMAP機制的原因: 不開啟PACKET_MMAP時的捕獲過程是非常低效的,它使用非常受限的緩衝區,並且每捕獲一個報文就需要一次系統呼叫, 如果還想獲取這個報文的時間戳,就需要再執行一次系統呼叫. 而啟用PACKET_MMAP的捕獲過程就是非常高效的,它提供了一個對映到使用者空間的長度可配的環形緩衝區,這個緩衝區可以用於收發報文. 用這種方式接收報文時,只需要等待報文到來即可,大部分情況下都不需要發出一個系統呼叫; 用這種方式傳送報文時,多個報文只需要一個系統呼叫就可以以最高頻寬傳送出去. 另外,使用者空間和核心使用共享快取的方式可以減少報文的拷貝。下面就從libpcap中的activate_mmap函式為線索,展開對PACKET_MMAP使用方法的分析。 /
* 嘗試對指定pcap控制代碼開啟PACKET_MMAP功能 * @返回值 1表示成功開啟;0表示系統不支援PACKET_MMAP功能;-1表示出錯 */ int activate_mmap(pcap_t *handle, int *status) { // 獲取該pcap控制代碼的私有空間 struct pcap_linux *handlep = handle->priv; // 分配一塊快取用於oneshot的情況,快取大小為該pcap控制代碼支援捕獲的最大包長 handlep->oneshot_buffer = malloc(handle->snapshot); // 設定普通緩衝區的預設長度為2M,這裡將嘗試作為PACKET_MMAP的環形緩衝區使用 if (handle->opt.buffer_size == 0) handle->opt.buffer_size = 2*1024*1024; // 為該捕獲套接字設定合適的環形緩衝區版本,優先考慮設定TPACKET_V3 prepare_tpacket_socket(handle); // 建立環形緩衝區,並將其對映到使用者空間 create_ring(handle, status); // 根據環形緩衝區版本註冊linux上PACKET_MMAP讀操作回撥函式 switch (handlep->tp_version) { case TPACKET_V1: handle->read_op = pcap_read_linux_mmap_v1; break; case TPACKET_V1_64: handle->read_op = pcap_read_linux_mmap_v1_64; break; case TPACKET_V2: handle->read_op = pcap_read_linux_mmap_v2; break; case TPACKET_V3: handle->read_op = pcap_read_linux_mmap_v3; break; } // 最後註冊一系列linux上PACKET_MMAP相關回調函式 handle->cleanup_op = pcap_cleanup_linux_mmap; handle->setfilter_op = pcap_setfilter_linux_mmap; handle->setnonblock_op = pcap_setnonblock_mmap; handle->getnonblock_op = pcap_getnonblock_mmap; handle->oneshot_callback = pcap_oneshot_mmap; handle->selectable_fd = handle->fd; return 1; } int prepare_tpacket_socket(pcap_t *handle) { // 獲取該pcap控制代碼的私有空間 struct pcap_linux *handlep = handle->priv; int ret; /* 只有該pcap控制代碼沒有使能immediate標識的前提下,才會首先嚐試將環形緩衝區版本設定為TPACKET_V3 * 這是因為實現決定了TPACKET_V3模式下報文可能無法被實時傳遞給使用者 */ if (!handle->opt.immediate) { ret = init_tpacket(handle, TPACKET_V3, "TPACKET_V3"); if (ret == 0) // 成功開啟TPACKET_V3模式 return 1; else if (ret == -1) // 開啟TPACKET_V3模式失敗且並非是kernel不支援的原因 return -1; } // 在kernel不支援TPACKET_V3模式的情況下,則嘗試開啟TPACKET_V2模式 ret = init_tpacket(handle, TPACKET_V2, "TPACKET_V2"); if (ret == 0) // 成功開啟TPACKET_V2模式 return 1; else if (ret == -1) // 開啟TPACKET_V2模式失敗且並非是kernel不支援的原因 return -1; /* 在kernel不支援TPACKET_V3、TPACKET_V2模式的情況下,則最後臨時假設為TPACKET_V1模式 * 因為只要核心支援PACKET_MMAP機制,就必然支援TPACKET_V1模式 */ handlep->tp_version = TPACKET_V1; handlep->tp_hdrlen = sizeof(struct tpacket_hdr); return 1; } int init_tpacket(pcap_t *handle, int version, const char *version_str) { // 獲取該pcap控制代碼的私有空間 struct pcap_linux *handlep = handle->priv; int val = version; socklen_t len = sizeof(val); // 首先嚐試獲取該版本環形緩衝區中幀頭長,這也是一種探測核心是否支援該版本的環形緩衝區的方式 if (getsockopt(handle->fd, SOL_PACKET, PACKET_HDRLEN, &val, &len) < 0) { // 返回這兩種錯誤號都表示kernel不支援 if (errno == ENOPROTOOPT || errno == EINVAL) return 1; return -1; } handlep->tp_hdrlen = val; // 如果核心支援,則將該pcap控制代碼關聯的接字設定一個該版本的環形緩衝區 val = version; setsockopt(handle->fd, SOL_PACKET, PACKET_VERSION, &val,sizeof(val)); handlep->tp_version = version; // 設定環形緩衝區中每個幀VLAN_TAG_LEN長度的保留空間,用於VLAN tag重組 val = VLAN_TAG_LEN; setsockopt(handle->fd, SOL_PACKET, PACKET_RESERVE, &val,sizeof(val)); return 0; } int create_ring(pcap_t *handle, int *status) { // 獲取該pcap控制代碼的私有空間 struct pcap_linux *handlep = handle->priv; struct tpacket_req3 req; socklen_t len; unsigned int sk_type,tp_reserve, maclen, tp_hdrlen, netoff, macoff; unsigned int frame_size; // 根據配置的版本建立對應的接收環形緩衝區 switch (handlep->tp_version) { case TPACKET_V1: case TPACKET_V2: /* V1、V2版本需要設定一個合適的環形緩衝區幀長,預設同步自snapshot, * 但是因為snapshot可能設定了一個極大的值,這會導致一個環形緩衝區放不下幾個幀,並且存在大量空間的浪費, * 所以接下來會嘗試進一步調整為一個合理的幀長值 */ frame_size = handle->snapshot; // 針對乙太網介面調整環形緩衝區幀長 if (handle->linktype == DLT_EN10MB) { int mtu; int offload; // 檢查該介面是否支援offload機制 offload = iface_get_offload(handle); // 對於不支援offload機制的介面,可以使用該介面的MTU值來進一步調整環形緩衝區的幀長 if (!offload) { mtu = iface_get_mtu(handle->fd, handle->opt.device,handle->errbuf); if (frame_size > (unsigned int)mtu + 18) frame_size = (unsigned int)mtu + 18; } } // 獲取套接字型別 len = sizeof(sk_type); getsockopt(handle->fd, SOL_SOCKET, SO_TYPE, &sk_type,&len); /* 獲取環形緩衝區中每個幀的保留空間長度 * 備註:對於V3/V2模式的環形緩衝區,之前是有設定過VLAN_TAG_LEN位元組的保留空間,而V1模式則沒有設定過 */ len = sizeof(tp_reserve); if (getsockopt(handle->fd, SOL_PACKET, PACKET_RESERVE,&tp_reserve, &len) < 0) { if (errno != ENOPROTOOPT) return -1; tp_reserve = 0; } // 以下一系列計算的最終目的是得到一個合適幀長值 maclen = (sk_type == SOCK_DGRAM) ? 0 : MAX_LINKHEADER_SIZE; tp_hdrlen = TPACKET_ALIGN(handlep->tp_hdrlen) + sizeof(struct sockaddr_ll) ; netoff = TPACKET_ALIGN(tp_hdrlen + (maclen < 16 ? 16 : maclen)) + tp_reserve; macoff = netoff - maclen; req.tp_frame_size = TPACKET_ALIGN(macoff + frame_size); // 最終通過一系列計算才得到合適的幀長值 req.tp_frame_nr = handle->opt.buffer_size/req.tp_frame_size;// 得到幀長值之後,就可以進一步計算得到環形接收緩衝區可以存放的幀總數 break; case TPACKET_V3: // 區別於V1/V2,V3的幀長可變,只需要設定一個幀長上限值即可 req.tp_frame_size = MAXIMUM_SNAPLEN; req.tp_frame_nr = handle->opt.buffer_size/req.tp_frame_size; break; } /* 計算V1/V2/V3的記憶體塊長度,記憶體塊長度只能取PAGE_SIZE * 2^n,並且要確保至少放下1個幀 * 備註:由於V3模式設定幀長上限MAXIMUM_SNAPLEN必然大於PAGE_SIZE,所以可知V3模式下1個記憶體塊中只會有1個幀 */ req.tp_block_size = getpagesize(); while (req.tp_block_size < req.tp_frame_size) req.tp_block_size <<= 1; frames_per_block = req.tp_block_size/req.tp_frame_size; retry: // 計算記憶體塊數量和幀總數,這裡顯然再次對幀總數進行調整,最終確保幀總數是記憶體塊總數的整數倍 req.tp_block_nr = req.tp_frame_nr / frames_per_block; req.tp_frame_nr = req.tp_block_nr * frames_per_block; // 設定每個記憶體塊的壽命 req.tp_retire_blk_tov = (handlep->timeout>=0)?handlep->timeout:0; // 每個記憶體塊不設私有空間 req.tp_sizeof_priv = 0; // 清空環形緩衝區的標誌集合 req.tp_feature_req_word = 0; // 建立接收環形緩衝區 if (setsockopt(handle->fd, SOL_PACKET, PACKET_RX_RING,(void *) &req, sizeof(req))) { // 如果失敗原因是記憶體不足,則減少幀總數然後再次進行建立 if ((errno == ENOMEM) && (req.tp_block_nr > 1)) { if (req.tp_frame_nr < 20) req.tp_frame_nr -= 1; else req.tp_frame_nr -= req.tp_frame_nr/20; goto retry; } // 如果kernel不支援PACKET_MMAP則直接返回 if (errno == ENOPROTOOPT) return 0; } // 程式執行到這裡意味著接收環形緩衝區建立成功 // 接著就是將新建立的接收環形緩衝區對映到使用者空間 handlep->mmapbuflen = req.tp_block_nr * req.tp_block_size; handlep->mmapbuf = mmap(0, handlep->mmapbuflen,PROT_READ|PROT_WRITE, MAP_SHARED, handle->fd, 0); // 最後還需要建立一個pcap內部用於管理接收環形緩衝區每個幀頭/塊頭的陣列 handle->cc = req.tp_frame_nr; handle->buffer = malloc(handle->cc * sizeof(union thdr *)); // 將接收環形緩衝區中每個幀頭地址記錄到管理陣列buffer中 handle->offset = 0; for (i=0; i<req.tp_block_nr; ++i) { void *base = &handlep->mmapbuf[i*req.tp_block_size]; for (j=0; j<frames_per_block; ++j, ++handle->offset) { RING_GET_CURRENT_FRAME(handle) = base; base += req.tp_frame_size; } } handle->bufsize = req.tp_frame_size; // 開啟PACKET_MMAP情況下,offset欄位其實不再有意義 handle->offset = 0; return 1; }
小結: 至此已經成功開啟PACKET_MMAP功能,顯然其中的核心部分在於環形緩衝區的配置, 接下來將以pcap_read_linux_mmap_v3為線索分析如何在開啟PACKET_MMAP的情況下進行捕獲
相關資料結構:
/* 建立TPACKET_V3環形緩衝區時對應的配置引數結構 * 備註: tpacket_req3結構是tpacket_req結構的超集,實際可以統一使用本結構去設定所有版本的環形緩衝區,V1/V2版本會自動忽略多餘的欄位 */ struct tpacket_req3 { unsigned int tp_block_size; // 每個連續記憶體塊的最小尺寸(必須是 PAGE_SIZE * 2^n ) unsigned int tp_block_nr; // 記憶體塊數量 unsigned int tp_frame_size; // 每個幀的大小(雖然V3中的幀長是可變的,但建立時還是會傳入一個最大的允許值) unsigned int tp_frame_nr; // 幀的總個數(必須等於 每個記憶體塊中的幀數量*記憶體塊數量) unsigned int tp_retire_blk_tov; // 記憶體塊的壽命(ms),超時後即使記憶體塊沒有被資料填入也會被核心停用,0意味著不設超時 unsigned int tp_sizeof_priv; // 每個記憶體塊中私有空間大小,0意味著不設私有空間 unsigned int tp_feature_req_word;// 標誌位集合(目前就支援1個標誌 TP_FT_REQ_FILL_RXHASH) }