首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >正经人谁用手机当服务器啊?但我真把 .NET Web 塞进小米了

正经人谁用手机当服务器啊?但我真把 .NET Web 塞进小米了

作者头像
桑榆肖物
发布2026-07-01 20:46:02
发布2026-07-01 20:46:02
1210
举报

这篇文章记录一次把 .NET Web 程序跑到小米手机上的完整过程:从 Termux、Ubuntu 子系统,到 linux-arm64 发布、glibc 依赖、Server GC 静默崩溃排查。

1. 为什么要折腾这个

之前就想试试把 .NET Web 程序塞进手机里跑,最近刚好有这个需求,也终于抽出时间把流程走通了。严格来说,这不是一个适合正式生产的方案,更像是极客玩法、临时服务、局域网小工具或者验证环境。

如果你的目标是认真做移动端应用,我还是建议优先考虑 .NET MAUI。本文讲的是另一件事:通过 Termux 部署一个相对完整的 Ubuntu 用户态环境,然后运行基于 linux-arm64 编译出来的 .NET 独立部署程序。

这里有几个前提需要先说清楚:

  • 手机必须是 64 位 ARM 架构,现代安卓机基本都满足。
  • .NET 程序发布时要选择 linux-arm64
  • 可以使用 Native AOT,也可以使用 Self-contained 独立部署。
  • 如果直接在 Termux 里运行标准 Linux 程序,大概率会遇到 glibc 和 Android bionic 的兼容问题。

关于 AOT 编译,可以参考我之前写过的文章:.NET 10 Native AOT 在 Linux 嵌入式设备上的实战

2. 准备工作

俗话说,工欲善其事,必先利其器。开始前我们需要先在电脑上把 .NET Web 项目发布成 linux-arm64,然后在手机上安装 Termux,并配置好权限和 SSH。

2.1 先把 .NET 程序发布成 linux-arm64

如果你已经有 .NET Web 项目,可以先在电脑上完成发布,再把产物传到手机里。这里以 Docker 编译 Native AOT 为例,核心就是把 Runtime Identifier 换成 linux-arm64

代码语言:javascript
复制
docker run --rm -v "$(pwd):/app" -w /app mcr.microsoft.com/dotnet/sdk:10.0 bash -c " \
    apt-get update -qq && \
    apt-get install -y -qq clang gcc-aarch64-linux-gnu lld && \
    dotnet publish ./bweb/bweb.csproj -c Release -r linux-arm64 \
        -p:PublishAot=true \
        -p:InvariantGlobalization=true \
        -p:LinkerFlavor=lld -p:ObjCopyName=aarch64-linux-gnu-objcopy \
        -o ./dist-aot"

如果不做 AOT,也可以发布 Self-contained 模式。重点不是一定要 AOT,而是发布目标必须和手机 CPU 架构一致,否则后面排查会非常痛苦。

2.2 安装 Termux,并把权限先处理好

Termux 不建议从应用商店下载,最好去 Termux GitHub Releases[1] 下载最新版安装包。现代手机一般选择带 arm64-v8a 字样的 APK。

以小米 / 澎湃 OS 为例,安装之后不要急着开始敲命令,先去系统设置里把权限放开。否则你后面可能会遇到程序突然被杀、后台服务断掉、文件访问异常等问题。

需要重点检查这几类权限:

  • 存储权限:允许读写手机存储,必要时开启“所有文件访问权限”。
  • 后台保活:允许 Termux 自启动、关联启动和后台活动。
  • 省电策略:把 Termux 设置为无限制,如果要后台长期运行,一定要调整。
应用设置
应用设置

应用设置

这里有个小坑:一般教程会告诉你,用 termux-setup-storage 命令给 Termux 授权访问手机存储。但在小米 / 澎湃 OS 上,这个命令可能会报错,提示 Aborted。实际上,这个报错不影响后续操作,只要系统权限已经给了,并且 ls /sdcard 能看到手机存储,就可以继续走。

