一.概述
插口层可以说是在用户程序与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惊群也参考这篇博文)