Linux文件操作:文件描述符fd

2024-08-23 09:20
文章标签 linux fd 描述符 操作

本文主要是介绍Linux文件操作:文件描述符fd,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言:
  • 回顾一下文件
    • 提炼一下关于文件的理解:
  • 理解文件:
  • 通过系统调用操作文件:
    • 理解标志位传参:
    • 打开文件 open
    • 写入信息 write
  • 理解文件描述符:
    • 对于open的返回值:
    • ==文件描述fd的本质是什么呢?==
  • 如何理解Linux中一切皆文件?
  • 打通系统调用和C语言函数

前言:

​ 现在我们对进程的总体概念也有了了解,下面我们进入新的模块学习。关于Linux如何操作文件。其关的操作也与进程有关。

回顾一下文件

​ 我们以前就使用C语言对文件进行读和写的操作,甚至说是追加append。下面代码就是一个最简单的使用C语言操作文件的代码:

#include <stdio.h>int main()
{FILE* fp = fopen("log.txt", "w"); // 以写方式打开log.txt文件if(fp == NULL){perror("fopen");return 1;}fprintf(fp, "hello file");fclose(fp);return 0;
}

​ 最后的结果就是在当前目录文件夹下生成了对应的log.txt文件,这些我们在之前学习C语言的时候是有了解过的,我在这里也不必多说。但是我们现在是要从操作系统的角度来理解文件的,因此我们需要学习操作系统关于文件管理的操作。

​ 值得注意的是,这里以写方式操作文件
​ 1、如果文件不存在,就在当前路径下,新建指定文件。
​ 2、默认打开文件的时候,就会把里面的数据全部清空

提炼一下关于文件的理解:

文件 == 属性 + 内容

  • 首先,从语言层面(c语言,c++)来讲,我们是无法真正理解文件的。

    ——这是因为各个语言对应文件管理的接口不一样。因此我们要从操作系统的角度来学习。
    我们要进行文件操作,前提是我们的程序跑起来了,文件的打开和关闭,本质是CPU在执行我们的代码。

  • 操作文件,本质是进程在操作文件

    1. 文件在没有被打开的时候是存在于磁盘中的。
    2. 一个进程可以打开多个文件

    在很多情况下,操作系统内部一定存在大量被打开的文件,因此操作系统必须对打开的文件进行管理!!!
    谈及管理永远六个字:“先描述,在组织

    因此我们猜测,未来估计会有一个类似于task_struct的结构体对文件进行管理

理解文件:

  • 操作文件,本质是进程在操作文件。

  • 文件最是存在于磁盘之中,是外设硬件

  • 向文件写入 => 向硬件中写入

    单用户没有权限直接写入,因为OS是硬件的管理者,OS必须给我们提供系统调用(OS不相信任何人),但是对于fprintf / fscanf 等等C库函数,我们却可以向显示器 / 磁盘 等硬件中写入 / 读取。
    本质:我们用的C / C++都是对系统调用的封装(后面再谈封装)

通过系统调用操作文件:

理解标志位传参:

​ 我们先来看一份代码:

#include <stdio.h>#define ONE 1             //  1   0000 0001
#define TWO (1 << 1)      //  2   0000 0010
#define FOUR (1 << 2)     //  4   0000 0100void print(int num)
{if(num & ONE){printf("1 ");}if(num & TWO){printf("2 ");}if(num & FOUR){printf("4 ");}
}int main()
{print(ONE);printf("\n");print(TWO);printf("\n");print(FOUR);printf("\n");print(ONE | TWO);printf("\n");print(ONE | FOUR);printf("\n");print(TWO | FOUR);printf("\n");}

最后的运行结果为:
image-20240822203053894

这需要我们理解按位或的操作,才能理解上述操作,接下来我们就来介绍系统调用函数

