Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇

前言

首先,关于文件我们最先要理解的是,文件不仅仅存储的是数据,一个文件包括 内容 + 数据。内容好理解,就是我们先要这文件存储哪一些数据,这些数据就是文件的内容。

但是,在计算机当中,有两种文件,一种是 正在打开的文件 ,另一种是 没有被打开文件。

文件没有被打开,那么就是存储在外设当中,最常见的存储数据的外设比如 磁盘,u盘,硬盘等等,那么我们知道,外设当中存储速度是比不上内存的,但是,外设有一个好处,就是存储容量大。

那么也就意味着,在外设当中存储了很多个文件, 我们如何 对这些文件进行 增删查改 ,其实靠的就是文件的属性,文件的属性有很多,比如文件的最近一次的修改时间,文件的创建的时间,文件的大小,文件的存储位置等等···  这些,在windows 当中右键某一个文件,我们也可以查看文件的属性:

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第1张图片

而,对于未被打开的文件,文件的属性就是为了方便我们来对文件 进行 增删查改 。也就是说,如何在“浩瀚”的外设当中找到文件,进行 增删查改,文件的属性不能少。

就好比是在快递站当中找快递,这些快递不是随机的,混乱的堆放在某处,然后让我们自己的一个一个找的;而是,把这个快递 编写一个快递号,比如是 12 - 3 - 4444,那么我就在 12 号货架的 第 3 层的位置,来找 4444 的快递就行了,这样不仅方便了 快递站的人来存放,也方便了 找快递的人来拿。 不会显得很乱。


文件要被打开,也就是要被修改,那么肯定是要加载到内存当中 ,而且,一个进程可能不只是打开一个文件,一般大多数情况下, 一个进程打开的文件数 是 1 : n 的关系。因为,存储进程的代码 数据,本身就是一个可执行文件当中存储的,同时,进程很可能还会打开其他的多个文件。

所以,不仅仅是外设当中有很多的文件,在内存当中,操作系统也要维护很多的 文件。

所以,这就要谈一个六字真言了 -- 先描述在组织

其实在操作系统 在 内存当中管理这么多个进程,和 文件,本质上就是管理这些进程 和 文件 的属性。操作系统 像 Linux 使用 C 语言实现的,所以,其实本质上就是用 一个个 struct 结构体来表述 一个进程 / 文件 的属性(也就是属性的集合)(这叫做描述),然后操作系统只要管理好了这些个 strutc 结构体就可以管理好 所以的进程 / 文件(这叫做组织)。而管理(组织)这些 结构体对象,本质上就是用 某种 数据结构来维护某种关系。

比如 进程 等待 硬件资源,但是这个硬件当前可能是多个进程在等待资源,那么就有先后顺序,就要等待。所以,在Linux 当中,基本上每一个 硬件在操作系统当中都有一个等待队列,用于进程之间在这个硬件上的排队。

所以,我们才说,在Linux 当中一切都是文件,为什么,就因为上述我们所说的 -- 先描述在组织


所以文件也是一样,操作系统在内存当中维护这些个资源,本质上就是维护 操作系统为 这个加载到内存当中文件 所 创建的 文件结构体对象

你在很多的编程语言当中看到的 ,为什么 像类似 fopen()这样的函数,返回的往往是一个 文件对象的指针,其实就离不开 操作系统当中为了更好管理文件,为这个文件所单独创建的 文件结构体对象。两者实际上是大同小异的。

文件

在C 语言当中的 各个文件接口(函数)

首先当然是 fopen()函数打开一个文件:
 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第2张图片

可知道 fopen()函数返回的是一个 FILE*(文件指针 / 文件句柄) 。

  • path : 打开文件的路径;(相对路径,绝对路径,或者不带路径)
  • mode:打开文件的方式;

