linux TC設計與實現


在傳統的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 內核源代碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM