本文讲述Linux五种IO模型以及java的三类api调用
用自己的服务器,来试验各种功能吧,一天不到三毛钱
【腾讯云】云产品限时秒杀,爆款1核2G云服务器,首年99元
【阿里云】云产品限时秒杀,爆款1核2G云服务器,首年96元
之前的IO操作以一次读取和写入解读过,涉及到用户空间、内核空间等,可以看传统IO到零拷贝
同步和异步的区别在于,被调用方的执行方式和返回时机。
通俗来讲,只有三种存在,同步阻塞(一直等待调用结果),同步非阻塞(直接做其他任务,时不时看调用结果有没有返回),异步非阻塞(直接做其他任务,被调用者回调或者事件触发结果返回)。
在介绍Linux IO模型前,先介绍几个linux系统调用函数。
函数名称 | 用法 |
---|---|
recvfrom | 用户用于接收网络IO的系统接口。从套接字上接收一个消息,可同时应用于面向连接和无连接的套接字。如果此系统调用返回值<0,并且 errno为EWOULDBLOCK或EAGAIN(套接字已标记为非阻塞,而接收操作被阻塞或者接收超时 )时,连接正常,阻塞接收数据 |
select | select系统调用允许程序同时在多个底层文件描述符上,等待输入的到达或输出的完成。以数组形式存储文件描述符,64位机器默认2048个。当有数据准备好时,无法感知具体是哪个流OK了,所以需要一个一个的遍历,函数的时间复杂度为O(n)。 |
poll | 以链表形式存储文件描述符,没有长度限制。本质与select相同,函数的时间复杂度也为O(n)。 |
epoll | 是基于事件驱动的,如果某个流准备好了,会以事件通知,知道具体是哪个流,因此不需要遍历,函数的时间复杂度为O(1)。 |
sigaction | 用于设置对信号的处理方式,也可检验对某信号的预设处理方式。Linux使用SIGIO信号来实现IO异步通知机制。 |
下面以一次数据交互为例,说明下五种数据模型。
注意:
其中IO多路复用有三种 select,poll,epoll
它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
select 选择句柄的时候,是遍历所有句柄,也就是说句柄有事件响应时,select需要遍历所有句柄才能获取到哪些句柄有事件通知
select支持的句柄数是有限制的, 同时只支持1024个,这个是句柄集合限制的,如果超过这个限制,很可能导致溢出,而且非常不容易发现问题, 当然可以通过修改linux的socket内核调整这个参数。
poll没有最大文件描述符数量的限制。
select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
Linux2.6才出现了由内核直接支持的实现方法,那就是epoll。最好的多路I/O就绪通知方法
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发)
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
epoll对于句柄事件的选择不是遍历的,是事件响应的,就是句柄上事件来就马上选择出来,不需要遍历整个句柄链表,因此效率非常高,内核将句柄用红黑树保存的。
CPU密集型的服务,事件驱动并不合适
事件驱动适合于IO密集型服务,多进程或线程适合于CPU密集型服务,它们各有各的优势,并不存在谁取代谁的倾向。
举个简单的例子,小明用水壶去烧水,阻塞不阻塞是取决于小明,同步和异步是取决于水壶。
小明去烧水,要一直站在水壶前(阻塞),等水壶里的水烧开(同步)。
小明去烧水,在此期间还去扫地,刷碗(非阻塞),每隔一段时间去观察一下(同步)。
小明去烧水,水壶更新了,到了100度会报警,小明放心的去玩(非阻塞),水烧开水壶报警了(异步)。
Java中有三个模型,分别对照上边的三种形式。
BIO通信模型,通常由一个独立的 Acceptor 线程负责监听客户端的连接。
服务端会有一个while循环来接收客户端的请求(accept),客户端发起一次请求,服务端相应一次,线程销毁。一请求,一应答通信模型。
如果需要高并发,就需要创建多个线程。有多少个客户端就有多少个线程与之相对应。如果后端建立线程池,可复用线程,减少线程创建和销毁时间。
NIO通信模型,通过通道(Channel)进行读写,通过选择器使用单个线程处理多个通道。
java的NIO有缓冲区Buffer,通道Channel,多路复用器Selector。
底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%。
进行读写操作时,只须直接调用api的read或write方法即可。一个有效请求对应一个线程,客户端的IO请求都是OS先完成了再通知服务器应用去启动线程进行处理。
java7以后的AIO(NIO2)。