1.I/O模型
5种基本I/O模型
- 阻塞式I/O
- 非阻塞式I/O
- I/O复用(select和poll)
- 信号驱动式I/O(SIGIO)
- 异步I/O
一个输入操作通常包括两个不同的阶段
- 等待数据准备
- 从内核向进程复制数据
对于一个套接口上的输入操作,第一步一般是等待数据到达网络,当分组到达时,它被拷贝到内核中的某个缓冲区,第二步是将数据从内核缓冲区拷贝到应用缓冲区。
1).阻塞式I/O
最流行的I/O模型是阻塞式I/O(blocking I/O)模型,默认情形下,所有的套接字都是阻塞的
上图中进程在从调用recvfrom开始到它返回的整段时间内被阻塞,recvfrom成功返回后,应用进程开始数据处理
2).非阻塞I/O模型
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成,不能把本进程投入睡眠,而是返回一个错误。
前三次调用recvfrom时没有数据可以返回,因此内核转而立即返回一个EWOULDBLOCK错误,第四次调用recvfrom时已经有数据报准备好,它被复制到应用程序缓冲区,于是recvfrom成功返回
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做 polling) 。应用程序不停的 polling内核来检查是否 I/O操作已经就绪。这将是一个极浪费CPU资源的操作。这种模式使用中不是很普遍。
3)I/O复用模型
有了I/O复用,我们就可以调用select或poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞真正的I/O系统之上
阻塞于select调用,等待数据报套接字变为可读,当select返回套接字可读这一条件时,调用recvfrom把所读的数据复制到应用程序缓冲区内。另外使用select的优势在于我们可以等待多个描述符就绪
4).信号驱动I/O模型
可以用信号让内核在描述符就绪时发送SIGIO信号通知
无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已处理好被处理,也可以是数据已准备被读取
5).异步I/O模型
异步 I/O 和 信号驱动I/O的区别是:
- 信号驱动 I/O 模式下,内核在操作可以被操作的时候通知给我们的应用程序发送SIGIO 消息。
- 异步 I/O 模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的应用程序。
6).各种I/O模型的比较
比较结果如下图
2.select函数
该函数允许进程指示内核等待多个事件的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间才唤醒它,也就是说我们调用select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间,当然感兴趣的描述符可以不仅局限于套接字,任何描述符都可以用select测试
#include <sys/select.h> #include <sys/time.h> int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout); // 返回: 准备好描述字的正数目, 0 -超时, -1 -出错
它告诉内核等待一组指定的描述字中的任一个准备好可花多长时间,结果timeval指定了秒数和微秒数成员
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ };
这个参数有以下三种可能:
a. 永远等待下去:仅在有一个描述字准备好I/O时才返回,为此,我们将参数timeout设置为空指针。
b. 等待固定时间:在有一个描述字准备好I/O是返回,但不超过由timeout参数所指timeval结构中指定的秒数和微秒数。
c. 根本不等待:检查描述字后立即返回,这称为轮询(polling)。为了实现这一点,参数timeout必须指向结构timeval,且定时器的值(由结构timeval指定的秒数和微秒数)必须为0
在前两者情况的等待中,如果进程捕获了一个信号并从信号处理程序返回,那么等待一般被中断。
中间三个参数readset,wirteset和exceptset指定我们要让内核测试读写和异常条件所需的描述字,参数maxfdp1说明了被测试的描述符的个数,它的值是要被测试的最大的描述符加1.
为了分配一个fd_set数据类型的描述符集,并用这些宏初始化,设置或测试该集合的每一位,有下面是四个宏函数
void FD_ZERO(fd_set * fdset); /* clear all bits in fdset */ void FD_SET(int fd, fd_set * fdset); /* turn on the bit for fd in fdset */ void FD_CLR(int fd, fd_set * fdset); /* turn on the bit for fd in fdset */ int FD_ISSET(int fd, fd_set * fdset); /* is the bit for fd on in fdset */ 举个例子,以下代码定义一个fd_set类型的变量,并打开描述字1,4和5的相应位 fd_set rset; FD_ZERO(&rset); /* initiallize the set, all bits off */ FD_SET(1, &rset); /* turn on bit for fd 1 */ FD_SET(4, &rset); /* turn on bit for fd 4 */ FD_SET(5, &rset); /* turn on bit for fd 5 */
描述符就绪条件
1).满足下面四个中任意条件,则一个套接字准备好读:
a.套接字接收缓冲区的数据字节数大于等于,套接字接收缓冲区低水位线,可以用SO_RCVLOWAT套接选项来设置低水位线,对于TCP和UDP套按字,默认值为1
b.该连接的读半部分关闭(接收到了FIN的TCP连接).对这样的套接字读操作,返回0(EOF)
c.该套接字是一个监听套接字且已经完成的连接数不为0.对这样的套按字的accept通常不会阻塞
d.其上有一个套接字错误待处理.对这样的套按字的读操作将不阻塞并返回-1(错误),同时把errno设置成错误条件,这些待处理错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取.
2).满足下面四个中任意条件,则一个套接字准备好写:
a.该套接字发送缓冲区的可用字节数大于等于套接字发送缓冲区低水位线的当前大小.并且或者该套接已经连接,或者套按字不需要连接(UDP),如果我们把这套接字设置成非阻塞,写操作将不阻塞并返回一个正值.可以使用SO_SNDLOWAT设置一个该套接字的低水位标记.对于TCP和UDP默认值通常为2048.
b.该连接的写半部关闭.对这样的套接写的写操作将产生SIGPIPE信号.
c.使用非阻塞式的connect的套按字已经建立连接,或者connect已经失败.
d.其上有一个套接字错误等处理。对这样的套接字进行写操作会返回-,且,把ERROR设置成错误条件,可以通过指定SO_ERROR套按选项调用getsockopt获取并清除.
3).如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理
3.shutdown函数
终止网络连接的正常方法是调用close,但close有两个限制可由函数shutdown来避免:
1). close将描述字的访问计数减1,仅在此计数为0时才关闭套接口。用shutdown我们可以激发TCP的正常连接终止序列,而不管访问计数。
2). close终止了数据传送的两个方向:读和写。由于TCP连接是全双工的,有很多时候我们要通知另一端我们已经完成了数据发送,即使那一端仍有许多数据要发送也是如此。
#include <sys/socket.h> int shutdown(int sockfd, int howto); // 返回: 0-成功, -1-出错 该函数的行为依赖于howto参数的值: ● SHUT_RD -- 关闭套接字的读取数据方向的连接 ● SHUT_WR -- 关闭套接字的写入数据方向的连接 ● SHUT_RDWR -- 关闭套接字双向的连接
4.非阻塞IO
1.概述
1).输入操作: read, readv, recv, recvfrom和recvmsg函数。
2).输出操作: write, writev, send, sendto和sendmsg函数。
3).接收外来连接: accept函数
4).初始化外出的连接: 用于TCP的connect函数
2.非阻塞读和写
我们维护两个缓冲区: to容纳从标准输入到服务器去的数据,fr容纳自服务器到标准输出来的数据。
3.非阻塞connect
非阻塞的connect有三种用途:
1). 我们可以在三路握手同时做一些其他的处理。完成一个connect要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网。
2). 可以用这种技术同时建立多个连接。这在Web浏览器中很普遍
3). 由于我们用select等待连接的完成,因此可以给select设置一个时间限制,从而缩短connect的超时时间。
非阻塞connect虽然听似简单,却有一些必须处理的细节
1).即使套接口是非阻塞的,如果连接的服务器在同一台主机上,那么在调用connect建立连接时,连接通常会立即建立成功.我们必须处理这种情况;
2).源自Berkeley的实现(和Posix.1g)有两条与select和非阻塞IO相关的规则:
- 当连接建立成功时,套接字描述符变成可写;
- 当连接出错时,套接子描述符变成既可读又可写;
注意:当一个套接口出错时,它会被select调用标记为既可读又可写
4.非阻塞accept
阻塞模式下,服务器会一直阻塞在accept调用上,知道其他某个客户建立一个连接为止,但是在此期间,服务器单纯阻塞在accept调用上,无法处理任何其他已就绪的描述符
非阻塞accept模式下解决办法
1).当使用select获悉某个监听套接字上何时有已完成连接准备被accept时候,总是把这个监听套接字设置为非阻塞
2).在后续的accept调用忽略以下错误:EWOULDBLOCK(客户终止连接时)、ECONNABORTED(客户终止连接时)、EPROTO(客户终止连接时)和EINTR(如果有信号被捕获)
5.信号驱动I/O
1.概述
信号驱动是指进程预先告知内核,使得当某个描述符上发生某事时,内核使用信号通知相关进程。需要注意的是这里描述的信号驱动不是真正的异步I/O。
注意第16章描述的非阻塞I/O同样不是异步I/O。对于非阻塞I/O,内核一旦启动,I/O操作就不像异步I/O那样立即返回到进程,而是等到I/O操作完成或遇到错误;内核立即返回的唯一条件是I/O操作的完成不得不把进程投入睡眠,这种情况下内核不启动I/O操作。
2.套接字的信号驱动式I/O
针对一个套接字使用信号驱动I/O(SIGIO) 要求进程执行以下三个步骤:
1). 给SIGIO信号建立信号处理程序
2). 设置套接口属主,通常使用fcntl的F_SETOWN命令
3). 激活套接口的信号驱动I/O,通常使用fcntl的F_SETFL命令打开O_ASYNC标志
3.UDP套接字的SIGIO信号
UDP上使用信号驱动I/O是简单的。当下述事件发生时产生SIGIO信号:
- 数据报到达套接字
- 套接口上发生异步错误
因此当捕获到SIGIO信号时,调用recvfrom或者读入到达的数据报或者获取发生的异步错误。
4.TCP套接字的SIGIO信号
不幸的是,信号驱动I/O对TCP套接字几乎是没用的,问题在于是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情。
下列条件均可在TCP套接口上产生SIGIO信号(假设信号驱动I/O是使能的):
- 在监听套接口上有一个连接请求已经完成
- 发起了一个连接拆除请求
- 一个连接拆除请求已经完成
- 一个连接的一半已经关闭
- 数据到达了套接字
- 数据已从套接字上发出(即输出缓冲区有空闲时间)
- 发生了一个异步错误
UNP上有示例程序
6.ioctl函数
#include <unistd.h> int ioctl(int fd,int request,.../* void *arg */);
其中第三个参数总是一个指针,但指针的类型依赖于request参数。
我们可以把网络相关的请求(request)划分为6类:
1.套接字操作
2.文件操作
3.接口操作
4.ARP高速缓存操作
5.路由表操作
6.流系统
下图列出了网络相关ioctl请求的request参数以及arg地址必须指向的数据类型:
套接字操作
SIOCATMARK 如果本套接字的读指针当前位于带外标记,那就通过由第三个参数指向的整数返回一个非0值,否则返回一个0值
SIOCGPGRP 通过由第三个参数指向的整数返回本套接字的进程ID或进程组ID
SIOCSGRP 把本进程进程ID或进程组ID设置成由第三个参数指向的整数
文件操作
FIONBIO 根据ioctl的第三个参数指向一个0值或非0值,可清除或设置本套接字的非阻塞式I/O标志
FIOASYNC 根据ioctl的第三个参数指向一个0值或非0值,可清除或设置本套接字的信号驱动异步I/O标志,它决定是否收取针对本套接字的异步I/O信号(SIGIO)
FIONREAD 通过由ioctl的第三个参数指向的整数返回当前本套接字接收缓冲区中的字节数
FIOSETOWN 对于套接字和SIOCSPGRP等效
FIOGETOWN 对于套接字和SIOCGPGRP等效
接口配置
需处理网络接口的许多程序的初始步骤之一就是从内核获取配置在系统中的所有接口。本任务由SIOCGIFCONF请求完成,它使用ifconf结构,ifconf又使用ifreq结构。这两个结构定义如下:
struct ifconf { int ifc_len; /* size of buffer */ union { char *ifcu_buf; /* input from user->kernel*/ struct ifreq *ifcu_req; /* return from kernel->user*/ } ifc_ifcu; }; #define ifc_buf ifc_ifcu.ifcu_buf /* buffer address */ #define ifc_req ifc_ifcu.ifcu_req /* array of structures */ //ifreq用来保存某个接口的信息 //if.h struct ifreq { char ifr_name[IFNAMSIZ]; union { struct sockaddr ifru_addr; struct sockaddr ifru_dstaddr; struct sockaddr ifru_broadaddr; short ifru_flags; int ifru_metric; caddr_t ifru_data; } ifr_ifru; }; #define ifr_addr ifr_ifru.ifru_addr #define ifr_dstaddr ifr_ifru.ifru_dstaddr #define ifr_broadaddr ifr_ifru.ifru_broadaddr
在调用ioctl前我们先分配一个缓冲区和一个ifconf结构,然后初始化后者。下面展示这个ifconf结构的初始化结果,其中缓冲区的大小为1024字节
假设内核返回2个ifreq结构,在ioctl返回时通过同一个ifconf结构所返回的值如下图。缓冲区被填入两个ifreq结构,而且ifconf结构的ifc_len成员也被更新
接口操作
SIOCGIFCONF请求为每个已配置的接口返回其明知以及一个套接字地址结构。我们接着可以发出多个接口类的其他请求以设置或获取每个接口的其他特性
这些请求的get版本(SIOCGxxx)通常由netstat程序发出,set版本(SIOCSxxx)通常由ifconfig程序发出
这些请求接受或返回一个ifreq结构中的信息,而这个结构的地址作为ioctl调用的第三个参数。
SIGCGIFADDR 在ifr_addr成员中返回单播地址
SIOCSIFADDR 用ifr_addr成员设置接口地址
SIOCGIFFLAGS 在ifr_flags成员中返回接口标志,如:是否处于在工状态(IFF_UP)
SIOCSIFFLAGS 用ifr_flags成员设置接口标志
SIOCGIFDSTADDR 在if_dstaddr成员中返回点到点地址
SIGCSIFDSTADDR 用if_dstaddr成员设置点到点地址
SIOCGIFBRDADDR 在ifr_broadaddr成员中返回广播地址
SIOCSIFBRDADDR 用ifr_broadaddr成员设置广播地址
SIOCGIFNETMASK 在ifr_addr成员中返回子网掩码
SIOCSIFNETMASK 用ifr_addr成员设置子网掩码
SIOCGIFMETRIC 在ifr_metric成员返回接口测度
SIGCSIFMETRIC 用ifr_metric成员设置接口测度
ARP高速缓存操作
这些请求使用如下所示的arpreq结构
struct arpreq{ struct sockaddr arp_pa; //协议地址 struct sockaddr arp_ha; //硬件地址 int arp_flags;//标志位 } #define ATR_INUSE 0x01 /* entry in use */ #define ATF_COM 0x02 /* completed entry (hardware addr valid) */ #define ATF_PERM 0x04 /* permanent entry */ #define ATF_PUBL 0x08 /* published entry (repond for other host) */
操纵ARP高速缓存的ioctl请求有以下3个
SIOCSARP 把一个新的表项加到ARP高速缓存,或者修改其中一个已经存在的表项
SIOCDARP 从ARP高速缓存中删除一个表项。调用者指定要删除的arp_pa
SIOCGARP 从ARP高速缓存中获取一个表项。调用者指定arp_pa,相应的硬件地址随标志一起返回
路由表操作
有些系统提供两个用于操纵路由表的ioctl请求,在支持路由域套接字的系统中,这些请求改由路由套接字完成
SIOCADDRT 往路由表中增加一个表项
SIOCDELRT 从路由表中删除一个表项