Nginx原理

Nginx长连接

在Nginx中默认是开启了长连接的,我们都知道nginx有正向代理和反向代理的,而nginx的配置文件中有两个参数,一个是工作进程参数一个是线程池的配置,这两个参数就是可以确定nginx的并发能够达到多少
在这里插入图片描述
所以nginx的并发是通过work_processes * worker-connections得出的,但是nginx是一个多进程单线程运行的,在nginx中有一个master进程,来管理worker进程,所以nginx是一个多进程单线程的非阻塞模型,在events中配置的worker_connections就表示每个worker进程的连接池配置是1024,这里配置的是默认的1024,所以nginx的并发数就是这两个相乘;但是我们都知道nginx是可以做正向代理和方向代理的,那么如果我们不考虑nginx做反向代理,只考虑nginx做正向代理,也就是说客户端和nginx直接交互,而客户端的请求就最多只能达到nginx层面,nginx也不用去请求后端的任何服务,那么nginx的并发就是工作进程数 * 每个进程的线程数,但是我们都知道nginx是需要做反向代理,与后端进行交互的,简单来说就是客户端发起一个请求,nginx接受到这个请求,然后匹配location,然后在反向代理到后端的服务,最后返回结果,这个过程中是由nginx的某一个进程去处理的,而这个进程从线程池中取出一个请求给这个客户端连接,同样的,如果需要转发到后台,那么还是需要从这个进程中的线程池中取出一个连接去处理后端请求,在整个过程中nginx的角色就是接受请求,转发请求,是一个异步的过程,nginx是多进程+单线程的模式,所以nginx是一个非阻塞的线程模型;所以nginx的并发数应该是worker_processes * woker_connections/2。

HTTP

http是个啥,可能很多人都会问整个问题,都在用http连接,http协议,http 长短 顶顶烦烦烦烦烦烦·连接,其实http只是一种请求响应的规范而已,它不是一个连接,它是属于应用层面的,只是一个约定而已,约定HTTP的格式而已,它的请求是一个无状态的,什么是无状态的,就是请求的前后大家都是无感知的,每个请求之间没有任何关系,以至于我们之前做web应用的时候,要保证回话的状态就使用了session机制来保存回话,所以http只是一个无状态的请求;那么HTTP1.1到底是一个长连接还是一个短连接呢?其实如果浏览器没有特殊的情况设置的话,那么默认都是一个长连接,你想下,如果说浏览器的HTTP1.1是一个短连接,比如你打开一个很大的页面,上面引用的js、css、img、后端的请求等如果每隔一都发起一个请求,那么不管是服务端还是客户端都会吃不消的,所以浏览器默认采用的是长连接,后端服务根据浏览器请求带过来的是长连接,那么只要你的请求没有超时或者服务端自己不要你使用长连接,那么在返回的head中也会带一个标识,标识下一次是否是长连接,但是长连接也有一个超时时间,在超时时间结束过后,会关闭这个链接,再次请求的时候会根据客户端的设置而决定是否也采用长连接,比如我们在浏览器中打开一个web页面,那么在web页面中可以看到:在这里插入图片描述
那么在请求头中设置了connection:keep-alive,而服务器端响应头里面也设置了一个connection:keep-alive,表示保持的是长连接,只有等到超时过后,这个链接才会被关闭,如果说服务器端返回的是connection:close,那么就代表这个链接已经被关闭,不使用长连接;因为我们都知道在短连接的过程中,交易处理完成过后了,连接会被断开,端口自动释放。
再回来说到nginx,nginx有接受客户端的请求和代理服务端的请求,那么这两个请求是不是都可以自行设置呢?
nginx默认采用的是长连接,并且默认设置了超时时间,nginx客户端的长连接配置只需要配置到http域里面就可以了
在这里插入图片描述
表示保持连接的时间是65s,如果在65s之内都没有请求过来,那么这个连接会被关闭,详细点的配置如下:

http {
    keepalive_timeout  120s 120s;
    keepalive_requests 10000;
}

