从零写OS(三十八):SMP 启动 —— 唤醒第二颗 CPU

到目前为止,内核一直跑在单核上。这一章迈出多核的第一步:让所有 CPU 核心都进入内核。 验证结果: [acpi] MADT found, 2 CPU(s) [smp] booting AP lapic=1 [cpu0] online [cpu1] online [smp] all APs online, total CPUs=2 / # x86 多核启动的规则 x86 多核启动有一套固定规则。系统上电后只有一颗 CPU 跑起来,叫做 BSP(Bootstrap Processor)。其余的核心叫 AP(Application Processor),处于等待状态,需要 BSP 主动唤醒。 唤醒的方式是通过 LAPIC(Local APIC) 发送 IPI(Inter-Processor Interrupt)。每颗核心内置一个 LAPIC,是核间通信的硬件基础。 第一步:找到所有 CPU CPU 的信息藏在 ACPI MADT 表里。MADT(Multiple APIC Description Table)是固件写好放在内存里的一张表,描述了系统上有多少颗 CPU 以及每颗 CPU 的 LAPIC ID。 内核启动时扫描 MADT,把所有有效 CPU 的 LAPIC ID 记下来: void acpi_parse_madt(void) { madt_t *madt = acpi_find_table("APIC"); // 遍历所有条目,找 type=0 的 Processor Local APIC 条目 // 记录 lapic_id → cpu_lapic_ids[],ncpus++ } 第二步:发送 INIT + STARTUP IPI 唤醒 AP 的序列是固定的(来自 Intel SDM): ...

June 4, 2026 · 2 min · 大飞

LLMLingua:用小模型压缩 Prompt,省 95% Token

调研范围:LLMLingua 系列(v1 / LongLLMLingua / LLMLingua-2)项目背景、技术演进、性能数据、横向对比、工程落地 主要文献:arXiv:2310.05736(EMNLP 2023)、arXiv:2310.06839(ACL 2024)、arXiv:2403.12968(ACL Findings 2024) 一、项目背景与核心原理 1.1 动机:Prompt 膨胀是 LLM 落地的成本瓶颈 CoT、ICL、RAG 等技术的普及使 prompt 长度从几百 token 迅速膨胀到数万 token。按 GPT-4 计费标准,一次请求仅 prompt 成本就可能达 $0.3–$1+。企业级应用中,80% 的 token 开销往往集中在冗余的上下文内容里。 技术范式 对 Prompt 的影响 In-Context Learning (ICL) 注入多个 few-shot 示例,每个示例 300–600 token Chain-of-Thought (CoT) 完整推理链进一步增大示例体积 RAG(检索增强生成) 每次请求注入若干检索文档段落 Agent / 工具调用 System prompt + 对话历史 + 工具描述累积 三类影响: 成本:Token 按量计费,压缩 20× 可节省约 95% 的 input token 费用 延迟:prefill 阶段与输入长度正相关,长 prompt 直接拉高首 token 延迟(TTFT) 准确率:超长上下文有 “lost-in-the-middle” 问题——LLM 对中间位置的信息关注度显著低于首尾 1.2 核心洞察:用小模型 PPL 代理信息量 LLMLingua 的根本思路是:用廉价的小语言模型(GPT-2/LLaMA-7B)计算 token 的困惑度(Perplexity),以此作为信息重要性的代理。 ...

June 3, 2026 · 8 min · 大飞

自制 x86-64 内核(46):mmap 文件映射

上一章实现了 TCP 服务端,这章来实现一个经典的内存管理功能:mmap 文件映射。 什么是 mmap 文件映射 mmap 把文件的一段内容映射到进程的虚拟地址空间。映射后可以像读内存一样读文件——不需要 read() 系统调用,直接解引用指针。 int fd = open("/bin/busybox", O_RDONLY); char *p = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0); // p[0..3] 就是文件的前 4 字节(ELF magic: 7f 45 4c 46) 关键设计:VMA 和 Lazy 分配 mmap 不会立刻分配物理内存,而是登记一个 VMA(虚拟内存区域),等到进程真正访问这段地址时,才通过缺页异常按需读取文件内容并建立页表映射。 mmap(fd) → 注册 VMA(vaddr, len, inum, offset) → 返回虚拟地址 ↓ 进程访问该地址 #PF not-present → 找到 VMA → 分配物理页 → ext2_read(inum, offset, page) → 建立页表 → 重试指令 这就是操作系统课上说的"按需分页"(demand paging)。 实现步骤 1. 定义 VMA 结构 在 process_t 中加入 VMA 数组: ...

