Linux:深入了解fd文件描述符

目录

1. 文件分类

2. IO函数

2.1 fopen读写模式

2.2 重定向

2.3 标准文件流

3. 系统调用

3.1 open函数认识

3.2 open函数使用

3.3 close函数

3.4 write函数

3.5 read函数

4. fd文件描述符

4.1 标准输入输出

4.2 什么是文件描述符

4.3 语言级文件操作


1. 文件分类

文件=文件内容+文件属性

因此,一个文件里面没有写入内容,也是占据空间的。

在C语言中,我们访问一个文件之前,都必须先调用fopen接口打开文件,这是为什么呢?下面是一份访问文件操作的C语言代码。

#include <stdio.h>int main()
{FILE* fp = fopen("log.txt", "w");if (fp == NULL){perror("fopen");return 1;}const char *message = "hello file\n";int i = 0;while(i < 4){fputs(message, fp);i++;}fclose(fp);return 0;
}

当执行可执行程序时,会在当前目录下创建一个log.txt文件,并往log.txt文件写入几行文字。但是这并不意味着文件在代码编写完毕或编译成可执行程序时已经打开。实际上,文件时在程序运行并且执行到fopen函数调用时,如果调用成功,文件才会被打开。所以是谁在访问文件?本质上是进程在访问。

进程是运行程序的实例,保存在内存当中。在文件没打开之前,它是存储在磁盘中。程序的执行本质上是给CPU执行程序里的代码并进行操作。根据冯诺依曼体系结构,CPU不能直接访问外设数据,它只能访问并处理内存数据。因此,为了实现对文件的读写操作,进程需要将文件从磁盘中加载到内存当中,这个过程被称之为打开文件。

文件从磁盘中加载到内存中,加载的通常是文件的内容或属性。一个进程可以同时打开多个文件,那么多个进程打开的文件数量可能会有上百个。这么多的文件在内存当中肯定也要被管理起来。如何管理文件?先描述,在组织。 在内核中,文件=文件内核数据结构+文件内容

而进程也有自己的内核数据结构task_struct。所以研究打开的文件,是在研究进程和文件的关系,可以转换成进程内核数据结构和文件内核数据结构的关系。

2. IO函数

2.1 fopen读写模式

fopen函数的第二个参数,是决定文件打开后读写的方式。 带上+号表示即可读也可写。

#include <stdio.h>int main()
{FILE* fp = fopen("log.txt", "w");if (fp == NULL){perror("fopen");return 1;}const char *message = "hello world\n";int i = 0;while(i < 4){fputs(message, fp);i++;}fclose(fp);return 0;}

 上面的代码中,fopen函数读写方式是“w”,它会将文件内容长度截断为零,或者创建不存在的文件,再进行写入。log.txt文件原本文本内容是“hello file”,运行filecode程序之后,变成了“hello world”。

#include <stdio.h>int main()
{FILE* fp = fopen("log.txt", "w");if (fp == NULL){perror("fopen");return 1;}fclose(fp);return 0;}

上面的代码使用“w”模式打开文件后,什么都不做。原本log.txt文件内部有四行文本,运行该程序后,什么都没打印出来,说明已经被清空。

#include <stdio.h>int main()
{FILE* fp = fopen("log.txt", "a");if (fp == NULL){perror("fopen");return 1;}const char *message = "hello file\n";int i = 0;while(i < 4){fputs(message, fp);i++;}fclose(fp);return 0;}

fopen函数的第二个参数传入“a”,表示再原有的文件内容末尾追加信息。如下面所示。

2.2 重定向

Linux中,也有类似fopen的“w”模式下的操作。如echo命令后面加上字符串,默认是打印在显示屏上的,如果加上“>”符号并在后面跟上文件名,就会将该文件内容先清空,再写入字符串。

如果你只想清空文件内容,可以直接再命令行上输入">"符号并加上文件名。

如果想做到在文件末尾追加内容,可以使用“>>”符号,类似fopen的append模式。

2.3 标准文件流

任何一个程序启动时,会打开三个标准文件流。C语言中有三个标准输入输出文件流,分别是:

  • stdin,标准输入,对应的是键盘,从键盘获取。
  • stdout,标准输出,对应的是显示器,输出到显示器。
  • stderr,标准错误,对应的是显示器,输出到显示器。