keepalive_timeout:
第一个参数:设置keep-alive客户端连接在服务器端保持开启的超时值(默认65s);值为0会禁用keep-alive客户端连接;
第二个参数:可选、在响应的header域中设置一个值“Keep-Alive: timeout=time”;通常可以不用设置;
keepalive_requests:
设置一个keep-alive连接上可以服务的请求的最大数量,当最大请求数量达到时,连接被关闭。默认是100。
keep alive建立之后,nginx就会为这个连接设置一个计数器,记录这个keep alive的长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则nginx会强行关闭这个长连接,客户端重新建立新的长连接。
服务端长链接:默认短链接

http {
    upstream backend {
      server 192.168.0.1:8080 weight=1 max_fails=2 fail_timeout=30s;
      server 192.168.0.2:8080 weight=1 max_fails=2 fail_timeout=30s;
      keepalive 300; #连接池最大空闲连接数(相当于核心线程数)
    }   
    server {
        listen 8080 default_server;
        server_name "";
        location / {
            proxy_pass http://backend;
            proxy_http_version 1.1;# 设置http版本为1.1
            proxy_set_header Connection "";# 清理Connection,这样即便是 Client 和 Nginx 之间是短连接,Nginx 和 upstream 之间也是可以开启长连接,也可以通过传递“Connection: Keep-Alive”头
        }
    }
}

服务端的长连接配置也非常简单,就是在upstream中配置一个keepalive就可以了,数字表示核心的线程数

连接池配置

worker_processes  8;#定义工作进程数量
 
events {
        use epoll;
        worker_connections  2048;#定义工作进程的连接池大小
}

同一个进程,上游下游共用一个连接池

这里的上游和下游指的就是客户端的连接和服务器端的连接,并且在events可以制定使用epoll模型。

多进程模型

前面大概提了下,nginx是一个多进程模型的,是什么意思呢?我们都知道在java中,java是一个单进程多线程的模型,简单来说就是jvm是一个单进程的,在这个进程中采用的是多线程的运行模式,为什么java不能是一个多进程的模型呢?其实我们想一下,在jvm中有很多共享的资源,比如堆内存,这个是java中的共享区域,而如果设计成一个多进程模型,那么进程之间的通信的代价将变得非常昂贵,因为进程之间是相互隔离的,各自都有自己的内存区域,互相不干扰的,如果多进程模型,那么进程间的切换带来的开销是非常大的,而如果是一个单进程模型,那么在java中的多线程之间的通信就将变的非常容易,而且代价也非常低,比如static变量,volatile等等都是很轻松的可以通信;nginx中只有一个master进程,它用来管理下面的woker进程,其实真正工作的是woker进程,而master进程的工作就是启动的时候通过fork函数fork出来woker进程,然后对woker进程进行管理,而nginx的设计原理是多进程模型,所以它利用了多进程的一个特点就是每个进程绑定一个cpu,所以进程之间也就不会去抢占cpu而带来一些资源开销,比如服务器是16个CPU,那么woker进程就是开16个,那么每个woker进程有自己独立的内存区域,进程之间相互不影响,相互不抢占资源,不用进行进程间的切换,某一个woker进程挂掉了,其他woker进程不受影响,而这个时候master进程就会去管理woker进程,如果检测到woker进程挂掉,会去启动woker进程,所以nginx的多进程模型机制是一种相对于容错率非常高的一种服务解决方案;在单进程的模型下,比如jvm,如果说某一个线程由于自身原因或者是程序原因导致了oom异常,那么整个进程可能都会挂掉,也就带来了灾难,而且多线程之间的运行是需要通过cpu的线程上下文切换来达到执行的,cpu在某一时刻是只有一条指令在运行的,也就只有一个线程在运行,而多线程的上下文切换带来的开销也很大,由用户态到内核态之间的切换带来的开销是非常大的,所以某种业务功能是否采用多线程是需要进行谨慎分析的。
我们都知道进程是资源分配的最小单位,而线程是cpu调度的最小单位,在jvm中有一个概念是堆栈的概念,堆是对所有的线程共享的,而栈是线程私有的,比如多线程环境中,需要进行线程上下文切换,那么在栈上需要保存线程,然后切换回来的时候还需要恢复线程,而且还需要进行用户态与内核态的切换,这性能开销也就很大了,所以nginx的模型就采用了多进程单线模型,并且是非阻塞的,所以性能也就相对高很多。