June 3, 2026 · 2 min · 大飞

自制 x86-64 内核(47):动态链接器基础

这一章实现了对 PIE(Position-Independent Executable)程序的支持,也就是不加 -static 编译出来的"动态"程序。 问题:PIE 和静态 ELF 的区别 用 musl-gcc 编译一个简单的 hello world: # 静态(之前一直用的) musl-gcc -static -O2 -o hello hello.c # 动态(PIE) musl-gcc -O2 -o hello_pie hello.c file 查看: hello: ELF 64-bit LSB executable, x86-64, statically linked hello_pie: ELF 64-bit LSB pie executable, x86-64, dynamically linked 区别在于: 属性 静态 (ET_EXEC) PIE (ET_DYN) e_type 2 3 虚地址 固定(如 0x400000) 从 0 开始,需加载时指定 base 重定位 无 .rela.dyn 表 外部依赖 无 通常有(但 musl PIE 已内嵌 libc) 为什么 musl PIE 没有"真正的"动态链接? musl-gcc 默认把所有 libc 代码静态链接进去,只是编译为 PIE 格式(支持 ASLR)。所以运行时: ...

June 3, 2026 · 1 min · 大飞

从零写OS(四十五):TCP 服务端——bind/listen/accept

前面几章都是做 TCP 客户端——发 SYN、连接、发请求、收响应。这章反过来,让内核能作为 TCP 服务端:监听端口,接受连接,响应请求。 客户端 vs 服务端的区别 客户端:主动发 SYN,等待 SYN-ACK。 服务端:被动等待 SYN,收到后发 SYN-ACK,等 ACK 完成三次握手,然后通过 accept() 把新连接交给应用层。 关键区别在于:服务端有两种 socket 角色—— 角色 职责 监听 socket(listen fd) 绑定端口,等待连接请求 连接 socket(accept fd) 每个客户端连接对应一个,负责实际数据收发 三步接口 bind(fd, &sa, len) // 绑定本地端口 listen(fd, backlog) // 进入监听状态 accept(fd, &sa, &len) // 阻塞,等到有连接,返回新 fd 数据结构 tcp_sock_t 加了几个字段: int is_listener; int accept_queue[8]; // 存已完成三次握手的连接 index uint32_t accept_head, accept_tail; 还新增了两个状态: TCP_LISTEN:监听中,等待 SYN TCP_SYN_RECV:收到 SYN 已回 SYN-ACK,等最终 ACK 核心流程 1. 收到 SYN tcp_handle() 里单独处理 SYN: if ((tcp->flags & TCP_SYN) && !(tcp->flags & TCP_ACK)) { // 找到监听该端口的 socket // 分配新连接 slot ns->state = TCP_SYN_RECV; ns->local_port = dport; ns->remote_port = sport; ns->remote_ip = src_ip; ns->ack = ntohl(tcp->seq) + 1; // 下次要 ACK 的序号 ns->syn_seq = 0xABCD1234; ns->snd_una = ns->snd_nxt = ns->syn_seq; tcp_send_seg(ns, ns->syn_seq, TCP_SYN | TCP_ACK, 0, 0); } 2. 收到最终 ACK(完成三次握手) if (s->state == TCP_SYN_RECV && (tcp->flags & TCP_ACK)) { if (ack_val == s->syn_seq + 1) { s->snd_una = s->snd_nxt = ack_val; s->state = TCP_ESTABLISHED; // 放入监听 socket 的 accept queue ls->accept_queue[ls->accept_tail % 8] = i; ls->accept_tail++; } } 3. accept() 取连接 int tcp_accept(int s, ...) { tcp_sock_t *ls = &tcp_socks[s]; while (ls->accept_head == ls->accept_tail) { sti; net_poll(); cli; // 阻塞等待 } int ni = ls->accept_queue[ls->accept_head++ % 8]; return ni; } 测试 写了一个简单的 HTTP 服务端程序: ...

June 2, 2026 · 2 min · 大飞

从零写OS(四十四):UDP Socket + DNS 解析