打开文件 open

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>int open(const char* pathname,  // 表示要打开的文件的所在路径,如果只有文件名则默认在当前路径int flags, 			  // 表示打开文件的方式mode_t mode            // 给文件赋予权限,可不提供该参数 (新文件权限随机,旧文件不变));			

​ 我们在上述介绍的位图的概念,其实是为我们的参数做铺垫。对于第一个参数pathname不难理解,怎对于int flag,本质是用比特位来进行标志位的传递(OS设计了很多系统调用接口的常见方法)。传递方式就如同上述所讲的一致。

  • O_WRONLY -> 以写方式打开文件
  • O_CREAT -> 不存在则创建文件
  • O_TRUNC -> 如果文件已经存在,将原有的内容清空(截断)
  • O_APPEND -> 追加信息
  • O_CREAT -> 如果文件不存在,则创建它。需要第三个参数 mode 来指定新文件的权限。
  • O_EXCL -> 和 O_CREAT 一起使用时,如果文件已存在,则 open 失败,确保文件是新建的。
  • O_TRUNC -> 如果文件已存在且以写方式打开,则将文件内容清空。
  • O_APPEND -> 以追加的方式打开文件,写入的数据会添加到文件末尾。
  • O_NONBLOCK -> 对于设备文件,以非阻塞方式打开
  • O_SYNC -> 将写操作同步到磁盘。
  • O_DSYNC -> 类 O_SYNC,但只同步写入操作,不包括元数据的更新

代码演示:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0270); // 已权限270的方式创建if(fd < 0){perror("open");return 1;}return 0;
}

image-20240822212004029

写入信息 write

#include <unistd.h>ssize_t write(int fd,           // 文件描述符,表示要写入的是哪个文件const void *buf,  // 要写入到文件中的字符串的起始地址size_t count      // 要写入到文件当中的字节数
);

代码演示:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0270); // 已权限270的方式创建if(fd < 0){perror("open");return 1;}const char* message = "hello system call -> write!\n";write(fd, message, strlen(message));return 0;
}

image-20240822212544165

在这里肯定也存在close关闭文件的,在这里我不做过多的赘述,不要忘记就好了。

理解文件描述符:

对于open的返回值:

​ 先看代码:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>int main()
{int fda = open("loga.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fda -> %d\n", fda);printf("fdb -> %d\n", fdb);printf("fdc -> %d\n", fdc);close(fda);close(fdb);close(fdc);return 0;
}

image-20240822213428312

​ 诶,在这里我们发现为什么我们创建新的文件后,对于open返回后的值是从3开始的呢?
不是都说程序员都是从0开始数数的吗,为什么不是从0,而是从3呢?

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.

  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器

  • 你说的嘛,0 && 1 && 2是默认打开的,那么我们可以直接去找对对应的显示1号,直接写入,最后肯定也会直接打印在显示器上:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>int main()
{const char* message = "hello No.1 -> Monitor!\n";write(1, message, strlen(message));return 0;
}

image-20240822214814456

​ 结果也确实如此,这也说明了我们后面创建的文件标识符都是从3开始的。

文件描述fd的本质是什么呢?

问题就是凭什么我们可以直接对一个整形1写入,就可以在显示器显示?

image-20240822222245468

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

原理说白了就是:先描述,再组织

简单来说,open是在干嘛呢?

  1. 创建struct file
  2. 开辟文件缓冲区的空间,加载文件数据(这个过程可以延后)
  3. 查看进程的文件描述符
  4. 将struct file地址填入对应的表下标中
  5. 返回下标

所以,本质是文件映射关系的数组的下标。无论读写,都必须在合适的时候让OS把文件的内容读到文件缓冲区中。

如何理解Linux中一切皆文件?

