0


Linux系统——基础IO

要努力,但不要着急,繁花锦簇,硕果累累,都需要过程!

1.文件基础必备概念

1.空文件也要占据磁盘空间

2.文件是由文件内容和文件属性组成的

3.文件操作等于对文件内容进行操作或者对文件属性操作,或者对文件内容和属性同时进行操作

4.标定一个文件,必须是文件路径加文件名

2.文件系统调用接口

    首先我们需要明白,文件存在在磁盘中,而磁盘又属于硬件,要想访问硬件,只能通过操作系统,而操作系统又为了保护自己,所以给用户提供了一系列的操作文件的接口,,用户可以通过这些接口直接访问文件了,下面我们一起来学习一下,关于文件对应的系统调用接口!

1.open && close

#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 open(const char *pathname, int flags);

对已经存在的文件进行操作

int open(const char *pathname, int flags, mode_t mode);

对没有存在的文件进行操作

pathname:标定一个文件,如果没有指明路径,默认在当前路径下创建文件

flags:打开文件时,可以传入多个选项

参数:O_RDONLY :只读打开,O_WRONLY:只写打开,O_RDWR:读写打开

O_CREAT:若文件不存在,则创建文件

O_APPEND:追加写

mode:指明创建文件的权限

打开失败返回-1

close:关闭文件

2.write

3.read

3.文件描述符fd

3.1什么是文件描述符

open打开一个文件,打开成功会返回一个文件标识符保存到fd中,那文件标识符fd到底是什么呢?

通过代码运行可以观察出,文件标识符是一段连续的整数,但又有一个问题就是为什么是从3开始的呢? 下面我们来介绍一下stdin,stdout,stderr

标准输入(键盘),标准输出(显示器),标准错误(显示器)

stdin,stdout,stderr是三个结构体指针,该结构体中保存了相关文件的属性包含fd

注:以上三个在程序启动的时候默认是被打开的

3.2文件描述符意义

上面,我们已经看到什么是文件描述符fd,下面我们继续研究一下为什么文件描述是这样的呢?

这其实和底层操作系统管理的结构决定的,下面用一组图例说明它们之间的关系是如何样的!

通过图例可以理解,当文件被加载到内存之后,操作系统为了方便管理,会给每个文件创建一个结构体,然后结构体中保存文件的相关属性,另外还有一个结构体,该结构体中有一个指针数组,数组中的每个元素指向每个文件,而元素保存的下标就被当作文件标识符在调用完成之后被返回了,因为在使用fd的时候,也能通过下标找到对应的文件,所以fd是一段连续的整数

3.3文件描述符的分配规则

通过上面两张图片的对比,可以发现文件描述符的规则是从未被占用下标最小的开始分配!

3.4文件描述符的重定向

当关闭标准输出之后,运行程序发现打印的内容不见了,这是为什么呢?打印的内容去哪里了呢?

此时查看log.txt的文件的时候,发现打印到显示器上的内容在文件当中,这是为什么呢?

未关闭标准输出:

关闭标准输出:

关闭标准输出之后,open打开文件,1就分配给了log.txt的fd,然后在底层中就像图中画的指向了log.txt,因为1代表的是标准输出,而printf函数就是向标准输出中打印内容,所以打印的内容就到了log.txt中了,而这种方式就被称为文件描述符的重定向

重定向的本质是:上层用的fd不变,在内核中更改fd对应的struct file*的地址

在Linux操作系统中专门提供了一个函数调用接口来完成重定向dup2

重定向包含:

:输出重定向

:追加重定向

将O_TRUNC改成O_APPEND

<:输入重定向

4.如何理解Linux下一切皆文件

    操作系统在管理硬件的时候不是直接进行管理的,而是每个硬件都有自己对应的结构体,每个结构体当中包含了各自硬件的属性信息,所以操作系统对硬件的管理就变成了对每个结构体对象的管理,所以在操作系统看来对普通文件的管理方式和对硬件的管理方式相同,所以站在操作系统的角度来看,Linux下一切皆文件!!

5.理解缓冲区

5.1什么是缓冲区

    缓冲区本质上是一块内存,当进程想要把数据写入到外设文件中时,进程就会向内存申请一块空间将数据放到缓冲区当中。

5.2缓冲区的意义

    进程向外设文件中写入数据的时候,因为访问外设速度比较慢,所以缓冲区的存在节省了进程进行IO的时间

