虎嗅

微软为什么不用指针,而要多绕一层?

该文章尚未提供 Русский 解读,以下为中文版内容。

核心内容总结

这篇文章通过对比Windows的HANDLE和Unix的文件描述符(fd),揭示了HANDLE设计的来龙去脉:它不是微软为了“炫技”搞的过度封装,而是为了解决1980年代末复杂商业环境下的内核安全、可维护性和多资源统一管理问题。HANDLE本质是“带权限的间接访问令牌”,通过牺牲一点性能换来了系统的强壮性和统一同步能力,但也带来了资源泄漏难查、IPC复杂等代价。最终,Linux在发展中也遇到了类似问题,不得不借鉴HANDLE的思路扩展fd功能,说明技术演进的殊途同归——没有绝对优雅的设计,只有针对特定场景的权衡。

1. 为什么微软不跟着Unix搞“一切皆文件”?

Unix的“一切皆文件”理念很优雅:打开文件给个fd(数组下标),读/写直接用fd操作,就像用吸管喝饮料一样简单。但这个优雅有个前提:所有资源都能抽象成“字节流”(比如文件、网络连接)。

可微软当时的目标不是做“更好的Unix”——它要面对混乱的商业环境:支持多处理器同时工作、精细到用户级的权限控制、还要在同一个内核上跑POSIX(Unix标准)、OS/2和Win32等多种系统。很多资源根本没法当字节流用:比如线程有执行状态、优先级,你总不能“读”一个线程吧?如果强行把这些资源塞进fd的壳子里,就会像Unix那样出现一堆难以维护的“控制码”(ioctl),就像给饮料吸管上贴满各种复杂按钮,用起来反而麻烦。

2. HANDLE到底是什么?不是指针,是“带权限的门卡”

很多人以为HANDLE是“包装过的指针”,其实它更像带权限的门卡:

  • 你拿到的HANDLE是一串无意义的数字(比如0x0000000C),不是内核资源的直接地址(就像门卡上的编号不是保险柜密码)。
  • 内核里有个“门卡登记表”(句柄表),每个HANDLE对应表里的一个槽位,槽位里才是真正的内核资源指针(比如线程、进程)。
  • 门卡还带权限:比如你拿到的HANDLE只能“终止进程”,不能“修改优先级”,就像门卡只能开大门,不能开办公室抽屉。

为什么这么设计?因为直接给用户内核地址太危险:用户程序会依赖地址的位置(比如某个字段在第3个字节),内核一旦升级改了结构,所有程序都会崩溃;而且用户可能伪造地址攻击内核(就像伪造钥匙开银行保险柜)。HANDLE相当于在用户和内核之间加了一道墙,既安全又灵活。

3. HANDLE的两大杀招:系统不崩溃+同步超简单

HANDLE的设计带来了两个关键优势:

(1)系统永远能升级,程序不用重写

因为用户只拿到HANDLE,碰不到内核的真实结构。微软从Win95到Win11,内核里的进程结构改得面目全非,但老程序照样跑——就像你家换了新锁,只要门卡不变,你不用重新配钥匙。

(2)所有资源都能用同一个“遥控器”控制

Windows里所有内核资源(线程、定时器、互斥锁)都有一个共同的“头结构”(DISPATCHER_HEADER),状态只有“有信号”或“无信号”。这意味着你可以用同一个API(WaitForMultipleObjects)等待多个资源:比如“等3个线程结束,或者1个定时器超时,或者1个互斥锁释放”——就像用万能遥控器同时控制电视、空调和灯,不用记不同的操作方式。这在1990年代初是非常震撼的设计。

4. HANDLE的三个“坑”:好用但不完美

天下没有免费的午餐,HANDLE也有代价:

(1)资源泄漏查起来头大

Unix的fd泄漏容易找(就那几个文件),但Windows里HANDLE对应各种资源(线程、事件、管道)。一个程序跑几天漏了几万个HANDLE,你根本不知道哪个是没关的事件,哪个是第三方SDK忘释放的线程——就像家里丢了一堆钥匙,不知道对应哪个锁。

(2)进程间传HANDLE太麻烦

HANDLE是绑定在进程身上的,你不能直接把自己的HANDLE发给另一个进程(就像你的门卡不能给别人用)。必须通过内核复制一份到对方的句柄表(DuplicateHandle API),步骤繁琐,增加了程序间通信的复杂度。

(3)并发能力有限制

WaitForMultipleObjects最多只能同时等64个HANDLE,现代服务器要处理几万个并发连接时,这个限制就成了瓶颈。微软后来不得不搞IOCP(I/O完成端口)、IORING来绕过这个问题——就像万能遥控器最多只能控制64个设备,家里有几百个智能设备就不够用了。

5. 殊途同归:Linux最终也活成了Windows的样子?

Linux一开始坚持“一切皆文件”,但后来也遇到了类似问题:比如用PID杀进程容易误杀(PID会重用)。于是Linux 5.1引入了pidfd_open,用fd代表进程——这个fd和HANDLE本质上一样:带权限、间接访问、不会重复。后来Linux又把定时器(timerfd)、信号(signalfd)都封装成fd,还加了权限检查。

这说明什么?技术演进到最后,大家都会遇到同样的物理约束(安全、可维护性、并发),最终选择类似的解决方案。Unix从“一切皆文件”出发,把fd变成了HANDLE;Windows从“一切皆对象”出发,用HANDLE做底座。没有谁更高贵,都是工程团队在特定时期的权衡和妥协。

最后一句话总结

HANDLE不是微软的“炫技产物”,而是解决实际问题的智慧结晶——它用间接性换来了安全和灵活,虽有缺点,但最终证明了自己的价值,甚至影响了对手的设计。技术的本质,就是在约束中找最优解。