TCP/IP實現(八) 插口層
一.概述
插口層可以說是在使用者程式與TCP/IP協議之間的一個呈上啟下的層次,它將使用者與某協議相關的請求對映到具體的協議實現。不同型別的套接字在產生時就會關聯到相關協議實現(通過一組函式指標來實現的)。比如在一個TCP套接字上呼叫write函式,則會轉而呼叫TCP協議相關的函式。
二.插口
插口也就是我們常說的套接字,它代表了一條通訊鏈路的一端,插口結構中儲存或指向了與鏈路相關的所有資訊。這些資訊包括使用的協議型別,協議的狀態資訊(如是否已連線,源和目的地址等),資料快取,插口選項(如SO_KEEPALIVE,SO_REUSEADDR等)。
插口的資料結構如下:
struct socket { short so_type; // 套接字關聯的協議型別 short so_options; // 套接字選項 short so_linger; /* time to linger while closing */ short so_state; // 套接字狀態(包括是否為訊號驅動和阻塞:SS_ASYNC,SS_NBIO以及連線狀態) caddr_t so_pcb; // 指向pcb控制塊 struct protosw *so_proto; /* protocol handle */ struct socket *so_head; // 當是排隊連線時,指向監聽套接字 // 下面5個欄位只對監聽套接字有用 struct socket *so_q0; // 還未完成3次握手的連結放在該佇列中 即accept函式還未返回 struct socket *so_q; // 已完成3次握手的連結放在該佇列中, 即accept函式已返回 short so_q0len; /* partials on so_q0 */ short so_qlen; /* number of connections on so_q */ short so_qlimit; /* max number queued connections */ // --------------------------------------------- short so_timeo; /* connection timeout */ u_short so_error; // 用於儲存差錯程式碼,當下次進行系統呼叫時會返回給程序 pid_t so_pgid; // 當設定了非同步時(SS_ASYNC),則當插口IO發生變化時會發送SIGIO訊號到so_pgid程序 u_long so_oobmark; // 用於標記帶外資料 // 套接字資料緩衝佇列 // 定義了收發兩個佇列 struct sockbuf { u_long sb_cc; /* actual chars in buffer */ u_long sb_hiwat; /* max actual char count */ u_long sb_mbcnt; /* chars of mbufs used */ u_long sb_mbmax; /* max chars of mbufs to use */ long sb_lowat; /* low water mark */ struct mbuf *sb_mb; /* the mbuf chain */ struct selinfo sb_sel; /* process selecting read/write */ short sb_flags; /* flags, see below */ short sb_timeo; /* timeout for read/write */ } so_rcv, so_snd; // 定義了一些常量如預設緩衝最大大小,SB為sockbuf的縮寫 #define SB_MAX (256*1024) /* default for max chars in sockbuf */ // - 保護套接字資料快取的鎖,因此不存在當用戶呼叫recv函式從快取讀入資料時,又將資料新增到快取中的情況。 // - 且多執行緒從一個套接字讀取資料也是安全的 #define SB_LOCK 0x01 #define SB_WANT 0x02 /* someone is waiting to lock */ #define SB_WAIT 0x04 /* someone is waiting for data/space */ #define SB_SEL 0x08 /* someone is selecting */ #define SB_ASYNC 0x10 /* ASYNC I/O, need signals */ #define SB_NOTIFY (SB_WAIT|SB_SEL|SB_ASYNC) #define SB_NOINTR 0x40 /* operations not interruptible */ caddr_t so_tpcb; /* Wisc. protocol control block XXX */ void (*so_upcall) __P((struct socket *so, caddr_t arg, int waitf)); caddr_t so_upcallarg; /* Arg for above */ };
三..訊號驅動I/O
我們可以使用訊號,讓核心在描述符就緒時傳送SIGIO訊號通知程序(程序ID記錄於socket結構體中)。稱這種模型為訊號驅動式I/O,如下圖所示:
當將套接字設定為訊號驅動IO後,每當套接字狀態發生改變,便會向套接字中記錄的程序傳送SIGIO訊號。在UDP上使用訊號驅動I/O是簡單的,此時SIGIO訊號會在以下事件發生時產生:1)資料報到達套接字(即已放入套接字快取); 2) 套接字上發生非同步錯誤 。在TCP上使用訊號驅動程式時(訊號驅動IO幾乎不會用在TCP連線上
POSIX保證被捕獲的訊號在其訊號處理函式期間總是阻塞的,關於訊號的排隊機制,我們將在其它博文中做進一步說明。
關於UDP使用訊號驅動I/O的例子可參見UNP P531。其伺服器端的主要程式碼如下:
void Server::start()
{
fcntl(_sockfd, F_SETOWN, getpid()); // 設定套接字的屬主程序,這樣訊號便會被髮送至該程序
fcntl(_sockfd, F_SETFL, O_ASYNC); // 設定套接字為訊號驅動IO
fcntl(_sockfd, F_SETFL, O_NONBLOCK);// 設定套接字為非阻塞
struct sigaction act;
act.sa_flags |= SA_INTERRUPT; //設定訊號的中斷模式是中斷後繼續執行被中斷函式之後的程式碼,而不重啟
act.sa_handler = sigio_fun; // 設定訊號處理函式
sigaction(SIGIO,&act,NULL);
sigset_t zeromask, newmask, oldmask; // 初始化訊號掩碼
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigemptyset(&oldmask);
sigaddset(&newmask, SIGIO);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);// 設定要遮蔽的訊號
while(1){
if(_dataQue.empty()){
sigsuspend(zeromask); // 該函式會先設定訊號掩碼,之後一直阻塞直至捕獲訊號並從訊號處理函式返回
}
sigprocmask(SIG_BLOCK, &oldmask, NULL); // 重新設定為非阻塞
// sendto(...)
_dataQue.pop();
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
}
}
四.幾個系統呼叫介紹及常見錯誤
1.socket函式
socket函式用於建立套接字及與之對應的檔案描述符,檔案物件結構。當用戶許可權不夠時(如建立原始套接字),則會將socket狀態設定為SS_PRIV,當呼叫其它系統呼叫時,便會檢測改狀態,並返回錯誤資訊。
2.accept系統呼叫
當在一個非監聽套接字上呼叫該函式,會返回EINVAL,這個錯誤碼錶示引數錯誤。若當前連線就緒佇列無就緒連線,且是非阻塞模式,則會返回EAGAIN,該錯誤碼一般表示請求的資源尚未就緒(如:套接字資料接收佇列無資料等),請稍後再試。
3.connect系統呼叫
對於一個非阻塞套接字而言,若呼叫connect時套接字的狀態是正在連線,則會返回EALREADY,這通常出現在:當前一次connect未呼叫成功,之後又在同一套接字上進行呼叫的情況(有的系統不會又這個問題,當儘量避免這樣使用)。
當在一個非阻塞套接字上成功呼叫connect時,會立刻返回,若此時仍在進行連線(即返回時連線尚未完成),則會返回錯誤碼EINPROGRESS,那麼如何知道連線已完成呢?當連線就緒時,該套接字的狀態會變為可寫,因此在select上關注該描述符的寫事件。那麼第二個問題又來了,當一個套接字上出現錯誤時,套接字會變得可讀亦可寫,那麼當套接字可寫時,我們如何判斷是連線建立成功還是存在待處理的錯誤呢?這個問題可以通過傳入引數SO_ERROR呼叫getsockopt(_sockfd, SOL_SOCKET, SO_ERROR, &err, &len);函式處理,若套接字上存在錯誤則說明是有錯誤待處理,否則為連線建立成功。muduo網路庫中也是採取的這種方式來進行區別的。
那麼對於阻塞式套接字呢?假設在一個阻塞式套接字上呼叫connect函式,而該函式被中斷(捕獲到某個訊號),此時會怎樣呢?加入核心不自動重啟,那麼它將返回EINTER,此時不應該再次呼叫connect,而應該採用與與非阻塞套接字一樣的檢測方法。
5.seleect系統呼叫與select衝突
參博文《高階I/O》(包括accept驚群也參考這篇博文)