ASP.NET Core Native AOT + systemd 实战:把 Linux ARM64 程序变成可靠的设备服务
前言
前面的文章已经完成了应用内部的主要架构:
text
ASP.NET Core Host
│
├── Minimal API
│
├── Application Service
│
├── BackgroundService
│
├── Runtime State
│
└── Infrastructure
Native AOT 编译之后,我们最终得到一个 Linux ARM64 可执行程序。
例如:
text
DeviceWeb
于是很容易产生一个想法:
bash
./DeviceWeb
程序能够启动。
是不是部署就完成了?
显然不是。
真正运行在 Linux 设备上的程序通常需要满足:
text
设备启动
↓
应用自动启动
应用运行
↓
统一日志
应用退出
↓
正确清理资源
异常崩溃
↓
自动恢复
系统关机
↓
优雅停止
这已经不是 ASP.NET Core 本身应该解决的问题。
它属于:
text
Process Supervision
在现代 Linux 系统中,承担这个职责的通常就是:
text
systemd
这一篇就把:
text
ASP.NET Core
Native AOT
Linux
systemd
journald
完整串起来。
一、为什么不能简单使用 &
最简单的后台运行方式可能是:
bash
./DeviceWeb &
这样 Shell 返回:
text
$
程序似乎已经在后台运行。
但问题很多:
text
谁负责启动它?
谁知道它是否还活着?
异常退出以后怎么办?
设备重启以后怎么办?
日志在哪里?
怎么停止?
怎么重新启动?
程序属于哪个用户?
环境变量在哪里配置?
所以:
bash
./DeviceWeb &
只是:
text
Background Process
不是:
text
Managed Service
二、systemd 到底是什么
很多开发者第一次接触 systemd,只把它理解成:
text
开机启动工具
实际上它承担的职责远不止如此。
可以简单理解成:
systemd 是 Linux 用户空间中的系统和服务管理器。
对于我们的 ASP.NET Core 程序:
text
systemd
│
▼
DeviceWeb
systemd 成为这个进程的:
text
Process Supervisor
负责:
text
Start
Stop
Restart
Dependency
Environment
Identity
Logging
Resource Management
等生命周期管理。
三、ASP.NET Core 和 systemd 的职责边界
这个边界非常重要。
ASP.NET Core 负责:
text
Application
│
├── HTTP
├── BackgroundService
├── Business Logic
├── Runtime State
└── Graceful Shutdown
systemd 负责:
text
Process
│
├── Start
├── Stop
├── Restart
├── User
├── Environment
├── Dependency
└── Process Supervision
所以:
text
systemd
↓
ASP.NET Core Host
↓
Application
形成三层结构:
text
Operating System
↓
systemd
↓
ASP.NET Core Host
↓
Application Runtime
四、Native AOT 在这里有什么优势
普通 Framework-dependent .NET 应用可能需要:
bash
dotnet DeviceWeb.dll
目标系统还需要对应的:
text
.NET Runtime
Native AOT 发布以后通常得到:
text
ELF Executable
systemd 可以直接:
text
ExecStart=/opt/device-web/DeviceWeb
因此部署模型非常简单:
text
systemd
↓
Native ELF
而不是:
text
systemd
↓
dotnet
↓
CLR
↓
DeviceWeb.dll
五、推荐的 Linux 目录结构
不要把程序随便放在:
text
/root
/home
/tmp
可以建立明确目录。
例如:
text
/opt/device-web/
│
├── DeviceWeb
├── wwwroot/
└── appsettings.json
应用程序:
text
/opt/device-web
运行时数据:
text
/var/lib/device-web
日志原则上交给:
text
journald
因此形成:
text
/opt/device-web
↓
Application
/var/lib/device-web
↓
Persistent Runtime Data
journald
↓
Logs
这是一个非常清晰的 Linux 部署模型。
六、为什么程序和数据要分开
例如:
text
/opt/device-web
应该主要包含:
text
程序
静态资源
默认配置
而:
text
/var/lib/device-web
保存:
text
用户配置
设备运行数据
持久化状态
这样升级程序:
text
replace /opt/device-web
不会自然要求覆盖:
text
/var/lib/device-web
里的用户数据。
也就是说:
text
Application Binary
≠
Persistent Data
这是设备升级设计里非常重要的原则。
七、先创建专用 Linux 用户
设备服务不应该默认:
text
root
运行。
例如:
bash
useradd \
--system \
--no-create-home \
--shell /usr/sbin/nologin \
device-web
得到:
text
device-web
系统用户。
然后程序运行:
text
User=device-web
Group=device-web
而不是:
text
User=root
八、为什么不要使用 root
如果 Web 应用存在漏洞:
text
HTTP
↓
Application Vulnerability
↓
Code Execution
如果进程本身就是:
text
root
攻击者可能直接获得:
text
root privilege
而普通用户:
text
device-web
至少增加了一层权限边界。
这就是:
text
Least Privilege
最小权限原则。
九、但设备程序经常需要访问硬件
例如:
text
/dev/ttyUSB0
/dev/i2c-1
/dev/gpiochip0
于是有人会直接:
text
那就 root 运行。
其实更合理的是:
text
给需要的设备权限
而不是:
text
给整个进程 root 权限。
例如通过:
text
Group
udev rules
device permissions
精确授权。
原则应该是:
text
需要什么权限
↓
授予什么权限
而不是:
text
为了省事
↓
root。
十、最简单的 systemd Service
建立:
text
/etc/systemd/system/device-web.service
内容:
ini
[Unit]
Description=Device Web Service
After=network.target
[Service]
Type=simple
User=device-web
Group=device-web
WorkingDirectory=/opt/device-web
ExecStart=/opt/device-web/DeviceWeb
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
这已经可以管理程序。
十一、三个 Section 分别是什么
systemd Service 文件通常:
text
[Unit]
[Service]
[Install]
三个部分。
十二、Unit
例如:
ini
[Unit]
Description=Device Web Service
After=network.target
主要描述:
text
这个 Unit 是什么
与其他 Unit 的启动顺序关系是什么
例如:
ini
After=network.target
表示:
text
device-web.service
在:
text
network.target
之后启动。
十三、注意:After 不等于 Depends On
这是 systemd 非常容易误解的地方。
ini
After=network.target
主要表达:
text
Ordering
即:
text
启动顺序。
并不自动意味着:
text
没有 network.target
就绝对不能启动。
systemd 中:
text
Order
和:
text
Dependency
是两个概念。
这是理解 systemd 很重要的一点。
十四、Service
真正定义进程:
ini
[Service]
Type=simple
User=device-web
Group=device-web
WorkingDirectory=/opt/device-web
ExecStart=/opt/device-web/DeviceWeb
最关键的是:
text
ExecStart
systemd 会启动:
text
/opt/device-web/DeviceWeb
十五、为什么 Native AOT 特别适合 ExecStart
因为 Native AOT 最终就是:
text
Linux Executable
所以:
ini
ExecStart=/opt/device-web/DeviceWeb
非常自然。
不需要:
ini
ExecStart=/usr/bin/dotnet /opt/device-web/DeviceWeb.dll
这也是 Native AOT 在设备部署方面非常舒服的一点。
十六、WorkingDirectory 为什么重要
假设程序内部:
csharp
File.ReadAllText(
"config/default.json");
这是相对路径。
那么:
text
当前工作目录
会影响:
text
config/default.json
到底指向哪里。
设置:
ini
WorkingDirectory=/opt/device-web
可以让进程的工作目录确定下来。
不过对于真正重要的持久化文件,我仍然更推荐:
text
明确绝对路径
或者通过配置生成绝对路径。
十七、ASP.NET Core 怎么设置监听地址
例如:
ini
Environment=ASPNETCORE_URLS=http://0.0.0.0:8080
完整:
ini
[Service]
Environment=ASPNETCORE_URLS=http://0.0.0.0:8080
ASP.NET Core 启动后:
text
Kestrel
↓
0.0.0.0:8080
也就是监听所有网络接口上的:
text
TCP 8080
十八、0.0.0.0 到底是什么意思
很多初学者容易把:
text
0.0.0.0
理解成设备 IP。
其实不是。
监听:
text
0.0.0.0:8080
表示:
在本机所有 IPv4 网络接口上接受连接。
例如设备有:
text
127.0.0.1
192.168.1.100
10.0.0.20
Kestrel 都可以接受相应接口:
text
:8080
上的连接。
十九、为什么不能只监听 localhost
如果:
text
http://127.0.0.1:8080
意味着:
text
只有设备本机
能够访问。
其他 PC:
text
Browser
↓
Ethernet
↓
Device
无法连接。
所以设备 Web 管理页面通常需要:
text
0.0.0.0
或者明确绑定某个设备接口地址。
二十、Environment 和 appsettings.json 谁负责什么
我比较推荐:
text
appsettings.json
保存:
text
Application Configuration
例如:
text
Polling Interval
Feature Settings
Application Options
systemd Environment:
text
Deployment Configuration
例如:
text
ASPNETCORE_URLS
DOTNET_ENVIRONMENT
特殊部署参数
这样:
text
应用逻辑配置
和:
text
部署环境配置
分开。
二十一、不要把大量配置全部塞进 service 文件
如果写成:
ini
Environment=A=...
Environment=B=...
Environment=C=...
Environment=D=...
Environment=E=...
Environment=F=...
最终:
text
systemd service
会变成另一个配置中心。
更合理的是:
text
少量 Deployment Environment
+
Application Configuration File
职责清晰。
二十二、StateDirectory 非常适合设备程序
systemd 提供一个非常实用的配置:
ini
StateDirectory=device-web
这样 systemd 可以管理:
text
/var/lib/device-web
这一类持久化状态目录。
Service:
ini
[Service]
User=device-web
Group=device-web
StateDirectory=device-web
这样应用可以把持久化数据放:
text
/var/lib/device-web
二十三、这比程序自己 mkdir 更好
如果程序启动时:
csharp
Directory.CreateDirectory(
"/var/lib/device-web");
但程序运行用户:
text
device-web
可能没有:
text
/var/lib
创建目录的权限。
于是你又开始:
text
root 创建目录
chown
chmod
而:
ini
StateDirectory=device-web
可以让 systemd 在服务生命周期管理中处理相应目录。
部署结构更加规范。
二十四、一个更完整的 Service
例如:
ini
[Unit]
Description=Device Web Service
After=network.target
[Service]
Type=simple
User=device-web
Group=device-web
WorkingDirectory=/opt/device-web
ExecStart=/opt/device-web/DeviceWeb
Environment=ASPNETCORE_URLS=http://0.0.0.0:8080
StateDirectory=device-web
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
这已经比较接近实际设备服务。
二十五、启动 Service
修改 service 文件以后:
bash
systemctl daemon-reload
然后:
bash
systemctl start device-web.service
查看:
bash
systemctl status device-web.service
二十六、为什么修改 service 后要 daemon-reload
systemd 不会因为:
text
/etc/systemd/system/device-web.service
文件发生变化,就自动重新解析所有 Unit。
所以:
bash
systemctl daemon-reload
相当于告诉 systemd:
Unit 定义发生变化,请重新加载。
注意它不是:
text
restart application
只是:
text
Reload systemd configuration。
二十七、修改 Service 的标准流程
通常:
text
修改 .service
↓
systemctl daemon-reload
↓
systemctl restart device-web
↓
systemctl status device-web
这是非常常用的一套流程。
二十八、开机自动启动
执行:
bash
systemctl enable device-web.service
这样设备下一次启动:
text
Boot
↓
systemd
↓
multi-user.target
↓
device-web.service
程序自动运行。
也可以:
bash
systemctl enable --now device-web.service
同时:
text
Enable
+
Start
二十九、Install 的作用
例如:
ini
[Install]
WantedBy=multi-user.target
主要影响:
text
systemctl enable
时建立的依赖关系。
简单理解:
text
multi-user.target
↓
device-web.service
从而进入正常多用户运行状态时:
text
启动 Device Web Service。
三十、systemctl status 能看什么
例如:
bash
systemctl status device-web.service
可以看到:
text
Loaded
Active
Main PID
Memory
CPU
最近日志
如果:
text
Active: active (running)
说明:
text
进程还活着。
但这里有一个非常重要的区别。
三十一、Process Running 不等于 Application Healthy
例如 ASP.NET Core 进程:
text
PID 1234
仍然存在。
systemd:
text
active (running)
但是应用内部可能:
text
Device State = Error
这时候:
text
systemd
认为:
text
Process Healthy Enough
因为:
text
进程没有退出。
而业务系统认为:
text
Device Runtime Error。
这两个概念完全不同。
三十二、这是理解 Restart=on-failure 的关键
很多人配置:
ini
Restart=on-failure
以后会认为:
应用进入 Error,systemd 会自动重启。
不会。
假设:
text
BackgroundService
↓
Device Error
↓
State = Error
↓
继续等待
此时:
text
ASP.NET Core Process
仍然:
text
Running。
所以 systemd 看到:
text
Exit Code?
没有。
进程根本没退出。
因此:
text
Restart=on-failure
不会触发。
三十三、Restart=on-failure 监控的是进程失败
例如:
text
Unhandled Exception
↓
Host Exit
↓
Process Exit Code != 0
↓
systemd
↓
Restart
这才是:
text
Restart=on-failure
的典型工作路径。
所以:
text
Business Error
和:
text
Process Failure
一定要区分。
三十四、推荐三层错误模型
可以理解成:
text
Level 1
Operational Failure
例如:
一次读取失败
一次网络超时
↓
text
Application 内部处理
第二层:
text
Level 2
Subsystem Failure
例如:
设备长期不可用
业务进入 Error
↓
text
State Machine / Recovery
第三层:
text
Level 3
Process Failure
例如:
程序内部不可恢复异常
Host 崩溃
关键 invariant 被破坏
↓
text
systemd
最终兜底。
三十五、不要让 systemd 代替应用状态机
例如一次设备查询失败:
text
Read Failed
不要立即:
text
Environment.Exit(1)
让 systemd:
text
Restart
否则:
text
一次普通业务错误
升级成:
text
整个应用重启。
可能导致:
text
Web 中断
状态丢失
资源重新初始化
服务抖动
systemd 应该是:
最后的进程级恢复层。
而不是:
第一层业务重试机制。
三十六、RestartSec 为什么重要
例如:
ini
Restart=on-failure
RestartSec=5
如果进程:
text
启动
↓
立即崩溃
systemd 等:
text
5 秒
再启动。
否则可能:
text
Start
↓
Crash
↓
Start
↓
Crash
↓
Start
↓
Crash
形成高速:
text
Crash Loop。
三十七、但 RestartSec 还不是全部
真正生产环境还应该考虑:
text
StartLimitIntervalSec
StartLimitBurst
避免程序长期不断:
text
Crash → Restart
最终消耗:
text
CPU
日志
Flash
系统资源
设备软件尤其需要注意这种:
text
Restart Storm。
三十八、ASP.NET Core 怎么收到 systemctl stop
现在来看非常重要的一条链路。
执行:
bash
systemctl stop device-web.service
systemd 会请求:
text
进程终止。
典型情况下 Linux 进程收到:
text
SIGTERM
然后:
text
systemd
↓
SIGTERM
↓
ASP.NET Core Host
↓
ApplicationStopping
↓
CancellationToken
↓
BackgroundService
三十九、这就是 stoppingToken 的来源
我们经常写:
csharp
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
这个:
text
stoppingToken
并不是随便生成的 Token。
当 Host 开始关闭时:
text
Generic Host
会触发相应的停止流程。
于是:
csharp
stoppingToken.IsCancellationRequested
变成:
text
true。
四十、所以后台循环必须响应 Cancellation
正确:
csharp
while (
!stoppingToken.IsCancellationRequested)
{
await PollAsync(
stoppingToken);
await Task.Delay(
1000,
stoppingToken);
}
错误:
csharp
while (true)
{
Poll();
Thread.Sleep(1000);
}
第二种:
text
systemctl stop
以后可能无法快速结束。
四十一、Graceful Shutdown 完整链路
整个过程:
text
systemctl stop
↓
systemd
↓
SIGTERM
↓
ASP.NET Core Host
↓
ApplicationStopping
↓
Cancel stoppingToken
↓
BackgroundService stops
↓
Kestrel stops accepting requests
↓
Dispose resources
↓
Process Exit
这就是:
text
Graceful Shutdown。
四十二、为什么设备软件特别需要 Graceful Shutdown
设备程序可能持有:
text
File
Socket
Serial Port
Lock
Temporary File
Runtime State
如果直接:
text
SIGKILL
进程没有机会:
text
Flush
Close
Dispose
Save
所以正常:
text
systemctl stop
systemctl restart
设备关机
应该尽量走:
text
SIGTERM
+
Graceful Shutdown。
四十三、但不要把重要数据只留到 StopAsync 才保存
这是另一个非常重要的原则。
不要设计:
text
运行时一直修改配置
↓
只在 StopAsync 保存
因为:
text
断电
Kernel Panic
SIGKILL
进程 Crash
都可能:
text
没有 StopAsync。
真正重要的数据应该:
text
发生重要变化
↓
及时持久化。
Graceful Shutdown 只是:
text
正常退出优化
不是:
text
数据可靠性的唯一保障。
四十四、journald 和 ASP.NET Core 日志
ASP.NET Core 默认日志:
text
ILogger
最终通常写到:
text
Console
systemd 启动的进程:
text
stdout
stderr
可以由:
text
journald
收集。
因此:
text
ILogger<T>
↓
Console Logger
↓
stdout/stderr
↓
systemd
↓
journald
形成完整日志链。
四十五、查看日志
例如:
bash
journalctl -u device-web.service
查看该 Service 日志。
实时:
bash
journalctl -u device-web.service -f
相当于:
text
Follow
日志流。
四十六、只看本次启动
可以:
bash
journalctl \
-u device-web.service \
-b
其中:
text
-b
表示当前:
text
Boot。
设备调试时非常有用。
四十七、查看最近日志
例如:
bash
journalctl \
-u device-web.service \
-n 100
查看最近:
text
100 条。
这比直接:
text
cat application.log
更适合 systemd 服务。
四十八、为什么我更推荐 journald
设备端如果自己写:
text
app.log
马上会遇到:
text
日志文件越来越大怎么办?
Rotation 怎么做?
磁盘满怎么办?
多个进程怎么办?
时间戳怎么办?
服务启动批次怎么区分?
而 journald 已经提供:
text
统一日志
时间
Unit
PID
Boot
Priority
Rotation
等能力。
所以设备程序可以优先:
text
ILogger
↓
journald
而不是重新造一个完整日志系统。
四十九、但 journald 也不是无限空间
设备 Flash 通常有限。
所以必须考虑:
text
日志级别
例如:
text
Trace
Debug
Information
Warning
Error
Critical
不要把:
text
每秒正常轮询成功
写成:
text
Information。
否则:
text
24h × 高频循环
会产生大量无意义日志。
五十、一个合理的日志原则
正常稳定运行:
text
不记录
或 Debug
状态发生变化:
text
Information
可恢复异常:
text
Warning
操作失败:
text
Error
进程无法继续:
text
Critical
这样:
text
journalctl
看到的才是真正:
text
值得关注的信息。
五十一、ASP.NET Core 可以动态设置日志级别
例如:
text
Logging:LogLevel
可以:
json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"DeviceWeb.Runtime": "Warning"
}
}
}
正常情况下:
text
Warning
减少日志。
调试时:
text
Debug
查看详细流程。
五十二、systemd 也可以覆盖配置
例如:
ini
Environment=Logging__LogLevel__DeviceWeb.Runtime=Debug
ASP.NET Core Configuration 中:
text
__
可以映射:
text
:
于是:
text
Logging__LogLevel__DeviceWeb.Runtime
对应:
text
Logging:LogLevel:DeviceWeb.Runtime
这非常适合:
临时提高某个模块日志级别,而不修改应用默认配置。
五十三、但不要直接修改原 service 文件做临时调试
更推荐:
text
systemd drop-in
例如:
text
/etc/systemd/system/device-web.service.d/
下面创建:
text
debug.conf
内容:
ini
[Service]
Environment=Logging__LogLevel__DeviceWeb.Runtime=Debug
然后:
bash
systemctl daemon-reload
systemctl restart device-web.service
这样:
text
主 Service
保持稳定,
调试配置作为:
text
Override
存在。
五十四、为什么 Drop-in 很适合现场调试
正式配置:
text
device-web.service
不要随便改。
现场临时:
text
Debug Logging
特殊环境变量
实验参数
可以:
text
device-web.service.d/debug.conf
调试结束:
text
删除 drop-in
↓
daemon-reload
↓
restart
恢复正式行为。
这比:
text
现场直接改主 service
安全很多。
五十五、ExecStart 也可以被 Override
systemd 中覆盖:
text
ExecStart
需要注意:
它不是简单追加。
通常先:
ini
ExecStart=
清空原来的值,
再:
ini
ExecStart=/opt/device-web/DeviceWeb ...
例如:
ini
[Service]
ExecStart=
ExecStart=/opt/device-web/DeviceWeb --SomeOption=Debug
这是 systemd drop-in 中非常常见的写法。
五十六、为什么这一点特别容易踩坑
如果直接:
ini
[Service]
ExecStart=/opt/device-web/DeviceWeb --Debug
systemd 可能认为:
text
存在多个 ExecStart
而对应 Service 类型不允许这样的配置。
所以:
text
Override ExecStart
通常是:
text
Clear
↓
Set
即:
ini
ExecStart=
ExecStart=...
五十七、如何确认 systemd 最终到底用了什么配置
非常推荐:
bash
systemctl cat device-web.service
它可以帮助查看:
text
主 service
+
drop-in
从而知道:
text
最终有哪些配置参与。
调试 systemd 时非常实用。
五十八、Environment 和命令行参数谁优先
ASP.NET Core Configuration 通常可能来自:
text
appsettings.json
appsettings.Environment.json
Environment Variables
Command Line
不同 Provider 有优先级。
所以:
text
同一个 Key
可能被后加载的配置覆盖。
这也是为什么:
text
systemd Environment
非常适合部署覆盖。
而:
text
Command Line
适合更加临时和明确的启动覆盖。
五十九、systemd 和 ASP.NET Core 形成两套配置层
可以理解:
text
Application Defaults
↓
appsettings.json
↓
Environment
↓
Command Line
外面还有:
text
systemd
负责给:
text
Environment
Command Line
提供部署参数。
于是:
text
Application
不需要知道:
text
自己到底运行在哪台设备。
六十、升级应用时不要忘记停止 Service
一个比较安全的部署流程:
text
systemctl stop
↓
备份旧版本
↓
部署新 Binary
↓
设置 Owner / Permission
↓
systemctl start
↓
systemctl status
↓
journalctl
不要:
text
程序还在运行
↓
直接覆盖所有文件
尤其:
text
wwwroot
配置
Native Binary
同时更新时,
可能出现:
text
程序版本
≠
静态资源版本。
六十一、Native AOT Binary 要有执行权限
例如:
bash
chmod +x \
/opt/device-web/DeviceWeb
否则 systemd:
text
ExecStart
可能出现:
text
Permission denied。
这是新板部署很常见的问题。
六十二、还要注意架构
设备:
bash
uname -m
如果返回:
text
aarch64
那么程序应该是:
text
ARM64
可以:
bash
file \
/opt/device-web/DeviceWeb
检查 ELF 信息。
如果误部署:
text
x86-64
到:
text
aarch64
设备上,
当然无法正常执行。
六十三、动态依赖也要检查
即使是 Native AOT:
text
Native
也不等于:
text
完全没有 Linux 动态库依赖。
可以:
bash
ldd \
/opt/device-web/DeviceWeb
查看运行时依赖。
例如可能涉及:
text
libc
libm
libpthread
libdl
具体情况取决于:
text
构建方式
目标平台
链接方式
所以:
Native AOT ≠ 一个与操作系统 ABI 完全无关的神秘单文件。
它仍然是:
text
Linux Native Program。
六十四、这也解释了为什么 sysroot 很重要
前面交叉编译文章中提到:
text
Linux x64 Build Host
↓
ARM64 Sysroot
↓
Native AOT
↓
ARM64 ELF
最终这个 ELF:
text
systemd
↓
exec
以后,
真正面对的是设备上的:
text
Linux Kernel
glibc
Dynamic Loader
Native Libraries
所以:
text
Build Environment
必须与:
text
Target Runtime ABI
兼容。
六十五、最终生产 Service 可以长什么样
例如:
ini
[Unit]
Description=Device Web Service
After=network.target
[Service]
Type=simple
User=device-web
Group=device-web
WorkingDirectory=/opt/device-web
ExecStart=/opt/device-web/DeviceWeb
Environment=ASPNETCORE_URLS=http://0.0.0.0:8080
StateDirectory=device-web
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
不要因为 systemd 有几十上百个选项,
一开始就把:
text
所有 Hardening Option
所有 Restart Option
所有 Resource Limit
全部塞进去。
原则还是:
text
先明确需求
↓
再增加配置。
六十六、整个系统现在完整了
我们终于可以把前几篇串起来。
编译阶段:
text
C#
↓
Roslyn
↓
Source Generator
↓
IL
↓
Native AOT
↓
ARM64 Native Code
↓
Linker
↓
ARM64 ELF
部署:
text
ARM64 ELF
↓
/opt/device-web
系统启动:
text
Linux Kernel
↓
systemd
↓
device-web.service
↓
Native ELF
应用启动:
text
ASP.NET Core Host
↓
Kestrel
+
BackgroundService
运行:
text
Browser
↓
Minimal API
↓
Application
↓
Runtime State
↑
BackgroundService
↑
Infrastructure
日志:
text
ILogger
↓
Console
↓
stdout / stderr
↓
journald
↓
journalctl
停止:
text
systemctl stop
↓
SIGTERM
↓
ASP.NET Core Host
↓
CancellationToken
↓
BackgroundService Exit
↓
Dispose
↓
Process Exit
异常:
text
Process Crash
↓
systemd
↓
Restart=on-failure
↓
Process Restart
到这里:
text
ASP.NET Core
+
Native AOT
+
Linux
+
systemd
才真正组成了一套完整的设备运行架构。
总结
在 Linux ARM64 设备上部署 ASP.NET Core Native AOT 程序时,不应该只关注:
text
程序能不能运行。
真正需要设计的是:
text
程序由谁启动?
以什么用户运行?
数据放在哪里?
日志由谁管理?
怎么停止?
怎么重新启动?
进程崩溃怎么办?
业务错误怎么办?
设备重启怎么办?
怎么临时调试?
而 systemd 正好补齐了 ASP.NET Core 外部的:
text
Process Lifecycle
管理能力。
最终形成:
text
┌──────────────────────────────┐
│ Linux OS │
│ │
│ systemd │
│ │ │
│ ▼ │
│ ASP.NET Core Process │
│ │ │
│ ┌───────┴───────┐ │
│ ▼ ▼ │
│ Kestrel Background │
│ Service │
└──────────────────────────────┘
最重要的是理解职责:
text
业务错误
↓
Application
子系统恢复
↓
Runtime / State Machine
进程生命周期
↓
ASP.NET Core Host
进程崩溃恢复
↓
systemd
不要把这些层混在一起。
对于工业设备软件来说:
systemd 不是业务恢复机制,而是应用进程之外最后一道进程级监督机制。
这条边界一旦明确,很多:
text
Restart
Recovery
Error State
Graceful Shutdown
Logging
相关设计都会清晰很多。
下一篇
下一篇建议继续深入 Linux 部署底层:
《ASP.NET Core Native AOT 程序在 Linux 上到底是怎么启动的?从 ELF、glibc 到动态链接器》
前面我们已经知道:
text
systemd
↓
ExecStart
↓
DeviceWeb
但 Linux 到底怎么把这个 Native AOT 文件变成一个运行中的进程?
下一篇继续往下一层:
text
ELF Header 是什么?
file 命令到底看到了什么?
Program Header 是什么?
ldd 为什么能看到动态库?
glibc 到底是什么?
动态链接器 ld-linux-aarch64.so.1 做什么?
Native AOT 为什么仍然可能依赖 .so?
Kernel execve() 做了什么?
虚拟地址空间怎么建立?
.text / .rodata / .data / .bss 是什么?
程序的 Main() 到底什么时候开始执行?
systemd ExecStart 到 ASP.NET Core Main() 中间发生了什么?
把最后一条链彻底打通:
text
systemd
↓
execve()
↓
ELF
↓
Linux Loader
↓
Dynamic Linker
↓
glibc / Native Libraries
↓
Native AOT Runtime
↓
Main()
↓
CreateSlimBuilder()
↓
Kestrel
这篇会把前面的 Native AOT、sysroot、glibc、ARM64、systemd 真正连成一条完整链路。