← 全部笔记

前置知识 · Linux 基础

Linux 进程与文件权限

进程携带数字身份;内核根据身份和文件权限决定能否访问。

约 15 分钟 · 截至 2026-11-03,Docker Engine 29.x · Docker 实验未实测

进程请求内核读取文件示意:进程的 UID 为 1000,文件 owner UID 为 1000,owner 权限 rw- 允许读取。PID 标识进程,UID 参与权限判定。 进程:正在运行的程序PID 42 · UID 1000(示意) 请求读取 内核:比较身份,再检查 r 位 允许读取 文件:owner 1000 · rw-
图 1 PID(进程号)回答“哪个进程”,UID(用户 ID)回答“以谁的身份访问”。

已会命令行即可阅读。容器的隔离背景可回看容器与虚拟机;本页用到的 Docker 选项会当场解释。

01 · 找到运行者

进程组成树

程序是文件,进程是它的一次运行。同一程序运行两次,可以产生两个进程。进程号(Process ID,PID)在同一个 PID 命名空间中标识一个活着的进程;命名空间就是一套隔离的编号视图。

父进程号(Parent Process ID,PPID)指出当前父进程。Shell 是解释命令的程序;它启动外部命令时通常创建子进程。沿 PPID 向上找,就得到进程树。[1]

由 PPID 连起的进程树编号为示意。Shell 的 PID 为 42;sleep 的 PID 为 81、PPID 为 42。后台运行并未切断父子关系。 ShellPID 42(示意) 创建子进程 sleepPID 81 · PPID 42 后台运行仍有 PID 和父进程
图 2 子进程的 PPID 指向父进程的 PID;“后台”不改变这条关系。

图中的 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 数字会不同。退出提示与列宽也可能不同。

ps -o pid,ppid,comm -p "$$,$child"
PID PPID COMMAND 3392 3391 bash 3393 3392 sleep

第二行的 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]

同一个作业从运行到退出示意:启动 sleep,Ctrl+Z 暂停,bg 恢复到后台,fg 调回前台,SIGTERM 请求退出,wait 回收退出状态。六步逐步显示。 前台运行 暂停SIGTSTP → 已暂停 bg后台运行 fg回到前台 TERMsleep 已退出 wait父进程取得退出状态
  1. 在交互式 Bash 中启动 sleep 60,它成为前台作业。先猜:按 Ctrl+Z 后,进程是退出还是暂停?
  2. Ctrl+Z 发出 SIGTSTP,sleep 暂停,Shell 再次接收命令。进程还活着。
  3. bg %1 向作业 1 发 SIGCONT,让它在后台继续运行。先猜:后台的 sleep 会自动多得到文件权限吗?
  4. fg %1 把同一个作业调回前台;若它暂停了,还会恢复运行。身份与文件权限不变。
  5. 从另一终端向该 sleep 的 PID 发送 kill -TERM PID。普通 sleep 按默认动作退出;其他程序可能先清理或忽略 TERM。
  6. 父进程用 wait 取得退出状态并回收记录;Shell 会管理自己的子作业。暂停过的进程无需重新启动,已经退出的进程则不能用 bg 复活。
图 3 六步示意中,前后台和暂停都保留进程;退出才结束它。手机可在图内横向查看。
信号常见来源对普通进程的含义
SIGINTCtrl+C中断请求;默认退出,程序可以处理。
SIGTERMkill -TERM PID终止请求;可处理,适合先清理再退出。
SIGKILLkill -KILL PID强制终止;不能捕获或忽略,程序无法执行清理。
SIGTSTP / SIGCONTCtrl+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 的进程能写它吗?选中该身份来检查。

ownerrw-4 + 2 = 6
← 选这一组
groupr--4 = 4
other---0 = 0

UID 1000 匹配 owner:可读、可写,不可直接执行。先匹配 owner 就只检查 owner 位。

图 4 UID 匹配 owner 就选 owner;否则检查组(含附加组),最后才选 other。三组权限不会相加。

数字权限每组用 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 和路径可能不同。

id -u
1000
id -G
1000
stat -c '%A %a %u:%g %n' note.txt
-rw-r----- 640 1000:1000 note.txt
chmod u+x note.txt
stat -c '%A %a %u:%g %n' note.txt
-rwxr----- 740 1000:1000 note.txt

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]

没有用户重映射时的数字身份示意:原生 Linux、未启用用户命名空间重映射。容器进程 UID 1000 对应宿主机 UID 1000,读取绑定挂载文件时匹配文件 owner UID 1000。 容器内(示意) 进程 UID 1000 · 名称 app 以 UID 1000 访问 Docker 宿主机 挂载文件 owner UID 1000rw- → owner 可读写
图 5 仅在原生 Linux 且未启用用户重映射时,容器与宿主机直接按相同数字 UID 对应。

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 DesktopLinux 容器由虚拟机承载;与 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 · 检索与迁移

带走四件事

同事说:“服务放到后台,就能读取那个权限不足的配置文件。”请用两句话解释问题。

前后台改变终端与作业的关系,不改变进程的数字身份。读取配置要检查 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 实验未实测。

  1. Linux credentials(7):PID / PPID、进程组、终端前后台和用户/组身份。
  2. Linux pid_namespaces(7):容器 PID 视图、PID 1 与信号特殊规则。
  3. Linux signal(7):信号默认动作、捕获与忽略、SIGKILL / SIGCONT。
  4. Docker Docs · Running containers:前后台、--user、用户名称与数字 UID。
  5. Docker Docs · docker container stop:停止信号与超时强制终止。
  6. Linux path_resolution(7):路径查找、目录 x、身份选择与权限检查范围。
  7. GNU chmod(1):符号模式、八进制权限与粘滞位。
  8. GNU chown(1):修改文件的所有者与所属组。
  9. Docker Docs · Bind mounts:共享宿主机路径、readonly、远程守护进程和 Desktop 边界。
  10. Docker Docs · Isolate containers with a user namespace:UID/GID 重映射及挂载权限。
  11. Docker Docs · Rootless mode:非 root 守护进程和容器、用户命名空间。
  12. GNU bash(1) · JOB CONTROL / SHELL BUILTIN COMMANDS:作业号、jobs / bg / fg、终端关闭与挂起信号。