网络IO的本质
网络IO的本质就是socket流的读取,通常一次IO读取会涉及两个阶段与两个对象,其中两个对象为:用户进程(线程)Process(Thread)、内核对象(kernel),两个阶段为:等待流数据准备阶段、从内核向进程复制数据阶段。
对于socket而言,第一步通常等待网络上的数据分组到达,然后被复制到内核的某个缓冲区,第二步数据从内核的缓冲区复制到应用进程的缓冲区。
网络IO模型
I/O模型可细分为五种类型:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO、异步IO。
阻塞IO模型
首先,在linux系统中默认所有的IO都是阻塞IO。
阻塞IO的特点是从kernel读取数据时信号并未立刻返回,而是等待数据到达完毕或发生错误才会返回结果,这个过程是阻塞的。
术语描述:当用户进程调用recvfrom这个系统调用时,kernel就开始等待数据到来,而进程这边会处于阻塞状态。当kernel将数据准备好后,就会将数据拷贝到用户进程的缓冲区,然后kernel返回结果,用户进程才会解除block状态,重新运行起来。
非阻塞IO模型
与阻塞IO不同,当用户进程发出recvfrom调用时,如果kernel中数据还没有准备好,那么并不会block用户进程,而是会返回error错误。相对于用户进程而言,每次发送读取操作后,并不需要等待,而是会立刻返回结果,当收到error时,就知道数据未准备完毕,然后继续发送读取操作,直到可以直接读取数据到缓冲区为止。虽然在执行read请求操作时,用户进程并未阻塞,但是当recvfrom将数据从内核拷贝到进程时,用户进程处于阻塞状态。
多路复用IO模型
产生原因
在具有大量IO请求的场景下,需要应用进程创建多个线程去读取数据,每个线程都会调用recvfrom去读取数据。在这种高并发的情况下,可能进程需要创建成千上万个线程,增加服务器负荷,并且造成了严重的资源浪费。
因此,有了多路复用IO模型,使用一个线程去监听多个网络请求,即文件描述符,这样就实现了使用少量线程对大量请求进行监听,然后再让对应的线程进行数据读取。那么目前常用select、poll、epoll函数对fd文件描述符进行监听。
多路复用IO又称事件驱动IO,进程使用IO多路复用在两个阶段都是阻塞的状态,进程使用select函数,其中select函数有一个参数是文件描述符集合,对这些文件描述符进行监听,当文件描述符就绪时,会返回readable信号,然后用户进程调用recvfrom进行读取数据,由于可同时监听多个IO,效率比阻塞IO高。
select
进程调用select后会被阻塞,将需要监听的文件描述符放入fd_set,并将fd_set复制到内核空间,内核空间会对fd_set进行轮询遍历,若无mark值,则会暂时挂起等待超时时间之后继续轮询,直到有数据准备就绪。最后将fd_set复制回用户进程,进行读/写操作。
复杂度O(n)
select的缺点:
1.select监控数量受限
select能监控的fd数量有上限,32位系统一般为1024,64位系统为2048,这个上限可以通过修改参数提高,但是相应的会损失性能。
2.轮询效率低
对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
3.频繁拷贝复杂,开销大
需要维护一个用来存放大量fd的数据结构,用户空间需要维护一个fd_set,fd_set的每一位都表示一个文件描述符,开始时会将其发给内核,这会使得用户空间和内核空间在传递该结构时复制开销大。
4.select是水平触发
应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作。那么之后select调用还是会将这些文件描述符返回,通知进程。
poll
poll和select基本是一样的,但是它对fd集合做了优化,使用链表存储,解决了连接数上限的问题。
epoll
epoll的实现与上述两种方式完全不同,因此不会造成上述的问题。
同select、poll不同,复杂度O(1),通过三个函数实现流程:
1.epoll_create: 创建一个epoll文件描述符集合,同时底层创建一个红黑树和就绪链表,红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据。
2.epoll_ctl: 用于添加新的描述符,首先判断红黑树中是否存在,如果不存在,插入数据,并告知内核注册回调函数(当文件描述就绪时通过网卡驱动触发),数据就绪后将事件添加到就绪队列中。
3.epoll_wait: 检查链表,并将数据拷贝到用户空间(两者维护的是片共享内存),最后清空链表。其中epoll的工作方式分为LT、ET。
注意:epoll是线程安全的,但是当一个线程调用epollwait,而另一个线程用epollctl向同一个epoll_fd添加一个监测fd后,epollwait有可能被改fd的读/写事件唤醒。
信号驱动IO模型
首先应用进程通过sigaction系统调用安装一个信号处理函数(在内核位置),该系统调用立即返回,进程继续工作(未被阻塞)。当数据准备就绪时,kernel就会为该进程产生一个SIGIO信号,随后用户进程就可以使用recvfrom调用去读取数据到内存,并返回成功指示。
异步IO模型
真正意义上的非阻塞IO,当用户进程发出aio_read操作之后,就立刻可以去做其它的事。另一方面,当kernel的角度,当kernel收到read信号后,会立刻返回结果,所以不会对用户进程block。之后,kernel会等待数据准备完毕,然后将数据拷贝到内存中,完成之后,kernel会给用户进程发送signal信号,表示数据已经read操作完毕。整个过程无阻塞状态的发生。
总结
1.阻塞IO、多路复用IO是两个阶段都处于阻塞状态,非阻塞IO、信号驱动IO是在第二阶段从kernel读取数据时处于阻塞状态,异步IO整个过程都未处于阻塞状态。
2.按照阻塞程度排序:阻塞IO > 非阻塞IO > 多路复用IO > 信号驱动IO > 异步IO ,并且效率由低到高。
3.异步与同步的区别:
在IO模型里面如果请求方从发起请求到数据最后完成的这一段过程中都需要自己参与,那么这种我们称为同步请求;反之,如果应用发送完指令后就不再参与过程了,只需要等待最终完成结果的通知,那么这就属于异步。
4.阻塞与非阻塞的区别:
阻塞就是发起读取数据请求的时,当数据还没准备就绪的时候,这时请求是即刻返回,还是在这里等待数据的就绪,如果需要等待的话就是阻塞,反之如果即刻返回就是非阻塞。
IO流
几种类型的IO流、区别
字节流:大多数情况我们都是使用字节流,因为大多数情况都是操作磁盘文件,所以大多数都是用的字节流的方式
字符流:如果频繁处理内存中字符串的情况,我们采用字符流,因为它具备缓冲区,提高了性能
读取大文件流的选择
字节流时,选择BufferedInputStream和BufferedOutputStream。
字符流时,选择BufferedReader和BufferedWriter
buffer读写操作过程