5.3缓冲区刷新策略

    进程将数据拷贝到缓冲区当中,那什么时候缓冲区的数据会将内容写入到文件中呢?其实从缓冲区向文件中写入数据并不是单一的按时将缓冲区的内容写入到文件中,因为访问外设速度比较慢,大概90%的时间是缓冲区在等待文件准备好,只有10%的时间是将缓冲区的数据写入到文件中,所以操作系统为了将数据以更高效率的写入到文件中,定制了不同的缓冲区刷新策略。

缓冲区刷新策略包括:

1.无缓冲——立即刷新

2.行缓冲——按行进行刷新——例如将数据写入到显示器当中

3.全缓冲——缓冲区满了之后进行刷新——例如向磁盘文件中写入数据

关于以上三种缓冲区刷新策略存在两种特例:

1.用户强制刷新——例如调用fflush函数进行强制刷新

2.当进程退出的时候缓冲区会自动刷新

5.4缓冲区存在的位置

下面先来看一段现象:

    这段代码:首先调用了C语言的三个接口向显示器输出内容,然后调用了系统调用接口write向显示器输出内容,然后fork创建子进程。

程序的运行结果:

向显示器输出了四条内容,和我们的预料是相符的,然后我们将这四条内容重定向到一个磁盘文件log.txt

    此时查看log.txt中的内容的时候发现C接口向显示器输出的内容重复出现了两次,而系统调用接口向显示器输出的内容只出现了一次,这是我们发现与我们的预料是不相符的,这是为什么呢?

关于上面这个现象,是与我们的缓冲区是有关的,而且此时我们还可以得出一个结论,这个缓冲区存在的位置是在用户层中的,因为C接口函数的内容重复出现了两次,而C接口的函数存在用户层。

    进一步我们可以明确缓冲区存在的位置在stdin,stdout,stderr中,而这三个又是属于FILE* 类型的,而FILE又是一种结构体的封装,而这个结构体当中就包含缓冲区存在的位置,所以最终得出一个结论,缓冲区存在在用户层,并且在FILE中!

明确了这些之后,我们也可以对上面代码出现的现象做出解释:

    首先,如果没有进行重定向,stdout默认使用的是行缓冲区刷新策略,在进程fork之前,已经将内容输出到显示器当中了,此时进程内部不存在对应的数据了,如果进行了重定向,写入文件就不再是显示器,而是普通文件,向普通文件中写入数据时,缓冲区的刷新策略是采用全缓冲, 调用C接口的函数将内容拷贝到缓冲区不足以将缓冲区填满,所以数据并没有进行刷新,执行fork函数的时候,创建子进程,子进程创建完成之后,子进程退出,前面我们提到缓冲区的刷新存在两个特例,其中有一个就是进程退出的时候会将缓冲区的内容进行刷新,又因为进程具有独立性,子进程退出不能影响父进程,所以子进程退出时刷新缓冲区中的内容本质就是要对缓冲区进行修改,所以针对上面的原因,子进程在刷新缓冲区的内容的时候就会发生写实拷贝,所以C接口输出的内容就会出现两份,分别是子进程和父进程向文件中刷新缓冲区的内容,而write没有FILE,也就没有C提供的缓冲区,所以只出现了一次!

5.5缓冲区和操作系统的关系

    以上所说的缓冲区是指在用户层中的,当我们将缓冲区的数据刷新到文件中时,并没有直接将内容写入到对应的外设文件中,而是将内容拷贝到操作系统内部对应的内核缓冲区当中,然后最后由操作系统将内核缓冲区数据写入到文件中,而如何将内核缓冲区的数据如何写入到文件中,这是由操作系统决定的!

    对于上面将数据写入到文件中最后是由操作系统来完成的,所以存在一个潜在的问题,如果在将数据写入到文件的时候,操作系统崩溃了,就会造成数据丢失的问题,针对这个问题,有一个调用接口fsync,这个函数是专门将内核缓冲区的数据同步写入到文件中,此时就解决了数据可能丢失的问题!

6.文件系统

    上面,关于进程和被打开文件的关系,我们理解清除了,下面我们继续来看一下关于未被打开的文件,操作系统又是如何管理的呢?

    首先,我们需要明白文件是存储在磁盘上的,所以我们想真正理解操作系统是如何管理磁盘上的文件,我们首先需要理解磁盘,理解磁盘的硬件结构,磁盘的存储结构以及磁盘的逻辑结构

磁盘的硬件结构:

     通过图片可以看到,磁盘是由许多的盘片组成,一个盘片有两个盘面,两个盘面上都可以进行数据的读写,而每个盘面上都有一个磁头来回摆动(磁头和盘面是不互相接触的),中间包含马达,带动盘片进行高速旋转

