Tag: linux

8 notes found.

三国里,托孤是刘备把阿斗交给诸葛亮;Linux 里,托孤是内核把孤儿进程交给 PID 1。 区别在于:诸葛亮会管,很多容器里的 PID 1 不会。

什么是「托孤」

Linux 里每个进程都有父进程。父进程先退出,子进程就成了孤儿进程;内核不会让孤儿无家可归,会把它过继给当前 PID namespace 里的 PID 1。

普通系统里 PID 1 是 init / systemd,它的职责之一就是不断调用 wait(),回收退出的子进程。这就是「托孤」:父进程走了,孩子交给 PID 1。

为什么容器里会出问题

容器里 PID 1 往往不是 init,而是应用本身(litestream、node、nginx、python…)。这些应用只 wait() 自己 fork 出来的子进程,托孤来的不认账,于是每个孤儿退出后都留在进程表里——ps 里显示为 <defunct>。

容器里这类孤儿的常见来源是 healthcheck:每 30s 一次,跑得越久堆得越多。docker exec(healthcheck 走同一条路)创建的进程,其父进程在容器 PID namespace 之外、对容器不可见,因此被托孤给 namespace 里的 PID 1。

僵尸不占 CPU、也不占内存(只剩进程表里一个槽位),但会一直占用 PID 表项;攒够了,容器连新进程都创建不了,排查别的问题时也是噪音。

解法:给容器一个真正的 PID 1

把 tini 放到 PID 1——它只做两件事:reap 托孤来的进程、把信号转发给真正的应用。

