ASP.NET Core Native AOT + systemd 实战:把 Linux ARM64 程序变成可靠的设备服务

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 真正连成一条完整链路。

相关推荐
Rabitebla41 分钟前
【Linux 系统编程】权限(一):身份、提权,和那 9 个权限位
linux·数据结构·c++·算法
byte轻骑兵1 小时前
【BlueZ 】Linux 内核蓝牙子系统入门:hci_core 模块与 BlueZ 的交互
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙
江屿风1 小时前
【Linux系统】【Linux 进程程序替换机制解析及自定义 Shell 核心逻辑实现 】流食般投喂
linux·运维·服务器·开发语言·笔记
好不玩呀1 小时前
一次PXE服务器上IPV6问题的记录
linux·运维·服务器
NamoAmitabha1289 小时前
libpng12-0 1.2.54-1ubuntu1.1 (amd64 binary) in ubuntu xenial
linux·ubuntu20.4
Madison-No710 小时前
搭建项目测试环境
linux·运维·服务器·python
吴声子夜歌11 小时前
Shell编程实例——编写安全的shell脚本(二)
linux·运维·shell
xiaoye-duck12 小时前
《Linux 网络编程》深入理解 IP 协议(二):网段划分、私有 IP 与 NAT 地址转换
linux·网络·ip
zx_7414848113 小时前
【Linux入门】Shell 函数、正则表达式与文本处理:cut 与 awk
linux·运维·正则表达式