IO多路复用
- 如果用JUC的方式来类比理解一下,BIO是相当于整个APP都加了synchronied,NIO相当于是使用自旋的方式。
Linux操作系统中断
中断的分类
- 内中断:陷入(系统调用),故障(缺页中断),终止(程序BUG)
- 外中断: I/O中断请求(设备准备就绪),人工干预(Ctrl + C);
系统中断,内核做什么事情
内核态 - 执行系统调用
Socket
Socket基础
Socket读写缓冲区机制
用户态和内核态
- Socket在底层采用TCP/IP协议,因此在数据传输的过程中是可以保证可靠性的。
- APP处于用户态,是无法处理TCP/IP协议的,必须要将其所需要传输的数据通过系统调用,传输到操作系统的内核空间中。
- 当操作系统处于内核态时,可以通过底层的方法,建立Socket连接,来传输数据。
缓冲区
- APP在用户空间中也有缓冲区,操作系统在内核空间中也有缓冲区。
- 内核空间的缓冲区是用来接收/发送TCP/IP传输过程中的数据缓冲。
- 用户空间的缓冲区是用来读取/写入内核缓冲区中的数据。
- 内核缓冲区中分成了输入缓冲区,和输出缓冲区。
写
BIO
- 如果一个APP写入输入缓冲区时,会对输出缓冲区加锁,同一时间仅仅能有该APP执行输入输出操作,如果输出缓冲区不足,也会加锁,该APP等待数据传输完成,再写入。其他APP必须等待该APP完成才能将自己的数据写入缓冲区。
- app
NIO
- 如果缓冲区有剩余,则APP中的数据写入缓冲区,如果没写完,则返回写到的数据位置。
- 如果缓冲区没有剩余,或者正在进行TCP/IP的数据传输,则返回-1,表示该次写入失败。
读
BIO
- 将内核的数据缓冲区写到APP的用户数据缓冲区,如果没写完就会阻塞,直到所有需要的数据都写入了缓冲区才结束。
NIO
- NIO不会堵塞,采用轮询的方式,持续查询缓冲区中是否有数据,有数据就直接写入用户缓冲区中,没有就直接返回。
内核缓冲区满的情况
BIO
- TCP会不断尝试去写,上层APP也会被挂起,直到等到缓冲区有位置可以写,APP会写入数据到内核缓冲区。
NIO
- NIO会不断尝试去写,如果缓冲区数据满了就会返回-1,表示此次写失败。
系统调用,用户态<—>内核态
为什么要有这两种状态
- 有些功能是仅仅操作系统才能使用,例如系统调用。
- 同时用户态有用户堆栈,内核态下有内核堆栈。
什么时候会切换到内核态
- 当发生中断的时候会切换到内核态
- 内中断:陷入(系统调用:例如申请外部I/O资源),故障(缺页中断),终止(系统bug)
- 外中断:外部I/O设备准备就绪,强行干预(Ctrl + C)
状态切换时,要做什么事情
- 保护用户堆栈的寄存器的现场。
- 同时执行内核堆栈的程序。
- 在中断执行之后,会重现现场,继续执行用户堆栈的程序。
具体流程
- 用户态
- 查找自己需要调用哪一个系统资源。
- 将用户参数变量保存到操作系统的寄存器内。
- 保存进程的上下文信息
- 切换到进程内核栈
- 根据映射表调用System_call函数
- 内核态
- 取到指定的寄存器的号码。
- 从寄存器中取出参数变量。
- 将需要的数据从用户态拷贝到内核态
- 执行函数
- 将结果从内核空间拷贝到寄存器中
- 用户态
- 回复现场
- 读取寄存器中的数值到用户空间中
- 继续程序
BIO通信底层原理
- 刘老师的原理图
- 我认为BIO就是一个线程仅仅关注自己的那个端口号,如果自己想要接收信息的客户端没有发送过来信息,就阻塞挂起,直到有信息发过来,被唤醒,获取数据。
- 而NIO就是一个线程一次性关注多个端口号,通过一定的手段识别那个端口号的数据对应哪个服务器的端口,再将对应端口号的数据发布出去。
select多路复用
- 刘老师的流程图
- 在Linux中,我们可以使用select函数实现I/O端口的复用,传递给select函数的参数会告诉内核:
- 我们所关心的文件描述符((读)readset、(写)writeset、(异常)exceptset )
- 对每个描述符,我们所关心的状态。
(对于内核态的接收到位图来说,0表示没有对应端口需要操作,1表示有对应端口需要操作)。 (对于用户态的接收位图来说,0表示对应缓冲区没有数据,1表示对应缓冲区有数据)。 - 我们要等待多长时间。(timeout)
- 从select函数返回后,内核告诉我们一下信息:
- 对我们的要求已经做好准备的描述符的个数
- 对于三种条件哪些描述符已经做好准备.(读,写,异常)
有了这些返回信息,我们可以调用合适的I/O函数(通常是read或write),并且这些函数不会再阻塞. - maxfdp:bitmap表示的最大有效位,指定从0到n的位图标识位是有效的。
- select()问题:
- 内核态和用户态之间需要频繁的拷贝fd_set
- 重新置位(因为用户态的rset 和 内核态的rset 是不一样的,每次进行select 都会导致rset 不同 因此不能复用)
- fd_set位图的数目是有限制的,最大是1024
- 都要从索引0开始遍历 有很多位置都是0 会浪费时间。
epoll多路复用
- 函数比较复杂,就把图贴着这里,这里挑重要的写。
- 刘老师的流程图
- epoll维护了两个空间,一个是epoll的监听事件空间,另一个是epoll_wait的等待队列。
- epoll监听空间是通过红黑树结构来维护的,方便查找对应的epfd。
epoll_create()
- 这个函数表示开启size个epoll结构空间,返回epoll文件描述符编号。
- 直接创建到内核空间的epoll中。
epoll_ctl()对epoll监听列表中的epoll进行修改
- epfd: epoll结构的进程fd编号,函数将依靠该编号找到对应的epol结构。
- op:表示当前请求类型,增加、减少、修改epoll结构中的结构,由三个宏定义(
EPOLL_CTL_ ADD :注册新的fd到epfd中)、(EPOLL_CTL_MOD :修改已经注册的fd的监听事件)、(EPOLL_CTL_DEL :从epfd中删除一个fd) - fd,需要监听的文件描述符,需要监听的端口号,一般指
socket _fd , - event,告诉内核对该fd资源感兴趣的事件(输入,输出等)
epoll_wait()对epoll就绪队列进行修改
- epfd: epoll结构的进程fd编号,目前线程感兴趣的epoll编号。
- *events, 是一个指针,必须指向一个epoll_event结构数组, 当函数返回时,内核会把就绪状态的数据拷贝到该数组中!
- maxevent,标明参数二epoll_event数组最多能接收的数据量,即本次操作最多能获取多少就绪数据。
- timeout,为0则表示立刻返回不阻塞,为+n则表示阻塞n秒,为-1则表示一直阻塞,直到接收到数据。
epoll相对select的优点
- epoll的大小可以很大,不被1024限制。
- 用户直接在内核空间中注册epoll空间
- epoll_wait中的都是已经操作过的epoll,无用的epoll不会进入epoll_wait中,也不会被返回。select中用0来表示没有被操作,这样节省了遍历的时间。
- epoll_wait返回的是接收数据的epoll数目;nfds
|