1. 程式人生 > >《TCP/IP詳解卷2:實現》筆記--插口I/O

《TCP/IP詳解卷2:實現》筆記--插口I/O

本文介紹有關網路連線上讀寫資料的系統呼叫,分三部分:

第一部分介紹四個用來發送資料的系統呼叫:write,writev,sendto和sendmsg。第二部分介紹四個用來接收資料的系統

呼叫:read、readv、recvfrom和recvmsg。第三部分介紹select系統呼叫,select呼叫的作用是監視通用描述符和特殊描述

符的狀態。

插口層的核心是兩個函式:sosend和soreceive。這兩個函式負責處理所有插口層和協議層之間的I/O操作。

1.插口快取

每一個插口都有一個傳送快取和一個接受快取。快取的型別為sockbuf。下圖列出了sockbuf結構的定義。


其中sb_hiwat和sb_lowat用來調整插口的流控演算法。本文的以後部分會進行說明。下圖顯示了Internet協議的預設設定。


因為每一個進入的UDP報文的源地址同資料一起排隊,所以UDP協議的sb_hiwat的預設值設定為能容納40個1K位元組長

的資料包和相應sockaddr_in結構(每個16位元組)。

sb_sel是一個用來實現select系統呼叫的selinfo結構。

下圖列出了sb_flags的所有可能的值。


sb_timeo用來限制一個程序在讀寫呼叫中被阻塞的時間,單位為時鐘滴答。預設為0,表示程序無限期的等待。SO_SNDTIMEO

和SO_RCVTIMEO插口選項可以改變或讀取sb_timeo的值。

2.write、writev、sendto和sendmsg系統呼叫

所有這些寫系統呼叫都要直接或間接地呼叫sosend。sosend的功能是將程序來的資料複製到核心,並將資料傳遞給與插口

相關的協議。下圖給出了sosend的工作流程。



write和writev系統呼叫適用於任何描述符,而其他的系統呼叫值適用於插口描述符;writev和sendmsg系統呼叫可以接受

從多個快取中來的資料。從多個快取中寫資料稱為收集,同它相對應的讀操作成為分散。執行收集操作時,核心按序接收

型別為iovec的陣列中指定的快取中的資料。陣列最多有UIO_MAXIOV個單元。下圖顯示了型別iovec的結構。


iov_base指向長度為iov_len個位元組的快取的開始。

如果沒有這種介面,一個程序將不得不將多個快取複製到一個大的快取中,或呼叫多個寫系統呼叫來發送從多個快取來的

資料。下圖說明了iovec結構在writev系統呼叫中的應用,圖中,iovp指向陣列的第一個元素,iocnt等於陣列的大小。


資料報協議要求每一個寫呼叫必須指定一個目的地址。因為write、writev和send呼叫介面不支援對目的地址的指定,因此

這些呼叫只能在呼叫connect將目的地址同一個無連線的插口聯絡起來後才被呼叫。呼叫sendto或sendmsg時必須提供目的

地址,或在呼叫他們之前呼叫connect來指定目的地址。

下圖顯示了sendmsg系統呼叫接收一個可選的控制標誌。


只有sendmsg系統呼叫支援控制資訊,控制資訊和另外幾個引數是通過結構msghdr一次傳遞給sendmsg,而不是分別傳遞。


控制資訊的型別為cmsghdr結構。


下圖說明了呼叫sendmsg時msghdr的結構。


3.sendmsg系統呼叫

sendmsg和sendit函式準備sosend系統呼叫所需的資料結構,然後sosend呼叫將報文傳送給相應的協議。對SOCK_DGRAM

協議而言,報文就是資料報,對SOCK_STREAM協議而言,報文是一串位元組流。對於SOCK_SEQPACKET協議而言,報文

可能是一個完整的記錄或一個大的記錄的一部分。sendmsg系統呼叫的宣告如下:


sendmsg函式的大概處理流程如下:

1.將iovec陣列從使用者空間複製到棧中的陣列或一個更大的動態分配的陣列中。

2.呼叫sendit函式。

4.sendit函式

sendit初始化一個uio結構,將控制和地址資訊從程序空間複製到核心。首先必須先介紹下uio結構。uio結構中包含了iovec

結構陣列和其他一些資訊。

sendit函式的大概處理流程如下:

1.初始化uio結構。將程序指定的輸出快取中的資料收集到核心快取中。

