起因:2026-06-30 AIOJ 企业站(
47.110.12.36)整机卡死,借这次把"远程访问 / 部署 / 运行"的底层机制梳理成一份能复看的笔记。尽量去掉"大楼"这类比喻,讲真身。
0. 一句话总纲
一台"服务器" = 一台(通常是虚拟的)电脑,上面跑着几个互相独立的进程,每个进程在一个端口上"听客"。你平时打交道的"数据库""网站""SSH",其实是这台机器上三个不同的服务/进程,各有各的门、各有各的钥匙、各能干不同的事。
1. "一个服务在跑"的真身 = 进程 + 一个监听 socket
没有"端口"这个实体。真实发生的是:
- 机器上有个进程(
node跑你的 Next.js、nginx、postgres…)。 - 它启动时向内核要一个套接字:
socket()→bind()把它绑到某端口(如:80)→listen()告诉内核"我在这个端口收客"。这就是"占用/监听端口"的真身——内核维护一张表:{协议, IP, 端口} → 哪个进程。 - 在机器上
ss -ltnp(旧命令netstat)能看到这张表:哪个端口、哪个进程 PID 在听。 - 客户端连进来时:内核完成 TCP 三次握手,把这条连接放进该 socket 的 accept 队列;进程再调用
accept()把它取出来,然后read()/write()收发数据。
→ 记住"握手是内核干的、收发数据是进程干的",第 7 节会用它解释宕机现象。
2. 三扇门:DB / web / SSH 是三个不同服务
| 端口 | 服务 | 钥匙 | 能干什么 |
|---|---|---|---|
:5432 | PostgreSQL 数据库进程 | 数据库账号密码(DATABASE_URL) | 只能读写数据 |
:80 | web(nginx / Node) | 无(公开) | 看页面 |
:22 | SSH(sshd) | 操作系统账号 / 密钥 | 拿到 shell,变成机器里的用户,能动整台机器 |
关键:数据库密码 ≠ 机器登录权。 那串 DATABASE_URL 只开数据库这扇窗,开不了 SSH。这是"最小权限"的故意设计:让应用/我们恰好拿到数据访问,不该能进去操作整台服务器。
3. 为什么"不进机器"也能写库、也能部署
我们干过的两类活都不需要 SSH 进机器:
- 写库(改数据 / 清理字段):拿
DATABASE_URL直连:5432的 postgres 进程,人根本没进机器。 - 部署代码 / 跑数据库迁移:
git push→ 触发 CI/CD 流水线,流水线(有机器登录权的机器人)替我们进机器 build + 重启 + 跑迁移。
→ 所以"迁移自动生效"不是我们进机器跑的,是流水线在机器上跑的。
4. "部署"机械上到底发生了什么(全链)
- 你
git push到远端(GitHub / Codeup)。 - 触发 CI/CD 流水线——流水线跑在另一台临时机器上,叫 runner。
- runner
git clone代码 →npm ci装依赖 →next build编译出产物(.next/、静态文件)。 - runner 把产物传到目标服务器(
scp/rsync,或打成 Docker 镜像推到镜像仓库再在服务器docker pull)。 - 在服务器上重启进程:停旧
node、起新node(让它重新bind端口)。数据库迁移(drizzle migrate)通常就在这一步跑。 - 新进程起来
listen,部署完成。
第 3 步的"runner 有机器登录权"是核心:有个有权限的机器人替我们进了机器,所以我们自己从不用 SSH。
5. 谁保证进程一直活着:进程管理器(supervisor)
node 崩了不能靠人盯。机器上有进程管理器负责开机自启、崩溃自动拉起、收日志:
- systemd(Linux 自带,最常见):写个 unit 文件描述服务怎么启动;
systemctl status/restart 服务名管理,journalctl -u 服务名看日志。 - pm2(Node 生态)、Docker / 容器(把进程+依赖+环境打包,
docker run起、docker logs看日志、restart: always崩了自动拉)。
→ "重启 web 服务" = systemctl restart / pm2 restart / docker restart 之一。得先知道这台机用哪个,才知道怎么重启——这也是为什么必须进机器看。
6. "机器"是一层层叠上去的
从下到上:
- 物理服务器(机房里一台真铁:CPU / 内存 / 网卡 / 硬盘)。
- Hypervisor(虚拟化层,如 KVM):把一台物理机切成很多台虚拟机。它在 OS 之外——所以能不管 OS 死活地画虚拟屏、断电重启(带外控制就住这)。
- 你的 ECS = 一台 Guest OS(Linux):以为自己是独立电脑,其实跑在 hypervisor 给的虚拟硬件上。
- OS 里再跑:进程管理器 → 你的进程。
- 你的代码,只是这台 OS 上的几个文件 + 一个被拉起来的进程。
7. 带内(in-band)vs 带外(out-of-band)—— 远程救援的核心
- SSH 是带内:sshd 只是机器里的一个普通进程,靠机器的 CPU/内存/磁盘活着。机器整体卡死,SSH 跟着死(连版本号 banner 都发不出)。它和故障"同生死",所以靠不住。
- 带外 = 不依赖那台机器本身的控制路径:
- 云:阿里云控制台 →【管理终端 / VNC】(hypervisor 画的虚拟屏,绕开 sshd 和网络,能看到登录界面)+【强制重启实例】(hypervisor 给虚拟机断电再上电,机器不配合也行)。
- 物理机:IPMI / BMC(戴尔 iDRAC、惠普 iLO)= 主板上一块独立小芯片 + 独立网口 + 独立供电,主 OS 死了甚至关机它都还活着,能远程看屏、按电源键。
- 铁律:严肃基础设施一定留一条与主系统不同生死的带外路径。
- 但带外锁在云账号后面(不是网络上某个端口,那样太危险),只有机器主人能用。
8. "机器卡死"在底层是什么样(2026-06-30 实测)
现象:ping 通、:22/:80/:5432 端口 TCP 都能握手,但 SSH 连 banner 都发不出、HTTP 进去 0 字节超时。底层机制:
- 三次握手是内核干的,在进程
accept()之前就完成 → 所以nc显示端口succeeded,哪怕进程一个字节都处理不了。 - 进程要真正服务你得
accept()+read()/write()。机器若:- 磁盘满:进程一
write()(写日志/临时文件)就返回ENOSPC或卡住;sshd 启动要读写文件 → banner 发不出。 - 内存爆(OOM):内核 OOM killer 杀进程(
dmesg见Out of memory: Killed process …),活着的也卡在内存回收。 - I/O 卡 / 负载爆:进程卡在 D 状态(uninterruptible sleep,等磁盘 I/O 永不返回),
top里 load average 飙到几十几百。
- 磁盘满:进程一
- 综合:内核还能握手(端口"开着"),但用户态进程拿不到资源去 accept/收发(应用层一片死寂)。这正是
:22banner 超时 +:80零字节的真身。 - 修它必须带外:进去
df -h(磁盘)/free -m(内存)/dmesg(OOM)→ 清理或重启 → 而进机器的 SSH 恰被同一故障掐死。
9. 进到机器后的实用命令小抄
ss -ltnp:看哪些端口、哪些进程在听。systemctl status/restart 服务、journalctl -u 服务 -n 200:看/重启服务、看日志。df -h:看磁盘是否满。free -m:看内存。top/htop:看负载与进程。dmesg | tail:看内核消息(OOM、磁盘错误)。docker ps/docker logs 容器:看容器与日志。
10. 再往下一层:每个抽象层"怎么实现"
想从"知道每层负责什么"跨到"知道每层用什么齿轮实现",有一把总钥匙:
几乎每一层的实现 = ① 一个硬件机制(让下层能强行夺权或强制隔离)+ ② 内核/宿主里的一张状态数据结构 + ③ 在它上面跑的一套算法。 以后看任何一层,就问三句:靠哪个硬件陷阱?状态记在哪张表?上面跑什么算法?
逐层填这三件套:
| 层 | ① 硬件机制 | ② 状态数据结构 | ③ 算法 / 机制 |
|---|---|---|---|
| 分 CPU(调度) | 定时器中断周期性夺回控制权(用户程序拦不住) | 运行队列(Linux CFS:按"已用虚拟运行时间"排序的红黑树) | 挑用得最少的,做上下文切换(存/挑/恢复寄存器) |
| 分内存(虚拟内存) | MMU + 页表基址寄存器(x86 CR3) | 每进程一棵页表(虚拟→物理地址映射) | 每次访存 MMU 查表翻译;换进程换 CR3;越界访问→缺页陷入内核 |
| 用户/内核态(syscall) | CPU 特权级 ring(ring3 用户 / ring0 内核)+ syscall 指令陷入 | 系统调用表 | 调用号进寄存器→查表→跑处理函数→返回 ring3。隔离是硬件强制 |
| 文件系统 | 硬盘 = 一堆编号的块 | inode(元信息+数据块指针)+ 目录(名→inode 号)+ 超级块/位图 | 按路径逐级查 inode → 读它指的数据块 |
| 网络 / TCP | 网卡中断 | 每 socket 一个 TCP 状态机 + 收/发缓冲区 | SYN 到→内核自动推进状态机→回 SYN-ACK→塞进 accept 队列(全程不用进程参与) |
| 虚拟化 | CPU VT-x / AMD-V(guest 模式)+ EPT 二级页表 | VMCS 等 guest 状态块 | 陷入并模拟:敏感操作→VM-exit→hypervisor 模拟→恢复 guest |
| 容器 | 无新硬件,纯内核功能 | namespaces(隔离"视野")+ cgroups(配额账本) | clone() 带 namespace 标志起进程;写 cgroup 文件封顶资源 |
| 进程管理器 | 无 | 它自己就是个用户态进程(PID 1) | fork()+exec() 起服务,waitpid() 收尸后重启 |
一句话:每层都是"硬件陷阱 + 一张状态表 + 一套算法"。这就是"怎么实现"的统一形状。
11. 想真正学到能自己写出来:学习路径
读(先建对模型)
- OSTEP《操作系统导论 / Operating Systems: Three Easy Pieces》(免费在线)——调度 / 虚拟内存 / 并发 / 文件系统,深度刚好,首推。
- CSAPP《深入理解计算机系统》——打通"硬件 ↔ OS"那条缝(ring、虚拟内存、进程、链接)。
- 网络:Kurose《自顶向下》建概念 + Beej's Guide 学 socket 编程。
做(落实到能自己写出来 —— 这才是"具体实现")
- MIT 6.1810(原 6.S081)+ xv6:公开课 + 实验,亲手在一个能跑的小 Unix 内核里实现 syscall / 页表 / 调度 / 文件系统。想落到细节,金标准。
- 虚拟化:搜
KVM API hello world/ LWN《Using the KVM API》——几百行写个迷你 hypervisor,亲眼看见 VM-exit。 - 容器:Liz Rice《Build a container in ~100 lines of Go》(GOTO 演讲) +
man namespaces(7)/cgroups(7),一个下午手搓一个。 - TCP:Saminiir《Let's code a TCP/IP stack》——在 TUN 网卡上从零实现握手。
看它活着跑(把抽象对到真实输出)
strace 程序(mac 上dtruss):实时看它在调哪些 syscall(会看到openat/read/write/socket/accept)。- 读
/proc/<pid>/:某进程的内存映射、打开的 fd 全在里面。 bpftrace/ eBPF:不改代码,探针式观察内核在干嘛。