后续我们对这个文件的操作都需要 fopen()函数的返回值,也就是这个 FILE* 文件指针。

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第3张图片

  • 如果你以 "w" 写的方式打开一个文件的话,如果文件在你传入的路径之下不存在,那么就会自动创建一个文件;
  • 而且,"w" 方式写入数据到文件的话,如果文件当中有数据,那么就先把文件的长度清零,然后在文件的最开始处写入。(这种方式 和 Linux 命令行当中的 ">" 输出重定向 向文件当中的写入方式是一样的。如 echo "hello Linux!" > text.txt  这个文件当中输入 "hello Linux!"字符串,就是以 'w' 的方式来写入的。所以像 "> text.txt"  这样的方式就可以 清空 text.txt 这个文件当中的数据
  • "a" 是在文件末尾处以 追加的方式来写。 同样 , 如果文件在你传入的路径之下不存在,那么就会自动创建一个文件;(这种方式 和 Linux 命令行当中的 ">>" 输出重定向 向文件当中的写入方式是一样的。">>" 就是追加的方式来在文件当中输入数据的
  • 对于 path 路径,如果你只是直接写上文件名,没有带上路径,那么就会默认是在当前 路径下查找这个文件。(此处的 "当前路径"指的是 调用 fopen()函数的 进程的当前路径)。

在 /proc/进程PID 当中就有这个进程对应的 当前工作路径:

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第4张图片

所以,如果把这个进程的  cwd,也就是这个进程的当前工作路径给改掉了,那么像 fopen("text.txt",'w'); 这样的操作,就会在新修改的 工作路径的下创建这个新文件。


c 程序在运行之时,会自动打开 三个 标准输入输出流

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第5张图片

 像 fprintf()这些函数可以像写入数据到文件,或者是读取数据到程序当中的一样,来像上述的 三种 标准输入输出流当中输入和输出数据:


上述只是简单的把 C 语言当中的一下文件操作浅谈一下,因为不是本篇博客的主要内容,所以不在过多阐述。

被打开的文件(被加载到内存当中的文件)

 我们说文件是存储在 磁盘上的,而磁盘是外部设备,访问文件的操作,其实是在访问硬件!!所以,其实 三个标准输入输出流也是在访问文件。

 又因为,像在 C 语言当中使用 printf/fprintf/fscanf/fwrite/fread/fgets/gets ···· 这些函数,想访问文件的话,其实就是想访问对应的硬件。这些函数使用用户所书写的,用户不能直接绕过中间的层级,直接访问到最底层的 硬件:

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第6张图片

 如上图所示,用户和底层硬件之间 还有 很多层级。只能一层一层去访问到 底层硬件

所以,这些访问到硬件的函数,本质上是 用户操作接口,然后在这些函数的实现当中,就需要使用到 系统调用接口几乎所有的库,只要是想要访问到底层硬件设备,都必定是要封住 系统调用接口的。

Linux 当中常见的 文件系统调用接口介绍

 open()函数

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第7张图片

#include 
#include 
#include 

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

    pathname: 要打开或创建的目标文件
    flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。

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

上述这三个常量,必须指定一个且只能指定一个

    O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
    O_APPEND: 追加写

返回值:

    成功:新打开的文件描述符
    失败:-1

上述的 类似 O_RDONLY 的5个参数,其实是宏,上述的 flags 这个参数,其实是作 标志的作用,用户表示某一些状态是否存在,比如 O_RDONLY 就表示 只读打开。

以往我们使用一个变量作为一个标志位的时候,一般是这一个变量只能作一个标志位,也就只能表示一种状态。但是其实,一个 int 变量,有 32 个bit 位,如果每一个位表示一个状态的话,那么按道理这个 int 变量是可以表示 32 种状态是否存在的。

上述的 flags 变量就是一个 比特位方式的标志位传递方式

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第8张图片

我们可以利用像上述一样的 二进制操作符 取出 int 变量 每一个 bit 位的值,从而判断某一个 function 状态是否存在,从而实现不同的功能。 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第9张图片

这样的话,我们在外部使用这个函数,就可以在 show 函数当中传入这些宏,而且,顺序无关,在 show 函数内部,就会一次按位与上 每一个 宏,从而解析出 当前传入了多少个宏(状态)。 


O_WRONLY 参数  和 O_CREAT   参数

而且,O_WRONLY 这个参数,如果路径当中没有 此文件的话,不会创建的新的文件。除非又引入了 O_CREAT  参数。

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第10张图片

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第11张图片

此时,在进程的当前目录下是没有 log.txt 这个文件的。发现:文件打开失败了,而且在进程的工作目录下没有 创建新的文件。

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第12张图片

 所以,要想上述一样 传入O_CREAT    参数告诉操作系统,如果打开文件之时,没有该文件,就创建一个新的文件。

但是我们又发现了问题:
 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第13张图片

使用上述  O_CREAT   参数来创建 文件的话,发现权限完全是不对的。

因为 在 Linux 当中新建一个文件的时候,必须要告诉 操作系统,要 创建的 新文件的 访问权限。

 我们要指定 新创建文件的权限,那么就要使用 带 mode 参数的 open()函数:

向下述一样传参:

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第14张图片

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第15张图片

 此时发现,新创建的 log.txt 文件就已经创建成功了,而且 文件的 访问权限还是 664 权限。

为什么不是 666 权限呢?因为 ,Linux 创建文件时候,有一个 umask 来控制 新创建的文件的权限。 

 如果必须要使用 666 权限的话,有一个 umask()系统调用接口
 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第16张图片

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第17张图片

进程的代码当中调用 umask ()函数,可以把当前进程的 umask 值修改为传入的参数的值。 

 调用 umask ()函数之后,在系统当中是系统的 umask,在进程当中是进程自己的 umask

两个 umask,那么进程此时应该使用 哪一个 umask 呢?使用 就近原则。此时在进程当中,最近就有一个新刷新的 umask,那么就使用 这个 umask,如果没有,就使用系统当中的。


open()函数的返回值是一个 整形

 这个返回值 称之为 -- file descriptor ,换句话说 就是 文件描述符。代表的意思就是:如果打开文件成功,返回的是一个  >0 的数,此时这个数代表的就是一个 文件。


O_TRUNC参数 

O_WRONLY  参数不仅不会 帮我们创造新的文件,而且,它不是像 fopen()函数当中的 "w" 一样,先把文件当中的数据进行清空,然后从文件的开始处进行书写;它虽然依然是从文件的开始处进行书写,但是, O_WRONLY 不会 把文件当中的数据进行清空;

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第18张图片

 所以,如果先要实现 fopen()函数当中的 "w" 参数一样的效果的话,需要加上 O_TRUNC 这个参数,它所实现的功能就是 把文件当中原本的数据清空

此时才能实现   fopen()函数当中的 "w" 参数一样的效果。


O_APPEND 参数

 这就是追加模式,在文件末尾追加数据。

很显然,追加 参数 和 清空 参数,两个参数是冲突的。

此时我们进行追加的方式在文件当中输入数据:
 


close()函数

关闭文件。

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第19张图片

fildes: 文件描述符,也就是上诉 open()函数当中,打开文件成功返回的  file descriptor。

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第20张图片


 write()函数

像文件当中写入数据:
 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第21张图片


 文件系统调用接口 和 文件库函数的关系

 我们之前说过,库函数是用户层面使用的接口,用户如果想要访问到 底层硬件的话,必须要一层一层往下去调用,才能调用到 底层硬件,或者是下层应用,数据或者说是 接口。

操作系统不会容许 用户,或者是上层的各种接口 跨层级 去访问到下层当中的数据,硬件,接口等等。

 所以,才会出现了上述所说的 文件系统调用接口 和 文件库函数的关系。库函数要想访问下层,只能通过系统调用接口。

 他一定是像下述类似的方式来在调用系统调用接口的:

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第22张图片


 fd 文件描述符 

 操作系统也要维护 打开了的文件,这些文件都是被加载到内存当中的来才能进行 增删查改的操作的。

操作系统如何进行管理这么多的文件,用的就是 --- 先描述在组织

 谁管理谁来描述这个被打开的文件,操作系统要把这个 被打开的文件的属性,放到一起(也就是放到一个 struct 一个结构体当中),每一个被打开的文件,都会在内核当中创建一个 内核结构体。比如 我们把这个内核结构体 称职为 --- struct file。

在这个 struct file 当中,之间或者间接的包含如下属性:

  • 在磁盘的什么位置?
  • 基本属性:权限,大小,读写位置(当前要从那个位置开始读写(开始位置的偏移量)),谁打开的·······
  • 文件的内核的缓冲区信息(文件被打开时,这个文件在内核当中对应的内核空间位置)(将来要想文件当中写数据,会先写到这个缓冲区当中,然后操作系统会定期从 缓冲区当中 向磁盘当中拷贝数据)
  • ·····

上述只是举了一些例子,其实一个文件还有很多信息,每一个被打开的文件,这些信息都会保存到一个 struct file 当中,操作系统可能要维护很多个 struct file;

操作系统会以 双链表的形式,把这一个一个的 struct file 链接在一起
 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第23张图片

现在,我们对被打开的文件进行操作的话,就变成了对 这个 文件结构体对象双联表  的 增删查改
这个过程就是  先描述在组织 的过程,和进程当中  先描述在组织  没什么区别。


在前言当中也进行了说明:一个进程打开的文件数 是 1 : n 的关系。所以,我们如何知道,哪一个进程 打开了那些文件?或者说是,这个进程 和 那些文件有什么关系?  进程 如何和 自己打开的文件做关联的?    这些都是需要 进程 的 PCB对象当中需要去维护 的。

进程PCB 当中会存在一个指针,比如是 struct files_struct* f ; 这个

源码当中是这样写的:

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第24张图片

这个指针指向一个 files_struct 结构体对象,在这个 files_struct 结构体对象 当中有一个数组 -- struct file * fd_array[]

struct file * fd_array[]  数组是一个指针数组,数组当中每一个元素都指向一个文件对象,而,在这个数组当中的文件对象 所 对应的文件,就是这个进程所 打开的文件

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第25张图片

 如上如图所示,这个 数组维护的就是一个一个 文件对象,所以我们使用 open()函数之时,open()函数的返回值就是一个 int 类型的返回值,其实对应的就是 这个 struct file * fd_array[]  数组 下标。而这个 struct file * fd_array[]  数组 我们称之为 -- 文件描述符表

在这个 文件描述符表当中就存储了 这个进程所打开的 各个文件的 struct file 文件的对象的地址

 所以,当我们调用open()函数打开一个文件,那么 就会帮这个文件创建一个 struct file 文件对象,然后,在 struct file * fd_array[]  数组当中,找到一个没有被使用过的 成员位置,把这个文件对象的 地址 存储到这个成员位置。

然后,把这个 成员 的下标,返回给用户。


 所以,之所以 被打开的文件(被加载到内存当中文件)为什么要单独用一个双链表来连接起来呢?

因为 ,进程可能会中途退出,但是文件可能还会被访问修改,而且打开文件,把文件内容加载到内存当中是操作系统做的事情,不是进程做的事情,进程只能向操作系统申请某一个文件的打开,但是不能直接访问到这个文件。

所以,关于文件的加载和操作系统所做的事情,那么操作系统就要用 一个数据结构来关联 为各个文件创建的 文件对象。方便操作系统进行管理。

进程要想和 操作系统维护的 文件对象数据结构 的话,就要用到上述所说的  files_struct 结构体对象。在这个 files_struct 结构体对象 当中有一个数组 -- struct file * fd_array[] 。这个数组就存储了这个进程当前所打开的文件的文件对象的地址。所以,就用这个数组的下标的方式 和 操作系统维护的 文件对象数据结构 联系起来了。

 stdin,stdout,stderr 三个标准输入输出流的 文件描述符

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第26张图片

在上图的程序当中,fd1 - fd4 是分别打开的四个文件的 下标返回值。那么输出结果是什么呢?

输出:
 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第27张图片

 发现,输出结果是 3 - 6 四个连续的下标,但是,fd1 不是我们打开的第一个文件吗?为什么fd 是从 3 开始而不是 从 0 号开始存储呢?

是不是 前三个 (0 - 2 下标的三个位置)已经存储了其他的文件了呢?

是的

在上述我们说过,运行一个C语言程序,默认就会打开三个标准输入输出流, stdin,stdout,stderr。而这  三个 标准输入输出流 其实本质上就是  文件

 而这 stdin,stdout,stderr 三个 标准输入输出流的名字,其实是 C语言当中规定的,不是操作系统当中规定的

操作系统当中,对于文件,只认 fd(文件描述符)。

 这三个对应的输入输出流的, stdin,stdout,stderr  对应 fd 就是 0 ,1 , 2

所以,对于 stdout,stderr   两个文件当中写入数据的话,程序执行就会直接 在屏幕上输出我们在文件当中写入的内容:
 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第28张图片

输出:

 同理,我们还可以从 stdin 文件当中读取数据到 程序当中,使用 read()函数可以从文件当中读取数据出来:
 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第29张图片

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第30张图片

输出:
 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第31张图片

在上两个例子当中,我们什么文件都没有打开,(0,1,2)三个 标准输出输入的文件是 默认打开的。


操作系统当中的  三个标准输入输出流

 stdin,stdout,stderr 三个标准输入输出流 是 C语言的特性吗?

肯定不是

各种语言肯定会已有自己的  三个标准输入输出流,这  三个标准输入输出流 不是C语言特有的。

所以,其实不是 C语言的程序在运行之时 自动打开 这 三个标准输入输出流。

  • 而是,这三个 标准输出输入流 文件,在操作系统启动之时,就已经被打开了
  • 所有的进程,只是把操作系统在 文件对象双链表当中,找到这三个文件,把这个三个文件 链接到 自己的  文件描述符表 当中。

 因为使用进程 和调试进程的 用户和程序员,就是需要程序的输入输出来查看 进程运行结果


 在 C 当中的 FILE 结构体,是 C库当中自己封装的一个结构体。因为操作系统当中只认 fd 文件描述符,所以在 FILE 这个结构体当中,一定封装得 有 fd 文件描述符

 而, C 当中的 stdin,stdout,stderr 三个标准输入输出流 他们的类型就是 FILE 结构体,所以在这 stdin,stdout,stderr 三个标准输入输出流  当中,一定有 对应的 保存 fd 文件描述符  的 成员属性

 所以,我们现在就来验证一下这 stdin,stdout,stderr 三个标准输入输出流 当中的  fd 文件描述符:

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第32张图片

程序输出:
 

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第33张图片

发现:

 stdin,stdout,stderr 三个标准输入输出流 对应的 fd 就是 0 , 1 , 2。

 同样的,如果使用 close ()接口之间关闭了 fd 为   某一个 文件(0 , 1 , 2,那么就相当于是这个文件没有在这个进程当中打开了

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇_第34张图片

 输出:

发现程序没有任何输出,因为 printf()函数就是在 stdout 当中输出,所以,这个 stdout 文件已经被关闭了,就不能在输出了 printf()函数底层必定用了 1 号文件

 其实这里的 printf()函数是写入成功了,已经在 操作系统当中的 显示器文件 当中输入了,但是当前进程已经把这个 显示器文件 从 数组当中剔除了,不能再打印了。

操作系统 文件对象双链表 中的 引用计数

 操作系统维护的 文件对象双链表 当中的文件对象,肯定是被很多的进程所指向的。一个文件对象,可能要多很多个 进程所服务。

比如 stdin,stdout,stderr 三个标准输入输出流 ,这三个文件,就是要本很多文件所使用的。

那么,操作系统如何知道一个文件被多少个 进程所使用呢?

答案就是使用 引用计数

 在 每一个文件对象当中都有一个 count 的字段,这个字段就 记录了 当前有多少 个 文件描述符指向这个文件对象。

 如果 count 字段不为了0,说明当前,虽然有进程 不再引用这个 文件对象了,但是还有进程 在引用 这个文件对象,那么就不能释放这个 文件对象空间。必须要等到 count 为0 才能释放这个文件对象空间。


 所以,一个进程 关闭文件 打开文件,本质上其实就是 把 这个文件对象当中的 count 字段           -- 或者 ++。修改 进程自己的 文件描述符表当中 对应下标当中的指针,比如 置空一个 或者 增加一个 下标位置的 指针。

在 各种语言当中 的 文件流,本质其实就是 用 结构体 或者 类 等等的方式把 fd 文件描述符 封装起来了,其中一定是有  fd 文件描述符 的。

你可能感兴趣的:(linux,单片机,stm32)