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惊群也参考这篇博文)

猜你喜欢

转载自blog.csdn.net/qq_34228327/article/details/84190812