深入理解 Socket, NIO 和 Epoll

之前在内部分享过一次关于NIO相关的知识,感觉通过这次整理,对NIO和Epoll整体上又多了一些认识,虽然没有能力阅读内核源码,但是希望这篇文章可以从整体上帮助各位认识NIO和Epoll。

中断

计算机执行程序时,会有优先级的需求。比如,当计算机收到断电信号时(电容可以保存少许电量,供CPU运行很短的一小段时间),它应立即去保存数据,保存数据的程序具有较高的优先级。

一般而言,由硬件产生的信号需要cpu立马做出回应(不然数据可能就丢失),所以它的优先级很高。cpu理应中断掉正在执行的程序,去做出响应;当cpu完成对硬件的响应后,再重新执行用户程序。中断的过程如下图,和函数调用差不多。只不过函数调用是事先定好位置,而中断的位置由“信号”决定。

在这里插入图片描述

不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标识。这些中断值通常被称为中断请求(IRQ)线。比如,IRQ0是时钟中断,而IRQ1是键盘中断。并不是所有的中断号都这样严格定义,像PCI总线上的设备,中断就是动态分配的。

中断号在中断处理过程中起到很重要的作用,在采用向量中断方式的中断系统中,CPU必须通过它才可以找到中断服务程序的入口地址,实现程序的转移。为了在中断向量表中查找中断服务程序的入口地址,可由中断号(n)×4得到一个指针,指向中断向量(即中断服务程序的入口地址)存放在中断向量表的位置,从中取出这个地址(CS:IP),装入代码段寄存器CS和指令指针寄存器IP,即转移到了中断服务程序。

网卡中断

当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据(也就是数据经过DMA已经从磁盘缓冲区到内核了,cpu介入内核态将数据从内核返回到用户进程内存,如果没有DMA的话,可能要CPU多次从磁盘缓冲区拷贝到内核态,然后再从内核态到用户进程内存)。

中断函数是 驱动程序注册到Linux Kernel中的中断子系统注册的中断处理函数。

数据的发送与接收:

  1. 当我们需要发送数据时,最终调用的是网卡驱动提供的函数:net_device->hard_start_xmit();

  2. 当我们接收到数据时,会触发中断,中断处理函数调用会调用内核函数来接收数据(放入缓冲区?),最终由驱动程序调用内核函数netif_receive_skb(),把报文送入协议栈(接下来的代码硬件无关,与具体报文处理协议相关,比如:ARP协议,IPv4协议,IPv6协议等)。

  3. 网卡的中断处理函数在调用内核函数接收数据时又分为非NAPI/NAPI两种方式;

  4. NAPI方式涉及到中断的下半部处理的概念以及软中断。

  5. 报文通过**netif_receive_skb()**送入协议栈之后,首先判断需不需要进行桥接处理;

  6. 如果报文没有被桥接代码处理,再调用协议处理函数来处理;

  7. 在这里插入图片描述

  8. 中断合并:当数据量很少的时候,每来一个数据包网卡都回产生一个中断,kernel响应这个中断,从网卡缓冲区中读出数据放进协议栈处理,当满足一定条件时,kernel回调用户代码,这里的“回调”一般情况下是指从一个kernel syscall中返回(在此之前用户代码一直处于block状态)。
    当数据量很大时,每个包都产生一个中断就划不来了,此时kernel可以启动interrupt coalescing机制,让网卡做中断合并也就是说来足够多的数据包或者等待一个timeout才会产生一个中断,kernel在响应中断时会把所有数据一起读出来处理,这样可以有效的降低中断次数
    当数据量更大时,网卡缓冲区里几乎总是有未处理的数据,此时kernel干脆会禁掉网卡的中断,切换到轮询处理的模式,说白了就是跑一个忙循环不停地读网卡缓冲区里的数据,这样综合开销更低。

软中断

系统调用就是软中断,也就是用户态切换到内核态

所有软中断的中断号都是0x80,它是上层应用程序与Linux系统内核进行交互通信的唯一接口。其中断处理程序是system_call,当检测到系统调用发生时(int 0x80中断),第一步先保存现场,通过一个宏指令SAVE_ALL实现的,这个指令是把寄存器的状态通过压栈的方式保存起来。

然后会调用sys_call_table,通过eax寄存器的值查找系统调用表,找到几号系统调用,然后调用相应的系统调用。

最终恢复现场,然后应用继续执行。

阻塞的原理

工作队列

操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻塞状态,比如上述程序运行到recv时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。

下图中的计算机中运行着A、B、C三个进程,其中进程A执行着上述基础网络程序,一开始,这3个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。

在这里插入图片描述

等待队列

