引入

DNS 是计算机网络世界中十分基础的一个概念 / 技术,工作机制很简单,因此在许多计算机网络八股文中常常出现。

然而,我们真的理解 DNS 吗?在终端内输入 ping www.google.com 时,ping 程序的哪个函数负责把域名转变为 IP 地址?如果一个域名有多个 IP 地址,ping 了哪个?nslookupdig 等工具又是如何实现 DNS 功能的,和 ping 共用了代码吗,还是自己单独实现?

除此之外,在 Nginx 中,我们可以配置一个 upstream,随后在 proxy_pass 中采用 http://<上游名> 的格式来访问上游。可是,上游得转变为 IP 才能访问呀,谁完成了这件事?如果一个上游对应多个 server 呢?

再比如,在 docker-compose 里,我们可以用服务名来访问微服务,而不需要指定 IP。比如在 Nginx 中配置 proxy_pass 的目标是 http://backend:8000,其中 backend 是后端服务名,对应了一个容器实例。这件事又是谁做的?

再推广到 kubernetes 中,Pods 可以有多个副本,共同暴露为服务,提供了一层抽象:用户不必考虑到底连接到的是哪个 Pod,只需要在意服务是否可用。服务通过名称来标注,那么,这里又涉及到转换,怎样把名称和某个 Pod 的 IP 地址对应起来?

Curiosity is all you need.


为了解答这些问题,推出一个系列专栏,每篇文章分别讲解 DNS 在不同场景的应用。DNS 远远不只在浏览器中输入域名的过程中发挥作用!下面列出系列文章的目录:

  • DNS 是如何工作的(Part 1)——getaddrinfo 库函数;
  • DNS 是如何工作的(Part 2)——Nginxupstream
  • DNS 是如何工作的(Part 3)——docker-compose 的服务名;
  • DNS 是如何工作的(Part 4)——Docker 网络;
  • DNS 是如何工作的(Part 5)——kubernetesService

背景知识

在进入调试与实验阶段之前,先复习一些和 DNS 相关的背景知识。

DNS (Domain Name System) 是互联网的「电话簿」,把人类可读的域名转换为机器可读的 IP 地址。在这一映射名称为数字的层级式、分布式系统的帮助下,让用户可以通过简单的文字来访问网页或者使用邮件服务。

先不要把 DNS 想的那么困难,也不要纠结与琐碎的细节,就像我们对待 AI 一样,把它当成黑盒子吧。输入是域名,输出是(一个或多个)IP 地址,仅此而已。

当然,这是 A 记录或者 AAAA 记录的功能。DNS 有很多记录类型,除了实现域名到 IP 的映射外还有许多其他类型的记录,各有用处。读者可以询问 AI 或者查阅网上遍地都是的相关资料,这不是我们这篇文章的重点。

DNS 初体验

工具函数 pingnslookupdig

为了切实体会 DNS 的应用,我们先从系统自带的工具入手。pingnslookup 在 Windows 和 Linux 上都已安装,而 dig 可以通过在 Linux 安装 bind9 来实现,在 Windows 的配置比较复杂。

下面在 Windows 上展示 pingnslookup,在 Linux 上展示 dig

ping 是网络配置中常用的命令,进行过计算机网络实验的读者可能使用过这个命令,它可以用来检测两台设备间的链路是否连通。当然,和计算机网络实验的常用方式不同,ping 也支持输入域名:

1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\xialing>ping www.tongji.edu.cn

正在 Ping www.tongji.edu.cn [192.168.80.60] 具有 32 字节的数据:
来自 192.168.80.60 的回复: 字节=32 时间=5ms TTL=61
来自 192.168.80.60 的回复: 字节=32 时间=3ms TTL=61
来自 192.168.80.60 的回复: 字节=32 时间=27ms TTL=61
来自 192.168.80.60 的回复: 字节=32 时间=3ms TTL=61

192.168.80.60 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
最短 = 3ms,最长 = 27ms,平均 = 9ms

观察到,由于在学校的内网环境里,学校的 Web 服务域名返回了保留地址。


再用 nslookup 来看看,到底是哪个服务器返回的上述 IP?nslookup 允许我们和域名服务器直接交互。

1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\xialing>nslookup
默认服务器: dnscache1.tongji.edu.cn
Address: 202.120.190.208

> www.tongji.edu.cn
服务器: dnscache1.tongji.edu.cn
Address: 202.120.190.208

非权威应答:
名称: www.tongji.edu.cn
Addresses: 2001:da8:b8:80:192:168:80:60
192.168.80.60

喔,看来我们默认的 DNS 服务器是 dnscache1.tongji.edu.cn,凭什么?

