Linux——基础IO

很楠不爱 2024-09-30 09:37:01 阅读 56

一.文件理解

我们要进行文件操作,前提是我们的程序跑起来了,文件的打开和关闭,是CPU在执行代码

打开一个文件,本质上是进程打开文件

文件没有被打开时,会在磁盘中存在。

进程能够打开很多文件,很多情况下,在OS内部,一定存在大量被打开的文件

每打开一个文件,在OS内部,一定要存在对应的描述文件属性的结构体,类似PCB。 

“>”符号,即输出重定向,也是文件操作,可以用来新建文件,清空文件,与用“w”方式打开文件一样。 

“>>”符号,为追加重定向,与用“a”方式打开文件一样。

文件->磁盘->外设->硬件->向文件中写入,本质是向硬件中写入->用户没有权利直接写入->OS是硬件的管理者->通过OS写入->OS必须给我们提供系统调用(OS不相信任何人)->fopen/fwrite/fread/fprintf/fscanf/scanf/printf/cin/cout...->我们用的C/C++等语言,都是对系统调用接口的封装


 二.系统调用文件操作

#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); 

#include<unistd.h>

//文件关闭

int close(int fd);

#include<unistd.h>

//文件写入

ssize_t write(int fd,const void *buf,size_t count);(文件描述符,缓冲区,缓冲区大小)

#include<unistd.h>

//文件读取

ssize_t read(int fd, void *buf, size_t count);(文件描述符,缓冲区,缓冲区大小)

open是OS系统下调用文件操作接口,其中第一个用于操作已有的文件,第二个用于操作新文件


1.参数 

open:

pathname:文件名

flags:文件操作方式

mode:设置文件权限(二进制方式)

由open创建的新文件都必须给出文件的权限

但是我们默认创建的权限会与系统默认的权限掩码umask其冲突 可以使用umask函数更改权限掩码

flags为文件操作方式,可以有多个同时使用,使用“|”隔开

O_WRONLY:只写O_CREAT:不存在就创建O_TRUNC:清空文件内容O_ADDEND:从末尾追加内容O_RDONLY:只读 


2.返回值

open函数的返回值称为文件描述符fd,其本质为内核的进程与文件映射关系的数组的下标

在操作系统中,每一个被操作的文件,都会链入类型为struct file的文件执行链表中,文件的属性存放在结构体中,文件的内容则存放在内核级缓存中

在进程task_struct内部,还有一个struct file_struct *files类型的指针,指向类型为struct file_struct的文件描述符表,该结构体中存在一个类型为struct file* fd_array[N]的指针数组,指针指向文件执行链表中的每个文件的地址,数组每个元素的下标即为文件描述符fd。 

当open函数打开错误时,返回 -1。

打开正确时,会返回文件映射关系的数组的下标。

该下标默认从3开始,每新建一个文件则加1。为什么不从0开始?

原因是0、1、2三个下标被占用,分别是:

0:标准输入 键盘1:标准输出 显示器2:标准错误 显示器

在OS内部,系统访问文件的时候,只认文件描述符! 

文件描述符的分配规则

进程查自己的文件描述符表,从0开始分配最小的没有被使用的fd下标给新文件。


3.open的实际作用

创建struct file;开辟文件缓冲区的空间,加载文件数据(延后);查进程的文件描述符表;获取file地址,填入对应的表下标中;返回下标。


三.重定向

#include <sys/types.h>

#include <sys/stat.h>

#include <unistd.h>

//获取文件的属性

int stat(const char *pathname, struct stat *statbuf);(路径,文件属性结构体)

int fstat(int fd, struct stat *statbuf);(文件描述符,文件属性结构体)

int lstat(const char *pathname, struct stat *statbuf);(路径,文件属性结构体)

在内核中改变文件描述符表特定下标的内容,从而使一个特定的文件描述符成为目标文件的下标,和上层无关。

例如我们先关闭标准输出stdout文件,随后再打开一个新的文件,那么标准输出stdout的文件描述符1就会给到新的文件

#include <unistd.h>

