面向 Linux 运维/SRE 的文件写入操作参考手册。既讲每条命令怎么用,也讲它背后的机制(为什么这么行为),配真实输出示例与边界情况,最后给出常见陷阱速查表与标准动作模板。
本人博客网站:https://blog.lonelybear.cn
目录
- 总览:运维"写文件"的全景
- [Shell 重定向:机制与基础符号](#Shell 重定向:机制与基础符号)
- [重定向顺序的坑:为什么
2>&1 > f不行](#重定向顺序的坑:为什么 2>&1 > f 不行) - [文件描述符与
exec:持久打开、交换、关闭](#文件描述符与 exec:持久打开、交换、关闭) - [防止误覆盖:
noclobber与>|](#防止误覆盖:noclobber 与 >|) - [Here 文档与 Here 字符串](#Here 文档与 Here 字符串)
tee:输出分流、多副本、进程替换- [文本写入命令:
echo/printf/cat](#文本写入命令:echo / printf / cat) - [
sed -i原地编辑(脚本化改配置主力)](#sed -i 原地编辑(脚本化改配置主力)) - [
awk的写入能力](#awk 的写入能力) - [运维头号陷阱:
sudo与重定向](#运维头号陷阱:sudo 与重定向) - [临时文件:
mktemp与/tmp的真相](#临时文件:mktemp 与 /tmp 的真相) - [大文件写入:
dd/fallocate/truncate](#大文件写入:dd / fallocate / truncate) - [同步与一致性:
sync/fsync/fsfreeze](#同步与一致性:sync / fsync / fsfreeze) - [特殊文件写入:
/proc/sys/dev/dev/tcp](#特殊文件写入:/proc /sys /dev /dev/tcp) - 权限、属主与安全上下文
- [日志写入与轮转(logrotate / journald)](#日志写入与轮转(logrotate / journald))
- [systemd unit 写入与生效](#systemd unit 写入与生效)
- [sysctl 内核参数写入与持久化](#sysctl 内核参数写入与持久化)
- [远程与网络写入(scp / rsync / ssh / curl)](#远程与网络写入(scp / rsync / ssh / curl))
- [写入性能与调度(ionice / nice / dirty_ratio)](#写入性能与调度(ionice / nice / dirty_ratio))
- 监控与诊断:谁在写、写了多少、卡在哪
- 常见陷阱速查表
- 运维写文件的标准动作模板
- 一页纸总纲
- 附录:命令速查卡
一、总览:运维"写文件"的全景
运维里"写文件"几乎都落在下面几类动作上:
| 类别 | 典型手段 | 典型场景 |
|---|---|---|
| 重定向输出 | > >> 2>&1 |
把命令输出落盘、静默 |
| 内容生成 | echo printf cat <<EOF |
写入一段确定内容/配置 |
| 原地编辑 | vim sed -i |
改一行/批量替换 |
| 复制/搬运 | cp rsync install dd |
文件复制、模板下发、烧盘 |
| 流分流 | tee |
既看又存、多副本写入 |
| 受保护文件 | sudo tee sudo bash -c |
写到 root-only 路径 |
| 特殊文件 | /proc/ /sys/ /dev/ |
内核参数、设备、网络 |
| 远程写入 | scp rsync ssh curl -T |
跨机写入 |
理解运维写文件,有一条贯穿始终的主线:
运维写文件 = 选择正确的工具 + 选对权限 + 防止误覆盖 + 必要时同步落盘 + 改完让它生效。
后面每一节都是围绕这条主线展开。先从最频繁的"重定向"讲起,因为它是理解其它一切的基础。
二、Shell 重定向:机制与基础符号
1. 先理解"文件描述符(fd)"
在 Unix 里,每个进程有一张打开文件表,表项就是文件描述符(fd),是非负整数。默认三个:
| fd | 名称 | 默认指向 |
|---|---|---|
| 0 | stdin(标准输入) | 终端键盘 |
| 1 | stdout(标准输出) | 终端屏幕 |
| 2 | stderr(标准错误) | 终端屏幕 |
其它 3+ 都是进程自己额外打开的。重定向的本质就是:在启动命令前,由 shell 用 open() 打开目标文件,把它的 fd 复制(dup2)到 0/1/2 上 。所以重定向是 shell 行为,不是被运行命令的行为------这是后面 sudo echo > file 失败的根因。
2. 基本符号
bash
cmd > file # 覆盖写:O_WRONLY|O_CREAT|O_TRUNC,清空后写
cmd >> file # 追加写:O_WRONLY|O_CREAT|O_APPEND
cmd 2> file # 只重定向 stderr
cmd 2>> file # 追加 stderr
cmd > file 2>&1 # stdout + stderr 都进 file(顺序很重要,见下节)
cmd &> file # bash 4+ 简写:stdout+stderr → file
cmd &>> file # bash 4+:都追加
cmd > /dev/null 2>&1 # 全静默,运维脚本最常用
3. > 覆盖 vs >> 追加的底层差别
| 写法 | open 标志 | 行为 |
|---|---|---|
> |
O_TRUNC |
打开时把文件截断为 0 字节再写 |
>> |
O_APPEND |
每次写前内核原子地把偏移移到末尾 |
>> 的"原子追加"很关键:多个进程并发 >> 同一文件,每次 write 是不会被交错撕开的(前提是 O_APPEND 且单次 write 在管道缓冲上限 PIPE_BUF 之内,POSIX 保证原子)。这也是日志多进程追加写用 >> 安全的原因。
4. 关闭 fd / 黑洞
bash
cmd 2>&- # 关闭 stderr(不推荐,有些程序会崩)
cmd > /dev/null # stdout 进黑洞
cmd > /dev/null 2>&1 # 全静默
/dev/null 是个特殊字符设备,写入永远成功且数据丢弃、读取立即 EOF。
三、重定向顺序的坑:为什么 2>&1 > f 不行
这是运维面试题与事故高发点,必须吃透。
机制
重定向是从左到右逐个处理的,每一步都会修改 fd 表:
bash
cmd > file 2>&1 # ✔ 正确
执行顺序:
> file:shell 以 O_TRUNC 打开 file,拿到一个新 fd(设为 N),再dup2(N, 1),于是 fd1 现在指向 file。2>&1:把 fd2 复制为 fd1 当前指向的目标。fd1 此刻指向 file,所以 fd2 也指向 file。- 结果:stdout 和 stderr 都进了 file。✓
bash
cmd 2>&1 > file # ✗ 错误
执行顺序:
2>&1:把 fd2 复制为 fd1 当前的目标。此刻 fd1 还指向终端,所以 fd2 也指向终端。> file:fd1 改指向 file。- 结果:stderr 留在终端,只有 stdout 进 file。
记忆口诀
永远先写
>再写2>&1;或者直接用 bash 的&>/&>>,语义最清晰、不会出错。
真实对照
bash
$ ls /不存在的 /tmp >/dev/null 2>&1 # 全静默,无输出
$ ls /不存在的 /tmp 2>&1 >/dev/null # stderr 还在屏幕:
ls: cannot access '/不存在的': No such file or directory
四、文件描述符与 exec:持久打开、交换、关闭
exec 不带命令名时,它做的 fd 操作会作用于当前 shell 本身并持续生效(直到显式关闭或 shell 退出)。
1. 持久打开一个日志文件
bash
exec 3>> /var/log/myapp.log # 当前 shell 打开 fd3 用于追加写
echo "$(date) start" >&3
echo "$(date) step1" >&3
echo "$(date) end" >&3
exec 3>&- # 用完关闭
机制:fd3 指向那个打开的文件对象。后续每条 echo ... >&3 都复用它,不必每次重新 open------减少系统调用、避免多次重打开导致的缓冲重置。
2. 交换 stdout 与 stderr
bash
exec 3>&1 4>&2 # 先保存原 fd1/fd2 到 fd3/fd4
exec 1>> /tmp/out.log # stdout 改去文件
exec 2>> /tmp/err.log # stderr 改去文件
# ... 一堆命令的输出都重定向了 ...
exec 1>&3 3>&- # 恢复 stdout,关 fd3
exec 2>&4 4>&- # 恢复 stderr,关 fd4
3. 同时读写一个文件(<>)
bash
exec 3<> /etc/hosts # 以读写方式打开,偏移在 0
read line <&3 # 从 fd3 读一行
echo "appended" >&3 # 追加写
exec 3<&-
适合对同一文件做"读一点、再写一点"的小工具,但要小心偏移位置。
4. 把整个脚本输出重定向
bash
#!/bin/bash
exec >> /var/log/myscript.log 2>&1
# 此后该脚本所有 echo/命令的 stdout+stderr 都追加到这个日志
echo "started at $(date)"
这是写"自身就记日志"的运维脚本的惯用法。
五、防止误覆盖:noclobber 与 >|
默认 > 对已存在文件会直接清空。运维里"误把 > 写成 >> 或反之"酿成事故屡见不鲜。shell 提供了一道保险:
bash
set -o noclobber # 开启:已存在的文件禁止用 > 覆盖
: > existing # 报错: bash: existing: cannot overwrite existing file
>| existing # 强制覆盖(忽略 noclobber)
set +o noclobber # 关闭
机制:noclobber 让 shell 在用 > 打开时改用不带 O_TRUNC 的逻辑,若文件已存在就拒绝;>| 是"绕过开关"。
实践提示:脚本里更稳的保险是动手前先备份:
cp -a file file.bak.$(date +%F-%H%M)。noclobber只挡>自身,挡不住cp、dd、程序内部open(O_TRUNC)。
六、Here 文档与 Here 字符串
1. Here 文档(heredoc):写多段内容的标配
bash
# 变量会展开 ------ 适合把变量塞进配置
cat > /etc/myapp.conf <<EOF
DB_HOST=$DB_HOST
PORT=5432
LOG_DIR=/var/log/myapp
EOF
# 用引号包裹结束符 ------ 变量不展开,字面写入(写脚本/模板时常用)
cat > /root/bootstrap.sh <<'EOF'
#!/bin/bash
echo $HOME # 这里 $HOME 是字面字符串 "$HOME",不会展开
EOF
机制:
<<EOF(无引号):heredoc 内容在交给命令前会经历一次变量/命令替换展开。<<'EOF'(有引号):内容被当作字面字符串,不做任何展开。写"会被人再执行"的脚本时强烈建议这种,避免$、反引号被提前展开。<<-EOF:前导 Tab(不是空格)会被剥离,便于缩进排版;结束符仍必须顶格。
2. 结束符的坑
结束符必须独占一行、顶格、行尾无空格 。否则不识别,会把 EOF 当正文写进文件:
bash
cat > bad.txt <<EOF
hello
EOF # ← 前面有空格,不识别
EOF # ← 真正的结束符在这,但前面那行已经当成正文了
诊断:写出来的文件里能看到字面的 EOF。
3. Here 字符串:<<<
bash
grep foo <<< "$myvar" # 把变量当 stdin 喂给命令,自带换行
sort <<< "$(cmd)"
适合把一个字符串(而非文件)喂给读 stdin 的命令,省去 printf | cmd 的管道。
4. 配合 sudo 与 tee 写 root 文件(后面详解)
bash
sudo tee /etc/myapp.conf > /dev/null <<'EOF'
[myapp]
db_host=127.0.0.1
EOF
七、tee:输出分流、多副本、进程替换
tee 从 stdin 读取,同时写到 stdout 和给定文件。它在运维里几乎是"既要实时看输出又要留底"的唯一选择。
1. 基本用法
bash
cmd | tee file # 屏幕显示 + 写 file(覆盖)
cmd | tee -a file # 追加
cmd | tee f1 f2 f3 # 同时写多个文件
cmd | tee -a f1 -a f2 # 多文件追加
-a 等价于 >> 行为;不带 -a 等价于 >。
2. 既看又压缩存档
bash
make 2>&1 | tee build.log | gzip > build.log.gz # ✗ 写错了:gzip 只收 tee 的 stdout
make 2>&1 | tee build.log | gzip > build.log.gz # 同上,gzip 不再向屏幕输出
make 2>&1 | tee build.log # ✔ 只看+存
make 2>&1 | tee >(gzip > build.log.gz) # ✔ 用进程替换:一份原样进屏幕,一份压缩进文件
进程替换 >(...) 把一个"看起来像文件"的路径喂给 tee,tee 往里写就等于往那个子进程 stdin 写。
3. 多副本写入(配置下发)
bash
echo "nameserver 1.1.1.1" | tee /etc/resolv.conf /tmp/resolv.bak
4. exit code 注意
bash
set -o pipefail # 默认管道只看最后一个命令的退出码;开了 pipefail 才看全链
cmd | tee file # 若 cmd 失败,默认 tee 成功 → 整体返回 0;开了 pipefail 才能抓到
写健壮运维脚本时建议 set -o pipefail。
八、文本写入命令:echo / printf / cat
1. echo 的可移植性陷阱
bash
echo "line" >> file
echo -e "tab\there\n" # bash 的 -e 解析转义
坑 :echo 在不同 shell / 系统行为不一:
- bash 的 builtin
echo支持-e -n。 - dash(Debian
/bin/sh)不支持-e。 /bin/echo(coreutils)行为又不同。- 老式系统
echo -n可能输出字面-n。
写脚本统一用 printf,它是 POSIX、行为一致。
2. printf:推荐的脚本写入工具
bash
printf '%s\t%s\n' key value >> file
printf '%s\n' "${arr[@]}" >> file # 每个数组元素一行
printf 'host=%s port=%d\n' "$h" "$p" > cfg
printf 不自动加换行(除非格式串里有 \n),这点和 echo 相反,要注意。
3. cat 一次性写整段
bash
cat > file <<'EOF'
...字面内容...
EOF
适合写一段已知内容(配置、脚本、模板)。比 echo 一行行拼更清晰。
4. cat 拼接多文件
bash
cat header.txt body.txt footer.txt > page.txt
cat part-*.log > combined.log # 按文件名通配合并
注意通配的排序是字典序,part-1...part-10 会乱序(part-10 排在 part-2 前),需 part-01 起两位。
九、sed -i 原地编辑(脚本化改配置主力)
sed 默认输出到 stdout,不动原文件。-i 让它"原地"修改:实际机制是输出到临时文件,再 rename 替换原文件。
1. 常见用法
bash
sed -i 's/old/new/g' file # 全局替换
sed -i.bak 's/old/new/g' file # 留 .bak 备份(强烈建议!)
sed -i '/^#Port /c\Port 2222' sshd_config # 整行替换(匹配行换成后面内容)
sed -i '/^#DEBUG=/d' file # 删匹配行
sed -i '3i\newline content' file # 第3行前插入
sed -i '5a\appended' file # 第5行后追加
sed -i '/\[myapp\]/a enabled=true' file # 匹配行后追加
2. 跨平台坑(GNU vs BSD/macOS)
| 平台 | in-place 语法 |
|---|---|
| GNU sed(Linux) | sed -i 's/.../.../'(后缀可省) |
| BSD sed(macOS) | sed -i '' 's/.../.../'(必须给空字符串后缀) |
在 macOS 上跑 sed -i 's/.../.../' 会被当成 s 命令的"后缀"参数,报错。
跨平台统一写法 :sed -i.bak 's/.../.../'(给一个真后缀),两边都兼容,还自带备份。
3. 多条编辑一次性应用
bash
sed -i \
-e 's/^Port .*/Port 2222/' \
-e 's/^#PermitRootLogin .*/PermitRootLogin no/' \
-e '/^#Banner/d' \
/etc/ssh/sshd_config
-e 可以叠加多条命令,避免多次扫描。
4. 改完别忘验证 + 重载
bash
sshd -t # 语法检查,有错会报行号
systemctl reload sshd # 平滑重载
十、awk 的写入能力
awk 不只是处理,也能直接写文件:
bash
# 选列写新文件
awk '{print $1, $4}' access.log > out.tsv
# 内置重定向:把不同内容写到不同文件
awk '{print > ("part-" int(NR/1000) ".log")}' big.log # 每 1000 行分一个文件
awk '/ERROR/{print >> "err.txt"; next} {print >> "ok.txt"}' app.log
机制:awk 的 print > file / print >> file 会缓存并复用 已打开的文件句柄,直到脚本结束才统一关闭------比每行 echo | tee 高效得多,适合大文件分流。
注意:awk 的
>第一次写也会截断 目标文件;要追加用>>。
十一、运维头号陷阱:sudo 与重定向
现象
bash
sudo echo "x" > /etc/important # ✗ 失败:bash: /etc/important: Permission denied
机制:重定向是 shell 干的
命令行的处理顺序是:
- shell(当前用户身份)解析整行,先处理重定向;
>让 shell 用open()打开/etc/important------此时身份是当前普通用户,无权写;- 打开失败,整条命令还没轮到
sudo执行就失败了。
即便 sudo echo 会被提权,echo 本身根本没机会写文件------写文件的是 shell 的 open。
正确写法三选一
bash
# ① 推荐:tee 以 root 身份写,最常用、最清晰
echo "x" | sudo tee /etc/important > /dev/null
# ② dd 也行
echo "x" | sudo dd of=/etc/important
# ③ 整个 shell 提权(注意变量展开作用域)
sudo bash -c 'echo "x" > /etc/important'
写多行到 root 文件(最干净的写法)
bash
sudo tee /etc/myapp.conf > /dev/null <<'EOF'
[myapp]
db_host=127.0.0.1
db_port=5432
EOF
注意:
<<'EOF'(带引号)表示变量不展开。如果你希望变量(如$DB_HOST)在写入前被替换,去掉引号用<<EOF。
十二、临时文件:mktemp 与 /tmp 的真相
1. 为什么不能直接写 /tmp/myapp.tmp
/tmp是共享可写目录,任何用户都能创建文件 。直接用固定名myapp.tmp会遭遇:- 符号链接攻击 :攻击者先
ln -s /etc/passwd /tmp/myapp.tmp,你以 root 写它就写进了/etc/passwd。 - 竞争条件(TOCTOU):判断"不存在"到"创建"之间被插入。
- 符号链接攻击 :攻击者先
mktemp生成不可预测的随机名 并以O_CREAT|O_EXCL原子创建,从根上避免这两类问题。
2. 正确用法
bash
tmp=$(mktemp) # 安全的空文件(随机名,excl 创建)
tmpd=$(mktemp -d) # 安全的空目录
trap 'rm -rf "$tmpd"' EXIT # 脚本退出(含异常)清理
# 指定位置/后缀
mktemp -p /var/tmp
mktemp --suffix=.conf # 带 .conf 后缀(程序认扩展名时用)
mktemp myapp.XXXXXX # 模板,XXXXXX 被随机替换
3. /tmp 的真相:很多发行版它是 tmpfs(内存)
bash
findmnt /tmp # 看 TYPE 是否 tmpfs
在 systemd 默认配置的发行版(Fedora、Debian 11+/RHEL 9 等),/tmp 常常是 tmpfs(基于内存)。含义:
- 写进
/tmp的数据占内存,写大了会撑爆 RAM 或被 OOM。 - 重启即消失。
/var/tmp默认是磁盘上的持久临时区,大文件应放这里。
| 目录 | 典型性质 | 清理策略 |
|---|---|---|
/tmp |
常是 tmpfs(内存) | tmpfiles.d 按期清理 |
/var/tmp |
磁盘持久 | 周期更长才清 |
/dev/shm |
tmpfs,世界可写 | 共享内存,别当垃圾场 |
十三、大文件写入:dd / fallocate / truncate
1. dd:底层块写入工具
bash
# 写一个 1GB 测试文件
dd if=/dev/zero of=big.bin bs=1M count=1024 status=progress
# 直接落盘(测真实写性能,绕过 page cache)
dd if=/dev/zero of=x bs=1M count=1024 oflag=direct conv=fdatasync
# 用随机数据填充(测压缩/校验)
dd if=/dev/urandom of=x bs=4k count=1000 oflag=direct
# 烧 U 盘镜像(高危!目标搞错会毁盘)
dd if=image.iso of=/dev/sdb bs=4M conv=fsync status=progress
# 整盘克隆
dd if=/dev/sda of=/dev/sdb bs=4M conv=noerror,sync status=progress
2. 关键选项
| 选项 | 作用 |
|---|---|
bs= |
块大小;通常 1M/4M 吞吐好 |
count= |
只写 count 个块 |
oflag=direct |
绕过 page cache(O_DIRECT),测真实盘速 |
oflag=sync |
每次 I/O 都同步落盘 |
conv=fdatasync |
写完做一次 fdatasync 再返回 |
conv=fsync |
写完做 fsync(含元数据) |
conv=noerror,sync |
读错继续(磁盘克隆/取证用) |
status=progress |
显示进度 |
3. truncate vs fallocate(预分配)
bash
truncate -s 10G big.bin # 稀疏文件:文件表显示 10G,实际占 0 字节
fallocate -l 10G big.bin # 真实分配 10G 块(立即占满空间)
机制差别:
truncate调用ftruncate(),只改文件 inode 的 size,不分配物理块 。读未写区域返回零。ls -l看到 10G,du -h看到 0。fallocate调用fallocate(),真实预留物理块(多数文件系统支持),适合数据库/虚拟磁盘预先占位,避免运行中"边写边扩容"导致的碎片与停顿。
诊断:
bash
ls -lh big.bin # 看逻辑大小
du -h big.bin # 看实际占盘大小(稀疏文件这里远小于逻辑大小)
4. 危险与确认
dd of=/dev/sdX 目标写错盘会瞬间毁盘。运维操作前务必:
bash
lsblk -o NAME,SIZE,MODEL,SERIAL,WWN # 先看清盘符对应物理盘
十四、同步与一致性:sync / fsync / fsfreeze
1. sync 命令
bash
sync # 把系统所有脏页刷盘
sync /var/log/myapp.log # 同步该文件的数据+元数据(GNU coreutils)
sync -f /data # 同步该文件所在文件系统
sync -d /data/big.bin # 只同步该文件数据,不等元数据
机制:sync() 系统调用排队所有脏 buffer 并等待完成。改完配置/写完关键文件后、拔 U 盘前、停服前都建议来一下。
2. fsync 的运维含义
fsync(fd) 是等真正落盘的强保证(数据 + 必要元数据)。运维层面它对应:
sync FILE(coreutils):就是 fsync 那个文件。dd ... conv=fsync/fdatasync:在 dd 内部调 fsync/fdatasync。
铁律:
write/>>/cp成功只代表进了 page cache,不代表落盘。要"确保不丢"必须 sync 或 fsync。
3. fsfreeze:文件系统级冻结(快照一致性)
bash
fsfreeze --freeze /mnt/data # 冻结:阻塞写、把脏页刷盘、记一致点
# → 此刻做 LVM / 存储快照,快照处于一致状态
fsfreeze --unfreeze /mnt/data # 解冻:恢复写入
机制:freeze 让文件系统进入一个干净的事务点,所有 pending write 落盘,新的写被阻塞。配合存储层快照,得到一个可挂载、可 fsck 的"快照时刻"。是数据库/虚拟机在线备份的常用手段。
十五、特殊文件写入:/proc /sys /dev /dev/tcp
1. /proc/sys/ ------ 内核运行时参数(临时)
bash
echo 1 > /proc/sys/net/ipv4/ip_forward # 立即生效,重启失效
sysctl -w net.ipv4.ip_forward=1 # 等价写法
echo never > /sys/kernel/mm/transparent_hugepage/enabled
机制 :这些是内核暴露的"伪文件",写它们等于直接改内核运行时变量,立即生效但重启即失 。持久化要写到 /etc/sysctl.d/(见第十九节)。
写
/proc/sys没有"已落盘"概念,它就是内核变量;sync对它无意义。
2. /sys/(sysfs,设备/子系统属性)
bash
echo 1 > /sys/class/scsi_host/host0/scan # 触发 SCSI 扫盘
echo disk > /sys/class/leds/.../brightness # 点 LED
echo 1 > /sys/devices/system/cpu/cpu3/online # 下线一个 CPU
注意:/sys 下很多是只读的;可写的也往往有严格的取值范围(如 cat /sys/.../brightness 能看到 max_brightness),乱写可能直接 oops。
3. /dev/ 设备文件
bash
dd if=image.iso of=/dev/sdb bs=4M conv=fsync # 烧盘(高危)
echo "command" > /dev/ttyS0 # 串口写
mknod /dev/mydev c 240 0 # 造字符设备(很少手写)
4. /dev/tcp、/dev/udp ------ bash 内置的网络"伪文件"
bash
# 仅 bash 支持(dash/zsh 默认无)
exec 3<>/dev/tcp/1.2.3.4/80
printf 'GET / HTTP/1.0\r\nHost: x\r\n\r\n' >&3
cat <&3
exec 3<&-
机制:这些不是真文件,是 bash 在 open 时识别特殊路径,改用 socket(2)+connect(2)。可做无 curl 的简易探活/发包。注意:写入是 TCP 流,边界要自己保证。
十六、权限、属主与安全上下文
1. 默认权限与 umask
bash
umask 022 # 新建文件 644、目录 755(运维常见默认)
umask 077 # 新建文件 600、目录 700(密钥/配置场景)
umask 027 # 新建文件 640、目录 750(组可读)
机制:实际权限 = 模板权限(文件 666、目录 777)减 umask。umask 只影响之后新建的文件,不影响已存在文件。
2. 写完一步到位定权限属主
bash
install -m 0644 -o app -g app cfg.yaml /etc/myapp/ # 复制 + 定权限属主
install -d -m 0750 /var/lib/myapp # 建目录并定权限
比 cp + chmod + chown 三步清爽得多,且原子。
3. ACL:给特定用户额外权限
bash
setfacl -m u:monitor:r-- /var/log/app.log # 给 monitor 用户只读
setfacl -m g:ops:rw- /data/shared # 给 ops 组读写
getfacl /var/log/app.log # 查看
setfacl -x u:monitor /var/log/app.log # 删除某条
ACL 适合"不改变主属组、但要给某用户/组读写"的场景(传统 Unix 权限只有 owner/group/other 三档)。
4. SELinux 上下文(Enforcing 模式下写入常踩)
bash
ls -Z /var/www/html/index.html # 看上下文
cp /tmp/index.html /var/www/html/ # cp 来的文件可能 context 不对,被 SELinux 拦
restorecon -Rv /var/www/html # 按策略修正上下文
chcon -t httpd_sys_content_t /var/www/html/index.html # 临时指定(重跑 restorecon 会复位)
机制:SELinux 在传统 DAC(权限位)之上加一层 MAC(类型强制)。文件写不仅看权限位,还看"进程的域(domain)能否写该类型(type)"。运维里"明明权限对了却写不动",先 getenforce 看是不是 Enforcing,再 ls -Z 看 context。
5. 文件系统挂载只读
bash
mount | grep ' /data ' # 看 ro / rw
mount -o remount,rw /data # 临时改可写(操作完最好改回 ro)
写不动的排查顺序:权限位 → 属主 → ACL → SELinux/AppArmor → 挂载是否 ro → 磁盘满/只读错误。
十七、日志写入与轮转(logrotate / journald)
1. 应用写日志的两类模式
- 直写文件 :应用自己
open(/var/log/app.log)写。简单,但需 logrotate 配合,且进程要支持"重开日志"。 - 走 syslog/journald :经
/dev/logunix socket 发给 rsyslog/systemd-journald,由它们统一落盘、轮转、转发。
2. logger 命令:从命令行写系统日志
bash
logger "deploy done" # 进 /var/log/messages 或 syslog
logger -t myapp -p local0.info "started" # 指定 facility/tag 与优先级
logger -n 192.168.1.10 "remote log" # 发到远程 syslog
3. logrotate 配置(对"直写文件"应用的续命)
conf
/var/log/myapp/*.log {
daily
rotate 14
compress
delaycompress # 最近一份不压缩,方便排查
missingok
notifempty
create 0644 app app # 轮转后新建文件
sharedscripts
postrotate
/bin/kill -HUP $(cat /var/run/myapp.pid 2>/dev/null) 2>/dev/null || true
endscript
}
核心机制:为什么必须 postrotate 发信号
轮转动作是:mv app.log app.log.1 → create 新 app.log。但应用进程仍持有旧的 fd,指向已改名为 .1 的文件 。于是应用继续写,实际写进了 app.log.1,而新 app.log 一直是空的。
诊断典型症状:"df 显示磁盘满,但 du 统计的日志目录很小" ------ 因为被删/改名但仍被持有的文件,inode 还没释放。
解决:在 postrotate 给进程发信号(常见 HUP/USR1),让它重新 open 日志文件,fd 就指向新文件了。前提是该应用实现了该信号的处理。copytruncate 是替代方案(不 mv,而是 cp 后 truncate 原文件),但它有丢日志窗口,且不更新 inode,某些场景不如 reopen 干净。
排查被删除但仍持有的文件
bash
lsof +L1 # 列出 link count < 1(已删但仍打开)的文件,看大小
# 或
lsof | grep deleted
找到持有进程后:发 reopen 信号,或重启该进程,空间立即释放。
4. journald
bash
journalctl -u myapp -f # 跟踪
journalctl --vacuum-size=500M # 控制占用
journalctl --vacuum-time=7d # 按时间清
# 持久化:改 /etc/systemd/journald.conf 的 Storage=persistent,并建 /var/log/journal 目录
mkdir -p /var/log/journal
systemctl restart systemd-journald
十八、systemd unit 写入与生效
1. 标准写入流程
bash
sudo tee /etc/systemd/system/myapp.service > /dev/null <<'EOF'
[Unit]
Description=My App
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/myapp
Restart=on-failure
User=app
Group=app
[Install]
WantedBy=multi-user.target
EOF
2. 生效链条(每一步都不能漏)
bash
sudo systemctl daemon-reload # ① 告诉 systemd 重新加载 unit 文件
sudo systemctl enable --now myapp # ② 开机自启 + 立即启动
systemctl status myapp # ③ 验证
journalctl -u myapp -f # ④ 看日志
高频坑:改了 unit 文件却不
daemon-reload,systemd 用的是内存里的旧版,改动"不生效"。这是运维日常的典型"我明明改了为啥没反应"。
3. 改 ExecStart 等需要重启
ExecStart/Type/User 这类只有重启服务才生效:systemctl restart myapp。环境变量、资源限制等也如是。
十九、sysctl 内核参数写入与持久化
1. 临时写(立即生效,重启失效)
bash
sysctl -w net.ipv4.ip_forward=1
# 等价于 echo 1 > /proc/sys/net/ipv4/ip_forward
2. 持久化写
bash
sudo tee /etc/sysctl.d/99-myapp.conf > /dev/null <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
net.core.somaxconn = 4096
vm.swappiness = 10
EOF
sudo sysctl --system # 重新加载 /etc/sysctl.d 与 /etc/sysctl.conf 全部配置
# 或只加载单个文件:
sudo sysctl -p /etc/sysctl.d/99-myapp.conf
3. 机制
/proc/sys/... 与 /etc/sysctl.d/ 的关系:
/proc/sys/X/Y/Z是内核运行时变量,写它=立即生效,重启即失。/etc/sysctl.d/*.conf是开机时由 systemd-sysctl 服务读取并写入/proc/sys的"持久化配方"。- 改完配方文件要
sysctl --system立即应用,否则要等下次开机。
高频坑:直接改
/proc/sys不写 conf 文件 → 重启丢参数;只写 conf 不sysctl -p/--system→ 当下不生效。两边都做才完整。
二十、远程与网络写入(scp / rsync / ssh / curl)
1. scp / rsync:运维搬文件主力
bash
scp -p cfg.yaml user@host:/etc/myapp/
rsync -avz --partial --checksum /data/ user@host:/data/
rsync 关键标志:
-a归档模式(保留权限/时间/软链)。--partial保留未传完的部分,断点续传。--delete删除目标多余的文件(让目标成为源的镜像,谨慎)。--checksum用校验而非 mtime+size 判断是否需传(更准更慢)。-e 'ssh -p 2222'指定 ssh 端口。
2. 经 ssh 把本地内容写到远程文件
bash
# 远程用户对目标路径有写权限
ssh user@host 'cat > /etc/myapp/conf' < local.conf
# 远程需要 sudo 才能写
ssh user@host 'sudo tee /etc/myapp/conf >/dev/null' < local.conf
机制:本地的 < 重定向把 local.conf 喂给 ssh 的 stdin;远程那条 cat >/tee 在远程 shell 里把 stdin 写到目标。整体是"本地读 + ssh 传输 + 远程写"。
3. HTTP 上传(curl)
bash
curl -T local.bin https://upload.example/dest/ # PUT 上传
curl -F file=@local.bin https://up.example/api # multipart 上传(表单)
curl -X POST -H 'Content-Type: application/json' -d '{"k":"v"}' https://api.example/x
二十一、写入性能与调度(ionice / nice / dirty_ratio)
1. 降低写入优先级,避免挤占业务 I/O
bash
ionice -c3 -n7 tar czf backup.tgz /data # idle 类,只在盘空闲时写
ionice -c2 -n7 rsync -a /data /backup/ # best-effort,低优先级
nice -n 19 ionice -c3 dd if=/dev/zero of=/backup/img bs=1M count=4096 # CPU+IO 双低
ionice 类:
-c1realtime(最高,root 才能给,危险)。-c2best-effort(默认),-n0..7调优先级。-c3idle(只在盘空闲时写)------备份、日志归档的好选择。
2. 调脏页回写策略
bash
sysctl vm.dirty_background_ratio # 默认 ~10,超过则后台异步回写
sysctl vm.dirty_ratio # 默认 ~20,超过则写者被强制同步回写(阻塞)
sysctl vm.dirty_expire_centisecs # 3000(脏页最多挂 30s)
sysctl vm.dirty_writeback_centisecs # 500(flusher 每 5s 醒一次)
调优方向:
- 调大 dirty_ratio → 写吞吐高、写者少阻塞,但掉电丢得多。
- 调小 → 更安全(丢得少),但写者更易在缓存满时被阻塞。
3. 挂载选项影响写
bash
mount -o remount,noatime /data # 关访问时间更新,减少写
# ext4 数据模式:data=ordered(默认)/journal/writeback
二十二、监控与诊断:谁在写、写了多少、卡在哪
1. 谁在打开/写某文件
bash
lsof | grep /var/log/myapp # 谁开着这个文件
lsof -p 1234 # 进程打开的所有文件
fuser -v /data/big.bin # 谁在用这个文件
2. 实时写盘速率
bash
iotop -o -a # 只看有 I/O 的进程,累计
iostat -xm 2 # 各设备写速率、await、%util
vmstat 2 # bo(块写出 KB/s)、wa(I/O 等待)
sar -d -p 1 # 历史磁盘写量
3. 文件变化监控
bash
inotifywait -m -r -e modify,create,delete /etc/myapp
# -m 持续,-r 递归,-e 指定事件
4. 追某进程的写入系统调用
bash
strace -e trace=write,openat,fsync -p 1234
strace -f -e trace=openat,write,fsync -p $(pidof myapp) 2>&1 | tee strace.log
ls -l /proc/1234/fd/ # 看 fd 实际指向哪个文件
readlink /proc/1234/fd/3
5. "删了日志磁盘还是满的"排查链
bash
df -h # 确认磁盘满
du -sh /var/log/* 2>/dev/null | sort -h | tail # 找大目录
lsof +L1 # 找被删但仍持有的文件(这里常有大块)
# 找到持有进程 → 发 reopen 信号 / kill,空间立即释放
二十三、常见陷阱速查表
| 现象 | 根因 | 解决 |
|---|---|---|
sudo echo x > /root/y 失败 |
重定向在当前 shell 执行,未提权 | `echo x |
>> 不追加反而覆盖 |
用成 > 或别处又 > |
核对脚本里的重定向符 |
heredoc 写出后 EOF 留在文件里 |
结束符前有空格 / 未顶格 | <<-EOF 且用 Tab;或结束符顶格 |
sed -i 在 macOS 报错 |
BSD sed 需要后缀参数 | sed -i.bak 's/.../.../' |
| 删日志但空间未释放 | 进程仍持有旧 fd | lsof +L1 找进程,reopen 或 kill |
| 写到 /tmp 系统变卡/OOM | /tmp 是 tmpfs(内存) | 大文件用 /var/tmp 或换盘 |
磁盘满但 df 有空间 |
inode 耗尽(海量小文件) | df -i,清理小文件 |
| 写到 root 保留区报错 | ext4 默认保留 5% 给 root | tune2fs -m 1 /dev/sdX(谨慎评估) |
| 普通用户写不动、权限对 | ACL / SELinux / 挂载 ro | getfacl、ls -Z、`mount |
| 配置改了不生效 | 只改 /proc(临时)或没 reload | 写 sysctl.d + sysctl --system;改 unit 后 daemon-reload |
| rsync 越传越慢 | 默认按 mtime+size 判断 | --checksum 或 --partial |
dd of=/dev/sdX 毁盘 |
目标盘符选错 | 先 lsblk -o NAME,SIZE,MODEL,SERIAL 确认 |
| `find ... | xargs cmd > out` 输出乱 | xargs 并发 + 重定向被 shell 截一次 |
tee 后命令失败没被抓 |
默认管道只看末位退出码 | set -o pipefail |
重定向到 /dev/tcp 报 No such file |
用了 dash/zsh(/bin/sh 非 bash) | 显式 #!/bin/bash |
truncate 后 ls 大、du 小 |
稀疏文件未占真实块 | 用 fallocate 真实预留 |
二十四、运维写文件的标准动作模板
1. 改配置(安全可回滚)
bash
# 备份
cp -a /etc/myapp.conf /etc/myapp.conf.bak.$(date +%F-%H%M)
# 编辑(vim 或 sed)
vim /etc/myapp.conf
# 或: sed -i.bak 's/^PORT=.*/PORT=2222/' /etc/myapp.conf
# 语法校验
myapp -t # 如 nginx -t / httpd -t / sshd -t
# 重载
systemctl reload myapp
# 留底
logger -t ops "$(hostname): changed /etc/myapp.conf"
2. 写一个 systemd unit 并启用
bash
sudo tee /etc/systemd/system/myapp.service > /dev/null <<'EOF'
[Unit]
Description=My App
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/myapp
Restart=on-failure
User=app
Group=app
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp
3. 持久化 sysctl
bash
sudo tee /etc/sysctl.d/99-forward.conf > /dev/null <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sudo sysctl --system
4. 条件性追加(避免重复行)
bash
grep -q '^PORT=2222' /etc/myapp.conf || echo 'PORT=2222' >> /etc/myapp.conf
5. 安全地写受保护多行配置
bash
sudo tee /etc/myapp/conf > /dev/null <<'EOF'
[myapp]
db_host=127.0.0.1
db_port=5432
EOF
二十五、一页纸总纲
运维写文件三件事:用对工具 (
>>>teecat<<EOFsed -iinstalldd)、用对权限 (sudo tee而非sudo echo >、install -m -o -g一步定权限、restorecon修 SELinux)、别让"写成功"骗了你 (sync/fsync落盘、改完reload/daemon-reload、日志轮转要 reopen、/proc改的是临时的)。任何"删/改/覆盖"前先
cp -a file file.bak.$(date +%F-%H%M);任何"写 root 路径"用| sudo tee;任何"写完要生效"记得 reload;任何"烧盘/dd"先lsblk确认目标。监控永远先
iotop+iostat -x+lsof +L1三件套。"删了日志磁盘还满"几乎一定是进程仍持有旧 fd,找出来 reopen 或 kill。重定向是 shell 干的,不是被运行命令干的------这条一句话解释了
sudo echo > file为什么失败,也解释了2>&1 > f为什么把 stderr 留在屏幕。
二十六、附录:命令速查卡
bash
# ── 重定向 ──
cmd > file # 覆盖写
cmd >> file # 追加
cmd > file 2>&1 # stdout+stderr 进 file
cmd &> file # 同上(bash)
cmd > /dev/null 2>&1 # 静默
# ── heredoc ──
cat > f <<'EOF' # 不展开变量
cat > f <<EOF # 展开变量
# ── tee ──
cmd | tee f # 看+写
cmd | tee -a f # 看+追加
cmd | tee f1 f2 # 多副本
# ── sudo 写受保护文件 ──
echo x | sudo tee /root/y >/dev/null
# ── 临时文件 ──
tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT
tmpd=$(mktemp -d); trap 'rm -rf "$tmpd"' EXIT
# ── 大文件 / 预分配 ──
dd if=/dev/zero of=x bs=1M count=1024 oflag=direct conv=fdatasync status=progress
fallocate -l 10G x # 真实预留
truncate -s 10G x # 稀疏
# ── 同步 ──
sync
sync /path/file
fsfreeze --freeze /mnt; fsfreeze --unfreeze /mnt
# ── 内核参数 ──
sysctl -w net.ipv4.ip_forward=1
sudo tee /etc/sysctl.d/99-x.conf >/dev/null <<'EOF'
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
# ── systemd ──
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
# ── 日志 ──
logger -t myapp "msg"
journalctl -u myapp -f
lsof +L1 # 找被删但仍持有的文件
# ── 监控 ──
iotop -o -a
iostat -xm 2
lsof | grep /path
strace -e openat,write,fsync -p PID
# ── 调度 ──
ionice -c3 -n7 cmd
nice -n 19 ionice -c3 cmd
文档面向 Linux 运维场景。命令实际行为以你所用发行版与 shell(bash/dash/zsh、GNU/BSD coreutils)为准------尤其 sed -i、echo -e、/dev/tcp 这几处跨平台差异最明显。
有错误的内容欢迎联系https://blog.lonelybear.cn/about/