RUN apt-get update \
    && apt-get install -y --no-install-recommends tini \
    && rm -rf /var/lib/apt/lists/*

# 应用变成 tini 的子进程,托孤来的孤儿由 tini 回收
ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"]

Alpine 用 apk add --no-cache tini,路径是 /sbin/tini。

不改镜像时,docker run --init(或 compose init: true)等效,但镜像内建更省事——调用方不会忘。Kubernetes 没有等价的开关,做法同上:把 tini 作为镜像的 ENTRYPOINT,让容器里的 PID 1 真正承担「家长」的责任。应用若自己实现了 SIGCHLD 处理与 wait() 循环,也可以不用 tini——但大多数业务应用并不想管这些事。

数僵尸

容器里常常没装 ps/procps,直接读 /proc:

for p in /proc/[0-9]*; do
  [ "$(awk '{print $3}' "$p/stat" 2>/dev/null)" = Z ] && \
    awk '{print "pid="$1" ppid="$4" comm="$2}' "$p/stat"
done

ppid 就是那个没 reap 的 PID 1。

同类工具:tini 之外的选择

tini 只做「reap + 转发信号」,单进程容器够了。但容器里一旦要跑多个进程、要按顺序初始化、要进程挂了自动重启,就需要一套完整的 init 了。

  • tini / docker --init:只 reap + 转发信号,单进程容器(最常见)
  • dumb-init:同一类,C 写的更小,能当 entrypoint 处理 shell 形式的 CMD
  • catatonit:同一类,Podman 默认
  • s6-overlay:完整 init + 进程监督(oneshot 初始化、依赖顺序、自动重启、优雅停机)

以 s6-overlay v3 为例(ARG S6_OVERLAY_VERSION=3.2.3.2),装法就是解两个 tar 包,然后 ENTRYPOINT ["/init"]:

RUN apt-get update && apt-get install -y nginx xz-utils
ARG S6_OVERLAY_VERSION=3.2.3.2
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz
ENTRYPOINT ["/init"]

(x86_64 换成目标架构的包名。)启动后 s6-svscan 当 PID 1,分三阶段:阶段 1 跑 /etc/cont-init.d/*(一次性初始化)→ 阶段 2 起 s6-rc 服务 → 阶段 3 监督 /etc/services.d/*。所以老的 cont-init.d / services.d 写法在 v3 里照旧可用,老镜像迁移不用改。

v3 新写法是声明式服务:

/etc/s6-overlay/s6-rc.d/myapp/type        → longrun(常驻,受监督)或 oneshot(跑一次)
/etc/s6-overlay/s6-rc.d/myapp/run         → 启动脚本,结尾 exec 你的进程
/etc/s6-overlay/s6-rc.d/myapp/dependencies.d/base      → 空文件 = 声明依赖
/etc/s6-overlay/user-bundles.d/user/contents.d/myapp   → 空文件 = 加入默认启动集

它本来就是给容器当 PID 1 用的,所以「托孤」这件事它天然接着:孤儿由 s6 回收、信号由 s6 转发、服务挂了 s6-supervise 自动拉起。选型上:单进程用 tini(--init 一行搞定),多进程 / 要顺序初始化 / 要自动重启再上 s6-overlay。

总结

「托孤」听起来很人文,在 Linux 里却是一次很具体的责任转移:内核把孤儿进程交给 PID 1。

PID 1 不只是一个编号,它意味着收养孤儿、回收僵尸、转发信号。容器里让普通应用当 PID 1,却不给它配一个 tini 这样的「诸葛亮」,结果往往就是一群没人收尸的僵尸,慢慢把容器拖死。

所以,别让 PID 1 只挂名,不干活。

当 Kubernetes 将 ConfigMap 挂载为 volume 时,pod 内的文件其实是 symlink,直接 kubectl cp 会跳过所有文件导致拷不出内容。用 kubectl exec + tar 管道即可复制真实文件。

问题

# 这样拷贝会失败:目录内全是 symlink,无 .conf 文件落地
kubectl cp platform/${POD}:/app/config/ /home/laborant/config/

warning: skipping symlink: "/home/laborant/config/server.conf" -> "..data/server.conf"
# ...
# kubectl cp 退出码为 0 却什么都没拷出来

原因:kubectl cp 内部用 tar 流式传输,但没传 --dereference。挂载路径下的条目全是 symlink,tar 视其为符号链接而跳过。kubectl cp 即使失败也返回 0,必须 ls 验证结果。

解决方案

在 pod 内部 tar 打包(内核 VFS 透明解析 symlink),再管道给本地 tar 解包:

# 左侧在 pod 内打包并写 stdout,右侧本地解包并剥离前两级路径
kubectl exec -n platform "$POD" -- tar cf - /app/config \
  | tar xf - --strip-components=2 -C /home/laborant/config
  • --strip-components=2:去掉 app/config 两级前缀,只留裸文件名
  • tar xf -:从 stdin 读取归档,-C 指定解包目录
ls /home/laborant/config/
# database.conf  feature-flags.conf  logging.conf  server.conf

通用注意点与变体

  • exec 不要加 -t(TTY):TTY 会对输出做换行转换,污染二进制归档流;无 TTY 的 exec 才是干净的 stdin/stdout 回传
  • -C 目标目录必须先存在:tar 不会自动创建目录,会报 Cannot open 之类错误,先 mkdir -p /home/laborant/config
  • 等价写法:左侧 tar cf - -C /app config 时归档成员只有 config/... 一级前缀,右侧只需 --strip-components=1;与”绝对路径 + strip 2”语义等价,任选一种保持一致
  • 压缩变体:网络较慢时左侧 tar czf -(gzip 压缩),右侧 tar xzf - 解压
  • 依赖:Pod 内需装有 tar(busybox/distroless 镜像可能没有,报 exec: tar: not found);本地的 --strip-components 由 GNU tar / bsdtar 提供,macOS / Linux 均有
  • 权限:非 root 解包时文件 owner 会变成当前用户(tar 仅在 root 下默认保留 owner),通常无碍

获取运行中的 pod 名

POD=$(kubectl get pod -n platform \
  -l app=config-service \
  -o jsonpath='{.items[0].metadata.name}')
原子更新机制:两层级联导致 kubectl cp 失效

ConfigMap 以 volume 挂载时,目录结构是两层间接:

/app/config/
├── ..2026_08_19_10_19_29.2041564214/   ← 真实目录,存真实字节
├── ..data -> ..2026_08_19_10_19_29.2041564214   ← 指向真实目录
└── server.conf -> ..data/server.conf    ← 每个文件都是 symlink
  • 权限串首字符 l 表示 symlink(lrwxrwxrwx)
  • kubelet 更新 ConfigMap 时写入新时间戳目录,再通过一次原子 rename() 重指 ..data,保证运行中的 pod 立即看到新内容且不出现新旧混合
  • tar 默认不跟随 symlink,kubectl cp 因此全部跳过

/usr/bin/env 是 Unix/Linux 系统中用于查找并运行程序的工具,主要用于 Shebang 中以提高脚本可移植性。

核心作用与用法

核心作用:提高脚本的可移植性

1. 硬编码路径(不推荐)

#!/usr/bin/python3 - 直接指定路径,跨系统兼容性差。

2. 使用 env(推荐)

#!/usr/bin/env python3 - 在 $PATH 中搜索,兼容性极佳。

工作流程

  1. 内核读取 Shebang,运行 env
  2. env 查看 $PATH 环境变量
  3. 按顺序搜索 python3
  4. 找到后启动该程序

对比表

特性直接路径使用 env
可移植性差极佳
虚拟环境支持不支持完美支持
灵活性低高

进阶技巧:临时设置环境变量

# 临时设置语言
env LANG=en_US.UTF-8 check_system_status

# 查看所有环境变量
env

注意事项

env 不支持传递多个参数,如 #!/usr/bin/env python3 -u 在旧系统上可能失效。

配置方法

在所有的 git 全局配置中,配置:

# Configure Git to ensure line endings in files you checkout are correct for macOS
git config --global core.autocrlf input

或者在对应的 ~/.gitconfig 中配置:

[core]
	autocrlf = input

但是 github 推荐在 windows 中将这个选项设置为 true,我查了下觉得在三端均采用 input 是非常合理的。

配置说明

配置值含义
true双向转换:
 • 输入(commit)时:CRLF → LF
 • 输出(checkout)时:LF → CRLF
input仅输入时转换:
 • 输入(commit)时:CRLF → LF
 • 输出(checkout)时:不做转换(保留 LF)
false完全不转换

参考链接:https://docs.github.com/en/get-started/git-basics/configuring-git-to-handle-line-endings?platform=linux

vps 内存爆炸了,上来查原因,使用 bottom 查看之后发现是 uwsgi 进程占用高内存,但我印象中我是我没有部署类似的服务的,因为我基本都是使用 docker 来部署的。

通过 cgroup 信息定位问题

Linux 通过 cgroup 来管理容器资源,每个进程都会记录自己属于哪个 cgroup。我们可以利用这一点来找到它所属的容器。

  1. 找到 uwsgi 进程的 PID (Process ID)

    pidof uwsgi

    这个命令会返回一个或多个数字,就是 uwsgi 的进程 ID。我们假设返回的是 12345。

  2. 查看该 PID 的 cgroup 信息

    cat /proc/12345/cgroup

    在输出中,你会看到类似下面这样的行:

    11:perf_event:/docker/a1b2c3d4e5f6...
    10:cpuset:/docker/a1b2c3d4e5f6...
    ...
    1:name=systemd:/docker/a1b2c3d4e5f6...

    注意 /docker/ 后面那串以 a1b2c3d4e5f6 开头的字符串,这就是容器的完整 ID。

  3. 根据容器 ID 找到容器名

    docker inspect a1b2c3d4e5f6 | grep "Name"

    或者更简单地,列出所有容器,手动找到这个 ID:

    docker ps

    在 CONTAINER ID 列中找到对应的 ID,它旁边的 NAMES 列就是你容器的名字,例如 my-web-app。

小技巧: 你可以组合成一条命令,一步到位: cat /proc/$(pidof uwsgi)/cgroup | grep -o '[0-9a-f]\{64\}' | xargs -I {} docker inspect {} | grep "Name"