Linux文件系统理解1
目录
- 一、初步理解系统层面的文件
- 1. 文件操作的本质
- 2. 进程管理文件核心思想
- 二、系统调用层
- 1. 打开关闭文件函数
- 2. 读写文件函数
- 三、操作系统文件管理
- 1. 文件管理机制
- 2. 硬件管理机制
- 四、理解重定向
- 1. 文件描述符分配规则
- 2. 重定向系统调用
- 3. 重定向命令行调用
- 五、理解缓冲区
- 1. 缓冲区介绍
- 2. 缓冲区刷新策略
- 3. 有趣现象
一、初步理解系统层面的文件
1. 文件操作的本质
在C语言里文件操作时,fopen打开文件,本质是cpu执行代码到这一行,进程帮我们创建相应的内核数据结构和相关初始化,打开文件本质是进程打开文件
2. 进程管理文件核心思想
一个进程可以打开多个文件,系统中有许多进程,所以大多数情况下,OS内部,一定存在大量的被打开的文件,同时,操作系统也要进行这些文件的管理
操作系统管理文件与管理进程的方式类似,先描述在组织,管理相应的结构体(类似于进程的pcb)
文件 = 属性 + 内容
二、系统调用层
1. 打开关闭文件函数
函数原型
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);int creat(const char *pathname, mode_t mode);#include <unistd.h>int close(int fd);//关闭一个打开的文件,参数fd为打开文件时open的返回值#include <sys/types.h>
#include <sys/stat.h>mode_t umask(mode_t mask); //动态更改当前进程创建文件时的权限掩码
参数说明
pathname为要打开文件的名称
falgs参数是用bit位来进行标志的传递,即位图,其含义为代码打开方式(只读,只写,读写,追加……),falgs常用选项如下:
O_APPEND 写的时候追加写入
O_TRUNC 写的时候清空文件
O_WRONLY 写方式打开
O_RDONLY 写方式打开
O-CREAT 打开时若没有文件,则在进程当前工作路径下创建文件
mode为新创建文件时的权限,该权限会由系统的权限掩码计算后再给设置到新文件
可以通过umask函数在进程内更改该进程创建文件时的权限掩码
umask
计算:最终权限 = mode & ~umask- 示例:
open("file", O_CREAT, 0666)
+ umask=002 → 实际权限664
返回值
返回值fd,一个整数,称为文件描述符,对应一个文件内核数据结构(在下文操作系统管理中详细说明),在后续的文件写入或者输出时,传参的fd都是文件描述符
返回值小于零打开失败
返回值非零时,打开对应文件会返回对应的值,前3个默认打开,分别是
0:标准输入 stdin 键盘
1:标准输出 stdout 显示器
2:标准错误 stderror 显示器
前3个进程启动时默认打开,一般情况下我们自己打开或者创建文件返回值从3开始依次增加
1和2都对应着显示器,为什么同时默认打开1和2
1和2对应的文件都是显示器文件,区别就是,当我们进行标准输出重定向时,只会将1号文件进行重定向,2号不会被重定向。
默认同时打开的主要原因就是我们输出信息时,有正确的信息也有错误信息,只需做一次输出重定向就可以将错误信息和正确信息分离开,标准输出重定向只会将1号文件重定向,2号文件不会改变,依旧输出在显示器上。
#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h> int main(){fprintf(stdout,"hello stdout \n");fprintf(stdout,"hello stdout \n");fprintf(stdout,"hello stdout \n");fprintf(stderr,"hello stderr \n");fprintf(stderr,"hello stderr \n");fprintf(stderr,"hello stderr \n");return 0;}
运行结果
也可以用两次重定向(在后文重定向中说明)将正确消息和错误消息分开放在不同的文件里,方便我们的调试查看信息。
2. 读写文件函数
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
//向文件fd里面写入buf指向的内容,写入内容大小为count,返回值大于0表示实际写入的字节,等于0表示什么都没写,返回-1表示失败,并且设置错误码ssize_t read(int fd, void *buf, size_t count);//向buf里面读入fd文件的内容,读入内容大小为count,返回值大于0表示实际读取的字节,等于0表示读到了文件结尾,返回-1表示失败,并且设置错误码#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>int stat(const char *path, struct stat *buf); //通过路径获取文件属性//buf为输出型参数,文件属性存入buf指向的结构体,实现函数功能,成功返回0,失败返回-1,并且设置错误码int fstat(int fd, struct stat *buf);//通过文件描述符获取文件属性int lstat(const char *path, struct stat *buf); //通过路径获取文件属性
内核中的文件属性(struct stat):
struct stat {dev_t st_dev; // 设备IDino_t st_ino; // inode号mode_t st_mode; // 文件类型和权限nlink_t st_nlink; // 硬链接数uid_t st_uid; // 所有者UIDgid_t st_gid; // 组GIDoff_t st_size; // 文件大小(字节)// ... 时间戳等字段
};
三、操作系统文件管理
1. 文件管理机制
核心思想:先描述,在组织
内核级管理
每打开一个文件时,操作系统要创建相应的内核数据结构(struct file),并且会创建文件内核级的缓存(开辟的一块空间)。内核数据结构中存在指针指向该缓存,并且会用文件的属性去初始化内核数据结构,将文件的内容加载到文件缓存里。
一个进程可以打卡多个文件,操作系统需要建立进程和文件的对应关系,所以在进程的pcb(task_struct)中存在一个struct files_struct * files指针(指向文件描述符表),struct files_struct数据结构中包含struct file* fd_array[N],一个文件指针的数组,对应该进程所打开的文件的内核数据结构file,该数组下标就是打开文件时所返回的文件描述符。
操作系统提供的系统调用可以用文件描述符快速找到文件对应的内核数据结构进行操作,在进行读写时,都必须在合适的时候让OS把文件的内容读写在缓冲区,在进行刷新
在操作系统内,访问文件只认文件描述符fd
语言级管理
在C语言中,通过封装系统调用设计出来一系列的文件操作函数(fprintf,fscnaf,fopen),在配合C语言封装的文件结构体struct FILE,我们常常定义的文件指针FILE*就是这种结构的指针。
struct FILE中封装这文件描述符,语言级的缓冲区,打开方式等信息。
如图,int _fileno 为文件描述符,_falgs为文件打开方式, _IO_write_end为该缓冲区的结束……
C语言中所有的文件操作都是对系统调用的封装。
由于在不同的系统中系统调用是不同的,因此我们写的含有系统调的代码不具备跨平台性,但是我们用C语言库中的函数,他是具备跨平台性的,因为我们在不同的平台下有不同的C语言标准库,他们底层封装的系统调用是对应系统的系统调用。
文件打开流程
- 创建
struct file
对象 - 分配内核缓冲区(可延迟加载数据)
- 查进程的文件描述符表空闲的下标
- 存储file对象地址于文件描述符表中
- 返回fd下标
2. 硬件管理机制
在Linux系统中,一切皆文件
硬件设备都会有自己的共同的属性(名称,厂商,生产日期等),这些属性都被封装在结构体中(struct device),同时,不用的硬件也有自己独特的操作方法(驱动程序中实现),比如像显示器上输出,从键盘鼠标内读取数据等,这些方法都是驱动程序中一个个的函数。
在Linux系统中,打开或者使用某一个外设时,会像管理文件一样管理硬件,创建一个struct file(文件内核数据结构),存放着对应硬件设备使用的函数的函数指针,还有指向属性结构体(struct device)的指针和属于该硬件的缓冲区的指针,向硬件设备中读或者写数据时,先在缓冲区操作,然后将缓冲区内容刷新到设备或者内存。
不同的设备的驱动程序中,类似操作的函数参数要设计相同,因为在struct file中函数指针只有一套,但要调用不同设备的方法,参数相同才可以兼容
源码中部分函数指针:
由于struct file在进行文件管理时(硬件也看做文件),即有属性(struct device,文件属性),还有方法(操作底层方法指针表,或者操作文件的方法列表),因此这也是一种类的实现,并且用相同的函数名(函数指针)来操作不同类型的硬件设备或者文件,也是用C语言实现的多态技术,这种管理技术在Linux系统中也叫做vfs(virtual file system)
四、理解重定向
1. 文件描述符分配规则
打开文件时,查文件描述符表从0开始分配,还没有被使用的最小的下标将被分配
重定向的本质是在内核中改变文件描述符表特定下标的内容,与上层无关
2. 重定向系统调用
#include <unistd.h>int dup(int oldfd);int dup2(int oldfd, int newfd);//将文件描述符表中oldfd下标对应的内容拷贝到newfd对应下标的位置
举例
#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h> int main(){const char* filename = "file.txt";//打开文件,不存在文件时新建,以写入方式打开,每次打开时清空文件int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);//重定向dup2(fd, 1);//默认向stdout中写入printf("hello Linux\n");//指定向stdout中写入fprintf(stdout, "hello Linux\n");return 0;}
运行结果:
可以看到,本应当向标准输出stdout中输出的内容输出到文件file.txt中了。
因为我们使用了重定向将该进程中文件描述符表里的fd(值为3,因为0,1,2默认被使用,从3开始分配)下标的内容拷贝到1下标位置,因此1号下标本来存放的标准输出文件对象指针被我们修改为“file.txt”文件的文件对象指针。
在底层的文件描述符表中,1号下标处为我们重定向的file.txt文件对象指针,但是上层stdout(C语言提供的FILE*指针,里面封装这文件描述符等信息)里面的文件描述符等其他信息都没有改变,在调用printf,fprintf时都向stdout中打印,stdout中封装着_fileno = 1,操作系统就会打印到文件描述符表里将下标为1处的文件指针所指的文件对象的缓冲区中,即向我们重定向的文件file.txt中打印。
3. 重定向命令行调用
简单调用
在指定被重定向的文件时,默认为标准输出重定向,即文件描述符为1的文件
可执行程序 > filename ,表示标准输出重定向到filename,不存在该文件时就新建
可执行程序 >> filename, 和> 大致相同,唯一区别就是>的重定向默认清空文件,>>是追加的打印,不会在打开文件时清空,
< filename 表示标准输入重定向
举例:
echo hello Linux > log.txt
该命令可以将本来向显示器打印的 hello Linux 打印到log.txt中
复杂调用
指令或可执行程序 1>filename1 2>filename2 ……,表示将文件描述符为1的文件重定向到filename1, 文件描述符为2的文件重定向到filename2
举例:
指令或可执行程序 1>filename 2>&1,将1号重定向到filename,在&1(1号下标的内容,即filename的地址)放入下标为2,即1,2同时指向filename
五、理解缓冲区
1. 缓冲区介绍
缓冲区就是一段内存空间,可以给上层提供高效的IO体验,间接提高整体的效率,
缓冲区有用户级缓冲区(语言提供,维护的)和内核级缓冲区(操作系统提供,维护的),缓冲区的优点有解耦(用户只需将数据交到缓冲区,缓冲区会自己刷新到下一个目标位置,一般不需要我们在进行操作,设计),提高效率。每一个打开的文件都有自己的缓冲区,语言级的缓冲区在struct FILE中(C语言中的文件指针FILE*),内核级的缓冲区在文件对象(struct file)中。
2. 缓冲区刷新策略
1.立即刷新,fflush(stdout),fsync(fd),这类函数调用可以立即刷新缓冲区,可以认为是无缓冲
2.行刷新,显示器通常采用行刷新
3.全缓冲,缓冲区写满,才刷新,普通文件通常采用全缓冲
4.进程退出,系统会自动刷新
3. 有趣现象
有下面2段代码
代码1:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>int main()
{printf("hello printf\n");fprintf(stdout, "hello fprintf\n");const char* message = "hello write\n";write(1, message, strlen(message));return 0;
}
执行结果
代码2:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>int main()
{printf("hello printf\n");fprintf(stdout, "hello fprintf\n");const char* message = "hello write\n";write(1, message, strlen(message));fork();return 0;
}
执行结果
现象:
重定向后先输出的是write,后输出printf和fprintf,
当创建子进程时,printf和fprintf被输出两遍
原因:
我们在命令行进行了重定向执行程序时,会使得原本向stdout输出的内容输出到file.txt文件中,这会改变刷新策略,显示器是按行刷新,普通文件是写满缓冲区或者进程退出在刷新,因此当"hello printf\n"和"hello fprintf\n"被写入用户缓冲区时,不会直接刷新,而调用write时,该调用是系统调用,无用户缓冲区,直接写入内核缓冲区,内核缓冲区直接进行刷新,因此第一行是"hello write",后面程序退出时在将用户缓冲区的内容刷新,先刷新到内核缓冲区,在刷新到文件,
图片转存中…(img-MnhIhwH7-1753197796549)]
现象:
重定向后先输出的是write,后输出printf和fprintf,
当创建子进程时,printf和fprintf被输出两遍
原因:
我们在命令行进行了重定向执行程序时,会使得原本向stdout输出的内容输出到file.txt文件中,这会改变刷新策略,显示器是按行刷新,普通文件是写满缓冲区或者进程退出在刷新,因此当"hello printf\n"和"hello fprintf\n"被写入用户缓冲区时,不会直接刷新,而调用write时,该调用是系统调用,无用户缓冲区,直接写入内核缓冲区,内核缓冲区直接进行刷新,因此第一行是"hello write",后面程序退出时在将用户缓冲区的内容刷新,先刷新到内核缓冲区,在刷新到文件,
创建子进程后,用户缓冲区没有写满还没有被刷新,内核缓冲区已刷新,父子进程各有自己的用户缓冲区,父子进程各自刷新一次,所以出现了printf和fprintf打印2次