Linux 运维文件写入操作

面向 Linux 运维/SRE 的文件写入操作参考手册。既讲每条命令怎么用,也讲它背后的机制(为什么这么行为),配真实输出示例与边界情况,最后给出常见陷阱速查表与标准动作模板。

本人博客网站:https://blog.lonelybear.cn


目录

  1. 总览:运维"写文件"的全景
  2. [Shell 重定向:机制与基础符号](#Shell 重定向:机制与基础符号)
  3. [重定向顺序的坑:为什么 2>&1 > f 不行](#重定向顺序的坑:为什么 2>&1 > f 不行)
  4. [文件描述符与 exec:持久打开、交换、关闭](#文件描述符与 exec:持久打开、交换、关闭)
  5. [防止误覆盖:noclobber>|](#防止误覆盖:noclobber 与 >|)
  6. [Here 文档与 Here 字符串](#Here 文档与 Here 字符串)
  7. tee:输出分流、多副本、进程替换
  8. [文本写入命令:echo / printf / cat](#文本写入命令:echo / printf / cat)
  9. [sed -i 原地编辑(脚本化改配置主力)](#sed -i 原地编辑(脚本化改配置主力))
  10. [awk 的写入能力](#awk 的写入能力)
  11. [运维头号陷阱:sudo 与重定向](#运维头号陷阱:sudo 与重定向)
  12. [临时文件:mktemp/tmp 的真相](#临时文件:mktemp 与 /tmp 的真相)
  13. [大文件写入:dd / fallocate / truncate](#大文件写入:dd / fallocate / truncate)
  14. [同步与一致性:sync / fsync / fsfreeze](#同步与一致性:sync / fsync / fsfreeze)
  15. [特殊文件写入:/proc /sys /dev /dev/tcp](#特殊文件写入:/proc /sys /dev /dev/tcp)
  16. 权限、属主与安全上下文
  17. [日志写入与轮转(logrotate / journald)](#日志写入与轮转(logrotate / journald))
  18. [systemd unit 写入与生效](#systemd unit 写入与生效)
  19. [sysctl 内核参数写入与持久化](#sysctl 内核参数写入与持久化)
  20. [远程与网络写入(scp / rsync / ssh / curl)](#远程与网络写入(scp / rsync / ssh / curl))
  21. [写入性能与调度(ionice / nice / dirty_ratio)](#写入性能与调度(ionice / nice / dirty_ratio))
  22. 监控与诊断:谁在写、写了多少、卡在哪
  23. 常见陷阱速查表
  24. 运维写文件的标准动作模板
  25. 一页纸总纲
  26. 附录:命令速查卡

一、总览:运维"写文件"的全景

运维里"写文件"几乎都落在下面几类动作上:

类别 典型手段 典型场景
重定向输出 > >> 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      # ✔ 正确

执行顺序:

  1. > file:shell 以 O_TRUNC 打开 file,拿到一个新 fd(设为 N),再 dup2(N, 1),于是 fd1 现在指向 file
  2. 2>&1:把 fd2 复制为 fd1 当前指向的目标。fd1 此刻指向 file,所以 fd2 也指向 file。
  3. 结果:stdout 和 stderr 都进了 file。✓
bash 复制代码
cmd 2>&1 > file      # ✗ 错误

执行顺序:

  1. 2>&1:把 fd2 复制为 fd1 当前的目标。此刻 fd1 还指向终端,所以 fd2 也指向终端。
  2. > file:fd1 改指向 file。
  3. 结果: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 只挡 > 自身,挡不住 cpdd、程序内部 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 干的

命令行的处理顺序是:

  1. shell(当前用户身份)解析整行,先处理重定向;
  2. > 让 shell 用 open() 打开 /etc/important------此时身份是当前普通用户,无权写;
  3. 打开失败,整条命令还没轮到 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/log unix 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.1create 新 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 类:

  • -c1 realtime(最高,root 才能给,危险)。
  • -c2 best-effort(默认),-n0..7 调优先级。
  • -c3 idle(只在盘空闲时写)------备份、日志归档的好选择。

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 getfaclls -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
truncatels 大、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

二十五、一页纸总纲

运维写文件三件事:用对工具 (> >> tee cat<<EOF sed -i install dd)、用对权限 (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 -iecho -e/dev/tcp 这几处跨平台差异最明显。

有错误的内容欢迎联系https://blog.lonelybear.cn/about/

相关推荐
城管不管1 小时前
重生——第十一次面试之挖财一面2026.8.19已OC
java·服务器·jvm·数据库·spring·面试·职场和发展
飞英思特无线充电2 小时前
严苛场景巡检机器人的无线补能设计:点位、BMS、认证与运维边界
运维·机器人
Jimmy_jimi2 小时前
利用nginx代理完成url重定位
运维·nginx
liuyicenysabel3 小时前
多服务上线日记三:
运维·服务器·笔记·学习
吠品4 小时前
Wine 在 Linux 上运行 Windows 软件完整指南
java·linux·服务器
一只鹿鹿鹿4 小时前
大数据中心安全系统解决方案(Word文件)
大数据·运维·物联网·安全·政务
深念Y6 小时前
从 Windows 迁移到 Linux 开发环境的记录
linux·windows·链接·bun·ram·imdisk
AuTumn-L6 小时前
使用 SSH 密钥认证实现安全免密登录
服务器·安全·ssh
领创工作室6 小时前
GitHub Workflows 全面解析:从核心概念到开源实现
运维·github