《TCP/IP詳解:實現》: mbuf 詳解二

五、mbuf相關宏與函數

如下:

1. Mbstat是一個全局變量

下面是全局結構mbstat中維護的各種統計

struct mbstat {
	u_long	m_mbufs;	/* mbufs obtained from page pool */ 從頁池(未用)中獲得mbuf數
	u_long	m_clusters;	/* clusters obtained from page pool */從頁池中獲得簇
	u_long	m_spare;	/* spare field */剩餘空間(未用)
	u_long	m_clfree;	/* free clusters */自由簇
	u_long	m_drops;	/* times failed to find space */尋找空間(未用)失敗的次數
	u_long	m_wait;		/* times waited for space */等待空間(未用)的次數
	u_long	m_drain;	/* times drained protocols for space */調用協議的drain函數來回收空
                                                               /間的次數
	u_short	m_mtypes[256];	/* type specific mbuf allocations */當前mbuf的分配數:
                                                               /MT_XXX索引
};

2. 獲取一個mbuf

MGET宏例如調用MGET來分配系統sendto系統調用的目標地址的mbuf如下所示:

MGET(m, M_WAIT, MT_SONAME);
If ( m == NULL)
	Return (ENOBUFS);

MGET宏的原型如下:MBUFLOCK來保護函數和宏不被中斷 

#define	MGET(m, how, type) { \
        //mbtypes[type]把mbuf的type轉換成MALLOC需要的type,如M_MBUF,M_SOCKET等
	MALLOC((m), struct mbuf *, MSIZE, mbtypes[type], (how)); \
	if (m) { \
		(m)->m_type = (type); \
                //MBUFLOCK改變處理器優先級,防止被網絡處理器中斷,共享資源的保護
		MBUFLOCK(mbstat.m_mtypes[type]++;) \
		(m)->m_next = (struct mbuf *)NULL; \
		(m)->m_nextpkt = (struct mbuf *)NULL; \
                //#define m_dat       M_dat.M_databuf  爲pkthdr和m_ext預留了空間   
		(m)->m_data = (m)->m_dat; \
		(m)->m_flags = 0; \
	} else \
                //嘗試重新分配,一個主要的問題,分配的內存從哪裏來?詳見後面
		(m) = m_retry((how), (type)); \
}

MGET一開始調用內核宏MALLOC,它是通用內核存儲器分配器進行的數組mbtypes把mbuf的MT_xxx值轉換成相應的M_xxx值。若分配成功,m_type被設置爲參數中的值。

MBUFLOCK用於跟蹤統計每種mbuf類型的內核結構加1(mbstat)當執行這句時,宏MBUFLOCK把它作爲參數來改變處理器優先級,然後把優先級恢復爲原值。這防止在執行語句mbstat.m_mtypes[type]++時被網絡設備中斷,因爲mbuf可能在內核中的各層中被分配。考慮這樣一個系統,它用三步來實現一個c中的++運算:(1)把當前值裝入到一個寄存器;(2)寄存器加1;(3)把寄存器值存入到存儲器。假設計數器值爲77並且MGET在插口層執行。假設執行了步驟1和2(寄存器值爲78),並且一個設備中斷髮生。若設備驅動也執行MGET來獲得同種類型的mbuf,在存儲器中取值(77),加1(78),並存回在存儲器。當被中斷執行的MGET的步驟3繼續執行時,它將寄存器的值(78)存入存儲器。但是計數器應爲79,而不是78,這樣計數器就被破壞了。

m_next和m_nextptk被設置爲空指針

數據指針m_data被設置爲指向108字節的mbuf緩存的起始地址,標誌m_flags設置爲0

若內核的存儲器分配調用失敗,調用m_retry。第一個參數是M_WAIT或者M_DONTWAIT。

3. 分配一個mbuf

struct mbuf *
m_get(nowait, type)
	int nowait, type;
{
	register struct mbuf *m;
 
	MGET(m, nowait, type);
	return (m);
}

這個調用表明參數nowait的值爲M_WAIT或M_DONTWAIT,它取決於在存儲器不可用時是否需要等待。例如,當插口層請求分配一個mbuf來存儲sendto系統調用的目標地址時,它指定M_WAIT,因爲在此阻塞是沒有問題的。但是當以太網設備驅動程序請求分配一個mbuf來存儲一個接收的幀時,它指定M_DONTWAIT,因爲它是作爲一個設備中斷處理來執行的,不能進入睡眠狀態來等待一個mbuf。在這種情況下,若存儲器不可用,設備驅動程序丟棄這個幀比較好。