网络连接详细信息

看看网络的信息,喔,原来指定了 IPv4 DNS 服务器,其中首要服务器的 IP 就是 dnscache1.tongji.edu.cn,嗯,一切很清楚。

是不是有点怪?明明网络配置中是 IP,为什么 nslookup 中展示的是域名?

PTR 查询

计算机的世界没有魔法,原来是 nslookup 会在启动后对 DNS 服务器自动进行一次 PTR 查询。不过这是支线任务,不重要。在现在的 AI 时代,一个 prompt 便能够解答。嗯,看上去我的计算机现在非常安静,原来网卡面临的是波涛汹涌的世界呢。

这是 Windows nslookup 的行为,bind9 的 nslookup 不会做 PTR 查询。


dig 的作用和 nslookup 类似,都是和 DNS 服务器交互,但是功能更强大,读者一看便知。可以通过下方的指令从根服务器开始,逐层地向域名服务器查询某个域名的 DNS 记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
$ dig www.tongji.edu.cn +trace
; <<>> DiG 9.18.39-0ubuntu0.24.04.2-Ubuntu <<>> www.tongji.edu.cn +trace
;; global options: +cmd
. 6707 IN NS g.root-servers.net.
. 6707 IN NS i.root-servers.net.
. 6707 IN NS e.root-servers.net.
. 6707 IN NS b.root-servers.net.
. 6707 IN NS c.root-servers.net.
. 6707 IN NS k.root-servers.net.
. 6707 IN NS l.root-servers.net.
. 6707 IN NS m.root-servers.net.
. 6707 IN NS d.root-servers.net.
. 6707 IN NS f.root-servers.net.
. 6707 IN NS h.root-servers.net.
. 6707 IN NS a.root-servers.net.
. 6707 IN NS j.root-servers.net.
;; Received 239 bytes from 127.0.0.53#53(127.0.0.53) in 0 ms

;; UDP setup with 2001:500:2::c#53(2001:500:2::c) for www.tongji.edu.cn failed: network unreachable.
;; no servers could be reached
;; UDP setup with 2001:500:2::c#53(2001:500:2::c) for www.tongji.edu.cn failed: network unreachable.
;; no servers could be reached
;; UDP setup with 2001:500:2::c#53(2001:500:2::c) for www.tongji.edu.cn failed: network unreachable.
;; UDP setup with 2001:503:ba3e::2:30#53(2001:503:ba3e::2:30) for www.tongji.edu.cn failed: network unreachable.
;; UDP setup with 2001:500:2f::f#53(2001:500:2f::f) for www.tongji.edu.cn failed: network unreachable.
cn. 172800 IN NS a.dns.cn.
cn. 172800 IN NS b.dns.cn.
cn. 172800 IN NS c.dns.cn.
cn. 172800 IN NS d.dns.cn.
cn. 172800 IN NS e.dns.cn.
cn. 172800 IN NS ns.cernet.net.
cn. 86400 IN DS 33094 8 2 CCCF13ED73A83244F7D2936F0B6C3507D85C3EBC5E1BE4FB644064BC 5B5FE3B2
cn. 86400 IN RRSIG DS 8 1 86400 20260501050000 20260418040000 54393 . m2WKxn+ABtAN48KRw9LTswYM8CA2N62SXpu1AIQHlEksbj+38SD2IHqy Ko8KN3jy7wIAqjftn5AhNlJDR3Xrse+pE4Y8sULkOpjezaGtbN1einej HKqh5nTrBmMOI37ftMp9RQ9uMIoXx5dm6iqWnAjwzMTlpvg0INqtUZ1Z RIsUxnw3IzomMxx6VR71rdE7SgXNaQFtXb4aXhPBCJIpN4peUWmeDyB3 hBv0t10F20Or9Cn425OXMUnC6VhR1tJEeJHEzV4QQKsdHfin5qvqMREh gU7/3GhSw2Bk1eYysAV2PnX5MGE6GAH1sg7pJQKIVWOj6FOJ3clcvwDX yADhSQ==
;; Received 728 bytes from 198.97.190.53#53(h.root-servers.net) in 153 ms