Nginx 采用的是固定数量的多进程模型,由一个主进程和数量与主机 CPU 核数相同的工作进程协同处理各种事件。主进程负责工作进程的配置加载、启停等操作,工作进程负责处理具体请求
在这里插入图片描述
进程间的资源都是独立的,每个工作进程处理多个连接,每个连接由一个工作进程全权处理,不需要进行进程切换,也就不会产生由进程切换引起的资源消耗问题。默认配置下,工作进程的数量与主机 CPU 核数相同,充分利用 CPU 和进程的亲缘性(affinity)将工作进程与 CPU 绑定,从而最大限度地发挥多核 CPU 的处理能力。
Nginx 主进程负责监听外部控制信号,通过频道机制将相关信号操作传递给工作进程,多个工作进程间通过共享内存来共享数据和信息。
主进程(Master Process)主要完成如下工作:

  1. 读取并验正配置信息;
  2. 创建、绑定及关闭套接字;
  3. 启动、终止及维护worker进程的个数;
  4. 无须中止服务而重新配置工作特性;
  5. 控制非中断式程序升级,启用新的二进制程序并在需要时回滚至老版本;
  6. 重新打开日志文件,实现日志滚动;
  7. 编译嵌入式perl脚本.

工作进程(Worker Process)
1.接收、传入并处理来自客户端的连接;
2.提供反向代理及过滤功能;
3.nginx任何能完成的其它任务.
在这里插入图片描述

上图为nginx的多进程模型,上面已经说了nginx是一个多进程单线程的模型,woker进程与cpu绑定,这样就可以最大化的利用cpu的性能,免去cpu进行上下文切换带来的性能开销,当nginx启动的时候,master进程会创建woker进程,创建多少个worker进程是可以配置的,但是一般配置和 cpu的核心数一致,保证每个cpu绑定一个worker进程,然后在nginx架构中还有两个进程,缓存管理器和缓存加载器,其中缓存加载器是用来加载缓存文件的,就是nginx有对请求是有缓存的,缓存的默认是GET HEAD请求,当nginx启动的时候会加载缓存文件到共享内存中,而缓存管理器是用来管理缓存的,看缓存是否失效,如果缓存失效了就会移除缓存文件。

进程调度

当工作进程被创建时,每个工作进程都继承了主进程的监听套接字(socket),所以所有工作进程的事件监听列表中会共享相同的监听套接字。但是多个工作进程间同一时间内只能由一个工作进程接收网络连接,为使多个工作进程间能够协调工作,Nginx 的工作进程有如下几种调度方式。
无调度模式
惊群效应:连接事件被触发时会唤醒所有工作进程,与客户端建立连接,建立连接成功则开始处理客户端请求。所有进程都会争抢资源,但最终只有一个进程可以与客户端建立连接,对于系统而言这将在瞬间产生大量的资源消耗。
互斥锁模式(accept_mutex)
互斥锁是一种声明机制,每个工作进程都会周期性地争抢互斥锁,一旦某个工作进程抢到互斥锁,就表示其拥有接收 HTTP 建立连接事件的处理权,并将当前进程的 socket 监听注入事件引擎(如 epoll)中,接收外部的连接事件。
其他工作进程只能继续处理已经建立连接的读写事件,并周期性地轮询查看互斥锁的状态,只有互斥锁被释放后工作进程才可以抢占互斥锁,获取 HTTP 建立连接事件的处理权。当工作进程最大连接数的 1/8 与该进程可用连接(free_connection)的差大于或等于 1 时,则放弃本轮争抢互斥锁的机会,不再接收新的连接请求,只处理已建立连接的读写事件。
互斥锁模式有效地避免了惊群现象,对于大量 HTTP 的短连接,该机制有效避免了因工作进程争抢事件处理权而产生的资源消耗。但对于大量启用长连接方式的 HTTP 连接,互斥锁模式会将压力集中在少数工作进程上,进而因工作进程负载不均而导致 QPS 下降。
套接字分片(Socket Sharding)
套接字分片是由内核提供的一种分配机制,该机制允许每个工作进程都有一组相同的监听套接字。当有外部连接请求时,由内核决定哪个工作进程的套接字监听可以接收连接。这有效避免了惊群现象的发生,相比互斥锁机制提高了多核系统的性能。该功能需要在配置 listen 指令时启用 so_reuseport 参数。
Nginx 1.11.3 以后的版本中互斥锁模式默认是关闭的,由于 Nginx 的工作进程数量有限,且 Nginx 通常会在高并发场景下应用,很少有空闲的工作进程,所以惊群现象的影响不大。无调度模式因少了争抢互斥锁的处理,在高并发场景下可提高系统的响应能力。套接字分片模式则因为由 Linux 内核提供进程的调度机制,所以性能最好。

