
文章目录
理解linux下"一切皆文件"
首先,在windows中是文件的东西,它们在linux中也是文件;其次⼀些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接⼝跟文件接⼝也是⼀致的。这样做最明显的好处是,开发者仅需要使用⼀套 API 和开发工具,即可调取 Linux 系统中绝大部分的资源 。举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以⽤read函数来进行;几乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write函数来进行。
之前我们讲过,当打开⼀个文件时,操作系统为了管理所打开的文件,都会为这个文件创建⼀个struct file结构体。
以下是该结构体的部分内容:
c
struct file {
...
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
...
atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向它,就会增加f_count的值。
unsigned int f_flags; // 表⽰打开⽂件的权限
fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所有的标志在头⽂件<fcntl.h> 中定义
loff_t f_pos; // 表⽰当前读写⽂件的位置
...
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK
*/
值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体,这个结构
体中的成员除了struct module* owner其余都是函数指针。
c
struct file_operations {
struct module *owner;
//指向拥有该模块的指针;
loff_t (*llseek) (struct file *, loff_t, int);
//llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个 "signed size" 类型, 常常是⽬标平台本地的整数类型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负,
返回值代表成功写的字节数.
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);
//对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤.
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 *);
//mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤
返回 -ENODEV.
int (*open) (struct inode *, struct file *);
//打开⼀个⽂件
int (*flush) (struct file *, fl_owner_t id);
//flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤;
int (*release) (struct inode *, struct file *);
//在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.
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 *);
//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 **);
};
file_operation 就是把系统调⽤和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都对应着⼀个系统调⽤。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从⽽完成了Linux设备驱动程序的⼯作。
用一张图总结一下:

虚拟文件系统(VFS) :
struct file 结构体:进程打开文件时通过 struct file 访问,其内部包含属性、内核级缓冲区和函数指针。函数指针指向底层设备的读写方法,屏蔽了底层硬件差异。上层通过文件描述符访问文件,通过struct file 下 file_operation 中的各种函数回调,便可调取Linux系统中绝大部分的资源,让进程认为一切都是文件,即实现了 "一切皆文件"。
缓冲区:
什么是缓冲区
缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓
冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设
备,分为输入缓冲区和输出缓冲区。
为什么要引入缓冲区机制
读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么
每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调
⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的
切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。
简单来说 :引入缓冲区机制为了减少磁盘的读写次数,大大提高计算机的运行速度。
缓冲区原理图:

缓冲类型
标准I/O提供了3种类型的缓冲区:
- 全缓冲区:这种缓冲⽅式要求
填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。 - ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到
换⾏符时,标准I/O库函数将会执⾏系统调用操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤行缓冲⽅式。因为标准I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执行I/O系统调⽤操作,默认⾏缓冲区的大小为1024。 - 无缓冲区:无缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流
stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
语言级缓冲区刷新策略:
1.强制刷新:fflush(FILE *stream);
2.刷新条件满足:全缓冲:用于文件
行缓冲(行刷新):用于显示器
3.进程退出:main 函数返回时进程并未结束,可能还有一些清理工作,如关闭文件描述符和 Stdin、Stdout 等,此时会进行刷新。
内核级缓冲区刷新策略:
由于操作系统刷新策略比用户层复杂的多,具体何时刷新、采用何种策略由操作系统自主决定,但也可通过控制层接口让其立即刷新。
但是我们可以认为只要将数据交给操作系统,就相当于交给了硬件。
数据流动的本质:
数据交给系统,交给硬件---本质全是拷贝!
计算机数据流动本质:一切皆拷贝!!!
C语言层面的缓冲区在哪里?
C语言中的fprintf, int fprintf(FILE *stream, const char format, ...);就是将格式化数据写到文件里。为什么呢?大家有没有发现fgets、fgetc...中都有FILE stream参数,其实FILE 是 C 语言提供的结构体,内部封装了文件描述符和缓冲区。所以fprintf是先将格式化数据先写到stream指向结构体中的缓冲区中,需要时使用 write 系统调用,从 FILE * 里拿文件描述符(fd)把数据拷贝到操作系统内核级缓冲区。
所以C语言层面的缓冲区在哪里:FILE结构体内:
- FILE 结构体(包括它指向的 buf 缓冲区)都在你程序的
用户态内存空间中(堆 / 数据段,取决于编译器实现),属于进程私有,操作系统内核无法直接访问,只有 C 标准库能操作。 - 缓冲区通过 FILE 结构体管理,每个打开的文件(包括标准流)对应一个 FILE,其内部的 buf 指针指向缓冲区的具体内存地址;
- C 标准库负责缓冲区的创建、数据写入 / 读取、刷新(到内核缓冲区),进程退出 /fclose()/ 强制刷新(fflush())时才会把缓冲区数据传递给内核级缓冲区。
FILE是typedef出来的,在内核中可以看看FILE结构体:

