網絡編程中咱們接觸得比較多的是socket api和epoll模型,對於系統內核和網卡驅動接觸得比較少,一方面可能咱們的系統沒有須要深度調優的需求,另外一方面網絡編程涉及到硬件,驅動,內核,虛擬化等複雜的知識,令人望而卻步。網絡上網卡收包相關的資料也比較多,可是比較分散,在此梳理了網卡收包的流程,分享給你們,但願對你們有幫助,文中引用了一些同事的圖表和摘選了網上資料,在文章最後給出了原始的連接,感謝這些做者的分享。javascript
網卡收包從總體上是網線中的高低電平轉換到網卡FIFO存儲再拷貝到系統主內存(DDR3)的過程,其中涉及到網卡控制器,CPU,DMA,驅動程序,在OSI模型中屬於物理層和鏈路層,以下圖所示。java
在內核中網絡數據流涉及到的代碼比較複雜,見圖1(原圖在附件中),其中有3個數據結構在網卡收包的流程中是最主要的角色,它們是:sk_buff,softnet_data,net_device。linux
sk_buff
sk_buff結構是Linux網絡模塊中最重要的數據結構之一。sk_buff能夠在不一樣的網絡協議層之間傳遞,爲了適配不一樣的協議,裏面的大多數成員都是指針,還有一些union,其中data指針和len會在不一樣的協議層中發生改變,在收包流程中,即數據向上層傳遞時,下層的首部就再也不須要了。圖2即演示了數據包發送時指針和len的變化狀況。(linux源碼不一樣的版本有些差異,下面的截圖來自linux 2.6.20)。
算法
圖2 sk_buff在不一樣協議層傳遞時,data指針的變化示例docker
softnet_data編程
本節主要引用網絡上的文章,在關鍵的地方加了一些備註,騰訊公司內部主要使用Intel 82576網卡和Intel igb驅動,和下面的網卡和驅動不同,實際上原理是同樣的,只是一些函數命名和處理的細節不同,並不影響理解。
網絡驅動收包大體有3種狀況:
no NAPI:
mac每收到一個以太網包,都會產生一個接收中斷給cpu,即徹底靠中斷方式來收包
缺點是當網絡流量很大時,cpu大部分時間都耗在了處理mac的中斷。
netpoll:
在網絡和I/O子系統尚不能完整可用時,模擬了來自指定設備的中斷,即輪詢收包。
缺點是實時性差
NAPI:
採用中斷 + 輪詢的方式:mac收到一個包來後會產生接收中斷,可是立刻關閉。
直到收夠了netdev_max_backlog個包(默認300),或者收完mac上全部包後,纔再打開接收中斷
經過sysctl來修改 net.core.netdev_max_backlog
或者經過proc修改 /proc/sys/net/core/netdev_max_backlogapi
start_kernel()
--> rest_init()
--> do_basic_setup()
--> do_initcall
-->net_dev_init
__init net_dev_init(){
//每一個CPU都有一個CPU私有變量 _get_cpu_var(softnet_data)
//_get_cpu_var(softnet_data).poll_list很重要,軟中斷中須要遍歷它的
for_each_possible_cpu(i) {
struct softnet_data *queue;
queue = &per_cpu(softnet_data, i);
skb_queue_head_init(&queue->input_pkt_queue);
queue->completion_queue = NULL;
INIT_LIST_HEAD(&queue->poll_list);
queue->backlog.poll = process_backlog;
queue->backlog.weight = weight_p;
}
//在軟中斷上掛網絡發送handler
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
//在軟中斷上掛網絡接收handler
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
}複製代碼
softirq
中斷處理「下半部」機制
中斷服務程序通常都是在中斷請求關閉的條件下執行的,以免嵌套而使中斷控制複雜化。可是,中斷是一個隨機事件,它隨時會到來,若是關中斷的時間太長,CPU就不能及時響應其餘的中斷請求,從而形成中斷的丟失。
所以,Linux內核的目標就是儘量快的處理完中斷請求,盡其所能把更多的處理向後推遲。例如,假設一個數據塊已經達到了網線,當中斷控制器接受到這個中斷請求信號時,Linux內核只是簡單地標誌數據到來了,而後讓處理器恢復到它之前運行的狀態,其他的處理稍後再進行(如把數據移入一個緩衝區,接受數據的進程就能夠在緩衝區找到數據)。
所以,內核把中斷處理分爲兩部分:上半部(top-half)和下半部(bottom-half),上半部(就是中斷服務程序)內核當即執行,而下半部(就是一些內核函數)留着稍後處理。
2.6內核中的「下半部」處理機制:
1) 軟中斷請求(softirq)機制(注意不要和進程間通訊的signal混淆)
2) 小任務(tasklet)機制
3) 工做隊列機制
咱們能夠經過top命令查看softirq佔用cpu的狀況:數組
NOTE:這裏的網絡設備是指MAC層的網絡設備,即TSEC和PCI網卡(bcm5461是phy)在網絡設備驅動中建立net_device數據結構,並初始化其鉤子函數 open(),close() 等掛載TSEC的驅動的入口函數是 gfar_probe
緩存
// 平臺設備 TSEC 的數據結構
static struct platform_driver gfar_driver = {
.probe = gfar_probe,
.remove = gfar_remove,
.driver = {
.name = "fsl-gianfar",
},
};
int gfar_probe(struct platform_device *pdev)
{
dev = alloc_etherdev(sizeof (*priv)); // 建立net_device數據結構
dev->open = gfar_enet_open;
dev->hard_start_xmit = gfar_start_xmit;
dev->tx_timeout = gfar_timeout;
dev->watchdog_timeo = TX_TIMEOUT;
#ifdef CONFIG_GFAR_NAPI
netif_napi_add(dev, &priv->napi,gfar_poll,GFAR_DEV_WEIGHT); //軟中斷裏會調用poll鉤子函數
#endif
#ifdef CONFIG_NET_POLL_CONTROLLER
dev->poll_controller = gfar_netpoll;
#endif
dev->stop = gfar_close;
dev->change_mtu = gfar_change_mtu;
dev->mtu = 1500;
dev->set_multicast_list = gfar_set_multi;
dev->set_mac_address = gfar_set_mac_address;
dev->ethtool_ops = &gfar_ethtool_ops;
}複製代碼
3.3.1 用戶調用ifconfig等程序,而後經過ioctl系統調用進入內核
socket的ioctl()系統調用服務器
--> sock_ioctl()
--> dev_ioctl() //判斷SIOCSIFFLAGS
--> __dev_get_by_name(net, ifr->ifr_name) //根據名字選net_device
--> dev_change_flags() //判斷IFF_UP
--> dev_open(net_device) //調用open鉤子函數複製代碼
對於TSEC來講,掛的鉤子函數是 gfar_enet_open(net_device)
3.3.2 在網絡設備的open鉤子函數裏,分配接收bd,掛中斷ISR(包括rx、tx、err),對於TSEC來講
gfar_enet_open
-->給Rx Tx Bd 分配一致性DMA內存
-->把Rx Bd的「EA地址」賦給數據結構,物理地址賦給TSEC寄存器
-->把Tx Bd的「EA地址」賦給數據結構,物理地址賦給TSEC寄存器
-->給 tx_skbuff 指針數組分配內存,並初始化爲NULL
-->給 rx_skbuff 指針數組分配內存,並初始化爲NULL
-->初始化Tx Bd
-->初始化Rx Bd,提早分配存儲以太網包的skb,這裏使用的是一次性dma映射
(注意:`#define DEFAULT_RX_BUFFER_SIZE 1536`保證了skb能存一個以太網包)複製代碼
rxbdp = priv->rx_bd_base;
for (i = 0; i < priv->rx_ring_size; i++) {
struct sk_buff *skb = NULL;
rxbdp->status = 0;
//這裏真正分配skb,而且初始化rxbpd->bufPtr, rxbdpd->length
skb = gfar_new_skb(dev, rxbdp);
priv->rx_skbuff[i] = skb;
rxbdp++;
}
rxbdp--;
rxbdp->status |= RXBD_WRAP; // 給最後一個bd設置標記WRAP標記複製代碼
-->註冊TSEC相關的中斷handler:錯誤,接收,發送複製代碼
request_irq(priv->interruptError, gfar_error, 0, "enet_error", dev)
request_irq(priv->interruptTransmit, gfar_transmit, 0, "enet_tx", dev)//包發送完
request_irq(priv->interruptReceive, gfar_receive, 0, "enet_rx", dev) //包接收完
-->gfar_start(net_device)
// 使能Rx、Tx
// 開啓TSEC的 DMA 寄存器
// Mask 掉咱們不關心的中斷event複製代碼
最終,TSEC相關的Bd等數據結構應該是下面這個樣子的
TSEC的RX已經使能了,網絡數據包進入內存的流程爲:
網線 --> Rj45網口 --> MDI 差分線
--> bcm5461(PHY芯片進行數模轉換) --> MII總線
--> TSEC的DMA Engine 會自動檢查下一個可用的Rx bd
-->把網絡數據包 DMA 到 Rx bd 所指向的內存,即skb->data
接收到一個完整的以太網數據包後,TSEC會根據event mask觸發一個 Rx 外部中斷。
cpu保存現場,根據中斷向量,開始執行外部中斷處理函數do_IRQ()
do_IRQ 僞代碼
上半部處理硬中斷
查看中斷源寄存器,得知是網絡外設產生了外部中斷
執行網絡設備的rx中斷handler(設備不一樣,函數不一樣,但流程相似,TSEC是gfar_receive)
gfar_receive{
#ifdef CONFIG_GFAR_NAPI
// test_and_set當前net_device的napi_struct.state 爲 NAPI_STATE_SCHED
// 在軟中斷裏調用 net_rx_action 會檢查狀態 napi_struct.state
if (netif_rx_schedule_prep(dev, &priv->napi)) {
tempval = gfar_read(&priv->regs->imask);
tempval &= IMASK_RX_DISABLED; //mask掉rx,再也不產生rx中斷
gfar_write(&priv->regs->imask, tempval);
// 將當前net_device的 napi_struct.poll_list 掛到
// CPU私有變量__get_cpu_var(softnet_data).poll_list 上,並觸發軟中斷
// 因此,在軟中斷中調用 net_rx_action 的時候,就會執行當前net_device的
// napi_struct.poll()鉤子函數,即 gfar_poll()
__netif_rx_schedule(dev, &priv->napi);
}
#else
gfar_clean_rx_ring(dev, priv->rx_ring_size);
#endif
}複製代碼
3.4.2 網絡接收軟中斷net_rx_actionnet_rx_action(){
struct list_head *list = &__get_cpu_var(softnet_data).poll_list;
//經過 napi_struct.poll_list,將N多個 napi_struct 連接到一條鏈上
//經過 CPU私有變量,咱們找到了鏈頭,而後開始遍歷這個鏈
int budget = netdev_budget; //這個值就是 net.core.netdev_max_backlog,經過sysctl來修改
while (!list_empty(list)) {
struct napi_struct *n;
int work, weight;
local_irq_enable();
//從鏈上取一個 napi_struct 結構(接收中斷處理函數里加到鏈表上的,如gfar_receive)
n = list_entry(list->next, struct napi_struct, poll_list);
weight = n->weight;
work = 0;
if (test_bit(NAPI_STATE_SCHED, &n->state)) //檢查狀態標記,此標記在接收中斷里加上的
//使用NAPI的話,使用的是網絡設備本身的napi_struct.poll
//對於TSEC是,是gfar_poll
work = n->poll(n, weight);
WARN_ON_ONCE(work > weight);
budget -= work;
local_irq_disable();
if (unlikely(work == weight)) {
if (unlikely(napi_disable_pending(n)))
//操做napi_struct,把去掉NAPI_STATE_SCHED狀態,從鏈表中刪去
__napi_complete(n);
else
list_move_tail(&n->poll_list, list);
}
netpoll_poll_unlock(have);
}
out:
local_irq_enable();
}
static int gfar_poll(struct napi_struct *napi, int budget){
struct gfar_private *priv = container_of(napi, struct gfar_private, napi);
struct net_device *dev = priv->dev; //TSEC對應的網絡設備
int howmany;
//根據dev的rx bd,獲取skb並送入協議棧,返回處理的skb的個數,即以太網包的個數
howmany = gfar_clean_rx_ring(dev, budget);
// 下面這個判斷比較有講究的
// 收到的包的個數小於budget,表明咱們在一個軟中斷裏就全處理完了,因此打開 rx中斷
// 要是收到的包的個數大於budget,表示一個軟中斷裏處理不完全部包,那就不打開rx 中斷,
// 待到下一個軟中斷裏再接着處理,直到把全部包處理完(即howmany<budget),再打開rx 中斷
if (howmany < budget) {
netif_rx_complete(dev, napi);
gfar_write(&priv->regs->rstat, RSTAT_CLEAR_RHALT);
//打開 rx 中斷,rx 中斷是在gfar_receive()中被關閉的
gfar_write(&priv->regs->imask, IMASK_DEFAULT);
}
return howmany;
}複製代碼
gfar_clean_rx_ring(dev, budget){
bdp = priv->cur_rx;
while (!((bdp->status & RXBD_EMPTY) || (--rx_work_limit < 0))) {
rmb();
skb = priv->rx_skbuff[priv->skb_currx]; //從rx_skbugg[]中獲取skb
howmany++;
dev->stats.rx_packets++;
pkt_len = bdp->length - 4; //從length中去掉以太網包的FCS長度
gfar_process_frame(dev, skb, pkt_len);
dev->stats.rx_bytes += pkt_len;
dev->last_rx = jiffies;
bdp->status &= ~RXBD_STATS; //清rx bd的狀態
skb = gfar_new_skb(dev, bdp); // Add another skb for the future
priv->rx_skbuff[priv->skb_currx] = skb;
if (bdp->status & RXBD_WRAP) //更新指向bd的指針
bdp = priv->rx_bd_base; //bd有WARP標記,說明是最後一個bd了,須要「繞回來」
else
bdp++;
priv->skb_currx = (priv->skb_currx + 1) & RX_RING_MOD_MASK(priv->rx_ring_size);
}
priv->cur_rx = bdp; /* Update the current rxbd pointer to be the next one */
return howmany;
}
gfar_process_frame()
-->RECEIVE(skb) //調用netif_receive_skb(skb)進入協議棧
#ifdef CONFIG_GFAR_NAPI
#define RECEIVE(x) netif_receive_skb(x)
#else
#define RECEIVE(x) netif_rx(x)
#endif複製代碼
在軟中斷中使用NAPI
上面net_rx_action的主要流程如圖4所示,執行一次網絡軟中斷過程當中,網卡自己的Rx中斷已經關閉了,即不會產生新的接收中斷了。local_irq_enable和local_irq_disable設置的是cpu是否接收中斷。進入網絡軟中斷net_rx_action的時候,會初始一個budget(預算),即最多處理的網絡包個數,若是有多個網卡(放在poll_list裏),是共享該budget,同時每一個網卡也一個權重weight或者說是配額quota,一個網卡處理完輸入隊列裏包後有兩種狀況,一次收到的包不少,quota用完了,則把收包的poll虛函數又掛到poll_list隊尾,從新設置一下quota值,等待while輪詢;另一種狀況是,收到的包很少,quota沒有用完,表示網卡比較空閒,則把本身從poll_list摘除,退出輪詢。整個net_rx_action退出的狀況有兩種:budget所有用完了或者是時間超時了。
在網卡收包中涉及到DMA的操做,DMA的主要做用是讓外設間(如網卡和主內存)傳輸數據而不須要CPU的參與(即不須要CPU使用專門的IO指令來拷貝數據),下面簡單介紹一下DMA的原理,如圖5所示。
網卡多隊列是硬件的一種特性,同時也須要內核支持,騰訊公司使用的Intel 82576是支持網卡多隊列的,並且內核版本要大於2.6.20。對於單隊列的網卡,只能產生一箇中斷信號,而且只能由一個cpu來處理,這樣會致使多核系統中一個核(默認是cpu0)負載很高。網卡多隊列在網卡的內部維持多個收發隊列,併產生多箇中斷信號使不一樣的cpu都能處理網卡收到的包,從而提高了性能,如圖6所示。
服務器虛擬化技術在分佈式系統中很常見,它能提升設備利用率和運營效率。服務器虛擬化包括處理器虛擬化,內存虛擬化和I/0設備的虛擬化,與網絡有關的虛擬化屬於I/0虛擬化,I/0設備虛擬化的做用是單個I/O設備能夠被多個虛擬機共享使用。對於客戶機操做系統中的應用程序來講,它發起 I/O 操做的流程和在真實硬件平臺上的操做系統是同樣的,整個 I/O 流程有所不一樣的在於設備驅動訪問硬件這個部分。I/0虛擬化通過多年的發展,主要模型如表1所示,早期的設備仿真如圖7所示,能夠看到網絡數據包從物理網卡到虛擬機中的進程須要通過不少額外的處理,效率很低。SR-IOV則直接從硬件上支持虛擬化,如圖8所示,設備劃分爲一個物理功能單元PF(Physical Functions)和多個虛擬功能單元 VF(Virtual Function),每一個虛擬功能單元均可以做爲一個輕量級的 I/O 設備供虛擬機使用,這樣一個設備就能夠同時被分配給多個虛擬機,解決了因設備數量限制給虛擬化系統帶來的可擴展性差的問題。每一個 VF 都有一個惟一的 RID(Requester Identifier,請求標識號)和收發數據包的關鍵資源,如發送隊列、接收隊列、DMA 通道等,所以每一個 VF 都具備獨立收發數據包的功能。全部的 VF 共享主要的設備資源,如數據鏈路層的處理和報文分類。
docker中使用SR-IOV
激活VF
#echo "options igb max_vfs=7" >>/etc/modprobe.d/igb.conf
#reboot
設置VF的VLAN#ip link set eth1 vf 0 vlan 12
將VF移到container network namespace#ip link set eth4 netns $pid
#ip netns exec $pid ip link set dev eth4 name eth1
#ip netns exec $pid ip link set dev eth1 up
In container:
設置IP#ip addr add 10.217.121.107/21 dev eth1
網關#ip route add default via 10.217.120.1