;; UDP setup with 2001:dd9::44#53(2001:dd9::44) for www.tongji.edu.cn failed: network unreachable.
;; UDP setup with 2001:da8:1:100::44#53(2001:da8:1:100::44) for www.tongji.edu.cn failed: network unreachable.
;; UDP setup with 2001:250:c006::44#53(2001:250:c006::44) for www.tongji.edu.cn failed: network unreachable.
;; UDP setup with 2001:dc7::1#53(2001:dc7::1) for www.tongji.edu.cn failed: network unreachable.
edu.cn. 172800 IN NS dns.edu.cn.
edu.cn. 172800 IN NS ns2.cernet.net.
edu.cn. 172800 IN NS ns4.cernet.net.
edu.cn. 172800 IN NS ns5.cernet.net.
edu.cn. 172800 IN NS dns2.edu.cn.
edu.cn. 172800 IN NS dns3.edu.cn.
edu.cn. 86400 IN DS 15397 8 2 3A6C89D32B3143D193521CE64389548821DA90F770AB09ECD9C8680B 2F4848B5
edu.cn. 86400 IN RRSIG DS 8 2 86400 20260506160830 20260406151642 55980 cn. Hb08oCD/IW5cx6ST9zCi7Uqg+j48ydvwbjxbA+7issg7r2oJ7+WFwmYr 61vktzvItcPf6VuKe28fM//RjblPDp6/TxclguOcBShPtFJW+HberF+5 Wdh514UfEb5wEmniTMULWyTiQre2SomBV41MnWxGGzC97HBAGKawQgCn Zps=
;; Received 546 bytes from 203.119.25.1#53(a.dns.cn) in 104 ms

;; communications error to 101.4.62.35#53: timed out
tongji.edu.cn. 172800 IN NS dns2.tongji.edu.cn.
tongji.edu.cn. 172800 IN NS dns1.tongji.edu.cn.
tongji.edu.cn. 172800 IN NS dns.tongji.edu.cn.
7BIE74R29CSII30PR1H5U8D10IJ93L7D.edu.cn. 21600 IN NSEC3 1 1 0 - 7L95KCFNNQRB0609QQTSVPQV4I0TQ4KI NS SOA RRSIG DNSKEY NSEC3PARAM CDS CDNSKEY
7BIE74R29CSII30PR1H5U8D10IJ93L7D.edu.cn. 21600 IN RRSIG NSEC3 8 3 21600 20260429024136 20260415035544 44583 edu.cn. VPpl3CMtiXYDVmV1pTu8T/WBX9apoYQ36PFKzYThcVZYP/dgtE6V+VuD VLOLdFtzmd6c7Ft4M2YUhGdFaXfAQ12TvO8o4rqa6DoyGFhNat5c9ZM9 BJ0fkh3cG/O/kdBKU8V7WLpHE8YGtDXfO7Au0EY43mDjqPqYxaXW0QJF HNfyF9/X6SIDDo3FYtclqGkAGpQsvg33U+o/mG+TdZtVPGz8iPWxe7I5 9ljsZQQZqi7JWT7gYJz4QDWBei3vDh/uW8jey3vksbnnGcyhXeO8FxQX gIhkKOOZFCeinhuSpEBwbG0nLkooNryFtPyfjHrDpCMcBgIo5W7r7sQ9 dVOqqw==
TCM6E1O4D70HBNKEKDIPKI94CU4RP1FJ.edu.cn. 21600 IN NSEC3 1 1 0 - TUHB8HAIDQUHFAT0MJSVJV2JESCOGC66 NS DS RRSIG
TCM6E1O4D70HBNKEKDIPKI94CU4RP1FJ.edu.cn. 21600 IN RRSIG NSEC3 8 3 21600 20260423201119 20260409192012 44583 edu.cn. jrMpLXchohdn0dN4bG4ELPd4Vl8w9EcMCIAuX11GoZpQaqn+RA7aIKvr 9yoHrteGUTqCE0H66PJlV4ZA58of1j59YNde6ON+RxUdPvG8WixwJkrU xY7pAJnucXjCCCCmoncOMk9geceMfCCURhCxcdOv4u1n9gXBcEizCIvX H2ZJt3DytTGtO71KwBd+Mgu/A5/uNmsb9PDrtbZDWlGwRJZZyoNsgufE Bn6vrR+87PoYBFrXlU8elABVQQDhNfQ3yRRBl/p9whXnUk/jRL6QjSXm fNe1lj4gaKbYkLXn4KRyFGUOTO2GsBfcsUQaddpNnQ9n3cr0sYS1j6NH UWLaTw==
;; Received 1016 bytes from 101.4.62.203#53(ns4.cernet.net) in 108 ms

;; UDP setup with 2001:da8:b8:277:202:120:191:30#53(2001:da8:b8:277:202:120:191:30) for www.tongji.edu.cn failed: network unreachable.
;; communications error to 222.66.109.33#53: timed out
;; UDP setup with 2001:da8:b8:277:222:66:109:33#53(2001:da8:b8:277:222:66:109:33) for www.tongji.edu.cn failed: network unreachable.
www.tongji.edu.cn. 3600 IN A 202.120.189.175
tongji.edu.cn. 3600 IN NS dns2.tongji.edu.cn.
tongji.edu.cn. 3600 IN NS dns.tongji.edu.cn.
tongji.edu.cn. 3600 IN NS dns1.tongji.edu.cn.
;; Received 250 bytes from 202.120.191.30#53(dns.tongji.edu.cn) in 231 ms

