一、前言
我们都知道数据从网络达到我们的应用程序是要先通过网卡, 再走ios7层协议到达我们的应用程序的, 那么具体流程是怎么样的呢? 本篇文章来讨论下。
本文基于《深入理解linux网络》所作的笔记, 该书基于linux源码3.10的版本
二、整体流程图
epoll内核准备阶段
- 创建阶段(epoll_create)
-
内核为当前进程创建一个 eventpoll 对象,它包含:
-
红黑树 (rbr):用于管理所有被监控的文件描述符(即注册的 socket)。
-
就绪队列 (rdllist):存放已触发事件、等待用户态处理的节点。
-
等待队列 (wq):当没有事件就绪时,存放被阻塞等待的进程
-
注册阶段(epoll_ctl)
当用户调用 epoll_ctl 注册 socket 时:
-
内核为该 socket 创建一个红黑树节点 epitem,并将其插入到 eventpoll 的红黑树中。
-
同时在该 socket 的等待队列 (sk_wq) 中挂载一个等待队列项 (wait_queue_entry_t),其回调函数设置为 ep_poll_callback。
-
当 socket 有新数据到达时,ep_poll_callback 会被触发,负责将对应的 epitem 加入 eventpoll 的就绪队列 rdllist。
- 等待阶段(epoll_wait)
当用户调用 epoll_wait 时:
- 内核首先检查 eventpoll 的就绪队列中是否有事件可读;若有,立即返回。
- 若无事件就绪,内核会为当前线程构造一个等待队列项,将其加入 eventpoll 的等待队列 (eventpoll.wq) 中,然后将线程置为休眠状态。
- 当 ep_poll_callback 被触发并向就绪队列中添加事件后,相关等待线程会被唤醒。
此时这些队列如下图分布

当前进程有服务端socket(底层是sock)和epoll内核对象(eventpoll)
socket有个等待队列sk_wq, 队列的每个节点代表一个阻塞的客户端请求, 不同于bio模型下*private需要指向当前线程(因为要唤醒), 这里直接是空的(*private指向null), 而func指向数据达到的回调函数(default_wake_function), 该节点还有个base指针指向红黑树节点对象epitem
epoll内核对象eventpoll有个等待队列sk_wq, 它的队列元素包含阻塞的进程和对应的回调函数
此时服务端各节点状态如下

数据从网卡到sock的接收队列

简单说明:
-
数据从网络达到网卡
-
DMA 将数据写入内核空间缓冲区
网卡驱动程序初始化时,会分配一组 环形接收队列(Rx Ring Buffer),
每个队列项对应一块预先映射的内核页框(由驱动通过 dma_map_single 建立映射)。
网卡接收到数据后,直接通过 DMA(Direct Memory Access) 把数据写入这些缓冲区
-
网卡触发硬件中断 (IRQ)
当环形缓冲区填入数据后,网卡向 CPU 发出硬中断信号
-
CPU 响应硬中断
中断处理函数运行在中断上下文中,通常不会直接收包。
它只会进行轻量的操作(例如记录事件、屏蔽中断),然后调度 软中断(softirq), 具体地是触发 NET_RX_SOFTIRQ。
-
软中断由内核线程执行 (ksoftirqd / NAPI)
当软中断触发时,会执行网络接收的 poll 回调函数(驱动注册的 napi->poll)。
这一步通常是 NAPI 机制(New API)在工作,驱动会主动轮询网卡的环形缓冲区,将数据批量取出
-
从 DMA 缓冲读取并构建 skb
poll 函数会从 DMA 队列中取出数据帧,构造一个 sk_buff (skb) 对象,然后交给协议栈。
-
交给网络子系统(以太网层)
内核网络子系统首先处理以太网头部,判断协议类型(如 IPv4 / IPv6 / ARP),然后调用相应的上层协议处理函数。
-
邻居子系统 (neigh layer)
对于 IP 包,会通过邻居层(Neighbor Subsystem)解析 MAC 与 IP 的映射关系,并维护 ARP/ND 缓存,确保路由和下一跳正确。
-
IP 层处理 (L3)
-
解析 IP 头部(TTL、校验和、源/目的地址)
-
进行分配重组(MTU)
-
通过 netfilter(iptables)过滤、路由查找
-
然后将数据交给上层 TCP 或 UDP 协议处理函数。
- TCP 层处理 (L4)
-
解析 TCP 头部(seq、ack、flags 等)
-
依据四元组(源 IP、源端口、目标 IP、目标端口)查找对应 socket
-
校验序列号与滑动窗口,更新拥塞控制状态
-
最终把数据封装到 skb 中,加入该 socket 的 接收队列 (sk_receive_queue)
整体流程图

