一、 同步 、异步、信号驱动IO、阻塞与非阻塞、I/O复用
同步包括了:
阻塞与非阻塞,I/O复用,信号驱动,因为他们在调用read等函数时,如果有数据可读,都会等到数据读取完毕才返回,而异步是发起后直接返回,等待内核通知读写完毕。
同步模型指的是所有的事情都按照某个顺序做完,并且只有一件事做完了才做下一件,这里以read调用为例:
异步: 发起aio_read,立即干别的,某时刻读完了,内核会通知进程,异步读写有自己的一套读写函数
注意与信号驱动式读写相区别,信号驱动依旧属于同步读写:
信号驱动:设置文件的信号驱动属性,设置信号处理函数, 发起读, 干别的, 等到数据准备好后内核发送信号
阻塞: 发起读 -> 阻塞并等待数据准备好 -> 继续阻塞,等待数据从内核拷贝至进程缓冲区 -> 拷贝完毕之后返回
非阻塞: 发起读-> 若没有数据准备好,直接返回, 若数据准备好了,阻塞并等待数据从内核拷贝至进程缓冲区并返回
I/O复用: 使用select,pselect,poll, epoll阻塞等待数据准备好 -> select 返回可读写集合,直接读写
二、读写socket
通常情况下,对socket的读写仅仅是将数据从进程拷贝至发送缓冲区,对于TCP,协议会保证发送可靠,对于UDP,除非之前先进行connect,否则在第一次发送时,即使数据不可达,也无法获取到错误
三、accept 群惊
多个进程/线程 同时使用accept 或者 select监听同一个socket时,当有新连接到达或者文件变得可读写,所有的进程/线程都会被唤醒,但是通常只有一个进程能够accept到新连接,其他的又继续睡眠,这会消耗大量资源。
解决办法是在accept 之前获取同步锁,并在accept之后释放,由内核决定哪个进程/线程 来accept,这样每一时刻只会有一个进程阻塞与accept,也就避免了群惊效果,select同理
四、使用select来监听accept时处理RST问题:
考虑下面情形:
使用select监听到一个新连接,select返回,此时线程切换,在这个过程中,对端又主动断开了连接,之后我们又切换回select的线程,使用accept接受新连接,由于新连接已经主动断开,程序阻塞在本不应该阻塞的accept上。
解决方案:使用select监听的accept 套接字都要设置非阻塞,同时accept忽略EPROTO等错误(具体错误取决于实现)
五、使用select监听非阻塞connect
当connect之后,若连接成功,则一定是可写的,如果出现错误,则一定是可读可写的,但是可读又可写并不代表一定是出现了错误,因为有可能是对端发来了数据,所以此时需要通过getsockopt函数来检查socket是否出错
六、非阻塞I/O的工作流程:
1,非阻塞写: 若有足够缓冲区可写,阻塞写,写完返回,否则返回EWOULDBLOCK错误
2,非阻塞读:所有数据可读(TCP中达到最低水位,通常是1字节,UDP中至少有一个数据报), 阻塞读,读完返回,否则返回EWOULDBLOCK错误
3,非阻塞accept:若无可用新连接,返回EWOULDBLOCK(或ECONNABORTED,EINTR,EPROTO等),否则正常返回可读写socket,使用select监听accept socket时,必须把accept socket设置为非阻塞并且忽略非阻塞错误,因为可能线程调度使得刚accept的连接挂掉然后一直不来新连接并阻塞在此处。
4,非阻塞connect: 若在同一台主机上,直接返回0,否则发起第一个SYNC之后直接返回EINPROGRESS错误
七、socket的半关闭:
1,执行close时会进行主动关闭,同时socket变得不可读并且不可写。
2,执行shutdown时会进行半关闭,发起FIN之后,socket变得不可写,但是可读
3,使用read读socket时,若返回0,表明对端发出了FIN,此时可以选择不理睬并继续写,从全双工变半双工,也可以执行close或者shutdown,关闭socket,由于收到FIN时表明对端已经至少进行了半关闭,所以如果对端不是半关闭,继续写可能会返回错误。
八、使用pselect
pselect比起select,可以在select时原子的设置信号掩码,防止select进入阻塞之后,丢掉进入select之前产生却未来得及处理的信号