2.從程序複製地址和控制資訊。

3.傳送資料和和清除快取。將插口,目的地址,uio結構,控制資訊和標誌全部傳給函式sosend。在返回之前,sendit釋放

包含目的地址的快取。sosend負責釋放控制資訊快取。

5.sosned函式

sosend是插口層中最複雜的函式之一。sosend的功能是:根據插口指明的協議支援的語義和快取的限制,將資料和控制

資訊傳遞給插口指明的協議的pr_usrreq函式。sosend從不將資料放在傳送快取中,儲存和移走資料應由協議來完成。

可靠的協議快取

對於提供可靠的資料傳送協議,傳送快取儲存了還沒有傳送的資料和已經發送但還沒有被確認的資料。sb_cc等於傳送快取

的資料的位元組數,且0<=sb_cc<=sb_hiwat。如果有帶外資料傳送,則sb_cc有可能暫時超過sb_hiwat。

sosend應該確保在通過pr_usrreq函式將資料傳遞給協議層之前有足夠的傳送快取。協議層將資料放到傳送快取中,sosend

通過下面兩種方式之一將資料傳送給協議層:

1.如果設定了PR_ATOMIC(protosw結構中的pr_flags),sosend就必須保護程序和協議層之間的邊界。sosend等待得到足夠

的快取來儲存整個報文,當獲取到足夠的快取後,構造儲存整個報文的mbuf鏈,並用pr_usrreq函式一次性傳送給協議層。

RDP和SPP就是這種型別的協議。

2.如果沒有設定PR_ATOMIC,sosend每次傳送一個存有報文的mbuf,可能傳送部分mbuf給協議層以防止超過上限,在

SOCK_STREAM類協議如TCP中和SOCK_SEQPACKET類協議如TP4中被採用。

當一個報文因為太大而沒有足夠的快取時,協議允許報文被分成多段,但sosend仍然不將資料傳送給協議層直到快取中的

閒置空間大小大於sb_lowat。

不可靠的協議快取

對於提供不可靠的資料傳輸的協議(UDP)而言,傳送快取不需要儲存任何資料,也不等待任何確認。每一個報文一旦被

排隊等待發送到相應的網路裝置,插口層立即將它送到協議。這種情況下,sb_cc總是等於0,sb_hiwat指定每一次寫的

最大長度,間接指明資料報的最大長度。

sosend函式的詳細情況將分四個部分來描述。

  • 初始化
  • 差錯和資源檢查
  • 資料傳送
  • 協議處理
大概的處理流程如下: 1.計算傳送資料位元組數。 2.關閉路由。如果僅僅要求對這麼報文不通過路由表進行路由選擇,則設定不要進行路由。 3.差錯檢查。在一下幾種情況下返回錯誤:
  • 插口輸出被禁止。
  • 插口正處於差錯狀態。
  • 協議請求連線且連線還沒有建立或連線請求還沒有啟動
  • 在無連線協議中沒有指定目的地址。
4.計算可用空間。計算髮送快取中剩餘的閒置空間位元組數,目的是防止太多的小報文消耗太多的mbuf快取。通過放寬快取 限制到1024個位元組來給予帶外資料更高的優先順序。 5.強制實施報文大小限制。如果atomic被置位,並且報文大於高水位標記(sb_hiwat),則返回錯誤。報文因為太大而不能 被協議接收,即使快取是空的。如果控制資訊的長度大於高水位標記,同樣返回錯誤。 6.是否等待更多的空間。如果傳送快取中的空間不夠,資料來源於程序,並且下面條件之一成立,則sosend必須等待更多空間:
  • 報文必須一次傳送給協議。
  • 報文可以分段傳送,但閒置空間大小低於低水位標記。
  • 報文可以分段傳送,但可用空間存放不小控制資訊。
當資料通過top(指向mbuf資料鏈)傳送給sosend時,資料已經在mbuf快取中。因此,sosend忽略快取高、低水位標記 限制,因為不需要附加的快取來儲存資料。 如果sosend必須等待快取且插口是非阻塞的,則返回錯誤。否則,快取鎖被釋放,sosend呼叫sbwait等待,直到快取狀 態發生變化,當sbwait返回後,sosend重新使能協議處理,並且重新獲取快取鎖,檢查差錯和快取空間。 7.分配分組首部或標準mbuf。當atomic被置位時,在第一次分配一個分組首部,隨後分配標準的mbuf。如果atomic沒有被 置位,則總是分配一個分組首部。 8.儘可能用簇。如果不用簇,儲存在mbuf中的位元組數受到下面三個量中最小的一個量的限制:
  • mbuf中的可用空間。
  • 報文的位元組數。
  • 快取的空間。
