Linux 文件写入
Linux 文件写入
内容以 bash 手册、GNU coreutils 手册、各命令的 man 页等官方文档为准,并参考权威资料整理,力求准确、详尽。 命令行为以实际发行版与所用 shell(bash/dash/zsh、GNU/BSD coreutils)为准——尤其
sed -i、echo -e、/dev/tcp几处跨平台差异。
目录
- 总览:运维"写文件"的全景
- Shell 重定向:机制与基础符号
- 重定向顺序的坑:为什么 2>&1 > f 不行
- 防止误覆盖:noclobber 与 >|
- 文件描述符与 exec:持久打开、交换、关闭
- Here 文档与 Here 字符串
- tee:输出分流、多副本、进程替换
- 文本写入命令:echo / printf / cat
- sed -i 与 awk:脚本化编辑写入
- 运维头号陷阱:sudo 与重定向
- 临时文件:mktemp 与 /tmp 的真相
- 大文件写入与同步落盘:dd / fallocate / truncate / sync
- 特殊文件写入:/proc /sys /dev /dev/tcp
- 权限、属主与安全上下文
- 日志写入与轮转:logrotate / journald
- 让写入生效:systemd unit 与 sysctl
- 远程与网络写入
- 写入性能、调度与监控诊断
- 总结与速查
- 参考资料
总览:运维"写文件"的全景
运维里"写文件"几乎都落在下面几类动作上:
| 类别 | 典型手段 | 典型场景 |
|---|---|---|
| 重定向输出 | > >> 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.1 先理解"文件描述符(fd)"
在 Unix 里,每个进程有一张打开文件表,表项就是文件描述符(fd),是非负整数。默认三个:
| fd | 名称 | 默认指向 |
|---|---|---|
| 0 | stdin(标准输入) | 终端键盘 |
| 1 | stdout(标准输出) | 终端屏幕 |
| 2 | stderr(标准错误) | 终端屏幕 |
重定向的本质:在启动命令前,由 shell 用 open() 打开目标文件,把它的 fd 复制(dup2)到 0/1/2 上。所以重定向是 shell 行为,不是被运行命令的行为——这是后面 sudo echo > file 失败的根因。
1.2 基本符号
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 # 全静默,运维脚本最常用
1.3 > 覆盖 vs >> 追加的底层差别
| 写法 | open 标志 | 行为 |
|---|---|---|
> |
O_TRUNC |
打开时把文件截断为 0 字节再写 |
>> |
O_APPEND |
每次写前内核原子地把偏移移到末尾 |
>> 的"原子追加"很关键:多个进程并发 >> 同一文件,单次 write 在管道缓冲上限内不会被交错撕开,这也是日志多进程追加写用 >> 安全的原因。
1.4 关闭 fd / 黑洞
cmd 2>&- # 关闭 stderr(不推荐,有些程序会崩)
cmd > /dev/null # stdout 进黑洞
cmd > /dev/null 2>&1 # 全静默
/dev/null 是特殊字符设备,写入永远成功且数据丢弃、读取立即 EOF。
参考:bash 手册 - Redirections
二、重定向顺序的坑:为什么 2>&1 > f 不行
这是运维面试题与事故高发点,必须吃透。
2.1 机制:重定向是从左到右逐个处理
cmd > file 2>&1 # 正确
执行顺序:
> file:shell 以 O_TRUNC 打开 file,拿到一个新 fd,再dup2(新fd, 1),于是 fd1 指向 file。2>&1:把 fd2 复制为 fd1 当前指向的目标。fd1 此刻指向 file,所以 fd2 也指向 file。- 结果:stdout 和 stderr 都进了 file。
cmd 2>&1 > file # 错误
执行顺序:
2>&1:把 fd2 复制为 fd1 当前的目标。此刻 fd1 还指向终端,所以 fd2 也指向终端。> file:fd1 改指向 file。- 结果:stderr 留在终端,只有 stdout 进 file。
2.2 记忆口诀
永远先写
>再写2>&1;或者直接用 bash 的&>/&>>,语义最清晰、不会出错。
2.3 真实对照
$ ls /不存在的 /tmp >/dev/null 2>&1 # 全静默,无输出
$ ls /不存在的 /tmp 2>&1 >/dev/null # stderr 还在屏幕:
ls: cannot access '/不存在的': No such file or directory
三、防止误覆盖:noclobber 与 >|
默认 > 对已存在文件会直接清空。shell 提供一道保险:
set -o noclobber # 开启:已存在的文件禁止用 > 覆盖
: > 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)。
参考:bash 手册 - The Set Builtin(noclobber)
四、文件描述符与 exec:持久打开、交换、关闭
exec 不带命令名时,它对 fd 的操作会作用于当前 shell 本身并持续生效(直到显式关闭或 shell 退出)。
4.1 持久打开一个日志文件
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,减少系统调用。
4.2 交换 stdout 与 stderr
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
4.3 同时读写一个文件(<>)
exec 3<> /etc/hosts # 以读写方式打开,偏移在 0
read line <&3 # 从 fd3 读一行
echo "appended" >&3 # 追加写
exec 3<&-
4.4 把整个脚本输出重定向(写"自带日志"的脚本)
#!/bin/bash
exec >> /var/log/myscript.log 2>&1
# 此后该脚本所有 echo/命令的 stdout+stderr 都追加到这个日志
echo "started at $(date)"
五、Here 文档与 Here 字符串
5.1 Here 文档(heredoc):写多段内容的标配
# 变量会展开 —— 适合把变量塞进配置
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 是字面字符串,不会展开
EOF
机制:
<<EOF(无引号):heredoc 内容在交给命令前会经历一次变量/命令替换。- <<'EOF'(有引号):内容被当作字面字符串,不做任何展开。写"会被人再执行"的脚本时强烈建议用这种,避免
$、反引号被提前展开。 <<-EOF:前导 Tab 会被剥离,便于缩进排版;结束符仍必须顶格。
5.2 结束符的坑
结束符必须独占一行、顶格、行尾无空格,否则不识别,会把 EOF 当正文写进文件。诊断:写出来的文件里能看到字面的 EOF。
5.3 Here 字符串:<<<
grep foo <<< "$myvar" # 把变量当 stdin 喂给命令,自带换行
sort <<< "$(cmd)"
适合把一个字符串(而非文件)喂给读 stdin 的命令,省去 printf | cmd 的管道。
参考:bash 手册 - Here Documents
六、tee:输出分流、多副本、进程替换
tee 从 stdin 读取,同时写到 stdout 和给定文件。它在运维里几乎是"既要实时看输出又要留底"的唯一选择。
6.1 基本用法
cmd | tee file # 屏幕显示 + 写 file(覆盖)
cmd | tee -a file # 追加
cmd | tee f1 f2 f3 # 同时写多个文件
cmd | tee -a f1 -a f2 # 多文件追加
-a 等价于 >> 行为;不带 -a 等价于 >。
6.2 既看又压缩存档(进程替换)
make 2>&1 | tee build.log # 只看+存
make 2>&1 | tee >(gzip > build.log.gz) # 进程替换:一份原样进屏幕,一份压缩进文件
进程替换 >(...) 把一个"看起来像文件"的路径喂给 tee,tee 往里写就等于往那个子进程 stdin 写。
6.3 多副本写入(配置下发)
echo "nameserver 1.1.1.1" | tee /etc/resolv.conf /tmp/resolv.bak
6.4 exit code 注意
set -o pipefail # 默认管道只看最后一个命令的退出码;开了 pipefail 才看全链
cmd | tee file # 若 cmd 失败,默认 tee 成功 → 整体返回 0
写健壮运维脚本时建议 set -o pipefail。
参考:GNU coreutils - tee
七、文本写入命令:echo / printf / cat
7.1 echo 的可移植性陷阱
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、行为一致。
7.2 printf:推荐的脚本写入工具
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 相反,要注意。
7.3 cat 一次性写整段
cat > file <<'EOF'
# ...字面内容...
EOF
适合写一段已知内容(配置、脚本、模板),比 echo 一行行拼更清晰。
7.4 cat 拼接多文件
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 与 awk:脚本化编辑写入
8.1 sed -i 原地编辑(脚本化改配置主力)
sed 默认输出到 stdout,不动原文件。-i 让它"原地"修改:实际机制是输出到临时文件,再 rename 替换原文件。
常见用法:
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 # 匹配行后追加
8.2 sed -i 跨平台坑(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/.../.../'(给一个真后缀),两边都兼容,还自带备份。
8.3 多条编辑一次性应用
sed -i \
-e 's/^Port .*/Port 2222/' \
-e 's/^#PermitRootLogin .*/PermitRootLogin no/' \
-e '/^#Banner/d' \
/etc/ssh/sshd_config
-e 可以叠加多条命令,避免多次扫描。
8.4 改完别忘验证 + 重载
sshd -t # 语法检查,有错会报行号
systemctl reload sshd # 平滑重载
8.5 awk 的写入能力
# 选列写新文件
awk '{print $1, $4}' access.log > out.tsv
# 内置重定向:把不同内容写到不同文件
awk '/ERROR/{print >> "err.txt"; next} {print >> "ok.txt"}' app.log
机制:awk 的 print > file / print >> file 会缓存并复用已打开的文件句柄,直到脚本结束才统一关闭——适合大文件分流。
注意:awk 的
>第一次写也会截断目标文件;要追加用>>。
参考:GNU sed 手册、GNU awk 手册(
man awk)
九、运维头号陷阱:sudo 与重定向
9.1 现象
sudo echo "x" > /etc/important # 失败:bash: /etc/important: Permission denied
9.2 机制:重定向是 shell 干的
命令行的处理顺序是:
- shell(当前用户身份)解析整行,先处理重定向;
>让 shell 用open()打开/etc/important——此时身份是当前普通用户,无权写;- 打开失败,整条命令还没轮到
sudo执行就失败了。
即便 sudo echo 会被提权,echo 本身根本没机会写文件——写文件的是 shell 的 open。
9.3 正确写法三选一
# ① 推荐: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'
9.4 写多行到 root 文件(最干净的写法)
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 的真相
10.1 为什么不能直接写 /tmp/myapp.tmp
/tmp 是共享可写目录,任何用户都能创建文件。直接用固定名会遭遇:
- 符号链接攻击:攻击者先
ln -s /etc/passwd /tmp/myapp.tmp,你以 root 写它就写进了/etc/passwd。 - 竞争条件(TOCTOU):判断"不存在"到"创建"之间被插入。
mktemp 生成不可预测的随机名并以 O_CREAT|O_EXCL 原子创建,从根上避免这两类问题。
10.2 正确用法
tmp=$(mktemp) # 安全的空文件(随机名,excl 创建)
tmpd=$(mktemp -d) # 安全的空目录
trap 'rm -rf "$tmpd"' EXIT # 脚本退出(含异常)清理
# 指定位置/后缀
mktemp -p /var/tmp
mktemp --suffix=.conf # 带 .conf 后缀
mktemp myapp.XXXXXX # 模板,XXXXXX 被随机替换
10.3 /tmp 的真相:很多发行版它是 tmpfs(内存)
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,世界可写 | 共享内存,别当垃圾场 |
参考:GNU coreutils - mktemp
十一、大文件写入与同步落盘:dd / fallocate / truncate / sync
11.1 dd:底层块写入工具
# 写一个 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
# 烧 U 盘镜像(高危!目标搞错会毁盘)
dd if=image.iso of=/dev/sdb bs=4M conv=fsync status=progress
关键选项:bs= 块大小、count= 只写 count 个块、oflag=direct 绕过 page cache(O_DIRECT)、oflag=sync 每次 I/O 同步落盘、conv=fdatasync 写完做 fdatasync、conv=noerror,sync 读错继续(磁盘克隆用)、status=progress 显示进度。
11.2 truncate vs fallocate(预分配)
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(),真实预留物理块,适合数据库/虚拟磁盘预先占位,避免运行中"边写边扩容"导致的碎片与停顿。
11.3 sync / fsync:确保真正落盘
sync # 把系统所有脏页刷盘
sync /var/log/myapp.log # 同步该文件的数据+元数据(GNU coreutils)
sync -f /data # 同步该文件所在文件系统
sync()系统调用排队所有脏 buffer 并等待完成。- 铁律:
write/>>/cp成功只代表进了 page cache,不代表落盘。要"确保不丢"必须 sync 或 fsync。 fsync(fd)是等真正落盘的强保证(数据 + 必要元数据)。
11.4 fsfreeze:文件系统级冻结(快照一致性)
fsfreeze --freeze /mnt/data # 冻结:阻塞写、刷盘、记一致点
# → 此刻做 LVM / 存储快照,快照处于一致状态
fsfreeze --unfreeze /mnt/data # 解冻:恢复写入
freeze 让文件系统进入干净的事务点,所有 pending write 落盘、新写被阻塞。配合存储层快照,得到可挂载、可 fsck 的"快照时刻",是数据库/虚拟机在线备份的常用手段。
参考:GNU coreutils - sync
十二、特殊文件写入:/proc /sys /dev /dev/tcp
12.1 /proc/sys/:内核运行时参数(临时)
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对它无意义。
12.2 /sys/(sysfs,设备/子系统属性)
echo 1 > /sys/class/scsi_host/host0/scan # 触发 SCSI 扫盘
echo 1 > /sys/devices/system/cpu/cpu3/online # 下线一个 CPU
注意:/sys 下很多是只读的;可写的也往往有严格的取值范围,乱写可能直接 oops。
12.3 /dev/ 设备文件
dd if=image.iso of=/dev/sdb bs=4M conv=fsync # 烧盘(高危)
echo "command" > /dev/ttyS0 # 串口写
12.4 /dev/tcp、/dev/udp: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 流,边界要自己保证。
十三、权限、属主与安全上下文
13.1 默认权限与 umask
umask 022 # 新建文件 644、目录 755(运维常见默认)
umask 077 # 新建文件 600、目录 700(密钥/配置场景)
umask 027 # 新建文件 640、目录 750(组可读)
机制:实际权限 = 模板权限(文件 666、目录 777)减 umask。umask 只影响之后新建的文件,不影响已存在文件。
13.2 写完一步到位定权限属主(install)
install -m 0644 -o app -g app cfg.yaml /etc/myapp/ # 复制 + 定权限属主
install -d -m 0750 /var/lib/myapp # 建目录并定权限
比 cp + chmod + chown 三步清爽得多,且原子。
13.3 ACL:给特定用户额外权限
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 三档)。
13.4 SELinux 上下文(Enforcing 模式下写入常踩)
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 # 临时指定
机制:SELinux 在传统 DAC(权限位)之上加一层 MAC(类型强制)。文件写不仅看权限位,还看"进程的域能否写该类型"。运维里"明明权限对了却写不动",先 getenforce 看是不是 Enforcing,再 ls -Z 看 context。
13.5 文件系统挂载只读
mount | grep ' /data ' # 看 ro / rw
mount -o remount,rw /data # 临时改可写(操作完最好改回 ro)
写不动的排查顺序:权限位 → 属主 → ACL → SELinux/AppArmor → 挂载是否 ro → 磁盘满/只读错误。
十四、日志写入与轮转:logrotate / journald
14.1 应用写日志的两类模式
- 直写文件:应用自己 open
/var/log/app.log写。简单,但需 logrotate 配合,且进程要支持"重开日志"。 - 走 syslog/journald:经
/dev/logunix socket 发给 rsyslog/systemd-journald,由它们统一落盘、轮转、转发。
14.2 logger 命令:从命令行写系统日志
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
14.3 logrotate 配置(对"直写文件"应用的续命)
/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 是替代方案,但它有丢日志窗口。
排查被删除但仍持有的文件
lsof +L1 # 列出 link count < 1(已删但仍打开)的文件,看大小
# 或
lsof | grep deleted
找到持有进程后:发 reopen 信号,或重启该进程,空间立即释放。
14.4 journald
journalctl -u myapp -f # 跟踪
journalctl --vacuum-size=500M # 控制占用
journalctl --vacuum-time=7d # 按时间清
持久化:改 /etc/systemd/journald.conf 的 Storage=persistent,并建 /var/log/journal 目录,然后 systemctl restart systemd-journald。
参考:logrotate 手册(
man logrotate)
十五、让写入生效:systemd unit 与 sysctl
15.1 写 systemd unit 的标准流程
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
15.2 生效链条(每一步都不能漏)
sudo systemctl daemon-reload # ① 告诉 systemd 重新加载 unit 文件
sudo systemctl enable --now myapp # ② 开机自启 + 立即启动
systemctl status myapp # ③ 验证
journalctl -u myapp -f # ④ 看日志
高频坑:改了 unit 文件却不
daemon-reload,systemd 用的是内存里的旧版,改动"不生效"。改ExecStart/Type/User这些需要systemctl restart。
15.3 sysctl 内核参数写入与持久化
临时写(立即生效,重启失效):
sysctl -w net.ipv4.ip_forward=1
# 等价于 echo 1 > /proc/sys/net/ipv4/ip_forward
持久化写:
sudo tee /etc/sysctl.d/99-myapp.conf > /dev/null <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
vm.swappiness = 10
EOF
sudo sysctl --system # 重新加载 /etc/sysctl.d 与 /etc/sysctl.conf 全部配置
# 或只加载单个文件:
sudo sysctl -p /etc/sysctl.d/99-myapp.conf
15.4 机制
/proc/sys/X/Y/Z是内核运行时变量,写它=立即生效,重启即失。/etc/sysctl.d/*.conf是开机时由 systemd-sysctl 服务读取并写入/proc/sys的"持久化配方"。- 改完配方文件要
sysctl --system立即应用,否则要等下次开机。
高频坑:直接改
/proc/sys不写 conf → 重启丢参数;只写 conf 不sysctl --system→ 当下不生效。两边都做才完整。
十六、远程与网络写入
16.1 scp / rsync:运维搬文件主力
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 端口。
16.2 经 ssh 把本地内容写到远程文件
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 传输 + 远程写"。
16.3 HTTP 上传(curl)
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
十七、写入性能、调度与监控诊断
17.1 降低写入优先级,避免挤占业务 I/O
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(默认)、-c3 idle(只在盘空闲时写)——备份、日志归档的好选择。
17.2 调脏页回写策略
sysctl vm.dirty_background_ratio # 默认 ~10,超过则后台异步回写
sysctl vm.dirty_ratio # 默认 ~20,超过则写者被强制同步回写(阻塞)
调大 dirty_ratio → 写吞吐高、写者少阻塞,但掉电丢得多;调小 → 更安全,但写者更易在缓存满时被阻塞。
17.3 监控诊断:谁在写、写了多少、卡在哪
lsof | grep /var/log/myapp # 谁开着这个文件
lsof +L1 # 找被删但仍持有的文件
iotop -o -a # 只看有 I/O 的进程
iostat -xm 2 # 各设备写速率、await、%util
vmstat 2 # bo(块写出 KB/s)、wa(I/O 等待)
inotifywait -m -r -e modify,create,delete /etc/myapp # 文件变化监控
strace -e trace=write,openat,fsync -p 1234 # 追某进程的写入系统调用
readlink /proc/1234/fd/3 # 看 fd 实际指向哪个文件
17.4 "删了日志磁盘还是满的"排查链
df -h # 确认磁盘满
du -sh /var/log/* 2>/dev/null | sort -h | tail # 找大目录
lsof +L1 # 找被删但仍持有的文件(这里常有大块)
# 找到持有进程 → 发 reopen 信号 / kill,空间立即释放
十八、总结与速查
18.1 常见陷阱速查表
| 现象 | 根因 | 解决 |
|---|---|---|
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,清理小文件 |
| 普通用户写不动、权限对 | ACL / SELinux / 挂载 ro | getfacl、ls -Z、`mount |
| 配置改了不生效 | 只改 /proc 或没 reload | 写 sysctl.d + sysctl --system;unit 后 daemon-reload |
dd of=/dev/sdX 毁盘 |
目标盘符选错 | 先 lsblk -o NAME,SIZE,MODEL 确认 |
tee 后命令失败没被抓 |
默认管道只看末位退出码 | set -o pipefail |
truncate 后 ls 大、du 小 |
稀疏文件未占真实块 | 用 fallocate 真实预留 |
18.2 改配置的标准动作模板(安全可回滚)
# 备份
cp -a /etc/myapp.conf /etc/myapp.conf.bak.$(date +%F-%H%M)
# 编辑(vim 或 sed -i.bak)
vim /etc/myapp.conf
# 语法校验
myapp -t # 如 nginx -t / httpd -t / sshd -t
# 重载
systemctl reload myapp
# 留底
logger -t ops "$(hostname): changed /etc/myapp.conf"
18.3 一页纸总纲
运维写文件三件事:用对工具(
>>>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 留在屏幕。
18.4 命令速查卡
# 重定向
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
# 大文件 / 预分配 / 同步
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; fsfreeze --freeze /mnt; fsfreeze --unfreeze /mnt
# 内核参数
sysctl -w net.ipv4.ip_forward=1
sudo sysctl --system # 应用 /etc/sysctl.d
# 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
# 调度
ionice -c3 -n7 cmd
nice -n 19 ionice -c3 cmd
参考资料
- bash 手册 - Redirections:https://www.gnu.org/software/bash/manual/html_node/Redirections.html
- bash 手册 - Here Documents:https://www.gnu.org/software/bash/manual/html_node/Here-Documents.html
- bash 手册 - The Set Builtin(noclobber):https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
- GNU coreutils - tee:https://www.gnu.org/software/coreutils/manual/html_node/tee-invocation.html
- GNU coreutils - sync:https://www.gnu.org/software/coreutils/manual/html_node/sync-invocation.html
- GNU coreutils - mktemp:https://www.gnu.org/software/coreutils/manual/html_node/mktemp-invocation.html
- man 手册:
man sed、man awk、man logrotate、man 5 nfs、man 5 fstab、man sysctl - 菜鸟教程 - Linux 教程:https://www.runoob.com/linux/linux-tutorial.html
本文以 bash 与 GNU coreutils 为准;在 macOS/BSD 上部分命令行为不同(尤其
sed -i、echo -e)。 建议在虚拟机中亲手验证各写入命令与重定向行为。