-
当数据接收完毕后, 会调用注册的
ep_poll_callback函数, 根据等待任务队列上额外的base指针找到epitem, 进而找到eventpoll对象, 然后将epitem添加到epoll的就绪队列。 -
若 epoll 实例上有等待进程
接着查看eventpoll对象上的等待队列里是否有等待项(epoll_wait执行的时候会设置), 如果没有, 软中断的事情就做完了, 如果有等待项, 就找到等待项里设置的回调函数(default_wake_function), 拿出进程描述符, 唤醒它
-
随后从 epoll_wait() 的阻塞点恢复执行,遍历 rdllist,将就绪事件返回给用户空间,并清空链表。
三、NIO 对 epoll 的底层支持流程
基于jdk17(jdk17u-jdk-17.0.7-7)
public class NioServer {
public static void main(String[] args) throws IOException {
System.setProperty("java.nio.channels.spi.SelectorProvider", "sun.nio.ch.PollSelectorProvider");
// 1.创建ServerSocketChannel, 创建内核socket(还有等待队列), 返回fd(在ServerSocketChannel中)
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// bind(ip和端口与socket绑定) + listen(创建全连接半连接队列)
serverChannel.bind(new InetSocketAddress(8080));
// 设置为非阻塞
serverChannel.configureBlocking(false);
// 2.创建Selector(mac上是KQueueSelectorImpl), linux是EpollSelectorImpl(对应EPollSelectorProvider)
Selector selector = Selector.open();
// 3.给Selector注册监听的ACCEPT事件 (这一步会调用epoll_ctl将socket添加到epoll中)
serverChannel.register(selector, SelectionKey.OP_ACCEPT, null);
System.out.println("服务器启动,监听端口 8080...");
// 4. 循环监听事件
while (true) {
// 1.服务端第一次调用时, 会将监听socket的就绪事件, 添加到selectedKeys集合中 并添加到epoll的红黑树节点上; 这里会调用epoll_ctl方法
// 2.然后阻塞(调用epoll_wait)直到有事件发生; epoll_wait/kqueue_wait
selector.select();
// selector.select(1000);
// selector.selectNow(); 这个方法不会阻塞, timeout传的是0
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
// 2.处理完当前所有的就绪事件
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 必须移除,避免重复处理
keyIterator.remove();
// 三次握手成功的事件(服务端事件)
if (key.isAcceptable()) {
// 处理新的客户端连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获取全队列连接; 根据服务端fd获取一个客户端连接的socket对象
SocketChannel client = server.accept();
client.configureBlocking(false);
// 监听读事件; 1.注册读事件到Selector, 将channel和selector绑定生成selectedKey 3.添加到selectedKeys集合中
client.register(selector, SelectionKey.OP_READ, null);
System.out.println("连接建立:" + client.getRemoteAddress());
}
else if (key.isReadable()) {
// 处理客户端发送的数据
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
System.out.println("连接关闭:" + client.getRemoteAddress());
client.close();
key.cancel();
continue;
}
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data);
System.out.println("收到消息:" + message);
// 回复客户端
ByteBuffer response = ByteBuffer.wrap(("服务器收到:" + message).getBytes());
client.write(response);
// ByteBuffer[] bs = new ByteBuffer[1];
// client.write(bs);
}
}
}
}
}
其中
- ServerSocketChannel.open()
- 在 Java 层会通过 IOUtil.newFD() 创建一个新的文件描述符(fd),
- 底层调用 socket() 系统调用,在内核中创建一个 TCP 套接字对象(struct socket + struct sock)
- 返回的 fd 与 Java 的 FileDescriptor 绑定。
- serverChannel.bind(new InetSocketAddress(8080))
- 调用 bind() 系统调用,将 fd 与指定的 IP 与端口绑定;
- 随后调用 listen(),在内核中为该 socket 创建半连接队列(SYN 队列)和全连接队列(accept 队列)。
- serverChannel.register(selector, SelectionKey.OP_ACCEPT, null)
- Java 层会调用 SelectorImpl.register() → EPollSelectorImpl.implRegister(),
- 这一步在 JDK 层只是把 SelectionKey 和感兴趣事件注册到 Selector 的内部数据结构中(keySet/registeredKeys),还没有与内核 epoll 对象交互。
- selector.select()
-
在 EPollSelectorImpl#doSelect() 中,会遍历 registeredKeys,找出新注册或兴趣变化的 fd。
-
对这些 fd,才会在 native 层通过 epoll_ctl(ADD) 或 MOD 将其添加到 eventpoll 红黑树中。
-
这样做的好处是 批量注册和事件修改,避免每次 register 都频繁切换到内核。
-
select 会调用 epoll_wait(),从 rdllist(就绪队列)中返回已就绪的 epitem。
-
Java 层再将这些事件封装到对应的 SelectionKeyImpl,交给 NIO 事件循环。
四、总结
当一帧数据从网线跃入网卡,它的命运便与操作系统的内核紧密相连。
- 物理层到内核
- 网卡通过 DMA 将数据写入预分配的内核缓冲区(环形队列),
- 并通过硬中断通知 CPU,触发软中断(NET_RX_SOFTIRQ),由内核线程(如 ksoftirqd)或 NAPI 机制批量轮询处理。
- 数据被封装为 skb(socket buffer),依次穿越 以太网层 → 邻居层 → IP 层 → TCP/UDP 层,
- TCP 层会依据四元组找到对应的 socket,将数据放入 socket 接收队列 (sk_receive_queue),处理滑动窗口和拥塞控制。
- 内核事件机制(epoll)
- socket 的等待队列 (sk_wq) 中注册了 epoll 的回调 (ep_poll_callback),关联 epitem 与 eventpoll 对象。
- 当数据就绪时,ep_poll_callback 将 epitem 放入 epoll 的就绪队列 (rdllist),
- 并检查 epoll 实例上的等待队列,如果有线程阻塞在 epoll_wait(),调用回调函数(default_wake_function)唤醒它。
- Java NIO 的映射与封装
- 在 Java 层,ServerSocketChannel.open() 创建 fd 并对应内核 socket; bind() 与 listen() 初始化端口绑定和连接队列。
- channel.register(selector, OP_ACCEPT) 仅在 Selector 内部注册事件,并不会直接调用内核 epoll_ctl;
- 真正的 fd 注册发生在下一次 selector.select() 调用时,通过 JNI 调用 epoll_ctl(ADD/MOD),将 fd 封装为 epitem 加入 eventpoll 红黑树。
- select 会调用 epoll_wait() 获取就绪事件,并将其映射为 SelectionKeyImpl 返回给 Java 应用层。
- 从内核到用户态
- 当用户线程被唤醒,它从阻塞点继续执行,从 eventpoll 的就绪队列 (rdllist) 取出事件,交给应用处理。
- 这个过程中,网络事件完成了从 电信号 → 内核 → socket → epoll → Selector → 应用事件循环 的完整生命周期。
搜索公众号: 行云代码 一起学习, 一起成长
3455

被折叠的 条评论
为什么被折叠?