代码语言:javascript
复制
~ $ termux-setup-storage
/data/data/com.termux/files/usr/bin/termux-setup-storage: line 29: 12604 Aborted
am broadcast --user 0 --es com.termux.app.reload_style storage -a com.termux.app.reload_style com.termux > /dev/null

2.3 配置 SSH,别在手机上搓玻璃板

第一次打开 Termux 时,先等初始化安装完成。之后建议第一时间配置 SSH,这样电脑端连接进去操作会舒服很多。

先切换国内软件源。在 Termux 里执行:

代码语言:javascript
复制
termux-change-repo
Termux 换源
Termux 换源

Termux 换源

弹出的蓝色界面里,在 Mirror group 中选 Mirrors in Chinese Mainland,按空格选中后回车确认。

然后安装并启动 OpenSSH:

代码语言:javascript
复制
pkg install openssh -y

安装完成后,启动 SSH 服务,并进行一些连接的准备工作:

代码语言:javascript
复制
sshd
whoami
passwd
Termux SSH 准备
Termux SSH 准备

Termux SSH 准备

whoami 会输出当前 Termux 用户名,一般类似 u0_a123passwd 用来设置 SSH 登录密码,输入时不会显示字符,正常盲打两次即可。

接着在手机里查看局域网 IP:

代码语言:javascript
复制
ifconfig

找到 wlan0 对应的地址,比如 192.168.x.x。电脑和手机在同一个 Wi-Fi 下时,就可以这样连接:

代码语言:javascript
复制
ssh -p 8022 你的用户名@你的手机IP

接下来把电脑上编译好的 .NET 发布包传到手机。可以用微信、QQ 先传到下载目录,也可以用 Xshell、MobaXterm、Termius 这类工具直接拖拽上传。

不过实际运行前,建议把程序复制到 Termux 的 home 目录下再解压、授权和执行。不要直接在下载目录里折腾执行权限,有些手机系统底层的防护会吞指令,导致添加执行权限异常或失败。

3. 爬坑进行时:第一坑

这里我们已经可以在电脑远程SSH到手机的 Termux 里了,接下来就可以尝试运行 .NET Web 程序了。

3.1 Termux 不是标准 Linux

我一开始把 linux-arm64 的程序放进去,解压、授权都没问题,但执行时还是报错:

代码语言:javascript
复制
~/bweb $ ./bweb
bash: ./bweb: cannot execute: required file not found

如果你之前发布错了架构,还可能看到类似这样的错误:

代码语言:javascript
复制
~ $ ./bweb
Could not open ELF interpreter /lib/ld-linux-armhf.so.3: Error 2 (No such file or directory)

换成 linux-arm64 后,armhf 的问题没有了,但 cannot execute: required file not found 仍然存在。这里“找不到”的不是 bweb 文件本身,而是它启动时需要的 Linux 底层运行环境,通常是标准 Linux 的 C 运行库 glibc,以及对应的 ELF 解释器 /lib/ld-linux-aarch64.so.1

问题的根源在这里:

  • 你发布的是 linux-arm64,.NET 会把目标环境当成标准 Linux,比如 Ubuntu、Debian、CentOS。
  • 但 Termux 本质上还是运行在 Android 上,Android 底层使用的是 bionic,不是 glibc。
  • Termux 里没有标准 Linux 的 /lib 目录和对应动态链接器,所以程序启动时会直接卡在地基上。

所以,只靠 Termux 直接跑标准 Linux 的 .NET 原生程序并不稳。解决办法是加一层完整的 Ubuntu 用户态环境。

3.2 用 proot-distro 安装 Ubuntu 子系统

Termux 里可以通过 proot-distro 安装 Ubuntu。它不是虚拟机,也不是完整容器,但足够给我们提供一个接近标准 Linux 的用户态环境。

先安装 proot-distro

代码语言:javascript
复制
pkg install proot-distro -y

然后安装 Ubuntu:

代码语言:javascript
复制
proot-distro install ubuntu