4. m_retry函數

/*
 * When MGET failes, ask protocols to free space when short of memory,
 * then re-attempt to allocate an mbuf.
 */
struct mbuf *
m_retry(i, t)
	int i, t;
{
	register struct mbuf *m;
        // 調用協議的註冊函數釋放內存
	m_reclaim();

        // 把m_retrydefine成NULL這樣就直接返回NULL了,但這裏怎麼保證這個MGET中m_retry返回的是 
        // NULL,而上一個返回的是這個函數 ? #define在預編譯期間就做替換了。
        // 這個的關鍵就是MGET是一個宏,而不是函數。

#define m_retry(i, t)	(struct mbuf *)0
	MGET(m, i, t);
#undef m_retry
	return (m);
}

5. m_reclaim

// 這個函數循環調用協議的drain函數分配內存
m_reclaim()
{                                                                                                
    register struct domain *dp;
    register struct protosw *pr;
    // 提升處理器的優先級不被網絡處理中斷
    int s = splimp();

    for (dp = domains; dp; dp = dp->dom_next)
        for (pr = dp->dom_protosw; pr < dp->dom_protoswNPROTOSW; pr++)
            if (pr->pr_drain)
                (*pr->pr_drain)();
    // 恢復處理器的優先級
    splx(s);
    mbstat.m_drain++;
}

6. MGETHDR宏

// 分配一個分組頭部的mbuf,對m_data和m_flags進行初始化
#define	MGETHDR(m, how, type) { \
	MALLOC((m), struct mbuf *, MSIZE, mbtypes[type], (how)); \
	if (m) { \
		(m)->m_type = (type); \
		MBUFLOCK(mbstat.m_mtypes[type]++;) \
		(m)->m_next = (struct mbuf *)NULL; \
		(m)->m_nextpkt = (struct mbuf *)NULL; \
		(m)->m_data = (m)->m_pktdat; \
		(m)->m_flags = M_PKTHDR; \
	} else \
		(m) = m_retryhdr((how), (type)); \
}

7. m_devget 函數

/*
 * Routine to copy from device local memory into mbufs.
 */
struct mbuf *
m_devget(buf, totlen, off0, ifp, copy)
	char *buf;
	int totlen, off0;
	struct ifnet *ifp;
	void (*copy)();
{
     ...
}

當接口接收到一個以太網幀時,設置驅動程序調用m_devget函數來創建一個mbuf鏈表,並把設備中的幀複製到這個鏈表中。根據所接收的幀的長度(不包括以太網首部),可能產生四種不同的mbuf鏈表。

                                                               圖9 m_devget創建的前兩種mbuf

 圖9左邊的mbuf用於數據長度在0~84字節之間的情況。在圖中我們假定有52字節的數據:20字節的IP首部和32字節的TCP首部(標準的20字節TCP首部+12字節的TCP選項),但不包括TCP數據。因爲m_devget返回的mbuf數據從IP首部開始,所以該mbuf的m_len的實際最小值爲28:20字節IP首部+8字節UDP首部+0字節UDP數據(此處選擇UDP是因爲UDP首部比TCP首部更小)。對於輸入幀,mbuf數據部分前16個字節保留未用;而對於輸出幀,前16字節分配了14字節的以太網首部。icmp_reflect和tcp_respond這兩個函數通過把接收到的mbuf作爲輸出來產生一個應答。這兩種情況接收到的數據報應該少於84字節,因此很容易在前面保留16字節的空間。分配16字節而不是14字節是爲了在mbuf中用長字節對準方式存儲IP首部。

圖9右邊的mbuf用於數據長度85~100字節之間,此時仍然存放在一個分組首部mbuf中,但沒有16字節的保留空間,數據直接從數組m_pktdat的開始位置進行存儲。


                                                             圖10 m_devget創建的第三種mbuf

圖10所示的是m_devget創建的第三種mbuf。當數據在101~207字節之間,需要兩個mbuf。前100字節存儲在第一個mbuf中(包含分組首部),剩下的數據存放在第二個mbuf中。同樣地第一個mubf中沒有保留的16字節空間。​

                                                                  圖11  m_devget創建的第四種mbuf