上述查询展示了 DNS 迭代解析的完整过程。客户端会先向本地 DNS 服务器(127.0.0.53) 查询根域名服务器地址,这就到了树的根部,一切故事的开始。

随后,从根服务器列表中随机选一个,问,www.tongji.edu.cn 的 IP 是什么呀?某个根服务器说,我不知道,你去问问 .cn 的顶级域名服务器去。客户端从后者再选一个问同样的问题,.cn 的域名服务器踢皮球给 edu.cn,以此类推,直到到达权威服务器,也就是直接管理这条记录的服务器。

权威服务器,在这里有 3 个:dns/dns1/dns2.tongji.edu.cn,我们选了 dns.tongji.edu.cn,它告诉我们,答案是 202.120.189.175

因为服务器不在校园网环境内,所以返回的是域名的公网地址。

看看它们的源码并调试

现在我们进入到 Linux 的环境吧,方便进行一些操作。首先,需要找到我们使用的二进制文件的源码是什么。怎么做?去搜索引擎询问吗?其实 Linux 倡导开源,因此这有一套标准的做法:

  1. 首先,使用 which <二进制名> 查看当前二进制的磁盘位置;
  2. 之后,使用 dpkg -S <路径> 来定位路径上的二进制文件属于的包;
  3. 使用 apt show <包名> 查看和包相关的信息。

ping 为例,使用一套组合拳,其中 Homepage 展示了源码仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xialing@natsu:~$ apt show $(dpkg -S $(which ping) | cut -d: -f1)
Package: iputils-ping
Version: 3:20240117-1ubuntu0.1
Priority: important
Section: net
Source: iputils
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Noah Meyerhans <noahm@debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 124 kB
Provides: ping
Depends: libcap2-bin, libc6 (>= 2.38), libcap2 (>= 1:2.10), libidn2-0 (>= 0.6)
Homepage: https://github.com/iputils/iputils/
Task: minimal
Download-Size: 44.6 kB
APT-Manual-Installed: no
APT-Sources: http://mirrors.tencentyun.com/ubuntu noble-updates/main amd64 Packages
Description: Tools to test the reachability of network hosts
The ping command sends ICMP ECHO_REQUEST packets to a host in order to
test if the host is reachable via the network.
.
This package includes a ping6 utility which supports IPv6 network
connections.

nslookupdig 的主页是下载地址,不过从下载地址可以定位到 Gitlab 仓库

git clone --depth 1 下载源码到本地吧。

根据各自仓库的 README 说明进行编译,看看 ping,使用 gdb 调试,感兴趣的读者可以学习古法调试的方式,觉得这个方法太老套的读者可以用 VSCode 来调试。

ping 实现域名-> IP 的方式

nslookup 调用了 getaddrinfo

dig 也调用了 getaddrinfo

这么看,原来它们都使用了 getaddrinfo 函数,所以我们只需要继续调查这个库函数的描述便 OK 了?

没那么简单。阅读 getaddrinfo 的 man page,我们发现它返回的是主机名的 IP 地址。换句话说,这个函数只能完成 DNS 查询中和 A 记录以及 AAAA 记录的功能。
除此之外,DNS 还能查询各种其他类型的记录,这是 dignslookup 这种专门处理 DNS 的工具所必备的,然而该库函数无法满足。

getaddrinfo 库函数之外

所以进行这样的推理,我们确定 dignslookup 肯定自行构建了 DNS 报文,而没有通过 getaddrinfo 函数。

确实是这样的。通过调试这两个程序,发现 dignslookup 共用了很大一部分代码,这是 AI 给我画出的关系图,我觉得很受用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────┐    ┌───────────────────────┐
│ dig.c │ │ nslookup.c │
│ (parse cli) │ │ (interactive gui) │
└────────┬────────┘ └────────────┬──────────┘
│ │
└────────────┬─────────────┘

┌───────────────────────────────┐
│ dighost.c │
│ (core DNS logic) │
│ • construct DNS packet │
│ • send/recv │
│ • parse reply │
└───────────────────────────────┘

对应到源代码,dig.c 中的 dig_startup 函数调用 isc_loopmgr_run 函数;而 nslookup.c 中的 main 函数也会调用该函数。所以我们接下来只需要考察这个函数,因为两个程序共用了相同的底层代码。

