写这篇学习记录时,我刚在云服务器上折腾完一批用户权限,顺便把之前一直没彻底搞懂的几个基础概念重新过了一遍。UID和GID,中文就是用户ID和组ID。如果你和我一样,刚开始学Linux时对着ls -l输出里的“root root”发过呆,或者好奇为什么不直接用用户名而是用一串数字来标识身份,那这篇文章大概能帮你把这些疑问一次性理清楚。它适合所有刚接触Linux的初学者,也适合那些已经在用Linux但总觉得基础不扎实、准备面试或者想深入理解权限体系的朋友。
我会从概念、原理、实操到常见问题,按我自己的学习路径来记录,全程配上可以直接复制的命令和踩坑心得。这不是教科书式的罗列,而是我经过实际测试、带着思考写下来的经验总结。
1. UID和GID到底是什么:先搞清概念再动手
1.1 从文件的属主和属组说起
第一次接触ls -l时,我看到每行前面都有类似-rw-r--r--的权限串,后面还跟着两个名字。比如:
drwxr-xr-x 2 root root 4096 Jan 15 10:30 testdir我当时有两个疑问:这里显示的“root root”到底是什么?第二个“root”是不是多余?后来才明白,第一个root是文件属主,也就是文件的主人;第二个root是文件属组,决定了这个文件所属的组。而内核在真正判断权限时,并不直接看“root”这个字符串,而是看root这个用户名背后对应的数字标识——这就是UID(用户ID)和GID(组ID)。
类比一下就很好懂:UID和GID就像身份证号,用户名和组名就像身份证上的名字。平时我们打交道时用名字更方便,但真正办理业务、核对身份时,系统认的还是那个唯一的编号。Linux里也是一样,内核只认数字,人名是给人类看的。
1.2 /etc/passwd和/etc/group里的奥秘
要理解UID和GID,必须打开两个文件:/etc/passwd和/etc/group。
/etc/passwd里每一行代表一个用户,典型的一行长这样:
zhangsan:x:1001:1001:张三:/home/zhangsan:/bin/bash用冒号分成7个字段,我刚开始时总记不住顺序,后来总结成一个口诀:名字、密码占位、UID、GID、注释、家目录、登录Shell。
- 第1个字段:用户名,比如zhangsan。
- 第2个字段:以前存密码哈希,现在普遍放一个
x占位,真正密码挪到了/etc/shadow,普通用户不可读。 - 第3个字段:UID,用户的数字ID。
- 第4个字段:GID,用户主组的数字ID。
- 第5个字段:注释信息,一般填姓名或说明。
- 第6个字段:家目录路径。
- 第7个字段:登录Shell,也就是用户登录后使用的解释器。
再看/etc/group,每一行代表一个组:
dev:x:1002:zhangsan,lisi,wangwu同样用冒号分隔:组名、组密码占位符、GID、组成员列表。这里有个容易忽略的点:一个用户创建时会被分配一个主组(primary group),同时还可以加入很多附加组(supplementary group)。文件权限判断时,主组和附加组都会被检查,但登录时的有效组通常是主组。这个细节在共享目录权限配置时特别关键,后面实操部分会再提。
2. 内核只认数字,不认名字:UID/GID为什么这么设计
2.1 用户名的存在只是给人看的
我一度觉得Linux这个设计很“反人类”:明明有更直观的用户名,为什么权限判断非要绕一层数字?后来接触了网络和嵌入式相关的场景才明白,数字ID在系统内部处理时效率高得多,而且不会因为改名或不同机器上用户名不同而产生歧义。
举例来说,你在两台服务器上各有一个用户,都叫test,但一台上UID是1001,另一台是2001。如果两台服务器通过NFS共享同一个目录,那么第一台服务器上的test用户写的文件,在第二台服务器上可能就变成另外一个用户的了。原因很简单:NFS协议在传递文件属主信息时,传的是UID/GID数字,不是用户名。也就是说,跨系统协同工作时,真正一致的不是名字而是数字ID。这也是为什么有经验的管理员在搭建多机环境时,会提前规划统一的UID/GID范围。
2.2 从文件权限到进程身份:UID如何生效
权限判断的过程可以简单概括为:当一个进程试图访问文件时,内核用进程的身份ID和文件属主、属组的ID进行比对,再根据比对结果决定应用哪一段rwx权限。
在Linux里,进程的身份比我们想得更复杂一点。一个运行中的进程至少有三种UID:
| 类型 | 含义 | 典型场景 |
|---|---|---|
| Real UID | 进程实际由哪个用户启动 | 记录“发起者”是谁 |
| Effective UID | 进程当前以谁的权限运行 | 决定真正拥有的权限 |
| Saved UID | 被保留的旧EUID,以便程序在必要时切换回 | 用于setuid程序的临时降权/恢复 |
大多数普通进程的这三种UID是相同的,但执行了带有setuid位的程序后会发生变化。最经典的例子是/usr/bin/passwd,你看它的权限时注意看属主权限部分的s:
ls -l /usr/bin/passwd -rwsr-xr-x 1 root root 59976 Feb 10 2023 /usr/bin/passwd普通用户执行passwd修改自己的密码时,按道理只能访问/etc/shadow,而这个文件是只有root能读写的。可实际上普通用户能正常改密码,关键就在于passwd程序带了setuid位,它会让进程的Effective UID临时变成root,从而获得读取/etc/shadow的权限。这就是“程序权限替身”的机制,理解它对排查权限问题非常有帮助。
2.3 普通用户、系统用户和root的边界
Linux里UID并不总是分配给真实用户,有很大一片保留给系统服务。按照常见发行版的约定:
UID 0:特权用户root,拥有整个系统最高权限。UID 1~999:系统保留账号,供守护进程或服务使用。这类账号通常没有登录Shell,也没有家目录,比如bin、daemon、mysql等。UID 1000~60000:普通用户,用于日常登录和业务操作。
不同发行版的具体范围略有差异,但大方向一致。比如Debian系曾经的普通用户起点是1000,而一些老系统可能从500开始。了解这个范围有助于你在创建用户时合理地手动指定UID,避免和系统用户撞车。
3. 实操环节:查看、创建、修改一套带走
3.1 查看用户的UID/GID
排查权限问题时,第一时间肯定是确认当前用户和文件属主。我最常用的查看命令是id:
id uid=1000(test) gid=1000(test) groups=1000(test),4(adm),27(sudo)输出第一行显示当前用户的UID和主组GID,groups部分显示所有附加组。如果只想看某个用户的,直接后面跟用户名:
id root uid=0(root) gid=0(root) groups=0(root)也可以用getent命令来查询用户数据库,它在某些场景下比直接cat /etc/passwd更可靠,因为它会用到NSS配置的完整用户来源(本地文件、LDAP等)。比如:
getent passwd test test:x:1000:1000::/home/test:/bin/bash想快速列出所有用户的UID和GID,可以配合awk处理/etc/passwd:
awk -F: '{print $1, $3, $4}' /etc/passwd想看文件真实的数字属主,用ls -n,它会用数字UID/GID取代用户名显示;或者用stat:
ls -n myfile.txt stat -c "%u %g" myfile.txt3.2 创建用户时如何规划UID和GID
刚开始我创建用户特别随意,后来发现不规范的用户名和ID会让权限管理变得很痛苦。推荐的做法是创建之前先规划好ID段,比如业务用户统一从10000开始,服务账号继续使用系统保留段下的自定义范围。
创建用户的命令是useradd,常用参数如下:
useradd -u 10001 -g dev -G docker,wheel -m -d /home/zhangsan -s /bin/bash zhangsan-u 10001:手动指定UID。-g dev:指定主组为dev组(该组必须已存在)。-G docker,wheel:指定附加组,多个组用逗号分隔。-m:同时创建家目录。-d /home/zhangsan:指定家目录路径。-s /bin/bash:指定登录Shell。
这里有个特别容易踩的坑:不同发行版的useradd默认行为不一样。在CentOS/RHEL上,useradd默认会创建家目录并生成对应的邮件目录;但在Ubuntu/Debian上,直接用useradd创建用户往往不会自动创建家目录,也不会要求设置密码。很多新手在Ubuntu上创建用户后,切过去发现连home目录都没有,就是这个原因。如果是Ubuntu环境,我更推荐用adduser这个友好交互命令,它会引导你设置密码并创建家目录;如果你坚持要用useradd,记得带上-m参数。
3.3 修改已有用户的UID/GID
如果一个用户创建之后ID不合适,可以用usermod修改。修改UID:
usermod -u 10002 zhangsan修改主组:
usermod -g dev zhangsan添加附加组:
usermod -aG docker zhangsan注意这个-aG里的-a一定要带上,它表示追加(append),如果不加,用户会被移出原来的所有附加组,只保留新指定的组。这个误操作我帮同事排查过一次,他本来想把用户加到docker组,结果用户直接被踢出了sudo组,导致机器上没法执行sudo,非常耽误事。
修改UID有个风险要特别提醒:usermod -u只修改了/etc/passwd里的UID记录,不会自动修改这个用户原来拥有的文件属主。也就是说,改ID之后,用户家目录或业务目录里的文件还会显示成旧的数字ID。正确的操作顺序是,先记录旧UID下有哪些文件,修改后再用chown把它们统一改过来。
批量改属主的命令可以这样写,先找出所有属于旧UID的文件再执行chown:
find /home/zhangsan -user 10001 -exec chown 10002:dev {} \;执行前先不加-exec只跑一遍find,确认列出的文件都在预期范围内,再执行真正的修改。尤其是服务器上有大量业务数据的场景,这个习惯能避免改错目录权限。
3.4 修改文件属主和属组
日常用得最多的应该就是chown和chgrp这两个命令了。基础用法如下:
chown zhangsan /data/test.txt chown zhangsan:dev /data/test.txt chown :dev /data/test.txt第一种只改属主,第二种同时改属主和属组,第三种只改属组。实际修改目录时,遇到目录里还有大量子文件,加上-R递归执行:
chown -R zhangsan:dev /data/projectchgrp则专门用来改文件的属组,等价于chown :组名:
chgrp -R dev /data/project有一点需要说明:普通用户只能修改自己拥有文件的属主和属组吗?不对,实际上普通用户几乎不能更改文件的属主,这个操作会直接拒绝,因为把文件“送”给别人是一个高风险操作。只有root或sudo权限才能执行chown。所以当你在权限受限的目录里执行chown报错时,先检查一下当前用户。
4. 特殊UID与权限位:很多人忽略的重要细节
4.1 uid 0、nobody和系统保留账号
uid 0是root,可以理解为系统的“最高权限身份证”。无论用户名改成了什么,只要UID是0,它就拥有root级权限。所以安全加固时,要特别检查是否有多余的UID 0账号:
awk -F: '$3 == 0 {print $1, $3}' /etc/passwd正常情况下这个命令只应该输出一行root 0。如果看到其他账号也对应UID 0,说明系统很可能被留下了后门账号,需要立即处理。
另一个常见的特殊ID是nobody,通常在/etc/passwd里对应UID 65534或65535,GID同理。它代表“没有任何权限的普通用户”,通常用于Nginx、某些容器进程等以低权限运行的服务。理解nobody的定位,能帮你避免把服务账号直接配成root运行,降低被攻击后的影响面。
4.2 setuid、setgid和sticky bit
讲到UID和GID,就绕不开权限位上的三个特殊标志:setuid(suid)、setgid(sgid)和sticky bit。它们和普通rwx权限一起存储在权限位上,平时显示在可执行位的x位置:
- 属主权限里出现
s:setuid,执行时进程EUID变成文件属主的UID。 - 属组权限里出现
s:setgid,执行时进程EGID变成文件属组的GID,同时如果对目录设置setgid,目录内新建的文件会自动继承该目录的属组。 - 其他用户权限里出现
t:sticky bit,常见于/tmp这种共享目录,只有文件属主(或root)可以删除自己的文件,避免用户误删他人临时文件。
用chmod可以给文件或目录设置这三个位:
chmod u+s /path/to/file # 设置setuid chmod g+s /path/to/dir # 设置setgid chmod +t /tmp/shared_dir # 设置sticky bit检查系统里有哪些setuid程序,可以用:
find / -perm -4000 -type f 2>/dev/null这个命令应该养成定期检查的习惯,因为setuid位一旦配合程序漏洞,很容易被利用来提权。如果发现某些非常规程序带suid位,就要格外警惕。
4.3 跨服务器时UID不一致的坑
前面说过NFS的场景,再举一个具体例子:公司有两台服务器,同一批开发者的账号在A机器上UID是1001,在B机器上忘了统一,变成了1002。当开发者把A机器上产生的文件传到B机器后,B机器上所有者和属组显示不再是他的名字,而是一串数字。更麻烦的是,如果两边共享同一个存储,权限判断会完全错乱,本来有权限的目录突然没权限了。
解决办法是提前规划:要么在每台机器上建立相同UID/GID的账号,要么使用集中认证方式(比如LDAP),要么在文件传输后统一重启一遍属主。没有绝对完美的方案,但最基础的一点是:在项目初始规划时,就把UID/GID纳入一致性方案里。
5. 新手常见问题与排查技巧实录
5.1 用户删了文件却显示一串数字
现象:用ls -l查看文件时,属主和属组变成了一串数字,比如1001。原因很简单:文件属主的UID在/etc/passwd里已经查不到对应的用户名了,通常是因为用户被userdel删除,但文件还在。
解决办法是重新创建同一个UID的用户,或者用chown把文件归属调整到现有用户:
chown test:test /path/to/file避免这个问题的最好习惯是删除用户之前先找出它名下的文件:
find / -user zhangsan -ls确认这些文件是否有保留价值,再决定保留还是删除。我在一台测试机上删过一个业务账号,结果漏掉了/var/log下的应用日志,后续排查问题时看日志全是数字,非常痛苦。
5.2 新建用户没有家目录
现象:useradd test之后,su - test进入用户环境,发现当前目录是/,而且/home/test不存在。
原因通常是发行版默认行为不同,或者创建命令里漏了-m参数,也可能家目录被其他文件占用了。解决方法很简单:
mkdir -p /home/test chown test:test /home/test usermod -d /home/test test如果是Debian/Ubuntu系,强烈建议直接用adduser交互式创建用户,它会自动创建家目录、设置密码,一次性处理好。
5.3 修改UID后文件权限“失效”
现象:给用户执行了usermod -u 10002 zhangsan,登录后发现自己的家目录进不去了,或者文件权限全乱了。
原因就是第3.3节提到的问题:文件属主还是旧UID,和用户新UID对不上,权限判断自然失败。解决套路是:
find /home/zhangsan -user 10001 -exec chown 10002:10002 {} \;如果你修改的是业务目录,记得也要扫描业务路径,别只改家目录。另外提醒一句,修改UID前最好通知相关同事,避免在线服务正在写入文件时你改了属主,导致服务瞬间没有写权限。
5.4 常用排查命令速查
我把自己平时用到的高频命令整理成了一个小表,方便随时查阅:
| 需求 | 命令 |
|---|---|
| 查看当前用户ID信息 | id |
| 查看指定用户ID信息 | id username |
| 查看passwd文件全部用户 | cat /etc/passwd或getent passwd |
| 查看group文件全部组 | cat /etc/group或getent group |
| 查找UID为0的特殊账号 | awk -F: '$3==0{print $1,$3}' /etc/passwd |
| 查看文件数字属主 | ls -n或stat -c "%u %g" file |
| 修改文件属主/属组 | chown 用户:组 文件 |
| 递归修改目录属主/属组 | chown -R 用户:组 目录 |
| 检查系统setuid程序 | find / -perm -4000 -type f 2>/dev/null |
这套命令配合/var/log/secure或/var/log/auth.log里的认证日志,基本能覆盖绝大多数权限相关问题的排查。
踩过几次坑之后,我的体会是:UID和GID这套体系虽然概念简单,但真正理解它需要把“用户—组—文件—进程”这条链路串起来。很多权限问题看着复杂,拆到最后无非就是以下三件套:当前进程以什么身份在跑、目标文件的属主和属组是什么、文件的所属组和用户是否匹配访问规则。把这三个问题回答清楚,权限相关的故障就解决了一大半。下一篇我打算接着写权限位的三位八进制数值和umask的计算逻辑,如果你也在这个问题上卡过,欢迎留言交流。