Files
efi-kernel/设计/02-内核设计.md
lou 0827b2399c 内核框架 v0.1 首次提交: 引导器 / 内核 / 驱动 + 日志系统
引导器 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
2026-09-16 21:03:48 +08:00

21 KiB
Raw Permalink Blame History

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 / eventscommands(管理命令)只有 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/socketunix 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 启动 <名>

  1. 查 PGstate == running/proc/<pid> 在 + cmdline 匹配 → 拒绝(幂等,不重复起)
  2. 拼命令:[解释器, entry] + argsexec 形态 [entry] + args),env = 基线 + EFI_DB + 配置 env
  3. Popen(cwd=驱动根, stdout/stderr=logs/<名>.log, start_new_session=True)
  4. 状态置 starting → 探 /proc/<pid>/cmdline 匹配 → running;进程秒退 → failed + exit_code
  5. 刷 PG driver_statepid/pgid/started_at/boot_hash/restarts+1+ 写 events(kind='start') + 刷驱动文件夹快照

停止 内核.py 停止 <名>

  1. 查 PG 拿 pid;无 pid 或 /proc/<pid> 不在 → 直接置 stopped(幂等)
  2. 校验 cmdline/proc/<pid>/cmdline 必须出现该驱动的入口路径(前缀匹配)→ 不是则判"pid 被复用" 不杀,只清状态 + 记 last_error(宁可不杀,不可误杀)
  3. SIGTERM进程组 killpg(-pgid) → 等超时(默认 10s
  4. 还活着 → SIGKILL,按 pid 逐个收残
  5. 复查 /proc 为空才算停干净,state=stopped + stopped_at + events(kind='stop')

状态 内核.py 状态 [名]

不看 ps 解析(会被截断),直读 /proc/<pid>/cmdline 精确点名:

判定 结果
PG 无 pid / pid 不存在 stoppedcrashed(有退出码且非零 → crashed
pid 在,cmdline 匹配自家 entry running(回填运行时长、CPU、内存 RSS
pid 在,cmdline 不匹配 exitedpid 被复用,不认领、不杀)

内核自身 = 常驻调度器(定稿:甲)

内核进程常驻跑着,命令从 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.jsonlog_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、资源限额与权限隔离。