发送 DNS 请求的核心是 run_loop 函数,它在 isc_loopmgr_setup 中被注册,在 isc_loopmgr_run 中被执行。这里面涉及到了复杂异步机制,不是本文的重点,感兴趣的读者可以自行探究,总之,下一步我们需要考察 run_loop 函数。

1
2
isc_loopmgr_setup(run_loop, NULL);
isc_loopmgr_run();
1
2
3
4
5
6
7
// dighost.c:4563
void
run_loop(void *arg) {
UNUSED(arg);

start_lookup();
}

其实还有一种更方便的方式来进行函数调用的跟踪。对于 dig,可以使用 dig -d 开启 debug,对于 nslookup 则是 nslookup -d2,会打印出源代码中所有 debug() 中的内容。

总之,最后我们定位到 send_udp 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// dighost.c:3020
static void
send_udp(dig_query_t *query) {
dig_query_t *sendquery = NULL;
isc_region_t r;

query_attach(query, &sendquery);

isc_buffer_usedregion(&query->sendbuf, &r);
debug("sending a request");
if (query->lookup->use_usec) {
query->time_sent = isc_time_now_hires();
} else {
query->time_sent = isc_time_now();
}

isc_nmhandle_attach(query->handle, &query->sendhandle);

isc_nm_send(query->handle, &r, send_done, sendquery);
isc_refcount_increment0(&sendcount);
debug("sendcount=%" PRIuFAST32, isc_refcount_current(&sendcount));

/* XXX qrflag, print_query, etc... */
if (query->lookup->qr) {
extrabytes = 0;
dighost_printmessage(query, &query->lookup->renderbuf,
query->lookup->sendmsg, true);
if (query->lookup->stats) {
print_query_size(query);
}
}
}

这里的关键是 isc_nm_send 这个包装的函数,它会调用 isc__nm_udp_send。根据发送队列的大小,会选择调用同步的 uv_udp_try_send 或者异步的 uv_udp_send

然后,到这里,我们卡住了,无法查看 uv_ 开头的函数内部。

这是因为,uv_ 函数属于 libuv,一个跨平台的异步 IO 库,它通过动态链接的方式加载且没有调试符号。

为了调试,可以从仓库中下载源码来查看。我们关注 uv_udp_send 函数吧。经过层层函数调用,uv__udp_sendmsgv 函数会发送消息,调用库函数 sendmmsg

如果想考察 sendmmsg 的实现,就需要看标准库的源码了。

在考察标准库之前,我们可以得到两个结论:

  1. libuv 依赖标准库;
  2. dig / nslookup 自己构造 UDP 报文,不依赖标准库的 getaddrinfo 接口。

或许读者有疑问,为什么在调试 dignslookup 时还会使用 getaddrinfo

这是因为,DNS 查询需要确定向哪个服务器发送查询请求,默认的配置在 Linux 系统中存放在 /etc/resolv.conf 下,其中的 nameserver 字段记录了默认 DNS 服务器

比如在之前 gdb 的截图中,可以看到 nslookupdig 传入 getaddrinfo 的参数是 127.0.0.53 以及 53 端口,即我服务器 /etc/resolv.conf 配置的默认 DNS 服务器 systemd-resolved)。

getaddrinfo 可以把字符串格式的 IP 地址 (e.g. 8.8.8.8) 转换为机器内部的数值格式。

下面开始调试标准库了,我们只调试 getaddrinfo 这个为我们做了好多杂事的库函数,sendmmsg 比较底层,相较于纯系统调用封装的内容不多,感兴趣的读者可以自行探究。

调试 getaddrinfo 函数

标准库有许多实现,glibc 是 Linux 系统的标准库,但是历史包袱过多,不易学习;musl 是一个更轻量化的实现。所以为了调试 C 标准库,尝尝鲜,选择 musl。

然而,像 ping、bind9 这种项目并不一定对 musl 有很好的支持,就算支持,编译构建起来也没那么简单,所以编写一个最小化测试程序,尽量减少依赖,聚焦于 getaddrinfo 这个库函数。

编写测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/* Simple DNS resolver using getaddrinfo
* Compile with musl: musl-gcc -g -O0 -o dns_resolve dns_resolve.c
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
struct addrinfo hints;
struct addrinfo *result, *rp;
int err;
char ipstr[INET6_ADDRSTRLEN];

if (argc != 2) {
fprintf(stderr, "Usage: %s <hostname>\n", argv[0]);
return 1;
}

const char *hostname = argv[1];

/* Clear hints structure */
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */
hints.ai_socktype = SOCK_STREAM; /* Stream socket */
hints.ai_flags = AI_PASSIVE; /* For wildcard IP address */

