Linux內核實現了數據包的隊列機制,配合多種不同的排隊策略,可以實現完美的流量控制和流量整形(以下統稱流控)。流控可以在兩個地方實現,分別為egress和ingress,egress是在數據包發出前的動作觸發點,而ingress是在數據包接收後的動作觸發點。Linux的流控在這兩個位置實現的並不對稱,即Linux並沒有在ingress這個位置實現隊列機制。那麼在ingress上就幾乎不能實現流控了。
既然有需求,就要想法子滿足需求。目前我們知道的是,只能在egress做流控,但是又不能讓數據真的outgoing,另外,我們需要可以做很多策略,這些策略遠不是僅由IP,協議,端口這5元組可以給出。那麼一個顯而易見的方案就是用虛擬網卡來完成,圖示如下:
以上的原理圖很簡單,但是實施起來還真有幾個細節。其中最關鍵是路由的細節,我們知道,即使是策略路由,也必須無條件從local表開始查找,在目標地址是本機情況下,如果希望數據按照以上流程走的話,就必須將該地址從local表刪除,然而一旦刪除,本機將不再會對該地址回應ARP請求。因此可以用幾個方案:
1.使用靜態ARP或者使用ebtables更改ARP,或者使用arping主動廣播arp配置;
vc3Ryb25nPjxiciAvJmd0O7K7v7zCx8+4vdqjrL32vs3Jz8r21K3A7c28zNbC27XEu7CjrMTjv8nS1NTas6O55sK3vra1xFBSRVJPVVRJTkfW0Nf2uty24MrCx+mjrLHIyOfKudPDc29ja2V0IG1hdGNoudjBqnNvY2tldKOs0rK/ydLUyrnTw0lQTUFSS6GjPGJyIC8mZ3Q7ICAgICAgIM/Cw+ajrM7Sw8e+zb/J0tSwtNXVyc/K9s28yr6jrMq1vMrKtc/W0ru49sTc08O1xKGjytfPyM/I0qrKtc/W0ru49tDpxOLN+L+ooaO3wtXVbG9vcGJhY2u907/a1/bSu7j208PT2sH3v9i1xNDpxOK907/ao6zK18/ItLS9qNK7uPbTw9PaaW5ncmVzc8H3v9i1xNDpxOLN+L+oyeixuDxiciAvJmd0OzxwcmUgY2xhc3M9"brush:java;">dev = alloc_netdev(0, "ingress_tc", tc_setup);然後初始化其關鍵字段
static const struct net_device_ops tc_ops = {
.ndo_init = tc_dev_init,
.ndo_start_xmit= tc_xmit,
};
static void tc_setup(struct net_device *dev)
{
ether_setup(dev);
dev->mtu = (16 * 1024) + 20 + 20 + 12;
dev->hard_header_len = ETH_HLEN; /* 14 */
dev->addr_len = ETH_ALEN; /* 6 */
dev->tx_queue_len = 0;
dev->type = ARPHRD_LOOPBACK; /* 0x0001*/
dev->flags = IFF_LOOPBACK;
dev->priv_flags &= ~IFF_XMIT_DST_RELEASE;
dev->features = NETIF_F_SG | NETIF_F_FRAGLIST
| NETIF_F_TSO
| NETIF_F_NO_CSUM
| NETIF_F_HIGHDMA
| NETIF_F_LLTX
| NETIF_F_NETNS_LOCAL;
dev->ethtool_ops = &tc_ethtool_ops;
dev->netdev_ops = &tc_ops;
dev->destructor = tc_dev_free;
}
static netdev_tx_t tc_xmit(struct sk_buff *skb,
struct net_device *dev)
{
skb_orphan(skb);
// 直接通過第二層!
skb->protocol = eth_type_trans(skb, dev);
skb_reset_network_header(skb);
skb_reset_transport_header(skb);
skb->mac_len = skb->network_header - skb->mac_header;
// 本地接收
ip_local_deliver(skb);
return NETDEV_TX_OK;
}接下來考慮如何將數據包導入到該虛擬網卡。有3種方案可選:
方案1:如果不想設置arp相關的東西,就要修改內核了。在此我引入了一個路由標志,RT_F_INGRESS_TC,凡是有該標志的路由,全部將其導入到構建的虛擬網卡中,為了策略化,我並沒有在代碼中這麼寫,而是改變了RT_F_INGRESS_TC路由的查找順序,優先查找策略路由表,然後再查找local表,這樣就可以用策略路由將數據包導入到虛擬網卡了。
方案2:構建一個Netfilter HOOK,在其target中將希望流控的數據NF_QUEUE到虛擬網卡,即在queue的handler中設置skb->dev為虛擬網卡,調用dev_queue_xmit(skb)即可,而該虛擬網卡則不再符合上面的圖示,新的原理圖比較簡單,只需要在虛擬網卡的hard_xmit中reinject數據包即可。(事實上,後來我才知道,原來IMQ就是這麼實現的,幸虧沒有動手去做無用功)
方案3:這是個快速測試方案,也就是我最原始的想法,即將目標IP地址從local表刪除,然後手動arping,我的測試也是基於這個方案,效果不錯。
Linux內核協議棧處理流程的重構
個人覺得,Linux網絡處理還有一個不對稱的地方,那就是路由後的轉發函數,我們知道Linux的網絡處理在路由之後有個分叉,根據目的地的不同,處理邏輯就此分道揚镳,如果路由結果帶有LOCAL標志,那麼就調用ip_local_deliver,反之調用ip_forward(具體參看ip_route_input_slow中對rth->u.dst.input的賦值)。如此一來,LOCAL數據就徑直發往本地了,這其實也是RFC的建議實現,它簡要的描述了路由算法:先看目標地址是不是本地,如果是就本地接收...然而我認為(雖然總是帶有一些不為人知的偏見),完全沒有必要分道揚镳,通過一個函數發送會更加好一點,比如發往本地的數據包同樣發往一塊網卡處理,只是該塊網卡是一塊LOOPBACK網卡,這樣整個IP接收例程就可以統一描述了。類似的,對於本地數據發送也可以統一由一個虛擬的LOOPBACK網卡發送,而不是直接發送給路由模塊。整體如下圖所示:
雖然這麼對稱處理看似影響了效率,邏輯上好像數據包到了第三層後又回到了第二層,然後第二層的本地LOOKBACK網卡調用ip_local_deliver本地接收,但是落實到代碼上,也就是幾次函數調用而已,完全可以在從ip_forward到dev_queue_xmit這條路上為LOCAL設置直通路線,只要經過這條路即可,這樣一來有4個好處:
1.不再需要INPUT這個HOOK點;
2.不再需要FORWARD這個HOOK點;
Linux的IMQ補丁在實現了自己的虛擬網卡並配置好可用的ingress流控之後,我看了Linux內核的IMQ實現,鑒於之前從未有過流控的需求,一直以來都不是很關注IMQ,本著什麼東西都要先自己試著實現一個或者給出個自己的方案(起碼也要有一個思想實驗方案)然後再與標准實現(所謂的標准一詞並不是那麼經得起推敲,實際上它只是“大家都接受的實現”的另一種說法,並無真正的標准可言)對比的原則,我在上面已經給出了IMQ的思想。
1.命名。IMQ中的Intermediate,我覺得非常好,明確指出使用一個中間層來適配igress的流控;
2.實現一個虛擬網卡設備。即所謂的Intermediate設備;
3.NF_QUEUE的使用。使用Netfilter的NF_QUEUE機制將需要流控的數據包直接導入虛擬設備而不是通過策略路由間接將數據引入虛擬設備。
4.擴充了skb_buff數據結構,引入和IMQ相關的管理字段。個人認為這是它的不足,我並不傾向於修改核心代碼。然而在我自己的虛擬設備實現中,由於ip_local_deliver函數並沒有被核心導出(EXPORT),導致我不得不使用/proc/kallsym來查找它的位置,這麼做確實並不標准,我不得不修改了核心,雖然只是添加了一行代碼: