既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
当我们要访问一个文件的时候,一般都是通过进程去访问,而文件是存在磁盘中的,所以肯定是进程通过操作次用来打开文件,那么操作系统一定要给进程提供调用文件的接口!
上图为操作系统提供的打开文件的接口,C中的 fopen函数,C++中的open函数,其实底层都封装了系统中的 open 接口。
调用 open 接口需要将文件路径和打开方式、文件权限作为参数传给 open,而 open 接口的返回值叫 **文件描述符 fd,**fd 是一个整数,它是进程访问文件的基本方式!在关闭某个文件的时候,只需要给 close 接口传这个文件的 fd 即可。
对于进程访问文件,用C程序来举例子:(下面先列举对应的C语言文件接口)
比如在C文件中调用 fopen 函数打开对文件做操作,当这个C文件被编译成一个可执行程序加载到并内存的时候,就会变成一个进程,当这个进程代码运行到 fopen 的时候,就会执行打开文件操作,文件就会被加载到内存中,一个C程序可以多次调用 fopen 函数,那么说明一个进程可以打开多个文件,那么多个进程在被轮转调度的时候,就可能会打开更多的文件!
所以 进程 :打开的文件 = 1:n
打开如此多的文件,那么在内核中必须形成对应的文件描述对象(里面存文件的属性等内容),通过某种数据结构将文件管理起来,来方便操作系统组织和访问。
文件打开的方式
第一个参数是要打开文件的路径,而第二个参数是打开的方式。
第二个参数,运行位图的方式,巧妙的传递了打开方式。
第一种传参方式类似于 fopen 函数中的以 “w” 方式打开(会清空文件重新写),而第二种传参方式类似于 fopen 中的以 “a” 方式打开(在已有文件后追加内容)
如果是创建文件的话,还可以将新文件的权限作为第三个参数传过去。
进程、系统、文件之间的关系
那么,在操作系统中,进程如何对打开的文件进行管理呢?
每一个被打开的文件,都会用一个 struct file 结构体来描述它,多个文件就用多个 struct file 结构体描述,并且将这些结构体用合适的数据结构管理起来。已知,在一个进程中,每一个被打开的文件都会用一个进程描述符 fd 来表示,这个 fd 就是进程访问文件的方式。
每个进程的 struct files_struct 中都有一个 struct files_struct* file 指针,指向同一个 struct files_struct ,而struct files_struct 中保存着一个数组 fd_array,这个数组中存储着当前进程打开的所有文件的地址,而这些文件的地址在这个数组中对应的下标就是 fd。也就是说,当一个文件打开被加载到内存的时候,它的地址就被存到了 struct files_struct 结构体对应的 fd_array 数组中,并且会返回它存储位置的数组下标 fd,以后就可以通过数组下标 fd 来得到文件地址,进而对文件进行访问!
操作系统访问文件,只认文件描述符!
但奇怪的是,当我们创建多个文件,并打印出文件描述符的时候,虽然 fd 是一个连续的小整数,很符合数组的连续存储,但却不是从零开始的,这是为什么呢?
Linux 中一切皆文件!!!
其实,一个进程在运行的时候,就默认打开了三个标准输入输出:
标准输入:键盘 stdin ——> fd:0
标准输出:显示器 stdout——> fd:1
标准错误:显示器 stderror ——> fd:2
1 ——> 常规输出 2——> 错误输出(C语言中的 perror 就可以打印错误信息),也可以将正常输出信息和错误输出信息分别重定向到不同的文件中,方便管理和调试。
这三个硬件,在Linux 经过系统上层封装后,键盘、显示器等可以用统一的接口(read()、write())利用不同的变量来访问,其他的软硬件也都经过通过文件的访问方式封装,所以Linux 下一切皆文件!(VFS 虚拟文件系统)
这提前打开这三个文件是为了方便程序员写代码的。
std、stdout、stderr都返回一个 FILE* 指针(而FILE 其实是C语言提供的结构体类型),因为操作系统访问文件时只认文件描述符,所以由此推断:FILE 必定封装了文件描述符!
由此得出:每一种语言,在底层其实封装的是一种系统调用接口,这是由操作系统决定的,而且必须是一样的,不然在同一个操作系统中没法使用!
文件重定向
文件重定向的本质是:修改特定文件 fd 下标对应的文件地址
一般有三种重定向方式:
- 输出重定向(先清空,再写入)
- 追加重定向
- 输入重定向
dup2(int oldfd, int newfd); // 系统调用接口
在文件被打开后调用,即可将 oldfd 对应的地址拷贝到 newfd 对应的地址处去,这样 oldfd 和 newfd 都指向同一个文件,在内核 struct file 中会有一个 f_count 的引用计数来控制,当要 close 文件的时候,f_count 先减1,只有当 f_count 减到 0 的时候,文件才会被关闭!
进程访问文件的步骤
总结:进程要访问,第一步,必须将文件数据先加载到文件缓冲区(内存中),然后通过进程中的struct files_struct* file指针,访问 struct files_struct,找到并遍历 fd_array 数组,找到最小的,未被使用的空间,将打开文件的地址存进去,然后就可以通过这个数组下标 fd 来访问文件了!
缓冲区
缓冲区分为:语言级缓冲区(用户级缓冲区)和内核缓冲区
缓冲区的主要作用是提高使用者的效率
缓冲区因为能暂存数据,必定有一定的刷新方式。
缓冲区刷新的一般策略有三种:无缓冲(立即刷新);行缓冲(行刷新,一般是显示器文件);全缓冲(缓冲区满了再刷新,一般是磁盘文件)
特殊情况:强制刷新;进程退出时,要刷新缓冲区。
我们平时使用的缓冲区,其实是语言级缓冲区(用户级缓冲区),从C语言缓冲区写入到OS/文件缓冲区中,这个工作叫做刷新。
以C语言为例:C语言给C库函数提供缓冲区,可以提高C库函数(IO类)的调用效率(都是在用户层),而IO的本质就是拷贝,先由用户级缓冲区拷贝至内核(文件缓冲区),再由文件缓冲区拷贝至文件(硬件)
正常情况下,在进行输入输出的时候,都有一个FILE,FILE是一个结构体,C语言中的缓冲区就在FILE中(FILE 中也包含了fd),FILE 可能会提供 buffer[]数组这种类型的东西作为缓冲区暂时储存数据。
int fsync(int fd);//将数据从文件缓冲区刷新到文件/磁盘中
文件系统
无论被打开的文件还是尚未被打开的文件,都要进行管理,路径的存在就是为了解决快速定位文件,因为无论什么对文件的操作,都得先找到!文件被管理起来,就是为了方便OS进行增删查改。
在磁盘中,通过 CHS 定位法(C、H、S)三个参数读取扇区位置,不同的扇区存不同的数据,这是用软件来控制的。
将磁盘高度抽象为以扇区大小为单位的数组,那么对磁盘的管理,就变成了对数组的管理,只要知道这个文件的起始扇区、大小,就可以通过索引来访问文件。
操作系统可以基于文件系统,按照文件块为单位进行数存取。
文件块都存储在磁盘中的 blocks[n] 数组中,对文件系统的管理,其实就是对 blocks 数组的管理;对存储设备的管理,在 OS 层面上,转换成了对数组的增删查改。
文件系统存储、寻址
我的文件信息分为内容和属性,内容和数据在原则上是分开存储的。很多管理文件的数据,得先让管理系统写入到块组中。
下图为文件系统图
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
- 数据区:存放文件内容
运行下面的指令
ls -li
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!