上一章(四十三)把 TCP 重传做好了。这一章加两个新功能:UDP socket 用户空间接口 和 DNS 解析 syscall,让用户程序可以用域名来连接服务器。 目标 内核里其实早就有 udp_send 和 dns_resolve 这两个函数了,但用户程序用不到,因为没有对应的 syscall。这章要做的就是把这两个内核能力"打通"到用户空间: socket(AF_INET, SOCK_DGRAM, 0) → 分配一个 UDP socket,返回 fd 200+ sendto / recvfrom → 通过 UDP socket 收发数据 SYS_DNS_RESOLVE(自定义 syscall 500)→ 通过域名查 IP UDP Socket 设计 UDP 比 TCP 简单很多:无连接、无状态机、无重传。核心是一个接收队列:内核收到 UDP 包时,把它放进对应 socket 的队列,用户程序再来取。 typedef struct { uint32_t src_ip; uint16_t src_port; uint16_t len; uint8_t data[512]; // 每个包最多 512 字节 } udp_pkt_entry_t; typedef struct { int used; uint16_t local_port; udp_pkt_entry_t queue[4]; // 最多暂存 4 个包 uint32_t qhead, qtail; } udp_sock_t; 队列用无限增长的 qhead/qtail 计数(不是环形下标),访问时用 % UDP_PKT_MAX,满了就丢包: ...

June 2, 2026 · 3 min · 大飞

源码分析(一):Netflix Headroom 是怎么把 LLM 账单砍掉一半的

最近看到一个叫 Headroom 的项目,Netflix 高级工程师 Tejas Chopra 个人开源的,号称能帮你把发给 LLM 的 token 减少 30-70%,而且不丢信息。 我把源码读了一遍,发现里面有几个设计很有意思,记下来。 项目地址:https://github.com/chopratejas/headroom 本文分析的主要源文件: headroom/transforms/content_router.py — 内容识别与路由 headroom/transforms/content_detector.py — 内容类型检测 crates/headroom-core/src/transforms/smart_crusher/ — SmartCrusher Rust 实现 crates/headroom-core/src/transforms/log_compressor.rs — 日志压缩 Rust 实现 headroom/transforms/cache_aligner.py — CacheAligner headroom/ccr/tool_injection.py — CCR 工具注入 它解决的是什么问题 你在用 Claude 或 GPT 做 agent 的时候,工具调用(tool call)的返回结果会吃掉大量 token。 比如你让 agent 查数据库,返回了 500 条记录,每条有 15 个字段。但其中 12 个字段在所有记录里都是完全相同的值,真正有用的只有 3 个字段。你把 500 × 15 的数据全塞给 LLM,它实际只需要 500 × 3。 这就是浪费。Headroom 做的事,就是在你把数据发给 LLM 之前,先把这些废话压掉。 ...

June 2, 2026 · 2 min · 大飞

tclaw(五):接入飞书、语音、图片

让 AI 融入日常 桌面应用之外,tclaw 还支持通过飞书和微信直接和 agent 对话。 飞书 飞书有开放平台,可以创建自己的机器人。tclaw 接入之后,在飞书里和 tclaw 对话,就相当于在桌面应用里聊天,但可以用手机。 支持的能力: 发文字消息,agent 回复 发图片给 agent,agent 可以分析图片内容 agent 可以把生成的图片发回飞书 微信 微信这边不需要额外配置,在设置里开启微信,直接扫页面上的二维码登录,之后就可以在微信里和 agent 对话了。 图片输入与截图 桌面端也补全了多模态能力: 聊天框可以直接粘贴或拖入图片发给 agent 内置截图工具,可以截当前屏幕,裁剪、标注之后直接发给 agent agent 可以调用图片生成工具,生成的图片直接显示在对话里 图片生成 接了两个图片生成后端: Ollama:本地跑,不花钱,速度慢一点 外部 API:质量更好,按量计费 agent 可以在任务里直接调用图片生成,比如写完一篇文章顺手配一张封面图。 tclaw 提供 Mac、Linux、Windows 版下载,Mac 版支持最好,感兴趣可以去 tclaw 体验。 也可以先玩玩 tclaw 用 AI 做的几个小游戏:→ 点这里玩

June 2, 2026 · 1 min · 大飞

tclaw(四):从能用到好用

能用只是开始 内核稳定之后,tclaw 已经能干活了。但"能用"和"好用"之间,有很长一段路。 用着用着,各种不顺手的地方就冒出来了。agent 生成的文档没法方便地看;上下文长了之后 token 烧得很快;只有命令行,想给别人用很难;文件多了之后找东西很费劲…… 接下来花了差不多一个月的时间,一件一件地打磨。 Wails 桌面应用 首先是 GUI。自己用命令行没问题,但如果想让更多人用,得有个像样的界面。 调研了一圈,选了 Wails——用 Go 写后端,前端是普通的 HTML/JS,打出来是一个原生桌面应用,体积小,对 Go 友好。最终打包出来的 Mac 应用只有几十 MB,不需要用户装任何运行时。 同一套 HTML,既可以跑在 Wails 桌面里,也可以用浏览器直接访问。 预览面板 agent 经常帮我生成文档、写 HTML 页面、画 mermaid 流程图。生成完要看效果,之前要自己去找文件打开,很麻烦。 做了一个右侧预览面板: Markdown 文件直接渲染,支持 mermaid 图表 HTML 文件直接在面板里预览网页效果 图片直接显示 还可以在预览内容上框选区域,加标注,直接发给 AI 问问题 文件浏览器 本地文件多了之后,找东西很头疼。做了一个内置的文件浏览器,可以快速搜索文件,并且直接在里面预览: Excel 打开直接显示表格 JSON/XML 显示树形结构,可以展开折叠 zip/tar 显示压缩包内容列表 SVG 可以缩放拖拽 PDF、图片、音视频都能预览 还加了文件格式转换和压缩功能,平时用得上的小工具基本都有了。 上下文管理 用 LLM 最头疼的问题之一是上下文窗口。对话长了,token 烧得很快,而且超出窗口就报错。 做了几件事: 自动剪裁:上下文快满的时候,自动把旧的 tool result 压缩,保留关键内容 保留读取位置:被剪裁的内容不是直接丢掉,而是记录偏移量,agent 需要的时候还能用 offset 继续读 cache 优化:加了多个 cache breakpoint,反复用的内容不重复计费 token 看板:侧边栏可以实时看每轮的 token 消耗,一眼就知道上下文用了多少 VSCode 风格布局 界面改版了一次,改成 VSCode 风格的 Activity Bar + Side Panel 布局: ...

June 2, 2026 · 1 min · 大飞

tclaw(三):重构——换一种方式让 Agent 协作

问题出在哪里 消息总线那套方案放弃之后,我坐下来想了一下,问题到底出在哪。 根本原因是:消息总线是对等的,每个 bot 都可以给任何人发消息。这种自由度在人类团队里没问题,因为人有判断力,知道什么时候该说话、什么时候不该插嘴。但 LLM 不一样,它的"判断"是概率性的,今天遵守规则,明天又忘了。你加再多提示词约束它,它该乱的时候还是会乱。 既然对等通信管不住,那就换成单向调用——上级调下级,下级只干自己的事,不主动找别人。 新的设计 重构后的 tclaw,多 agent 协作的方式变成了这样: list_categories → list_agents → run_agent 用户或者顶层 agent 先用 list_categories 看看现在有哪些领域,再用 list_agents 列出某个领域下的所有 agent,然后用 run_agent 调用具体的 agent 去做事。 就像公司里的层级结构:你要找人做事,先找到对应的部门,再找到对应的人,直接下任务。这个人完成任务后把结果交回来,不会自己跑去找别的部门。 这样一来,协作的控制权始终在调用方手里,被调用的 agent 只负责完成自己的任务,不能主动发起新的协作链路。 pcclaw → tclaw 这次重构等于把整个架构重新来过,代码改动很大,干脆起了个新名字:tclaw。t 是我名字的首字母。 2026 年 4 月 21 日,tclaw 第一次提交。从 pcclaw 的第一次提交到现在,差不多过了五周。 新架构稳定多了。agent 之间的协作变得可预测,出了问题也知道去哪里找。当然也不是完美的——有时候顶层 agent 该调用专业 agent 的时候,它自己就把事情做了,没有委托出去。但比起消息总线那套,已经好太多了。 同期做的其他事 重构完内核,顺手把周边也整理了一遍: 把引擎抽成独立包,方便后续扩展 加了 WebSocket server,可以用浏览器访问 做了 WebGUI,有 tool call 展示 加了 session 历史侧边栏 做了 onboarding wizard,引导新用户配置 从只能在命令行用的小工具,开始变成一个像样的产品。 ...

June 2, 2026 · 1 min · 大飞
京ICP备14031575号-3