在傳統的TCP/IP網絡的路由器中,所有的IP數據包的傳輸都是采用FIFO(先進先出),盡最大努力傳輸的處理機制。在早期網絡數據量和關鍵業務數據不多的時候,並沒有體現出非常大的缺點,路由器簡單的把數據報丟棄來處理擁塞。但是隨着計算機網絡的發展, 數據量的急劇增長,以及多媒體,VOIP數據等對延時要求高的應用的增加。路由器簡單丟棄數據包的處理方法已經不再適合當前的網絡。單純的增加網絡帶寬也不能從根本上解決問題。所以網絡的開發者們提出了服務質量的概念。概括的說:就是針對各種不同需求,提供不同服務質量的網絡服務功能。提供QoS能力將是對未來IP網絡的基本要求。
1.Linux內核對QoS的支持
Linux內核網絡協議棧從2.2.x開始,就實現了對服務質量的支持模塊。具體的代碼位於net/sched/目錄。在Linux里面,對這個功能模塊的稱呼是Traffic Control ,簡稱TC。
首先我們了解一下Linux網絡協議棧在沒有TC模塊時發送數據包的大致流程。如圖1。
注:上圖的分層是按照Linux實現來畫,並沒有嚴格遵守OSI分層
從上圖可以看出,沒有TC的情況下,每個數據包的發送都會調用dev_queue_xmit,然后判斷是否需要向AF_PACKET協議支持體傳遞數據包內容,最后直接調用網卡驅動注冊的發送函數把數據包發送出去。發送數據包的機制就是本文開始講到的FIFO機制。一旦出現擁塞,協議棧只是盡自己最大的努力去調用網卡發送函數。所以這種傳統的處理方法存在着很大的弊端。
為了支持QoS,Linux的設計者在發送數據包的代碼中加入了TC模塊。從而可以對數據包進行分類,管理,檢測擁塞和處理擁塞。為了避免和以前的代碼沖突,並且讓用戶可以選擇是否使用TC。內核開發者在上圖中的兩個紅色圓圈之間添加了TC模塊。(實際上在TC模塊中,發送數據包也實現對AF_PACKET協議的支持,本文為了描述方便,把兩個地方的AF_PACKET協議處理分開來了)。
下面從具體的代碼中分析一下對TC模塊的支持。
net/core/dev.c: dev_queue_xmit函數中略了部分代碼:
int dev_queue_xmit(struct sk_buff *skb) { ………………. q = dev->qdisc; if (q->enqueue) { /*如果這個設備啟動了TC,那么把數據包壓入隊列*/ int ret = q->enqueue(skb, q); /*啟動這個設備發送*/ qdisc_run(dev); return; } if (dev->flags&IFF_UP) { …………. if (netdev_nit) dev_queue_xmit_nit(skb,dev); /*對AF_PACKET協議的支持*/ if (dev->hard_start_xmit(skb, dev) == 0) { /*調用網卡驅動發送函數發送數據包*/ return 0; } } ……………… }
從上面的代碼中可以看出,當q->enqueue為假的時候,就不采用TC處理,而是直接發送這個數據包。如果為真,則對這個數據包進行QoS處理。
2.TC的具體設計與實現
第一節描述了linux內核是如何對QoS進行支持的,以及是如何在以前的代碼基礎上添加了tc模塊。本節將對TC的設計和實現進行詳細的描述。
QoS有很多的擁塞處理機制,如FIFO Queueing(先入先出隊列),PQ(優先隊列),CQ(定制隊列),WFQ(加權公平隊列)等等。QoS還要求能夠對每個接口分別采用不同的擁塞處理。為了能夠實現上述功能,Linux采用了基於對象的實現方法。
上圖是一個數據發送隊列管理機制的模型圖。其中的QoS策略可以是各種不同的擁塞處理機制。我們可以把這一種策略看成是一個類,策略類。在實現中,這個類有很多的實例對象,策略對象。使用者可以分別采用不同的對象來管理數據包。策略類有很多的方法。如入隊列(enqueue),出隊列(dequeue),重新入隊列(requeue),初始化(init),撤銷(destroy)等方法。在Linux中,用Qdisc_ops結構體來代表上面描述的策略類。
前面提到,每個設備可以采用不同的策略對象。所以在設備和對象之間需要有一個橋梁,使設備和設備采用的對象相關。在Linux中,起到橋梁作用的是Qdisc結構體。
通過上面的描述,整個TC的架構也就出來了。如下圖:
加上TC之后,發送數據包的流程應該是這樣的:
(1) 上層協議開始發送數據包
(2) 獲得當前設備所采用的策略對象
(3) 調用此對象的enqueue方法把數據包壓入隊列
(4) 調用此對象的dequeue方法從隊列中取出數據包
(5) 調用網卡驅動的發送函數發送
接下來從代碼上來分析TC是如何對每個設備安裝策略對象的。
在網卡注冊的時候,都會調用register_netdevice,給設備安裝一個Qdisc和Qdisc_ops。
int register_netdevice(struct net_device *dev) { …………………. dev_init_scheduler(dev); …………………. } void dev_init_scheduler(struct net_device *dev) { …………. /*安裝設備的qdisc為noop_qdisc*/ dev->qdisc = &noop_qdisc; …………. dev->qdisc_sleeping = &noop_qdisc; dev_watchdog_init(dev); } 此時,網卡設備剛注冊,還沒有UP,采用的是noop_qdisc, struct Qdisc noop_qdisc = { noop_enqueue, noop_dequeue, TCQ_F_BUILTIN, &noop_qdisc_ops, }; noop_qdisc采用的數據包處理方法是noop_qdisc_ops, struct Qdisc_ops noop_qdisc_ops = { NULL, NULL, "noop", 0, noop_enqueue, noop_dequeue, noop_requeue, };
從noop_enqueue,noop_dequeue,noop_requeue函數的定義可以看出,他們並沒有對數據包進行任何的分類或者排隊,而是直接釋放掉skb。所以此時網卡設備還不能發送任何數據包。必須ifconfig up起來之后才能發送數據包。
調用ifconfig up來啟動網卡設備會走到dev_open函數。
int dev_open(struct net_device *dev) { ……………. dev_activate(dev); …………….. } void dev_activate(struct net_device *dev) { …………. if (dev->qdisc_sleeping == &noop_qdisc) { qdisc = qdisc_create_dflt(dev, &pfifo_fast_ops); /*安裝缺省的qdisc*/ } …………… if ((dev->qdisc = dev->qdisc_sleeping) != &noqueue_qdisc) { ……………./*.安裝特定的qdisc*/ } …………….. }
設備啟動之后,此時當前設備缺省的Qdisc->ops是pfifo_fast_ops。如果需要采用不同的ops,那么就需要為設備安裝其他的Qdisc。本質上是替換掉dev->Qdisc指針。見sched/sch_api.c 的dev_graft_qdisc函數。
static struct Qdisc * dev_graft_qdisc(struct net_device *dev, struct Qdisc *qdisc) { …………… oqdisc = dev->qdisc_sleeping; /* 首先刪除掉舊的qdisc */ if (oqdisc && atomic_read(&oqdisc->refcnt) <= 1) qdisc_reset(oqdisc); /*安裝新的qdisc */ if (qdisc == NULL) qdisc = &noop_qdisc; dev->qdisc_sleeping = qdisc; dev->qdisc = &noop_qdisc; /*啟動新安裝的qdisc*/ if (dev->flags & IFF_UP) dev_activate(dev); ………………… }
從dev_graft_qdisc可以看出,如果需要使用新的Qdisc,那么首先需要刪除舊的,然后安裝新的,使dev->qdisc_sleeping 為新的qdisc,然后調用dev_activate函數來啟動新的qdisc。結合dev_activate函數中的語句:
if ((dev->qdisc = dev->qdisc_sleeping) != &noqueue_qdisc)
可以看出,此時的dev->qdisc所指的就是新的qdisc。(注意,上面語句中左邊是一個賦值語句。)
在網卡down掉的時候,通過調用dev_close -> dev_deactivate重新使設備的qdisc為noop_qdisc,停止發送數據包。
Linux中的所有的QoS策略最終都是通過上面這個方法來安裝的。在sch_api.c中,對dev_graft_qdisc函數又封裝了一層函數(register_qdisc),供模塊來安裝新的Qdisc。如RED(早期隨即檢測隊列)模塊,就調用register_qdisc來安裝RED對象(net/sched/sch_red.c->init_module())。
3. Linux缺省策略對象pfifo_fast_ops分析
在Linux中,如果設備啟動之后,沒有配置特定的QoS策略,內核對每個設備采用缺省的策略,pfifo_fast_ops。下面的pfifo_fast_ops進行詳細的分析。
上圖中的信息可以對應於pfifo_fast_ops結構體的每個部分:
static struct Qdisc_ops pfifo_fast_ops = { NULL, NULL, "pfifo_fast", /*ops名稱*/ 3 * sizeof(struct sk_buff_head), /*數據包skb隊列*/ pfifo_fast_enqueue, /*入隊列函數*/ pfifo_fast_dequeue, /*出隊列函數*/ pfifo_fast_requeue, /*重新壓入隊列函數*/ NULL, pfifo_fast_init, /*隊列管理初始化函數*/ pfifo_fast_reset, /*隊列管理重置函數*/ };
在注冊pfifo_fast_ops的時候首先會調用pfifo_fast_init來初始化隊列管理,見qdisc_create_dflt函數。
static int pfifo_fast_init(struct Qdisc *qdisc, struct rtattr *opt) { ……… for (i=0; i<3; i++) skb_queue_head_init(list+i); /*初始化3個優先級隊列*/ ………. }
init函數的作用就是初始化3個隊列。
在注銷一個Qdisc的時候都會調用Qdisc的ops的reset函數。見dev_graft_qdisc函數。
static void pfifo_fast_reset(struct Qdisc* qdisc) { ………….. for (prio=0; prio < 3; prio++) skb_queue_purge(list+prio); /*釋放3個優先級隊列中的所有數據包*/ ………….. }
在數據包發送的時候會調用Qdisc->enqueue函數(在qdisc_create_dflt函數中已經將Qdisc_ops的enqueue,dequeue,requeue函數分別賦值於Qdisc分別對應的函數指針)。
int dev_queue_xmit(struct sk_buff *skb) { ………………. q = dev->qdisc; if (q->enqueue) { /* 對應於pfifo_fast_enqueue 函數*/ int ret = q->enqueue(skb, q); /*啟動這個設備的發送,這里涉及到兩個函數pfifo_fast_dequeue ,pfifo_fast_requeue 稍后介紹*/ qdisc_run(dev); return; } …………… }
入隊列函數pfifo_fast_enqueue:
static int pfifo_fast_enqueue(struct sk_buff *skb, struct Qdisc* qdisc) { ………….. list = ((struct sk_buff_head*)qdisc->data) + prio2band[skb->priority&TC_PRIO_MAX]; /*首先確定這個數據包的優先級,決定放入的隊列*/ if (list->qlen <= skb->dev->tx_queue_len) { __skb_queue_tail(list, skb); /*將數據包放入隊列的尾部*/ qdisc->q.qlen++; return 0; } …………….. }
在數據包放入隊列之后,調用qdisc_run來發送數據包。
static inline void qdisc_run(struct net_device *dev) { while (!netif_queue_stopped(dev) && qdisc_restart(dev)<0) /* NOTHING */; }
在qdisc_restart函數中,首先從隊列中取出一個數據包(調用函數pfifo_fast_dequeue)。然后調用網卡驅動的發送函數(dev->hard_start_xmit)發送數據包,如果發送失敗,則需要將這個數據包重新壓入隊列(pfifo_fast_requeue),然后啟動協議棧的發送軟中斷進行再次的發送。
static struct sk_buff * pfifo_fast_dequeue(struct Qdisc* qdisc) { ………….. for (prio = 0; prio < 3; prio++, list++) { skb = __skb_dequeue(list); if (skb) { qdisc->q.qlen--; return skb; } } ………………. }
從dequeue函數中可以看出,pfifo的策略是:從高優先級隊列中取出數據包,只有高優先級的隊列為空,才會對下一優先級的隊列進行處理。
requeue函數重新將數據包壓入相應優先級隊列的頭部。
static int pfifo_fast_requeue(struct sk_buff *skb, struct Qdisc* qdisc) { struct sk_buff_head *list; list = ((struct sk_buff_head*)qdisc->data) + prio2band[skb->priority&TC_PRIO_MAX]; /*確定相應優先級的隊列*/ __skb_queue_head(list, skb);/*將數據包壓入隊列的頭部*/ qdisc->q.qlen++; return 0; }
總結:
QoS是當前一個非常熱門的話題,幾乎所有高端的網絡設備都支持QoS功能,並且這個功能也是當前網絡設備之間競爭的一個關鍵技術。Linux為了在在高端服務器能夠占有一席之地,從2.2.x內核開始就支持了QoS。本文在linux 2.4.0的代碼基礎上對Linux如何支持QoS進行了分析。並且分析了Linux內核的缺省隊列處理方法PFIFO的實現。
參考資料
- (1) linux 2.4.0 內核源代碼。