加载中

毕设学习

网卡会把接收到的数据写入内存, 网卡向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

posted @ 2026-01-16 10:24  Hinton07  阅读(5)  评论(0)    收藏  举报