圖11所示的是m_devget創建的第四種mbuf。如果數據超過或者等於208字節(208字節可以使用在第三種mbuf,個人覺得),要用一個或者多個簇。圖11中的例子假設一個1500字節的以太網,如果使用1024字節的簇,則需要兩個標誌爲M_EXT的mbuf。

8. mtod和dtom宏

宏mtod和dtom用於簡化mbuf結構表達式。

#define mtod(m, t) ((t) ((m)->m_data))

mtod(“mbuf到數據”)返回一個指向mbuf數據的指針,並把指針聲名爲指定類型。例如代碼:

struct mbuf *m;

struct ip *ip;

ip = mtod(m, struct ip *);

ip->ip_v = IPVERSION;

將ip指向mbuf中存儲的數據(m_data),然後通過指針ip引用IP首部。當一個C結構(通常是一個協議首部)存儲在mbuf中時,可能通過該宏獲取該結構的指針;同樣當數據存在mbuf或者簇中時,也可能使用該宏獲取數據指針。

#define dtom(x) ((struct mbuf *) ((int)(x) &~(MSIZE-1)))

 dtom(“數據到mbuf”)取得一個存放在mbuf中任意位置的數據指針,並返回這個mubf結構本身的指針。例如,若我們知道ip指向一個mbuf的數據區,下列語句序列中,將這個mubf的起始地址賦值給m。

struct mbuf *m;

struct ip *ip;

m = dtom(ip);

我們知道MSIZE(128)是2的冪,並且內核存儲器分配器總是爲mbuf分配連續的MSIZE字節存儲塊,dtom僅僅是通過清除參數中指針的低位來確定mbuf的起始位置。

宏dtom有一個問題:當它的參數指向一個簇或者簇內時,因爲沒有指針從簇內指回mbuf結構,dtom不能被使用。此時,另外一個函數m_pullup就派上用場了。

9.  m_pullup函數

①. m_pullup函數和連續的協議首部

m_pullup函數有兩個目的。第一個是當一個協議(IP、ICMP、IGMP、UDP或TCP)發現在第一個mbuf的數據長度(m_len)小於協議首部的最小長度(例如:IP是20,UDP是8,TCP是20)時,調用m_pullup是基於假設協議首部的剩餘部分是存儲在鏈表的下一個mbuf中。m_pullup重新安排mbuf鏈表,使得前N個字節的數據被連續的存入在鏈表的第一個mbuf中。N是這個函數的一個參數,它必須小於或者等於100(因爲第一個mbuf最多隻有100字節的空間)。如果前N字節連續存入在第一個mbuf中,則可以使用mtod和dtom。例如,在IP輸入例程會遇到如下代碼:

	if (m->m_len < sizeof (struct ip) &&
	    (m = m_pullup(m, sizeof (struct ip))) == 0) {
		ipstat.ips_toosmall++;
		goto next;
	}
	ip = mtod(m, struct ip *);

如果第一個mbuf中的數據少於20字節(標準IP首部大小),m_pullup被調用。函數m_pullup有兩個原因會失敗:a. 如果它需要其它mbuf並且調用MGET失敗;b. 如果整個mbuf鏈表中的數據總數少於要求的連續字節數(即參數N值,本例中是20)。上述代碼在實際情況中m_pullup很少被調用,因爲在第一個mbuf中,從IP首部開始至少有100字節的連續字節,而IP首部最大60字節,後面還可以跟着40字節的TCP首部(ICMP、UDP等其它協議首部不到40字節)。

②. m_pullup函數和IP的分片與重組

m_pullup函數的第二個用途是IP和TCP的重組。假定IP接收到一個長度爲296的分組,它是一個大的IP數據報的一個分片。這個從設備驅動程序傳到IP輸入的mbuf看起來像圖11所示的mbuf:296字節數據存放在一個簇中。我們將這顯示在圖12中。​

                                                                          圖12 一個長度爲296的IP分片

IP分片算法將各分片都存放在一個雙向鏈表中,使用IP首部的源與目標IP地址來存放向前和向後的鏈表指針(當然,這兩個IP地址需要保存在這個鏈表表頭中,因爲還需要將它們放回到重組的IP數據報中,原著10章詳細討論這個問題)。

