毕设学习
网卡会把接收到的数据写入内存, 网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来, 再通过网卡中断程序去处理数据
最low的就是在用户代码中自旋实现所有阻塞socket的监听。但是每次判断socket是否产生数据,都涉及到用户态到内核态的切换。
于是select改进:使用等待队列,让线程在没有资源时park(阻塞),当有数据到达时唤醒select线程,去处理socket。
同时监视多个socket的简单方法
select: 如果列表中的socket都没有数据,挂起进程,直到有一个socket收到数据,唤醒进程
缺点是:其一,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
其二,进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次
epoll
当文件描述符的内核缓冲区非空的时候,发出可读信号进行通知,当写缓冲区不满的时候,发出可写信号通知的机制。
同步 :Java 自己处理IO 读写
异步 :ava 将 IO 读写委托给OS 处理,需要将数据缓冲区地址和大小传给OS
阻塞和非阻塞:针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,
阻塞方式下, 读取或者写入函数将一直等待
非阻塞方式下,读取或者写入方法会立即返回一个状态值。
Blocking IO - BIO 同步阻塞
NoneBlocking IO - NIO 同步非阻塞
IO multiplexing - IO多路复用
signal driven IO - 信号驱动IO
asynchronous IO - AIO 异步非阻塞
事件
可读事件,当文件描述符关联的内核读缓冲区可读,则触发可读事件。 (可读:内核缓冲区非空,有数据可以读取)
可写事件,当文件描述符关联的内核写缓冲区可写,则触发可写事件。 (可写:内核缓冲区不满,有空闲空间可以写入)
通知机制的反面,就是轮询机制。
基本用法
//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)
// recv是个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行。
// epoll用法
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int epfd = epoll_create(...);
// 进程调用epoll_create方法时,内核会创建一个eventpoll对象(也就是程序中epfd所代表的对象)。eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
// 创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket
while(1){
int n = epoll_wait(...)
for(接收到数据的socket){
//处理
}
}
| 系统调用 | select | poll | epoll |
|---|---|---|---|
| 底层实现 | 数组 | 链表 | 红黑树 |
| 事件集合 | 用户通过3个参数分别传入感兴趣的可读、可写及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数。 | 统一处理所有事件类型,因此只需一个事件集参数。用户通过 pollfd.events 传入感兴趣的事件,内核通过修改 pollfd.revents 反馈其中就绪的事件。 |
内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用 epoll_wait 时,无须反复传入用户感兴趣的事件。epoll_wait 系统调用的参数 events 仅用来反馈就绪的事件。 |
| 应用程序索引就绪文件描述符的时间复杂度 | O(n) | O(n) | O(1) |
| 最大支持文件描述符数 | 一般有最大值限制 | 65,535 | 65,535 |
| 工作模式 | LT(水平触发) | LT | 支持ET(边沿触发)高效模式 |
| 内核实现和工作效率 | 采用轮询方式来检测就绪事件,算法时间复杂度为 O(n) | 采用轮询方式来检测就绪事件,算法时间复杂度为 O(n) | 采用回调方式来检测就绪事件,算法时间复杂度为 O(1) |
| fd拷贝 | 每次调用时,都需要把fd集合从用户态拷贝到内核态 | 调用时, 拷贝进内核并保存, 之后每次epoll_wait 不拷贝 |
BIO
首先在服务端启动一个ServerSocket来监听网络请求,客户端启动Socket发起网络请求,默认情况下ServerSocket回建立一个线程来处理此请求,如果服务端没有线程可用,客户端则会阻塞等待或遭到拒绝。
且建立好的连接,在通讯过程中,是同步的。在并发处理效率上比较低。大致结构如下:
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO
基于事件驱动思想, 解决的是BIO的大并发, 基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。
NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。
在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题
AIO
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel
但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。
Reactor
参考
https://blog.csdn.net/Long_xu/article/details/150620611
http://blog.chinaunix.net/uid-28541347-id-4273856.html
https://blog.csdn.net/JMW1407/article/details/107939268

浙公网安备 33010602011771号