多进程和多线程

nginx使用多进程而不是多线程的原因:
多线程:
共享一块进程空间,存在并发问题需要问题
cpu在线程间的切换会消耗大量资源,
线程发生异常会影响整个进程
多进程+单线程:
进程之间空间独立不需要处理并发问题
进程与cpu绑定的机制,节省了cpu切换时间
健壮性:一个进程异常不会影响其他进程

缓存机制

Cache模块,主要由缓存加载(缓存索引重建Cache Loader)和缓存管理(缓存索引管理Cache Manager)两类进程完成工作。缓存索引重建进程是在Nginx服务启动一段时间之后(默认是1分钟)由主进程生成,在缓存元数据重建完成后就自动退出;缓存索引管理进程一般存在于主进程的整个生命周期,负责对缓存索引进行管理。
cache loader进程主要完成的任务包括:

  1. 检查缓存存储中的缓存对象;
  2. 使用缓存元数据建立内存数据库;
    cache manager进程的主要任务:
  3. 缓存的失效及过期检验;
    默认只对 GET 和 HEAD 方法的请求进行缓存,如果想对 POST 请求方法的数据进行缓存,则可以使用 proxy_cache_methods 指令进行设置。
    proxy_cache_methods GET HEAD POST;
    缓存加载进程在nginx启动时,从磁盘加载缓存文件,将缓存元数据(缓存索引)存储到共享内存,为避免缓存加载影响nginx整体性能,可以设置加载参数,按迭代进行加载
    proxy_cache_path /data/cache keys_zone=niyueling:10m loader_threshold=300 loader_files=200;
    loader_threshold - 迭代的持续时间,以毫秒为单位(默认为200)
    loader_files - 在一次迭代期间加载的最大项目数(默认为100)
    loader_sleeps - 迭代之间的延迟(以毫秒为单位)(默认为50)
    缓存管理进程:定期检查缓存状态、查看缓存总量是否超出限制、如果超出,就移除其中最少使用的部分

信号机制

kill -HUP cat /data/nginx/nginx.pid
CHLD:当worker进程出现异常关闭时,会给master进程发送该信号,master进程收到信号会重启worker进程
TERM, INT: 这两个信号都是立即停止服务,而不会等待已连接的tcp处理完请求
QUIT: 优雅的停止服务,不会立刻断开用户的tcp连接
HUP: 重载配置文件
USR1: 重新打开日志文件,可以做日志文件的切割
USR2: 启动新的master主进程
WINCH: 让master进程优雅的关闭所有的worker进程。
reload流程:
1、向master进程发送HUP信号
2、master进程校验配置语法是否正确
3、master进程打开新的监听端口(如果有)
4、master进程用新的配置启动新的worker子进程
5、master进程向老的worker子进程发送QUIT信号
6、老worker进程关闭监听句柄,处理完当前连接后结束进程
热升级流程:
1、将旧的nginx文件换成新的nginx文件(替换binary文件,即sbin目录下的nginx文件 cp -f 备份)
2、向master进程发送USR2信号
3、master进程修改pid文件名,加后缀.oldbin
4、master进程用新nginx文件启动新master进程
5、向老的master进程发送WINCH信号,关闭老的worker
6、回滚:向老的master发送HUP,向新的master发送QUIT

IO模型

传统的IO模型

