引导器 UEFI.boot.py (纯 stdlib, 内核的管家) 体检 6 项 (解释器/venv/包/驱动目录/PG) / venv 重建 / 包台账 / 内核进程启停记账 / 参数原样透传 内核日志 / 引导器日志 / 内核 stdio 三条道的查看入口都归它 内核 内核/ (常驻调度 = 甲) 命令消费 (PG commands + LISTEN/NOTIFY) / 驱动调用仲裁六条 (越权·成环·无人提供·按需拉起·同锁排队·超时收权) 依赖链巡检 10s / 心跳 300s / 独一份调度锁 (pg_try_advisory_lock 0x65666901) 扫描: 9 条校验 + 契约匹配 + 拓扑排序; 状态: 快照原子写 + 断电收尸判定 db.py 是唯一碰 SQL 的文件 (drivers/driver_state/events/scans/commands/calls/kernel_env/kernel_runs) 日志系统 (2026-09-16 完整化, 设计/04-日志系统.md) 三条道一个文件一种内容: 内核.log (结构化行) / 内核.out.log (命令输出 + 崩溃原文) / 引导器.log (引导器动作) 驱动日志每轮启动前插分隔头; 门槛 log_level / 轮转 log_max_mb + log_keep / -n · -f · --级别 · -g · --json · --全部 实现 内核/日志.py (内核与引导器共用一份); 修掉"命令输出混进日志文件" (实证存档 归档/20260916-日志系统重做前/) 驱动样板 (也是写驱动的示范): Json解码 (oneshot) / 样板常驻 (provides 样板:心跳) / 样例消费器 (needs 只发契约名) 代码标点统一为 ASCII (保留界面用的框线 ─│◄▶ 与表格占位符 —); 已用 AST 等价对比证明逻辑零改动 自测 (真机, 不 mock): 进程 (真起进程真收子树) / 内核 58 项 / 配置 / db 182 项 / 日志 86 项 验收: 试跑引导器.py PASS 11 / FAIL 0 / 残留无; uvx pyright 与 uvx basedpyright 均 0 errors 0 warnings
21 KiB
02 内核设计
内核 = 总调度(驱动互不认识,一切协调都经它)+ 纯 CLI 界面 + PostgreSQL 当内存。 没有通信协议、没有 TUI:命令进来,内核干活,状态写 PG。
1. 职责边界
| 内核做 | 内核不做 |
|---|---|
| 总调度:契约匹配 / 排序 / 路由 / 级联启停(见 01 第 3 节末) | 不读驱动源码内容(只记 hash) |
扫 驱动/ 找 配置.efi.json |
不替驱动装依赖 / 建 venv |
| 校验配置、写注册表 | 不改驱动的 配置.efi.json |
| 拉起 / 停止 / 重启驱动进程 | 不解析驱动的输出(驱动自己写 PG) |
| 判活、收尸、记事件到 PG | 不自启、不自动重启(除 restart: on-failure) |
| 写驱动文件夹下的状态快照 | 不管自己的环境/包(那是引导器的活,见 03) |
2. 四步流程(原设计逐条落地)
老板 内核设计.md 那 4 行 → 对应实现:
| 原设计 | 落地 | 产物 |
|---|---|---|
| 1. 遍历文件夹目录下驱动 efi 文件 | 扫 驱动/ 一级子目录,认 配置.efi.json 为驱动标志 |
drivers 表 upsert |
| 2. 遍历驱动配置文件 | 逐个读 配置.efi.json + 9 条校验(见 01 第 5 节) |
drivers.valid/error |
| 3. 生成驱动 json | 每个驱动文件夹下写 运行.efi.json 状态快照;全局清单 = drivers 表(要看文件就 内核.py 清单 > 清单.json) |
快照 + PG |
| 4. 管理驱动进程用 linux 命令 | 启动/停止/重启/状态/日志 → /proc 直读 + 信号 |
driver_state 表 + logs/ |
内核 = 总调度(驱动为什么不需要认识彼此)
驱动之间零耦合:不 import 对方、不互相调用、配置里也不写对方的名字。所以必须有个总调度 —— 内核是唯一知道全局关系的一方,所有协调都经它。
| 内核的调度职责 | 做法 |
|---|---|
| 契约匹配 | 驱动只声明 provides(我产出什么)/ needs(我要什么),都是中立契约名。内核全局匹配"谁提供 → 谁消费",驱动之间零引用 |
| 启动排序 | 按 provides/needs 拓扑排序;无环才启动;成环 → 相关驱动全标 invalid 并写明是哪个契约成环 |
| 就绪等待 | 上游没起来不拉下游;上游起来了才按序放行下游 |
| 数据路由 | 驱动不认识彼此,产出/取用只在 PG 里按契约进行(表名由内核约定并注入),内核负责把契约对上 |
| 级联生命周期 | 停上游 → 依赖它的下游一并停(标"上游已停");上游崩 → 下游标"依赖失效",不装作没事 |
| 崩溃处理 | 按 restart 策略决定重拉;重拉后按依赖顺序把下游恢复 |
一句话:驱动只管"我要什么、我产出什么","谁给谁、什么顺序、谁先谁后"全归内核。
内核调用:驱动不互相直连,只向内核发请求
老板定调:驱动发起一个请求 = 给内核的一条命令;内核转发;驱动执行完数据落到 PG;再把结果给请求方。 主驱动要调副驱动,就跟内核说一声,内核去转发 —— 这样驱动之间不可能打架(它们连对方是谁都不知道)。
主驱动 内核(常驻) PG 副驱动
│ │ │ │
├─ INSERT calls ──────►│ (LISTEN 唤醒) │ │
│ 契约 + 参数 │ │ │
│ ├─ 校验:谁提供该契约 / 权限 / 锁 / 超时 / 调用链成环 │
│ ├─ 已被占用 → 排队 waiting │ │
│ ├─ 转发 ──────────────────────┼───────────────────────►│
│ │ │ 副驱动执行 │
│ │◄── 完成(产出写契约表 / 回填 calls)──────────────────┤
│ ├─ 写 result + 释放锁 ───────►│ │
│◄── NOTIFY / 轮询到 done ┤ │ │
└─ 从 PG 取数据 │ │ │
请求按契约寻址,不按驱动名:请求方只说"我要 表格:网页",内核去匹配谁提供。
请求方始终不知道、也不需要知道对面是谁,calls.provider 那一列是内核自己记的账。
-- 驱动 → 内核的调用请求(内核是唯一处理者)
CREATE TABLE calls (
id bigserial PRIMARY KEY,
ts timestamptz DEFAULT now(),
caller text NOT NULL, -- 哪个驱动发的
want text NOT NULL, -- 契约名,不是驱动名
args jsonb DEFAULT '{}',
state text DEFAULT 'pending', -- pending|waiting|running|done|failed|timeout|denied
provider text, -- 内核匹配出的提供方(请求方看不到)
lock_key text, -- 占用的锁(防打架)
result jsonb, -- 小结果直放;大数据放引用(表名 / 行 id)
error text,
deadline timestamptz, -- 超时线
started_at timestamptz,
finished_at timestamptz
);
防打架六条(这就是内核存在的理由):
| # | 打架长什么样 | 内核怎么挡 |
|---|---|---|
| 1 | 两个驱动同时写同一份数据 | 同一 lock_key 串行化:先到先执行,后面的排队 |
| 2 | 调用链成环(A 要 B、B 要 A) | 内核记调用链,成环直接 denied(与契约依赖成环同一套判定) |
| 3 | 想调一个没起来的驱动 | 先 /proc 校验提供方是否 running,没起就先起(或按 mode 按需拉起),否则拒 |
| 4 | 一个驱动卡死拖垮全局 | 每个请求带 deadline,超时内核收权、标 timeout、释放锁 |
| 5 | 越权(想用没声明过的契约) | 请求方只能要自己 needs 里声明过的契约,否则 denied |
| 6 | 驱动乱指挥(去停别人的进程) | 权限分家:驱动只许写 calls / events;commands(管理命令)只有 CLI 和引导器能写;drivers / driver_state 只有内核能写 |
第 6 条 v0.1 用"约定 + 内核校验",v0.2 可上 PG 角色(给驱动一个只有 calls / events 权限的角色)做硬隔离。
驱动的两种活法(决定内核怎么"转发")
mode |
内核怎么对它 | 适合 |
|---|---|---|
resident(默认) |
起一次、常驻等活;转发靠 NOTIFY driver_<名>,驱动自己 LISTEN |
主驱动、服务型 |
oneshot |
按需:有请求时内核拉起它跑一遍,跑完收尸 | 副驱动、批处理型 |
两种都走同一个 进程.py(启停 / 判活 / 日志),对请求方没有任何区别 —— 请求方只发契约,不关心对面是常驻还是现拉。
3. 内存 = PostgreSQL(不设通信协议)
驱动 ↔ 内核之间没有协议。 两边都读写同一个库,events 表就是总线:
驱动进程 ──写──┐ ┌──读/写── 内核(CLI)
├──► PostgreSQL ◄─────┤
驱动进程 ──读──┘ efi_kernel └──读/写── 内核(CLI)
推论(这就是为什么不要协议):
| 好处 | 说明 |
|---|---|
| 内核可以随时死 | 状态在库里,重跑一条 CLI 就接着干,不需要"恢复会话" |
| 驱动不用跟内核握手 | 驱动直接 INSERT INTO events 汇报,内核不用解析 stdout |
| 不用设计消息格式 | 表结构就是格式,jsonb 装不确定的载荷 |
| 断电不丢现场 | 22:30 断电后,库里最后一条 heartbeat/exit 就是死因时间线 |
库与连接
| 项 | 值 |
|---|---|
| 库名 | efi_kernel(沿用"一项目一库";项目正式命名后一起改) |
| 实例 | 本机 PG 18,数据目录 /home/lou/pgdata,端口 5432 |
| 连接 | socket /home/lou/pgdata/socket(unix socket 优先,不走 TCP) |
| 驱动侧 | 连接串经环境变量 EFI_DB 由内核注入,不落盘(密钥不落盘口径) |
| 唯一碰 SQL 的文件 | 内核/db.py(建表 + 读写 + 事件写入),别处不写 SQL |
表设计(4 张,够用不超配)
-- ① 驱动注册表:内核扫描后 upsert
CREATE TABLE drivers (
name text PRIMARY KEY,
dir text NOT NULL, -- 驱动文件夹绝对路径
runtime text NOT NULL, -- python | exec
entry text NOT NULL,
interpreter text,
args jsonb DEFAULT '[]',
env jsonb DEFAULT '{}',
deps text[] DEFAULT '{}',
autostart boolean DEFAULT false,
restart text DEFAULT 'no',
config_hash text, -- sha256(配置.efi.json)
entry_hash text,
valid boolean DEFAULT true,
error text, -- invalid 原因原文(不吞错)
scanned_at timestamptz DEFAULT now()
);
-- ② 运行时状态:一行一驱动,内核每次动作刷新
CREATE TABLE driver_state (
name text PRIMARY KEY REFERENCES drivers(name) ON DELETE CASCADE,
state text NOT NULL DEFAULT 'stopped',
pid integer,
pgid integer,
started_at timestamptz,
stopped_at timestamptz,
exit_code integer,
restarts integer DEFAULT 0,
boot_hash text, -- 起进程时的配置指纹
list_version bigint,
last_error text,
updated_at timestamptz DEFAULT now()
);
-- ③ 事件流 = 总线:内核写,驱动也写
CREATE TABLE events (
id bigserial PRIMARY KEY,
ts timestamptz DEFAULT now(),
source text NOT NULL, -- kernel | <驱动名>
driver text, -- 关联驱动(可空)
level text DEFAULT 'info', -- info | warn | error
kind text, -- start|stop|exit|log|produce|heartbeat|error
message text,
data jsonb
);
CREATE INDEX events_ts_idx ON events (ts DESC);
CREATE INDEX events_driver_idx ON events (driver, ts DESC);
-- ④ 扫描批次:list_version 自增,用来判"这份状态是不是本次扫描的"
CREATE TABLE scans (
list_version bigserial PRIMARY KEY,
started_at timestamptz DEFAULT now(),
kernel text,
total int, valid int, invalid int, running int
);
boot_hash != drivers.config_hash → 该驱动显示"配置已改,待重启"(不用 diff 内容)。
引导器自己的两张表(kernel_env 环境体检 / kernel_runs 内核运行台账)定义在 03;内核的
commands(命令表)与 calls(驱动调用请求)见第 2 节末。全部同一个库 efi_kernel。
4. 进程管理(第 4 步)
用 linux 原语,不引入 supervisor / systemd。不论内核自身是否常驻(这个分叉见本节末),
驱动判活一律以 /proc 为准,不信 PG 里的旧 pid —— pid 会被系统复用。三条铁律来自踩过的坑:
| 铁律 | 为什么 |
|---|---|
Popen(cwd=驱动目录, start_new_session=True) |
独立进程组 → 停的时候能连子树一起收(杀父不等于杀子树) |
停之前先读 /proc/<pid>/cmdline 校验,再发信号 |
pid 会被系统复用,裸 kill <pid> 可能杀到别人的进程 |
| 启动与停止分两条 CLI 调用,一次只起一份 | 同一 shell 里连做会留孤儿互抢端口 |
启动 内核.py 启动 <名>
- 查 PG:
state == running且/proc/<pid>在 + cmdline 匹配 → 拒绝(幂等,不重复起) - 拼命令:
[解释器, entry] + args(exec 形态[entry] + args),env= 基线 +EFI_DB+ 配置env Popen(cwd=驱动根, stdout/stderr=logs/<名>.log, start_new_session=True)- 状态置
starting→ 探/proc/<pid>/cmdline匹配 →running;进程秒退 →failed+exit_code - 刷 PG
driver_state(pid/pgid/started_at/boot_hash/restarts+1)+ 写events(kind='start')+ 刷驱动文件夹快照
停止 内核.py 停止 <名>
- 查 PG 拿 pid;无 pid 或
/proc/<pid>不在 → 直接置stopped(幂等) - 校验 cmdline:
/proc/<pid>/cmdline必须出现该驱动的入口路径(前缀匹配)→ 不是则判"pid 被复用", 不杀,只清状态 + 记last_error(宁可不杀,不可误杀) SIGTERM给进程组killpg(-pgid)→ 等超时(默认 10s)- 还活着 →
SIGKILL,按 pid 逐个收残 - 复查
/proc为空才算停干净,state=stopped+stopped_at+events(kind='stop')
状态 内核.py 状态 [名]
不看 ps 解析(会被截断),直读 /proc/<pid>/cmdline 精确点名:
| 判定 | 结果 |
|---|---|
| PG 无 pid / pid 不存在 | stopped 或 crashed(有退出码且非零 → crashed) |
| pid 在,cmdline 匹配自家 entry | running(回填运行时长、CPU、内存 RSS) |
| pid 在,cmdline 不匹配 | exited(pid 被复用,不认领、不杀) |
内核自身 = 常驻调度器(定稿:甲)
内核进程常驻跑着,命令从 PG 进来:
| 项 | 落法 |
|---|---|
| 内核进程 | 常驻;引导器负责它的存在(启停 / 判活 / 日志 / 记账,见 03) |
| 命令通道 | CLI 客户端把命令写 commands 表;内核 LISTEN PG 自带的 LISTEN/NOTIFY |
| 为什么必须常驻 | 总调度要在运行时看着依赖链:契约满足要唤醒下游、上游崩要级联、驱动请求要转发仲裁 —— 只在启动那一下算一遍不够 |
| 手动控制 | 引导器不 autostart,起停由人敲命令 |
| 自造协议 | 零。没有 socket、没有自定消息格式,全部走 PG(表 + NOTIFY) |
-- 甲才需要:命令表(CLI 客户端写,常驻内核消费)
CREATE TABLE commands (
id bigserial PRIMARY KEY,
ts timestamptz DEFAULT now(),
source text, -- cli | 引导器
cmd text NOT NULL, -- 启动|停止|重启|扫描|状态...
args jsonb DEFAULT '{}',
state text DEFAULT 'pending', -- pending|running|done|failed
result jsonb,
started_at timestamptz,
finished_at timestamptz
);
5. 断电收尸(本机 22:30 断电,这是必做项)
内核每次 扫描 / boot() 都先看 PG 再判:
PG driver_state |
/proc/<pid>/cmdline |
判定 | 动作 |
|---|---|---|---|
running 有 pid |
存在且匹配 | 认领 → running |
不重起,接管 |
running 有 pid |
不存在 | crashed |
events(kind='exit', message='断电/被杀');autostart=true 才拉起 |
running 有 pid |
存在但不匹配 | exited |
清 pid,不杀 |
stopped / 无记录 |
— | stopped |
无 |
好处:断电重启后不用翻日志,一条 内核.py 状态 就把"上次谁在跑、崩在哪步"全列出来;驱动文件夹里的
运行.efi.json 快照是它的离线副本(文件夹自包含,拷走也带状态)。
6. 界面 = 纯 CLI + 日志
没有 TUI。 就两条:对齐的中文输出 + 结构化日志。人是看输出,机器看 --json。
子命令
python3 内核/内核.py 列表 # 驱动清单表(默认动作)
python3 内核/内核.py 扫描 # 只扫不启,刷新 PG + 快照
python3 内核/内核.py 启动 <名>
python3 内核/内核.py 停止 <名>
python3 内核/内核.py 重启 <名>
python3 内核/内核.py 状态 [名] [--json] # --json 给机器读
python3 内核/内核.py 日志 <名> [-n 200] [-f] # -f = tail -f 实时跟(要能看实时进度)
python3 内核/内核.py 事件 [-n 50] # 全局事件流(PG events 表)
python3 内核/内核.py 清单 # 输出清单 JSON(重定向就是文件)
输出样例(列表)
状态 驱动名 形态 PID 运行时长 配置 说明
停止 抓取器 exec — — 一致 —
运行 Json解码 python 12345 00:12:31 待重启 配置已改,需重启生效
无效 坏驱动 python — — — 入口路径越界
────────────────────────────────────────────────────────────────────────
驱动 3 / 有效 2 / 运行 1 / 无效 1 扫描 #7 22:50:03
日志规范(2026-09-16 完整化,细节见 04-日志系统.md)
三条道,一个文件只装一种内容(修前命令输出会混进日志文件——实测抓到 4 行表格污染,
存档 归档/20260916-日志系统重做前/内核.log):
| 文件 | 内容 | 从哪看 |
|---|---|---|
内核/logs/内核.log |
只有结构化日志行 | 内核 日志 / 日志 --内核 |
内核/logs/内核.out.log |
内核进程 stdout/stderr 原始流(命令输出 + 崩溃原文) | 内核 日志 --输出 |
内核/logs/引导器.log |
引导器的动作(体检 / 包 / 移交 / 内核启停) | UEFI.boot.py 日志 |
驱动/<名>/logs/<名>.log |
驱动原始输出(内核重定向不解析;每轮启动前写一条分隔头) | 日志 <驱动名> |
- 行格式:
2026-09-15T22:50:03+08:00 INFO [内核] 扫描完成 drivers=3 valid=2 invalid=1 version=7(时刻带时区 / 级别 /[来源]/ 内容;多行内容照原文写,不截断) - 级别与门槛:
DEBUG / INFO / WARN / ERROR,门槛 =环境.efi.json的log_level(低于它的不写); 错误一律带原文(不吞错、不静默降级) - 轮转:超过
log_max_mb(默认 5)改名.1,留log_keep(默认 3)份; 只在拉起进程之前轮转(运行中改名会让进程继续写老 inode = 日志丢了) - 过滤 / 实时跟 / JSON:
日志 [驱动名|--全部|--内核|--引导器|--输出] [-n 200] [-f] [--级别 X] [-g 关键词] [--json] - 驱动自己的结构化汇报:走 PG
events表(source = 驱动名)—— 这就是"不要协议"的落地方式 - 实现只有一份:
内核/日志.py(内核与引导器共用,纯 stdlib)
7. 内核自己的文件布局
一套职责一个文件,不合并:
工作区/内核/
UEFI.boot.py 引导器(内核的管家,见 03)
环境.efi.json 项目级配置(引导器写,内核只读,见 03)
环境状态.efi.json 环境体检快照(引导器写)
驱动/ 驱动文件夹(一个驱动一个目录)
<驱动名>/
配置.efi.json 驱动作者写
运行.efi.json 内核写的状态快照
logs/ 内核收驱动的 stdout/stderr
内核/
内核.py 入口:CLI 子命令分发
扫描.py 扫驱动 + 读配置 + 校验 + 写 PG
进程.py 启动/停止/判活/日志(/proc + 信号)—— 通用进程库
状态.py 快照读写 + 收尸判定
db.py 唯一碰 SQL 的地方:连接/建表/事件
日志.py 日志系统:写/解析/过滤/轮转/实时跟(内核与引导器共用, 见 04)
logs/内核.log 内核的结构化日志行(内核自己写)
logs/内核.out.log 内核进程的 stdout/stderr(命令输出 + 崩溃原文)
logs/引导器.log 引导器的动作日志
自测*.py 五份自测(进程 / 内核 / 配置 / db / 日志)
设计/ ← 本目录(01~04 + 图/)
进程.py 是通用进程库:内核用它管驱动进程,引导器用它管内核进程(见 03 第 7 节)。
只此一份实现,不复制。
配置:内核不再自持配置文件
内核读项目根的 环境.efi.json(唯一一份配置,引导器管,见 03 第 4 节)。原先设想的"内核自己的
配置.efi.json"取消 —— 两份配置迟早对不上。
8. 错误处理
| 场景 | 内核行为 |
|---|---|
| PG 连不上 | 明确报错退出(内存不在就没法干活),不降级到文件模式(避免两份真相) |
| 驱动目录不存在 | 报明确错,不建空目录(避免静默假成功) |
| 某驱动配置错 | 只标它 invalid + error 原因,其他驱动照常(隔离失败) |
| 启动后秒退 | failed + exit_code + 日志末尾若干行原文 |
| 停止超时 | 升级 SIGKILL,记 last_error |
| pid 被复用 | 不杀,清状态,记 last_error |
| 快照 json 损坏 | 当无快照处理重建;原子写(.tmp + os.replace)不留半截文件 |
手工删了 .venv |
回落 python3 + WARN,不假死 |
9. v0.1 范围
做:驱动识别 / 契约匹配与排序 / PG 注册表·状态·事件 / 启停状态日志收尸 / 常驻调度内核(甲) /
命令表 + 驱动调用转发(calls) / 纯 CLI。
不做(明确留给后面):程序 / Skill 层加载(见 01 第 7 节)、PG 角色级硬隔离(v0.2)、 开机自启、TUI、资源限额与权限隔离。