跳到主要内容
Linux 一切皆文件:深入理解文件与文件 IO | 极客日志
C
Linux 一切皆文件:深入理解文件与文件 IO Linux 系统编程中一切资源皆文件。文件概念(内容与属性),对比 C 标准库文件接口与系统调用接口(open/close/read/write)。阐述文件描述符分配规则及标准流(stdin/stdout/stderr)机制,介绍重定向原理及 dup2 函数用法。深入分析缓冲区机制(行缓冲/全缓冲/无缓冲),解释用户态与内核态数据拷贝流程,并通过 fork 示例说明缓冲区刷新时机对程序输出的影响。
kaikai 发布于 2026/2/5 更新于 2026/7/20 6.7K 浏览一、理解文件
1.1、文件的概念
文件存储在磁盘上。(狭义)
Linux 中一切皆文件 ,即把所有需要交互的资源全部抽象成为文件:普通文件,目录文件,设备文件,管道文件...。(广义)
1.2、文件的认知
文件 = 内容 + 属性。
内容:文件存储的数据,如文本中的文字,程序二进制代码。
属性:文件的信息,包括文件名、大小、创建时间、权限、所有者等。
对于 0KB 的文件,即没有任何内容,但由于属性数据,所以占磁盘空间。
从系统角度看:对文件的操作其实是进程对文件的操作 。
二、回顾 C 文件
2.1、C 文件接口
FILE *fopen (const char *path, const char *mode) ;
2.2、实现 cat 指令
当我们执行 cat log.txt 指令,其实就是读 log.txt 文件。
#include <stdio.h>
#include <string.h>
int main (int argc, char * argv[]) {
if (argc != 2 ) {
printf ("cat error\n" );
return 1 ;
}
FILE* fp = fopen(argv[1 ], "r" );
if (fp == ) {
perror( );
;
}
buff[ ];
( ) {
ch = fread(buff, , (buff), fp);
(ch > ) {
buff[ch] = ;
( , buff);
}
(feof(fp)) ;
}
fclose(fp);
;
}
NULL
"fopen"
return
2
char
1024
while
1
int
1
sizeof
if
0
'\0'
printf
"%s"
if
break
return
0
2.3、stdin & stdout & stderr
• **stdin-标准输入流:**在大多数的环境中从键盘输入,scanf 函数就是从标准输入流中读取数据。
• **stdout-标准输出流:**大多数的环境中输出至显示器界面,printf 函数就是将信息输出到标准输出流中。
• **stderr-标准错误流:**大多数环境中输出到显示器界面。
三、系统文件 IO 系统文件 IO 就是通过系统调用的方式实现文件的读和写,C 语言文件读写的接口就是通过底层封装 Linux 系统调用接口实现的。C 语言读写文件通过 w,r,a 等选项确定,而系统调用则是通过传递标志位的方式实现的。
3.1、传递标志位的方法 定义几个宏,每个宏代表一个标志位,而标志位就是只有一个二进制位为 1 的数,当我们将参数 flags 与标志位进行按位与&操作时,只有当 flags 的对应二进制与标志位同时为 1 时,才执行相应的操作。
将文件读写的各种方式都设置一个对应的标志位,通过这种方式我们就可以控制对文件的读和写。
#define FIRST_FLAGS (1<<0)
#define SECOND_FLAGS (1<<1)
#define THIRD_FLAGS (1<<2)
#define FORTH_FLAGS (1<<3)
void Print (int flags) {
if (flags & FIRST_FLAGS) printf ("FIRST_FLAGS : %d\n" , FIRST_FLAGS);
if (flags & SECOND_FLAGS) printf ("SECOND_FLAGS : %d\n" , SECOND_FLAGS);
if (flags & THIRD_FLAGS) printf ("THIRD_FLAGS : %d\n" , THIRD_FLAGS);
if (flags & FORTH_FLAGS) printf ("FORTH_FLAGS : %d\n" , FORTH_FLAGS);
}
int main () {
Print(FIRST_FLAGS);
Print(FIRST_FLAGS | SECOND_FLAGS);
Print(FIRST_FLAGS | SECOND_FLAGS | THIRD_FLAGS);
Print(FIRST_FLAGS | SECOND_FLAGS | THIRD_FLAGS | FORTH_FLAGS);
return 0 ;
}
常用的标志位:
创建 :O_CREAT ,如对应 c 文件在以 "w" 方式打开,当文件不存在就会新建。
写:O_WRONLY ,对应 "w"。
读 :O_RDONLY ,对应 "r"。
追加 :O_APPEND ,对应 "a"。
清空 :O_TRUNC ,当以 "w" 方式写文件时,就会将文件的内容先清空。
3.2、系统调用接口
1、open——打开文件
参数:
(1)**pathname:**文件名(路径可带也可不带);
(2)**flags:标志位;
• 当我们读文件即为:**O_RDONLY;
• 写文件:**O_CREAT | O_WRONLY | O_TRUNC;
• 追加写文件:O_CREAT | O_WRONLY | O_APPEND。
(3)mode:权限位。
如果不传该参数,且文件不存在,创建的文件默认初始权限为:-r-xr-x--x
所以如果文件不存在,我们就需要设置权限位,而我们前面已经介绍过,修改文件权限可以直接用八进制数。通过0666 就可以正常设置普通文件的权限 。
返回值:文件描述符。
2、close——关闭文件 参数即为要关闭文件的文件描述符,open 函数的返回值。
3、write——写文件 **write()函数会从指针 buf**指向的缓冲区中,向文件描述符 fd 所引用的文件写入最多 **count**个字节的数据。而且系统其实并不关心写入数据的类型。
4、read——读文件 **read()函数尝试从文件描述符 fd所指向的文件中,读取最多 count个字节的数据,并将其存入以 buf**为起始地址的缓冲区中。
3.3、文件描述符 open 函数返回值即为创建或打开文件的文件描述符,但我们注意到这个数字是 3 而不是其他。
当启动一个 Shell(比如 Bash)时,Shell 进程本身会默认打开这三个标准流 ,即标准输入流,标准输出流和标准错误流。而后续在这个 Shell 中启动的子进程,也会继承这三个已打开的流。
这三个流其实也是文件,对应文件描述符为 0,1 和 2。
标准输入(stdin, fd=0) → 默认绑定到键盘
标准输出(stdout, fd=1) → 默认绑定到显示器
标准错误(stderr, fd=2) → 默认绑定到显示器
而我们在 c 语言中打开文件时,是用 FILE* 指针指向我们打开的文件,但是在底层一个整数即代表打开的文件。0,1,2,3... 是不是跟数组下标又有什么关系?这就涉及到操作系统对文件的管理
将文件的特性用一个结构体进行描述,用一个指针指向 file 结构体,然后将这个指针放在文件描述符表中,就可以进行管理了。
struct file {
union {
struct llist_node fu_llist ;
struct rcu_head fu_rcuhead ;
} f_u;
struct path f_path ;
const struct file_operations *f_op ;
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner ;
const struct cred *f_cred ;
struct file_ra_state f_ra ;
void *private_data;
};
此时,我们也就懂了为什么 log.txt 文件的文件标识符为 3 了。
文件描述符分配规则: 创建文件后,操作系统遍历文件标识符表,找到的第一个空位置就用来存放指向该文件 file 结构体的指针。
验证:我们手动关闭标准输入流文件,然后创建 log.txt 文件,观察 log.txt 文件的文件标识符。
3.4、重定向
• 输入重定向 < :将键盘读入改成从文件读入。
• 输出重定向 :将向显示器输出改为向文件输出,> 覆盖写入和 >> 追加写入。
现在我们已经知道 fd_array[0] 指向标准输入流文件,fd_array[1] 指向标准输出流文件,fd_array[2] 指向标准错误流文件。所以,我们只需要让原来指向对应流的指针指向我们的文件即可,因此:
重定向函数——dup2 dup2 为系统接口,用来将 oldfd 重定向到 newfd。
对于输入重定向 :oldfd = fd,newfd = 0;即 dup2(fd, 0)
输出重定向 :oldfd = fd,newfd = 1;即 dup2(fd, 1)
根据文件描述符分配规则,如果我们先关闭标准输出流,则新创建的文件的 fd = 1,此时我们再向标准输出流打印数据,实际上也会重定向到该文件。
int main () {
close(1 );
int fd = open("log.txt" , O_CREAT | O_WRONLY | O_TRUNC, 0666 );
printf ("xxxxxxxxxx\n" );
fprintf (stdout , "sssssssssss\n" );
return 0 ;
}
注意:有一个细节,我们最后不关闭 fd 文件,这里涉及缓冲区刷新的问题,下面会讲到。
补充:标准错误重定向 直接看效果:标准输出流重定向 ./myfie > log.txt ,其实真正的写法为:./myfile 1 > log.txt
int main () {
printf ("wwwwwwww\n" );
fprintf (stdout , "ssssssssss\n" );
const char *s = "hello Linux\n" ;
fwrite(s, strlen (s), 1 , stdout );
return 0 ;
}
标准错误流与标准输出流一样,都和显示器绑定。因此,重定向操作为:
将标准输出流内容与标准错误流的内容全部重定向到一个文件:
❌ ./myfile 1>log.txt 2>log.txt :第二次重定向时即第二次打开文件,先清空之前内容再写入,无法将所有内容重定向到 log.txt 文件。
✔️**./myfile 1>>log.txt 2>>log.txt**:追加重定向。
还有一种写法:./myfile 1>log.txt 2>&1
3.5、理解一切皆文件 Linux 中键盘,显示器,磁盘,网卡等外设,也被抽象为文件,来方便操作系统管理(先描述,再组织)。但是对于不同的外设,读写方式不同,而在 file 结构体中还有一个东西:f_op 指针
其类型为 const struct file_operations ,在struct file_operations 结构体中的成员除了 struct module* owner 其余都是函数指针 ,这些函数指针可以指向不同外设的读写的函数。
file_operation 就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都 对应着一个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从而 完成了 Linux 设备驱动程序的工作。
struct file {
const struct file_operations *f_op ;
};
struct file_operations {
struct module *owner ;
loff_t (*llseek) (struct file *, loff_t , int );
ssize_t (*read) (struct file *, char __user *, size_t , loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t , loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long , loff_t );
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long , loff_t );
int (*readdir) (struct file *, void *, filldir_t );
unsigned int (*poll) (struct file *, struct poll_table_struct *) ;
int (*ioctl) (struct inode *, struct file *, unsigned int , unsigned long );
long (*unlocked_ioctl) (struct file *, unsigned int , unsigned long );
long (*compat_ioctl) (struct file *, unsigned int , unsigned long );
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int , struct file *, int );
int (*lock) (struct file *, int , struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int , size_t , loff_t *, int );
unsigned long (*get_unmapped_area) (struct file *, unsigned long , unsigned long , unsigned long , unsigned long ) ;
int (*check_flags)(int );
int (*flock) (struct file *, int , struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t , unsigned int );
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t , unsigned int );
int (*setlease)(struct file *, long , struct file_lock **);
};
甚至管道,也是文件;将来我们要学习网络编程中的 socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。
四、缓冲区
4.1、什么是缓冲区? 缓冲区(Buffer) 是内存中开辟的一块临时存储区域 ,核心作用是协调两个速度不匹配的设备 / 组件之间的数据传输 ,通过'批量读写'替代'逐字节读写'减少高频交互的开销,提升整体效率。
缓冲区根据其对应的是输入设备还是输出设 备,分为输入缓冲区和输出缓冲区
4.2、为什么要有缓冲区? 你往硬盘写数据(硬盘速度:MB/s 级),内存速度是 GB/s 级,两者速度差上万倍;如果逐字节写,内存要等硬盘每次写完,大部分时间都在闲置;
先把数据写到内存缓冲区 ,攒够一批再一次性写入硬盘,内存不用频繁等待,硬盘也能批量处理,整体效率大幅提升。
我们向显示器打印,如果每调用一次 fprintf 就对应调用一次系统调用 write,那么系统调用的频率就会大大增加。我们知道操作系统是非常忙的,而这样就会降低操作系统的效率。
所以在用户层,设置用户态缓冲区 (C 标准库封装,针对**FILE*,C 标准库( <stdio.h>)为每个 FILE*对象(如 stdin/stdout/stderr**)维护的一块内存区域);可以看看 FILE 结构体:
typedef struct _IO_FILE FILE ;
struct _IO_FILE {
int _flags;
#define _IO_file_flags _flags
char * _IO_read_ptr;
char * _IO_read_end;
char * _IO_read_base;
char * _IO_write_base;
char * _IO_write_ptr;
char * _IO_write_end;
char * _IO_buf_base;
char * _IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers ;
struct _IO_FILE *_chain ;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset;
#define __HAVE_COLUMN
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1 ];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
当调用 库函数(fopen/fwrite/printf/fputs 等 )时,先将内容存储到缓冲区,然后按照一定的规则进行批量化的刷新,将数据刷新到文件内核缓冲区 ,就会大大降低系统调用的次数,从而提高效率。
行缓冲 (stdout 默认):缓冲区遇到换行符\n 、缓冲区满、调用**fflush()/fclose()时,才会把数据 批量传给内核**;无缓冲 (stderr 默认):数据不经过缓冲区,调用**fprintf(stderr)时 直接传给内核**,这也是错误信息能实时输出的原因;全缓冲 (普通文件默认):只有缓冲区满 或调用**fflush()/fclose()**时,才会批量传给内核(缓冲区大小一般为 4KB/8KB)。
深入了解缓冲区 int main () {
printf ("hello printf\n" );
fprintf (stdout ,"hello fprintf\n" );
const char *s = "hello fwrite\n" ;
fwrite(s, strlen (s), 1 , stdout );
const char * ss = "hello write\n" ;
write(1 , ss, strlen (ss));
fork();
return 0 ;
}
我们发现 printf 和 fwrite(库函数)都输出了 2 次,而 write 只输出了一次(系统调用)。为 什么呢?肯定和 fork 有关!
• 一般 C 库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
• printf fwrite 库函数 + 会自带缓冲区,当发生重定向到普通文 件时,数据的缓冲方式由行缓冲变成了全缓冲。
• 而我们放在缓冲区中的数据,就不会被立即刷新,甚至 fork 之后。
• 而 fork 的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online