登录时建议把 Termux 的 home 目录挂载到 Ubuntu 的 /root,这样进去之后能直接看到前面解压好的程序:

代码语言:javascript
复制
proot-distro login ubuntu --bind ~:/root

成功后,提示符会变成类似 root@localhost:~#。这时候你已经进入 Ubuntu 环境了。

接下来补齐 .NET 常见依赖。极简 Ubuntu 里经常缺全球化、加密和压缩相关库:

代码语言:javascript
复制
apt update && apt install -y libicu-dev libssl-dev zlib1g libstdc++6

如果你的程序不是 AOT,或者没有通过 InvariantGlobalization=true 去掉 ICU 依赖,那么 libicu-dev 基本是必须的。否则程序可能报错,也可能没有明显输出就退出。

ICU 报错
ICU 报错

ICU 报错

4. 爬坑进行时:第二坑

虽然这里我们已经进入了 Ubuntu 子系统,并且补齐了依赖,但实际运行时,还是有坑要填的。

4.1 程序静默退出

进入 Ubuntu 后,我以为事情差不多结束了。但实际运行时,.NET 程序仍然会出现“静默崩溃”:没有正常启动日志,没有明确错误,只留下一个退出状态。

这种时候不要靠猜,直接用 Linux 工具把它拆开看。

按下面四步排查,基本可以覆盖大多数启动失败场景。

4.2 查看真实退出码

Linux 程序退出后会留下状态码。先运行程序,再立刻查看 $?

代码语言:javascript
复制
./bweb
echo $?

如果是 0,说明程序认为自己正常结束了;如果是 126127134139255,就要继续查权限、缺库、段错误或运行时崩溃。

4.3 确认是不是 64 位程序

先装 file,再看可执行文件的架构:

代码语言:javascript
复制
apt update && apt install file -y
file bweb

正常情况下,你应该能看到 aarch64ARM6464-bit 之类的信息。如果看到的是 armhf32-bit,说明发布产物架构不对,先回到发布阶段重新打包。

4.4 检查底层依赖是否缺失

ldd 看动态库依赖:

代码语言:javascript
复制
ldd bweb

如果输出里有 not found,就说明缺库。常见的是 libssllibicuzliblibstdc++ 这一类,可以按前面的命令补齐。

4.5 用 strace 看最后死在哪里

如果前面几步都看不出问题,就上 strace

代码语言:javascript
复制
apt install strace -y
strace ./bweb

strace 会输出大量系统调用,不需要每一行都看懂,重点看最后几行。通常会看到类似 SIGSEGVENOENTEACCESENOMEM 这样的关键信息。

排查截图
排查截图

排查截图

5. 真正的根因:Server GC

这次 strace 把问题定位得很清楚,关键日志在最后几行:

代码语言:javascript
复制
mmap(NULL, 274877911040, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)

ENOMEM 表示系统拒绝了这次内存申请,程序随后以 255 退出。重点是这个数字:274877911040 字节,换算下来刚好是 256 GB,一上来就玩这么大么?

一个普通 .NET Web 程序刚启动,为什么要申请 256 GB?

这里和 .NET 的 Server GC 有关。通过 strace 日志可以看到,运行时识别到手机是 8 核处理器,比如读取到了类似 0-7 的 CPU 信息。Server GC 面向服务器吞吐场景,会按 CPU 核心规划堆空间。在这个环境里,它尝试为每个核心预留大约 32 GB 的虚拟地址空间,8 个核心就是 256 GB。

在常规 Linux 服务器上,这种大块虚拟内存预留通常能通过,因为系统允许内存超卖,也就是 Overcommit。它并不是马上吃掉 256 GB 物理内存,而是先占一块虚拟地址空间。

但现在环境比较特殊:程序跑在 Android 内核之上,再通过 Termux 和 proot 进入 Ubuntu 用户态。Android 对这种夸张的内存预留更敏感,内核直接拒绝了这次 mmap,于是 .NET 运行时刚启动就退出了。

6. 解决方案:关闭 Server GC,并限制堆内存