printf("Resolving: %s\n", hostname);
printf("Calling getaddrinfo()...\n\n");

/* Call getaddrinfo - this is the function we want to debug! */
err = getaddrinfo(hostname, NULL, &hints, &result);

if (err != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(err));
return 1;
}

printf("getaddrinfo() returned successfully!\n\n");

/* Iterate through results */
printf("Results:\n");
for (rp = result; rp != NULL; rp = rp->ai_next) {
void *addr;
const char *ipver;

if (rp->ai_family == AF_INET) {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)rp->ai_addr;
addr = &(ipv4->sin_addr);
ipver = "IPv4";
} else if (rp->ai_family == AF_INET6) {
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)rp->ai_addr;
addr = &(ipv6->sin6_addr);
ipver = "IPv6";
} else {
continue;
}

/* Convert binary IP to string */
inet_ntop(rp->ai_family, addr, ipstr, sizeof(ipstr));
printf(" %s: %s\n", ipver, ipstr);
}

freeaddrinfo(result);
return 0;
}

AI 给我写了一份非常好的测试代码。为了能够进入到标准库函数的内部进行调试,要做这样几件事情:

  1. 克隆 musl 的源码到本地;
  2. 编译 musl-gcc,需要 -g 带有调试符号;
  3. 使用 musl-gcc 编译该程序,也需要 -g 带有调试符号;
  4. gdb 或者 VSCode 的 launch.json 中用 directory 命令指明标准库源码的路径。

调试符号就像路标,能对应行号和源代码的位置。光有路标还不够,必须携带源代码,才能真正复原到源代码视图。如果只有路标,还是能够到达终点,但走的比较盲目,换言之,看不到源代码的风景。

上述操作,在 AI 的帮助下都不成问题。

宏观认知——straceltrace

