ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux mkdir命令深度解析:权限、幂等与跨语言实现

Linux mkdir命令深度解析:权限、幂等与跨语言实现 1. 先把话说清楚mkdir这条命令到底在解决什么问题1.1 一次目录结构失控引发的重写我最早接触 Linux 是从一堆乱七八糟的日志目录开始的。当时要在一个全新的数据盘上搭一套日志归档结构按日期分年份、月份、类型、来源机大概四层每台机器每天生成一次。我最初的做法很朴素写了一段脚本cd /data/logs mkdir 2024 cd 2024 mkdir 03 cd 03 ...。脚本跑了三天就崩了原因也很简单——某天重跑时目标目录已经存在cd进去了但mkdir报了File exists脚本没做判断后面整条链全乱。后来把这个脚本重写成一行mkdir -p问题当场消失。这件事让我意识到mkdir 命令看着是 Linux 里最没有存在感的一条命令但它的参数设计、权限行为、错误码、脚本里的容错方式随便拎出一条都能让新手卡半天。mkdir这三个字母来自 make directories属于 coreutils 包几乎每个发行版都自带which mkdir出来一般是/usr/bin/mkdir或/bin/mkdir。这条命令解决的问题可以一句话概括把一个还不存在的目录名变成文件系统里真实可寻址的目录项。听起来简单但要回答清楚下面这些问题就不简单了——目录默认权限为什么是 755 而不是 777-p到底递归创建了什么中间层目录的权限受谁控制为什么我mkdir一个目录会报Permission denied我明明是 rootJava 里mkdir()返回 false 却不抛异常到底是哪里错了这些问题在后面几节我都会拆开讲。适合谁看刚上手 Linux、正在被目录层级和权限搞晕的朋友写了几年脚本但一直靠复制粘贴-p的老手以及在做跨平台项目、需要在 C 或 Java 里自己建目录的开发者。这篇东西不打算停留在man mkdir的翻译层面我会把实际踩过的坑和验证过程一起写出来。1.2 基本语法和最小可用例子mkdir的语法结构非常规整记住主干就够了mkdir [选项] 目录名 [目录名...]最朴素的用法是创建一个单级目录mkdir project执行完你会发现在当前工作目录下多了一个project目录。这里有两个容易忽略的点。第一mkdir默认不接受相对路径以外的隐式创建它要求父目录必须已经存在mkdir a/b在a不存在时会直接报mkdir: cannot create directory a/b: No such file or directory。第二mkdir一次可以接多个参数mkdir src bin docs会一口气建出三个同级目录这个特性在脚本里比循环好用得多。如果要确认命令到底做了什么加上-vmkdir -v project # mkdir: created directory project很多人不知道mkdir还支持--help之外的--version也支持--结束选项解析。这个--在处理奇怪目录名的时候是救命的后面第 4 节会专门讲。1.3 为什么值得为一条建目录的命令单独写一篇我给出三个理由都是实际遇到过的。第一目录权限模型和文件权限模型不一样。文件有 rwx 三个位目录也有但含义完全不同目录的r是能列出目录内容x是能穿过这个目录访问里面的东西w是能在目录里增删条目。如果你把目录设成 644少了 xls能看到里面有什么名字但cd进不去cat里面文件也会报错。这类问题在只懂文件权限的人手里经常找不到北。第二mkdir 是脚本幂等性的第一道门槛。运维脚本、部署脚本、CI 流水线几乎所有东西一上来就是建目录。这一行写不对整条流水线都会在莫名其妙的时机挂掉。-p有没有加、错误有没有吞、路径有没有引号括起来都是真实会影响生产的细节。第三mkdir 是少数几个命令行工具和系统调用同名的命令。当你在 C 里调mkdir()、在 Java 里调File.mkdirs()、在 Python 里调os.makedirs()的时候它们的行为差异和底层的mkdir(2)系统调用是强相关的。搞懂命令行这层跨语言的坑能少一大半。2. 参数逐个拆解以及它们背后的取舍逻辑2.1 -p递归创建脚本幂等的核心开关-p是--parents的短选项它做两件事把路径里所有不存在的中间目录一并创建出来如果最终目录已经存在不报错、直接返回成功退出码 0。mkdir -p /data/logs/2024/03/web01假设/data存在/data/logs不存在这一条命令会把logs、2024、03、web01四层一次性建出来。这里有个非常关键但很少有人注意的差异加了-p之后目标目录已存在时不会报错但如果不加-p目录已存在会返回非零退出码并打印File exists。在 shell 脚本里set -e打开的情况下这个非零退出码会直接终止脚本。我见过太多脚本在第二次执行时突然中断排查半天才发现是第一行的mkdir没有-p。mkdir -p /tmp/demo # 第一次创建 mkdir -p /tmp/demo # 第二次静默成功退出码 0 mkdir /tmp/demo # 第二次报错退出码 1再补一个实测细节-p对中间目录的权限处理和最终目录不完全一样。GNU coreutils 在创建中间目录时会在你指定的 mode 基础上额外加上所属用户的uwx保证自己后续还能继续往下建最后再受umask裁剪。这个机制导致了一个常见困惑——mkdir -p -m 555 a/b之后a的权限往往不是 555。所以我的建议很直接只要你对多级目录的权限有精确要求就别指望-m一把梭老实用chmod收尾。注意-p是幂等的但它不检查路径里被占位的东西是什么类型。如果/data/logs是一个普通文件而不是目录mkdir -p依然会报Not a directory不会帮你删掉重建。2.2 -m绕过 umask 直接指定权限-m是--mode的短选项用来指定新建目录的权限写法是八进制数和chmod一样。mkdir -m 700 private mkdir -m 2775 shared不加-m的时候目录权限是拿 777 减去umask的值。绝大多数发行版默认umask是 022所以目录出来是 755。-m的作用就是跳过这个默认推导直接给出你想要的最终权限。它的实现方式值得说一句mkdir在创建时先用mode ~umask交给内核然后在创建成功后再调用一次chmod把权限修正为你-m指定的精确值。这就解释了为什么-m的结果是精确的、不受umask干扰的。也正因为多了一次系统调用-m在极端性能场景下会比不带多一点点开销当然这点开销可以忽略不计。-m还支持符号形式的写法比如-m urwx,grx,o等价于 750-m arwx就是 777。符号写法可读性更好但在需要精确控制 setgid、sticky 位的时候八进制更直观比如-m 1777sticky等价/tmp的经典权限、-m 2775setgid让目录内新建文件继承父目录属组做团队共享目录特别有用。2.3 -v让脚本的每一步都有迹可循-v是--verbose会把每一个创建动作打印出来。单次手工执行意义不大但在批量建目录的脚本里它是排查问题时最省事的手段。mkdir -pv /srv/app/{conf,logs,data,tmp} # mkdir: created directory /srv/app # mkdir: created directory /srv/app/conf # mkdir: created directory /srv/app/logs # ... 每一级都清晰可见需要留意的一点-v只为实际创建的目录输出信息。如果目录已经存在-p -v组合下不会打印任何东西。这个特性在写幂等脚本时刚好可以当判断依据——当然更规范的做法是用[ -d path ]显式判断别依赖日志输出做逻辑分支。还有一个组合是-p加-v再重定向到日志mkdir -pv /srv/app/{conf,logs,data} /var/log/init.log 21这样每次初始化部署都有痕迹留下来出了事可以对照这一行看是哪一级没建起来。2.4 参数组合的优先级与冲突处理实际用的时候怎么组合我给一张对照表把常见场景和推荐写法列清楚这张表是我自己整理过很多次才定下来的。使用场景推荐写法关键理由交互式手工建单层目录mkdir name最简单出错能立刻看到原因脚本里建多级目录mkdir -p path幂等重跑不报错建多级目录并要精确权限mkdir -p path chmod 750 path规避-m对中间目录的歧义需要一个团队共享目录mkdir -m 2775 sharedsetgid 保证新建文件属组一致需要看清每一步创建过程mkdir -pv path输出可追溯目录名以横杠开头mkdir -- -rf或mkdir ./-rf避免被当成选项解析目录名含空格或特殊字符mkdir my dir必须加引号关于优先级-m和-p同时出现时-m只精确作用于最后一级-v和-p之间没有冲突可以随意叠加。-m和umask之间-m胜出但这只对最终目录成立。记住这条界限能省掉大量调试时间。3. 权限、umask 与目录权限的底层逻辑3.1 默认 755 是怎么算出来的很多人第一次ls -ld一个刚建的目录看到drwxr-xr-x就去查文档查完还是不明白为什么不是 777。答案就是umask。内核在创建目录时用的权限基数是 777对目录而言文件是 666目录少了执行位因为文件默认不需要执行权限。umask是一个屏蔽位表示要从基数里去掉哪些权限位。默认 umask 022 目录基数 777 去掉被屏蔽的位777 中把 group 的 w、other 的 w 去掉 结果 755 → drwxr-xr-x对照表如下umask目录结果权限目录权限表示常见场合022755drwxr-xr-x大多数发行版默认002775drwxrwxr-x团队协作服务器常见设置027750drwxr-x---生产环境偏严格077700drwx------私密数据、密钥目录查看当前 umask 很简单umask # 输出 0022前面的 0 是八进制前缀 umask -S # 输出 urwx,grx,orx符号形式更直观临时改 umask 只影响当前 shell 会话要永久生效得写进/etc/profile、/etc/bashrc或用户级的~/.bashrc。我个人的习惯是不轻易动全局 umask需要特殊权限的目录在创建时就显式指定这样排查问题时因果关系是清楚的。全局改了 umask半年后你自己都不记得为什么某个目录是 775。3.2 目录权限里 x 位的特殊含义这是目录权限最容易踩的一类坑。对文件来说x表示这个文件可执行对目录来说x表示你可以穿过这个目录去访问它下面的条目。具体来说r允许你列出目录里的名字ls能出结果x允许你访问目录里的具体条目cat dir/file才能成功w允许你在目录里创建、删除、重命名条目。这意味着下面这种组合是合法但反直觉的mkdir -m 444 noread # 目录权限变成 dr--r--r-- ls noread # 能列出因为有 r cd noread # 报错 Permission denied因为没有 x反过来-m 111的目录ls会失败但cd能进去如果你知道里面文件的具体名字cat反而能读到。这个行为在做最小权限设计的时候特别有用给目录只留x不留r别人无法枚举你的文件列表但你依然能定向访问。还有一个细节删除文件靠的是父目录的w权限不是文件自身的权限。一个目录里你的文件是 000只要父目录对你有w你就能把这个文件删掉。这不是 bug是 Unix 权限模型的一贯设计。理解了这一点你才能明白为什么共享目录通常要把 sticky 位打开-m 1777它就是为了让只有文件属主或目录属主才能删自己的文件。3.3 -m 与 umask 的交互实测我在一个umask 077的环境里跑过下面这组命令结果很能说明问题umask 077 mkdir a # a 的权限700777 减去 077 mkdir -m 755 b # b 的权限755精确生效 mkdir -p -m 755 c/d # c 的权限700中间目录走 umask # d 的权限755最终目录被 chmod 修正结论很明确不加-m时权限完全由umask推导目录可能被收紧得很厉害脚本部署到不同机器上结果可能不一样。加了-m时最终目录的权限是精确的不受umask影响。-p的中间目录权限受umask和 coreutils 内部策略双重影响不要把它当成可预期的值。所以我在写部署脚本时的固定套路是mkdir -p /srv/app/{conf,logs,data} chmod 750 /srv/app chmod 750 /srv/app/conf /srv/app/logs /srv/app/data多写两三行换来的是跨机器、跨环境的行为一致。这个习惯我保持了七八年值。提示如果整个目录树需要统一权限用chmod -R会连带修改目录里的文件风险高。更稳的方式是用find分别处理目录和文件比如find /srv/app -type d -exec chmod 750 {} \;。3.4 特殊权限位setgid 与 stickymkdir 的-m可以直接带上前导的特殊位。setgid2xxx用在目录上效果是在这个目录里新建的文件和子目录属组自动继承父目录的属组而不是创建者的主属组。做团队共享目录必备。mkdir -m 2775 /srv/share chgrp devs /srv/share之后任何devs组成员往这个目录里放文件文件属组都是devs不需要每个人手动chgrp。sticky1xxx用在目录上效果是只有文件属主、目录属主或 root 才能删除、重命名该目录里的条目。系统的/tmp就是1777。mkdir -m 1777 /srv/upload这两个位在ls -ld里的显示方式不一样setgid 会显示为drwxrwsr-x属组的 x 位变成 ssticky 显示为drwxrwxrwtother 的 x 位变成 t。大写的 S 或 T 表示位置有但执行位没开属于一种权限配置错误看到就要留意。4. 从单条命令到脚本化实操全过程4.1 单条命令的完整验证流程我建议每次调整目录结构时都走一遍这个验证流程花不了一分钟但能省掉后面几十分钟的排查。第一步确认目标位置和当前身份whoami pwd ls -ld /srv第二步用-pv创建看清每一条动作mkdir -pv /srv/app/{conf,logs,data,tmp}第三步立刻核验权限和属组ls -ld /srv/app /srv/app/* stat -c %A %U:%G %n /srv/app /srv/app/*stat这个命令比ls -l更适合脚本化核验因为它的输出格式可以精确控制%A是符号权限%a是八进制权限%U:%G是属主属组。第四步做一次可写性探测别只信权限数字touch /srv/app/tmp/.probe rm /srv/app/tmp/.probe echo OK这一步看起来很土但在面对 ACL、SELinux、只读挂载、NFS 的 root_squash 这些权限数字之外的因素时它才是真正能证明目录可用的方式。我遇到过权限显示 777 但一写就报Permission denied的情况最后查出来是文件系统挂载选项里带了只读参数光看权限位根本看不出来。第五步确认磁盘空间避免建完目录写不进去df -h /srv4.2 批量创建目录结构的几种写法手工一条条敲是低效的我常用的有三种写法。大括号展开最简洁适合层级规整的结构mkdir -p /srv/app/{conf,logs,data}/{in,out}这会建出conf/in、conf/out、logs/in、logs/out等共 8 个末级目录。大括号展开是 shell 的行为不是 mkdir 的功能所以在大括号里不能有空格除非加引号写错了会得到一个字面量目录名。循环写法适合名字列表来自外部的情况for d in web01 web02 web03; do mkdir -p /data/logs/$d/{access,error} done从文件读取适合目录列表由别人维护while IFS read -r dir; do [ -n $dir ] mkdir -p -- $dir done dirlist.txt这里三个细节必须做到位IFS防止行首行尾空白被吃掉-r防止反斜杠被解释--防止目录名以横杠开头被当成选项。这三条是我在批量处理用户输入的目录列表时被坑出来的。再补一个我常用的组合先 dry-run 打印确认无误再去掉 echo 执行。while IFS read -r dir; do echo mkdir -p -- $dir done dirlist.txt4.3 目录名带空格、横杠和特殊字符怎么办这是新手最容易翻车的地方分三种情况处理。带空格不加引号的话mkdir my dir会被当成两个参数建出my和dir两个目录。mkdir my dir # 正确 mkdir my\ dir # 也正确反斜杠转义以横杠开头mkdir -rf会被解析成-r和-f选项虽然 mkdir 没有这两个选项会报错但如果是mkdir -p这样命名就很危险。mkdir -- -rf # 用 -- 结束选项解析 mkdir ./-rf # 用相对路径前缀 mkdir /tmp/-rf # 用绝对路径前缀包含$、*、?、[、]等 shell 元字符必须用单引号包裹避免被 shell 展开。mkdir file[1] mkdir $HOME如果你要建的目录名来自外部数据我强烈建议用单引号而不是双引号因为双引号里$、反引号、反斜杠依然有特殊含义。写脚本时用printf %q输出转义后的名字可以帮你提前发现隐患namea b$c printf %q\n $name # 输出 a\ b\$c一眼看出哪些字符需要处理4.4 把 mkdir 封装成可复用的函数我在自己的脚本库里放了一个ensure_dir函数用到现在没出过事故ensure_dir() { local path$1 local mode${2:-750} if [ -d $path ]; then return 0 fi if [ -e $path ]; then echo ERROR: $path exists but is not a directory 2 return 1 fi mkdir -p -- $path || { echo ERROR: mkdir $path failed 2; return 1; } chmod $mode -- $path || { echo ERROR: chmod $path failed 2; return 1; } }这个函数做了四件普通mkdir -p不做的事区分已存在且是目录和已存在但不是目录把权限设置从-m拆到chmod行为更可预期用--保护奇怪路径每一步失败都有明确的错误信息。十行代码换来的是整套脚本的可维护性。调用方式ensure_dir /srv/app/logs 750 || exit 1 ensure_dir /srv/app/tmp 1777 || exit 15. 跨语言对照C 和 Java 里的目录创建5.1 C 里的 mkdir 与系统调用的差异命令行mkdir是工具C 里调的是系统调用包装名字一样但行为边界不同。类 Unix 平台的声明在sys/stat.h#include sys/stat.h #include sys/types.h int mkdir(const char* pathname, mode_t mode);返回 0 表示成功-1 表示失败失败原因要看errno。几个必须知道的点它只创建最后一级目录。mkdir(a/b, 0755)在a不存在时返回 -1errno是ENOENT。想要递归效果只能自己写循环从根往下逐级检查逐级创建。这就是为什么很多 C 项目会自己写一个mkdir_p()函数。mode 参数同样受 umask 影响。你传 0777实际得到的是0777 ~umask。想要精确权限得在创建后调chmod()。没有-p的幂等性。目标已存在时返回 -1errno是EEXIST。所以工程代码里判断目录是否存在的惯用写法是struct stat st; if (stat(path, st) ! 0) { if (mkdir(path, 0755) ! 0 errno ! EEXIST) { // 真正的错误需要处理 return -1; } }注意errno ! EEXIST这个兜底它处理的是两次 stat 之间目录被别的进程创建了的竞态情况。这个细节在多进程环境里非常关键。Windows 平台完全不同。Visual C 里的函数是_mkdir(const char*)在direct.h里没有 mode 参数权限由 ACL 控制。这就是为什么跨平台项目里我在 Linux 上测好的权限代码搬到 Windows 编译就报参数数量不对。正确做法是用条件编译隔开#ifdef _WIN32 #include direct.h #define MKDIR(path) _mkdir(path) #else #include sys/stat.h #include sys/types.h #define MKDIR(path) mkdir(path, 0755) #endifC17 之后有了标准库的filesystemstd::filesystem::create_directory()建单级create_directories()建多级等价-p。它把权限问题抽象掉了多级创建时用的权限受实现和 umask 影响需要精确控制依然得自己补permissions()。如果你的项目能上 C17我建议优先用它跨平台问题少一大半。5.2 Java 里 mkdir 和 mkdirs 的区别Java 这边最经典的坑就是这俩方法差一个字母行为完全不同。File dir new File(/srv/app/logs); boolean ok1 dir.mkdir(); // 只建一级父目录必须存在 boolean ok2 dir.mkdirs(); // 递归建多级等价于 mkdir -p关键点它们都返回 boolean不抛异常。目录已存在时返回false父目录不存在时返回false权限不足时也返回false磁盘满还是返回false。你从返回值里区分不出到底是哪种情况。这是 Java 老 IO API 被吐槽最多的地方之一。想在失败时知道原因只能自己额外探测File dir new File(/srv/app/logs); if (!dir.exists()) { if (!dir.mkdirs()) { if (dir.exists()) { // 竞态别的进程抢先建好了 } else { throw new IOException(failed to create dir.getAbsolutePath()); } } }NIO 的写法好得多Path path Paths.get(/srv/app/logs); Files.createDirectories(path); // 多级已存在不报错 Files.createDirectory(path); // 单级已存在抛 FileAlreadyExistsException这两个方法失败时会抛IOException异常消息里直接带路径和原因AccessDeniedException、FileAlreadyExistsException、NotDirectoryException等排查成本低很多。Java 设置目录权限的方式要注意平台差异。File.setWritable()这类方法粒度太粗精确控制要靠 POSIX 视图SetPosixFilePermission perms PosixFilePermissions.fromString(rwxr-x---); Files.setPosixFilePermissions(path, perms);在 Windows 上这会抛UnsupportedOperationException所以生产代码里一般要判断FileSystems.getDefault().supportedFileAttributeViews().contains(posix)再走这条路。Java 自身没有创建目录时指定权限的参数只能先建后设这一点和 C 的mkdir(path, mode)有本质区别。5.3 跨平台代码里最常见的三个坑坑一以为返回值 false 代表创建失败。在并发环境下mkdir返回 false 但目录实际已经存在是正常且正确的状态。只判断返回值不判断exists()会导致程序在并发启动时随机报错。坑二路径分隔符硬编码。/srv/app/logs在 Java 里能跑但拿到 Windows 上就变成盘符根目录下的路径。用File.separator或直接上Paths.get(srv, app, logs)才是稳妥做法。坑三假设多级创建能一次性完成权限设置。命令行这边-m对中间目录不可靠Java 和 C 这边则根本没有这个能力。我在项目里的统一做法是创建阶段只负责把目录建出来权限阶段单独走一遍按需要的目录列表逐个设置。两件事分开出问题的时候能立刻定位到是哪一段。6. 常见问题与排查技巧实录6.1 报错信息速查表下面这张表是我这些年一条条攒出来的基本覆盖了 mkdir 会遇到的绝大部分报错。看到报错先查这张表能省大量搜索时间。报错信息退出码根本原因处理方式File exists1目标已存在且未加-p加-p或先判断[ -d ]No such file or directory1父目录不存在且未加-p加-p或先建父目录Permission denied1父目录对当前用户无w或无x检查父目录权限、属主、ACLNot a directory1路径中某个中间项是普通文件改名或删除冲突文件Read-only file system1目标文件系统以只读方式挂载检查挂载参数重新挂载Argument list too long1一次传的目录名太多改用循环批量处理Invalid argument1目标文件系统不支持-m某些位去掉特殊权限位或换位置Too many levels of symbolic links1路径中有循环软链用readlink -f定位6.2 权限被拒的完整排查链路Permission denied是最常见也最容易查错方向的报错。我按这个顺序排查基本三分钟内能定位。第一步确认当前身份。whoami和id。很多人在容器里以为自己是在宿主机上有管理员权限的用户其实容器里跑的是非特权用户。第二步从根往下逐级检查路径上每一层的权限。只看最终要建的那个目录的父目录是不够的路径上任何一层缺x你都会走不下去。namei -l /srv/app/logsnamei -l会按层级把每一段的权限和属主都列出来这是定位这类问题最省事的工具比手工一层层ls -ld快得多。第三步看文件系统是不是只读。mount | grep /srv findmnt -T /srv/app第四步看 ACL 和扩展属性。普通权限位看着没问题但就是写不进去多半是 ACL 在起作用。getfacl /srv/app lsattr -d /srv/applsattr输出里出现i表示文件被设置了不可变属性出现a表示只允许追加。这两个属性连 root 都绕不过去需要先chattr -i解除是那种权限全对但你动不了的典型情况。第五步看 SELinux 或同类安全模块的状态。getenforce ls -Z /srv/app如果是 Enforcing且新目录的上下文类型不对同样会报权限被拒。6.3 目录名诡异问题的排查有几类问题不是权限而是名字本身有问题。目录建出来只有一半。mkdir a b c想建一个名字带空格的目录结果建出三个。用ls -b或ls -Q看真实名字ls -b # 用转义形式显示 ls -Q # 用双引号包住名字目录名末尾有不可见字符。粘贴路径时经常带上尾随空格或制表符肉眼完全看不出来。用find配合cat -A检查find . -maxdepth 1 -name * -print0 | xargs -0 -I{} sh -c printf %s {} | cat -A; echo目录里文件名显示乱码。这种情况通常和终端编码、压缩包解压时的编码推断有关。处理思路是统一环境变量LANG和LC_ALL在解压时明确指定编码而不是事后重命名。我一般会在解压前先unzip -l或tar -tf看一眼文件名是否正常确认无误再动。大小写敏感问题。Linux 默认区分大小写Data和data是两个不同目录。如果从 Windows 或 macOS 迁移过来很容易建出看起来重名的目录。要检查的话find /srv -maxdepth 1 -type d | sort -f | uniq -Di这个命令用忽略大小写的方式排序后找重复能快速发现这类问题。6.4 几条我真心建议记住的避坑技巧第一条别在脚本里用 777 图省事。我给这个建议不是出于洁癖。777 意味着任何用户都能在你的目录里创建、替换、删除文件配合定时任务或者服务进程很容易变成一条提权路径。同样的效果完全可以用 775 加正确的属组实现。真需要所有人都能写就把 sticky 位加上1777至少能防止互相删文件。第二条路径务必加引号并且优先用单引号。这条规则不区分使用场景脚本里、命令行里都适用。我见过太多因为路径里有空格导致rm -rf删错东西的例子而这一切的源头就是变量展开时没加引号。第三条破坏性的操作先 echo 一遍。批量建目录本身不危险但同一批脚本里往往同时存在rm -rf。我的习惯是任何批量操作先跑 dry-run确认输出符合预期再把echo删掉执行。第四条-p和-m同时用时权限一定要单独核验。不要相信我写了-m就一定对了。部署脚本里加一行stat -c %a %n打印结果成本极低收益极高。我现在的部署脚本里都会带这么一段check_mode() { local path$1 expect$2 local actual actual$(stat -c %a -- $path) if [ $actual ! $expect ]; then echo MODE MISMATCH: $path expect$expect actual$actual 2 return 1 fi }第五条容器镜像里建目录要留意层和用户。Dockerfile 里RUN mkdir -p /app/data会用构建时的 root 用户创建之后USER appuser切过去如果权限没放开运行时就写不进去。解决办法是在同一条RUN里把chown一起做完或者直接用COPY --chown。第六条跨机器部署要显式控制 umask。如果你依赖默认 umask 得到 755在别人umask是 077 的机器上就会得到 700服务起不来还找不到原因。在脚本开头加一句umask 022明确告诉大家这一段我依赖这个值比隐式依赖强太多。第七条注意磁盘 inode 而不是只看空间。大批量小目录的场景下df -h显示还有空间但mkdir报No space left on device这时候要看df -i。inode 用尽在日志归档、缓存目录这类场景里很常见。第八条目录创建成功后立刻写一个探测文件并删除。前面提过权限数字、ACL、只读挂载、SELinux 上下文中任意一个不对都会导致目录看起来正常但写不进去。touch加rm这两个动作是最廉价的有效性验证。注意给高并发服务创建目录时如果多个进程会同时执行mkdir -p优先用系统调用层的EEXIST兜底逻辑或者干脆在服务启动前由统一的初始化脚本完成目录创建。让几千个进程同时去抢建同一个目录即使不出错也会在启动瞬间制造不必要的文件系统压力。最后再提一个我在实际项目里养成的习惯把项目需要创建的目录列表单独放在一个配置文件里由初始化脚本读入后统一创建和设置权限。这样做的好处是目录结构变成了可审计、可版本控制的一等公民而不是散落在各个脚本里的几十行mkdir。新人接手的时候看一眼这个文件就知道整套系统的目录骨架长什么样比在代码里翻来覆去地搜 mkdir 舒服得多。
返回列表