​ 现在我们只是知道0 && 1 && 2 代表着键盘和显示器,可是这些是硬件啊,我们又该如何向这些硬件中写入和读取呢?

  • 如何使用C语言创建类?

    ​ 了解过C++的都知道,C++是一门面向对象的编程语言,而在C++中一切皆对象已经熟的不能再熟。为什么我们使用C++那么爱”类“?无非就是方便管理,正如老板肯定想的是如何赚大钱,那赚大钱的基础肯定是需要管理好手下的员工。正如使用C++目的是为了创建维护良好的项目,基础就是将数据代码管理好,因此也是需要管理!谈及管理,永远绕不开的六个字 —— 先描述,再组织
    ​ 类的出现就完美的实现了这六个字,不同于C语言创建的struct 对象,C++通过类创建出来的class 对象拥有灵活的“组织“功能,关键就是类能“描述”函数,函数就是方法,每个对象都有自己对应的方法!
    ​ 所以知道区别后,C语言想要实现C++的类,需要的正是“描述”函数,来创建动作。
    ​ 对于不同struct对象的函数,我们便可以使用函数指针来找到对应的函数,从而实现类的操作。

  • 深入剖析:

    image-20240822235724252

    ​ 对于每个硬件来说,都有自己独特的函数,例如read和write方式,但是硬件与硬件之间,甚至说型号与型号之间的read函数肯定不一样,然而我们并不需要关系这些区别,我们只负责使用就好!至于read函数是如何实现的,不必关心,因为在每个硬件都会收到对应的struct file进行管理,每个struct file内部都会有对应硬件的函数的函数指针,通过函数指针就能直接使用专门对应的函数

    不必关系底层实现,只负责使用。便是一切皆文件的意义!

    ​ 而我们又会发现,以上这种通过函数指针调用不同函数的方式,不就是我们在C++学习的多态吗!!!!

打通系统调用和C语言函数

​ 现在我们知道,系统在访问文件时,OS只认文件标识符fd。
image-20240823000619243

  • 那对于系统调用和C语言的库函数有什么关系吗?

    我们在使用C语言操作文件时,打开文件是使用函数fopen的,而函数fopen的返回值确实FILE*。
    FILE 本质是一个结构体,内部是会封装文件标识符的。

    #include <stdio.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <sys/types.h>int main()
    {FILE* fp = fopen("log.txt", "w");printf("stdin-> %d\n", stdin->_fileno);printf("stdout-> %d\n", stdout->_fileno);printf("stderr-> %d\n", stderr->_fileno);printf("fp-> %d\n", fp->_fileno);fclose(fp);return 0;
    }
    

    image-20240823001610356

    FILE 本质是一个结构体,内部是会封装文件标识符的。而这个文件标识符就是_fileno。

    因为stdin、stdout和stderr的类型如下:

    extern FILE* stdin;
    extern FILE* stdout;
    extern FILE* stderr;
    

    所以我们也能打印他们对于的文件标识符,也和上面讲的一样,默认为0、1、2

  • 为什么要进行封装?

    ​ C语言的这些函数代码,是在我们配置环境时存在于自动下载的库当中,而这个库就是我们
    熟知的——C标准库。

    ​ 我们当然可以不用C语言的方式而使用系统调用来操作文件,但是不同平台具有不同的系统调用。windows有自己的一套,Linux有自己的,mac当然也有自己的。所以为了保证代码跨平台性我们建议使用C语言的方式来操作文件
    ​C语言在封装的时候,会使用条件编译来区分操作系统的类别:

    fopen()
    {
    #if windows[...windows...]
    #elif mac[...mac...]
    #elif linux[...linux...]
    }
    

    然后通过这一份代码,针对不同的操作系统生成不同的C标准库,即可完成跨平台性!

这篇关于Linux文件操作:文件描述符fd的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1098977

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

【Linux】应用层http协议

一、HTTP协议 1.1 简要介绍一下HTTP        我们在网络的应用层中可以自己定义协议,但是,已经有大佬定义了一些现成的,非常好用的应用层协议,供我们直接使用,HTTP(超文本传输协议)就是其中之一。        在互联网世界中,HTTP(超文本传输协议)是一个至关重要的协议,他定义了客户端(如浏览器)与服务器之间如何进行通信,以交换或者传输超文本(比如HTML文档)。

如何编写Linux PCIe设备驱动器 之二

如何编写Linux PCIe设备驱动器 之二 功能(capability)集功能(capability)APIs通过pci_bus_read_config完成功能存取功能APIs参数pos常量值PCI功能结构 PCI功能IDMSI功能电源功率管理功能 功能(capability)集 功能(capability)APIs int pcie_capability_read_wo