网卡如何通知CPU有数据?硬中断与软中断处理流程解析
3: 网卡通过硬件中断(IRQ)通知CPU,告诉它有数据来了
4: CPU根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC )中相应的函数
5: 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断。
6: 启动软中断。这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致CPU没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。
如下图:

内存-网络模块-协议栈
RPS实现了数据流的hash归类,并把软中断的负载均衡分到各个cpu
7: 内核中的进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第6步中是网卡驱动模块抛出的软中断,会调用网络模块的函数
8: 调用网卡驱动里的poll函数来一个一个的处理数据包
9: 在pool函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道
10: 驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用函数
11: 会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈。然后判断是否开启了RPS,如果开启了,将会调用
12: 在函数中,会将数据包放入CPU的结构体的中,然后返回,如果满了的话,该数据包将会被丢弃,queue的大小可以通过
来配置
13: CPU会接着在自己的软中断上下文中处理自己里的网络数据(调用core)
14: 如果没开启RPS,会直接调用core
15: 看是不是有类型的(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。抓包就是抓的这里的包。
16: 调用协议栈相应的函数,将数据包交给协议栈处理。
17: 待内存中的所有数据包被处理完成后(即poll函数执行完成),启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU
如下图:

下面,数据包将交给相应的协议栈函数处理,进入第三层网络层。
IP 层的入口函数在 函数。该函数首先会做包括 在内的各种检查,如果需要的话会做 IP (将多个分片合并),然后 调用已经注册的 Pre- hook ,完成后最终到达 函数。
函数会调用 函数,进入路由处理环节。它首先会调用 来更新路由,然后查找 route,决定该 将会被发到本机还是会被转发还是丢弃:
1、如果是发到本机的话,调用 函数,可能会做 de-(合并多个 IP ),然后调用 函数。该函数根据 的下一个处理层的 ,调用下一层接口,包括 (TCP), (UDP), (ICMP),(IGMP)。对于 TCP 来说,函数 函数会被调用,从而处理流程进入 TCP 栈。
2、如果需要转发 (),则进入转发流程。该流程需要处理 TTL,再调用 函数。该函数会
(1)处理 Hook
(2)执行 IP
(3)调用 ,进入链路层处理流程。
如下图:

在上图中,
【文章福利】需要C/C++ Linux服务器架构师学习资料加群(资料包括C/C++,Linux,技术,内核,Nginx,,MySQL,Redis,,,ZK,流媒体,CDN,P2P,K8S,,TCP/IP,协程,DPDK,等)

传输层
1、传输层 TCP包的 处理入口在 函数(位于 linux/net/ipv4/tcp ipv4.c 文件中),它会做 TCP 检查等处理。
2、调用 ,查找该 的 open 。如果找不到,该 会被丢弃。接下来检查 和 的状态。
3、如果 和 一切正常,调用 使 从内核进入 user space,放进 的 queue。然后 会被唤醒,调用 call,并最终调用 函数去从 queue 中获取 。
应用层
1、每当用户应用调用 read 或者 时,该调用会被映射为/net/.c 中的 系统调用,并被转化为 调用,然后调用 函数。
2、对于 INET 类型的 ,/net/ipv4/af inet.c 中的 方法会被调用,它会调用相关协议的数据接收方法。
3、对 TCP 来说,调用 。该函数从 中拷贝数据到 user 。
4、对 UDP 来说,从 user space 中可以调用三个 call recv()/()/() 中的任意一个来接收 UDP ,这些系统调用最终都会调用内核中的 方法。
整个报文接收的过程如下:

分层:


1、 位于传输层协议之上,屏蔽了不同网络协议之间的差异
2、 是网络编程的入口,它提供了大量的系统调用,构成了网络程序的主体
3、在Linux系统中, 属于文件系统的一部分,网络通信可以被看作是对文件的读取,使得我们对网络的控制和对文件的控制一样方便
nginx处理套接字的流程
nginx解析用户配置,在所有端口创建并启动监听。
nginx解析配置文件是由各个模块分担处理的,每个模块注册并处理自己关心的配置,通过模块结构体的字段 *实现。
main方法会调用,其完成了服务器初始化的大部分工作,其中就包括启动监听(
)
假设nginx使用epoll处理所有事件,e模块是事件处理核心模块,初始化此模块时会执行it函数,包括将监听事件添加到epoll
1、在创建启动监听时,会添加可读事件到epoll,事件处理函数为,用于接收连接,分配连接,并调用对象的处理函数(tion)
2、连接成功后,nginx会等待客户端发送HTTP请求,默认会有60秒的超时时间,即60秒内没有接收到客户端请求时,断开此连接,打印错误日志。函数tion用于设置读事件处理函数,以及超时定时器。
3、函数
为解析HTTP请求的入口函数
4、函数est创建并初始化对象
5、解析完成请求行与请求头,nginx就开始处理HTTP请求,并没有等到解析完请求体再处理。处理请求入口为uest。
下面进入nginx http请求处理的11个阶段
绝大多数HTTP模块都会将自己的添加到某个阶段(将添加到全局唯一的数组中),注意其中有4个阶段不能添加自定义,nginx处理HTTP请求时会挨个调用每个阶段的
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0, //第一个阶段,目前只有realip模块会注册handler,但是该模块默认不会运行(nginx作为代理服务器时有用,后端以此获取客户端原始ip)
NGX_HTTP_SERVER_REWRITE_PHASE, //server块中配置了rewrite指令,重写url
NGX_HTTP_FIND_CONFIG_PHASE, //查找匹配的location配置;不能自定义handler;
NGX_HTTP_REWRITE_PHASE, //location块中配置了rewrite指令,重写url
NGX_HTTP_POST_REWRITE_PHASE, //检查是否发生了url重写,如果有,重新回到FIND_CONFIG阶段;不能自定义handler;
NGX_HTTP_PREACCESS_PHASE, //访问控制,比如限流模块会注册handler到此阶段
NGX_HTTP_ACCESS_PHASE, //访问权限控制,比如基于ip黑白名单的权限控制,基于用户名密码的权限控制等
NGX_HTTP_POST_ACCESS_PHASE, //根据访问权限控制阶段做相应处理;不能自定义handler;
NGX_HTTP_TRY_FILES_PHASE, //只有配置了try_files指令,才会有此阶段;不能自定义handler;
NGX_HTTP_CONTENT_PHASE, //内容产生阶段,返回响应给客户端
NGX_HTTP_LOG_PHASE //日志记录
} ngx_http_phases;
nginx 在函数中初始化11个阶段的数组,把http模块注册到相应的阶段去。注意多个模块可能注册到同一个阶段,因此是一个二维数组
使用GDB调试,断点到方法执行所有HTTP模块注册之后,打印数组:
p cmcf->phases[*].handlers
p *(ngx_http_handler_pt*)cmcf->phases[*].handlers.elts
11个阶段(7个阶段可注册)以及模块注册的如下图:

处理请求的过程
1、HTTP请求的处理入口函数是uest,其主要调用ases实现11个阶段的执行流程
2、ases遍历预先设置好的cmcf->.数组,调用其函数
3、内部就是调用,并设置下一步要执行的索引
所以综上看来,nginx处理请求的过程可以归纳为:
初始化 HTTP (读取来自客户端的数据,生成 HTTP 对象,该对象含有该请求所有的信息)。处理请求头。处理请求体。如果有的话,调用与此请求(URL 或者 )关联的 。依次调用各 phase 进行处理。
