标准文件流的数据类型就是C语言标准库提供的文件流指针FILE*,C语言对键盘和显示器进行包装,成为三个文件流,之后我们对键盘和显示器就可以通过文件指针的形式进行访问。

实际上,其实是进程会打开这个三个标准输入输出流。C++中的标准文件流是cin、cout、cerr。java也会有对应的标准输入输出。其他任何编程语言都会带有标准输入输出,所有语言都具有的性质,那么就要上升到系统的高度来看待这个问题。

那么我们有几种方法打印文本到显示器上?

#include <stdio.h>int main()
{printf("printf\n");fputs("fputs\n", stdout);fwrite("fwrite\n", 1, 7, stdout);fprintf(stdout, "fprintf\n");return 0;
}

3. 系统调用

我们使用C语言提供的标准库,会对键盘,显示器和磁盘进行访问,访问的对象都是硬件。操作系统作为硬件和软件的管理者,不会允许用户直接访问硬件。所以,C语言文件操作接口函数,不可能直接越过操作系统直接访问硬件,底层一定要封装对应的文件类系统调用。

3.1 open函数认识

open函数就是fopen函数封装的系统调用接口。第一个参数是文件的路径。

第二个参数是一个标记位。上面五个是常见的选项,都是宏。O_RDONLY表示只读,O_WRONLY表示只写。O_RDWR表示可读可写。O_APPEND表示追加。O_CREAT表示传入路径文件不存在,新建该文件。

flags是一个整型变量,一般有32个比特位,每个比特位0和1就能代表某个选项的存在,所以说flags实际上是一个32比特位的位图。上面的选项是只有一个比特位为1的值。

那怎么实现的呢,下面有代码实例。

#include <stdio.h>#define ONE (1 << 0)   // 00001
#define TWO (1 << 1)   // 00010
#define THREE (1 << 2) // 00100
#define FOUR (1 << 3)  // 01000
#define FIVE (1 << 4)  // 10000void Test(int flags)
{if (flags & ONE){printf("one\n");}if (flags & TWO){printf("two\n");}if (flags & THREE){printf("three\n");}if (flags & FOUR){printf("four\n");}if (flags & FIVE){printf("five\n");}
}int main()
{printf("-----------------------------\n");Test(ONE);printf("-----------------------------\n");Test(TWO);printf("-----------------------------\n");Test(ONE | THREE);printf("-----------------------------\n");Test(FIVE | TWO | ONE);printf("-----------------------------\n");Test(ONE | TWO |THREE | FOUR);printf("-----------------------------\n");return 0;
}

 首先需要定义几个宏值,使用左移符号,可以获得只有特定比特位为1的值。Test函数通过按位与实现判断。main函数中,如果想打印多个值,可以将宏选项通过按位或组合起来。

3.2 open函数使用

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{open("log.txt", O_WRONLY | O_CREAT);return 0;
}

 使用open函数暂时不考虑返回值,使用O_WRONLY和O_CREAT选项的组合,会发现创建出来的文件权限是乱码。

int main()
{open("log.txt", O_WRONLY | O_CREAT, 0666);return 0;
}

 如果打开的文件不存在,需要传入第三个参数设置文件读写权限,一般都是0666。如果打开的文件已经存在,可以不传第三个参数。我们设置文件为所有人可读可写,但是其他人只能读。因为系统存在umask掩码,该进程会使用系统的umask掩码,最终权限跟umask有关。

int main()
{umask(0);open("log.txt", O_WRONLY | O_CREAT, 0666);return 0;
}

 我们可以使用umask函数设置自己的umask值。如果设置成0,新建文件的权限就是open函数第三个参数。需要注意,进程内部修改umask值,不会影响系统外的umask值。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{open(argv[1], O_WRONLY | O_CREAT, 0666);return 0;
}

此时,我们可以自己实现一个touch指令,touch指令用来创建新文件,新文件权限664。使用命令行参数表,将第二个命令行参数当做文件名,默认传入权限0666。

open函数的返回值是一个整数。我们随便打开一个文件,返回值是3。该返回值叫做文件描述符,用来表示打开的文件。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd1 = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd1 < 0){perror("open");return 1;}printf("fd1: %d\n", fd1);return 0;
}

3.3 close函数

close函数用来关闭打开文件的文件描述符。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd1 = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd1 < 0){perror("open");return 1;}close(fd1);return 0;
}

3.4 write函数

