【Linux】文件描述符

思维导图

学习目标

这篇博客学习文件描述符,对文件描述符进行进一步的学习,在了解一下硬件如何与文件联系起来。

一、回顾一下文件系统

我们在C语言中学习了文件系统,连接了一下关于文件的一些函数:例如:fopen函数,fclose函数等......

|---------|---------|-------|
| 字符输入函数 | fgetc | 所有输入流 |
| 字符输出函数 | fputc | 所有输入流 |
| 文本行输入函数 | fgets | 所有输入流 |
| 文本行输出函数 | fputs | 所有输入流 |
| 格式化输入函数 | fscanf | 所有输入流 |
| 格式化输出函数 | fprintf | 所有输入流 |
| 二进制输入 | fwrite | 文件 |
| 二进制输出 | fread | 文件 |

1.1 介绍一下文件的打开模式:

|-------|----------------------|-------------|
| r(只读) | 为了输入数据,打开一个已经存在的文本文件 | 失败 |
| w(只写) | 为了输出数据,打开一个文本文件 | 建立了一个新的文本文件 |
| a(追加) | 向文本文件末尾添加数据 | 建立了一个新的文本文件 |

我们来写一段代码进行文件的一些基本操作,我们可以通过fopen函数来打开一个文件,并利用fwrite函数进行数据的写入,还有很多的写入操作;最后我们可以利用fclose函数进行文件的关闭。我们在进行文件操作时,我们需要先将程序跑起来,这样文件的打开和关闭是在CPU上进行运行的。

cpp 复制代码
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main()
{
  FILE* fp = fopen("log.txt", "w");
  if(fp == NULL)
  {
    perror("fopen:");
    return 1;
  }
  char* tmp = "Hello, Linux!\n";
  fwrite(tmp, strlen(tmp), 1,fp);
  fclose(fp);
  return 0;
}

之后,我们来看一些现象:在之前我们学习了两个符号:> 和 >> 。我们需要将这个fopen函数和这两个函数有一定的关系。

fopen以读的形式打开文件和 > 的关系

当我们以读的形式打开文件时,当文件不存在时,就需要进行创建文件;当文件存在时,我们需要将文件进行清空,然后再进行写入操作。

当我们使用echo > 指令时,会出现同一个现象:

所以,fopen函数以读的形式打开文件和 > 指令追加到文件的操作是一样的。

fopen以追加的形式打开文件和 >> 的关系

当我们以追加的形式打开文件,如果文件不存在,我们需要重新创建一个文件;如果文件存在,我们不需要进行刷新,直接将数据写入文件的末尾。

在使用echo >> 指令进行文件数据的追加:

1.2 提炼一下对文件的理解

我们的文件的打开,本质是将文件的打开和写入都是交给进程进行操作的,文件在没有打开之前,在磁盘中存放,在一个进程中,我们会打开很多文件,因此操作系统将会把文件进行统一管理,先描述后组织。 文件 = 内容 + 属性。

1.3 什么叫做当前路径?

我们使用fopen函数时,需要将要打开文件的路径进行传入,我们大多数人都只是利用当前路径进行创建文件,并进行文件的操作,我们需要将当前路径给大家进行解释:

当我们在使用fopen函数进行文件操作时,出现了未存在的文件路径,在执行完程序后,我们可以看出文件创建在当前可执行程序的路径下。

所以,当前路径是否为可执行程序的路径??

在我们获取到进程的pid,进行查询该进程信息,我们可以看到两个软链接文件cwd和exe,cwd就是进程运行时我们所处的路径,而exe就是该可执行程序的所处路径。

实际上,我们这里所说的当前路径不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。

1.4 stdin & stdout & stderr

  • C语言会默认打开三个输入输出流,分别是stdin、stdout、stderr;
  • 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

二、文件的系统调用接口

我们在进行文件的操作时,如果需要向文件中进行写入,我们需要访问磁盘,磁盘是一种硬件,因此,向文件中进行写入,本质就是向硬件中进行写入。我们用户没有权利向硬件进行写入操作,需要操作系统进行写入,操作系统必须要提供系统调用(OS不相信任何人的话),在之前的函数,就是在语言层面中对系统调用函数的封装。

下面,来介绍一下常用的系统调用函数:

open函数的原型:

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);