磁盘的物理存储结构:

     了解了磁盘的硬件结构后我们知道了磁盘存储数据是在盘片的盘面上,那具体是如何存储的呢?这就需要我们进一步了解磁盘的物理存储结构了,观察上面的图片我们可以看到,一个磁盘是由多个盘片组成的,一个盘片有两个盘面,每个盘面上又划分了一个个的磁道,每个磁道又被划分为一个一个的扇区,数据就是存储在一个一个的扇区上的,每个扇区上存储数据的大小是相同的,都是512byte.

    现在我们已经知道了在磁盘上具体是如何存储数据的之后,那如果是在单面上我们应该如何找到对应文件存储的扇区呢?盘片在高速旋转,磁头在来回摆动的过程中,先确认是在哪一个磁道上的,然后再定位到对应的扇区上!

    上面所说的是在单面的一个盘面上定位扇区,那如何在磁盘上定位对应的扇区呢?因为一个磁盘是由多个盘片组成的。在磁盘上定位一个扇区其实和在单面盘上定位扇区本质上是相同的,因为在磁盘定位扇区还是由磁头进行完成的,前面我们已经知道每个盘面上都一个磁头,盘片在高速旋转的过程中,磁头同时摆动,每个磁头摆动的频率都是相同的,所以定位扇区,首先要定位在哪一个磁道上,然后再定位在哪一个盘面上,最后在定位在哪一个扇区上。而我们把这种定位方式就称为是CHS定位法!

磁盘的逻辑结构:

    上面我们已经认识了磁盘的硬件结构和磁盘的物理存储结构,那磁盘的逻辑结构又是什么呢?磁盘的逻辑结构通俗的讲就是将磁盘的一个个盘片由原型结构扯开变成线性结构,把这种磁盘的每一个盘片想象成为线性结构就称为是磁盘的逻辑结构。

    那为什么要有磁盘的逻辑结构呢?其实抽象成这种逻辑结构有两个原因,一个是是方便操作系统进行管理的,第二个原因是不想让操作系统的代码和硬件强耦合,因为抽象成线性结构之后,可以将一个个对应的扇区划分为一个个的区域,就像是在一个数组中对应的一个个元素,此时,操作系统就将对磁盘硬件的管理转变为对数组的管理了。现在我们要找一个扇区就只需要知道扇区对应的下标就可以了,在操作系统内部,下标就是对应的一个地址,把这种地址就称为是LBA地址!

     知道LBA地址之后,我们在逻辑上定位了扇区,但是最终操作系统还是要访问磁盘的,既然要找在磁盘对应扇区的位置,那我们必然就要用到CHS定位法,所以接下来的问题就是如何通过LBA地址转换为对应的CHS定位呢?下面来举一个对应的例子来进行一下转换:

假设:

一个磁盘有4个盘面,每个盘面有10个磁道,每个磁道上有100个扇区,每个扇区是512byte

所以可以计算出总容量=410100*512

下标范围=410100

假设现在在数组中定位到扇区在对应的125号位置,因为每个盘面有1000个扇区,所以可以计算出在哪个盘面上:125 /1000 = 0 说明就在0号盘面上,因为每个盘面上有10个磁道,所以可以计算出在哪个磁道上:125/100 =1,说明在1号磁道上,因为在每个磁道上有100个扇区,所以可以计算出在哪个扇区上125%100=25,说明在25号扇区上。

通过以上这种计算方式就可以通过LBA地址转换为CHS定位,进而找到磁盘上对应的扇区!

     上面我们已经了解了磁盘的三种结构,并且知道了磁盘被划分为一个个的扇区结构,每个扇区大小为512byte,操作系统在寻址的时候是以一个扇区为基本单位的,但是以一个扇区为基本单位进行寻址的时候依旧很小,假如说要访问一个4kb的数据,就需要进行8次IO的过程,而我们也知道每一IO都是一次访问外设的过程,而访问外设是比较慢的,所以操作系统为提高效率,在读取数据的时候,一般是以4kb为基本单位进行访问数据的,即使你只访问1个bit位的数据,也会将4kb的数据加载到内存,进行读取或者修改,而这种方式我们称为局部性原理,本质上是以空间换时间的做法!

    有了上面的这些基础知识之后,下面我们来正式看一下操作系统对于每个区域具体是如何进行管理的:

    假设你的磁盘有512GB,操作系统直接进行管理不太方便,所以操作系统将512GB空间大小进行分块和分组的方式,将一块大的空间划分为一块块的小空间,所以只需要将其中的一块小空间管理好之后,其它空间按照同样的方法进行管理,这样就可以实现管理一块小的空间实现对整个磁盘进行管理!

下面这张图片就是操作系统对磁盘进行分区和分组后的具体划分:

