1. 程式人生 > >TCP/IP實現(八) 插口層

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連線上

),在出現以下幾種狀況時都會產生訊號:1)連結建立完成;2)開始斷開連結;3)斷開連結完成;4)連線的一個通道已關閉(讀關閉或寫關閉?沒有測試過);5)插口上有資料到達(此時資料已準備就緒);6)資料已被髮送(即可寫,(是否需要剩餘資料低於高水位));7)當一個UDP或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驚群也參考這篇博文)