lseek的“时空跳跃”:从获取大小到制造“文件空洞”
80T资源合集下载
链接:https://pan.quark.cn/s/5643428d4f9f
在 Linux 的文件世界里,我们的读写操作就像是在一本书上顺序阅读或书写。但有时,我们需要更灵活的能力:
- 想知道这本书到底有多少页?
- 想直接翻到某一页开始阅读?
- 甚至,想在这本书的末尾凭空加上 100 张空白页?
lseek 系统调用就是赋予我们这种“时空跳跃”能力的魔法棒。今天,我们将通过三个实战案例,彻底掌握 lseek 的三大核心技巧,并揭开一个有趣的概念——文件空洞 (File Hole)。
lseek 函数速览
在我们开始“跳跃”之前,先看一下它的“操作面板”:
#include <sys/types.h>
#include <unistd.h>off_t lseek(int fd, off_t offset, int whence);-
fd: 文件描述符,我们的“书本”。 -
offset: 偏移量,我们要跳跃的“页数”。正数向后跳,负数向前跳。 whence: 起始位置,我们从哪里开始跳?
-
SEEK_SET: 从书的开头(文件头)开始。 -
SEEK_CUR: 从当前翻到的位置(当前读写位置)开始。 -
SEEK_END: 从书的结尾(文件末尾)开始。
- 返回值: 这是一个关键!**无论
whence 是什么,成功时它永远返回从文件头算起的新位置。**失败则返回 -1。
技巧一:获取文件大小 —— 翻到最后一页
想知道文件有多大?最简单的方法就是用 lseek 直接“翻到”文件的末尾,然后看看“页码”。
lseek(fd, 0, SEEK_END) 这行代码的意思是:从文件末尾开始,移动 0 个字节。这并不会改变文件内容,但它的返回值恰好就是文件末尾相对于文件头的偏移量,也就是文件的大小。
案例1:测量文件大小
- 准备文件
data.txt
echo "Hello, lseek world!" > data.txt
# wc -c data.txt (可以看到文件大小是 20 字节)- 编写代码
get_size.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>int main() {int fd = open("data.txt", O_RDONLY);if (fd < 0) {perror("open");exit(1);}off_t file_size = lseek(fd, 0, SEEK_END);if (file_size == -1) {perror("lseek");exit(1);}printf("The size of data.txt is: %ld bytes\n", file_size);close(fd);return 0;
}- 编译并运行
gcc get_size.c -o get_size
./get_size- 运行结果
The size of data.txt is: 20 bytes分析:结果与 wc -c 命令完全一致,我们成功地用 lseek 获取了文件大小。
技巧二:精确定位 —— 读写指针的共享秘密
lseek 最本职的工作是移动读写指针。但这里有一个非常重要的“坑”:文件的读指针和写指针是共享的,它们是同一个东西!
这意味着,你执行了一次 write 操作后,指针会停在写入内容的末尾。如果你紧接着执行 read,你将从那个新位置开始读,而不是从文件头。
案例2:写后立即读的“陷阱”与破解 这个程序会先向一个空文件写入 "ABC",然后尝试立即读出文件内容。
// read_after_write.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>int main() {int fd = open("test.log", O_RDWR | O_CREAT | O_TRUNC, 0664);// ... 错误检查 ...// 写入 "ABC"write(fd, "ABC", 3);printf("Wrote 'ABC' to file. Current position should be at 3.\n");char buffer[10] = {0};// 陷阱:直接读取ssize_t n = read(fd, buffer, sizeof(buffer));printf("\nAttempt 1 (without lseek):\n");printf("Read %ld bytes. Content: '%s'\n", n, buffer);// 破解:使用 lseek 将指针移回文件头lseek(fd, 0, SEEK_SET);printf("\nUsed lseek(fd, 0, SEEK_SET) to rewind.\n");// 再次读取n = read(fd, buffer, sizeof(buffer));printf("\nAttempt 2 (with lseek):\n");printf("Read %ld bytes. Content: '%s'\n", n, buffer);close(fd);return 0;
}编译并运行
gcc read_after_write.c -o read_after_write
./read_after_write运行结果
Wrote 'ABC' to file. Current position should be at 3.Attempt 1 (without lseek):
Read 0 bytes. Content: ''Used lseek(fd, 0, SEEK_SET) to rewind.Attempt 2 (with lseek):
Read 3 bytes. Content: 'ABC'分析:第一次 read 失败了!因为它从指针停留的第 3 个字节处开始读,那里已经是文件末尾 (EOF),所以读到了 0 字节。通过 lseek 将指针“倒带”回文件头后,我们才成功读出了写入的内容。切记:读写共享同一指针!
技巧三:文件拓展与“文件空洞” —— 凭空造页
这是 lseek 最神奇也是面试中最常被问到的功能。lseek 允许你将指针移动到超过文件末尾的地方。
但这还不够,单纯移动指针并不会改变文件大小。你必须在那个遥远的位置执行一次 write 操作,哪怕只写一个字节。这时,文件系统才会真正地将文件大小拓展到你的目标位置。
那么,从原文件末尾到你新写入位置之间的那段巨大空间,是什么呢?它就是文件空洞 (File Hole)。
- 特点:它在逻辑上存在,你可以
lseek 到其中任何位置。当你读取它时,你会读到满屏的 \0 (空字符)。 - 优点:它不占用任何实际的磁盘空间! 只有你真正写入数据的部分才会占用磁盘块。这种文件被称为“稀疏文件 (Sparse File)”,在虚拟机镜像、数据库等场景中非常有用。
案例3:创建一个带“空洞”的大文件 这个程序会创建一个 10 字节的文件,然后通过 lseek 和 write 将其拓展到 1000 字节。
// create_hole.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>int main() {int fd = open("hole.dat", O_RDWR | O_CREAT | O_TRUNC, 0664);// ... 错误检查 ...// 写入 10 字节的初始数据write(fd, "0123456789", 10);// 将指针移动到 999 字节处lseek(fd, 989, SEEK_CUR); // 从当前位置(10)再向后移动989字节,到达999// 在 999 字节处写入一个字节write(fd, "X", 1);// 文件大小现在应该是 1000 字节close(fd);printf("hole.dat created.\n");return 0;
}编译并运行
gcc create_hole.c -o create_hole
./create_hole验证结果
- 查看逻辑大小
ls -l hole.dat
# 输出:
# -rw-rw-r-- 1 user user 1000 date time hole.datls 告诉我们,这个文件逻辑上确实有 1000 字节。
- 查看实际占用的磁盘空间
du -h hole.dat
# 输出:
# 4.0K hole.datdu (Disk Usage) 告诉我们,它实际占用的磁盘空间只有一个块的大小(通常是 4KB),远小于 1000 字节!这就是“文件空洞”的魔力。
别忘了 truncate
虽然 lseek + write 的方式很酷,但如果你只是想简单地改变文件大小(无论是增大还是截断),有一个更直接的函数:truncate。
truncate("file.path", new_size);它会直接修改文件的元数据,扩展的部分会自动用 \0 填充。它比 lseek 方案更简洁,是拓展文件的“正规军”。
总结
lseek 是一个强大而底层的工具,它让我们能够:
- 高效获取文件大小:
lseek(fd, 0, SEEK_END) - 自由定位读写: 牢记读写指针共享的特性。
- 创建稀疏文件: 通过
lseek + write 制造不占磁盘空间的“文件空洞”。

