ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

udhcp源码剖析(四)——DHCP服务器的superloop:从socket到select的事件骨架

udhcp源码剖析(四)——DHCP服务器的superloop:从socket到select的事件骨架 1. 从一个真实场景说起为什么 DHCP 服务器会“卡住”如果你在嵌入式设备上跑过 udhcpd大概率遇到过这种场景板子启动后 DHCP 服务看起来正常客户端也能拿到 IP但过一段时间后客户端续租失败或者服务器进程莫名其妙不响应了。你去看日志发现 udhcpd 还在跑socket 也没关但就是不回包。这类问题的根因往往不在 DHCP 协议本身而在 udhcpd 的主循环调度骨架上。udhcpd 不像那些用 epoll 线程池的现代服务它用的是最朴素的selectsuperloop模型一个 while 循环里先建 socket再把 socket 和 signal_pipe 塞进 fd_set然后 select 阻塞等待返回后按顺序处理信号、收包、解析 state、回包。整个流程是单线程串行的任何一个环节阻塞或漏处理都会让整个服务“假死”。这篇是 udhcp 源码剖析系列的第四篇聚焦udhcpd_main里那个 superloop 的事件骨架。我会带你从 socket 创建开始一步步拆到 select 返回后的分支处理最后给一份可复制的最小udhcpd.conf和抓包验证动作让你在本地把 DHCP 请求响应链路跑通亲眼看到 select 返回后代码走了哪条路。适合正在学嵌入式网络协议栈、想搞懂“协议实现到底长什么样”的读者。2. 前置准备TaoToken 与本地环境在动手改配置、抓包之前先把两件事准备好一是能快速查 DHCP 协议细节和 udhcp 源码的工具入口二是本地能跑 udhcpd 的环境。我平时查 RFC2131 的状态机、对照 udhcp 的选项解析逻辑时会用 TaoToken 的模型对话来快速定位问题。它的入口在这里模型对话https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat如果你后面要写脚本自动解析抓包结果或者把 udhcp 的 state 分支逻辑整理成文档可以用 API Key 接入API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys接入文档在接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc本地环境方面你需要一台 Linux 机器物理机或虚拟机都行装上busyboxudhcpd 通常随 busybox 提供、tcpdump和iproute2。确认 udhcpd 可用which udhcpd udhcpd --help如果系统里没有可以装 busyboxsudo apt install busybox tcpdump注意udhcpd 需要绑定 67 端口所以要用 root 或加CAP_NET_BIND_SERVICE权限运行。下面所有操作默认你有 sudo 权限。3. 可复制配置最小 udhcpd.conf 骨架udhcpd 的配置文件通常放在/etc/udhcpd.conf但为了实验干净我们单独建一个目录mkdir -p ~/udhcp-test cd ~/udhcp-test创建udhcpd.conf内容如下# udhcpd 最小配置骨架 start 192.168.99.100 end 192.168.99.150 interface eth0 max_leases 20 lease_file /home/yourname/udhcp-test/udhcpd.leases pidfile /home/yourname/udhcp-test/udhcpd.pid # 可选指定服务器标识默认会用 interface 的 IP # server 192.168.99.1 # 可选租约时间秒 # lease 3600 # min_lease 60 # offer_time 60 # decline_time 3600 # 可选auto_time 控制 select 超时写 lease 文件的周期 # auto_time 7200 # 下发的选项 option subnet 255.255.255.0 option router 192.168.99.1 option dns 8.8.8.8 option lease 3600几个关键字段和 superloop 的对应关系用表格说清楚配置项作用在 superloop 中的体现interface绑定监听的硬件接口listen_socket(SERVER_PORT, server_config.interface)auto_timeselect 超时周期到期写 lease 文件tv.tv_sec timeout_end - monotonic_sec()lease_filelease 持久化路径write_leases()写入目标lease/min_lease租约时长上下限sendOffer里lease_time_align的钳制decline_timeDECLINE 后地址冻结时长lease-expires time(0) decline_time把yourname换成你的实际用户名。然后确认eth0存在或者改成你实际要监听的接口名。启动 udhcpdsudo udhcpd -f -S ~/udhcp-test/udhcpd.conf-f是前台运行方便看日志-S表示不写 syslog直接输出到终端。你会看到类似udhcpd: started, v1.36.1 udhcpd: listening on eth0这时候 superloop 已经进入 select 阻塞状态了。4. 源码级拆解superloop 的四个阶段4.1 阶段一socket 创建与 signal_pipe 初始化superloop 每轮开始第一件事是检查server_socket是否有效。如果小于 0就重新建if (server_socket 0) { server_socket listen_socket(SERVER_PORT, server_config.interface); }listen_socket内部做的事创建 UDP socket绑定INADDR_ANY:67设置SO_BROADCAST再绑定到指定接口。这里有个细节——它用的是SO_BINDTODEVICE所以只有从server_config.interface进来的包才会被收到。如果你发现 udhcpd 收不到包先确认接口名对不对。紧接着是 signal_pipe。udhcpd 用socketpair创建一对 fdsignal_pipe[0]读端signal_pipe[1]写端。信号处理器收到 SIGUSR1/SIGTERM 后往写端写一个字节读端就变成可读select 立刻返回。这是把异步信号“翻译”成 fd 可读事件的经典手法。max_sock udhcp_sp_fd_set(rfds, server_socket);udhcp_sp_fd_set把signal_pipe[0]和server_socket都加进rfds返回较大的那个 fd供 select 的第一个参数用。4.2 阶段二select 等待与超时计算select 的超时时间由auto_time决定。如果auto_time非 0tv.tv_sec设为距离timeout_end的剩余秒数如果已经超时tv.tv_sec 0直接跳过 select走写 lease 文件的分支。if (server_config.auto_time) { tv.tv_sec timeout_end - monotonic_sec(); tv.tv_usec 0; } if (!server_config.auto_time || tv.tv_sec 0) { max_sock server_socket signal_pipe[0] ? server_socket : signal_pipe[0]; retval select(max_sock 1, rfds, NULL, NULL, server_config.auto_time ? tv : NULL); } else { retval 0; }注意auto_time为 0 时select 的 timeout 传 NULL也就是永久阻塞直到有 fd 可读。这意味着如果你不配auto_timeudhcpd 不会定期写 lease 文件断电后租约信息可能丢失。生产环境建议设一个合理的值比如 7200 秒。select 返回后有三种情况retval 0超时写 lease 文件更新timeout_endcontinue回到循环开头。retval 0 errno ! EINTRselect 出错打日志continue。retval 0有 fd 可读继续往下走。4.3 阶段三信号处理与收包select 返回后先调udhcp_sp_read(rfds)判断是不是 signal_pipe 可读switch (udhcp_sp_read(rfds)) { case SIGUSR1: write_leases(); timeout_end monotonic_sec() server_config.auto_time; continue; case SIGTERM: goto ret0; case 0: break; /* 没有信号继续处理 socket */ default: continue; }udhcp_sp_read内部会检查signal_pipe[0]是否在rfds里如果在就读出信号值。返回 0 表示没有信号那就说明是server_socket可读进入收包流程。收包用udhcp_get_packetbytes udhcp_get_packet(packet, server_socket); if (bytes 0) { if (bytes -1 errno ! EINTR) { close(server_socket); server_socket -1; } continue; }这里有个容易忽略的点udhcp_get_packet不只是 read它还会做校验——检查 DHCP magic cookie 是否为0x63825363如果不是直接丢弃还会根据 op 和 options 里的 vendor 字段决定是否强制广播标志。所以如果你抓包看到客户端发了包但服务器没回先确认 cookie 对不对。4.4 阶段四state 提取与分支响应拿到 packet 后用get_option提取消息类型state get_option(packet, DHCP_MESSAGE_TYPE); if (state NULL) { bb_error_msg(cannot get option from packet, ignoring); continue; }get_option按 CLVCode-Length-Value格式遍历 options 字段找到对应 code 就返回 value 指针。DHCP 的 options 是 TLV 结构code 1 字节、length 1 字节、value 变长get_option就是干这个解析的。然后找 leasestatic_lease_ip getIpByMac(server_config.static_leases, packet.chaddr); if (static_lease_ip) { static_lease.yiaddr static_lease_ip; static_lease.expires 0; lease static_lease; } else { lease find_lease_by_chaddr(packet.chaddr); }静态租约优先expires 0表示永不过期。没有静态租约就按 chaddrMAC在动态 lease 表里找。最后按 state 分支。RFC2131 规定服务器需要处理 5 种消息DISCOVER、REQUEST、DECLINE、RELEASE、INFORM。其中 REQUEST 最复杂因为客户端可能在 SELECTING、INIT-REBOOT、RENEWING、REBINDING 四种状态下发 REQUEST靠server_id和requested_ip两个选项区分有server_idSELECTING 状态校验 server_id 和 requested_ip 是否匹配本地 lease匹配就 ACK。无server_id有requested_ipINIT-REBOOTrequested_ip 和 lease-yiaddr 一致就 ACK否则 NAK。无server_id无requested_ipRENEWING/REBINDINGlease-yiaddr packet.ciaddr就 ACK否则 NAK。这个分支逻辑是 udhcpd 最容易出 bug 的地方也是你排查“客户端续租失败”时要重点看的地方。5. 验证请求抓包看 select 返回后的分支配置和源码都清楚了现在动手验证。开三个终端。终端 A启动 udhcpd 前台运行观察日志。sudo udhcpd -f -S ~/udhcp-test/udhcpd.conf终端 B抓包只看 67/68 端口。sudo tcpdump -i eth0 -n -vv port 67 or port 68终端 C模拟一个 DHCP 客户端。如果你有另一台机器接在同一网段直接用它。没有的话可以用dhcping或nmap的 dhcp-discover 脚本sudo nmap --script broadcast-dhcp-discover -e eth0或者用 busybox 的 udhcpcsudo udhcpc -i eth0 -f -q观察终端 A 的日志你应该能看到类似udhcpd: received DISCOVER udhcpd: sending OFFER of 192.168.99.100 udhcpd: received REQUEST udhcpd: sending ACK of 192.168.99.100终端 B 的抓包会显示完整的 DISCOVER - OFFER - REQUEST - ACK 四步交互。重点看 OFFER 包里的Your-IP字段和 ACK 包里的Lease-Time选项确认和配置文件一致。如果你想验证 select 超时分支把auto_time设成 10 秒重启 udhcpd观察日志里是否每 10 秒出现一次写 lease 文件的动作。可以配合strace看write系统调用sudo strace -f -e tracewrite,select,pselect6 udhcpd -f -S ~/udhcp-test/udhcpd.conf你会看到 select 的返回值和后续的 write 调用直观对应到源码里的retval 0分支。6. 本篇常见错排查问题一udhcpd 启动报 “cant open lease file”lease_file 路径的目录不存在或没写权限。确认目录存在且运行用户有写权限。用绝对路径别用~因为 udhcpd 可能不以你的用户身份运行。问题二客户端发 DISCOVER 但服务器不回 OFFER先看抓包确认 DISCOVER 到了服务器网卡。如果到了但没回检查interface配置是否和抓包接口一致。再检查start/end地址段是否和客户端所在网段匹配。如果地址池耗尽日志会打 “lease pool is full -- OFFER abandoned”。问题三select 一直返回但日志没有收包记录可能是 signal_pipe 被误触发。检查是否有其他进程在给 udhcpd 发信号。另外确认udhcp_sp_read返回 0 后代码确实走到了udhcp_get_packet可以在源码里加DEBUG打印确认。问题四客户端拿到 IP 但无法续租重点看 REQUEST 分支。如果客户端在 RENEWING 状态发 REQUESTciaddr是它当前 IPserver_id和requested_ip都不带。服务器要检查lease-yiaddr packet.ciaddr如果 lease 表里记录丢了比如 lease_file 没写成功就会走 NAK。检查auto_time是否设置确保 lease 定期落盘。问题五抓包看到 OFFER 但客户端不认检查 OFFER 里的subnet、router、dns选项格式。udhcpd 的 option 配置里IP 地址是直接写点分十进制但底层会转成 4 字节网络序。如果 option 写错客户端可能解析失败。用tcpdump -vv看 option 的十六进制值对照 RFC2132 确认。7. 继续深入与工具入口superloop 的骨架拆到这里你应该能看清 udhcpd 的单线程事件模型了socket 创建 - fd_set 组装 - select 阻塞 - 信号/收包分流 - state 分支响应。每一轮循环都是这个顺序没有并发没有回调全靠 select 的返回值和 fd_set 的状态驱动。如果你想继续往下挖下一步可以看sendOffer里find_address的地址分配策略以及write_leases的持久化格式。这两个函数直接决定地址池管理和租约恢复的正确性。需要快速查 RFC 原文、对照 udhcp 选项解析逻辑或者把 state 分支整理成状态转移图时可以用模型对话模型对话https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat如果你打算写脚本自动跑 DHCP 交互测试、批量验证不同 state 下的响应用 API Key 接入更顺手API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys接入方式看文档接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc最后留一个实操建议把auto_time设成 30 秒跑一晚上第二天看 lease 文件里的时间戳是否连续更新。如果中间有断档说明 select 超时分支或 write_leases 有问题顺着timeout_end的更新逻辑往回查基本能定位到。
返回列表