01 · 找到运行者
进程组成树
程序是文件,进程是它的一次运行。同一程序运行两次,可以产生两个进程。进程号(Process ID,PID)在同一个 PID 命名空间中标识一个活着的进程;命名空间就是一套隔离的编号视图。
父进程号(Parent Process ID,PPID)指出当前父进程。Shell 是解释命令的程序;它启动外部命令时通常创建子进程。沿 PPID 向上找,就得到进程树。[1]
图中的 sleep 放到后台后,PPID 会自动变成 1 吗?为什么?
不会。前后台控制的是终端与作业的关系。Shell 仍在运行时,sleep 的父进程仍是这个 Shell。父进程退出后的收养规则属于另一件事。
用 ps 看父子关系
在 Linux Shell 中运行下面四行,只会启动并结束你刚创建的 sleep。& 让命令进入后台;$! 是刚启动的后台进程 PID;$$ 是当前 Shell 的 PID。ps 列出进程快照。
sleep 60 &
child=$!
ps -o pid,ppid,comm -p "$$,$child"
kill -TERM "$child"
以下是本环境在 Bash 中的真实 ps 摘录;你的 PID 数字会不同。退出提示与列宽也可能不同。
第二行的 PPID 3392 正好等于第一行的 PID。进程结束后,PID 可能被复用;发信号前要重新确认目标。
深入一点:容器里的 PID 1
PID 隔离使同一进程在容器内外可能有不同 PID。新 PID 命名空间里的第一个进程是 PID 1;它退出时,内核会结束这个命名空间里的其他进程。它还承担收养孤儿进程的职责。
PID 1 的信号规则也特殊:不能套用普通进程收到默认终止信号就退出的直觉。应用应正确处理信号,或由初始化进程管理子进程。这里先记住“主进程要持续运行”,后续生命周期模块再展开。[2]
02 · 控制运行
信号改变状态
后台运行、暂停和退出是三种不同状态。交互式 Shell 把一条命令或一条管道组织成一个作业,作业可以含多个进程。Shell 通常把同一作业的进程放进同一个进程组,便于一起控制。
前台作业能读取终端;后台作业让你继续输入命令,但仍可能向终端输出。
信号(signal)是内核递送给进程的通知。kill 的作用是发信号;它既能请求退出,也能暂停或恢复。通常,Ctrl+C 向终端前台进程组发送 SIGINT,Ctrl+Z 发送 SIGTSTP。[1] [3]
- 在交互式 Bash 中启动
sleep 60,它成为前台作业。先猜:按 Ctrl+Z 后,进程是退出还是暂停? - Ctrl+Z 发出 SIGTSTP,sleep 暂停,Shell 再次接收命令。进程还活着。
bg %1向作业 1 发 SIGCONT,让它在后台继续运行。先猜:后台的 sleep 会自动多得到文件权限吗?fg %1把同一个作业调回前台;若它暂停了,还会恢复运行。身份与文件权限不变。- 从另一终端向该 sleep 的 PID 发送
kill -TERM PID。普通 sleep 按默认动作退出;其他程序可能先清理或忽略 TERM。 - 父进程用
wait取得退出状态并回收记录;Shell 会管理自己的子作业。暂停过的进程无需重新启动,已经退出的进程则不能用 bg 复活。
| 信号 | 常见来源 | 对普通进程的含义 |
|---|---|---|
SIGINT | Ctrl+C | 中断请求;默认退出,程序可以处理。 |
SIGTERM | kill -TERM PID | 终止请求;可处理,适合先清理再退出。 |
SIGKILL | kill -KILL PID | 强制终止;不能捕获或忽略,程序无法执行清理。 |
SIGTSTP / SIGCONT | Ctrl+Z / bg | 默认暂停 / 继续;暂停期间仍占用内存等资源。 |
jobs -l 查看当前 Shell 的作业及 PID;%1 是这个 Shell 的作业号,不等于 PID 1。图中假设它是唯一作业;实际先用 jobs 确认。& 也不保证关闭终端后继续运行。[12]
在 Docker 中,docker run -d 让客户端脱离容器运行;容器内主进程仍须保持运行。docker stop 向主进程发停止信号,未另行配置时为 SIGTERM;超时仍未退出才发 SIGKILL。TERM 不保证立即退出。[4] [5]
你按 Ctrl+Z 暂停了 sleep,想让它继续运行并腾出终端。该怎么做?
SIGKILL 会结束进程;bg 只能恢复尚未退出的作业。
chmod 改文件权限,与已经运行的进程是否暂停无关。
Ctrl+Z 默认暂停前台作业。bg 发送 SIGCONT 并让作业在后台运行;作业号是当前 Shell 的编号,要先确认。
03 · 判定访问
按身份选一组
普通权限判定先选一组,再检查操作需要的位。用户 ID(User ID,UID)与组 ID(Group ID,GID)都是数字。进程有用户身份、主组及附加组;文件有所有者 UID 和所属组 GID。用户名、组名是这些数字在当前系统中的显示名称。
rwx 分别是读(read)、写(write)、执行(execute);- 表示没有该权限。ls -l 的权限字段先用 - 表示普通文件、d 表示目录,后面九位按所有者(owner)、所属组(group)、其他人(other)分三组。[6] [7]
固定的普通文件:-rw-r-----,UID 1000,GID 2000。
先猜:一个 UID 为 1001、附加组含 2000 的进程能写它吗?选中该身份来检查。
UID 1000 匹配 owner:可读、可写,不可直接执行。先匹配 owner 就只检查 owner 位。
数字权限每组用 r=4、w=2、x=1 相加。因此 640 是 rw- r-- ---,755 是 rwx r-x r-x。UID 1000 是身份;640 是权限,不能混为一谈。
要设为“owner 可读写;group 只读;other 无权限”,补全数字:
chmod note.txt
owner 是 4+2=6,group 是 4,other 是 0,所以是 640。chmod 改权限位;chown 才改所有者或所属组。
在临时目录里读数字
在 Linux 上以普通用户运行。mktemp -d 新建临时目录,stat 按指定格式显示权限与身份。只修改本实验新建的文件,最后逐项清理。
lab=$(mktemp -d)
printf 'hello\n' > "$lab/note.txt"
chmod 640 "$lab/note.txt"
id -u
id -G
stat -c '%A %a %u:%g %n' "$lab/note.txt"
chmod u+x "$lab/note.txt"
stat -c '%A %a %u:%g %n' "$lab/note.txt"
rm "$lab/note.txt"
rmdir "$lab"
本环境实测摘录:在临时目录内以相对路径 note.txt 调用 stat。你的 UID、GID 和路径可能不同。
u+x 只给 owner 加执行位,并保留其他位。加 x 不会把文本变成可运行程序;执行还需要有效的程序格式或脚本解释器。
| 位 | 普通文件 | 目录 |
|---|---|---|
r | 读文件内容 | 列出目录中的名字 |
w | 修改文件内容 | 与 x 配合,新建、删除或重命名目录项 |
x | 作为程序执行 | 搜索、穿过目录;访问已知名字 |
即使文件可读,路径上的父目录缺 x 也会阻止访问。删除文件主要看其父目录的 w+x,不能只看文件自身的 w。/tmp 等目录还有额外的“粘滞位”限制删除,详见深入阅读。[6] [7]
深入一点:这个权限模型的范围
本页交互假设普通非特权进程、普通读写文件、父目录可穿过,没有额外访问控制。实际访问使用进程的文件系统 UID/GID,通常等于有效 UID/GID。
root(UID 0)及具有相关特权能力(capabilities)的进程可以绕过部分权限检查。访问控制列表(ACL)能增加细分规则;SELinux / AppArmor 是额外安全策略,只读挂载也会限制写入。不能只看 rwx 就断言最终一定成功。
粘滞位(sticky bit)常见于共享临时目录:即使有目录写权限,普通用户也不能随意删除其他人的文件。chown 改所有者通常需要特权;普通用户改组也受所属组限制。[6] [7] [8]
04 · 接到 Docker
容器也有身份
文件访问仍由内核判断,容器里的用户名不会自动授予宿主机权限。绑定挂载(bind mount)把 Docker 宿主机上的一个文件或目录放进容器路径;两边因此访问同一份文件。[9]
docker run --user 1000:2000 … 指定容器进程的 UID 和主 GID。纯数字 UID 不要求镜像中有同名用户;使用用户名则要求它在容器中存在。主组之外的附加组也要确认,不能只比较用户名。[4]
按图 5 的环境,宿主机 alice 的 UID 是 1000;容器 app 的 UID 也是 1000。文件 600、owner UID 1000。名字不同会让读取失败吗?若容器 app 的 UID 改成 1001 呢?
名字不同不会导致失败。前者按数字 UID 匹配 owner,可以读取;改成 1001 后不再匹配 owner,600 的 group 与 other 都没有读权限,因此普通进程不能读取。这里仍假设路径可穿过,且无额外限制。
| 环境 | 如何理解 UID |
|---|---|
| 原生 Linux,常规 rootful Docker,无用户重映射 | 图 5 的直接数字对应模型适用。rootful 表示 Docker 守护进程以 root 运行。 |
启用 userns-remap | 用户命名空间重新映射 UID/GID。容器内 0 可对应宿主机的非特权 UID;不能直接照抄容器数字。 |
| Rootless Docker | 守护进程和容器都以非 root 方式运行,也使用用户命名空间;映射规则需单独检查。 |
| Docker Desktop | Linux 容器由虚拟机承载;与 macOS / Windows 共享文件还经过文件共享机制,不能机械套用原生 Linux 的推断。 |
这些边界决定了为什么“改成同名用户”可能无效。先检查运行身份、映射方式、文件数字所有者和路径权限;需要写入时,给目标用户或组必要的权限。chmod 777 会让所有类别都有 rwx,也无法修复只读挂载等问题。[10] [11] [9]
深入一点:与模块 8 的最小权限呼应
--user 只选择运行身份,不会自动更改已有文件的所有者,也不会开启 UID 重映射。容器内 UID 0 能做什么,还受到映射、capabilities 和安全策略约束。
后续“安全与生产实践”会继续学习非 root 运行与最小权限。本页先建立判断顺序:确认身份 → 确认映射 → 看文件与路径 → 查额外策略。不要为验证这个模型而修改 Docker 守护进程配置。
05 · 自己验证
实验只换身份
先测容器内身份,再测绑定文件的访问。下面两组命令均为未实测:编写环境没有 Docker。所列为基于文档的预期现象,待维护者补充真实输出;没有伪造终端记录。
需要可访问的 Linux 容器 Docker Engine。实验用 alpine:3.22 镜像;--rm 让容器退出后自动移除,sh -c 在容器里执行引号内的 Shell 脚本。首次运行可能先拉取镜像。实验结束后,镜像仍保留在本机。
A · 数字 UID 不依赖用户名
在宿主机终端执行;未实测。没有宿主机目录挂载。
docker run --rm --user 1000:2000 alpine:3.22 sh -c '
id -u
id -g
printf "shell PID=%s\n" "$$"
'
预期:前两项依次报告 UID 为 1000、主 GID 为 2000;最后一项显示容器 Shell 的 PID。它属于容器的编号视图,不能当作宿主机 PID 使用。这里不依赖镜像中的用户名记录。
B · 同一文件,比较两个 UID
在原生 Linux、普通非 root 登录用户、本机 rootful Docker、没有 userns-remap的前提下运行。它会在宿主机创建一个临时目录和文件并设置其权限,容器只读挂载这个目录;最后清理这两个对象。不操作系统目录,不执行宿主机网络配置命令,也不使用 --privileged。
Shell 中的 $(id -u) 取得当前用户 UID。--mount 指定绑定挂载,dst=/lab 是容器内路径,readonly 禁止容器修改挂载内容。临时目录设为 755,使两个测试身份都能穿过它;文件设为 600,只允许 owner 读写。
lab=$(mktemp -d)
chmod 755 "$lab"
printf 'hello\n' > "$lab/note.txt"
chmod 600 "$lab/note.txt"
# 第一轮:以当前登录用户的数字 UID/GID 运行
# test -r 检查当前身份能否读取文件
docker run --rm --user "$(id -u):$(id -g)" \
--mount "type=bind,src=$lab,dst=/lab,readonly" \
alpine:3.22 sh -c '
id -u
if test -r /lab/note.txt; then cat /lab/note.txt; else echo DENIED; fi
'
# 第二轮:换一个不同的非零 UID,主 GID 不变
other_uid=12345
if [ "$(id -u)" = "$other_uid" ]; then other_uid=12346; fi
docker run --rm --user "$other_uid:$(id -g)" \
--mount "type=bind,src=$lab,dst=/lab,readonly" \
alpine:3.22 sh -c '
id -u
if test -r /lab/note.txt; then cat /lab/note.txt; else echo DENIED; fi
'
rm "$lab/note.txt"
rmdir "$lab"
预期现象(未实测):第一轮先报告你的 UID,再读到文件内容 hello。第二轮报告另一个 UID,随后脚本进入不可读分支,打印自定义标记 DENIED。即使 GID 相同,600 的 group 位也没有 r。
第二轮如果读不到文件,把两个容器里的用户名都改成 alice,会解决问题吗?请给出检查顺序。
不会,名称相同不会改变数字 UID。先看 id 的 UID 和组,再查是否有用户重映射,接着看文件所有者数字、权限位和父目录 x。最后排查挂载是否只读及额外安全策略。
若两轮结果不符合预期,先核对实验前提。Rootless、重映射、Desktop 或远程守护进程都可能使此实验不适用;SELinux / AppArmor 也可能阻止读取。不要为了得到预期结果而放开所有权限或关闭安全策略。绑定源路径属于 Docker 守护进程所在机器。[9]
06 · 检索与迁移
带走四件事
- PID 找进程,PPID 连父子;Shell 作业号只在当前 Shell 有意义。
- 后台仍在运行,暂停仍活着;TERM 请求退出,KILL 不给清理机会。
- 先按 UID 和组选择 owner / group / other,再检查 rwx;目录 x 决定能否穿过路径。
- 容器访问文件也要检查身份;先确认 UID 映射,再谈“内外同一个用户”。
同事说:“服务放到后台,就能读取那个权限不足的配置文件。”请用两句话解释问题。
前后台改变终端与作业的关系,不改变进程的数字身份。读取配置要检查 UID、组、文件读权限及路径目录的 x,容器环境还要确认身份映射与额外限制。
- 区分运行控制与访问权限
- 按身份、文件与路径解释访问判定
原生 Linux、无用户重映射。普通容器进程 UID=1001、组含 2000;挂载文件 owner UID=1000、GID=2000、权限 640。路径可穿过且无额外限制。它能做什么?
题目给了数字 UID,它不匹配 owner。用户名相同不能替换 UID 判断。
三组权限不会合并。先判身份,选中一组后只检查该组。
1001 不等于 1000,跳过 owner;组含 2000,选 group。640 的中间一位 4 对应 r--,所以可读、不可写、不可直接执行。
来源
Docker 命令与行为核对日期:2026-11-03,面向 Docker Engine 29.x。Linux 样例已在本文标明实测范围;Docker 实验未实测。
- Linux credentials(7):PID / PPID、进程组、终端前后台和用户/组身份。
- Linux pid_namespaces(7):容器 PID 视图、PID 1 与信号特殊规则。
- Linux signal(7):信号默认动作、捕获与忽略、SIGKILL / SIGCONT。
- Docker Docs · Running containers:前后台、--user、用户名称与数字 UID。
- Docker Docs · docker container stop:停止信号与超时强制终止。
- Linux path_resolution(7):路径查找、目录 x、身份选择与权限检查范围。
- GNU chmod(1):符号模式、八进制权限与粘滞位。
- GNU chown(1):修改文件的所有者与所属组。
- Docker Docs · Bind mounts:共享宿主机路径、readonly、远程守护进程和 Desktop 边界。
- Docker Docs · Isolate containers with a user namespace:UID/GID 重映射及挂载权限。
- Docker Docs · Rootless mode:非 root 守护进程和容器、用户命名空间。
- GNU bash(1) · JOB CONTROL / SHELL BUILTIN COMMANDS:作业号、jobs / bg / fg、终端关闭与挂起信号。