01 我为什么会困惑?
前两篇讲完了"命令怎么跑起来"和"路径怎么定位文件"。这一篇终于可以说"动手改文件"了。
但我一开始学这块,卡的全是设计上的问题:
touch明明是"摸一下"的意思,凭什么用它来新建文件?- 删文件是
rm,删目录又是rmdir,rm删目录还得加-r------为什么要搞这么麻烦? cp和mv长得几乎一模一样,差别到底在哪?- 看文件内容居然有五个命令(
cat/more/less/head/tail),什么时候用哪个?
这四个问题的答案,全部指向同一条分界线。想通它,这一整块就不用背了。
02 先建立一个直觉
2.1 一个文件有两半
text
文件
┌──────┴──────┐
内容 属性(元数据)
你写的数据 名字、大小、权限、三个时间......
存在数据块里 存在 inode 里
2.2 而"操作文件"也分成两半
⭐ 这是这一篇最重要的一句话:
改内容 = 动文件本体。
改名 / 移动 / 删除 = 动目录里的那张表。
回忆上一篇说过的一句话:目录的本质就是一张表,一行是"名字 → inode 号"。
text
目录 test 的这张表:
┌────────────┬───────────┐
│ 名字 │ inode 号 │
├────────────┼───────────┤
│ . │ 1844719 │
│ .. │ 1844712 │
│ file.txt │ 1844720 │
│ dir │ 1844730 │
└────────────┴───────────┘
想通这张表,下面这些"奇怪设计"全都不奇怪了:
| 现象 | 解释 |
|---|---|
为什么目录的 w 权限管的是"能不能删里面的文件" |
因为删文件 = 改这张表,而改表就是"写"这个目录 |
为什么 mv 快得离谱 |
它只是把表里的一行改个名字,数据一个字节都没动 |
为什么 rm 删目录非要 -r |
目录里可能还有子目录,得递归进去把那些表也一起收拾 |
为什么 rmdir 只肯删空目录 |
非空目录里还有别人的名字,它没地方安置 |
03 正式理解
3.1 为什么"新建文件"要用 touch 这种怪命令
因为 touch 的本职工作根本不是建文件,是改时间。
它的逻辑是:
text
文件存在 → 把它的时间戳改成"现在"
文件不存在 → 顺手创建一个空文件(时间戳自然是"现在")
所以"新建文件"只是它改时间时的一个副产品。这条命令设计出来是为了"摸一下文件让它变新",被大家拿来建文件,属于顺势而为。
⭐ 理解这一点有什么用? 有三件事你会立刻明白:
touch建出来的文件默认是空的(0 字节),因为它压根没写内容这一步- 要建一个有内容的文件,得用输出重定向 :
echo hello > a.txt(这是 shell 干的,上一篇讲过) touch的真正用武之地是改时间------比如强制让某个文件看起来"刚修改过",好骗过 make
3.2 三个时间,和那条最容易搞错的规律
上一篇讲过 atime / mtime / ctime,这里把 touch 和它们接上:
| 命令 | 只改哪个时间 |
|---|---|
touch -a 文件 |
atime(访问时间) |
touch -m 文件 |
mtime(修改时间) |
touch 文件 |
atime 和 mtime 都改成现在 |
⚠️ 但 ctime 你改不了。 它总是跟着一起变------因为"改时间"这个动作本身就是改 inode 元数据,元数据一变,ctime 必然更新。
这就是上一篇那条规律的验证:
改内容 → mtime 和 ctime 一起变;改属性 → 只有 ctime 变。
💡 顺便指出课件里的一个小笔误:选项表里把 -c 写成了 "change only the modification time",但示例里用的是 -m。
以示例为准:-m 才是"只改修改时间"。 -c 在 GNU 版 touch 里的意思是"文件不存在就不要创建"(--no-create),跟时间无关。
3.3 目录为什么删起来这么讲究
三个命令的分工:
| 命令 | 能删什么 | 为什么这么设计 |
|---|---|---|
rmdir 空目录 |
只能删空目录 | 它只删表里的一行,非空的话里面那些名字没地方去 |
rm -r 目录 |
递归删,连带里面所有东西 | 它愿意替你逐个进子目录收拾 |
rm 文件 |
删文件 | 只删表里的一行 |
rm 目录(不加 -r) |
报错 | 系统在拦你 |
bash
[whb@bite-alicloud test]$ rm dir
rm: cannot remove 'dir': Is a directory
[whb@bite-alicloud test]$ rm -r dir # 加 -r 才行
[whb@bite-alicloud test]$ rmdir -p path1/path2/path3
rmdir: failed to remove 'path1/path2/path3': Directory not empty
⭐ 看出设计意图了吗:删目录比删文件危险得多,所以系统默认不让你顺手把一整个目录干掉。
rm 不加 -r 拒绝目录,rmdir 只肯删空目录------这两道都是为了拦你。
那 -p 又是什么?它是"删完之后,如果父目录也空了,就一路往上一起删":
bash
[whb@bite-alicloud test]$ rmdir -p path1/path2/path3/path4
# path4 删掉后 path3 空了 → 删;path2 空了 → 删;path1 空了 → 删
⚠️ 但只要有任何一个上层目录里还有东西,它就停下来报错:
bash
[whb@bite-alicloud test]$ touch path1/myfile.txt
[whb@bite-alicloud test]$ rmdir -p path1/path2/path3/path4
rmdir: failed to remove directory 'path1/path2': Directory not empty
注意报错说的是 path1/path2------说明 path4、path3 已经被删掉了,它是在往上走的时候撞墙的 。这个细节值得留意:rmdir -p 是边走边删的,不是先检查再删。
3.4 cp 和 mv:一个搬数据,一个改名字
这俩命令长得几乎一样,但本质完全不同:
cp |
mv |
|
|---|---|---|
| 数据 | 真的复制一份 | 不复制(同文件系统内) |
| 原名 | 还在 | 没了(相当于改名) |
| 速度 | 跟文件大小成正比 | 几乎是瞬间 |
| 本质 | 读原文件 + 写新文件 | 改目录表里的一个名字 |
⭐ mv 为什么这么快?
因为它是 rename 系统调用------只改了目录表里那一行的名字字段,数据一个字节都没动。 所以你 mv 一个 10G 的文件,和 mv 一个空文件,耗时几乎一样。
⚠️ 但这条只在"同一个文件系统内"成立。 如果跨文件系统(比如从 / 搬到另一个挂载的磁盘),mv 只能老老实实复制 + 删除,那时候就跟 cp 一样慢了。
3.5 mv 的两种语义
⭐ mv 到底在干什么,完全由第二个参数决定:
| 第二个参数是 | mv 干什么 |
源能有几个 |
|---|---|---|
| 一个不存在的名字(或文件名) | 改名 | 只能 1 个 |
| 一个已存在的目录 | 全搬进去 | 可以多个 |
bash
$ mv myfile.txt yourfile.txt # 第二个参数不存在 → 改名
$ mv myfile.txt temp # temp 是已存在的目录 → 搬进去
$ mv temp ../ # 搬走
⚠️ mv 最需要注意的坑:它默认直接覆盖,不问你。
bash
[whb@bite-alicloud test]$ touch myfile.txt yourfile.txt
[whb@bite-alicloud test]$ mv yourfile.txt myfile.txt
[whb@bite-alicloud test]$ ll
total 0
-rw-rw-r-- 1 whb whb 0 Jan 11 15:57 myfile.txt # 原来的 myfile.txt 内容没了
所以想安全就加 -i:
bash
[whb@bite-alicloud test]$ mv -i yourfile.txt myfile.txt
mv: overwrite 'myfile.txt'? y
3.6 看内容:五个命令其实是一条梯度
这五个命令不是一个一个独立的工具,它们是对同一个问题"我要读多少"的不同回答:
text
全都要 ──────────────────────────────────────────► 只看一点
cat less head / tail
(一口气全打) (分页,随便翻) (只看开头/结尾)
│ │ │
│ └─ 大文件的正经看法 └─ 看日志、看表头
└─ 小文件、往管道里塞数据
more
(分页,但只能往前翻,已被 less 取代)
⭐ 为什么 cat 不能看大文件?
因为 cat 不做任何分页,它会把整个文件一口气全写到标准输出。文件一大,屏幕就会飞速滚动,滚完之后你停在文件末尾,前面的内容再也回不去。
💡 而 less 的聪明之处有两点:
- 它能前后翻页、能搜索
- 它不会在打开前把整个文件读进内存------所以你用它看一个几百 G 的日志也不会卡死
课件里那句话说得挺准确:"less 在查看之前不会加载整个文件。"
3.7 为什么 head 和 tail 能组合出任意区间
bash
[whb@bite-alicloud test]$ head -200 temp.txt | tail -20
拆开看:
text
temp.txt 有 2001 行
head -200 temp.txt → 取出前 200 行(第 1 ~ 200 行)
│
▼ 通过管道
tail -20 → 从这 200 行里再取最后 20 行
(也就是第 181 ~ 200 行)
算一遍就明白了:先截前面,再从截下来的部分里取尾部,剩下的正好是你要的区间。
💡 想取第 m 到第 n 行,公式就是 head -n 文件 | tail -$(n-m+1)。
04 它到底是怎么工作的?
4.1 创建一个文件,内核做了三件事
🔬 你敲 touch a.txt,内核实际干了这些:
text
1. 在文件系统里分配一个新的 inode
(inode 负责记录:类型、权限、所有者、大小、三个时间、数据块在哪)
2. 在父目录的那张表里加一行: a.txt → 这个 inode 号
3. 把这一行的"链接数"记为 1
⭐ 注意第 2 步是必须的。 只有 inode 没有目录项,这个文件就是一个"没人知道它叫什么"的孤儿------你没法通过路径找到它。
这也解释了上一篇那个 link count:
- 普通文件链接数是
1→ 它只有一个名字 - 目录链接数是
2 + 子目录数→ 它的"名字"除了自己还有子目录的..
4.2 删除文件,删的到底是什么
🔬 这是这一篇最值得记住的一节。
rm 做的事,本质上是 unlink() 系统调用------把目录表里的那一行删掉,然后把链接数减 1。
text
删之前:
目录表:a.txt → inode 100 inode 100 的链接数 = 1
rm a.txt 之后:
目录表:(这一行没了) inode 100 的链接数 = 0 → 数据块被回收
⭐ 只有当链接数减到 0,内核才会真正回收数据块。 这就推出两个非常实用的结论:
结论一:如果这个文件还有别的名字(硬链接),删掉一个名字并不会删掉数据。
链接数还没归零,数据就还在。
结论二:如果这个文件正被某个进程打开着,删掉名字,数据也不会立刻消失。
因为那个进程的 fd 还指着这个 inode。空间要等进程关掉这个 fd 之后才真正释放。
💡 这是一个非常经典的坑:你在 Linux 上 rm 了一个巨大的日志文件,df 一看磁盘还是满的------因为写日志的进程还没重启,它的 fd 还开着。 解决办法是让那个进程重新打开日志文件,而不是反复 rm。
4.3 cat 把内容写到哪去了
🔬 这一条是上一篇的复习。
bash
$ cat temp.txt
cat 做的事很简单:打开 temp.txt,把内容读出来,写到 fd 1(标准输出)。
而 fd 1 默认连着你的终端,所以内容出现在屏幕上。
所以下面这些是等价的:
bash
$ cat temp.txt > out.txt # 把 fd 1 指向文件 → 内容进文件
$ cat temp.txt # fd 1 是终端 → 内容上屏幕
⭐ cat 自己不知道这两者的区别 ,它只管往 fd 1 写。是 shell 在它启动前就把 fd 1 换掉了------和上一篇 echo hello > out.txt 是同一个机制。
4.4 tail -f 是怎么"跟着"的
🔬 tail -f 的 -f 是 follow(跟随)的意思。
它做的事是:
text
1. 打开文件,跳到末尾,把最后几行打出来
2. 不退出,而是进入一个循环:
每隔一小会儿去问一次内核:"这个文件有没有变大?"
变大了 → 把新增的内容读出来打给你看
3. 一直循环,直到你按 Ctrl+C
💡 一个关键细节:tail -f 跟的是"文件描述符",不是"文件名"。
所以如果你用 > 重定向把日志文件重建 了(inode 换了),tail -f 会呆呆地看着那个已经被删掉的旧 inode,再也刷不出新内容。这时候要么重启 tail,要么用 tail -F(大写 F,它会重新按文件名去找)。
4.5 为什么 less / more 里要按 q 退出
💡 因为 less 这类程序会接管你的终端。
text
正常情况:键盘输入 → shell → 命令
运行 less:键盘输入 → less(它直接读终端设备,自己解释按键)
所以你在 less 里按的每一键,都是 less 在解释,不是 shell:
| 按键 | less 里干什么 |
|---|---|
空格 / PageDown |
往后翻一页 |
PageUp |
往前翻一页 |
/字符串 |
向下搜索 |
?字符串 |
向上搜索 |
n / N |
重复上一次搜索 / 反向重复 |
q |
退出,把终端还给 shell |
⭐ 理解"程序接管终端"这件事,你以后就不会觉得"为什么按 q 才能出来"很神秘了------它只是在还你终端控制权。
05 把几个概念串起来
text
目录(一张表) inode
┌──────────────────────────┐ ┌──────────────────────┐
│ 名字 inode 号 │ │ 类型 / 权限 / 所有者 │
├──────────────────────────┤ │ 大小 / 三个时间 │
│ . 1844719 │ │ 数据块在磁盘哪里 │
│ .. 1844712 │ │ 链接数 │
│ file.txt ──┼────────────┼───────►│ 1844720 │
│ dir 1844730 │ └───────────┬──────────┘
└──────────────────────────┘ │
▲ ▼
│ 数据块(真正的内容)
┌──────────┴───────────┐
│ │
改这张表的操作 改本体 的操作
───────────── ────────────
touch(加一行) echo > file
rm (删一行) vim / cat > file
mv (改名字) 程序 write
cp (加一行新的,
但同时也写了数据)
一句话总结:
touch/rm/mv动的是目录那张表 ,rm -r无非是"递归地动很多张表";而改内容动的是数据块 。
cp特殊一点:它两头都要动------写新数据,还要在表里加一行。
06 实际命令
6.1 touch
bash
$ touch newFile.txt # 文件不存在就建一个(0 字节)
$ touch -a newFile.txt # 只改访问时间(atime)
$ touch -m newFile.txt # 只改修改时间(mtime)
验证一下它到底改了哪个时间:
bash
[whb@bite-alicloud test]$ stat newFile.txt
File: 'newFile.txt'
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: fd01h/64769d Inode: 1844719 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1003/ whb) Gid: ( 1003/ whb)
Access: 2023-01-11 15:06:45.832787572 +0800
Modify: 2023-01-11 15:06:45.832787572 +0800
Change: 2023-01-11 15:06:45.832787572 +0800
Birth: -
# 只改 access 时间
[whb@bite-alicloud test]$ touch -a newFile.txt
[whb@bite-alicloud test]$ stat newFile.txt
Access: 2023-01-11 15:07:00.062846804 +0800 ← 变了
Modify: 2023-01-11 15:06:45.832787572 +0800 ← 没变
Change: 2023-01-11 15:07:00.062846804 +0800 ← 也跟着变了
# 只改 modify 时间
[whb@bite-alicloud test]$ touch -m newFile.txt
[whb@bite-alicloud test]$ stat newFile.txt
Access: 2023-01-11 15:07:00.062846804 +0800 ← 没变
Modify: 2023-01-11 15:09:19.428426903 +0800 ← 变了
Change: 2023-01-11 15:09:19.428426903 +0800 ← 也跟着变了
⚠️ 看第三行:touch -a 明明只动了 atime,为什么 Change 也跟着变?
课件里留了个问号。答案就是 3.2 那条规律:改 atime 这个动作本身就是在改 inode 的元数据,元数据一变,ctime 必然更新。
仔细看两次操作的差别,规律就浮出来了:
| 操作 | Access | Modify | Change |
|---|---|---|---|
touch -a |
✅变 | ❌不变 | ✅变 |
touch -m |
❌不变 | ✅变 | ✅变 |
Change 永远跟着变------因为它的定义就是"inode 状态最后变化的时间"。
6.2 mkdir
bash
$ mkdir mydir # 建一个目录
$ mkdir -p path1/path2/path3/path4 # 路径缺哪层就补哪层
bash
[whb@bite-alicloud test]$ mkdir mydir
[whb@bite-alicloud test]$ ls -l
drwxrwxr-x 2 whb whb 4096 Jan 11 14:22 dir
-rw-rw-r-- 1 whb whb 0 Jan 11 14:22 file.txt
drwxrwxr-x 2 whb whb 4096 Jan 11 15:15 mydir ← 多了这个
-rw-rw-r-- 1 whb whb 0 Jan 11 15:09 newFile.txt
-p 的价值 :一次把整条路径的缺口补齐,而且已存在的目录不会报错。
bash
[whb@bite-alicloud test]$ mkdir -p path1/path2/path3/path4
[whb@bite-alicloud test]$ tree path1
path1
└── path2
└── path3
└── path4
⚠️ 不加 -p 会怎样?
bash
$ mkdir a/b/c
mkdir: cannot create directory 'a/b/c': No such file or directory
因为 mkdir 只肯建最后那一层,它不会替你把中间那些不存在的一起造出来。这也是上一篇"路径解析要一层层走"的直接后果------中间那层不存在,走到那儿就断了。
6.3 rm 和 rmdir
bash
$ rm file.txt # 删文件
$ rm -r dir # 删目录(递归)
$ rm -i file.txt # 删前问一句
$ rm -f file.txt # 强制删,不存在的文件也不报错
$ rm -rf dir # 递归 + 强制,不问也不报错
$ rmdir 空目录 # 只能删空目录
$ rmdir -p path1/path2/path3/path4 # 连带空的父目录一起删
逐个看真实输出:
bash
# 删普通文件
[whb@bite-alicloud test]$ rm file.txt
[whb@bite-alicloud test]$ ll
-rw-rw-r-- 1 whb whb 0 Jan 11 15:09 newFile.txt
# 直接删目录 → 被拦
[whb@bite-alicloud test]$ rm dir
rm: cannot remove 'dir': Is a directory
# 加 -r → 可以了
[whb@bite-alicloud test]$ rm -r dir
# -i 会逐个问
[whb@bite-alicloud test]$ rm -i newFile.txt
rm: remove regular empty file 'newFile.txt'? y
# -f 对目录没用!
[whb@bite-alicloud test]$ rm -f path1
rm: cannot remove 'path1': Is a directory
# -rf 才行
[whb@bite-alicloud test]$ rm -rf path1
⚠️ 重点看那一行:rm -f path1 仍然报错。
这是个特别常见的误解:-f 的意思是"force(不询问、忽略不存在)",不是"什么都能删"。 要删目录,关键永远是那个 -r。
所以:
| 命令 | 能删文件 | 能删目录 |
|---|---|---|
rm |
✅ | ❌ |
rm -f |
✅ | ❌ |
rm -r |
✅ | ✅(会问) |
rm -rf |
✅ | ✅(不问) |
⭐ -r 和 -f 是两个独立的开关,一个管"要不要递归进目录",一个管"要不要问你"。很多人把它们当成一个东西记。
6.4 cp
bash
$ cp a.txt b.txt # 复制文件
$ cp *.txt dir # 多个文件拷进目录
$ cp -i a.txt b.txt # 覆盖前问一句
$ cp -r dir1 dir2 # 递归复制目录
$ cp -rf dir1 dir2 # 递归 + 强制
bash
# 基本复制
[whb@bite-alicloud test]$ cp myfile.txt myfile-backup.txt
[whb@bite-alicloud test]$ ll
-rw-rw-r-- 1 whb whb 22 Jan 11 15:47 myfile-backup.txt
-rw-rw-r-- 1 whb whb 22 Jan 11 15:47 myfile.txt
# 多个文件拷进目录(* 是 shell 展开的,上一篇讲过)
[whb@bite-alicloud test]$ cp *.txt dir
[whb@bite-alicloud test]$ tree dir
dir
├── myfile-backup.txt
└── myfile.txt
# 目标已存在 → 直接覆盖
[whb@bite-alicloud test]$ cp myfile.txt myfile-backup.txt
# 加 -i 就会问
[whb@bite-alicloud test]$ cp -i myfile.txt myfile-backup.txt
cp: overwrite 'myfile-backup.txt'? y
# 拷目录必须 -r
[whb@bite-alicloud ~]$ cp -rf test test-backup
⚠️ cp 和 mv 在"覆盖"这件事上是一对儿:都默认不问、直接覆盖。 想安全就 -i。
💡 什么时候该用 -i? 我的经验是:只要目标是个已存在的目录,就顺手带上 -i。 尤其是 cp -r 大目录的时候,覆盖错了不好恢复。
6.5 mv
bash
$ mv a.txt b.txt # 改名
$ mv a.txt dir # 搬进目录
$ mv -i a.txt b.txt # 覆盖前问一句
$ mv -f a.txt b.txt # 覆盖不问
bash
# 改名
[whb@bite-alicloud test]$ touch myfile.txt
[whb@bite-alicloud test]$ mv myfile.txt yourfile.txt
[whb@bite-alicloud test]$ ll
-rw-rw-r-- 1 whb whb 0 Jan 11 15:56 yourfile.txt
# 同名就覆盖(危险动作)
[whb@bite-alicloud test]$ touch myfile.txt
[whb@bite-alicloud test]$ mv yourfile.txt myfile.txt
[whb@bite-alicloud test]$ ll
-rw-rw-r-- 1 whb whb 0 Jan 11 15:57 myfile.txt # yourfile.txt 没了
# 搬目录
[whb@bite-alicloud test]$ mv myfile.txt temp
[whb@bite-alicloud test]$ mv temp ../
[whb@bite-alicloud test]$ ls -d ../temp
../temp
💡 最后那两行演示了"把一个目录整个搬走"。注意 ls -d------ls 后面跟目录名会列出目录里的东西 ,而 -d 让它显示目录本身(这个上一篇讲过,很容易忘)。
6.6 cat
bash
$ cat temp.txt # 全打出来
$ cat -b temp.txt # 非空行编号(空行不占号)
$ cat -n temp.txt # 所有行都编号(空行也占号)
$ cat -s temp.txt # 连续多个空行压成一个
三个选项的差别,用一个带空行的文件看最清楚:
bash
[whb@bite-alicloud test]$ cat -n temp.txt
1 hello bite
2 hello bite
3 hello bite
4
5
6
7 hello bite
[whb@bite-alicloud test]$ cat -b temp.txt
1 hello bite
2 hello bite
3 hello bite
4 hello bite ← 空行没占号,所以第 4 行还是内容
| 选项 | 空行怎么处理 |
|---|---|
-b |
不编号,也不占号 |
-n |
照样编号(空行也占一个号) |
-s |
多个连续空行压缩成一个 |
⭐ 怎么记:-b 的 b 是"有内容的行"(blank 之外的),-n 是"所有行"(number 一切)。
实操里 -b 更常用------因为看代码时你希望行号对应的是真实代码行,空行不参与编号。
⚠️ 再次提醒:cat 不适合看大文件。 它会一口气全打出来,你只能看着屏幕滚完。
6.7 more 和 less
bash
$ more -10 temp.txt # 每次显示 10 行
$ less -N temp.txt # 显示行号
bash
[whb@bite-alicloud test]$ more -10 temp.txt
hello bite
...(10 行)
--More--(0%)
那个 --More--(0%) 是 more 在等你按键:空格往下翻,q 退出。
为什么推荐 less 而不是 more:
more |
less |
|
|---|---|---|
| 往后翻 | ✅ | ✅ |
| 往前翻 | ❌ | ✅ |
| 搜索 | 很弱 | ✅ / 和 ? 双向搜 |
| 打开前是否加载整个文件 | --- | ❌ 不加载(大文件友好) |
less 里的按键:
| 按键 | 作用 |
|---|---|
空格 / PageDown |
往后翻 |
PageUp |
往前翻 |
/字符串 |
向下搜索 |
?字符串 |
向上搜索 |
n |
重复上一次搜索 |
N |
反向重复上一次搜索 |
q |
退出 |
💡 搜索这一条特别实用 :less 打开一个大文件,直接 /error 就能跳到第一个出错的地方,比 cat 完再往上翻强太多。
6.8 head 和 tail
bash
$ head temp.txt # 前 10 行(默认)
$ head -5 temp.txt # 前 5 行
$ tail temp.txt # 后 10 行(默认)
$ tail -3 temp.txt # 后 3 行
$ tail -f log.txt # 跟着日志走(不退出,有新内容就刷)
bash
[whb@bite-alicloud test]$ head temp.txt
hello 0
hello 1
...(到 hello 9,共 10 行)
[whb@bite-alicloud test]$ head -5 temp.txt
hello 0
hello 1
hello 2
hello 3
hello 4
[whb@bite-alicloud test]$ tail -3 temp.txt
hello 1998
hello 1999
hello 2000
⭐ tail -f 是这几个里最该记住的。 看服务器日志基本靠它:文件在长大,你屏幕跟着长,出问题立刻就能看到。
退出要用 Ctrl+C(不是 q------tail -f 不是分页器,它是在一个死循环里跟着文件)。
组合技:取任意区间
bash
# 取第 180 ~ 200 行
[whb@bite-alicloud test]$ head -200 temp.txt | tail -20
hello 180
hello 181
...
hello 199
💡 前面 3.7 讲过它的原理:先截前 200 行,再从这 200 行里取最后 20 行。
07 常见误区
| 误区 | 真相 |
|---|---|
touch 是"创建文件"命令 |
它是改时间的命令,建文件只是副作用。所以它建出来的文件一定是空的 |
rm -f 能删目录 |
不能。 -f 是"不询问",-r 才是"能进目录"。删目录永远要 -r |
rm 把文件的数据删掉了 |
它删的是目录表里那一行 (unlink)。有硬链接、或文件正被打开时,数据还活着 |
rmdir 和 rm -r 差不多 |
rmdir 只肯删空目录,这是故意设计的安全阀 |
mv 和 cp 都是"移动" |
cp 真的复制数据;mv(同文件系统内)只改一个名字,所以快得多 |
mv 会问你要不要覆盖 |
默认不问,直接覆盖 。要问就加 -i |
cp -r 目标 目标存在时会覆盖它 |
目标目录存在时,cp -r test test-backup 会创建 test-backup/test,不是替换 |
cat 什么文件都能看 |
大文件不适合,它会一口气全打出来,你只能看它滚完 |
more 和 less 随便用哪个 |
more 不能往回翻 ,搜索也弱。习惯用 less |
tail -f 跟的是"这个文件" |
它跟的是打开时的那个 inode 。用 > 重建文件后它会跟丢,得用 tail -F |
rmdir -p 会先检查完再删 |
它是边走边删的,所以中间撞墙时,下面几层已经删掉了 |
head / tail 默认显示 5 行 |
默认是 10 行 |
| 删除很快,所以数据一定没了 | 删除只是断开名字。释放空间要等链接数归零 + 没人打开它 |
08 和 C/C++ 联系起来
这一节把这几个命令和系统调用对上号------你会发现自己早就在用它们了,只是没意识到。
8.1 命令背后的系统调用
| shell 命令 | 背后是哪个系统调用 |
|---|---|
touch a.txt(建文件) |
`open("a.txt", O_CREAT |
touch -a a.txt(改时间) |
utimensat() 或老的 utime() |
rm a.txt |
unlink("a.txt") |
mv a.txt b.txt(同文件系统) |
rename("a.txt", "b.txt") |
cp a.txt b.txt |
open + read + write + close 循环 |
mkdir mydir |
mkdir() |
rmdir mydir |
rmdir() |
ls |
opendir + readdir |
⭐ rm 的真名是 unlink,mv 的真名是 rename。 记住这两个名字,4.2 那节讲的"删的其实是名字"就一点不抽象了------函数名本身就在说这件事:它解除的是 link(链接),不是数据。
8.2 自己写一个 cat
理解了 cat 就是"读文件,写到 fd 1",那它其实特别短:
c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "用法: %s 文件名\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open"); // 出错信息走 stderr(fd 2)
return 1;
}
char buf[4096];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
write(STDOUT_FILENO, buf, n); // 直接写到 fd 1
}
close(fd);
return 0;
}
编译运行:
bash
$ gcc -o mycat mycat.c
$ ./mycat temp.txt # 内容上屏幕
$ ./mycat temp.txt > out.txt # 内容进文件
$ ./mycat temp.txt | head -3 # 内容进管道
⭐ 注意最后两条:同一份代码,三个不同的去处。
因为程序只管往 fd 1 写,而 fd 1 被谁接走,是 shell 决定的------这就是第一篇讲的重定向和管道。
你自己写的程序和系统自带命令,在这一点上没有任何区别。
8.3 为什么 open 的第一个参数是路径,不是文件名
c
open("data.txt", O_RDONLY);
open("/home/whb/data.txt", O_RDONLY);
哪个能用,取决于进程的 cwd(上一篇第 8.1 节那个坑:换个目录跑同一个程序,相对路径就失效了)。
想稳定,就用绝对路径,或者先用 chdir() 明确设置工作目录。
8.4 为什么我用 fopen 写了内容,却没立刻出现在文件里
c
FILE *fp = fopen("a.txt", "w");
fprintf(fp, "hello\n");
// 这时候用另一个终端 cat a.txt,可能什么都看不到
fclose(fp); // 到这一步才真的落盘
因为 FILE* 是带缓冲的(第一篇第 8.2 节讲过):
text
fprintf → 写进用户态缓冲区 → (缓冲区满了 / fflush / fclose)→ write → 内核 → 磁盘
💡 open / write 是不带缓冲 的裸系统调用;fopen / fprintf 是 C 库在上面加的"带缓冲的壳"。
这是两套接口,不是一套。 想立刻落盘就 fflush(fp) 或者用裸的 write。
8.5 一个能解释"磁盘满了但找不到大文件"的实验
结合 4.2 的结论,你可以在程序里复现那个经典现象:
c
int fd = open("bigfile", O_WRONLY | O_CREAT, 0644);
// ... 往里写很多数据 ...
unlink("bigfile"); // 名字删掉了,ls 也看不到了
// 但 fd 还开着 → 数据块没释放 → df 看磁盘还是满的
close(fd); // 到这一步空间才真正回收
⭐ 记住这个顺序:先删名字,后关 fd,空间就"卡"在中间。 线上服务日志把磁盘吃满,往往就是这个原因。
09 最后真正记住什么?
text
【所有文件操作,只有两条线】
┌─────────────────────────────┐ ┌──────────────────────────┐
│ 改「目录那张表」 │ │ 改「文件本体」 │
│ (名字、位置、在不在) │ │ (数据块里的内容) │
├─────────────────────────────┤ ├──────────────────────────┤
│ touch 加一行 │ │ echo > file │
│ mkdir 加一行(目录) │ │ vim file │
│ rm 删一行 ← unlink │ │ 程序里的 write │
│ rmdir 删一行(仅限空目录) │ │ │
│ mv 改一行 ← rename │ │ │
└─────────────────────────────┘ └──────────────────────────┘
▲ ▲
│ │
└──────── cp 两条都动 ─────────────┘
(写新数据 + 加一行)
这部分读完,脑子里留下五个模型就够了:
- 一个文件 = 目录表里的一行 + 一个 inode + 一堆数据块。 名字和内容是两个东西,这是理解后面所有命令的底座。
touch的主业是改时间,建文件只是顺手的副作用------所以它建出来的文件一定是空的。rm删的是名字(unlink),mv改的也是名字(rename)。 所以mv快得离谱;所以删掉的文件只要还有人打开着,空间就不会释放。-r和-f是两个独立开关 :一个管"能不能进目录",一个管"要不要问你"。rm -f删不掉目录,永远是-r的事。- 看内容五个命令是一条梯度 :
cat全打(小文件)、less分页(大文件,能搜能翻)、head/tail看两头、tail -f跟日志。
下一篇 :查找、时间、帮助、打包和系统命令(man / find / which / whereis / alias / grep / date / cal / tar / zip / top / uname / 热键 / shutdown)。