既然根因是 Server GC 的虚拟内存预留,那解决就很直接:让 .NET 切回更适合小设备的 Workstation GC,当然这在其他平台上也是可以进行相关的优化。

在 Ubuntu 终端里执行:

代码语言:javascript
复制
export DOTNET_gcServer=0

这行命令会禁用 Server GC。为了更稳一点,还可以给 GC 堆加一个上限,比如限制到 512 MB:

代码语言:javascript
复制
export DOTNET_GCHeapHardLimit=536870912

然后重新启动程序:

代码语言:javascript
复制
./bweb

这次就应该能看到熟悉的 .NET 启动日志了。

如果希望每次进入 Ubuntu 后都自动生效,可以把这两行写进 ~/.bashrc

代码语言:javascript
复制
echo 'export DOTNET_gcServer=0' >> ~/.bashrc
echo 'export DOTNET_GCHeapHardLimit=536870912' >> ~/.bashrc
source ~/.bashrc

7. 最终启动时再检查这些细节

最后整理一下完整启动步骤。进入 Ubuntu 后,先到程序目录,确认权限,再启动:

代码语言:javascript
复制
cd /root/bweb
chmod +x bweb
export DOTNET_gcServer=0
export DOTNET_GCHeapHardLimit=536870912
./bweb

另外,appsettings.json 里的监听端口建议设置为大于 1024。Android 底层不会允许普通应用随便监听 80443 这类特权端口,换成 5000808018080 这类端口会省很多事。

如果你希望局域网其他设备访问,还要确认 ASP.NET Core 不是只监听 localhost。可以在启动参数或环境变量里指定监听地址,例如:

代码语言:javascript
复制
export ASPNETCORE_URLS=http://0.0.0.0:8080
./bweb

这样电脑端就可以通过手机的局域网 IP 加端口访问这个 .NET Web 服务。

8. 小结

这次折腾看起来是“把 .NET Web 塞进手机”,实际踩到的是三层环境差异:

  • Termux 是 Android 用户空间,不等于标准 Linux。
  • linux-arm64 程序需要 glibc 环境,直接在 Termux 里跑会缺 ELF 解释器。
  • proot Ubuntu 能解决 glibc 问题,但 .NET Server GC 在 Android 内核环境下可能因为 256 GB 虚拟内存预留被拒绝。

最终可行路径是:电脑端发布 linux-arm64,手机端用 Termux 安装 Ubuntu 子系统,在 Ubuntu 里补齐依赖,然后通过 DOTNET_gcServer=0DOTNET_GCHeapHardLimit 控制 GC 行为。

它不一定适合长期生产部署,但用来做局域网小服务、临时演示、设备侧验证,废旧设备利用,还是挺有意思的。更重要的是,这个过程把 filelddstrace 这几个排查工具又串了一遍。遇到“没有输出就退出”的问题时,别急着重装环境,还是先让工具告诉你它到底死在哪里吧。

References

[1] Termux GitHub Releases: https://github.com/termux/termux-app/releases?wt.mc_id=DT-MVP-5005195

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 桑榆肖物 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 为什么要折腾这个
  • 2. 准备工作
    • 2.1 先把 .NET 程序发布成 linux-arm64
    • 2.2 安装 Termux,并把权限先处理好
    • 2.3 配置 SSH,别在手机上搓玻璃板
  • 3. 爬坑进行时:第一坑
    • 3.1 Termux 不是标准 Linux
    • 3.2 用 proot-distro 安装 Ubuntu 子系统
  • 4. 爬坑进行时:第二坑
    • 4.1 程序静默退出
    • 4.2 查看真实退出码
    • 4.3 确认是不是 64 位程序
    • 4.4 检查底层依赖是否缺失
    • 4.5 用 strace 看最后死在哪里
  • 5. 真正的根因:Server GC
  • 6. 解决方案:关闭 Server GC,并限制堆内存
  • 7. 最终启动时再检查这些细节
  • 8. 小结
    • References
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档