write函数可以向打开的文件进行写入。第一参数是打开文件的文件描述符,第二个参数是指向写入内容的指针变量,第三个参数是写入内容的大小。

write函数的参数跟C语言的文件操作函数非常类似。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main()
{int fd1 = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd1 < 0){perror("open");return 1;}const char *message = "helloworld\n";write(fd1, message, strlen(message));close(fd1);return 0;
}

但是调用write写入之前是清空文件,还是追加,或者覆盖,要看open函数传入的标记。当我们把上面代码中的字符指针变量内容改为“xxxxx”时,执行该程序,会发现log.txt文件里只是覆盖了helloworld前面的字符。

    const char *message = "xxxxx";

添加O_TRUNC到open函数的第二个参数中,Truncate表示截断,向文件写入前会先清空文件内容。

    int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);

如果添加O_APPEND选项,就会在文件末尾追加写入内容。

    int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);const char *message = "xxxxx";

3.5 read函数

read函数可以读取打开的文件内容。第一参数是打开文件的文件描述符,第二个参数是指针变量,

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd1 = open("log.txt", O_RDONLY, 0666);char buffer[128];ssize_t s = read(fd1, buffer, sizeof(buffer));if(s > 0){buffer[s] = 0;printf("%s", buffer);}return 0;
}

4. fd文件描述符

4.1 标准输入输出

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd1 = open("log.txt1", O_WRONLY | O_CREAT, 0666);int fd2 = open("log.txt2", O_WRONLY | O_CREAT, 0666);int fd3 = open("log.txt3", O_WRONLY | O_CREAT, 0666);int fd4 = open("log.txt4", O_WRONLY | O_CREAT, 0666);printf("fd1: %d\n", fd1);printf("fd1: %d\n", fd2);printf("fd1: %d\n", fd3);printf("fd1: %d\n", fd4);return 0;
}

 当我们使用open函数打开四个文件,它们的文件描述符从3开始,依次递增。为什么新建的文件描述符会从3开始呢?

因为进程启动时,默认打开三个标准输入输流。文件描述符0,1,2分别被stdin标准输入,stdout标准输出,stderr标准错误占据。那怎么证明呢?可以使用write函数,对文件描述符1里写入。

#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{const char *message = "hello stdout\n";write(1, message, strlen(message));return 0;
}

还可以读取键盘输入内容,使用read函数,填上0文件描述符。启动程序,界面会卡在那里,等待键盘输入。

int main()
{char buffer[128];ssize_t s = read(0, buffer, sizeof(buffer));if(s > 0){buffer[s] = 0;printf("%s", buffer);}return 0;
}

 

4.2 什么是文件描述符

根据上面的讲解,我们知道文件描述符是个整数,并且从0开始,不断递增。碰巧的是数组下标也是从0开始的整数,不断递增。那么文件描述符和数组下标有什么关系呢?

  • 一个程序启动,被加载到内存中,操作系统会分配给一个task_struct结构体对象。如果有多个进程,那么操作系统会使用一张链表管理所有的进程的task_struct对象,这是进程管理。
  • 当CPU执行到open函数,打开log.txt文件,那么需要将磁盘上的文件加载到内存中。为了方便管理,操作系统会创建一个名为file的结构体对象给该文件,file结构体里面包含许多属性,一部分来自磁盘上文件的属性,还有一部分是操作系统所赋予的。
  • 进程启动时,默认打开三个标准输入输出流,对应键盘和显示器。操作系统也会给它们创建file结构体对象。当进程打开的文件变多了,内存中就存在许多file结构体对象。此时,操作系统也会使用一张链表,来管理所有的file结构体对象,这就是文件管理。
  • 而进程管理和文件管理必须是松耦合的,如果直接使用一个数据结构统一链接,将难以管理。

  • 可进程与文件的关系通常是一对多,一个进程可以打开多个文件。进程要与文件产生关联,只需要加些字段到task_struct结构体中。
  • task_struct结构体中有struct file_struct结构体指针对象,file_struct结构体中有个重要的对象,即fd_array数组。它是一个指针数组,存储的是struct file结构体指针,N的大小通常是32或者是64。
  • 当进程启动时,自动将fd_array数组前三位填入标准输入输出的file结构体对象地址。如果进程打开其他文件,填充该文件的file结构体对象地址到fd_array数组中空余的位置,然后再返回对应的下标。这就是为什么使用open函数打开的文件,得到的文件描述符从3开始。
  • fd_array也叫做文件描述符表,它构建了进程和文件之间的联系。
  • 所以文件描述符就是fd_array函数对应的下标。当进程使用某个文件描述符进行操作,会到fd_array数组中对应下标位置中的file结构体地址,找到该文件的file结构体对象。