在使用 gdb 或者 VSCode 动态调试前,其实可以先执行 straceltrace 命令看看使用了哪些系统调用或者库函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
execve("./dns_resolve", ["./dns_resolve", "www.tongji.edu.cn"], 0x7ffc1af8d548 /* 55 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x79044cac4b08) = 0
set_tid_address(0x79044cac4f70) = 335599
brk(NULL) = 0x5fe178fe0000
brk(0x5fe178fe2000) = 0x5fe178fe2000
mmap(0x5fe178fe0000, 4096, PROT_NONE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x5fe178fe0000
mprotect(0x5fe13f916000, 4096, PROT_READ) = 0
arch_prctl(ARCH_SET_FS, 0x5fe13f917718) = 0
set_tid_address(0x5fe13f9176f0) = 335599
ioctl(1, TIOCGWINSZ, 0x7ffce88924c8) = -1 ENOTTY (Inappropriate ioctl for device)
writev(1, [{iov_base="Resolving: www.tongji.edu.cn", iov_len=28}, {iov_base="\n", iov_len=1}], 2) = 29
open("/etc/hosts", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
fcntl(3, F_SETFD, FD_CLOEXEC) = 0
read(3, "#\n127.0.1.1 localhost.localdomai"..., 1024) = 212
read(3, "", 1024) = 0
close(3) = 0
open("/etc/resolv.conf", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
fcntl(3, F_SETFD, FD_CLOEXEC) = 0
read(3, "# This is /run/systemd/resolve/s"..., 248) = 248
read(3, "# This is a dynamic resolv.conf "..., 248) = 248
read(3, "rrently in use.\n#\n# Third party "..., 248) = 248
read(3, "link.\n#\n# See man:systemd-resolv"..., 248) = 176
read(3, "", 248) = 0
close(3) = 0
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
sendto(3, "Y?\1\0\0\1\0\0\0\0\0\0\3www\6tongji\3edu\2cn\0\0"..., 35, MSG_NOSIGNAL, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, 16) = 35
sendto(3, "[>\1\0\0\1\0\0\0\0\0\0\3www\6tongji\3edu\2cn\0\0"..., 35, MSG_NOSIGNAL, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, 16) = 35
poll([{fd=-1}, {fd=-1}, {fd=3, events=POLLIN}], 3, 2500) = 1 ([{fd=3, revents=POLLIN}])
recvmsg(3, {msg_name={sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, msg_namelen=16, msg_iov=[{iov_base="Y?\201\200\0\1\0\1\0\0\0\0\3www\6tongji\3edu\2cn\0\0"..., iov_len=4800}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 51
recvmsg(3, {msg_name={sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, msg_namelen=16, msg_iov=[{iov_base="[>\201\200\0\1\0\1\0\0\0\0\3www\6tongji\3edu\2cn\0\0"..., iov_len=4800}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 63
close(3) = 0
socket(AF_INET6, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_UDP) = 3
connect(3, {sa_family=AF_INET6, sin6_port=htons(65535), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2001:da8:b8:80:192:168:80:60", &sin6_addr), sin6_scope_id=0}, 28) = -1 ENETUNREACH (Network is unreachable)
close(3) = 0
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_UDP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(65535), sin_addr=inet_addr("202.120.189.175")}, 16) = 0
getsockname(3, {sa_family=AF_INET, sin_port=htons(37930), sin_addr=inet_addr("10.3.4.11")}, [16]) = 0
close(3) = 0
brk(NULL) = 0x5fe178fe2000
brk(0x5fe178fe4000) = 0x5fe178fe4000
mmap(0x5fe178fe2000, 4096, PROT_NONE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x5fe178fe2000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x79044ca0e000
munmap(0x79044ca0e000, 4096) = 0
writev(1, [{iov_base="Calling getaddrinfo()...\n\ngetadd"..., iov_len=134}, {iov_base=NULL, iov_len=0}], 2) = 134
exit_group(0) = ?
+++ exited with 0 +++

ltrace 捕捉不到任何库函数的使用,因为 musl 将库函数静态链接到可执行文件中,ltrace 无法插入追踪代码。

strace 的输出中,有几个有意思的地方:

  • 使用 writev 系统调用往终端输出内容;
  • 使用 open 系统调用打开 /etc/hosts/etc/resolv.conf 文件;
  • 使用 read 系统调用读取文件内容;
  • 使用 socket 系统调用创建套接字,随后 bindsendtopollrecvmsg 组合拳得到了解析结果;
  • 通过 connect 测试连通性(Happy Eyeballs 算法)。

这里,/etc/hosts 是本地的 DNS 文件,持久化了一些记录。在向 DNS 服务器请求之前,先查查本地有没有记录,如果有就不必请求了。

1
2
3
4
5
6
7
8
9
10
11
# /etc/hosts
#
127.0.1.1 localhost.localdomain VM-4-11-ubuntu
127.0.0.1 localhost

::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts

/etc/resolv.conf 用来告知 Linux 如何处理 DNS 请求,其中的 nameserver 字段指定了查询要发送到的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# /etc/resolv.conf
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
#
# This file might be symlinked as /etc/resolv.conf. If you're looking at
# /etc/resolv.conf and seeing this text, you have followed the symlink.
#
# This is a dynamic resolv.conf file for connecting local clients to the
# internal DNS stub resolver of systemd-resolved. This file lists all
# configured search domains.
#
# Run "resolvectl status" to see details about the uplink DNS servers
# currently in use.
#
# Third party programs should typically not access this file directly, but only
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
# different way, replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.

nameserver 127.0.0.53
options edns0 trust-ad
search .

使用 gdbVSCode 调试代码

使用调试器调试标准库,关键在符号表和标准库源码。这点不必废话,把这句话喂给 AI,它能给出详尽的配置方案。我们只需要在 VSCode 中打上断点,便能进入到库函数中了。

函数调用关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
getaddrinfo()  [src/network/getaddrinfo.c:12]                                                                                  

├── __lookup_serv() [src/network/lookup_serv.c:12]
│ ├── 解析数字端口 (如 "80")
│ └── 读取 /etc/services 查找服务名

└── __lookup_name() [src/network/lookup_name.c:308] *核心函数*

├── name_from_null() [lookup_name.c:26]
│ └── host 为 NULL 时返回 localhost (127.0.0.1 / ::1)

├── name_from_numeric() [lookup_name.c:44]
│ └── __lookup_ipliteral() [src/network/lookup_ipliteral.c:12]
│ ├── __inet_aton() 解析 IPv4 (如 "192.168.1.1")
│ └── inet_pton() 解析 IPv6 (如 "::1")

├── name_from_hosts() [lookup_name.c:49]
│ └── 读取 /etc/hosts 文件查找

└── name_from_dns_search() [lookup_name.c:190]

├── __get_resolv_conf() [src/network/resolvconf.c:9]
│ └── 读取 /etc/resolv.conf 获取 nameserver 和 search 域

└── name_from_dns() [lookup_name.c:143]

├── __res_mkquery() [src/network/res_mkquery.c:5]
│ └── 构造 DNS 查询包 (A 记录和 AAAA 记录)

├── __res_msend_rc() [src/network/res_msend.c:79]
│ ├── socket() 创建 UDP socket
│ ├── bind() 绑定本地地址
│ ├── sendto() 发送查询到 nameserver
│ ├── poll() 等待响应
│ └── recvmsg() 接收 DNS 响应
│ └── (TCP fallback: 如果响应被截断)

└── __dns_parse() [src/network/dns_parse.c:4]
└── dns_parse_callback() [lookup_name.c:114]
└── __dn_expand() 解压域名

这里面的函数调用过程,AI 帮我绘制的树结构阐释得非常清楚。整体来看,符合我们的认知:先查本地文件,没找到记录再发送 DNS 请求这个基本道理反映在了代码中。除此之外,还有一些细节机制,如输入为 null 如何处理,如果已经是 IP 地址字符串如何处理等等。

关于 __lookup_serv(),在 DNS (绝大多数)记录中,不关心端口号,或者说服务,为什么这里要先解析数字端口?

AI 给我的回答是,这是历史包袱问题。getaddrinfogethostbyname 的下一代 API。相较于 gethostbynamegetaddrinfo 可以一站式提供创建 socket 的全部要素,而 gethostbyname 只能得到 IP,其他的数值要手动填写。

更多的细节可以阅读这些库函数的 man page。

实际上,在讨论 Nginx 的下一篇文章中,我们可以看到 Nginx 是如何同时兼容这两个接口的。这样,从 Nginx 这个应用程序的视角来看这两个库函数的作用,可以帮助我们理解概念。

Takeaway

  1. pingnslookup 等与 DNS 相关的应用程序通过 getaddrinfo 或者手动构造 DNS 请求来查询 DNS 信息;
  2. getaddrinfo 的局限性是只能查询 IP 地址,因此对于 dig 等支持各种 DNS 记录类型查询的工具来说并不够,它们自己构造 DNS 请求;
  3. getaddrinfogethostbyname 的下一代 API,一站式提供了创建 socket 的所有参数,简化编程;
  4. getaddrinfo 在查询 DNS 时,按照空输入、IP 地址、本机文件和实际发送请求的顺序来进行查询,发送目标取决于 /etc/resolv.conf 中的 nameserver
  5. 不管到底是如何发送 DNS 请求的,应用程序都没有直接和系统调用交互,而是使用标准库封装的函数或者 libuv 封装的函数(libuv 还依赖标准库);
  6. musl 是一个历史包袱很少、便于学习的标准库,通过下载源码以及编译时添加调试符号,可以调试到标准库内部。

既然 AI 已经能把整个流程分析的很好,要我们有何用?

掌控代码, from [jyy](https://jyywiki.cn/OS/2026/lect1.md)

确实现在 AI 辅助编程,处理一些业务代码,甚至是底层代码都 OK 了,但我认为,只有了解底层机制,才能真正驾驭代码,否则代码会变成屎山。

就我个人的编程经验来看,如果是为了理解原理的简单 demo,我基本能够完全看懂 AI 生成的代码。然而,在实际系统中,比如我从 pre-AI 时代开始编写的一个辅助排课的服务,在 AI 的帮助下缝缝补补。就这么一个非常简单的单页面应用,涉及到前端的 js 逻辑以及后端复杂的 SQL 查询时,我已经有些力不从心了。

当然,可以解释成我志不在此,我不喜欢前端和 SQL,不想深究,只要过了测试用例便可(虽然我没有写测试用例。。)。但对于自己感兴趣的领域,有必要看一看到底这一切是如何进行的,Read the friendly manual。头脑中的一级结论足够了,联想推出更多的二级结论才自然流畅。

相较于(敏捷)开发追求速度的目标,SRE 对于稳定性有执念。现在的应用基本上需要 24*7 地运转,容不下出错的差池。AI 生成的代码进一步加快了速度,而在稳定性上,并没有很大帮助。

这样进行下去早晚会有一个临界点,要么 AI 能力绝对领先,以后的代码 AI 生成,AI review,没人类什么事了;要么 AI 管杀不管埋,生成一堆 poop,又回到人类开发的史前阶段。我觉得后者的概率大一些,当然,有可能我是错的。

Where are you?

想起来程序设计老师讲过的一个概念与思考,到底自己的职业期望是什么?希望加入哪个团队?

老师给的例子是二元的,就这个例子来说,乙团队更不可替代。但计算机是一个层层抽象的结构,乙团队底层可能还要依赖丙团队,一直到 IC 工程师这一层。所以,我觉得这页 PPT,朝花夕拾,现在来看,我能得到的 takeaway message 是:

找到你既感兴趣、又能做到前 20% 的那一层,比盲目追求”最底层”更实际。真正的技术人,是在自己负责的层次上知道下一层在干什么,而不是假装自己无所不知。

找到自己的 niche。