java网络编程—IO及相关原理(上下文切换\多路复用\Reactor\epoll)

版权声明:坚持原创,坚持深入原理,未经博主允许不得转载!! https://blog.csdn.net/lemon89/article/details/78290389

相关文章
java网络编程—NIO与Netty(四)
java网络编程—NIO与Netty(三)
java网络编程—NIO与Netty(二)
java网络编程—NIO与Netty(一)
java网络编程—基石:五种IO模型及原理(多路复用\epoll)

理解Netty首先要理解NIO,理解NIO首先要理解reactor模型、多路复用select\poll\epoll等等原理。所以我尝试按照这个来详细讲解网络编程的来龙去脉。

基本概念

文件描述符(File descriptor)

是一个用于表述在操作系统中,指向一个文件(文件可理解为信息载体,包括socket)的引用指针的抽象化概念。

对一个 socket 的读写也会有相应的描述符,称为 socketfd(socket 描述符)

文件描述符形式上是一个非负整数。实际上,它是一个索引值,指向内核中为每个进程所维护的该进程打开文件的记录表。

当程序打开一个文件或者新创建一个文件,内核就会向进程返回一个对应的文件描述符(fd)。这个概念只适用于unix、linux操作系统。

上下文

当一个进程在执行时,CPU 的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文

用户态(用户空间)与核心态(内核空间)

Linux从整体上分为内核态与用户态。
比如一个32位的操作系统,寻址地址(虚拟内存空间)是2的32次方,也就是4G。操作系统将较高的1G字节作为内核空间,而将较低的3G字节作为用户空间。内核空间具有用户空间所不具备的操作权限。

这里写图片描述

内核态就是内核所处的空间,内核负责调用底层硬件资源,并为上层应用程序提供运行环境。用户态即应用程序的活动空间。

应用程序通常运行在用户空间,当某些操作需要内核权限时,通过系统调用(System calls),进入内核态执行。这也就是一次用户态\内核态的转换。

这里写图片描述

应用程序的运行必须依托于内核提供的硬件资源(如cpu\存储\IO),内核通过暴露外部接口供应用程序调用——称为“SystemCall,系统调用”。系统调用是操作系统的最小功能单位。每次系统调用都会发生一次用户态与内核态的切换,切换过程中涉及了各种函数的调用以及数据的复制。
liunx权限升降
用户态vs核心态
最终通过内核态、用户态的划分与协助,保证了操作系统的安全性、稳定性。

多路复用

多路复用通过linux的select\poll\epoll模型实现的,但它们本质上都是同步 IO。

IO 多路复用通过把多个 IO 阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下,可以同时处理多个 client 请求

select\poll\epoll

select\poll\epoll它们均属于实现多路复用的SystemCall(系统调用)。

select

select可以同时监听多个fd,调用select后产生阻塞当其中一个fd处于就绪状态便从阻塞中返回,然后遍历fdset(被监听的fd集合),找出就绪的fd
这里写图片描述

select缺点

int select(int numfd, fd_set * readfds, fd_set * writefds,

           fd_set * exceptfds, struct timeval * tv);
  • 每次调用select,都需要将fdset从用户空间复制到和内核空间
  • 每次select返回后,需要遍历整个fdset以便找出就绪的fd。select执行效率会随着fdset的增大而线性下降。
  • select对监听的fd有限制,默认是1024.

poll

int poll(struct pollfd *ufds, unsigned int nfds, int timeout);

poll与select类似,唯一区别是没有使用fdset而是pollset,对fd的个数没有限制。但是随着fd个数增大,一样会产生性能问题。

epoll
jdk1.5之后使用epoll代替了传统的select/poll,极大提升了NIO通信的新能。

//注册监听事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event), 
            typedef union epoll_data {
                void *ptr;
                int fd;
                __uint32_t u32;
                __uint64_t u64;
            } epoll_data_t;

            struct epoll_event {
                __uint32_t events;      /* Epoll events */
                epoll_data_t data;      /* User data variable */
            };
//是等待事件的产生
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

对select\poll的改进(区别)

  • 不需要将fd从用户态复制到核心态,使用MMAP共享内存映射实现

Linux 提供的 mmap 系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后,
用户对这段内存区域的修改可以直接反映到内核空间; 同样地, 内核空间对这段区域的修改也直接反映用户空间. 正因为有这样的映射关系,
我们就不需要在 用户态(User-space) 与 内核态(Kernel-space) 之间拷贝数据, 提高了数据传输的效率.

  • 不需要遍历所有的fd会给每个监听的fd增加一个回调函数当就绪后回调将fd放入就绪列表中,只需要从就绪列表中获取就绪的fd即可。

  • 没有fd限制,1g内存可以处理10w个fd

epoll的LT(level trigger)与ET(edge trigger)模式

  • LT epoll_wait返回后会通知应用程序,如果应用程序没有应答,下次调用epoll_wait返回后会再次通知
  • ET epoll_wait返回后会通知应用程序,如果应用程序没有应答,下次调用epoll_wait返回后不会再次通知

阻塞与非阻塞 同步与异步 五种IO模型

同步和异步关注的是消息通信机制
同步:发出一个调用时,等待直到,调用结果的完成后返回。
异步:发送一个调用后,立刻返回,不需要等待调用结果。通过通知机制或者回调函数通知。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞:发出调用后,当前线程被挂起,直到结果返回。
非阻塞:发出调用后,当前线程继续保持运行状态。

总之,同步异步要看是否需要等待操作执行的结构。阻塞非阻塞指请求后是否能够理解返回保持Running的状态。

而在IO中,一个完整的IO实际分为如下两个部分

1. 等待数据准备就绪
2. 将数据从内核拷贝到用户空间。
(准确来说:数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间)

对于阻塞IO\非阻塞IO主要针对的是阶段1(等待数据准备就绪):

  • 阻塞IO表示线程挂起直到准备就绪;
  • 非阻塞IO表示发现准备未就绪立即返回(通常多次去检查)。

对于同步IO\异步IO主要针对的是阶段2:

  • 同步IO表示执行数据传输直到全部完成后才返回;
  • 异步IO表示执行数据传输的指令发出后立刻返回不必关注结果是否完成。

这里写图片描述

BIO-阻塞同步IO

这里写图片描述

可以看到,等待数据准备就绪是阻塞等待的,另外将数据从内核拷贝到用户空间 也是阻塞等待的。所以BIO是阻塞同步IO

NIO-非阻塞同步IO

这里写图片描述
可以看到,等待数据准备就绪是非阻塞等待的,另外将数据从内核拷贝到用户空间 也是阻塞等待的。所以NIO是非阻塞同步IO

NIO的补充版-多路复用NIO

这里写图片描述

多路复用的NIO作为NIO的补充,它允许通过一个线程持有的selector对象管理多个Socket连接(channel),并通过这一个Selector去轮序哪些Socket准备就绪。

这样便可以避免每个线程持有一个Socket,并且这个线程不停轮序自己的Socket是否准备就绪的操作。

最终实现了一个线程管理多个Socket,解决了高并发的线程问题。

AIO-非阻塞异步IO

这里写图片描述
完全非阻塞的IO。
如图应用程序发送了一个读操作后立即返回,当一些完成,操作系统通知应用程序。
内核在I/O操作完成后再通知应用进程操作结果。
内核是通过向应用程序发送 signal 或执行一个基于线程的回调函数来完成这次 IO 处理过程,告诉用户 read 操作已经完成

这里写图片描述


Reactor

Netty系列之Netty线程模型

猜你喜欢

转载自blog.csdn.net/lemon89/article/details/78290389