在传统的tcp编程中,当建立了一个连接过后,那么这个连接就会一直阻塞,等到有数据发送过来的时候就会进行处理,那么如果客户端一直不发送数据过来的话,只是建立连接的话,那么这个线程什么事儿也干不了,就会一直阻塞,在nginx中,如果不使用io多路复用,那么它的一个连接过程是这样的:
在这里插入图片描述
上图是nginx的连接建立的一个过程,首先启动的时候是由master进程来建立连接和绑定端口的,通过master进程fork出worker进程,虽然是master建立的连接,但是其他建立连接的过程中是绑定的ip和端口,而nginxi是多进程的,那么如果有客户端连接过来,其实是与worker建立的连接,也就是说master启动的时候建立的连接,然后fork出子进程worker进程,worker和master之间是父子进程的关系,所以连接的监听端口woker和master进程是通用的;
首先我们来看当一个client连接到worker的时候,调用connect,然后worker接受这个连接,如果这个时候client不发送数据,那么worker会一直阻塞在recv,recv是一个缓冲区,worker要从缓冲区读取数据,所以这个时候这个线程就会一直等待client发送数据过来,否则就一直阻塞,它什么事儿也 干不了,这就是传统的io模型;而当client发送一个数据过来,这个时候这个线程就开始处理,也就是说一个连接一个线程,来一个开一个线程去处理。简单来说就是用户线程发起IO请求过后,内核会去检查数据是否就绪,如果数据没有就绪,那么用户线程就会一直阻塞住,此时用户线程需要交出cpu的执行时间片,当内核检查到数据已经就绪过后,内核会将数据拷贝到用户线程,从而唤醒用户线程的执行,此时这个用户线程什么事儿也干不了,只会一直阻塞在用户线程上,等待数据的读取。

IO多路复用

io多路复用是在传统IO的基础之上的一个改革,传统的io在用户线程数据未就绪的时候会一直进行阻塞,在数据就绪前就处于阻塞状态,没有办法做其他的事儿,而io多路复用的意思就是复用两个字,复用的什么呢?其实复用的就是线程,说白了就是线程复用,io多路复用就是说一个线程可以处理多个请求,一个线程最大能处理的连接数是1024,那么io的多路复用到底是怎么样的呢?io多路复用有两种方式,一种是select,一种是epoll,这里会简单根据这两个模型进行分析。

缓冲区

在NIO中,有了缓冲区的概念,那么缓冲区的概念是什么呢?为什么要有缓存区,举个简单的例子就是比如CPU、内存、硬盘这三个它们的运行速度都不是一样的,很显然CPU速度最快,再就是内存,硬盘,如果说没有缓存区,那么比如程序要写数据到内存中,那么这个时候内存就会一直等读取硬盘的数据,读一点写一点,那么这样的就是不匹配的,所以有了缓冲区的概念,硬盘可以将数据一次性的读到缓冲区,当缓冲区满了以后,一次性的高速写入内存
在这里插入图片描述

用户态和内核态

程序在处理的过程中,比如读取银盘的数据,这个时候不是用户线程去直接操作了硬盘,用户线程是没有权限操作硬盘的,硬盘的操作只能是内核态去操作,所以包括线程的处理,数据的接受都是通过内核态拷贝数据到用户态过后,然后通过中断机制通知用户线程去处理,比如我tcp过程中,线程接受到的数据的过程是通过网卡接受了数据放入了内核态,然后内核态将数据拷贝到用户态,然后通过中断机制通过我们的用户线程去处理,这就是简单的用户态和内核态感念,简单来就是线程中或者你读取硬盘的上的文件时候,用户线程是没有办法直接操作的,是需要交给内核态去处理,内核态处理完成过后将数据拷贝到用户态,然后通过中断机制告诉用户线程你可以处理了,这个时候用户线程就开始处理
在这里插入图片描述

线程队列

在io的多路复用中有几个概念就是工作队列、阻塞队列概念,只有处于工作队列的线程才能够被cpu调度,而一个线程要么处于工作队列,要么处于阻塞队列,要么在就绪队列,比如一个线程正在运行,那么这个线程肯定是在工作队列中,如果这个时候调用了阻塞的操作,那么这个线程就会从工作队列移除,放入到阻塞队列,那么在某个时刻,这个线程被唤醒重新执行,那么这个线程又会从阻塞队列中移除,放入到工作队列中,这些操作都是在内核中完成的,可能对用户是无感知的,在内核中维护了这样的一些队列。

fd文件描述符