函数的参数部分:

  • 第一个参数:我们需要进行写入的文件路径,我们需要将其传入
  • 第二个参数:一些标志位,我们需要认识一些标志位,这些标志位通过按位与进行传参,我们需要通过位图的知识点来将每一个标志位进行分开,分别进行不同函数的操作。open函数的一些标志位的写法和用途:

|----------|--------------------------------------------|
| 标志位的写法 | 标志位的功能 |
| O_RDONLY | 只读模式,打开文件用于读取,文件必须存在 |
| O_WRONLY | 写模式,打开文件用于写入,如果文件已存在则清空文件内容,如果文件不存在则创建新文件。 |
| O_RDWR | 读写模式,打开文件用于读取和写入,文件必须存在。 |
| O_APPEND | 追加模式,打开文件用于写入,在文件末尾添加数据,如果文件不存在则创建新文件。 |
| O_CREAT | 创建新文件,如果文件不存在则创建新文件,如果文件已存在则不做任何操作。 |
| O_EXCL | 与O_CREAT一起使用,用于创建新文件,如果文件已存在则返回错误。 |
| O_TRUNC | 与O_WRONLY或O_RDWR一起使用,打开文件用于写入时清空文件内容。 |

下面,我们来讲解一下标志位是如何进行传参的,这种传参方式只需要一个整数就能发挥出好几个整数的作用。

我们先将int整数有32个比特位,每一个比特位,我们都可以表示一个信息,所以最多我们可以将每一个比特位上都放置一个函数,来进行函数的操作。这样传参的好处是:将函数的参数不需要进行堆积。下面进行代码:

cpp 复制代码
#define O_one   1  // 0000 0001
#define O_two   2  // 0000 0010
#define O_three 4  // 0000 0100
#define O_four  8  // 0000 1000

void solve(int n)
{
  if(n & O_one)
  {}
  if(n & O_two)
  {}
  if(n & O_three)
  {}
  if(n & O_four)
  {}
}
  • 第三个参数:表示创建文件的默认权限。在进行创建时,我们有可能创建出的文件的默认权限不是我们想要的,我们需要进行修改起始默认权限,这样就可以将我们想要的文件默认权限给求出。

这里来简要介绍一下umask函数:

umask函数原型:

cpp 复制代码
#include <sys/stat.h>

mode_t umask(mode_t cmask);

**umask函数的用途:**是在创建文件时设置或者屏蔽掉文件的一些权限,使用时是遵循就近原则。
write函数的原型:

cpp 复制代码
ssize_t write(int fd,const void*buf,size_t count);

函数的参数部分:

  • 第一个参数:文件描述符,我们之后的文件在进程中都是以文件描述符来进行标识,将我们要写入的文件描述符带入其中。
  • 第二个参数:要写入文件中的数据,在一个缓冲区中存储。
  • 第三个参数:写入数据的大小。
  • 本质是拷贝函数

open函数在干嘛??

  1. 创建文件file;
  2. 开辟文件缓冲区的空间,加载文件数据;
  3. 查看进程的文件缓冲区表;
  4. 将file地址填入文件缓冲区表的下标中;
  5. 返回下标。

read函数的原型:

cpp 复制代码
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

read函数的参数部分:

  • 第一个参数:fd是文件描述符,指明了我们从哪一个文件进行读取数据。
  • 第二个参数:buf是接收数据的缓冲区地址,我们将读取的数据放在缓冲区中。
  • 第三个参数:count表示期望读取的字节数。
  • 本质是拷贝函数

**read函数的返回值:**返回实际读取到的字节数

总结:

fd是文件描述符,buf是接收数据的缓冲区地址,count表示期望读取的字节数。read函数会从指定的文件中读取count个字节到buf中,并返回实际读取到的字节数。在读取过程中,文件指针会根据读取的字节数偏移。
close函数的原型:

cpp 复制代码
#include <unistd.h>

int close(int fd);

close函数的参数部分:

  • 参数fd是要关闭文件描述符。当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。

由open返回的文件描述符一定是该进程尚未使用的最小的描述符。

三、文件描述符

在上述函数的使用中,我会发现文件描述符其实是一个小整数。

