內核代碼中,ip_rcv是ip層收包的主入口函數,該函數由軟中斷調用。存放數據包的sk_buff結構包含有目的地ip和端口信息,此時ip層進行檢查,如果目的地ip不是本機,且沒有開啟轉發的話,則將包丟棄,如果配置了netfilter,則按照配置規則對包進行轉發。
tcp_v4_rcv是tcp層收包的接收入口,其調用__inet_lookup_skb函數查到數據包需要往哪個socket傳送,之后將數據包放入tcp層收包隊列中,如果應用層有read之類的函數調用,隊列中的包將被取出。
最近遇到一個問題,就是libpcap的收包,比tcpdump的收包,要慢。
然后修改測試代碼如下:
#include <pcap.h> /*libpcap*/ static pcap_t * pcap_http_in; int initPcapIn_http() { int snaplen = 1518;//以太網數據包,最大長度為1518bytes int promisc = 1;//混雜模式 int timeout = 1000; char errbuf[PCAP_ERRBUF_SIZE];//內核緩沖區大小 /*這個設備號需要根據測試服務器更改*/ char * _pcap_in = "enp8s0f1"; char * _bpf_filter = "tcp[13]=24"; /*打開輸入設備或者文件*/ if((pcap_http_in = pcap_open_live(_pcap_in, snaplen, promisc, timeout, errbuf)) == NULL) { printf("pcap_open_live(%s) error, %s\n", _pcap_in, errbuf); pcap_http_in = pcap_open_offline(_pcap_in, errbuf); if(pcap_http_in == NULL) { printf("pcap_open_offline(%s): %s\n", _pcap_in, errbuf); } else printf("Reading packets from pcap file %s...\n", _pcap_in); } else { printf("Capturing live traffic from device %s...\n", _pcap_in); /*設置bpf過濾參數。*/ if(_bpf_filter!= NULL) { struct bpf_program fcode; if(pcap_compile(pcap_http_in, &fcode, _bpf_filter, 1, 0xFFFFFF00) < 0) { printf("pcap_compile error: '%s'\n", pcap_geterr(pcap_http_in)); } else { if(pcap_setfilter(pcap_http_in, &fcode) < 0) { printf("pcap_setfilter error: '%s'\n", pcap_geterr(pcap_http_in)); } else printf("Succesfully set BPF filter to '%s'\n", _bpf_filter); } } /*設置一些參數*/ if(pcap_setdirection(pcap_http_in, PCAP_D_IN)<0) /*只抓入向包*/ { printf("pcap_setdirection error: '%s'\n", pcap_geterr(pcap_http_in)); } else printf("Succesfully set direction to '%s'\n", "PCAP_D_IN"); } return 0; } static inline unsigned long long rp_get_us(void) { struct timeval tv = {0}; gettimeofday(&tv, NULL); return (unsigned long long)(tv.tv_sec*1000000L + tv.tv_usec); } int main(int argc, char *argv[]) { initPcapIn_http(); unsigned char * pkt_data = NULL; struct pcap_pkthdr pcap_hdr; struct pcap_pkthdr * pkt_hdr = &pcap_hdr; while(1) { while( (pkt_data = (unsigned char * )pcap_next( pcap_http_in, &pcap_hdr))!=NULL) { if(pkt_hdr->caplen == 454) { unsigned long long time1 = rp_get_us(); printf("---BEGIN: %ld us\n",time1); } } } if (pcap_http_in) { pcap_close(pcap_http_in); } return 0; }
一開始靜態編譯,gcc靜態編譯報錯,/usr/bin/ld: cannot find -lc
Makefile中肯定有-static選項。這其實是靜態鏈接時沒有找到libc.a。
其實需要安裝glibc-static.xxx.rpm,如glibc-static-2.12-1.107.el6_4.2.i686.rpm,或是yum install glibc-static,我最終下載的是:glibc-static-2.17-157.el7.x86_64.rpm.
結果測試發現,我們打印的---BEGIN的時間,比tcpdump對應的時間晚1ms左右,也就是1000us左右。
然后我們根據tcpdump的調用方式,發現
ldd /sbin/tcpdump |grep -i pcap libpcap.so.1 => /usr/local/lib/libpcap.so.1 (0x00007fd1903f1000)
ls -alrt /usr/local/lib/libpcap.so.1
lrwxrwxrwx. 1 root root 16 7月 25 19:36 /usr/local/lib/libpcap.so.1 -> libpcap.so.1.5.3
然后我將我的編譯方式改成動態鏈接方式,即
gcc -lpcap -g -o pcap.o pcap.c
發現效果很好,跟tcpdump差不多,也就是說,動態鏈接的lpcap的性能比靜態鏈接的lpcap的性能要好。顛覆了我的認知,因為我一直認為靜態鏈接快一點是有可能的。
我下載的源碼是http://www.tcpdump.org/release/官網的,系統自帶的版本和我的版本號一致。
發現不管是gcc -O2還是O3都是如此,我的靜態鏈接的庫就是慢,然后將tcpdump官網的libpcap庫改成動態鏈接,還是慢。對比如下:
自己tcpdump官網下載的1.5.3的libpcap如下 poll([{fd=3, events=POLLIN}], 1, 1000) = 1 ([{fd=3, revents=POLLIN}]) <0.103021> poll([{fd=3, events=POLLIN}], 1, 1000) = 1 ([{fd=3, revents=POLLIN}]) <0.095322> poll([{fd=3, events=POLLIN}], 1, 1000) = 1 ([{fd=3, revents=POLLIN}]) <0.101384> poll([{fd=3, events=POLLIN}], 1, 1000) = 1 ([{fd=3, revents=POLLIN}]) <0.100031> 對應的系統自帶的1.5.3版本如下: poll([{fd=3, events=POLLIN}], 1, 1000) = 1 ([{fd=3, revents=POLLIN}]) <0.000139> poll([{fd=3, events=POLLIN}], 1, 1000) = 1 ([{fd=3, revents=POLLIN}]) <0.000061> poll([{fd=3, events=POLLIN}], 1, 1000) = 1 ([{fd=3, revents=POLLIN}]) <0.000231> poll([{fd=3, events=POLLIN}], 1, 1000) = 1 ([{fd=3, revents=POLLIN}]) <0.000062>
從參數看,是一模一樣的,但是調用的消耗看,前者明顯慢,直覺告訴我,應該看fd的屬性,所以針對屬性又單獨跟蹤了一次:
系統自帶的1.5.3版本: [root@localhost libpcap-1.5.3]# strace -e setsockopt ./pcaptest setsockopt(3, SOL_PACKET, PACKET_ADD_MEMBERSHIP, "\3\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0", 16) = 0 setsockopt(3, SOL_PACKET, PACKET_AUXDATA, [1], 4) = 0 setsockopt(3, SOL_PACKET, PACKET_VERSION, [1], 4) = 0 setsockopt(3, SOL_PACKET, PACKET_RESERVE, [4], 4) = 0 setsockopt(3, SOL_PACKET, PACKET_RX_RING, {block_size=4096, block_nr=655, frame_size=1600, frame_nr=1310}, 16) = 0 Capturing live traffic from device enp5s0... setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, "\1\0\0\0\0\0\0\0\224\246c\0\0\0\0\0", 16) = 0 setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, "\v\0\0\0\0\0\0\0\260\276\357\1\0\0\0\0", 16) = 0 我在tcpdump官網下載的libpcap版本: [root@localhost libpcap-1.5.3]# strace -e setsockopt ./pcaptest setsockopt(3, SOL_PACKET, PACKET_ADD_MEMBERSHIP, "\3\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0", 16) = 0 setsockopt(3, SOL_PACKET, PACKET_AUXDATA, [1], 4) = 0 setsockopt(3, SOL_PACKET, PACKET_VERSION, [2], 4) = 0 setsockopt(3, SOL_PACKET, PACKET_RESERVE, [4], 4) = 0 setsockopt(3, SOL_PACKET, PACKET_RX_RING, "\0\0\2\0\20\0\0\0\0\0\2\0\20\0\0\0\350\3\0\0\0\0\0\0\0\0\0\0", 28) = 0 Capturing live traffic from device enp5s0... setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, "\1\0\0\0\0\0\0\0\224\246c\0\0\0\0\0", 16) = 0 setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, "\v\0\0\0\0\0\0\0P\6\343\1\0\0\0\0", 16) = 0 Succesfully set BPF filter to 'tcp[13]=24' Succesfully set direction to 'PCAP_D_IN'
果然參數不一樣,設置的PACKET_VERSION,快的是2,慢的是3.
同樣的版本號,難道代碼有區別,走查代碼流程,再加上gdb和strace,確定是PACKET_VERSION的問題。
(gdb) p *(struct pcap_linux*)handle->priv
$2 = {packets_read = 26, proc_dropped = 76591622, stat = {ps_recv = 0, ps_drop = 0, ps_ifdrop = 0}, device = 0x81c460 "br0", filter_in_userland = 0, blocks_to_filter_in_userland = 0, must_do_on_close = 0, timeout = 1000, sock_packet = 0, cooked = 0, ifindex = 8, lo_ifindex = 1, oldmode = 0, mondevice = 0x0, mmapbuf = 0x7ffff513d000 "\002", mmapbuflen = 2097152, vlan_offset = 12, tp_version = 2, tp_hdrlen = 36, oneshot_buffer = 0x81c940 "", current_packet = 0x0, packets_left = 0}
PACKET_VERSION就是一個宏決定的,后來發現,系統自帶的版本,沒有定義define HAVE_TPACKET3,所以不會走V3的版本。
那么,下一步就需要排查,怎么會慢,慢在哪里。
用戶態沒有問題,直接調用gettimeofday打印下時間。內核態要用stap跟蹤了。
由於是抓包,那么在哪里下樁呢?
我們先來看pacap的版本,理清楚調用。
在 pcap_activate_linux 函數中,會先設置handle的默認的一些回調。
handle->inject_op = pcap_inject_linux; handle->setfilter_op = pcap_setfilter_linux; handle->setdirection_op = pcap_setdirection_linux; handle->set_datalink_op = pcap_set_datalink_linux; handle->getnonblock_op = pcap_getnonblock_fd; handle->setnonblock_op = pcap_setnonblock_fd; handle->cleanup_op = pcap_cleanup_linux; handle->read_op = pcap_read_linux; handle->stats_op = pcap_stats_linux;
在此之后,會先嘗試activate_new方法,如果失敗的話,則會回退到activate_old方法,其實這種命名不太好,因為后續內核發展了,總不能叫activate_new_new.
在activate_new中,
sock_fd = is_any_device ?
socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));:后面要用到,協議類型和socket類型
這個申請socket單獨拿出來說,是因為根據是否抓包帶-any 參數,后面下鈎子的地方不一樣。
如果activate_new調用成功,則會繼續判斷activate_mmap ,這個函數其實就是測試內核是否支持mmap的方式來抓取報文,如果支持的話,就使用mmap的方式來獲取報文,否則,就會回退到
之前的方法使用。針對mmap收包的情況,tp_version也有三個version,分別是v1,v2,v3,代碼中會嘗試先設置v3,如果不行則設置v2,以此類推。最終會調用 init_tpacket 來設置socket屬性。先通過 getsockopt(handle->fd, SOL_PACKET, PACKET_HDRLEN, &val, &len) 來獲取能力,然后使用
setsockopt(handle->fd, SOL_PACKET, PACKET_VERSION, &val,sizeof(val)) 來設置。PACKET_MMAP非常高效,它提供一個映射到用戶空間的大小可配置的環形緩沖區。這種方式,讀取報文只需要等待報文就可以了,大部分情況下不需要系統調用(其實poll也是一次系統調用)。通過內核空間和用戶空間共享的緩沖區還可以起到減少數據拷貝的作用。
當然為了提高捕獲的性能,不僅僅只是PACKET_MMAP。如果你在捕獲一個高速網絡中的數據,你應該檢查NIC是否支持一些中斷負載緩和機制或者是NAPI,確定開啟這些措施。
PACKET_MMAP減少了系統調用,不用recvmsg就可以讀取到捕獲的報文,相比原始套接字+recvfrom的方式,減少了一次拷貝和一次系統調用,但是低版本的libpcap是不支持PACKET_MMAP,比如libpcap 0.8.1以及之前的版本都不支持,具體的資料,大家可以參考Document/networking/packet_mmap.txt,而本文最終的問題,剛好出在PACKET_MMAP上面,所謂成也蕭何,敗也蕭何,下面繼續分析代碼:
activate_mmap -->|prepare_tpacket_socket-->init_tpacket
-->|create_ring(關於ring的一些設置),很關鍵地調用了if (setsockopt(handle->fd, SOL_PACKET, PACKET_RX_RING,(void *) &req, sizeof(req)))
-->|pcap_read_linux_mmap_v3(1,2),以及handle的其他一些回調設置。
繼續libpcap的代碼:
switch (handlep->tp_version) { case TPACKET_V1: handle->read_op = pcap_read_linux_mmap_v1; break; #ifdef HAVE_TPACKET2 case TPACKET_V2: handle->read_op = pcap_read_linux_mmap_v2; break; #endif #ifdef HAVE_TPACKET3 case TPACKET_V3: handle->read_op = pcap_read_linux_mmap_v3; break; #endif } 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;
如果activate_new失敗,則會嘗試activate_old,那么創建socket就會使用:
handle->fd = socket(PF_INET, SOCK_PACKET, htons(ETH_P_ALL));
這個注意和上面activate_new使用的socket方法相區別。這種模式不支持-any參數。
因為我目前使用的內核是3.10.0+,所以不會走active_old流程。
在libpcap的庫中,pcap_open_live-->|pcap_create--->pcap_create_interface,設置handle->activate_op = pcap_activate_linux;
|pcap_activate-->pcap_activate_linux
在tcpdump的代碼中,直接就是main函數調用pcap_create和pcap_activate。然后在pcap_loop中循環收包,調用鏈分析結束。
下一步,需要了解,在內核態收包和在用戶態收包的鈎子下在哪里比較合適。因為使用的socket是SOCK_RAW,family是PF_PACKET,而且我們使用的是mmap收包,所以有必要看一下
PF_PACKET針對mmap的收包函數。所以就針對SOCK_RAW的收包去下鈎子。
從創建socket的地方看起:
static const struct net_proto_family packet_family_ops = { .family = PF_PACKET, .create = packet_create, .owner = THIS_MODULE, }; static int packet_create(struct net *net, struct socket *sock, int protocol, int kern) { 。。。 spin_lock_init(&po->bind_lock); mutex_init(&po->pg_vec_lock); po->prot_hook.func = packet_rcv; 。。。 }
那么看起來要在packet_rcv 下鈎子,但是因為libpcap里面針對create_ring的函數調用if (setsockopt(handle->fd, SOL_PACKET, PACKET_RX_RING,(void *) &req, sizeof(req)))
使得packet_setsockopt函數中針對po->prot_hook.func 修改為了 tpacket_rcv ,所以我們應該在這個函數下鈎子。tpacket_rcv是PACKET_MMAP的實現,packet_rcv是普通AF_PACKET的實現。
static int
packet_setsockopt(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen) { 。。。。。 case PACKET_RX_RING: case PACKET_TX_RING: { union tpacket_req_u req_u; int len; switch (po->tp_version) { case TPACKET_V1: case TPACKET_V2: len = sizeof(req_u.req); break; case TPACKET_V3: default: len = sizeof(req_u.req3); break; } if (optlen < len) return -EINVAL; if (pkt_sk(sk)->has_vnet_hdr) return -EINVAL; if (copy_from_user(&req_u.req, optval, len)) return -EFAULT; return packet_set_ring(sk, &req_u, 0, optname == PACKET_TX_RING); 。。。。。。 } static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u, int closing, int tx_ring) { 。。。。 po->prot_hook.func = (po->rx_ring.pg_vec) ? tpacket_rcv : packet_rcv;-------------決定了下樁的函數,我們是tpacket_rcv
。。。。。
這個偷懶一下,借用同事塗強的腳本,
probe kernel.function("tpacket_rcv") { iphdr = __get_skb_iphdr($skb) saddr = format_ipaddr(__ip_skb_saddr(iphdr), %{ /* pure */ AF_INET %}) daddr = format_ipaddr(__ip_skb_daddr(iphdr), %{ /* pure */ AF_INET %}) tcphdr = __get_skb_tcphdr($skb) dport = __tcp_skb_dport(tcphdr) sport = __tcp_skb_sport(tcphdr) psh = __tcp_skb_psh(tcphdr) ack = __tcp_skb_ack(tcphdr) if(dport == 80 && psh == 1 && ack == 1) { printf("%-25s %-10d, ts:%ld %s %s %d %d\n", execname(), pid(), gettimeofday_us(), saddr, daddr, dport, sport) } }
取樣數據如下:
swapper/2 0 , ts:1516709467430812 10.74.44.16 10.75.9.158 80 53217 //內核時間戳 swapper/2 0 , ts:1516709467430822 10.74.44.16 10.75.9.158 80 53217 ---BEGIN: 1516709467431852 us //tcpdump官網下載的libpcap版本 ---BEGIN: 1516709467430831 us //centos官網下載的系統自帶libpcap版本
可以看出,tcpdump官網下載的版本,也就是TPACKET_V3使能的,在內核收到包的時候,還是和TPACKET_V2的差10us,但是到用戶態poll收包,則慢了1000us左右。說明redhat系列內核對
TPACKET_V3支持得肯定有問題。
rpm -qpi libpcap --changelog 查詢變更記錄,找到了相關信息:
* Tue Dec 02 2014 Michal Sekletar <msekleta@redhat.com> - 14:1.5.3-4
- disable TPACKET_V3 memory mapped packet capture on AF_PACKET socket, use TPACKET_V2 instead (#1085096)
在https://git.centos.org/blobdiff/rpms!libpcap.git/ed18a5631cc5de8fa95805d0cfd29a0678ea1458/SPECS!libpcap.spec中,找到了對應的patch。
Patch5: 0001-pcap-linux-don-t-use-TPACKETV3-for-memory-mmapped-ca.patch
uname -a
Linux centos7 3.10.0+
綜上所述,如果是redhat系列,需要關閉TPACKETV3,不能直接使用tcpdump官網的libpcap包,suse的不存在這個問題。