Madeira 单进程模型深度解析:为什么所有 Windows 进程都是线程及其 6 大后果
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
Madeira 是让 iPhone 直接运行 x86-64 Windows PC 游戏的开源项目,它通过 FEX-Emu + Wine + DXMT 把未经修改的 Windows 游戏装进单个 iOS App 里。这套玩法能成立的关键,正是它的单进程模型:iOS 应用无法启动其他程序,于是 Madeira 把 Wine 的全部"进程"都变成了同一个 App 内的线程。本文带你从新手视角看懂这个模型,以及它带来的 6 个真实后果。
一、单进程模型是什么?为什么非这么做不可?
先建立直觉。在普通 PC 上,Wine 的架构是"一主多从":
- 一个独立的wineserver守护进程,负责窗口、文件、注册表等系统服务的仲裁
- 每个 Windows 程序(
explorer.exe、游戏本体、Steam 客户端……)各自是一个真正的操作系统进程 - 它们通过 Unix 套接字互相通信,崩溃时互不影响
但 iOS 的沙箱有两条硬性限制:
- 应用不能 fork/exec 启动新程序—— 你看到的每个 App 只能是一个进程
- 信号(signals)是进程级别的—— 无法用来"精准击杀"某一个 Windows 程序
于是 Madeira 做了一个大胆的决定:
"iOS 应用不能启动其他程序,所以一切都跑在一个进程里:连 Wine 的服务器都变成线程而不是独立程序。" —— 见 README.md
也就是说,打开 Madeira 后,你的 iPhone 里只有一个 Mach 任务(一个 iOS 进程),里面住着:
| 角色 | 在 PC 上 | 在 Madeira 里 |
|---|---|---|
| 应用界面(SwiftUI) | 不存在 | 主线程组 |
| wineserver | 独立守护进程 | 一个线程 |
| explorer.exe | 独立进程 | 一个"伪进程"(线程) |
| 游戏本体 | 独立进程 | 一个"伪进程"(线程) |
🧵 所谓"伪进程"(pseudo-process),就是一组共享同一线程局部身份(TEB/PEB)的线程,对外表现得像一个 Windows 进程。
二、它是怎么实现的:"启动进程"= 创建一个线程
在标准 Wine 里,spawn_process的核心动作是fork()两次再exec加载器。而 Madeira 在 iOS 上把这段整个换掉了:
- 原来的
fork + exec被替换成pthread_create—— 直接新建一个线程,把命令行参数、工作目录、一个专用 socket 打包进去 - 新线程进入
wine_ios_child_main,像真正的加载器一样把 Windows 可执行文件"拉起来" - 线程退出时用
setjmp/longjmp模拟进程的结束码,并清理该"进程"占用的资源
实现就在 process_ios.c(spawn_process的 iOS 分支)和 process_ios.c(子线程入口ios_child_thread_entry)。
wineserver 那边同样被"收编"。在 main_ios.c 中可以看到两处关键改造:
- 忽略了
SIGTERM/SIGINT等信号——因为信号是进程级的,任何一个 Wine 客户端线程触发信号都会杀死整个 App - 主套接字超时设为无限——不能像原版那样"3 秒没客户端就
exit(0)",否则exit()会带走整个 App
三、单进程模型的 6 大后果
这是本文的重点。共享一个进程意味着共享一切:文件描述符表、信号、内存、锁。每一条都直接塑造了 Madeira 的行为。
后果 1:一个"进程"崩溃,可能带走整个 App
PC 上杀掉steam.exe不会伤到游戏;但在 Madeira 里,所有伪进程住在同一个 Mach 任务中,任何未处理的崩溃(如空指针访问)都可能让 iOS 直接把整个 App 打下来。
代码注释里就记录过一次真实事故:Steam 的steamsysinfo.exe(硬件探测工具)触发一次故障后,"因为这里每个 Windows '进程'都是同一个 Mach 进程内的伪进程,这次故障带走了整个 App"。
💡 这也是为什么玩家偶尔会遇到"登录窗口黑屏后 App 直接关闭"——那是某个后台子进程崩溃的连带伤害。
后果 2:文件描述符表是全局共享的
普通进程各自持有 fd 表;单进程模型下,wineserver 和所有游戏线程共用同一张 fd 表。一个"进程"误关了别的"进程"正在用的通信管道,就会让其他所有人卡死。
为此 Madeira 建了一套完整的"fd 账本"(fdtrace):谁注册了哪个 fd、谁在什么场景下关闭它,都要登记核对,一旦发现跨属主关闭立刻高亮告警,见 server_ios.c。
后果 3:信号杀不了线程,只能"关管道 + 延迟退出"
Linux 上 wineserver 用SIGQUIT杀线程;但 iOS 里信号到不了特定线程,被"杀"的线程会一直跑着,直到某次请求发现管道已关闭。
更隐蔽的坑是:被杀的线程可能正握着某把全局互斥锁。由于所有 Windows 进程共享这些锁,它会让所有进程的后续请求永久阻塞(真实案例:msiexec 被杀后,等待它的主程序永远挂在NtClose上)。解决方案是"延迟退出"——被杀线程先失败掉当前请求、走到最外层代码块再退出,见 server_ios.c。
后果 4:32 位游戏要抢一块"稀缺地址空间"
32 位游戏通过 WoW64 层运行,必须占用 2GB 以下的"客户地址窗口"——这块空间在单进程里是全局唯一且不可重占的。
因此当一个 32 位伪进程(比如游戏的启动器)退出时,必须把它的地址窗口归还,下一个 32 位伪进程才能接手这个槽位。这条"释放窗口"逻辑直接写在子线程退出路径里,见 process_ios.c。32 位支持的更多细节可以看 docs/WOW64.md。
后果 5:内存压力会互相传染
所有伪进程共享同一个地址空间和 JIT 编译池。一个不起眼的后台子进程(比如硬件探测工具)加载 Python 运行时和 COM 初始化,就可能吃掉大几百 MB 的 JIT 池空间,把整个游戏的性能基线拉低。
这也是单进程模型下调试必须"全局看内存"的原因:一个线程的分配,就是所有线程的问题。
后果 6:必须维护一张"进程黑名单"
既然不能阻止所有子进程,又不能让危险子进程崩掉全家,Madeira 选择在NtCreateUserProcess门口设了一道闸门,直接拒绝启动几类"可有可无但容易出事"的 Windows 程序:
steamerrorreporter, gldriverquery, vulkandriverquery, steamsysinfo, hardwareupdater, unitycrashhandler64见 process_ios.c。这些大多是 Steam/Unity 的错误上报、驱动探测助手——启动失败它们自己也"容忍"(日志里一句Failed spawning ... continues),拒绝启动反而比放任它们崩溃更安全。
🎮 对玩家来说这意味着:App 日志里出现"某进程拒绝启动"是正常设计,不是错误。
四、对新手玩家的实际意义
把上面的技术细节翻译成日常体验:
- 游戏崩溃可能 = 整个 App 崩溃:这是 iOS 沙箱 + 单进程的物理限制,遇到时重开 App 即可,游戏存档不受影响(存档跨重装保留)
- Steam 里有些进程"不启动"是正常的:黑名单拦截是保护机制
- 32 位老游戏可以玩,但有窗口槽位机制:启动器退出后主程序才会真正接棒,所以 32 位游戏启动可能比 64 位更慢
- 性能波动是全局的:一个后台线程吃满 CPU,所有游戏线程都会跟着掉帧
五、想深入?从这里继续
- 架构总览与构建:docs/BUILDING.md
- 32 位游戏(WoW64):docs/WOW64.md
- Steam 客户端(Madeira Dock):docs/MADEIRA_DOCK.md
- "进程启动"线程化改造:build/ntdll-unix/process_ios.c
- wineserver 线程化改造:build/wineserver/main_ios.c
- 客户端-服务器通信与伪进程身份管理:build/ntdll-unix/server_ios.c
一句话总结:Madeira 用"进程皆线程"的单进程模型,绕过了 iOS 无法启动新程序的沙箱限制——代价是崩溃、文件描述符、信号和内存全部变成全局共享,而整个项目的很多"奇怪设计",其实都是对这几条代价的工程化回应。
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考