3.1 0 && 1 &&& 2

  • 这三个数就是C语言互默认打开的三个输入输出流:stdin、stdout、stderr。
  • 在Linux系统中,我们会默认打开这三个描述符,分别是标准输入0,标准输出1,标准错误2;
  • 这里的0、1和2分别对应于键盘、显示器和显示器。

所以上述代码还可以通过写出下面的形式:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
 char buf[1024];
 ssize_t s = read(0, buf, sizeof(buf));
 if(s > 0){
 buf[s] = 0;
 write(1, buf, strlen(buf));
 write(2, buf, strlen(buf));
 }
 return 0;
}


而现在知道,文件描述符就是从0 开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file 结构体。表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表 files_struct, 该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

总结:文件描述符的本质就是:就是在文件结构体中的数组的下标。

3.2 文件描述符的分配规则

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
 int fd = open("myfile", O_RDONLY);
 if(fd < 0){
 perror("open");
 return 1;
 }
 printf("fd: %d\n", fd);
 close(fd);
 return 0;
}

这个结果是:fd = 3。

如果我们进行关闭0或者2,在来看结果:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
 close(0);
 //close(2);
 int fd = open("myfile", O_RDONLY);
 if(fd < 0){
 perror("open");
 return 1;
 }
 printf("fd: %d\n", fd);
 close(fd);
 return 0;
}

发现是结果是: fd: 0 或者 fd 2 可见, 文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

四、如何理解硬件在系统中是以文件的形式存在?

在上述过程中,我们知道了文件描述符0, 1和2对应的是键盘,显示器和显示器。那么我们应该怎么进行理解这个硬件和文件描述符进行关联起来的?Linux中一切皆文件!!!!

首先,在iOS设备上建立一层驱动层,我们只需要将不同的设备建立不同的驱动层,用函数指针区调用函数的使用,在驱动层上面,使用文件将每一个设备的属性和方法建立在文件中。我们可以使用函数指针来进行调用函数。因此,将硬件设备和文件练习起来。

我们可以在原码进行验证:

五、进行进一步理解上面的东西(打通)

5.1 写入文件的操作进行串联一遍

进程在打开文件时,进程会打开文件描述符表。文件描述表会指向一个文件,文件中有文件缓冲区和方法列表,我们使用write函数指定文件描述符和所要写入的数据,将数据写入文件缓冲区中,经过刷新将会刷新到磁盘中。

5.2 如何理解C语言通过FILE*访问文件呢??

在系统中,系统只认文件描述符。但是,我们在使用C语言进行文件操作时,只使用FILE*进行文件操作。fopen、fclose、fread等函数是库函数, 而标准输入、标准输出和标准错误都是以FILE*为类型的。

FILE 是一个C语言提供的结构体类型,在结构体中的属性有fd。fd = (FILE*)fp->_fileno;C语言的接口在底层实现是系统调用。

C语言为什么要这样做?

我们以后可以使用系统调用,也可以使用语言提供的文件方法,但是不推荐使用系统调用,在系统不同的情况下,系统调用的接口是不同的,代码不具有跨平台性,但是所有语言的代码是具有跨平台性的,所有语言要对不同的平台的系统调用进行封装,但是函数接口就有区别了。

相关推荐
许白掰1 小时前
Linux入门篇学习——Linux 工具之 make 工具和 makefile 文件
linux·运维·服务器·前端·学习·编辑器
longze_75 小时前
Ubuntu连接不上网络问题(Network is unreachable)
linux·服务器·ubuntu
Dirschs5 小时前
【Ubuntu22.04安装ROS Noetic】
linux·ubuntu·ros
qianshanxue115 小时前
ubuntu 操作记录
linux
AmosTian8 小时前
【系统与工具】Linux——Linux简介、安装、简单使用
linux·运维·服务器
这我可不懂10 小时前
Python 项目快速部署到 Linux 服务器基础教程
linux·服务器·python
车车不吃香菇11 小时前
java idea 本地debug linux服务
java·linux·intellij-idea
tan77º11 小时前
【Linux网络编程】Socket - TCP
linux·网络·c++·tcp/ip
kfepiza12 小时前
Linux的`if test`和`if [ ]中括号`的取反语法比较 笔记250709
linux·服务器·笔记·bash
CodeWithMe12 小时前
【Note】《深入理解Linux内核》 第十九章:深入理解 Linux 进程通信机制
linux·运维·php