9.從程序複製資料。從程序複製位元組到mbuf。傳送完畢後,更新mbuf的長度,前面的mbuf連線到新的mbuf,更新mbuf鏈 的長度。 10.是否寫另一個快取。當atomic沒有被設定時,一次只傳送一個mbuf給協議。如果設定了atomic,只有當足夠的快取空間 來存放整個報文時才進行快取的寫入。 11.傳輸資料和控制mbuf給插口指定的協議。如果程序傳送的是帶外資料,則傳送PRU_SENDOOB請求;否則,它傳送 PRU_SEND請求,同時將地址和控制mbuf傳給協議。

6.read、readv、recvfrom和recvmsg系統呼叫

我們將這些系統呼叫成為讀系統呼叫,從網路連線上接收資料,同recvmsg相比,前三個系統呼叫比較簡單。下圖給出了

這四個系統呼叫和一個庫函式recv的特點。


只有read和readv系統呼叫適用於各類描述符,其他的呼叫只適用於插口描述符。同寫呼叫一樣,通過iovec結構陣列來

指定多個快取。對資料報協議,recvfrom和recvmsg返回每一個收到的資料報的源地址。對於面向連線的協議,getpeername

返回連線對方的地址。

下圖說明讀系統呼叫的流程。


7.recvmsg系統呼叫

recvmsg函式是最通用的讀系統呼叫。函式的大概處理流程如下:

1.複製iov陣列。同sendmsg一樣,recvmsg將msghdr結構複製到核心,如果自動分配的陣列aiov太小,則分配一個更大的

iovec陣列,並且將陣列單元從程序複製到iov指向的核心陣列。

2.recvit和釋放快取。recvit收完資料後,將更新過的快取長度和標誌的msghdr結構再複製到程序。如果分配了一個更大的iovec

結構,則返回之前釋放它。

8.recvit函式

recvit函式被recv、recvfrom和recvmsg呼叫,基於recv xxx呼叫提供的msghdr結構,recvit函式為soreceive的處理準備了一個

uio結構。

recvit函式的大概處理流程如下:

1.初始化uio結構,該結構描述從核心到程序之間的一次資料傳送。

2.呼叫soreceive函式。

3.將地址和控制資訊複製到程序。如果程序傳入了一個存放地址或控制資訊或兩者都有的快取,則recvit將結果寫入該快取,

並且根據soreceive返回的結果調整它們的長度。如果快取太小,則地址資訊可能被截掉。

4.釋放儲存源地址和控制資訊的mbuf快取。

9.soreceive函式

soreceive函式將資料從插口的接收快取傳送到程序指定的快取。某些協議還提供傳送者的地址,地址可以同可能的附加控制

資訊一起返回。

recvmsg是唯一返回標誌欄位給程序的讀系統呼叫。在其他的系統呼叫中,控制返回給程序之前,這些資訊被核心丟棄,下圖

列出了msghdr中recvmsg能設定的標誌。

9.1.帶外資料

帶外資料(OOB)在不同的協議中有不同的含義。一般來說,協議利用已經建立的通訊連線來發送OOB資料。OOB資料可能

與傳送的正常資料同序。插口層支援兩種與協議無關的機制來實現對OOB資料的處理:標記和同步。本文討論插口層實現的

抽象的OOB機制。UDP不支援OOB資料。TCP的緊急資料機制與插口層的OOB資料之間有關係。

傳送程序通過sendxxx呼叫設定MSG_OOB標誌將資料標記為OOB資料。sosend將這個訊息傳遞給插口協議,插口層收到這

個訊息後,對資料進行特殊處理,如加快傳送資料或使用另一種排隊策略。

當一個協議收到OOB資料後,並不將它放進插口的接收快取而是放到其他地方。程序通過設定recvxxx呼叫中的MSG_OOB標誌

來接收到達的OOB資料。另一種方法是,通過設定SO_OOBINLINE插口選項,接收程序可以要求協議將OOB資料放到正常的

