【Linux】聊聊文件那些事:从空文件占空间到系统调用怎么玩
前言:欢迎各位光临本博客,这里小编带你直接手撕**,文章并不复杂,愿诸君**耐其心性,忘却杂尘,道有所长!!!!
《C语言》
《C++深度学习》
《Linux》
《数据结构》
《数学建模》
文章目录
- 1. 空文件(内容为0)也占磁盘空间吗?
- 2. 文件存在哪?
- 3. 谁在操作文件?
- 4. 读/写文件的本质
- 5. 举个例子
- 6. 怎么用纯C把内容输出到显示器?
- 7. `fwrite`函数怎么用?
- 8. 键盘、显示器也是“文件
- 9. `main`不是C程序的“第一站”?
- 10. 打开文件的方式很关键
- 11. 输出重定向(`>`)
- 12. 写文件别带`\0`!
- 13. 读文件比写难?用结构体就能“对得上”格式
- 14. 直接调用操作系统的`open`:文件描述符和位图传参
- 为什么用“位图传参”?`flags`的小技巧
1. 空文件(内容为0)也占磁盘空间吗?
你可能会想:如果我在电脑上建一个文件,里面什么都不写(内容是0),它是不是就像“空气”一样,不占磁盘空间?其实不是——因为文件不只是“里面写的内容”,还包含它的“身份信息”(属性) ,这俩加起来才是一个完整的文件。
就像你给朋友寄快递,快递盒里的东西是“内容”,但快递单上的收件人、地址、寄件时间、快递单号这些“信息”,也是快递的一部分,不能少。文件的属性也一样,比如文件名、创建时间、修改时间、权限、存储位置——这些属性不管文件内容是不是空的,都得存在磁盘上,操作系统要靠它们找到文件、判断能不能操作它。所以哪怕文件内容是0,属性也会占一点磁盘空间(比如Linux里空文件默认占4KB,因为磁盘按“块”存数据,最小块就是4KB)。
而且我们对文件的所有操作(读、写、复制),本质都是围绕“内容+属性”展开的。这一点从文件在磁盘的存储逻辑上也能直观看到,比如下面这张图就展示了文件内容与属性在磁盘中的关联关系:
2. 文件存在哪?
我们平时说的“文件”,默认都是存在磁盘里的——不管是机械硬盘、固态硬盘还是U盘,都算“永久性存储介质”(内存是临时存储,断电就没了,所以文件不能只存内存)。
但磁盘是“外部设备”,系统不能直接操作它,必须通过“输入输出操作”(简称“IO”)交换数据:比如读文件是“输入”(把磁盘内容读到内存),写文件是“输出”(把内存数据写到磁盘)。换句话说,所有对文件的操作(打开、读、写、关闭),本质都是系统和磁盘之间的“IO操作”——没有IO,就没法跟磁盘里的文件打交道。
3. 谁在操作文件?
你双击打开文件、用cat
看文件内容,背后其实是“进程”在干活(比如记事本进程、cat进程)——系统里只有进程能发起操作,你点击鼠标、输命令,本质是让某个进程执行“操作文件”的任务。
那进程怎么操作文件?比如用C语言写fopen("test.txt", "r")
,这是调用C标准库的“库函数”(fopen
、fclose
这些),但库函数不是“最终执行者”——它是“中间人”,底层会调用操作系统的“系统调用” (比如Linux的open
、close
)。因为只有操作系统能直接指挥磁盘,不管你用C、Python还是Java,想操作文件,最后都得走系统调用的路子。
操作系统管理文件的核心逻辑是“先描述,再组织”:当进程打开文件时,操作系统不会把整个磁盘文件搬内存(太浪费),而是在内存建一个“文件描述结构”(比如Linux的struct file
)——这就是“内存级文件”,记录文件当前读写位置、权限、磁盘位置等;而磁盘里的是“磁盘级文件”(原始文件)。操作系统会把所有“内存级文件”组织起来(比如用链表),方便管理。这个逻辑可以通过下面的示意图理解:
4. 读/写文件的本质
不管你用printf
写显示器,还是用fwrite
写文件,本质都不是靠库函数——库函数只是“打包”请求,最终要调用操作系统的“读/写系统调用”(比如Linux的read
、write
)。因为读/写要跟外设打交道,只有操作系统能指挥外设。
而且有个铁律:访问文件必须先打开。打开文件的过程,是操作系统帮你做“准备工作”(查权限、建“内存级文件”、分配文件描述符),没准备好,后续读/写都没法做。
5. 举个例子
我们在Linux终端用cat 文件名
看内容(比如cat test.txt
),背后是完整的“打开→读→写→关闭”流程,正好能理解文件操作逻辑。
比如下面这张图,执行cat test.txt
后,终端清晰显示了文件里的内容(比如“Hello, I’m a test file!”):
这个过程的底层逻辑,在下面这张流程图里更直观:
具体步骤是:
- 终端启动
cat
进程; cat
调用open
系统调用,以“只读”方式打开test.txt
——操作系统查权限、建“内存级文件”、分配文件描述符(比如fd=3);cat
调用read
系统调用,通过fd=3读文件内容到内存;cat
调用write
系统调用,把内容写到“显示器”(显示器也是外设,对应文件描述符1);- 读完后调用
close
关闭文件,释放资源。
6. 怎么用纯C把内容输出到显示器?
显示器是外设,但操作系统把它当成“文件”处理(统一接口,方便操作)。用纯C输出到显示器有好几种方式,下面这张图展示了最常用的两种:
比如用printf
:
#include <stdio.h>
int main() {printf("Hello, this goes to screen!\n"); // 默认写往stdout(显示器)return 0;
}
或者用fwrite
直接指定stdout
(stdout
是系统预定义的“标准输出”文件,对应显示器):
#include <stdio.h>
#include <string.h>
int main() {char buf[] = "Hi, fwrite to screen!\n";fwrite(buf, 1, strlen(buf), stdout); // 把buf写到stdoutreturn 0;
}
运行后,内容都会显示在显示器上——本质都是调用系统调用,把数据写到显示器对应的外设接口。
7. fwrite
函数怎么用?
fwrite
是C标准库的“写文件函数”,不仅能写磁盘文件,还能写显示器,下面这张图详细展示了它的用法和代码示例:
先看fwrite
的函数原型:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
四个参数的意思:
ptr
:要写的内容地址(比如字符串、数组的地址);size
:每个元素的大小(比如字符是1字节,int是4字节);nmemb
:要写的元素个数;stream
:写的目标(可以是fopen
返回的文件指针,也可以是stdout
)。
比如写内容到test.txt
的代码:
#include <stdio.h>
#include <string.h>
int main() {FILE *fp = fopen("test.txt", "w"); // 只写方式打开,不存在则新建if (fp == NULL) { // 必须判断打开是否成功(比如权限不够)perror("fopen error");return 1;}char content[] = "I'm written by fwrite!";// 写content到fp:1字节/元素,共strlen(content)个元素size_t write_num = fwrite(content, 1, strlen(content), fp);if (write_num != strlen(content)) {printf("Write failed! Only wrote %zu bytes\n", write_num);} else {printf("Write success!\n");}fclose(fp); // 一定要关闭文件,避免缓存数据没写到磁盘return 0;
}
运行后打开test.txt
,就能看到写进去的内容;如果把fp
换成stdout
,内容就会显示在显示器上。
8. 键盘、显示器也是“文件
操作系统的重要设计:把所有能输入输出的东西都统一成“文件” ——磁盘文件、键盘、显示器、打印机,都用“打开→读→写→关闭”操作。这样进程不用记“读键盘用A函数,读磁盘用B函数”,只要记“读文件用read
”就行。
Linux里每个进程启动时,系统会自动打开3个“标准文件”,不用手动fopen
:
stdin
(标准输入):对应键盘,fd=0(scanf
、fgets
默认读这里);stdout
(标准输出):对应显示器,fd=1(printf
默认写这里);stderr
(标准错误):对应显示器,fd=2(专门输出错误信息,比如perror
)。
比如你用scanf("%d", &a)
,就是从stdin
(键盘)读输入;用printf
,就是往stdout
(显示器)写内容——这三个文件是进程和用户交互的默认通道。
9. main
不是C程序的“第一站”?
我们以为main
是程序入口,但其实main
是“我们写的代码的入口”——C程序启动前,需要初始化栈、全局变量,还要打开stdin
/stdout
/stderr
,这些工作靠编译器和操作系统加的“启动代码”(比如Linux的crt0.o
)完成。
整个流程是:
- 操作系统加载可执行文件,启动进程;
- 进程先执行“启动代码”:初始化环境、打开3个标准文件;
- 启动代码调用
main
,我们的代码才开始跑; main
返回后,启动代码调用exit
结束进程,释放资源。
所以main
不是“第一站”——系统已经悄悄帮我们做好了所有准备,我们才能直接用printf
、scanf
。
10. 打开文件的方式很关键
用fopen
打开文件时,第二个参数是“打开方式”,比如"r"
(只读)、"w"
(只写)、"a"
(追加),下面这张图展示了不同方式的效果差异:
"w"
(只写):文件存在就清空内容,不存在就新建;"a"
(追加):文件存在就把指针移到末尾(写内容加在最后),不存在就新建;"r"
(只读):文件必须存在,否则打开失败,不会清空内容。
比如用"w"
打开test.txt
,原本的内容会被删掉;用"a"
打开,新内容会跟在原有内容后面——这两种方式的选择直接影响文件操作结果,一定要注意。
11. 输出重定向(>
)
我们在终端用echo "hello" > test.txt
,内容会写到test.txt
而不是显示器,还会清空原有内容——为什么?因为重定向时,系统用了"w"
方式打开文件。下面这张图解释了重定向的底层逻辑:
具体步骤:
- 启动
echo
进程; - 看到
>
,系统把echo
的stdout
(原本对应显示器)改成test.txt
; - 系统用
"w"
方式打开test.txt
——存在就清空,不存在就新建; echo
把“hello”写到stdout
(现在对应test.txt
);- 进程退出,系统关闭文件。
如果不想清空,就用>>
(追加重定向),这时系统用"a"
方式打开文件,新内容会追加到末尾,下面这张图对比了>
和>>
的效果:
12. 写文件别带\0
!
C语言字符串以\0
结尾,但\0
是C的“专属标记”,文件不认识它——写文件时带\0
,会导致文件里多一个“空字符”,用cat
看可能显示乱码(比如hello^@
)。
下面这张图强调了“写文件别带\0
”的注意事项和正确代码:
正确写法是用strlen
控制写的长度(只写实际内容):
char str[] = "hello";
fwrite(str, 1, strlen(str), fp); // 写5个字节(不含\0)
// 错误写法:fwrite(str, 1, sizeof(str), fp); // 写6个字节(含\0)
记住:文件只存“实际内容”,不用带C语言的\0
标记。
13. 读文件比写难?用结构体就能“对得上”格式
很多人觉得“读文件难”,是因为不知道“文件内容按什么格式存的”——比如写了“张三 20”,读的时候按“读整数→读字符串”就会错。解决办法是用“结构体”定义存储格式,写和读都按结构体来,就能“对得上”。
比如存“用户信息”(名字、年龄、身高),定义结构体struct User
,写的时候按结构体写,读的时候也按结构体读——这样读出来的数据不会乱。
14. 直接调用操作系统的open
:文件描述符和位图传参
fopen
是库函数,底层调用操作系统的open
系统调用——如果想“跳过中间人”,可以直接用open
,它返回“文件描述符(fd)”(非负整数,0/1/2是stdin
/stdout
/stderr
,新文件从3开始)。
下面这张图展示了open
的函数原型、参数含义和代码示例:
open
的原型(Linux下):
// 新建文件时需要传权限,已有文件不用
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname
:文件名(比如"test.txt"
);flags
:打开标记(用位图传参,下面详细说);mode
:文件权限(比如0644
,只有flags
带O_CREAT
时需要)。
比如新建并打开test.txt
的代码:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {// 只写+新建+清空(O_WRONLY|O_CREAT|O_TRUNC),权限0644int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd == -1) { perror("open error"); return 1; }char buf[] = "Write via open!";write(fd, buf, strlen(buf)); // 用write系统调用写内容close(fd);return 0;
}
为什么用“位图传参”?flags
的小技巧
flags
是open
的关键参数,用“位图传参”——一个整数的每一位代表一个功能标记,比如第0位是“只读”,第2位是“新建”。这样做高效灵活:组合标记用“或(|)”,判断标记用“与(&)”。
下面这张图展示了常见flags
的位图含义:
比如O_WRONLY | O_CREAT | O_TRUNC
,就是“只写+新建+清空”的组合——对应的二进制位分别置1,操作系统通过判断这些位,就知道要执行哪些操作。位图传参不仅用在open
,很多系统调用(比如fcntl
)都用这种方式,是操作系统设计的常用技巧。
理解这些逻辑,再看各种语言的文件操作代码,就能从“记函数”变成“懂原理”——不管是C的fwrite
、Python的open
,还是Java的File
,背后都是这套操作系统级的设计思路。