研究一下这段代码:
c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <unistd.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0); // 库函数,用户级缓冲区
fwrite(msg1, strlen(msg1), 1, stdout); // 库函数,用户级缓冲区
write(1, msg2, strlen(msg2)); // 系统调用,无缓冲区,数据直接写入内核缓冲区。
fork(); // 复制进程,包括未刷新的用户级缓冲区
return 0;
}
运行一下:

重定向输出到文件呢?

输出重定向到文件后,C 标准库的缓冲区策略变为全缓冲(不是行刷新),因此hello printf和hello fwrite并没有立即写入文件,而是暂存在父进程的用户级缓冲区中。所以printf/fwrite 的用户级缓冲区未刷新就执行 fork,子进程完整复制父进程的内存空间状态,退出时父子进程各自刷新导致重复。而write是系统调用,直接把hello write写入内核缓冲区,无用户级缓冲,所以只会输出 1 次。
简单设计一下libc库:
mystdio.h:
c
1 #pragma once
2 #define SIZE 1024
3
4 #define NONE_FLUSH (1<<0)
5 #define LINE_FLUSH (1<<1)
6 #define FULL_FLUSH (1<<2)
7 typedef struct MY_IO_FILE
8 {
9 int flag;//刷新方式
10 int fileno;//文件描述符
11 char outbuffer[SIZE];//缓冲区大小
12 int size;//缓冲区有效内容长度
13 int cap;//缓冲区容量
14
15 }MYFILE;
16
17 //设计接口
18 MYFILE* Myfopen(const char* path,const char* mode);
19 void Myfclose(MYFILE* stream);
20 void Myfflush(MYFILE* stream);
21 int Myfwrite(const void* ptr,int num,MYFILE* stream);
mystdio.c:
c
1 #include"mystdio.h"
2 #include<string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include<unistd.h>
7 #include<stdlib.h>
8 MYFILE* BuyFile(int fd)
9 {
10 MYFILE* mf=(MYFILE*)malloc(sizeof(MYFILE));
11 if(mf==NULL)
12 {
13 //申请空间失败,返回并关闭文件描述符
14 close(fd);
15 return NULL;
16 }
17
18 mf->flag=LINE_FLUSH;//设置默认刷新方式
19 mf->fileno=fd;
20 mf->cap=SIZE;
21 mf->size=0;
22 //初始化内存
23 memset(mf->outbuffer,0,mf->cap);
24 return mf;
25 }
26 MYFILE* Myfopen(const char* path,const char* mode)
27 {
28 int fd=-1;
29 if(strcmp(mode,"r")==0)
30 {
31 fd=open(path,O_RDONLY);
32 }
33 else if(strcmp(mode,"w")==0)
34 {
35 fd=open(path,O_CREAT | O_WRONLY | O_TRUNC,0666);
36 }
37 else if(strcmp(mode,"a")==0)
38 {
39 fd=open(path,O_CREAT | O_WRONLY | O_APPEND,0666);
40 }
41 if(fd<0)
42 {
43 return NULL;
44 }
45 return BuyFile(fd);
46 }
47 void Myfclose(MYFILE* stream)
48 {
49 if(stream->size>0)
50 {
51 Myfflush(stream);
52 }
53 close(stream->fileno);
54 free(stream);
55 }
56 //强制刷新到内核缓冲区和外设
57 void Myfflush(MYFILE* stream)
58 {
59 if(stream->size>0)
60 {
61 //刷新到内核缓冲区
62 write(stream->fileno,stream->outbuffer,stream->size);
63 //立即刷新到外设
64 fsync(stream->fileno);
65 //刷新到外设后,缓冲区容量要清零
66 stream->size=0;
67 }
68 }
69 int Myfwrite(const void* ptr,int num,MYFILE* stream)
70 {
71 //向语言级缓冲区写入数据,本质是拷贝
72 memcpy(stream->outbuffer+stream->size,ptr,num);
73 stream->size+=num;
74
75 //检查是否要刷新
76 if((stream->flag & LINE_FLUSH) && stream->outbuffer[stream->size-1]=='\n')
77 {
78 Myfflush(stream);
79 }
80 return num;
81 }
usercode.c:
c
1 #include"mystdio.h"
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<string.h>
5 int main()
6 {
7 MYFILE* fp=Myfopen("log.txt","a");
8 if(fp==NULL)
9 {
10 perror("open fail");
11 return 1;
12 }
13 //写入文件信息
14 int cnt=10;
15 char* buf="hello linux!!";
16 while(cnt--)
17 {
18 Myfwrite(buf,strlen(buf),fp);
19 printf("%s\n",fp->outbuffer);
20 sleep(1);
21 }
22
23 //关闭文件
24 Myfclose(fp);
25 return 0;
26 }
当要写入的字符串buf,不带'\n'时,Myfwrite写入到语言级缓冲区的内容不会立即刷新到内核缓冲区,因为不满足行刷新的条件,最后进程退出才会将语言级缓冲区的数据写入到内核缓冲区中,然后再由内核刷新到外设。