資料之內。當SO_OOBINLINE被設定時,協議將收到OOB資料放到正常資料的接收快取,在這種情況下,MSG_OOB不用來

接收OOB資料,讀呼叫要麼返回所有的正常資料,要麼返回所有的OOB資料。兩種型別的資料從來不會再一個輸入呼叫的輸入

快取中混淆。程序使用recvmsg來接收資料時,可以通過檢查MSG_OOB標誌來決定返回的資料是正常資料還是OOB資料。

9.2.接收快取的組織:報文邊界

對於支援報文邊界的協議,每一個報文存放在一個mbuf鏈中。下圖說明了由三個mbuf組成的UDP接收快取的結構。


9.3.接收快取的組織:沒有報文邊界

當協議不需要維護報文邊界(及SOCK_STREAM協議,如TCP)時,資料被加到快取中的最後一個mbuf鏈的尾部。如果進入

的資料長度大於快取的長度,則資料將被截掉。下圖說明了僅僅包含正常資料的TCP接收快取的結構。


9.4.控制資訊和帶外資料

不像TCP,一些流協議支援控制資訊,並且將控制資訊的相關資料作為一個新的mbuf鏈加入接收快取,如果協議支援內含

OOB資料,則插入一個新的mbuf鏈到任何包含OOB資料的mbuf之後,但在任何包含正常資料的mbuf之前,這一點確保進入

的OOB資料總是排在正常資料之前。下圖說明了包含控制資訊和OOB資料的接受快取的結構。


10.soreceive函式程式碼

在接收資料時,soreceive必須檢查報文邊界,處理地址和控制資訊以及讀標誌所指定的任何特殊操作。一般來說,soreceive

的一次呼叫只處理一個記錄,並且儘可能返回要求讀的位元組數。函式的大概處理流程如下:

1.接收OOB資料,因為OOB資料不存放在接收快取中,所以soreceive為其分配一塊標準的mbuf,並給協議傳送PRU_RCVOOB

請求。while迴圈將協議返回的資料複製到指定快取中。

2.如果需要,等待資料。soreceive要檢查幾種情況,如果需要,它可能要等待接收更多的資料才能往下執行。如果soreceive

在這裡進入睡眠狀態,則它醒來後會檢視是否有足夠的資料到達。這個過程一直繼續,直到收到足夠的資料為止。

3.處理地址和控制資訊。在傳輸之前,首先處理地址資訊和控制資訊。

4.建立資料傳送。因為只有OOB資料或正常資料是在一次soreceive呼叫中傳送,所以必須記住佇列前段的資料型別,這樣在型別

改變時,soreceive能夠停止傳送。

5.傳送資料迴圈。只要快取中還有mbuf,請求的資料還沒有傳送完畢,且沒有差錯出現,迴圈就不會退出。

6.退出處理。主要是更新指標、標誌和偏移;釋放插口快取鎖;使能協議處理並返回。

11.selecct系統呼叫

下圖列出了select能夠監控的插口狀態。

select函式的大概處理是:掃描程序指示的檔案描述符,當一個或多個描述符處於就緒狀態或定時器超時或訊號出現時返回。

11.1.selscan函式

select函式的核心是selscan函式。對於任意一個描述符集合中設定的每一個位元,selscan找出同它相關聯的描述符,並且

將控制分散給與描述符相關聯的so_select函式。對於插口而言,就是soo_select函式。

11.2.soo_select函式

對於selscan在輸入描述符集合中發現的每一個狀態就緒的描述符,selscan呼叫與描述符相關的fileops結構中的fo_select指標

引用的函式。函式判斷插口的可讀、可寫或例外情況,呼叫selrecord函式。

11.3.selrecord函式

該函式記錄了足夠的資訊,使得快取內容發生變化時協議處理層能夠喚醒程序。

11.4.selwakeup函式

當協議處理改變插口快取的狀態,並且只有一個程序選擇了該快取時,Net/3就能根據selrecord中記錄的資訊立即將該程序

放入執行佇列。

每一個呼叫select的程序在呼叫tsleep時使用selwait作為等待通道。這意味著對應的wakeup將喚醒所有阻塞在select上的

程序。

下圖說明如何呼叫selwakeup。

當改變插口狀態的事件出現時,協議處理層負責呼叫上圖底部列出的函式來通知插口層。這些函式都導致selwakeup被呼叫,

在插口上選擇的任何程序被排程執行。