当进程A执行到创建socket的语句时,操作系统会创建一个由文件系统管理的socket对象(如下图)。这个socket对象包含了发送缓冲区、接收缓冲区、等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该socket事件的进程。

在这里插入图片描述

创建socket

当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中。由于工作队列只剩下了进程B和C,依据进程调度,cpu会轮流执行这两个进程的程序,不会执行进程A的程序。所以进程A被阻塞,不会往下执行代码,也不会占用cpu资源。

ps:操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下。

唤醒进程

当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。也由于socket的接收缓冲区已经有了数据,recv可以返回接收到的数据。

内核接受网络数据的全过程

在这里插入图片描述
如下图所示,进程在recv阻塞期间,计算机收到了对端传送的数据(步骤①)。数据经由网卡传送到内存(步骤②),然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。此处的中断程序主要有两项功能,先将网络数据写入到对应socket的接收缓冲区里面(步骤④),再唤醒进程A(步骤⑤),重新将进程A放入工作队列中。

唤醒进程的过程如下图所示。

在这里插入图片描述

以上是内核接收数据全过程

这里留有两个思考题,大家先想一想。

其一,操作系统如何知道网络数据对应于哪个socket?

其二,如何同时监视多个socket的数据?

第一个问题:因为一个socket对应着一个端口号,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。

第二个问题则是epoll等内核技术来解决

Unix 网络IO分类

Unix提供了5种不同的I/O模型,分别是

  • 阻塞I/O(blocking I/O)
  • 非阻塞I/O(non-blocking I/O)
  • I/O复用(I/O multiplexing)
  • 信号驱动式I/O(signal-driven I/O)
  • 异步I/O(asynchronous I/O)

一个I/O操作需要从用户态进入内核态运行,通常包括俩阶段

  1. 等待数据
  2. 从内核向进程复制数据

对于socket I/O而言,第一步通常是等待数据从网络中到达,到达之后会复制到内核的某个缓冲区
第二步就是从内核缓冲区复制到应用进程缓冲区

阻塞I/O

在这里插入图片描述

默认情况下,所有的socket都是阻塞的. 如图所示

  1. 应用发起recvfrom这个系统调用, 应用被阻塞
  2. 内核等待数据准备好
  3. 数据准备好, 内核将数据复制到应用缓冲区
  4. 应用从阻塞里恢复,处理数据

这也是理解和编程起来比较简单的模型,所以计算机早期用的很多,现在在处理超大文件的时候,也依然适用于这种模型。

非阻塞I/O

在这里插入图片描述

相比于Blocking I/O, Non-Blocking I/O等待数据阶段不会被阻塞,也就是说操作系统不会挂起应用, 应用
不断轮询(polling)内核看是否数据准备好。某次轮询发现准备好了,再直接发系统调用阻塞取数据.

Unix网络编程里对轮询的定义是:
应用进程对非阻塞描述符循环发送系统调用,以查看某个操作是否就绪

I/O多路复用

在这里插入图片描述
在处理非常多的描述符的时候,I/O多路复用技术显得非常有用。I/O多路复用需要发送2次系统调用:

  1. select或者poll, 获取可读条件, 等待描述符变成可读
  2. 发起recvfrom系统调用,内核复制数据到应用。

在只有1个客户端的时候,I/O多路复用技术甚至不如阻塞I/O.因为多发了一次系统调用。
但有些常用的网络场景,如:

  • 既要处理TCP,又要处理UDP
  • 一个服务器处理多个服务或者多个协议
  • 一个TCP服务器既要监听socket,又要处理已连接socket

这些场景下,多发的这次系统调用能带来更高的I/O处理效率,能更均匀的使用服务器时间片, 处理更多连接。
常用的方法是把I/O多路复用和非阻塞I/O结合使用,这样应用进程不需要阻塞,能处理别的业务,同时又能够处理多个I/O请求

这里可以看出,多路复用也是在第一阶段生效,也就是说进程阻塞在多路复用器epoll上,当数据从缓冲区读到内核完毕后,进程被唤醒

但是第二步,进程发起系统调用,将数据从内核buffer read到用户空间,这一步仍然是阻塞的,但是这一步通常都很快(比如一个socketChannel 一端连接的是一个socket,一端是buffer,所以get(buffer)就是将socker 在内核中的数据读到用户空间的buffer)

信号驱动I/O

在这里插入图片描述
信号模型的步骤是:

  1. 应用进程发起一个信号,告诉内核要什么文件,然后立马返回
  2. 内核准备好数据
  3. 应用进程收到信号发起recvfrom系统调用来阻塞取数据

信号驱动模型类似于你去一点点买奶茶,对方给你一个小票。
什么时候好了对方喊xx号(这就是信号)好了.然后你来取奶茶(recvfrom取数据)

异步I/O

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值