int dup2(int oldfd,int newfd); 

 重定向函数,其参数为文件描述符,作用是让newfd成为oldfd的副本。从而使文件描述符newfd成为oldfd所指向的文件的下标本质是文件描述符下标所对应的内容的拷贝

思考一下,标准输出和标准错误都是向显示器打印,那么两者有什么区别呢???

两者有不同的文件描述符,这样就使得我们可以通过重定向将正常的输出信息和错误信息分开,以便我们去寻找错误,常用的perror函数就是向标准错误打印信息。 


四.缓冲区

在struct FILE内部,存在语言级别的文件缓冲区,printf,fprintf等语言级系统调用获取到数据后,先会放入文件缓冲区中,然后通过文件描述符,将数据冲刷进内核的文件缓冲区,最后在冲刷进磁盘空间。 

缓冲区是一段内存空间,给上层提供高效的IO体验,间接提升整体的效率。

每一个文件都有一个自己的缓冲区。

C语言为什么要在FILE中提供用户级缓冲区:为了减少底层调用系统调用的次数,让使用C语言IO函数(printf,fprintf)效率更高。

函数会将数据先放入缓冲区,最后执行一次性刷新。


1.如何工作(用户级)

 (1)刷新策略

立即刷新。fflush(stdout)(语言层)、int fsync(int fd)(系统层)行刷新。显示器全缓冲。缓冲区写满才刷新,普通文件。


(2)特殊情况

进程退出,系统自动刷新。强制刷新


五.文件系统

在系统中存在很多的文件,但是被打开的文件只是很少的一部分

没有被打开的文件则存放在磁盘中,称为磁盘文件

想要打开一个文件,就需要通过文件路径+文件名找到该文件,进而从磁盘中将其打开。


1.磁盘的存储结构

磁盘读写的基本单位是扇区:512字节,4KB。

如何找到一个指定位置的扇区?

找到指定的磁头。Header找到指定的磁道(柱面)。Cylinder找到指定的扇区。Sector

该方法称为CHS定址法

文件其实就是在磁盘中占用几个扇区的问题


2.逻辑抽象磁盘存储

文件由很多个块构成,块从0开始记号,每个块由8个连续的扇区构成我们只需文件的起始地址,以及磁盘的总大小,那么有多少块,每个块的块号,进而转换到对应的多个CHS地址就都可以得到了每个块对应的地址称为LBA :逻辑区块地址

一整个磁盘会先分为若干个区,即我们电脑中常见的C盘、D盘等,每个区又会分为若干个组,管理好一组,就可以管理好一个分区,进而管理好好整个磁盘,这种思想称为分治。

如图所示即为区的一个分组,下面我们来看一个组中的各种构成。 

超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Bloc的信息被破坏,可以说整个文件系统结构就被破坏了。

GDT,Group Descriptor Table:块组描述符,描述块组属性信息。

inode Bitmap:每个bit表示一个inode是否空闲可用。

Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。

inode table:存放文件属性如文件大小,所有者,最近修改时间。

Data blocks:存放文件内容。

linux文件系统特定:文件内容和文件属性分开存储

linux中文件的属性是一个大小固定的集合体,即struct inode,128个字节。        

内核层面上,每一个文件属性结构体中都要有inode number,我们通过inode号标识一个文件。 

使用指令:ll -li便可以看到当前目录下的文件的inode number。 

inode编号是以整个分区为单位进行划分的,一个分区内不可能存在相同的inode编号,不同的分区则可能存在。 

目录本身也是一个文件,目录 = 文件属性 + 文件内容目录的内容即:文件名和inode编号的映射关系

由此我们能够得出一下结论: 

一个目录下不能建立同名文件。查找文件的顺序:文件名->inode编号。目录的r权限,本质是是否允许我们读取目录的内容即,文件名:inode的映射关系。目录的w权限,新建文件,最后一定要向当前所处目录内容中写入:文件名和inode的映射关系。

想要找到一个文件,就需要找到其所在的目录,但是目录也是一个文件,我们该如何找到这个目录呢?目录通常是由谁提供的呢

你或者你的进程早就已经提供了内核文件系统提前写入并组织好,然后我们提供的。 

