
做Linux系统编程也快十年了从最早的嵌入式交叉编译到后来写服务端中间件目录和用户这两块内容始终绕不开。很多人觉得“目录操作不就是mkdir、cd吗用户操作不就是useradd吗”等真到了程序里遇到Permission denied、目录遍历读到奇怪文件、多级目录创建失败这些问题时才开始理解背后的坑有多深。这篇总结我不会从零讲“Linux是什么”而是直接把目录操作和用户操作在系统编程层面的完整链路拆开底层数据结构、系统调用、权限模型、常见踩坑全部串起来讲。适合正在写Linux服务端程序、做嵌入式开发、或者准备Linux面试的朋友建议直接收藏。1. 目录操作先搞懂目录在文件系统里到底是什么1.1 目录不是文件夹是一张名字到inode的索引表先说一个最常见的误解很多人把目录想象成Windows里的“文件夹”觉得目录就是“装文件的东西”。在Linux里这个理解会让你后续排查问题走很多弯路。目录本质上是一个特殊的文件它的数据区域存的是“目录项”dentry列表每个目录项里记录两件事一个文件名以及这个文件名对应的inode号。你调用open(test.txt)的时候内核先找到test.txt这个名字在哪个目录里取出对应的inode号再通过inode去定位磁盘上的数据块。这个设计带来一个非常实用的推论目录的读写和其他文件一样也是受权限控制的。你不能访问一个目录不一定是权限位没给够也可能是这个目录文件本身不可读。另外“移动文件”在Linux里本质是修改目录项不是搬数据。同一文件系统内mv一个G级文件几乎是瞬间完成的就是因为内核只改了目录里的一条记录数据块完全没动。这两个推论在后面排查问题时非常有用。如果没接触过inode可以简单理解成文件名是人用的inode是内核用的。目录就是这张“名字到编号”的映射表。为什么硬链接不能跨文件系统因为硬链接本质是在另一个目录里添加一条指向同一个inode的记录而inode号只在当前文件系统内有效。这个知识点面试经常考也是理解目录文件本质的钥匙。1.2 系统编程里真正高频的目录API命令行的ls、cd大家都熟但做系统编程时跟目录打交道用的是一组更底层的接口。按我的使用频率排一下这几组函数在C/嵌入式面试中也是出现率最高的第一组是创建和删除mkdir()和rmdir()。注意C函数mkdir和shell命令mkdir不一样它只有路径参数和mode参数比如mkdir(/data/app, 0755)。问题在于它不能递归创建多级目录如果你mkdir(/data/app/log, 0755)而/data不存在直接返回-1errno是ENOENT。后面我会专门讲怎么实现递归创建。第二组是遍历和读取opendir()、readdir()、closedir()。这里有个经典坑readdir返回的struct dirent里有个d_type字段能告诉你这个条目是文件、目录还是链接但是很多文件系统尤其是某些网络文件系统并不填充这个字段返回DT_UNKNOWN。所以代码里如果只靠d_type DT_DIR做判断在部分环境下会漏掉目录。稳妥的做法是拿到名字后再调stat()确认。要判断详细元数据还要提一下stat()、lstat()、fstat()三兄弟stat跟路径走lstat不跟随符号链接fstat针对已打开的文件描述符。第三组是路径解析和切换getcwd()、chdir()、realpath()、access()。写多线程程序尤其要注意chdir因为工作目录是进程级的不是线程级的。一个线程chdir整个进程的所有线程都跟着换目录这对多线程服务是灾难。所以我很少在服务端代码里用chdir要么fork子进程后改要么直接全路径操作。1.3 多级目录创建从一次失败的面试题说起有次面试一个岗位对方直接出了一道题“用C语言实现mkdir -p也就是递归创建多级目录。”有十年经验的我张口就说用system(mkdir -p ...)当场被面试官按在地上摩擦——系统编程里尽量避免system调用一来forkexec的开销大二来命令注入风险高三来无法做细粒度错误处理。正确思路是用stat()判断路径是否存在用逐级拆解的方式把/a/b/c先试/a再试/a/b最后/a/b/c每级先stat()不存在就mkdir()然后继续下一级。要注意权限参数必须写mkdir(path, 0755)如果漏了mode会继承一个受umask影响的随机权限。另外递归创建时每一级目录的权限都要落实不要等最后一级建完再整体chmod因为中间目录对后续写入是必需的。实际开发里我更推荐直接用现成方案在PHP里mkdir($path, 0755, true)第三个参数true就是递归创建这个true底层也不是PHP自己实现的而是映射到系统调用级别的循环创建原理跟我上面写的C逻辑一致。搞懂了底层换到哪个语言都顺手。热词里“phpmkdir多级目录”搜得很勤本质问题其实就在这个底层机制上。2. 用户与权限Linux安全模型的真正核心2.1 UID/GID内核眼里根本没有用户名Linux的用户名只是给人类看的一张显示标签内核里所有权限判断都围绕**UID用户ID、GID主组ID**进行。你在命令行敲ls -l看到root root那是getpwuid()把UID 0翻译回字符串的结果。系统编程里做权限校验千万不要解析passwd文件里的名字再去比较直接比较UID整数性能和安全性都好得多。有几个细节强烈建议背下来面试常考排查也是刚需UID 0就是root所有常规权限检查对UID 0直接放行UID 1-999一般是系统用户给daemon、nobody这类服务账号用普通用户从1000开始。另外用户还有一组“附属组”supplementary groups主组GID写在passwd第4字段附属组写在他所在的group条目里。进程判断权限时不一定看当前UID有时看的是有效用户IDeuid和有效组IDegid这就是setuid程序的来源。理解了这套机制你就能明白为什么资料里总说“不要用root跑服务”用普通用户加系统服务托管才是标准做法。2.2 权限位与目录的关系为什么x权限对目录如此重要文件权限的rwx三组位大家都懂但目录上的rwx意义和文件完全不是一回事这里必须单独说透r读对目录来说允许“列出目录内容”也就是可以调用readdir()看到里面有哪些名字。注意光有r没有x你执行ls会报权限错误因为ls要读取名字还要知道每个名字的inode信息后者需要x。w写允许在目录里“创建、删除、重命名条目”。注意这只针对目录项本身不涉及文件内容。很多新手以为改了文件的权限就要对文件所在目录有w其实操作文件内容只需要文件本身的w而创建或删除一个文件必须有目录的w。x执行/进入对目录来说x意味着能否“穿过traverse这个目录”。你要访问/a/b/c中的c文件需要/a、/a/b、/a/b/c三层目录都有x权限。这也是为什么很多教程反复强调“目录要给x否则所有子项都访问不了”。这个区别最好的记忆方式是三类操作分别想一遍列目录靠r增删条目靠w路径访问靠x。我之前排查过一个诡异现象用户说“我能ls这个目录但cat不了里面的文件”一查目录权限是drwxr--r--有r没x。他确实能看到文件名但readdir之后要stat每个文件stat需要路径中每层目录的x这就卡住了。所以目录权限里x往往比r更关键。2.3 用户管理背后的文件passwd/shadow/group 不只是三个配置文件做系统编程的人不能把用户操作理解成“敲几条命令就完事”。用户管理的底层其实是三个文件加上getpwnam()、getpwuid()这些库函数在读写它们/etc/passwd每一行7个字段用户名、密码占位、UID、GID、注释、家目录、登录shell。注意这个文件所有用户可读所以密码永远不在里面只存一个x占位符。/etc/shadow真正保存密码哈希和过期策略的地方只有root和shadow组可读。系统编程里如果要校验用户登录正确做法不是自己去读shadow比对哈希而是用PAM否则就是自己造了一个有漏洞的认证轮子。/etc/group组名、组密码占位、GID、组成员列表。前面说的附属组就是从这里的第4字段拆出来的。命令层面useradd和adduser不一样前者是底层工具后者是前者的友好封装壳脚本Debian系尤其明显。系统编程里我主要关注getpwuid/getpwnam这类接口它们能让你在C/C代码里安全地做UID到用户名的映射。要提醒的是不要直接fopen(/etc/passwd)去解析请使用标准库的getpwnam接口因为有些环境例如LDAP认证的用户信息根本不在这三个文件里标准接口会帮你走完整的NSS配置链。直接读文件读不到LDAP用户必然出诡异bug。3. 实操写一个目录归档与权限巡检的小工具3.1 需求分析与工具选型理论讲太多没有用我拿自己最近在用的一个实际场景完整走一遍服务器上有若干个业务目录日志每天产生大量文件目录权限偶尔会被人为改动导致服务起不来。我想做一个轻量巡检工具功能是遍历指定目录树统计每种扩展名的文件大小并检查每个目录的权限是否在预期范围内比如要求所有目录至少755、所有普通文件不高于644。技术选型上用C实现核心逻辑因为标题就是系统编程而且这个工具要部署在最小化系统上不能依赖Python运行时。整个思路是用opendir加readdir实现递归遍历对每个条目判断类型后分目录、文件处理最后输出汇总报告。考虑到生产环境目录数量可能过万我没有用递归函数而是显式维护一个目录栈避免栈溢出风险。以下代码可以直接抄也可以改成Go或Rust的版本。3.2 核心代码与关键参数说明先说整个遍历的核心。我维护一个char dirs[][PATH_MAX]栈初始把要巡检的根目录压栈然后循环while (stack_top 0) { DIR *dp opendir(stack[stack_top--]); if (!dp) { perror(opendir); continue; } struct dirent *entry; while ((entry readdir(dp)) ! NULL) { if (!strcmp(entry-d_name, .) || !strcmp(entry-d_name, ..)) continue; char full[PATH_MAX]; snprintf(full, sizeof(full), %s/%s, current_dir, entry-d_name); struct stat st; if (lstat(full, st) -1) { /* stat失败比如权限不足或路径过长跳过 */ continue; } if (S_ISDIR(st.st_mode)) { push(full); /* 压栈继续遍历 */ } else if (S_ISREG(st.st_mode)) { record_size(full, st.st_size); /* 按扩展名累加大小 */ } } closedir(dp); }这里要展开三个实际中反复踩过的点。第一不要用entry-d_type DT_DIR做过滤直接lstat最稳原因前面说过部分文件系统的d_type不可信。第二判断文件类型必须用S_ISREG(st.st_mode)等宏不要只靠扩展名判断因为生产环境里一个目录完全可以命名为xxx.log靠后缀分类会把目录当文件统计进去。第三遍历时一定要用lstat而不是stat否则遇到符号链接时stat会跟随链接指向真正文件万一有循环链接你的目录栈会无限增长。权限检查部分我用st_mode 07777提取权限位然后对目录执行规则如果(mode 0022) ! 0就标记为“危险目录”。换句话说只要目录对group或other有写权限就报警。日志目录被人不小心chmod -R 777这种事我见过太多次这个规则能在权限事故扩散前给出预警。代码本身不算复杂复杂的是规则要跟业务约定好比如哪些目录确实需要组可写这些就要单独加白名单。3.3 实测效果与边界条件我在一个生产调试环境的/app/logs目录下跑了这个小工具目录里混着20多个子目录、几百个文本、若干压缩包和两个软链接。实测输出大概长这样脱敏后[巡检报告] /app/logs .log 文件138 个共 2.1 GB .gz 文件12 个共 830 MB .out 文件3 个共 45 MB [警告] /app/logs/2024/archive 权限为 0777 [警告] /app/logs/tmp 权限为 0775 共发现 2 个异常目录0 个异常文件结果符合预期archive目录是上周某同事图省事chmod出来的tmp目录是程序安装脚本误用了0775导致其他用户也能写。整个遍历大约耗时0.2秒远快于用find -exec stat的方式。我还故意测了边界条件把巡检根目录设为/proc会频繁报opendir: Permission denied或stat失败这说明工具够稳——遇到无法访问的目录只是跳过不会崩。这里也有个值得说的经验不要把巡检工具做成“遇到权限不足就报错终止”因为系统里总有那么些目录是主动访问不了的。巡检工具的正确形态是能访问的就分析访问不了的就记录跳过项数量最后汇总时把“跳过项”也打出来。我最初版本遇到unreadable目录直接退出第一次跑/proc就卡死后来改成“跳过并计数”才真正可用。4. 高频问题排查把踩过的坑一次性讲清楚4.1 Permission denied权限、所有者、挂载选项三查法服务起不来、文件写不进去第一反应基本都是查权限。我的排查习惯按三个层级来第一层看权限位。用ls -l和namei -l /path/to/file后者能逐级显示每一层目录的权限。我见过太多文件没问题、目录有问题的case用namei能一目了然。第二层看所有者。不要只看rwx要看这个UID到底是谁。有一种很隐蔽的坑二进制文件的属主是别的用户即使权限是755风险等级也和你预想的不一样取决于当前进程的UID。第三层看挂载选项这在容器环境尤其关键。mount | grep /data能看到那行rw,noexec,nosuid如果挂载参数里有noexec即使脚本有x权限也执行不了nosuid会屏蔽setuid位。另外SELinux或AppArmor开启时权限位全对也可能报Permission denied需要用ausearch -m avc或dmesg | grep denied看审计日志。4.2 目录删不掉占用、只读、粘滞位“rm -rf删不掉”是开发桌上最常见的玄学问题实际原因就那几类。第一类文件或目录正被占用英文环境常提示Device or resource busy。典型场景是某个进程把目录当成了当前工作目录或者有进程打开了目录里的文件文件已被删除但描述符还开着。用lsof D /path列出占用者或者fuser -mv /path看哪个进程在用。第二类只读挂载mount -o remount,rw /data可以解决前提是有root权限。第三类粘滞位。/tmp目录权限是drwxrwxrwt那个t就是sticky bit。在粘滞位目录里即使你有写权限也不能删除不属于你的文件。有些团队把公共共享目录也加了粘滞位新人不知道就出现“文件明明在我却删不掉”的问题。删除自己的文件没问题删除别人的就报Operation not permitted。解决办法是确认自己是文件属主或者用root操作。我特别想强调一个反向经验能用find限定目录就别写裸的rm -rf。生产环境删除目录前我习惯先readlink -f确认目标路径再用“先rename再删”的两段式方法先把目录mv到一个临时名字确认服务没受影响后再异步删掉临时目录。这个习惯救过我两次。4.3 su/sudo切换后的环境陷阱用户操作里最高频的坑不是建用户失败而是切用户后环境变量不对、执行结果跟预期不一致。有几类场景我几乎每周都会遇到。场景一su不带-切过去后仍然保留原用户的环境变量PATH还是旧的于是出现“我明明装了python为什么python不存在”的疑问。记住su只切身份su -会重新加载目标用户的环境。场景二sudo执行命令时默认不继承当前用户的PATH有些发行版sudoers里配了secure_path所以sudo后可能找不到自定义命令。排查时直接sudo which一看便知。场景三sudo默认会把HOME改为目标用户家目录有些脚本用$HOME拼接路径sudo执行后行为完全不同这是隐蔽的bug源头。再看一个系统编程层面的setuid程序不生效。写了setuid位的程序运行时要靠内核把euid切换成属主UID但很多现代系统出于安全考虑会忽略脚本类文件的setuid内核只对二进制可执行文件响应setuid。另外nosuid挂载选项也会让setuid位静默失效。如果你写了个setuid工具却感觉权限没生效先查mount选项再确认文件确实是ELF二进制而不是shell脚本。4.4 Linux面试与自检目录和用户操作速查表很多朋友在搜“linux面试题”我直接把目录和用户这部分高频考点整理成一张自查表能全部答上来这部分基本就过关了考察点高频问题一句话关键回答目录结构/usr、/var、/tmp的区别是什么/usr是系统软件资源/var存可变数据/tmp是公共临时目录硬链接为什么硬链接不能跨文件系统硬链接是目录项的inode复用inode只在单一文件系统内有效系统调用mkdir为什么不支持递归创建多级目录mkdir是POSIX最小化接口只创建一个目录递归是shell层功能权限判断目录的r/w/x分别控制什么列目录、增删条目、路径穿越权限深度为什么有时候ls可见但cat失败缺少目录x权限导致stat无法获得文件信息用户管理UID 0和普通UID的区别UID 0拥有全系统特权普通UID受权限位约束su/sudosu和su -有什么区别一个保留原环境一个重新加载目标用户环境排查技巧如何快速定位目录权限问题用namei -l逐层看目录权限这张表可以当面试前的复习提纲也可以当排查手册。写这篇总结的时候我有个明显感受Linux目录和用户操作的很多问题根子都在“目录既是文件又是索引表”这同一个本质上。所有Permission denied、递归异常、环境不生效的表现最终都能回溯到权限位、UID映射、路径解析这三件事中的某一件。把这个本质想明白九成以上的坑都不再是坑。最后说点个人体会。我在实际维护服务器和写系统工具的过程中最大的收获不是记住了多少条命令而是建立了一个习惯遇到目录和用户相关的怪问题先别急着chmod -R 777也别急着把用户删了重建先按“目录本质是索引表、权限分三层、UID贯穿内核判断”这三条主线去推演绝大多数诡异现象推一遍就能找到原因。这个工具和排查方法我迭代过好几个版本现在仍在用。目录和用户操作看起来基础但基础打得牢不牢在系统编程里能直接拉开差距希望这篇总结对你有用。