使用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的情況下進行捕獲
int pcap_read_linux_mmap_v3(pcap_t *handle, int max_packets, pcap_handler callback,u_char *user)
{
// 獲取該pcap句柄的私有空間
struct pcap_linux *handlep = handle->priv;
int pkts = 0; // 記錄了成功捕獲的報文數量
union thdr h;
again:
// 等待接收環形緩衝區中有內存塊提交給用戶空間
if (handlep->current_packet == NULL) {
h.raw = RING_GET_CURRENT_FRAME(handle);
// 如果當前等待讀取的內存塊中沒有報文,則在這裏進行等待
if (h.h3->hdr.bh1.block_status == TP_STATUS_KERNEL) {
pcap_wait_for_frames_mmap(handle);
}
}
/* 再次檢查接收環形緩衝區中是否有內存塊提交給用戶空間,如果沒有,通常就會結束捕獲操作;
* 除非該pcap句柄設置的是收到至少1個報文捕獲操作纔會結束
*/
h.raw = RING_GET_CURRENT_FRAME(handle);
if (h.h3->hdr.bh1.block_status == TP_STATUS_KERNEL) {
if (pkts == 0 && handlep->timeout == 0)
goto again;
return pkts;
}
// 程序運行到這裏意味着當前等待讀取的內存塊中有報文可讀
/* 循環捕獲並處理數據包,具體的流程就是依次遍歷每個內存塊以及其中的每個幀,進行讀取,
* 直到達到設置的捕獲數量或者接收環形緩存區沒有報文可讀
*/
while ((pkts < max_packets) || PACKET_COUNT_IS_UNLIMITED(max_packets)) {
int packets_to_read;
// 每次讀取一個新的內存塊前都需要刷新current_packet和packets_left
if (handlep->current_packet == NULL) {
// 如果當前等待讀取的內存塊中沒有報文,則退出捕獲
h.raw = RING_GET_CURRENT_FRAME(handle);
if (h.h3->hdr.bh1.block_status == TP_STATUS_KERNEL)
break;
handlep->current_packet = h.raw + h.h3->hdr.bh1.offset_to_first_pkt;
handlep->packets_left = h.h3->hdr.bh1.num_pkts;
}
/* 在一個內存塊中要讀取的報文數量不僅受到該內存塊中能被讀取的報文數量的限制,
* 還收到本次捕獲操作設置的捕獲數量的限制
*/
packets_to_read = handlep->packets_left;
if (!PACKET_COUNT_IS_UNLIMITED(max_packets) && packets_to_read > (max_packets - pkts))
packets_to_read = max_packets - pkts;
/* 從一個內存塊中循環捕獲packets_to_read數量的幀,直到滿足以下任一條件才退出:
* 超過了要讀取的包數量;
* 強制退出
*/
while (packets_to_read-- && !handle->break_loop) {
// 獲取接收環形緩衝區中每個幀的頭部結構
struct tpacket3_hdr* tp3_hdr = (struct tpacket3_hdr*) handlep->current_packet;
// 嘗試捕獲環形緩衝區中每個幀
ret = pcap_handle_packet_mmap(
handle,
callback,
user,
handldp->current_packet,
tp3_hdr->tp_len,
tp3_hdr->tp_mac,
tp3_hdr->tp_snaplen,
tp3_hdr->tp_sec,
handle->opt.tstamp_precision == PCAP_TSTAMP_PRECISION_NANO ? tp3_hdr->tp_nsec : tp3_hdr->tp_nsec / 1000,
(tp3_hdr->hv1.tp_vlan_tci || (tp3_hdr->tp_status & TP_STATUS_VLAN_VALID)),
tp3_hdr->hv1.tp_vlan_tci,
VLAN_TPID(tp3_hdr, &tp3_hdr->hv1));
if (ret == 1) {
// 這種情況意味着成功捕獲到一幀
pkts++;
handlep->packets_read++;
} else if (ret < 0) {
// 這種情況意味着捕獲該幀的過程中出現錯誤
handlep->current_packet = NULL;
return ret;
}
// 準備讀取該內存塊中的下一幀
handlep->current_packet += tp3_hdr->tp_next_offset;
handlep->packets_left--;
}
// 當一個內存塊中的幀都被讀取之後,需要把該內存塊還給內核
if (handlep->packets_left <= 0) {
h.h3->hdr.bh1.block_status = TP_STATUS_KERNEL;
// 準備讀取下一個內存塊
if (++handle->offset >= handle->cc)
handle->offset = 0;
handlep->current_packet = NULL;
}
// 每次讀完一個內存塊後都會檢查是否要求強制退出
if (handle->break_loop) {
handle->break_loop = 0;
return PCAP_ERROR_BREAK;
}
}
// 如果該pcap句柄設置的超時爲0,則會一直等待下去直到收到報文爲止
if (pkts == 0 && handlep->timeout == 0)
goto again;
return pkts;
}
int pcap_handle_packet_mmap(...)
{
struct sockaddr_ll *sll;
unsigned char *bp;
struct pcap_pkthdr pcaphdr;
// 確保幀長不超過閾值
if (tp_mac + tp_snaplen > handle->bufsize)
return -1;
bp = frame + tp_mac;
sll = (void *)frame + TPACKET_ALIGN(handlep->tp_hdrlen);
// 如果工作在加工模式,這在這裏構建對應的僞頭
if (handlep->cooked) {
// 略
}
// 如果該pcap句柄開啓了用戶級別過濾,就在這裏進行過濾
if (handlep->filter_in_userland && handle->fcode.bf_insns) {
// 略
}
// 拒絕方向不匹配的包
if (!linux_check_direction(handle, sll))
return 0;
// 記錄包的基礎信息
pcaphdr.ts.tv_sec = tp_sec;
pcaphdr.ts.tv_usec = tp_usec;
pcaphdr.caplen = tp_snaplen;
pcaphdr.len = tp_len;
// 如果工作在加工模式,兩個長度字段還要加上僞頭長
if (handlep->cooked) {
pcaphdr.caplen += SLL_HDR_LEN;
pcaphdr.len += SLL_HDR_LEN;
}
// 只有TPACKET_V2和TPACKET_V3才支持攜帶vlan信息,這裏就是對vlan幀進行了復原
if (tp_vlan_tci_valid && handlep->vlan_offset != -1 && tp_snaplen >= (unsigned int) handlep->vlan_offset) {
struct vlan_tag *tag;
bp -= VLAN_TAG_LEN;
memmove(bp, bp + VLAN_TAG_LEN, handlep->vlan_offset);
tag = (struct vlan_tag *)(bp + handlep->vlan_offset);
tag->vlan_tpid = htons(tp_vlan_tpid);
tag->vlan_tci = htons(tp_vlan_tci);
pcaphdr.caplen += VLAN_TAG_LEN;
pcaphdr.len += VLAN_TAG_LEN;
}
// 如果用戶空間收到了超出閾值長度的包,則截斷
if (pcaphdr.caplen > (bpf_u_int32)handle->snapshot)
pcaphdr.caplen = handle->snapshot;
// 最後調用用戶註冊的捕獲回調函數
callback(user, &pcaphdr, bp);
return 1;
}
小結: 至此,基於TPACKET_V3模式的PACKET_MMAP機制已經分析完畢,至於libpcap中關於TPACKET_V1、TPACKET_V3模式的捕獲流程不再做分析.
這裏僅僅列出了V1、V2、V3這三種模式的差別:
TPACKET_V1 : 這是缺省的環形緩衝區版本
TPACKET_V2 : 相比V1的改進有以下幾點
32位的用戶空間環形緩衝區可以基於64位內核工作;
時間戳的精度從ms提升到ns;
支持攜帶VLAN信息(這意味着通過V1接收到的vlan包將會丟失vlan信息);
TPACKET_V3 : 相比V2的改進有以下幾點
內存塊可以配置成可變幀長(V1、V2的幀長都是tpacket_req.tp_frame_size固定值);
read/poll基於block-level(V1、V2基於packet_level);
開始支持poll超時參數;
新增了用戶可配置選項:tp_retire_blk_tov等;
RX Hash數據可以被用戶使用;
備註: 需要注意的是,V3當前只支持接收環形緩衝區.
相關數據結構:
/* 創建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)
}
// 開啓PACKET_MMAP時libpcap對V1、V2、V3版本環形緩衝區的統一管理結構
union thdr {
struct tpacket_hdr *h1;
struct tpacket2_hdr *h2;
struct tpacket_block_desc *h3;
void *raw;
}
// TPACKET_V3環形緩衝區每個幀的頭部結構
struct tpacket3_hdr {
__u32 tp_next_offset; // 指向同一個內存塊中的下一個幀
__u32 tp_sec; // 時間戳(s)
__u32 tp_nsec; // 時間戳(ns)
__u32 tp_snaplen; // 捕獲到的幀實際長度
__u32 tp_len; // 幀的理論長度
__u32 tp_status; // 幀的狀態
__u16 tp_mac; // 以太網MAC字段距離幀頭的偏移量
__u16 tp_net;
union {
struct tpacket_hdr_variant1 hv1; // 包含vlan信息的子結構
};
__u8 tp_padding[8];
}