但是如果IP首部在一個簇中,如圖12所示,這些鏈表指針會存放在這個簇中,並且當以後遍歷鏈表時,指向IP首部的指針(即指向這個簇的起始的指針)不能被轉換成指向mubf的指針。這是我們本文前面提到的問題:如果m_data指向一個簇時不能使用宏dtom,因爲沒有從簇指回mbuf的指針。IP分片爲解決這個問題,當收到一個分片時,若分片存放在一個簇中,IP分片例程總是調用m_pullup,將20字節的IP首部放到它的mbuf中。代碼如下:

	if (ip->ip_off &~ IP_DF) {
		if (m->m_flags & M_EXT) {		/* XXX */
			if ((m = m_pullup(m, sizeof (struct ip))) == 0) {
				ipstat.ips_toosmall++;
				goto next;
			}
			ip = mtod(m, struct ip *);
		}

 

                                                                        圖13 m_pullup後的長度爲296的IP分組

圖13中,IP分片算法在左邊的mbuf中保存了一個指向IP首部的指針,並且可以用dtom將這個指針轉換成一個指向mbuf本身的指針。

③. TCP重組避免調用m_pullup

重組TCP報文段使用一個不同的技術,而不是調用m_pullup函數。這是因爲調用m_pullup開銷較大:分配存儲器並且將數據從一個mbuf複製到一個mbuf中。TCP試圖儘可能地避免數據的複製。

TCP數據大約一半是批量數據(每個報文段有512或者更多字節的數據);另外一半是交互式數據(其中90%報文段不到10字節的數據)。因此,當TCP從IP接收報文段時,通常是如圖10左邊所示的格式(小量的交互數據,存儲在mbuf本身)或者圖11所示的格式(批量數據,存儲在一個簇中)。當TCP報文段失序到達時,它們被 TCP存儲到一個雙向鏈表中。如IP分片一樣,在IP首部的字段用於存放鏈表的指針,既然這些字段在TCP接收了IP數據報後不再需要,這完全可行。但當IP首部存放在一個簇中,要將一個鏈表指針轉換成一個相應的mbuf指針時,會引起同樣的問題(圖12)

爲了解決這個問題,TCP把mbuf指針存放在TCP首部中一些未用的字段中,提供一個從簇指回mbuf的指針,來避免對每個失序的報文段調用m_pullup。如果IP首部包含在mbuf中數據區(圖13),則這個回指指針是無用的,因爲宏dtom可能通過這個鏈表指針可以指到mbuf的開始位置。

關於m_pullup使用的總結

大多數設置驅動程序不把一個IP數據報的第一部分(首部部分)分割到幾個mbuf中。假設協議首部都能緊挨着存放,則在每個協議(IP、ICMP、IGMP、UDP和TCP)中調用m_pullup的可能性很小。如果調用m_pullup,通常是因爲IP數據報太小,並且如果調用m_pullup返回一個差錯,這時數據報被丟棄。

對於每個接收到的IP分片,當IP數據報被存放在一個簇中時,m_pullup被調用。這意味着幾乎對於每個接收的分片都要調用m_pullup,因爲大多數分片的長度大於208字節。

只要TCP報文段不被IP分片,接收一個TCP報文段,不論是否失序都不需要調用m_pullup。這是避免IP對TCP分片的一個原因。

六、分析舉例: Net/3中mbuf的常用打開方式

下面將介紹幾種基於mbuf的常用數據結構。

一個mbuf鏈:一個通過m_next指針鏈接的mbuf鏈表。

只有一個頭指針的mbuf鏈的鏈表(隊列)。mbuf鏈通過每個鏈的第一個mubf中的m_nextpkt指針鏈接起來。如圖16所示,這種數據結構的例子是一個插口發送緩存和接收緩存。

                                                                     圖16 只有頭指針的mbuf鏈的鏈表

頂部的兩個mbuf形成這個隊列中的第一個記錄,底下三個mbuf形成這個隊列的第二個記錄。對於一個基於記錄的協議,例如UDP,我們在每個隊列中能遇到多個記錄。但對於像TCP這樣的協議,它沒有記錄的邊界,每個隊列我們只能發現一個記錄(一個mbuf鏈可能包含多個mbuf)。

把一個mbuf追加到隊列的第一個記錄中需要遍歷所有第一個記錄的mbuf,直到遇到m_next爲空的mbuf。而追加一個包含新記錄的mbuf鏈到這個隊列中,要查找所有記錄的第一個mbuf,直到遇到m_nextpkt爲空的記錄。

一個有頭指針和尾指針的mbuf鏈的鏈表。圖17顯示的是這種類型的鏈表。我們在接口隊列中會遇到它。

                                                                    圖17 有頭指針和尾指針的鏈表

雙向循環鏈表,如圖18所示,我們在IP分片與重裝、協議控制塊及TCP失序報文段隊列中會遇到這種數據結構。

                                                                                圖18 雙向循環列表

m_copy和簇引用計數

使用簇的一個明顯的就是在要求包含大量數據時能減少mbuf的數目。例如,如果不使用簇,要有10個mbuf才能包含1024字節的數據(100+8*108+60),分配並鏈接10個mbuf比分配一個1024字節簇的mbuf開銷要大。但是簇一個潛在缺點是浪費空間。在我們的例子中使用一個簇(2048+128)要2176字節,而1280字節用不完一個簇的空間。

簇的另外一個好處是在多個mbuf間可以共享一個簇。假如應用程序執行一個write,把4096字節寫到TCP插口中,假設插口發送緩存原來是空的,接口窗口至少有4096,則會發生以下操作。插口層將前2048字節的數據放到一個簇中,並且調用協議的發送例程。TCP發送例程把這個mbuf追加到它的發送緩存後,如圖19所示,然後調用tcp_output。結構socket中包含sockbuf結構,這個結構存儲着發送緩存mbuf鏈的鏈表表頭:so_snd.sb_mb。​

                                                              圖19 包含2048字節數據的TCP插口發送緩存

假設這個連接(以太網)的一個TCP最大報文段(MSS)爲1460,tcp_output創建一個報文段來發送包含前1460字節的數據。它還創建一個包含IP和TCP首部的mbuf,爲鏈路層首部預留16字節空間,並將這個mbuf鏈傳給IP輸出。接口輸出隊列尾部的mbuf鏈顯示如圖20所示。對於TCP協議,因爲它是一個可靠協議,所以它必須維護一個發送數據的副本(保存在它的發送緩存中),直到數據被對方確認;對於UDP協議,不需要保存副本,所以不會將這個mbuf保存在它的發送緩存中。

                                                              圖20 TCP插口發送緩存和接口輸出隊列中的報文段

在這個例子中,tcp_output調用m_copy函數,請求複製1460字節的數據,從發送緩存起始位置開始。但由於數據被存放在一個簇中,m_copy創建一個mbuf如圖20的右下側,並對其進行初始化,將它指向那個已存在的簇的正確位置,例子中是簇的起始位置。這個mbuf的數據長度是1460,雖然還有另外588字節存儲在簇中。圖20中下面的mbuf鏈的長度是1514,包括以太網首部、IP首部和TCP首部。

注意:圖20右下側的mbuf包含一個分組首部,因爲它是從圖20上面的mbuf複製而來,不過由於這個mbuf不是mbuf鏈中的第一個mbuf,所以分組首部中的m_pkthdr.len和m_pkthdr.rcvif字段可以忽略。

這種共享簇的方式避免了內核將數據從一個mbuf拷貝到另一個mbuf中,節約開銷。它是通過爲每個簇提供一個引用計數來實現的。

繼續我們的例子,由於在發送緩存的簇中剩餘的588字節不能組成一個報文段,tcp_out在把1460字節的報文段傳給IP後返回(原著26章詳細說明這種條件下tcp_output發送數據的細節,這裏先不深究)。插口層繼續處理來自應用進程的數據:剩下的2048字節被存放在一個新的帶有一個簇的mbuf中,TCP發送例程再次被調用,並且新的mbuf被追加到插口發送緩存中。因爲能發送一個完整的報文段,tcp_output建立另外一個帶有協議首部和1460字節數據的mbuf鏈表。m_copy的參數指定了1460字節的數據在發送緩存中起始位移和長度(1460字節)。如圖21所示,並假設這個mbuf鏈在接口輸出隊列中(這個鏈中的第一個mbuf的長度反映了以太網首部、IP首部及TCP首部)。

這次1460字節的數據來自兩個簇:前588字節來自發送緩存的第一個簇,後面的872字節來自發送緩存的第二個簇。它用兩個mbuf來存放1460字節,但m_copy還是不復制這1460字節的數據——通過引用已存在的簇。​

                                                          圖21 用於發送1460字節TCP報文段的mbuf鏈

m_copy函數這個名字隱含着對數據進行物理複製,但是如果數據在一個簇中,卻只是引用這個簇而不是複製。

以上大致介紹了數據從進程到接口輸出隊列的一個流程,認真理解、捋順數據處理流程對後文的理解有很大的幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章