fd在linux中叫做文件描述符(文件句柄)定义如下:
内核为了高效管理已被打开的文件数而建立的索引,值是一个非负数;
用于被打开的文件,所有的执行IO操作系统调用都是通过文件描述符;
每次打开一个文件都会分配一个fd。

Nginx的多进程单线程

nginx默认使用的是多进程单线程,那么单线程的运行原理是:
运行状态:运行状态的线程是处于工作队列中的,会被cpu调度执行,cpu只会调度在工作队列中的线程;
等待状态:当资源未就绪的时候会进入阻塞队列,会将工作队列中的阻塞线程移除,加入到阻塞队列socket:fd中,比如socket的recv操作就会进行阻塞,阻塞过后是不会消耗cpu的资源的,等待队列也属于资源,比如文件描述符socket:fd也是属于资源。
唤醒:当socket接受到数据过后,内核会将socket fd从等待队列中移除,然后加入到工作队列中,并且向cpu发送一个中断机制,告诉cup调度用户线程进行执行,这个时候就可以从socket fd中通过recv接受数据。

socket fd文件描述符

在linux中,一切皆文件,所以一个socket也是一个文件,所以在linux,一个socket连接也是打开了一个文件,所以socket fd的定义如下:
可以发送数据到缓存区;
从缓冲区接受数据;
等待队列:当调用了阻塞的方法时,比如recv时,会将socket fd从工作队列移除放入等待队列;
接受数据:socket接受数据时,都是通过网卡接受的,网卡接受到数据过后对应肯定包含了ip和端口信息,然后内核可以通过端口号接受到数据,然后拷贝到用户空间,然后将线程从阻塞队列移除,放入工作队列,然后向cpu发送一个中断信号,cpu这个时候就开始调度工作队列中的线程。
函数:
connect:客户端发起建立连接,返回成功,注册写事件;
accept:服务器建立连接,返回成功,注册读事件;
recv:服务端等待数据接受,对应读事件。
一个socket句柄,可以看成是一个文件,在socket收发数据,就相当于对一个文件进行了读写,所以一个socket的句柄,通过也可以通过fd来表示。

数据处理流程

一般接受数据都是通过网卡进行接受数据,网卡接受的数据中就包含了ip和端口信息,通过端口信息就可以找到是哪个socket监听的端口,然后网卡将接受到的数据写入内存,然后内核将数据拷贝到用户空间;内核拷贝数据到用户空间以后,然后向cpu发送一个中断信号,操作系统就知道有数据来了:

  1. 先将网络数据写入到端口对应的socket缓冲区中;
  2. 在唤醒recv进程,重新将线程从阻塞队列移除放入工作队列中;
  3. 在工作队列中的线程就属于运行态,可以被cpu进行调度。

IO多路复用之select

在这里插入图片描述

select的流程如上图:select内核维护了一个fd的数组,这个数组是一个bit位图,所以最大的大小极限为1024,所以一个线程只能管理1024个socket连接,这个fd数组维护的是socket fd的文件描述符,执行流程如下:
当程序调用select的时候,内核会遍历fd位图数组,看是否有数据过来,在上图中没有数据过来时,那么关系是这样的:select管理的是一个socket fd位图列表,位图中的socket fd是具体的socket连接文件描述符,在socket fd中是有一个缓存区和一个等待队列,比如上图中有三个socket fd,这个时候都没有数据过来的时候,那么Thread会注册到每个socket fd中的等待队列中;
当select监控的fd对应的socket中有数据过来以后,可以是其中的一个socket过来了数据,也可以是全部数据过来,只要有一个socket有数据过来,那么就会将数据从内核空间拷贝到用户空间(socket的缓冲区),然后将每个socket中等待队列中的thread移除,然后放入工作队列,最后向cpu发起一个中断信号,从而唤醒threa的调用。
这个时候select方法就返回了一个fd数组,然后thread开始循环这个fd数组,看那个socket有数据过来,然后调用这个对应的socket代码执行。
select的缺点:
三次遍历:
每次调用select都要将当前工作现场加入到所有的socket fd中的等待队列;
每次唤醒都需要遍历每个socket,将等待队列中的thread移除,加入到工作队列中;
每次select返回过后,也就是来了数据过后,都需要遍历socket fd数组,遍历了才知道那个socket来了数据。
每次都将这个fd列表传给内核,内核需要将数据拷贝到用户空间(socket fd的缓冲区)和加入工作队列;
处于性能的考虑,所以才规定一个线程只能处理1024个socket fd。
select的时间复杂度是O(N)