上面的一行就是被划分为一个块区,下面一行就是对每个分区进行分组后的结果,下面我们就来一一解释一下分组中的每个区域保存的信息:

Boot Block:保存的计算机在启动的时候,所需要加载到操作系统内部的数据

Block group 0~n:就是被操作系统划分的一个个分区区域

Super Block:保存的是整个文件系统的信息,关于这个区域你可能会有疑问,既然保存的是整个文件系统的信息,那为什么不放在分区的位置,而保存在分组的位置呢,这样做的目的是为了做备份,假如说文件系统因为某些原因而出现了故障,此时就可以用其它块区的Super Block进行恢复

之前我们已经知道文件是由文件属性和文件内容组成的

Data blocks:保存的是分组内部所有的数据块

Linux文件属性是分批存储的,保存在Inode当中,Inode一般是128byte或者是256byte,一个文件一个Inode,文件几乎所有的属性都保存在Inode中,文件名并不再Inode中存储,Inode为了区分彼此,每一个Inode都有一个自己的Id

inode table:保存了分组内部所有可用的inode(已经使用的没有使用的)

inode Bitmap:保存的inode的位图结构,如果inode被使用了对应的bit位由1表示,没有被使用就用0表示!

Block Bitmap:保存的是数据块的位图结构,与data block数据块的位置是一一对应的,如果被使用了对应的bit位由1表示,没有被使用就用0表示!

Group Descriptor Table:保存的是inode使用了多少,没有被使用了多少,data block使用了多少,还有多少没有被使用的信息!

上面我们已经介绍每个区域保存的内容了,下面如果想要查找一个文件该如何找呢?

答案是查找一个文件使用对应的Inode编号,因为可以先在Inode Bitmap确认是否对应的bit为1,然后再Inode Table中提取对应的属性信息。现在可以拿到一个文件的属性信息可,那对于文件内容又如何获取呢?我们不能直接获取data block中的值,因为在data block中保存的是一个个的分组数组块。要想回答这个问题,我们就必须要了解一下Inode了。

Inode是一个结构体

struct inode
{
    int id;//inode编号
    mode_t mode;//文件权限
    uid;//拥有者
    gid;//所属组
    size_t size;//个数
    ……
    int data_block[15];
}

inode中除了基本的属性信息之外,还有一个data_block的数组,前12个是每个下标对用一个文件

后面几个是保存的是下一个文件的地址,通过建立索引的方式将所有的文件连接起来(二级索引,三级索引……),通过这样的方式就可以找到一个文件对应的内容了!

7.软硬链接

7.1理解硬链接:

概念:和原来的文件具有相同的inode的文件

特点:建立一个硬链接根本没有创建一个新的文件,因为没有分配独立的inode

**硬链接和原来文件的关系: **

所以硬链接的本质是在指定的路径下,新增文件名和inode的映射关系!

如何标识新增的映射关系呢?

在inode中有一个引用计数,当新增了一组映射关系之后,count++,把这种引用计数也被称为硬链接数

所以一个文件正真被删除是硬链接数变为0才算该文件被正真删除了。

硬链接的作用:

当创建一个普通文件的时候硬链接数为1,这是因为文件名和inode只有一组映射关系,那创建一个空目录,为什么硬链接数是2呢?

这是因为在一个空目录中包含一个.的隐藏文件,该文件的inode与目录的inode相同,所以也就是建立了两组映射关系,所以硬链接数为2.

在empty中在创建一个dir目录:

此时硬链接数又变为3了,这是为什么呢?

这是因为在empty中又创建了一个目录,这个目录中有一个..的隐藏文件,和empty的inode相同,所以有三组映射关系,硬链接数为3.所以cd..可以返回到上级

所以硬链接存在可以回退到上一级。

7.2理解软链接:

概念:具有独立的inode的文件

** 软链接的本质**:是一个新的文件,软链接的data blocks中保存原来文件的文件路径和文件名:

因此在Linux上建立软链接就类似于windows上的快捷方式!

建立软链接的作用:

当我们在一段比较深的路径下写好一段程序,如果不在当前进程所在的路径时想要运行就必须带上一串路径,如果此时建立软链接,就可以直接不用带路径了。

8.动态库和静态库

8.1动静态库的概念:

静态库:以.a为结尾,程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。

动态库:以.so结尾,程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

~:一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
~:在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
~:动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

8.2库的本质:

我们将写的代码生成可执行二进制程序需要经历预处理,编译,汇编,链接四个阶段,当程序中调用库中包含的内容时,如果不想提供源代码,就可以将库中的.o文件(可重定位二进制目标文件),提供给使用者,然后使用者将.o文件链接起来就可以生成可执行程序了。

