Web 服务器如何“侦听” IP 地址、中断或轮询?

use*_*911 89 networking embedded webserver

我试图了解网络服务器的底层细节。我想知道服务器,比如 Apache,是否在不断轮询新请求,或者它是否通过某种中断系统工作。如果是中断,是什么触发中断,是网卡驱动?

Gre*_*ser 182

简短的回答是:某种中断系统。本质上,它们使用阻塞 I/O,这意味着它们在等待新数据时休眠(阻塞)。

  1. 服务器创建一个侦听套接字,然后在等待新连接时阻塞。在此期间,内核将进程置于可中断睡眠状态并运行其他进程。这是很重要的一点:让进程不断轮询会浪费 CPU。内核可以通过阻塞进程直到有工作要做,从而更有效地使用系统资源。

  2. 当新数据到达网络时,网卡发出中断。

  3. 内核看到有来自网卡的中断,通过网卡驱动,从网卡中读取新数据并存入内存。(这必须快速完成,并且通常在中断处理程序中处理。)

  4. 内核处理新到达的数据并将其与套接字相关联。在该套接字上阻塞的进程将被标记为可运行,这意味着它现在可以运行。它不一定立即运行(内核可能决定仍然运行其他进程)。

  5. 在空闲时,内核将唤醒被阻止的网络服务器进程。(因为它现在可以运行了。)

  6. 网络服务器进程继续执行,就好像没有时间过去一样。它的阻塞系统调用返回并处理任何新数据。然后……转到第 1 步。

  • +1 清楚地描述内核与网络服务器进程。 (18认同)
  • 我无法相信如此复杂的事情可以如此清晰和简单地概括,但您做到了。+1 (13认同)
  • +1 很好的答案。此外,使用现代 NIC、操作系统和驱动程序,2 到 3 之间的步骤可能会变得更加复杂。例如,对于 Linux 上的 [NAPI](http://en.wikipedia.org/wiki/New_API),实际上并没有在中断上下文中接收到数据包。相反,内核会说“好吧 NIC,我知道你有数据。别再烦我了(禁用中断源),我很快就会回来获取这个数据包*和*可能在我之前到达的任何后续数据包.” (8认同)
  • 轻微挑剔:实际上没有必要阻止。一旦服务器进程创建了一个监听套接字,内核就会接受那个端口上的 SYN,即使你没有在 `accept` 中被阻塞。它们是(幸运的是,否则会很糟糕!)独立的、异步运行的任务。当连接进来时,它们被放入一个队列中,在那里`accept` 将它们拉出来。只有当没有时,它才会阻塞。 (8认同)
  • “从网卡读取新数据并将其存储到内存中。(这必须快速完成,通常在中断处理程序内部处理。)” 不是通过直接内存访问完成的吗? (3认同)
  • @SiyuanRen 通常是这样,但不一定。一些低端微控制器或嵌入式 CPU 没有支持 DMA 的 NIC。不过,在大多数情况下,你是对的。 (3认同)
  • @Aron 它与此答案中描述的内容没有太大区别 - 不是许多线程每个都阻塞自己的事件,而是有一个线程阻塞,直到它获得 *any* 事件,但原理仍然相同。 (2认同)
  • @DebugErr 这个答案有点误导,因此是关于 NIC 的问题。NIC 驱动程序通过内核/硬件接收中断。此驱动程序将数据包转换为消息格式,然后将其传递给网络驱动程序 (TCP/IP)。网络驱动程序维护一个 PID 到 IP:PORT:PROTO 的列表,并通过内核消息传递将数据包传送到程序的 I/O 或消息队列。然后内核决定在注意到待处理消息时最终唤醒哪个线程。使用环回时,NIC 驱动程序永远不会看到数据包;TCP/IP 将数据从第一个应用程序传递到另一个应用程序。 (2认同)

小智 9

有很多“较低”的细节。

首先,考虑内核有一个进程列表,并且在任何给定时间,这些进程中的一些正在运行,而另一些则没有。内核允许每个正在运行的进程占用一些 CPU 时间,然后中断它并移动到下一个。如果没有可运行的进程,那么内核可能会向 CPU发出类似于HLT的指令,该指令会暂停 CPU,直到出现硬件中断。

服务器中的某处是一个系统调用,上面写着“给我做点什么”。有两大类方法可以做到这一点。在 Apache 的情况下,它调用acceptApache 先前打开的套接字,可能监听端口 80。内核维护一个连接尝试队列,并在每次收到TCP SYN时添加到该队列中。内核如何知道接收到 TCP SYN 取决于设备驱动程序;对于许多 NIC,接收网络数据时可能会出现硬件中断。

accept要求内核在下一次连接启动时返回给我。如果队列不为空,则accept立即返回。如果队列为空,则从正在运行的进程列表中删除进程 (Apache)。当稍后启动连接时,该过程将恢复。这被称为“阻塞”,因为对于调用它的进程来说,它accept()看起来像一个函数,直到它有结果才返回,这可能是从现在开始的一段时间。在此期间,该过程无能为力。

一旦accept返回,Apache 就知道有人正在尝试发起连接。然后调用fork将 Apache 进程拆分为两个相同的进程。其中一个进程继续处理 HTTP 请求,另一个进程accept再次调用以获取下一个连接。因此,总有一个主进程只调用accept和生成子进程,然后每个请求都有一个子进程。

这是一种简化:可以使用线程而不是进程来执行此操作,也可以fork事先这样做,以便在收到请求时准备好工作进程,从而减少启动开销。根据 Apache 的配置方式,它可以执行以下任一操作。

这是如何做到这一点的第一个大类,它被称为阻塞 IO,因为像acceptreadwrite对套接字进行操作的系统调用将挂起进程,直到它们有东西要返回。

另一种广泛的方法称为非阻塞或基于事件或异步 IO。这是通过像selector 之类的系统调用来实现的epoll。它们都做同样的事情:你给它们一个套接字列表(或者一般来说,文件描述符)以及你想用它们做什么,内核会阻塞,直到它准备好做这些事情之一。

使用此模型,您可能会告诉内核(使用epoll),“告诉我何时在端口 80 上有新连接或在我打开的 9471 个其他连接中的任何一个上读取新数据”。epoll阻塞,直到其中一项准备就绪,然后你才去做。然后你重复。系统调用acceptreadwrite从不阻塞,部分是因为每当你调用它们时,epoll只是告诉你它们已经准备好了,所以没有理由阻塞,还因为当你打开套接字或文件时,你指定了你想要它们在非阻塞模式下,因此这些调用将失败EWOULDBLOCK而不是阻塞。

这种模型的优点是您只需要一个过程。这意味着您不必为每个请求分配堆栈和内核结构。NginxHAProxy使用这种模型,这是它们在类似硬件上可以处理比 Apache 多得多的连接的一个重要原因。