4.3 语言级文件操作

通过上面的认识,进程对文件的操作,是通过文件描述符进行关联的。那么语言级的文件操作肯定是通过封装系统级文件操作来实现的。

C语言也提供了许多文件操作,如fopen,fwrite,fget等函数。它们的返回值类型都是FILE*,FILE也是个结构体类型,它内部会有许多字段,其中必定有文件描述符的字段。

我们可以通过打印其内部_fileno变量获取文件的文件描述符。

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

C语言的文件操作进行了两层封装,第一层是类型的封装,使用FILE封装文件描述符,第二层是接口函数的封装,fopen函数封装了系统调用级open函数。

不管是什么语言,如果要进行文件操作,它的文件类型里面必定包含文件描述符,并对系统调用函数进行封装。


创作充满挑战,但若我的文章能为你带来一丝启发或帮助,那便是我最大的荣幸。如果你喜欢这篇文章,请不吝点赞、评论和分享,你的支持是我继续创作的最大动力!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/505682.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构:栈(Stack)和队列(Queue)—面试题(一)

目录 1、括号匹配 2、逆波兰表达式求值 3、栈的压入、弹出序列 4、最小栈 1、括号匹配 习题链接https://leetcode.cn/problems/valid-parentheses/description/ 描述&#xff1a; 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] …

51单片机(一) keil4工程与小灯实验

直接开始 新建一个工程 在这里插入图片描述 添加文件 另存为 添加文件到组 写下一个超循环系统代码 调整编译项编译 可以在工程目录找到编译好的led_fst.hex 自行烧写到各自的开发板。 会看到什么都没有。 现在定义一个GPIO端口与小灯的连接&#xff0c;再点亮小灯…

基于 Python 和 OpenCV 的人脸识别上课考勤管理系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

Vue2:el-table中的文字根据内容改变颜色

想要实现的效果如图,【级别】和【P】列的颜色根据文字内容变化 1、正常创建表格 <template><el-table:data="tableData"style="width: 100%"><el-table-column prop="id" label="ID"/> <el-table-column …

案例研究:UML用例图中的结账系统

在软件工程和系统分析中&#xff0c;统一建模语言&#xff08;UML&#xff09;用例图是一种强有力的工具&#xff0c;用于描述系统与其用户之间的交互。本文将通过一个具体的案例研究&#xff0c;详细解释UML用例图的关键概念&#xff0c;并说明其在设计结账系统中的应用。 用…

73.矩阵置零 python

矩阵置零 题目题目描述示例 1&#xff1a;示例 2&#xff1a;提示&#xff1a; 题解思路分析Python 实现代码代码解释提交结果 题目 题目描述 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 示例…

C++【深入底层,从零模拟实现string类】

在学习了类和对象、模板等前期的C基础知识之后&#xff0c;我们可以尝试根据C标准库中所提供的接口类型&#xff0c;来搭建我们自己的string类型。这个过程有助于初学者掌握C的基础语法及底层逻辑。 框架的搭建 首先搭建模型的基础框架&#xff0c;需要建立my_string.h和my_st…

切忌 SELECT *,就算表只有一列

原文地址 尽量避免 SELECT *&#xff0c;即使在单列表上也是如此 – 如果你现在不同意这一点&#xff0c;读完这篇文章&#xff0c;你可能就要动摇了。 2012年的一个故事 这是我 12 年前&#xff08;约 2012-2013 年&#xff09;在客户后台应用程序中遇到的一个真实故事。 当…

DEV C++软件下载

一、进入网站 https://sourceforge.net/projects/orwelldevcpp/ 二、点击下载 三、安装步骤 1、点击 “OK” 2、点击“I agree” 3、点击“Next” 4、按步骤切换路径&#xff0c;本文选在D盘&#xff0c;可自行选取文件路径 5、等待安装 6、点击完成 7、选择语言 8、点击“N…

OpenBSD之安装指南

