一.簡要說明:
1.netmap API主要為兩個頭文件netmap.h 和netmap_user.h ,當解壓下載好的netmap程序后,在./netmap/sys/net/目錄下,本文主要對這兩個頭文件進行分析。
2.我們從netmap_user.h頭文件開始看起。
二.likely()和unlikely()
這兩個宏定義是對編譯器做優化的,並不會對變量做什么改變。后面看到這兩個宏的調用自動忽略就好了。
#ifndef likely #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) #endif /* likely and unlikely */
三.netmap.h頭文件
1.netmap.h被netmap_user.h調用,里面定義了一些宏和幾個主要的結構體,如nmreq{}, netmap_if{}, netmap_ring{}, netmap_slot{}。
2.一個網卡(或者網絡接口)只有一個netmap_if{}結構,在使用mmap()申請的共享內存中,通過netmap_if{}結構可以訪問到任何一個發送/接收環(也就是netmap_ring{}結構,一個netmap_if{}可以對應多發送/接收環,這應該和物理硬件有關 ,我在虛擬機下只有一對環,在真實主機上有兩隊環)。
3.找到netmap_ring{}的地址后,我們就可以找到環中每一個buffer的地址(buffer里面存儲的是將要發送/接收的數據包)。后面會講解這是如何實現的。
4.通過一個nifp是如何訪問到多個收/發環的,通過一個ring如何找到多個不同的buffer地址的,其實都是通過存儲這些結構體相鄰的后面一部分空間實現。(申請共享內存的時候,這些均已被設計好)
四.幾個重要的宏定義
1._NETMAP_OFFSET
#define _NETMAP_OFFSET(type, ptr, offset) \ ((type)(void *)((char *)(ptr) + (offset)))
解釋:該宏定義的作用是將ptr指針(強轉成char *類型)向右偏移offset個字節,再將其轉化為指定的類型type。
2.NETMAP_IF
#define NETMAP_IF(_base, _ofs) _NETMAP_OFFSET(struct netmap_if *, _base, _ofs)
解釋:該宏定義將_base指針向右偏移_ofs個字節后,強轉為netmap_if *類型返回。在nemap中通過此宏得到d->nifp的地址。
3.NETMAP_TXRING
#define NETMAP_TXRING(nifp, index) _NETMAP_OFFSET(struct netmap_ring *, \ nifp, (nifp)->ring_ofs[index] )
解釋:1.通過該宏定義,可以找到nifp的第index個發送環的地址(index是從0開始的),ring_ofs[index]為偏移量,由內核生成。
2.其中,我們注意到struct netmap_if{}最后面只定義了const ssize_t ring_ofs[0],實際上其它的netmap環的偏移量都寫在了該結構體后面的內存地址里面,直接訪問就可以了。
4.NETMAP_RXRING
#define NETMAP_RXRING(nifp, index) _NETMAP_OFFSET(struct netmap_ring *, \ nifp, (nifp)->ring_ofs[index + (nifp)->ni_tx_rings + 1] )
解釋:通過該宏定義,可以找到nifp的第index個接收環的地址,其中(nifp)->ring_ofs[]里面的下標為index+(nifp)->ni_tx_rings+1,正好與發送環的偏移量區間隔開1個。(我想這應該是作者特意設計的)
5.NETMAP_BUF
#define NETMAP_BUF(ring, index) \ ((char *)(ring) + (ring)->buf_ofs + ((index)*(ring)->nr_buf_size))
解釋:1.通過該宏定義,可以找到ring這個環的第index個buffer的地址(buffer里面存的就是我們接收/將發送的完整數據包),每個buffer占的長度是2048字節(在(ring)->nr_buf_size也給出了)。
2.其中(ring) ->buf_ofs是固定的偏移量,不同的環這個值不相同,但所有的(char *)(ring)+(ring)->buf_ofs會指向同一個地址,也就是存放buffer的連續內存的開始地址(d->buf_start會指向該地址)。
6.NETMAP_BUF_IDX
#define NETMAP_BUF_IDX(ring, buf) \ ( ((char *)(buf) - ((char *)(ring) + (ring)->buf_ofs) ) / \ (ring)->nr_buf_size )
解釋:在講NETMAP_BUF的時候我們說(char *)(ring) + (ring)->buf_ofs)總會指向存放buffer的起始位置(無論是哪一個環),在這段內存中將第一個buffer下標標記為0的話,NETMAP_BUF_IDX計算的恰好是指針buf所指buffer的下標。
上面幾個宏一時沒弄懂也沒關系,下面調用的時候還會提的。
五.nm_open函數
1.調用nm_open函數時,如:nmr = nm_open("netmap:eth0", NULL, 0, NULL); nm_open()會對傳遞的ifname指針里面的字符串進行分析,提取出網絡接口名。
2.nm_open()會對struct nm_desc *d申請內存空間,並 通過d->fd = open(NETMAP_DEVICE_NAME, O_RDWR);打開一個特殊的設備/dev/netmap來創建文件描述符d->fd。
3.通過ioctl(d->fd, NIOCREGIF, &d->req)語句,將d->fd綁定到一個特殊的接口,並對d->req結構體里面的成員做初始化,包括a.在共享內存區域中 nifp 的偏移,b.共享區域的大小nr_memsize,c.tx/rx環的大小nr_tx_slots/nr_rx_slots(大小為256),d.tx/rx環的數量nr_tx_rings、nr_rx_rings(視硬件性能而定)等。
4.接着在if ((!(new_flags & NM_OPEN_NO_MMAP) || parent) && nm_mmap(d, parent))語句中調用nm_mmap函數,繼續給d指針指向的內存賦值。
六.nm_mmap函數
nm_mmap()源碼:
static int nm_mmap(struct nm_desc *d, const struct nm_desc *parent) { //XXX TODO: check if mmap is already done if (IS_NETMAP_DESC(parent) && parent->mem && parent->req.nr_arg2 == d->req.nr_arg2) { /* do not mmap, inherit from parent */ D("do not mmap, inherit from parent"); d->memsize = parent->memsize; d->mem = parent->mem; } else { /* XXX TODO: 檢查如果想申請的內存太大 (or there is overflow) */ d->memsize = d->req.nr_memsize; /* 將需要申請的內存大小賦值給d->memsize */ d->mem = mmap(0, d->memsize, PROT_WRITE | PROT_READ, MAP_SHARED, d->fd, 0); /* 申請共享內存 */ if (d->mem == MAP_FAILED) { goto fail; } d->done_mmap = 1; } { struct netmap_if *nifp = NETMAP_IF(d->mem, d->req.nr_offset); /*通過d->req.nr_offset這個偏移量的到nifp的地址,NETMAP_IF前面說過*/ int i; /* *for(i=0; i<=2; i++) * printf("ring_ofs[%d]:0x%x\n",i,nifp->ring_ofs[i]); // 這里是我自己加的,為了手動計算收/發環的偏移量 */ struct netmap_ring *r = NETMAP_RXRING(nifp,); //對nifp,找接收包的環r,因為index為0,所以省略了 *(struct netmap_if **) (uintptr_t) &(d->nifp) = nifp; //對d->nifp賦值,雖然d->nifp使用const定義的,但對其取地址再強值類型轉換后,依然可以對其指向的空間進行操作 *(struct netmap_ring **) (uintptr_t) &d->some_ring = r; //同理,對d->some_ring進行賦值,此處指向了第一個接受(rx)環。 //printf("buf_ofs:0x%x\n", (u_int)r->buf_ofs); *(void **) (uintptr_t) &d->buf_start = NETMAP_BUF(r, 0);//計算第一個buffer的地址,並存入d->buf_start指針中 *(void **) (uintptr_t) &d->buf_end = (char *) d->mem + d->memsize; //計算共享區間的最后一個地址,賦值給d->buf_end } return 0; fail: return EINVAL; }
其中:
1.nifp為申請的共享內存首地址d->mem向右偏移d->req.nr_offset(該值在調用前面的ioctl()時得到)得到。並且一個網絡接口(網卡)只對應一個nifp。(使用宏NETMAP_IF計算)
2.得到的nifp的地址,nifp結構體里最后定義的ring_ofs[0]以及接下來內存中的ring_ofs[1],ring_ofs[2]...,這些內存中存儲的是訪問每一個環(tx or rx ring)的偏移量,通過這個偏移量我們可以得到每一個環的地址(使用宏NETMAP_RXRING/NETMAP_TXRING進行計算)。
3.得到每個收/發環的地址了,netmap_ring結構體最后面有一個struct netmap_slot slot[0];,通過slot[0],后面內存的slot[1],slot[2],slot[3]...,取出里面的偏移量就可以得到每一個buffer(也叫數據包槽)的地址了(使用宏NETMAP_BUF計算得到)。 到這里,netmap如何訪問到內存槽中的每一個buffer的,我們都知道了。實際上netmap運行的數據結構就和下圖描述的一樣:
4.在struct nm_desc中,nifp,some_ring,buf_start,buf_end等指針都定義為const的,但我們通過對其取地址再強轉指針的方式去往這些指針指向的內存中賦值。
注:在nm_mmap()中使用mmap()申請共享的時候,這些數據結構里數據的設計是內核模塊就已寫好了的,我們在這里其實是在做驗證。
七.nm_nextpkt函數
1.nm_nextpkt()是用來接收網卡上到來的數據包的函數。
2.nm_nextpkt()會將所有rx環都檢查一遍,當發現有一個rx環有需要接收的數據包時,得到這個數據包的地址,並返回。所以nm_nextpkt()每次只能取一個數據包。
nm_nextpkt()源代碼:
static u_char *nm_nextpkt(struct nm_desc *d, struct nm_pkthdr *hdr) { int ri = d->cur_rx_ring; //當前的接收環的編號 do { /* compute current ring to use */ struct netmap_ring *ring = NETMAP_RXRING(d->nifp, ri); //得到當前rx環的地址 if (!nm_ring_empty(ring)) //判斷環里是否有新到的包 { u_int i = ring->cur; //當前該訪問哪個槽(buffer)了 u_int idx = ring->slot[i].buf_idx; //得到第i個buffer的下標 //printf("%d\n", idx); u_char *buf = (u_char *) NETMAP_BUF(ring, idx); //得到存有到來數據包的地址 // __builtin_prefetch(buf); hdr->ts = ring->ts; hdr->len = hdr->caplen = ring->slot[i].len; ring->cur = nm_ring_next(ring, i); //ring->cur向后移動一位 /* we could postpone advancing head if we want * to hold the buffer. This can be supported in * the future. */ ring->head = ring->cur; d->cur_rx_ring = ri; //將當前環(d->cur_rx_ring)指向第ri個(因為可能有多個環)。 return buf; //將數據包地址返回 } ri++; if (ri > d->last_rx_ring) //如果ri超過了rx環的數量,則再從第一個rx環開始檢測是否有包到來。 ri = d->first_rx_ring; } while (ri != d->cur_rx_ring); return NULL; /* 什么也沒發現 */ }
八.nm_inject函數
1.nm_inject()是用來往共享內存中寫入待發送的數據包數據的。數據包經共享內存拷貝到網卡,然后發送出去。所以nm_inject()是用來發包的。
2.nm_inject()也會查找所有的發送環(tx環),找到一個可以發送的槽,就將數據包寫入並返回,所以每次函數調用也只能發送一個包。
源代碼:
static int nm_inject(struct nm_desc *d, const void *buf, size_t size) { u_int c, n = d->last_tx_ring - d->first_tx_ring + 1; for (c = 0; c < n; c++) { /* 計算當前的環去使用(compute current ring to use) */ struct netmap_ring *ring; uint32_t i, idx; uint32_t ri = d->cur_tx_ring + c; //該訪問第幾個tx環了 if (ri > d->last_tx_ring) //當超過訪問的tx環的下標范圍時,從頭開始訪問 ri = d->first_tx_ring; ring = NETMAP_TXRING(d->nifp, ri); //得到當前tx環的地址 if (nm_ring_empty(ring)) //如果當前tx環是滿的(ring->cur=ring->tail表示沒地方存數據包了),就跳過 { continue; } i = ring->cur; //當前要往哪個槽(槽指向buffer)中寫入數據 idx = ring->slot[i].buf_idx; //得到這個槽相對於buffer起始地址(d->buf_start)的下標編號 ring->slot[i].len = size; //size為待發送數據包的長度 nm_pkt_copy(buf, NETMAP_BUF(ring, idx), size); //將buf里存的數據包拷貝給ring這個環的第i個槽 d->cur_tx_ring = ri; ring->head = ring->cur = nm_ring_next(ring, i); //將head和cur指向下一個槽 return size; } return 0; /* 失敗 */ }
九.nm_close函數
1.nm_close函數就是回收動態內存,回收共享內存,關閉文件描述符什么的了。
源代碼:
static int nm_close(struct nm_desc *d) { /* * ugly trick to avoid unused warnings */ static void *__xxzt[] __attribute__ ((unused)) = { (void *) nm_open, (void *) nm_inject, (void *) nm_dispatch, (void *) nm_nextpkt }; if (d == NULL || d->self != d) return EINVAL; if (d->done_mmap && d->mem) munmap(d->mem, d->memsize); //釋放申請的共享內存 if (d->fd != -1) { close(d->fd); //關閉文件描述符 } bzero(d, sizeof(*d)); //將d指向的空間全部置0 free(d); //釋放指針d指向的空間 return 0; }