I/O模型
可划分为5
个类别
1.阻塞式
2.非阻塞+
轮询
3.I/O
复用(阻塞等待多个描述符)
4.信号驱动(操作条件满足时,意味这此时操作不会阻塞,给进程发信号)
5.异步I/O
(I/O
请求先发出,立即返回.I/O
最终完成时通知应用)
基于select,poll的I/O复用
select
// > 0,3个集合中条件满足的描述符个数和
// = 0,超时
// < 0,出错
int select(
int maxfdp1,// 待测试的最大描述符+1
fd_set* readset,// 值-结果参数
fd_set* writeset,// 值-结果参数
fd_set* exceptset,// 值-结果参数
const struct timeval* timeout);
void FD_ZERO(fd_set*);
void FD_SET(int fd, fd_set*);
void FD_CLR(int fd, fd_set*);
int FD_ISSET(int, fd_set*);
1.描述符异常条件被触发情形:
a.套接字存在带外数据
b.套接字处于带外标记
2.对套接字描述符来说可读被触发的情形
a. 套接字接收缓冲区可读取数据量超过或等于指定值(低水位标记),对其读,可读到数据.
b. 套接字接收缓冲区无可读取数据,且收到此套接字对端的FIN
,对其读,返回0
.
c. 套接字接收缓冲区无可读取数据,且收到此套接字对端的RST
,对其读,返回-1
.
3.监听套接字关联的已连接套接字队列不是空,可对其调accept
,以将已连接套接字返回
4.内核收到关于套接字的一个错误通知,对其读,返回-1
.
5.对套接字描述符来说,可写被触发的情形
a. 套接字发送缓冲区可写入数据量大于指定值(低水位标记),对其写,写入数据.
b. 已经收到套接字对端发来的RST
(表明对端主机不会接收发向其的数据),对其写,将产生SIGPIPE
信号.
c. 内核收到关于套接字的一个错误通知,对其写,返回-1
.
d. 对非阻塞调connect
返回的套接字,后续调select
,在此套接字收到对端的FIN ACK
或RST
/超时时
TCP的注意点
1.当本地不想继续发消息给对端时,可使用shutdown
,无视此套接字底层结构引用计数立即给对端发FIN
.
此后,仍然可以用套接字接收对端的数据.
若本端直接使用close
,若close
更新套接字引用计数后,其仍然大于0
,不会发FIN
给对端,若close
后,引用计数变为0
,发FIN
给对端.但close
无论是否让引用计数变为0
都会让本端丧失通过此套接字描述符给对端发消息,接收对端消息的能力.
连接关闭主动发经历TIME_WAIT
后,套接字会被销毁.一旦消息包达到目的主机,但找不到匹配的接收套接字,内核会以RST作为回复.
在已经收到rst
下,继续向套接字写入数据,会触发SIGPIPE
信号通知.
2.主动关闭用shutdown
主动方,一般shutdown+SHUT_WR
:
a. 本地此后无法继续向套接字写入数据,且套接字发送缓冲区现有数据发完后,发出一个fin
b. 此时本端仍然可接收对端发来的数据.
c. 收到对端的fin
后,发回fin ack
,进入time wait
d. 持续时间过后,进入closed
状态
被动方:
a. 在收到fin
时,发回fin ack
b. 此后,对端发数据,一切正常(仍然会被ack
)
c. 若对端发了fin
,等到响应的fin ack
时,套接字直接进入closed
shutdown
// SHUT_WR/SHUT_RD/SHUT_RDWR
int shutdown(int sockfd, int howto);
1.SHUT_WR
套接字发送缓冲区剩余数据发完,自动给对端发一个FIN
.此后,应用无法通过此套接字描述符向对端写入任何数据.
2.SHUT_RD
对接收到的对端发来的数据内核仍然会对其确认.但应用在此之后无法通过此套接字描述符读取对端的任何数据.
3.SHUT_RDWR
等价于以SHUT_RD
调一次shutdown
,再以SHUT_WR
调一次shutdown
poll
int poll(
struct pollfd *fdarray,
unsigned long nfds,
// INFTIM
// 0
// > 0
int timeout);
struct pollfd
{
int fd;
// 传入
short events;
// 传出
short revents;
};
各类内核实现:
1.所有正规TCP/UDP
数据被认为是普通数据
2.TCP
的带外数据,视为优先级带数据
3.收到对端FIN
,认为是普通可读
4.收到RST
或超时或其他错误,有的认为是普通数据,有的认为是错误
5.监听套接字上有已经完成连接,有的认为是普通数据,有的认为是优先级数据
6. 非阻塞下connect
后续完成,认为是普通可写
poll
存在的原因是,select
检测的描述符个数超出FD_SET
支持的范围时,可尝试改用poll
(poll
每个描述符一个pollfd元素,元素个数有使用者分配和指定).
高级I/O函数
超时
为阻塞调用添加超时支持:
a. 采用alarm+
信号中断
b. 采用select
c. 对套接字支持SO_RCVTIMEO/SO_SNDTIMEO
下,用套接字选项
recv,send
ssize_t recv(
int sockfd,
void* buff,
size_t nbytes,
int flags);
ssize_t send(
int sockfd,
const void* buff,
size_t nbytes,
int flags);
关于flags
:
1.MSG_OOB
对send
,指明即将发送的是带外数据
对recv
,指明即将读入的是带外数据
2.MSG_PEEK
适用于recv/recvfrom
,允许获取数据,但数据不移出缓冲区
3. MSG_DONTWAIT
单次调用不阻塞
readv,writev
ssize_t readv(
int filedes,
const struct iovec* iov,
int iovcnt);
ssize_t writev(
int filedes,
const struct iovec* iov,
int iovcnt);
struct iovec
{
void* iov_base;
size_t iov_len;
};
recvmsg,sendmsg
ssize_t recvmsg(
int sockfd,
struct msghdr* msg,
int flags);
ssize_t sendmsg(
int sockfd,
struct msghdr* msg,
int flags);
struct msghdr
{
void* msg_name;// 对端地址
// 对sendmsg是一个值参数
// 对recvmsg是一个值-结果参数
socklen_t msg_namelen;// 对端地址对象长
struct iovec* msg_iov;
int msg_iovlen;
void* msg_control;// 辅助数据地址
// 对sendmsg是一个值参数
// 对recvmsg是一个值-结果参数
socklen_t msg_controllen;
// recvmsg用其来接收内核处理后标志[来告知应用recvmsg中出现的一些应用可能感兴趣信息]
int msg_flags;
};
关于recvmsg,sendmsg的辅助数据
对IPV4
,目前可用辅助数据来
a. 随UDP
数据报接收目的地址
b. 随UDP
数据报接收接口索引
对IPV6
,目前可用来
a. 指定或接收 跳限
b. 指定下一跳地址
c. 指定或接收路由首部
d. 指定或接收分组流通类别
辅助数据由一个或多个辅助数据对象构成
struct cmsghdr
{
socklen_t cmsg_len;
int cmsg_level;
int cmsg_type;
...
};
// 用于简化对辅助数据的处理
struct cmsghdr* CMSG_FIRSTHDR(
struct msghdr* mhdrptr);
struct cmsghdr* CMSG_NXTHDR(
struct msghdr* mhdrptr,
struct cmsghdr* cmsgptr);
unsigned char* CMSG_DATA(struct cmsghdr* cmsgptr);
unsigned int CMSG_LEN(unsigned int length);
unsigned int CMSG_SPACE(unsigned int length);
非阻塞式I/O
1.使用非阻塞式I/O
目的是希望提升效率
可以以非阻塞方式打开描述符,或使用fcntl
让一个描述符变为非阻塞.一般,以多线程/多进程可以达到和非阻塞I/O
一样的效果.
2.阻塞模式下,套接字的可读/可写
a. 已经连接套接字
a.1. 可读:
a.1.1. 接收缓存区有足量的尚未读取数据.
a.2.可写:
a.2.1.发送缓存区有足量的可写空间足量可写空间指的是可写空间大于或等于要求写入的字节数.
b. 未连接套接字
不可读,不可写
c. 收到了对端FIN
的套接字
c.1.可读:
c.1.1.接收缓存区有足量数据
c.1.2.接收缓存区空
c.2.可写:
c.2.1.发送缓存区有足够可写空间
d. 收到了对端RST的套接字
d.1.可读:
d.1.1. 接收缓存区有足量数据
d.1.2.接收缓存区空
d.2.可写:
d.2.1.发送缓存区有足够可写空间
d.2.2.发送缓存区无足够可写空间
e.监听套接字
e.1.可读:
e.1.1.关联已经连接队列非空
e.2.可写:
不可写
f.出错套接字(套接字上存在待处理错误)
f.1.可读:
任何情况均可读
f.2.可写:
任何情况均可写
3.非阻塞模式下,套接字的可读/可写
a. 已经连接套接字
a.1.可读:
a.1.1.接收缓存区有足量的尚未读取数据.
如不可读,立即返回EWOULDBLOCK
a.2.可写:
a.2.1.发送缓存区有足量的可写空间.如无可写空间,立即返回-1
,错误码设置为EWOULDBLOCK
.如可写空间不足,将可写部分写入,并返回实际写入个数.
b. 未连接套接字
不可读,不可写.若对未连接套接字执行connect
,对其执行select
,在连接建立后,转变为已经连接套接字,可读,可写也转变为按已经连接套接字来处理.若调connect
,连接无法立即建立(目的端不是本机一般都无法立即建立),立即返回EINPROGRESS
.
若对调connect
返回EINPROCESS
的非阻塞套接字接着调select
,收到对端FIN ACK
时,称为已经连接套接字.一般时立即可写的.若对端附带发来了数据,则也是立即可读的.若连接建立失败,超时或对端无监听等,套接字会变成有错误的套接字.既可读,又可写.此时,进一步用SOL_SOCKET+SO_ERROR
获取套接字错误,如有错误,则为错误套接字,如无错误,则为已经连接套接字.
c. 收到了对端FIN
的套接字
c.1.可读:
c.1.1.接收缓存区有足量数据
c.1.2.接收缓存区空
c.2.可写:
c.2.1.发送缓存区有足够可写空间
d. 收到了对端RST
的套接字
d.1.可读:
d.1.1.接收缓存区有足量数据
d.1.2.接收缓存区空
d.2.可写:
d.2.1.发送缓存区有足够可写空间
d.2.2.发送缓存区无足够可写空间
e. 监听套接字(对其执行accept
)
e.1.可读:
e.1.1.关联的已经连接队列非空.若关联的已经连接队列为空,立即返回EWOULDBLOCK.
对监听套接字,正确的使用方式为:
监听套接字设置为非阻塞
对其调select
,若select
指示该监听套接字可读,对其执行accept
.
若accept
返回EWOULDBLOCK/ECONNABORTED/RPOTO/EINTR
均忽略(在select
返回到accept
执行间,若收到对端的RST
,上述错误码是可能返回的)
e.2.可写:
不可写
f.出错套接字(套接字上存在待处理错误)
f.1.可读:
任何情况均可读
f.2.可写:
任何情况均可写
信号驱动式I/O
进程预先告知内核,使得当某个描述符上发生某事时,内核使用信号通知相关进程.通知可以开始执行I/O
.
异步I/O
I/O
委托给内核,内核完成I/O
通知应用
套接字的信号驱动式I/O
对一个套接字用信号驱动式I/O
a. 建立SIGIO
的信号处理函数
b. 设置该套接字的属主(fcntl+F_SETOWN
)
c. 开启该套接字的信号驱动式I/O
,通常用fcntl
的F_SETFL
打开O_ASYNC
标志.
对UDP
套接字的SIGIO
信号,SIGIO
信号在发生以下事件时产生
a. 数据报到达套接字
b. 套接字上发生异步错误(UDP
可获知异步错误前提是其用connect
绑定了连接)
对TCP
套接字的SIGIO
信号,信号驱动式I/O
对TCP
没什么作用,很多情况都导致SIGIO
的产生
a. 监听套接字上某个连接请求已经完成
b. 某个断开连接请求已经发起/完成
c. 某个连接关闭/半关闭
d. 数据到达套接字
e. 数据已经从套接字发走
f. 发生异步错误