上面这种方式就是库实现的方式,但是这种方式存在的问题是如果存在许多.c文件,就需要提供多个.o文件,这样就是使得成本变高,所以一般会将所有的".o文件",打一个包,给对方提供一个库文件。

所以库文件的本质是:.o文件的集合

8.3制作静态库:

1.将多个.o文件打包成一个库文件:

2.使用库的本质是将库的实现方式和实现方法下载到自己的本地上,然后在使用的时候在本地找对应的内容,所以上面我们已经将库生成了,如果要想让别人使用就需要下载我们制作的静态库,所以为了让别人方便下载,我们将实现方式和实现方法放在相应的目录中

3.此时我们就将实现方式和实现方法都放在同一个目录中,然后可以将这个目录文件打包压缩,放在yum源中,使用者在使用下载对应的库,使用的时候就可以正常使用了

4.使用者下载完成后进行使用:

此时在使用的时候出现报错,说.h文件找不到,这是因为编译器在编译的时候.h文件首先会在当前路径下找,如果找不到就会在下载安装的库文件找,找不到就报错,我们已经下载完成了,为什么还会报错呢?这是因为我们的.h文件在多级路径下,不再当前路径下,并且我们并没有再库文件下安装,所以解决方式是指定路径下查找:

当指明路径之后,程序再编译阶段没有报错,在链接阶段出现了链接错误,这个是因为程序在链接阶段找不到对应的库,所以也需要指明库路径:

注意:在指明库的时候不仅需要指明库路径,还需要指明库名称,库名称指明需要去除库的前缀名lib和后缀名.a

当程序运行完成之后,查看程序链接属性,发现是动态链接的,但是我们提供的是静态库,为什么会是动态链接的呢?这个gcc编译器的实现是有关系的,gcc生成可执行程序默认是动态链接的,对于一个特定的库,是否是动态链接取决与库本身,只有静态库时属于静态链接,同时包含动态库和静态库时,链接属性为动态链接

除了上述按照指定路径和指定名字的方式实现之外,还可以将所使用的库安装到系统默认的库路径下,此时执行程序就不用在指明指定的路径了,gcc编译器默认就会去找

安装的本质就是拷贝

8.4制作动态库:

1.生成.o文件:在生成.o文件的时候多加一个-fPIC的选项

2.将.o文件打包形成动态库

3.使用动态库:

和静态库一样的使用一样,将动态库和.h文件放到指定的路径下

4.利用动态库生成可执行程序:

当运行的时候发现报错了,我们已经将库和对应的头文件都提供了,但是为什么还会报错呢?

这是因为我们将动态库和对应的头文件告诉了gcc,而gcc在编译完成之后,在运行的时候程序加载到内存中,被操作系统调度运行,但是动态库并没有放到系统默认的指定路径下,所以操作系统在找的时候找不到,因此出现了报错!

该如何解决上述问题呢?操作系统除了在系统默认的路径下查找之外,还会在下面的这个环境变量中查找,我们只需要将库路径放到该环境变量中就可以了:

注:将库路径导入到环境变量中的方法只在当前登录有效,不能永久使用。

解决方法:

第一种方法:

1.修改系统配置文件,将库路径放入到/etc/ld.so.conf.d/路径下的一个.conf文件中

此时就可以永久使用了

第二种方法:

在运行的时候,库文件会在当前路径下查找,因此可以建立软链接的方式

8.5动静态库加载:

1.静态库的加载:

采用绝对编址的方式,当代码在磁盘上编译的时候,静态库中使用的代码和自己本身写的代码会按照不同地址区域划分保存在对应的位置,然后当程序运行的时候加载到内存中,会将原来磁盘上代码的地址映射到虚拟地址空间,然后让PCB调度进程

2.动态库的加载:

采用相对编址的方式,当代码在磁盘上编译的时候,自己本身的代码会安宅不同地址区域划分保存在对应的位置,遇到动态库的代码的时候,会将对应的代码按照代码本身加偏移量的方式保存在相应的位置,(偏移量是指调用动态库的接口在库中的位置)然后当程序加载到内存之后,将地址映射到虚拟地址空间,最后将调用的动态库映射到共享区中,然后使用动态库接口的函数按照原来保存的偏移量在上下文中寻找,然后让PCB调度进程!

标签: linux

本文转载自: https://blog.csdn.net/qq_65307907/article/details/128341348
版权归原作者 终为nullptr 所有, 如有侵权,请联系我们删除。

“Linux系统——基础IO”的评论:

还没有评论