epoll

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210在这里插入图片描述

epoll的模型就没有select模型的一个线程只能最大监控1024个socket的限制了,epoll的流程如下:
操作系统启动,内核初始化epoll,epoll会向内核注册一个文件系统;
开辟出epoll自己的内核高速缓冲区(建立连续的物理内存页,建立slab层);
调用epoll_create,创建eventepoll对象(创建一个就绪列表和一个红黑树,这个红黑树就是存放的socket 注册fd和一个回调函数),因为epoll的监听列表是红黑树,在红黑树上存放的数据有回调函数,所以当有数据过来的时候就不需要进行循环,得到的就绪列表就是过来的数据列表,都是需要处理的,然后调用回调函数就可以了,所以epoll维护了一个就绪列表,这个就绪列表就是代表有数据过来就会注册到这个就绪列表中,所以红黑树作为注册的列表就有socket和socket对应的回调函数。
调用epoll_ctl对epoll中的监听列表(红黑树)进行操作,可以添加、修改、删除socket,其实操作的就是eventepoll对象;
调用epoll_wait进行阻塞,其实我觉得epoll_wait这里就是做了一个自旋,死循环,一直去看就绪列表是否有数据过来了,如果有数据过来了,就拿到这个就绪列表,然后讲就绪列表的数据清空,继续监听后续的数据,当socket有数据过来的时候,也是从从网卡接受的数据放入了内核态的内存区域,然后内核将数据拷贝到用户态,也就是就绪列表的的内存区域,这个时候调用了epoll_wait就开始获取就绪列表的数据,当就绪列表的数据获取到以后清空就绪列表,将拿到的数据循环去调用 socket的代码,也就是回调函数,因为epoll这里有一个红黑树,这个红黑树记录了socket 的fd和这个fd对应的回调函数,所以获取到的就绪列表就是过来的数据,就是都需要处理的数据,所以epoll和select的区别就是在这里不 需要 去循环迭代看fd数组是事那个数据过来了。

在这里插入图片描述

epoll_wait流程

在这里插入图片描述

epoll_wait的执行流程如上图,具体的流程如下:
当用户线程调用epoll_wait的时候,如果有数据就返回数据,将就绪列表rdlist从内核态复制句柄(fd)到用户态,从而让用户线程Thread得到rdlist就绪列表数据从而执行scoket操作;如果没有数据就进行sleep,等到timeout时间结束过后即便rdlist没有数据也 是需要返回的,如果没有设置timeout,那么rdlist没有数据也就立即返回。
具体的流程:
检查rdlist,如果不为空,则去到步骤6,如果为空则去到步骤2;
计算timeout,开始死循环;
设置线程状态为TASK_INERRUPTIBLE;
检测rdlist,如果不为空则去到步骤6,否则去到步骤5;
调用schedule_hrtimeout_range睡眠到timeout,中途有可能被ep_poll_callback唤醒回到3,如果真的timeout则break;
设置线程状态为TASK_RUNNING,rdlist如果不为空则退出循环,否则继循环;
调用ep_send_events将rdlist返回给用户态。

epoll的三个函数的关系如下:

在这里插入图片描述
epoll_create:创建eventpoll 对象,eventpoll创建就绪列表和红黑树;
epoll_ctl:负责管理eventpoll对象,向红黑树中注册、删除、修改socket fd对象;
epoll_wait:复杂监控就绪列表,就绪列表有了数据过后开始(从内核态拷贝数据到用户态)。
epoll的时间复杂度为O(1)
nginx是多进程单线程的,就比较适合使用io多路复用中的epoll,因为nginx大部分都是转发场景,没有业务处理,就是做一个转发,而且并发要求较高,epoll可以同时处理很多的socket
,nginx虽然接受client和转发反向代理服务都使用了不同的连接,但是是在一个线程中完成的,所以nignx是单线程处理的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值