FileOutputStream vs BufferedxxxStream/ByteArrayxxxStream
FileOutputStream 每次都向内核调一次syscall和write(byte[]),将二进制流写入操作系统内核的page cache。不提供flush(),只有close。
BufferedxxxStream,buffer缓存区是jvm层面的东西,默认开辟了8kb的数组。每次write先写入缓存区,等这8kb写满了才调用一次内核syscall和write(byte[]),将二进制流写入内核的page cache。提供flush,手动将数据刷入内核。
- 减少了应用程序和内核的IO次数,即减少系统调用;
- 减少了用户态和内核态切换的次数。
要使用选择器(Selector),需要创建一个Selector实例(使用静态工厂方法open())并将其注册(register)到想要监控的信道上(注意,这要通过channel的方法实现,而不是使用selector的方法)。最后,调用选择器的select()方法。该方法会阻塞等待,直到有一个或更多的信道准备好了I/O操作或等待超时。select()方法将返回可进行I/O操作的信道数量。现在,在一个单独的线程中,通过调用select()方法就能检查多个信道是否准备好进行I/O操作。如果经过一段时间后仍然没有信道准备好,select()方法就会返回0,并允许程序继续执行其他任务。
阻塞式 I/O BIO
socket系统调用函数:
客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。
recvfrom和sendto除了支持TCP,还支持UDP。recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。sendto是与recvfrom对应的发送Socket请求函数。
ssize_t read(int fd, void *buf, size_t count);
成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0。
ssize_t write(int fd, const void *buf, size_t count);
如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。
int recv( SOCKET s, char FAR *buf, int len, int flags );
int send( SOCKET s, const char FAR *buf, int len, int flags );
int recvfrom(SOCKET s,void *buf,int len,unsigned int flags, struct sockaddr *from,int *fromlen);
参数:
s: 标识一个已连接套接口的描述字。
buf: 接收数据缓冲区。
len: 缓冲区长度。
flags: 调用操作方式。
from: (可选)指针,指向装有源地址的缓冲区。
fromlen:(可选)指针,指向from缓冲区长度值。
若无错误发生,recvfrom()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
int sendto (IN SOCKET s, IN const char FAR * buf, IN int len, IN int flags, IN const struct sockaddr FAR *to, IN int tolen);
参数:
s 套接字
buff 待发送数据的缓冲区
size 缓冲区长度
Flags 调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式
addr (可选)指针,指向目的套接字的地址
len addr 所指地址的长度
返回值为整型,如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR。
(2条消息) recv,write,send,read,recvfrom,sendto区别,详解_qq_31833457的博客-优快云博客_write 和send
read()函数使数据从磁盘拷到页缓存,再复制到应用程序缓存区。
send()函数使数据从应用程序缓冲区复制到Socket缓冲区,然后再发给网卡。
(2条消息) 使用零拷贝进行高效数据传输_cblstc的博客-优快云博客_java sendfile
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。处理多个客户端请求,就必须使用多线程(主要原因是 socket.accept()
、 socket.read()
、 socket.write()
涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。
应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。
应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。
而这里BIO的问题主要是两个:
第一,线程占用内存。创建线程是要分配给每个线程对应独立的内存空间的,很占资源,而且如果这个连接不做任何事情会造成不必要的开销。
第二,线程的创建、切换都会消耗CPU资源。多个线程cpu在执行时会给每个线程分配时间去调度执行他们,如果线程很多,则cpu会有很多时间都浪费在了线程之间调度切换,切换也不是很简单的操作,其中包含了当前线程挂起,线程的执行场景保留和下一个线程的执行状态恢复等操作。
¶ 非阻塞式 I/O
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。再服务端轮询,每次轮询都要执行系统调用。
NIO一个线程就干了接受客户端连接和读取客户端数据的工作,解决了BIO中的线程内存浪费和cpu调度消耗的问题。NIO的优势就是解决了客户端连接多线程的问题,那么NIO有哪些弊端呢,C10K问题(client 有10K个),假如你有一万个客户端连接,每次你去读取客户端数据都要向内核进行recv(读数据)的系统调用,但是假如此时此刻发来数据的客户端只有一个,那剩下9999次的调用都将会是无效的。
由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。
¶ I/O 复用
C10K的主要问题在于for循环调用了一万次系统调用,如果我们可以降低循环的次数,减少对应的系统调用,那性能将大大提升,好比说我们在一万次的循环之前,访问某一个系统调用,将一万个客户端连接描述传递给内核,让他给我们返回到底有几个客户端发来了数据,在这之后我们就可以只遍历内核返回回来的真正有数据传达了的客户端,假如有三个客户端传来了数据,加上我们循环之前访问内核的某一个系统调用,总共有只四次系统调用,而只循环了三次,是不是大大提高了效率。
我们可以将这一万个客户端理解为一万个数据通路,这多个数据通路都同时使用了某一个系统调用完成了数据传输状态的确认,我们将这个过程称之为多路复用,使用到的某一个系统调用称之为多路复用器。
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。
如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。并且相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。
多路复用器也是随着内核系统调用发展的产物,是内核为我们提供的可以管理多个数据通路的系统调用方法。上边三个版本多路复用器大致又可以分为两类:
一类:select,poll
二类:epoll
int select(int maxfpd1, fdset *read_fds, fdset *write_fds, fdset *exception_fds, struct timeval *restrict tvpr);//返回值为可以操作的文件描述符的数量。
int类型的参数nfds就表示的是文件描述符的个数,这里具体表示的就是多少个客户端连接需要询问内核(C10K);
中间三个参数是三个文件描述符的集合,可读集合,可写集合和异常集合(注意是集合,所以每次系统调用都要把所有文件描述符都传递给内核)。
最后参数timeval是请求时间超时的限制。
这里select多路复用器的作用就是替代了之前循环遍历客户端的过程,用户端时间复杂度从一万减少到了一,但是其实在将一万个文件描述符传递给内核后,内核还是要遍历,不过内核内部的自行遍历相比于用户循环一万次做系统调用,要快的多(因为用户态到内核态有保护模式,系统调用执行间还有好长的一段路要走)。
多路复用器poll,select和poll是一类的,区别在于select有一个源代码的1024限制(不同版本代码可能限制数不一样),一个select最多同时监视1024个文件描述符(可以理解为一个select同时最多管理1024个客户端连接),而poll是没有显示的限制的,是随着操作系统底层配置来实现限制的。
那么一类的多路复用器select和poll有哪些问题呢,也可以总结为两个
第一个:每次会重复传递文件描述符,每次系统调用都传一万个过去,循环多了,资源空间上也很浪费
第二个:多路复用器select和poll都要全量遍历文件描述符,有没有什么方法可以减少遍历次数呢?或者说可不可以不用再主动遍历文件描述符了呢(无论是程序遍历还是内核遍历)?
EPOLL可以解决,这也是为什么现在用的比较多的同步IO模型的多路复用器是epoll,那么如何解决上边两个问题呢,
第一个:让内核开辟一块内存空间,来保留文件描述符,不用重复传递了
第二个:属于计组的知识,使用中断,callback回调等来实现被动获取文件描述符的状态,不再主动遍历所有文件描述符(连接信息)。
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_create时内核开辟一块内存空间,由红黑树和链表组成。
红黑树用来存放全部已经连接的客户端文件描述符,链表存放有了对应读取状态的客户端文件描述符。我们的程序现在不用循环遍历客户端连接了,只需要从链表获取对应的客户端就可以了,内核负责监视红黑树中文件描述符的发生的事件,当监听的事件发生时,内核就会使用回调机制callback,就将客户端文件描述符从红黑树拷贝到链表,再由程序轮询获得。此时就是通过内核牺牲部分内存空间来换取时间。
¶ 信号驱动 I/O
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
或者网友提供的
¶ 异步 I/O
在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。
进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
异步IO模型使用了Proactor设计模式实现了这一机制。
图6 Proactor设计模式
如图6,Proactor模式和Reactor模式在结构上比较相似,不过在用户(Client)使用方式上差别较大。Reactor模式中,用户线程通过向Reactor对象注册感兴趣的事件监听,然后事件触发时调用事件处理函数。而Proactor模式中,用户线程将AsynchronousOperation(读/写等)、Proactor以及操作完成时的CompletionHandler注册到AsynchronousOperationProcessor。AsynchronousOperationProcessor使用Facade模式提供了一组异步操作API(读/写等)供用户使用,当用户线程调用异步API后,便继续执行自己的任务。AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步。当异步IO操作完成时,AsynchronousOperationProcessor将用户线程与AsynchronousOperation一起注册的Proactor和CompletionHandler取出,然后将CompletionHandler与IO操作的结果数据一起转发给Proactor,Proactor负责回调每一个异步操作的事件完成处理函数handle_event。虽然Proactor模式中每个异步操作都可以绑定一个Proactor对象,但是一般在操作系统中,Proactor被实现为Singleton模式,以便于集中化分发操作完成事件。
类型 | BIO | NIO | 多路复用 | |||
select,poll | epoll | |||||
特点 | 一个连接一个线程 | NIO一个线程就干了接受客户端连接和读取客户端数据的工作,解决了BIO中的线程内存浪费和cpu调度消耗的问题 | 一个线程去监听多个事件,当监听到某种状态时,比如可读,才进行数据传输 | 遍历时不用执行系统调用,在内核遍历文件描述符 | 让内核开辟一块内存空间,来保留文件描述符,不用重复传递了;callback回调等来实现被动获取文件描述符的状态,不再主动遍历 | |
缺点 | 1)线程占用内存 2)线程的创建、切换都会消耗CPU资源 | 轮询执行系统调用,有C10K问题,效率较低。 | 1)select最多同时监视1024个文件描述符 2)每次会重复传递文件描述符 3)全量遍历文件描述符 | |||
¶ I/O 模型比较
¶ 同步 I/O 与异步 I/O
- 同步 I/O: 应用进程在调用 recvfrom 操作时会阻塞。
- 异步 I/O: 不会阻塞。
阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O,虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞,但是在之后的将数据从内核复制到应用进程这个操作会阻塞。
¶ 五大 I/O 模型比较
前四种 I/O 模型的主要区别在于第一个阶段,而第二个阶段是一样的: 将数据从内核复制到应用进程过程中,应用进程会被阻塞。
¶ IO多路复用
¶ IO多路复用工作模式
epoll 的描述符事件有两种触发模式: LT(level trigger)和 ET(edge trigger)。
¶ 1. LT 模式
当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
¶ 2. ET 模式
和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。
很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
Select Poll Epoll
很容易产生一种错觉认为只要用 epoll 就可以了,select 和 poll 都已经过时了,其实它们都有各自的使用场景。
¶ 1. select
应用场景:select 的 timeout 参数精度为 1ns,而 poll 和 epoll 为 1ms,因此 select 更加适用于实时要求更高的场景,比如核反应堆的控制。
select 可移植性更好,几乎被所有主流平台所支持。
¶ 2. poll
应用场景:poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且epoll 的描述符存储在内核,不容易调试。
¶ 3. epoll
应用场景:只需要运行在 Linux 平台上,并且有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。