1. 程式人生 > >libpcap原始碼分析_PACKET_MMAP機制

libpcap原始碼分析_PACKET_MMAP機制

使用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)
}