Linux内核再被使用时,一定存在大量的解析完毕的路径,系统同样需要对这些路径进行管理

 通过struct dentry{}结构体,保存路径解析的信息,一个文件对应一个该结构体,所有的结构体通过链表管理。

但是得到inode,更前提得是,我的文件在哪一个分区中,如何寻找分区呢? 

分区->写入文件系统(格式化)->挂载到指定的目录下->进入该目录->在指定的分区进行文件操作

这样一来,只要我们知道文件在哪个目录下,就同样可以知道它在哪个分区中。 


六.软硬链接

软链接指令:ln -s + 目标文件 + 链接文件名(后缀.link)

硬链接指令:ln + 目标文件 + 链接文件名(后缀.link) 


1.特征

软链接是一个独立的文件,因为有独立的inode number。硬链接不是一个独立的文件,因为没有独立的inode number,用的是目标文件的inode。文件属性中的有一列数字即为硬链接数,即文件的磁盘级引用计数:有多少个文件名字符串通过inode number指向inode。

软连接的内容:目标文件对应的路径字符串,即windows中的快捷方式。 

硬链接就是一个文件名和inode的映射关系建立硬链接,就是在指定的目录下,添加一个新的文件名和inode number的映射关系

硬链接的作用

构建linux的路径结构,让我们可以使用"."和".."来进行路径定位。用做文件备份。


七.动静态库

所谓的库文件,本质就是把.o文件打包。 

1.静态库

静态库创建

ar -rc + 库名(前缀lib,后缀.a) + 要打包的文件

使用静态库

使用静态库有多种方式:

一是库、头文件以及main文件在同一目录下,直接使用:

gcc + main文件 + 库  

 二是将头文件添加到系统的头文件目录下,同时将库文件也添加到系统的库文件目录下

gcc + main文件 + -l库名

但是我们不推荐将第三方库添加到系统的库文件下,所以当头文件和库文件在其他路径下时,还有第三种方法: 

gcc + main文件 + -I(大i) + 指定头文件所在路径 + -L + 指定库文件所在路径 + -l第三方库名 


 2.动态库

动态库创建

 gcc -shared + 要打包的.o文件 + -o + 库名(后缀.so)

我们平时使用gcc进行编译时,如果不使用static选项时,默认自动连接的为动态库。

动态库在程序运行时,需要找到动态库并加载运行,所以使用动态库有以下几种方法:

将动态库安装到系统的动态库目录中(/lib64)。在系统动态库目录中建立软连接。将动态库所在路径导入环境变量(LD_LIBRARY_PATH)。修改.bashrc配置文件,让环境变量永久生效在/etc/ld.so.conf.d路径下新增动态库搜索的配置文件(.conf后缀),将动态库的绝对路径写入配置文件中,再通过ldconflg指令进行更新。


3.动态库VS静态库

系统默认链接的为动态库。

如果没有使用-static,并且只提供了当前的.a库,其他库正常动态链接。

-static的意义?

必须强制将我们的程序进行静态链接,这就要求我们链接的任何库都必须提供对应的静态版本。


4.动态库加载

动态库以文件形式存放在磁盘中,当程序运行需要加载动态库时,动态库先要被加载到内存中,动态库在内存中也要被OS通过链表管理在一起,随后通过页表映射到程序地址空间的堆栈共享区中。


八.可执行程序理解

我们的可执行程序,编译成功,没有加载运行时,依然具有地址。

可执行程序编译后,会形成很多行汇编语句,每条汇编语句都有它的地址,即虚拟地址。

进程创建阶段,会初始化自己的地址空间,让CPU知道main函数的入口地址。

pcb创建需要从elf和加载器拿虚拟地址和mian函数入口地址,pc指针先存着入口地址等后面OS根据页表运行;把可执行程序加载入物理内存,得到物理地址,同时在虚拟地址空间拥有虚拟地址,所以这时候就有虚拟地址和物理地址,根据这两个地址就可以构造页表印射关系,OS运行找pc指针印射物理地址执行代码,pc指针顺便存下一跳地址,然后OS和pc指针继续这种操作,代码就运行起来了。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。