
做Linux系统编程绕不开目录和用户这两块地基。很多人一开始只盯着文件读写、进程通信这些高级功能结果一遇到权限报错、用户删不掉、密码过期把运维卡在门外才发现最基础的东西反而最要命。这篇东西就聚焦目录操作、用户/组信息、密码管理这三件事从底层机制讲到命令行实操再落到C语言API和常见故障排查适合刚入门系统编程的同学也适合被权限问题折磨过的运维和嵌入式开发者参考。目录操作不只是cd和ls用户管理也不只是useradd加个账号。这两块背后是inode、进程凭证、shadow哈希这些操作系统核心概念。把这一层想透了很多问题不用查文档也能自己推出来。1. 先搞懂目录在系统里的真实形态它并不是一个文件夹1.1 目录的本质一张文件名-inode编号的映射表在图形界面里目录看起来就是个装文件的盒子。但在Linux内核眼里目录就是一个特殊类型的文件里面存储的是一系列目录项dentry每个目录项保存了两样关键信息文件名和对应的inode编号。inode才是文件的真正本体保存着文件大小、权限、属主、时间戳、数据块指针。文件名只是inode的一个入口而已。这也是为什么硬链接hard link能存在多个文件名指向同一个inode编号ls -l看到的链接数就是指向该inode的目录项数量包括原文件名、硬链接名、还有.和..这两个特殊目录项子目录会多算两个。在C语言里遍历目录依赖的就是这套机制。用opendir打开目录得到DIR*句柄然后反复调用readdir读取目录项最后closedir关闭。我写过不少统计目录文件数的工具核心代码其实就是这三步#include stdio.h #include dirent.h int main(int argc, char *argv[]) { const char *path argc 1 ? argv[1] : .; DIR *dir opendir(path); if (dir NULL) { perror(opendir); return 1; } struct dirent *entry; long count 0; while ((entry readdir(dir)) ! NULL) { // 跳过 . 和 .. if (entry-d_name[0] .) continue; printf(%s\n, entry-d_name); count; } closedir(dir); printf(total entries: %ld\n, count); return 0; }这里有个容易忽略的细节readdir返回的顺序是没有固定规则的取决于文件系统的哈希算法和目录项布局。你千万别指望readdir返回的结果按文件名排序否则写脚本统计文件时结果会忽变。真要排序列得自己把所有名字收集到数组里再qsort。还有一点struct dirent里的d_type字段可以告诉你这个目录项是普通文件、目录、符号链接还是设备文件但不是所有文件系统都支持。比如某些网络文件系统返回DT_UNKNOWN严谨的代码应该在这种情况下降级调用stat去判断类型。这种细节在实际项目里特别容易踩。1.2 目录权限和三组rwx位的关键区别文件的权限好理解r读、w写、x执行。但目录的权限含义完全不同很多人第一次栽跟头就是在这里r读允许读取目录项列表。没有r权限ls能看到目录名但列不出内容提示权限不够。w写允许在目录里创建、删除、重命名文件或子目录。注意删除一个文件写权限看的是目录不是文件本身。这是新手最容易懵的地方。x执行允许穿越目录也就是进入目录去访问里面的文件。只有r没有x你能看到文件名却无法访问内容只有x没有r你可以通过明确路径访问已知文件但列不出有什么。当你用chmod给目录设置权限时现实中常用组合是755rwxr-xr-x属主能读写进入其他人只能进入和查看。如果要组建团队共享目录常设2770或1770带特殊权限位后面实操章节细说。特殊权限位在这块也很关键。setgid2在目录上的效果是该目录下新建的文件或目录自动继承目录的属组而不是创建者的默认组。这是共享协作目录最重要的机制之一。sticky bit1最典型的就是/tmp有写权限的所有人都能在这里新建文件但只能删除自己拥有的文件防止互相乱删。设置方式是chmod gs和chmod t数字模式就是2770和1770。1.3 df和du为什么数值对不上别被命令输出骗了df和du是排查磁盘问题的首选命令但真到排查时你会发现两个命令算出来的已用空间经常对不上。这不是bug而是统计口径不同。du是从文件角度统计的遍历目录树把每个文件实际占用的数据块累加起来。df是从文件系统角度统计的直接看整个分区上块的分配情况。两者差异主要来自几类东西已经被删除但仍被进程占用的文件文件句柄还开着inode和块还在账上但路径已经没了、文件系统自身元数据超级块、inode表、以及不是通过普通文件路径写入的数据比如数据库的裸设备写。有一次线上告警df -h显示根分区满到100%但du -sh /算出来只用了40G。最后用lsof | grep deleted定位到是一个日志进程把2G的删除文件一直占着重启进程后空间立刻释放。你要是只盯着du排查永远找不到问题。还有两个命令是排查和目录操作强相关的df -i看inode是否耗尽文件数太多但每个都很小块没满inode先满了du --max-depth1 -h快速定位哪个一级目录吃掉了空间。这几个小技巧在面试题里也常出现。2. 用户和组为什么是权限的源头四个核心文件一读就懂2.1 passwd、shadow、group、gshadow先背下字段再干活用户信息最终都落在文本文件里这也是Linux用户管理透明的根源。系统编程和运维排查绕不开这四个文件/etc/passwd每行是一个用户标准七个字段冒号分隔用户名:密码占位符:UID:GID:注释信息:家目录:登录shell细心的人会发现密码字段现在都写成x真正的密码哈希根本不在这里而在/etc/shadow。这是历史演进的结果passwd文件需要被很多程序读取比如ls -l要显示用户名所以权限不能太严但密码哈希必须保护于是独立出来放到只有root能读的shadow文件里。/etc/shadow一行九个字段用户名:密码哈希:最后修改日:最小修改间隔:最大有效期:警告期:不活动期:过期日:保留字段这里的日期都是以1970年1月1日为0计算的天数。比如最后修改日是19800换算一下就是19800天约等于54.25年对应2024年中。chage命令帮你算好但面试里可能会考你公式date -d 1970-01-01 19800 days。/etc/group一行四个字段组名、组密码占位符、GID、组成员列表逗号分隔。/etc/gshadow对应组密码和组管理员。组密码日常基本用不上但听说过总比没见过强。实操里最重要的一条铁律改这些文件一定要备份。我一般先cp /etc/passwd /etc/passwd.bak再动手改坏了至少能救回来。另一种推荐是用vipw和vigr命令它们会加锁防止并发修改改完语法检查。2.2 useradd、usermod、userdel先弄清默认值再谈建号useradd创建用户看似一条命令实际它读了很多配置/etc/default/useradd默认shell、家目录基础路径、默认组策略和/etc/login.defsUID范围、密码过期默认天数、创建家目录开关。很多人建出来的用户没有家目录、shell是/bin/sh就是因为这些默认值没搞清楚。常用参数组合我建议记成一套模板# 创建用户并指定家目录、登录shell、附加组 useradd -m -d /home/alice -s /bin/bash -G wheel,develop alice-m创建家目录并默认从/etc/skel拷贝初始化文件.bashrc、.profile等-d指定家目录路径不写默认是/home/用户名-s登录shell。写成/sbin/nologin可以让用户不能登录但能跑系统服务-G附加组用户会同时属于主组和这些附加组-u手动指定UID批量迁移用户时有用-e设置账号过期日期格式YYYY-MM-DD临时工账号很有用usermod就是修改这些属性usermod -L锁定用户在shadow密码前加!usermod -U解锁usermod -aG develop alice追加附加组必须带-a否则会覆盖原有的附加组列表这个坑我栽过不止一次。userdel -r alice是删除用户并连家目录和邮件目录一起删不加-r会留一堆残留。删除前最好先确认这个用户没有占用文件用find / -user alice搜一下否则删完一堆文件变成无主inode权限混乱很难收拾。如果你要的是查那id alice、groups alice、whoami、w这些命令就够了。id一次输出UID、GID、附加组比任何文档都直观。2.3 系统编程视角getpwuid和getgrgid是后台的隐形英雄前面几个文件都是文本但系统编程里不能直接去解析字符串标准做法是用库函数。你随手敲一个ls -l它显示用户名和组名的过程其实就是调用了getpwuid和getgrgid根据文件的UID/GID去查对应名称。举一个打印指定用户完整信息的C程序#include stdio.h #include sys/types.h #include pwd.h #include grp.h int main(int argc, char *argv[]) { if (argc ! 2) { fprintf(stderr, Usage: %s username\n, argv[0]); return 1; } struct passwd *pwd getpwnam(argv[1]); if (pwd NULL) { perror(getpwnam); return 1; } printf(username: %s\n, pwd-pw_name); printf(uid: %d\n, pwd-pw_uid); printf(gid: %d\n, pwd-pw_gid); printf(gecos: %s\n, pwd-pw_gecos); printf(home: %s\n, pwd-pw_dir); printf(shell: %s\n, pwd-pw_shell); struct group *grp getgrgid(pwd-pw_gid); if (grp ! NULL) { printf(primary group: %s\n, grp-gr_name); } return 0; }这里有个线程安全的问题。老接口getpwnam和getgrgid返回的是静态缓冲区指针下一次调用会把上一次的结果覆盖掉。在多线程程序里这会造成数据错乱。替代方案是getpwnam_r和getgrgid_r它们需要你预先分配足够大的缓冲区函数会填充用户传入的struct passwd结构。真实企业代码或面试手写写_r版本能加不少印象分。3. 密码管理不只是passwd从shadow字段到过期策略3.1 密码哈希的演化为什么shadow里长串字符不能反解/etc/shadow里密码字段长这样$6$salt$hash$6$表示SHA-512算法$1$是MD5$5$是SHA-256$y$是较新的yescrypt$2b$是bcrypt。哈希是单向函数理论上不可反解所以密码破解的本质就是暴力尝试字典反复哈希比对考验的是算法计算速度和字典质量。每个用户密码有个独立的随机盐salt作用就是让相同明文密码产生不同哈希值防止两个用户密码相同导致shadow里的哈希串相同也防止预计算彩虹表直接命中。这也是为什么你不能拿一个已知哈希去比对另一个用户是不是同一密码。作为系统管理员一个重要原则是永远不要在命令行直接传密码明文。useradd alice -p mypassword这种写法密码会出现在shell历史记录和进程列表中等于裸奔。正确做法是passwd alice交互式输入或者脚本里用chpasswd并通过标准输入喂数据。用passwd命令改密码时它走的是PAM可插拔认证模块的passwd服务会检查密码长度、复杂度策略。这些策略定义在/etc/pam.d/passwd和/etc/security/pwquality.conf学了PAM之后你会发现改密码不是一条命令的事而是一整条策略链的事。3.2 chage让密码会过期才是安全的态度密码过期是运维面试和实际工作里的高频词。一个永久有效的密码哪怕再强泄露了也毫无办法。用chage可以让密码有过期日、到期强制更换、过期前提醒# 查看用户密码老化信息 chage -l alice # 设置最低修改间隔0天最大有效期90天提前7天警告账号2025-12-31过期 chage -m 0 -M 90 -W 7 -E 2025-12-31 alice # 强制用户下次登录必须先改密码 chage -d 0 alice注意-d 0把最后修改日设成0等价于让系统认为密码在1970年就修改过因此已经严重过期用户一登录就会被要求设置新密码。这是创建临时账号、初始密码发放的标配操作。密码过期有个典型流程过期前-W天数开始警告用户仍然能登录过期当天开始拒绝登录提示Your password has expired如果设了-I不活动天数在过期后的那段时间内还没改密账号会被锁定必须管理员介入。/etc/login.defs里还有一堆默认值PASS_MAX_DAYS、PASS_MIN_DAYS、PASS_WARN_AGE。新用户默认密码策略全在这里定义。我建议企业环境统一设成PASS_MAX_DAYS 90既能满足多数安全合规要求又不会让用户因为改密码太频繁而抱怨。3.3 PAM和sudo认证不只是登录那一刻的事PAM全称可插拔认证模块所有需要认证的服务登录、ssh、su、sudo、passwd都通过/etc/pam.d/下对应的配置文件控制。比如你想禁止root从远程ssh登录改/etc/pam.d/sshd或sshd_config都有办法。想强制所有新建密码符合复杂度设pam_pwquality.so。PAM的模块调用顺序很讲究auth、account、password、session四种管理组分别对应不同阶段。初学时不用背全套但你至少要知道为什么改密码失败日志在/var/log/secure里能查到pam_unix(passwd:auth)这类记录。排查认证问题离不开这些日志。sudo权限则走/etc/sudoers这个文件必须用visudo修改因为它会做语法检查防止你写错一行把整个sudo搞瘫痪。sudoers的语法核心是谁能用什么身份在哪些主机上执行哪些命令# 允许wheel组成员以任意身份执行所有命令 %wheel ALL(ALL) ALL # 允许alice以root身份执行系统管理命令无需密码 alice ALL(root) NOPASSWD: /usr/sbin/useradd, /usr/sbin/usermod实际运维里最小权限原则强调的是不要把ALL(ALL) ALL随便给。我看到很多团队图省事把全员都加进wheel组一出事所有用户都是root权限审计无从做起。正确姿势是普通用户只有日常权限运维成员用sudo而且sudo执行日志默认记录在/var/log/secure出了问题知道谁干了什么。4. 实操从零搭建用户组目录密码完整链路4.1 新员工环境搭建一次演示全流程假设来了一个新同事岗位是后端开发要加入dev团队并且能执行sudo。完整流程如下# 1. 检查是否已有同名用户 id alice # 2. 创建一个组如果不存在 groupadd dev # 3. 创建用户家目录、bash shell、加入dev组和wheel组 useradd -m -d /home/alice -s /bin/bash -G dev,wheel alice # 4. 设置初始密码使用stdin方式避免历史记录 echo TempPass2025 | chpasswd # 5. 强制首次登录改密码 chage -d 0 alice # 6. 设置密码过期策略90天有效期7天提醒 chage -m 0 -M 90 -W 7 alice # 7. 验证 id alice chage -l alice第4步用chpasswd而不是passwd因为脚本里能自动化。第5步和第6步连起来的意思是用户拿到临时密码第一次登录必须改成自己的之后每90天更换一次。这套流程在用户量大的环境里直接写成shell脚本或Ansible任务清单每次新人都一样。有个细节值得注意-G dev,wheel里的wheel是sudo组的约定不同的发行版组名可能不一样。Debian/Ubuntu习惯用sudo组RHEL系用wheel组。跨发行版排查时先cat /etc/sudoers看里面写的是哪个组名别想当然。4.2 共享目录的最佳实践setgid和sticky bit的搭配团队开发最怕文件权限混乱A创建的文件B改不了或者谁都能删别人的文件。标准解法是建一个组共享目录用setgid保证目录下新文件自动继承组# 创建共享目录 mkdir /srv/dev chown root:dev /srv/dev # 属组给dev组 chmod 2770 /srv/dev # setgid 属组读写执行其他无权限2770中第一个2就是setgid位。效果是dev组成员的任何人在/srv/dev里新建文件和目录属组自动变成dev而不是他个人的主组。这样B即使不是文件创建者只要属于dev组也能按组权限操作。如果你希望里面的文件和子目录默认只允许属主和组读写、其他人不可见可以在子目录上再保持组权限。如果还嫌麻烦可以设置默认ACLsetfacl -d -m g::rwx /srv/dev这条命令给目录设置默认ACL之后新建的文件都会带组读写执行权限。ACL和setgid并不冲突ACL的优先级更高可以根据具体场景选择。对于大多数团队共享场景chmod 2770setfacl已经够用不需要把权限放太开。还要注意umask的影响。系统默认umask 022会让新建文件权限变成644、目录755在共享目录里组就没有写权限。如果要用setgid方案建议在这个目录相关用户的.bashrc或系统级/etc/profile里调整umask 027或007确保新建文件天然带组读写权限。4.3 用户生命周期的管理锁定、禁用、删除场景用户离开团队时不要直接userdel -r一刀切。安全做法是先锁定、再归档、最后删除。# 立即锁定账号禁止登录 usermod -L alice # 或者 passwd -l alice效果类似 # 把账号过期时间设为昨天双重保险 chage -E 0 alice # 如果要删除先确认没有占用文件 find / -user alice 2/dev/null # 确定归档没问题后 userdel -r alice锁定只是改shadow密码哈希加!前缀张三的进程如果已经登录了锁定不会踢掉已建立的会话。要强制下线用pkill -u alice或者skill -u alice。那个删不掉用户提示userdel: user xxx is currently used by process的报错就是有进程还赖着。排查时看ps -u alice列出进程清掉再删。还有一种场景出口设备上要给外包开账号只允许他们用堡垒机跳转不给交互shell。这时useradd -s /sbin/nologin或者-s /usr/sbin/nologin就派上用场账号能认证、能跑服务但怎么登录都进不了shell。5. 常见问题与排查技巧实录5.1 忘记密码怎么办root密码和普通用户分开说先说普通用户密码忘了。有sudo权限就直接sudo passwd alice没sudo权限只能由root重置。这也是为什么root密码一定得有一个人管着。root密码忘了常规做法是重启进入单用户模式rd.break或init/bin/bash在initramfs阶段挂载根文件系统并重新设置。但渲染环境差异很大不同发行版和引导方式操作细节不一样。我要强调一点这操作必须是你有物理或控制台权限的机器在授权范围内进行。真忘了root密码网上资料一堆但每一步都涉及挂载、chroot、selinux标签修复建议先在一台测试虚拟机练熟了再上生产。chattr i /etc/shadow这个隐藏机制我也遇到过明明root身份改了密码提示成功但下次登录还是进不去一查lsattr /etc/shadow发现不可变属性。这是防篡改的措施。排查任何改密码无效问题时lsattr值得查一下。5.2 密码过期卡在登录环节的应急处理生产服务器最尴尬的时刻某用户出差回来登录提示Your password has expired. You must change your password now然后他想改密码结果密码策略太严格最短间隔、历史密码不能重复怎么改都失败连system入口都断了。这种场景我处理过好多次应急方案就是root执行# 重置密码清空最短修改间隔再把过期时间清掉 passwd alice chage -m 0 alice chage -d 0 alicechage -d 0强制下次登录再改密比直接删掉expire状态更合理。如果你需要的是赶紧进去干活密码策略回头再说也可以直接chage -M -1取消有效期但这等于放弃了过期策略安全上不太推荐只适合临时救急。5.3 sudo报错not in the sudoers file的三种解法新用户第一次用sudo最常见的就是这么一条alice is not in the sudoers file. This incident will be reported.原因很简单用户没进sudo组。解法看具体环境有root权限usermod -aG wheel aliceRHEL系或usermod -aG sudo aliceDebian系只想给个别命令visudo里加alice ALL(root) /usr/bin/systemctl restart nginx临时借用root执行su - alice切过去或者sudoers里加alice ALL(ALL) ALL慎用我见过最坑的是有人直接把/etc/sudoers权限改成666结果sudo直接拒绝加载系统所有sudo操作全废。sudoers必须是440权限、属主root。这个权限一旦错了visudo都跑不起来只能root手动修复。5.4 用户删除后目录残留与UID复用风险有个历史教训某公司删了一个用户后来新用户用了同一个UID结果发现一堆旧文件属于新用户权限全部错乱。这是因为删除用户时没删干净文件系统里还挂着旧UID的inode。排查命令是find / -nouser无属主的文件和find / -nogroup无属组的文件。处理思路是提前归档或清理删除用户前先用find / -user alice /tmp/alice_files.txt列出文件清单再决定是备份、迁移还是删除。如果只是临时移除账号但保留数据可以先把家目录打个tar包存到归档目录再执行userdel -r。有一句忠告值得记住UID是复用资源删账号必须同时处理他占用的文件否则后患无穷。5.5 目录权限异常导致服务无法访问的完整排查套路排查目录能列但文件访问不了或者服务一直报权限不足时我的思路逐步排查ls -ld /path/to/dir看目录权限确认x位ls -l /path/to/file看文件权限确认属主属组namei -l /path/to/file一次看清路径上每一级目录的权限看是不是中间某层卡住getfacl /path/to/file检查ACL可能是ACL设置了额外拒绝检查selinuxgetenforce确认状态ls -Z看文件的安全上下文大部分目录访问问题都出在中间路径缺少x权限或者ACL覆盖了传统权限上。比如有些README文件权限644没写权限但服务因为要以root之外身份运行所以读不了。还有些人喜欢把家目录设成777服务倒是能跑但登录shell会报警告提示家目录权限不安全因为.bashrc可被任意用户篡改。安全和便利之间得掂量清楚。5.6 常见问题速查表问题现象常见原因快速解法Permission denied目录缺x权限或ACL限制namei -l查路径getfacl查ACLuserdel: user is currently used用户有残留进程ps -u alice清掉后重试sudo: no valid sudoers sourcessudoers权限不对或语法错误root登录检查ls -l /etc/sudoers密码过期无法登录shadow里过期日到了root执行chage -d 0 用户名df满但du不大删除文件仍被进程占用lsof L1找到并重启进程新建用户没家目录useradd没有-m参数usermod -m -d /home/alice alice补建目录共享但互相删不了没有sticky bitchmod t /srv/dev6. 一点实际体会收尾做了几年系统编程和运维之后我的感受是目录和用户操作的内容看似零散但它们全是操作系统资源管理的具体表现。文件权限、inode、进程凭证、PAM认证每一个概念都串在一条线上。所以排查问题时先别急着执行命令先问自己这个操作最终作用于哪个数据结构方向对了解法自然就出来了。另一个经验是目录和用户相关的配置文件宁可多备份、多验证不要图快直接改。/etc/passwd、/etc/shadow、/etc/group、/etc/sudoers这四个文件建议每次批量操作前都做带时间戳的备份操作完用pwck和grpck检查一致性。这些检查在自动化脚本里也建议加上能帮你尽早发现问题。最后分享一个小技巧写脚本或写程序前先man一下相关命令和API的返回值。比如useradd的退出码9是用户名已存在ls的-d和-ld差在是否追踪目录本身passwd --stdin不是所有发行版都支持。很多看起来诡异的问题根源都是对命令默认行为和退出码不够敏感。把这一层补上Linux系统编程才算是真正入了门。