【Linux系统编程】指令(三):创建、删除、复制、移动、看内容

01 我为什么会困惑?

前两篇讲完了"命令怎么跑起来"和"路径怎么定位文件"。这一篇终于可以说"动手改文件"了。

但我一开始学这块,卡的全是设计上的问题:

  1. touch 明明是"摸一下"的意思,凭什么用它来新建文件?
  2. 删文件是 rm,删目录又是 rmdirrm 删目录还得加 -r------为什么要搞这么麻烦?
  3. cpmv 长得几乎一模一样,差别到底在哪?
  4. 看文件内容居然有五个命令(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 复制代码
文件存在 → 把它的时间戳改成"现在"
文件不存在 → 顺手创建一个空文件(时间戳自然是"现在")

所以"新建文件"只是它改时间时的一个副产品。这条命令设计出来是为了"摸一下文件让它变新",被大家拿来建文件,属于顺势而为。

理解这一点有什么用? 有三件事你会立刻明白:

  1. touch 建出来的文件默认是空的(0 字节),因为它压根没写内容这一步
  2. 要建一个有内容的文件,得用输出重定向echo hello > a.txt(这是 shell 干的,上一篇讲过)
  3. 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 的聪明之处有两点:

  1. 它能前后翻页、能搜索
  2. 不会在打开前把整个文件读进内存------所以你用它看一个几百 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

⚠️ cpmv 在"覆盖"这件事上是一对儿:都默认不问、直接覆盖。 想安全就 -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)。有硬链接、或文件正被打开时,数据还活着
rmdirrm -r 差不多 rmdir 只肯删空目录,这是故意设计的安全阀
mvcp 都是"移动" cp 真的复制数据;mv(同文件系统内)只改一个名字,所以快得多
mv 会问你要不要覆盖 默认不问,直接覆盖 。要问就加 -i
cp -r 目标 目标存在时会覆盖它 目标目录存在时,cp -r test test-backup 会创建 test-backup/test不是替换
cat 什么文件都能看 大文件不适合,它会一口气全打出来,你只能看它滚完
moreless 随便用哪个 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 的真名是 unlinkmv 的真名是 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 两条都动 ─────────────┘
                              (写新数据 + 加一行)

这部分读完,脑子里留下五个模型就够了:

  1. 一个文件 = 目录表里的一行 + 一个 inode + 一堆数据块。 名字和内容是两个东西,这是理解后面所有命令的底座。
  2. touch 的主业是改时间,建文件只是顺手的副作用------所以它建出来的文件一定是空的。
  3. rm 删的是名字(unlink),mv 改的也是名字(rename)。 所以 mv 快得离谱;所以删掉的文件只要还有人打开着,空间就不会释放。
  4. -r-f 是两个独立开关 :一个管"能不能进目录",一个管"要不要问你"。rm -f 删不掉目录,永远是 -r 的事。
  5. 看内容五个命令是一条梯度cat 全打(小文件)、less 分页(大文件,能搜能翻)、head / tail 看两头、tail -f 跟日志。

下一篇 :查找、时间、帮助、打包和系统命令(man / find / which / whereis / alias / grep / date / cal / tar / zip / top / uname / 热键 / shutdown)。

相关推荐
程序员老陆1 小时前
CMake 常用关键字与命令全景:从 set到 target_link_options的现代工程视角
c++·cmake·程序开发
AR-26710-1 小时前
Linux Day9——复习第一部分(实操产出)
linux
qq_410194292 小时前
.NET 8.0 下基于 PaddleOCRSharp 的离线文字识别落地指南
算法·.net
Omics Pro2 小时前
1个月2轮融资!长寿虚拟细胞
数据库·人工智能·算法·机器学习·自然语言处理
residual_fan2 小时前
航空发动机故障诊断专用智能体(六):实际机队健康管理中的应用考量与展望
人工智能·算法·数据挖掘·数据分析
码匠许师傅2 小时前
【C++三方组件】cxxopts:轻量首选的命令行解析
c++
hansang_IR2 小时前
【讲解】CSP-S2026 第一轮初赛
c++·算法
hanlin032 小时前
刷题笔记:力扣第76题-最小覆盖子串
笔记·算法·leetcode
Zane19942 小时前
比较排序跑不出O(nlogn)是数学上证明过的,计数、桶、基数排序凭什么能绕开这堵墙
算法·排序算法