安装介质下载 OpenBSD的官网下载地址&#xff1a;https://www.openbsd.org/faq/faq4.html#Download&#xff0c;同时也是《OpenBSD FAQ - Installation Guide》。长篇大论了很多&#xff0c;每一个章节都能看懂是干嘛的&#xff0c;连起来就容易晕。并且是英文的&#xff0c;要…

Vue.config.productionTip = false 不起作用的问题及解决

文章目录 一、问题描述二、解决方法 一、问题描述 当我们在代码页面上引入Vue.js(开发版本)时&#xff0c;运行代码会出现以下提示&#xff0c;这句话的意思是&#xff1a;您正在开发模式下运行Vue&#xff0c;在进行生产部署时&#xff0c;请确保打开生产模式 You are runni…

C#,图论与图算法,输出无向图“欧拉路径”的弗勒里(Fleury Algorithm)算法和源程序

1 欧拉路径 欧拉路径是图中每一条边只访问一次的路径。欧拉回路是在同一顶点上开始和结束的欧拉路径。 这里展示一种输出欧拉路径或回路的算法。 以下是Fleury用于打印欧拉轨迹或循环的算法&#xff08;源&#xff09;。 1、确保图形有0个或2个奇数顶点。2、如果有0个奇数顶…

oracle闪回表

文章目录 闪回表案例1&#xff1a;&#xff08;未清理回收站时的闪回表--成功&#xff09;案例2&#xff08;清理回收站时的闪回表--失败&#xff09;案例3&#xff1a;彻底删除表&#xff08;不经过回收站--失败&#xff09;案例4&#xff1a;闪回表之后重新命名新表总结1、删…

CSS——22.静态伪类(伪类是选择不同元素状态)

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>静态伪类</title> </head><body><a href"#">我爱学习</a></body> </html>单击链接前的样式 左键单击&#xff08;且…

Java Spring Boot实现基于URL + IP访问频率限制

点击下载《Java Spring Boot实现基于URL IP访问频率限制(源代码)》 1. 引言 在现代 Web 应用中&#xff0c;接口被恶意刷新或暴力请求是一种常见的攻击手段。为了保护系统资源&#xff0c;防止服务器过载或服务不可用&#xff0c;需要对接口的访问频率进行限制。本文将介绍如…

数据结构(Java版)第七期:LinkedList与链表(二)

专栏&#xff1a;数据结构(Java版) 个人主页&#xff1a;手握风云 一、链表的实现&#xff08;补&#xff09; 接上一期&#xff0c;下面我们要实现删除所有值为key的元素&#xff0c;这时候有的老铁就会想用我们上一期中讲到的remove方法&#xff0c;循环使用remove方法&#…

C#Halcon找线封装

利用CreateMetrologyModel封装找线工具时&#xff0c;在后期实际应用调试时容易把检测极性搞混乱&#xff0c;造成检测偏差&#xff0c;基于此&#xff0c;此Demo增加画线后检测极性的指引&#xff0c;首先看一下效果 加载测试图片 画线 确定后指引效果 找线效果 修改显示 UI代…

ORB-SALM3配置流程及问题记录

目录 前言 一、OPB-SLAM3基本配置流程 1.下载编译Pangolin 二、ORB-SLAM3配置 1.下载源码 2.创建ROS工作空间并编译ORB-SLAM3-ROS源码 3.尝试编译 三、运行测试 一、OPB-SLAM3基本配置流程 ORB-SLAM3是一个支持视觉、视觉加惯导、混合地图的SLAM&#xff08;Simultane…

Unity2D初级背包设计后篇 拓展举例与不足分析

Unity2D初级背包设计中篇 MVC分层撰写(万字详解)-CSDN博客、 如果你已经搞懂了中篇&#xff0c;那么对这个背包的拓展将极为简单&#xff0c;我就在这里举个例子吧 目录 1.添加物品描述信息 2.拓展思路与不足分析 1.没有删除只有丢弃功能&#xff0c;所以可以添加垃圾桶 2.格…

领域驱动设计(DDD)——限界上下文(Bounded Context)详解

限界上下文&#xff08;Bounded Context&#xff09;在 DDD 中的定义 在领域驱动设计&#xff08;DDD&#xff09;中&#xff0c;限界上下文&#xff08;Bounded Context&#xff09;是一个核心概念。它定义了领域模型的边界&#xff0c;帮助我们将复杂的业务系统划分成多个相对…