我正在尝试了解 Web 服务器的底层细节。我想知道服务器(例如 Apache)是否在不断轮询新请求,或者它是否通过某种中断系统工作。如果是中断,那么是什么引发了中断,是网卡驱动程序吗?
答案1
简短的回答是:某种中断系统。本质上,它们使用阻塞 I/O,这意味着它们在等待新数据时处于休眠(阻塞)状态。
服务器创建一个监听套接字,然后阻塞以等待新的连接。在此期间,内核将进程置于可中断睡眠状态并运行其他进程。这一点很重要:让进程持续轮询会浪费 CPU。内核可以通过阻止进程直到有工作要做来更有效地使用系统资源。
当新的数据到达网络时,网卡就会发出中断。
看到网卡有中断,内核通过网卡驱动程序从网卡读取新数据并将其存储到内存中。(这必须快速完成,通常在中断处理程序内部处理。)
内核处理新到达的数据并将其与套接字关联。阻塞在该套接字上的进程将被标记为可运行,这意味着它现在可以运行。它不一定立即运行(内核可能决定继续运行其他进程)。
内核会在空闲时唤醒被阻塞的 Web 服务器进程。(因为它现在处于可运行状态。)
Web 服务器进程继续执行,就像没有时间流逝一样。它的阻塞系统调用返回并处理任何新数据。然后...转到步骤 1。
答案2
还有相当多的“较低”细节。
首先,假设内核有一个进程列表,在任何给定时间,这些进程中有些正在运行,有些则没有运行。内核允许每个正在运行的进程占用一定时间,然后中断它并转到下一个进程。如果没有可运行的进程,那么内核可能会发出类似肝移植到 CPU,它会挂起 CPU,直到出现硬件中断。
服务器中某处有一个系统调用意思是“给我点事做”。有两大类方法可以做到这一点。对于 Apache,它调用accept
Apache 先前打开了一个套接字,可能在监听端口 80。内核维护一个连接尝试队列,每次有TCP 同步收到。内核如何知道 TCP SYN 已收到取决于设备驱动程序;对于许多 NIC,在收到网络数据时可能会发生硬件中断。
accept
要求内核将下一个连接启动返回给我。如果队列不为空,则accept
立即返回。如果队列为空,则从正在运行的进程列表中删除该进程(Apache)。稍后启动连接时,将恢复该进程。这称为“阻塞”,因为对于调用它的进程来说,它accept()
看起来像一个函数,直到有结果才会返回,而结果可能要过一段时间才会返回。在此期间,进程无法执行任何其他操作。
一旦accept
返回,Apache 就知道有人正在尝试发起连接。然后它调用叉将 Apache 进程拆分为两个相同的进程。其中一个进程继续处理 HTTP 请求,另一个进程accept
再次调用以获取下一个连接。因此,总是有一个主进程,它只负责调用accept
和生成子进程,然后每个请求都有一个子进程。
这是一种简化:可以使用线程而不是进程来实现这一点,也可以fork
预先这样做,这样当收到请求时就会有一个工作进程准备就绪,从而减少启动开销。根据 Apache 的配置方式,它可以执行其中任何一项操作。
这是关于如何做到这一点的第一个大类,它被称为阻塞 I/Oaccept
因为诸如和read
和等对套接字进行操作的系统调用write
将暂停该进程,直到它们有内容可以返回。
另一种广泛的方法是非阻塞或基于事件的或异步输入输出。这是通过以下系统调用实现的:select
或者epoll
它们各自做同样的事情:你给它们一个套接字列表(或者一般来说,文件描述符)以及你想用它们做什么,然后内核会阻塞,直到它准备好做其中一件事情。
使用此模型,您可以告诉内核(使用epoll
),“告诉我端口 80 上有新连接或我打开的这 9471 个其他连接中有新数据要读取”。epoll
阻塞直到其中一个准备好,然后您执行该操作。然后重复。系统调用(如accept
和read
)永远write
不会阻塞,部分原因是每当您调用它们时,epoll
只会告诉您它们已准备就绪,因此没有理由阻塞,也因为当您打开套接字或您指定的文件时,您希望它们处于非阻塞模式,因此这些调用将失败EWOULDBLOCK
而不是阻塞。
这种模型的优点是只需要一个进程。这意味着您不必为每个请求分配堆栈和内核结构。Nginx和HAProxy使用这种模型,这也是他们能在类似硬件上比 Apache 处理更多连接的一个重要原因。