文件 fd

2024-09-03 19:44
文章标签 fd

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

目录

  • 1. 建立共识原理
  • 2. 回忆 C 文件接口
    • 2.1 当前工作路径
    • 2.2 w / a 方式写入
    • 2.3 默认打开的三个文件流
  • 3. 认识文件系统调用
    • 3.1 O_WRONLY|O_CREAT 写时创建
    • 3.2 O_TRUNC 截断长度(也即全覆盖式写入)
    • 3.3 O_APPEND 追加
  • 4. 浅谈文件访问的本质
    • 4.1 简证

1. 建立共识原理

  • 文件 = 内容 + 属性
  • 文件分为两种:打开的文件 和 没打开的文件
  • 文件是被谁打开的? ----- 进程。因此研究文件打开这个话题,本质就是在研究进程与文件的关系。
  • 没打开的文件,存储在磁盘;在磁盘上我们最关注什么问题?----- 没有被打开的文件非常多,文件如何被有序分类的放置好,因为我们要快速对文件进行增删查改等操作,所以我们需要快速的找到指定文件。

我们要打开一个文件,在打开之前,文件存储在磁盘中,而打开文件的本质就是访问文件,最终都是要通过代码去访问的(对文件增删查看等操作),编译完运行起来变为进程。换言之,访问文件一定是 cpu 去执行的,根据冯诺依曼体系结构,访问时,文件就一定需要被加载到内存中。

在代码编写上,我们是可以打开诸多个文件的,这也就代表着一个进程与打开文件的比例一定是 1: n,系统中存在着大量的进程,也就一定存在着更加大量的被打开的文件。操作系统都需要对进程做管理,因此大量的被打开的文件同样的需要被管理起来。如何管理?? ----- 先描述,再组织!

在内核中,一个被打开的文件都必须有自己的文件打开对象,包含文件的诸多属性。 例如:struct File { file_attributes; struct xxx* next };


2. 回忆 C 文件接口

FILE *fopen(const char *path, const char *mode);
示例:
FILE *fopen("log.txt", "w");

2.1 当前工作路径

这个 C 打开文件的库函数我们在熟悉不过了,并且以 w 的方式打开指定文件时,若该文件不存在,那么 fopen 这个函数会自动在当前路径下创建该文件,再打开它。

而这里的当前路径到底是什么呢?? ----- 当前运行的进程所处的工作目录 cwd。

换言之,只要我把当前正在运行的这个进程的 cwd 更改了,那么如果 fopen 时指定是路径依旧不带全局路径,那么就会在 cwd 所指向的目录下创建该文件。

chdir("/home/outlier");
FILE *fp = fopen("log111111111111111111.txt", "a");

在这里插入图片描述
在这里插入图片描述

2.2 w / a 方式写入

fopen w 的方式打开文件,对文件进行写入,我们还发现了,每次重新写入的时候,文件上一次的内容总是不见了。这不仅仅是 fopen 每次都会从文件的起始地址开始写入,而是在写入之前,还会将文件的长度置 0。man 手册中对 fopen 的读写方式是这样说的:

 w+    Open for reading and writing. The file is created if it does not exist, oth‐erwise it is  truncated. The stream is positioned at the beginning of the file.

在这里插入图片描述

在 linux 中,我们也可以通过 echo + 输出重定向 向一个文件写入啊。要向一个文件写入,那么就必须先打开文件。但是 > 重定向写入文件时,我们一样发生,上次的内容被清空了,只留下最新写入的内存。所以 > 的本质还是 “w”, 它做的事情还是 打开文件 + w 方式的覆盖式写入!如果你愿意,在代码中 fopen 之后,对文件不做任何写入,程序运行起来之后,也可以达到清空文件的效果。

a      Open for appending (writing at end of file). The file is created if it does not exist. The stream is positioned at the end of the file.

与 w 方式不同写入就是 a 方式写入,w 清空并从文件头开始写入,a 在上次写入的结尾,继续追加写入。

2.3 默认打开的三个文件流

在 C 程序启动时,会默认打开 stdin,stdout,stderr 这三个文件输入输出流,C++ 则有 cin, cout,cerr。
其中的 stdin 对应的是键盘文件,stdout,stderr 对应显示器文件

换言之,在 c 代码中,我们同样也可以实现向 stdout 这个文件流做写入操作

const char *s = "hello, linux!";
fprintf(stdout, "%s: %d\n", s, 1234);
fprintf(stderr, "%s: %d\n", s, 1234);

3. 认识文件系统调用

我们需要有一个共识:文件是存储在磁盘上的,而磁盘是外部设备,所以访问磁盘文件,本质就是访问硬件!但是访问硬件这件事,我们用户并不擅长啊!更重要的是,硬件是被操作系统所管理的,作为用户,我们无法绕开操作系统访问硬件,操作系统不允许这种操作,但是作为用户的我们又有访问硬件的需求,怎么办呢?? ----- 于是操作系统向外提供了系统调用接口,供用户访问磁盘文件。换言之,语言层面上的各种 fopen,fprint 等访问文件的库函数,底层一定是调用的系统接口。

3.1 O_WRONLY|O_CREAT 写时创建

#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);		

在这里插入图片描述

关于 flag 参数,O_RDONLY 只读,O_WRONLY 只写,O_RDWR 读写,必须包含这三大方式中的其中一中。flag 采用的是比特位作为标志进行传递的。

#define ONE (1<<0) 
#define TWO (1<<1) 
#define THREE (1<<2) 
#define FOUR (1<<3) 
void show(int flags)
{if(flags & ONE) printf("function1\n");if(flags & TWO) printf("function2\n");if(flags & THREE) printf("function3\n");if(flags & FOUR) printf("function4\n");
}

类似于上面的小demo,一种 flag 只会有一个比特位为1,可以将多个 flag 通过位运算组合起来使用。

如果 open 打开一个未存在的文件,并且 flag 只设置为 O_WRONLY,是不会自动创建文件的,需要再加一个 O_CREAT 进行或运算传递。

open("log.txt", O_WRONLY|O_CREAT);

在这里插入图片描述

但是这样还不够,open 创建文件时,如果不加以设置文件权限,就会出现 “乱码” 的现象。

open("log.txt", O_WRONLY|O_CREAT, 0666);

在讲 Linux中常见的权限问题 时,我们谈论到了目录起始权限是777,文件的起始权限是 666,而因为有 umask 的存在,创建出来的文件权限并不会是 666,而是 664,那么在 open 打开创建一个文件时,同样也会受到系统中 umask 的限制。

如果想要创建出来的文件权限,所见及所写,那么就需要在调用 open 时,先调用一下 umask,它也是一个系统调用,作用域只局限于该进程。

  • 在 C 代码中调用了 umask 系统调用修改了,指定了 umask,但是系统中也有一个 umask,那进程运行起来该听谁的呢?? ----- 局部优先原理。肯定是听该进程内设置的 umask。
umask(0);
open("log.txt", O_WRONLY|O_CREAT, 0666);

在这里插入图片描述

3.2 O_TRUNC 截断长度(也即全覆盖式写入)

RETURN VALUE:open()  and  creat()  return  the new file descriptor, or -1 if an error occurred (inwhich case, errno is set appropriately).
// 其中的 file descriptor 就是文件描述符,int 类型,可用于关闭文件int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
// 第一次文件内容
const char* s = "this is a system call!";
write(fd, s, strlen(s));		// write 也是系统调用
close(fd);	// 关闭文件
// 第二次
const char* s = "aaaa";
write(fd, s, strlen(s));

在这里插入图片描述

现象:aaaa 覆盖了上一次的内容,但是并没有清空文件内容,因为上一次的其它内容还在。只是从文件头等长度覆盖而已。
也就是说,write 这个系统调用只会覆盖式写入,不会清空,是这样吗?? ------ 其实不是 write 的问题,是 open 写入方式的问题,flag 参数上我们只设置了 O_WRONLY|O_CREAT,如果想要达到每次打开文件都截断文件长度,使其为 0, 还需要加上 O_TRUNC,即

// 打开文件时,文件不存在自动创建,文件起始权限设置为 666。如果文件存在,清空文件内容。
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);

3.3 O_APPEND 追加

// 打开文件时,文件不存在自动创建,文件起始权限设置为 666。如果文件存在,追加写入
// 追加写入与截断长度O_TRUNC是相矛盾的,无法同时存在
int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND , 0666);

在了解了上述这些文件的系统调用,我们应该要明天,诸如 C 中的 fopen,fwrite 这样的库函数,底层一定是封装的系统调用。

FILE *fopen("log.txt", "w");  ===> int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
FILE *fopen("log.txt", "a");  ===> int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);

4. 浅谈文件访问的本质

一个文件被打开,就需要被操作系统管理起来。管理的理念:先描述,再组织。

如何描述呢? ----- 还是 struct 结构体对象,其中必定要直接或间接的包含文件的各种属性,例如:

  • 文件所在磁盘的位置
  • 文件基本属性,权限,大小,读写位置,谁打开的…
  • 文件的内核缓冲区信息
  • struct file* next 指针

如何组织? ----- 操作系统会以双链表的形式,将所有文件对象连接起来,所以对文件的管理,就转换成对双链表的增删查改,新增一个文件,就创建一个 struct file 对象,插入到双链表中;关闭一个文件,在双链表中删除结点,释放 struct 对象,把数据刷新到磁盘。

文件打开都是通过代码实现的,编译运行后都会变成进程。换言之,我们需要知道,哪些文件被哪些进程所打开,即 要关联起进程与打开文件的关系。进程的 PCB 中会包含一个 struct file_struct* files 这样的结构体指针,指向的结构体内部含有一个数组 struct file_struct* fd_array[],数组内部存储的都是 struct file* 指针,指向的就是 stuct file 的地址(即被打开文件对象的地址)。而 这个数组就称为 文件描述符表!

在这里插入图片描述

因此 int open(const char *pathname, int flags, mode_t mode); open 的返回值就是记录文件对象地址在数组中所处的下标位置!
当我们 open 创建一个文件时,在文件对象的双链表中连接起来,然后把文件对象的地址填充到文件描述符表中,再把所在数组下标返回。当我们调用 write 对文件做写入操作时,传递的 fd 参数,就是通过进程的 PCB 中的 files_struct* 找到 files 这个结构体,然后根据 fd 索引到对应位置,根据里面的 strcut file* 指针找到指定的文件对象,对其做增删查改等操作!

4.1 简证

上面讲了那么多,fd 就是数组下标,如何证明呢??

int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
int fd2 = open("log2.txt", O_WRONLY|O_CREAT, 0666);
int fd3 = open("log3.txt", O_WRONLY|O_CREAT, 0666);
int fd4 = open("log4.txt", O_WRONLY|O_CREAT, 0666);
printf("fd: %d\n", fd);
printf("fd2: %d\n", fd2);
printf("fd3: %d\n", fd3);
printf("fd4: %d\n", fd4);

在这里插入图片描述

数组下标是从0开始的,但是我打开的第一个文件,它的下标是3。那么 0 1 2 这三个位置去哪了呢??

文章一开始的时候就说过,C 语言会默认为我们打开三个输入输出流 stdin,stdout,stderr。我们可以再来证明一下,这三个默认打开的,就是存储在 fd_array[ ] 数组中的0、1、2号下标处。

char input[1024] = {0};
read(0, input, sizeof(input));
printf("ehco: %s\n", input);
printf("--------------------\n");
const char* s = "hello, linux\n";
write(1, s,strlen(s));
write(2, s,strlen(s));

在这里插入图片描述
wirte 和 read 这两个系统调用,第一个参数传递的可是 fd 啊!fd 是什么?? fd 不就数组下标吗,所以这次我直接往 0 1 2 下标做读写,0 号下标对应的就是键盘文件,而 1 号 显示器文件,2 号也是显示器,分别对应 C 默认打开的 stdin,stdout,stderr。

  • C语言程序默认打开三个标准输入输出流,真的是 C 语言做的事情吗??又或者这是语言上的特性吗??
    c/c++、java等语言都会默认会打开三个输入输出流,这是操作系统的特性!是操作系统做的事情,因为一起程序运行起来都会变成进程,进程会默认打开键盘、显示器,而进程是操作系统的!

  • 为什么操作系统要默认打开键盘和显示器呢??
    因为用户有需求!当我们启动操作系统时,操作系统就会默认打开键盘文件和显示器文件;当启动一个进程时,操作系统只需要讲键盘文件和显示器文件的地址填充到 fd_array[ ] 数组的 0 1 2 号下标处即可。

我们说过,C 中的 fopen 是对系统调用 open 的封装,但是他们两个的返回值好像看起来就不一样,一个是 FILE,一个是 int,FILE 就是 C 库自己封装的结构体,这个结构体里面必定封装了文件描述符。

再次证明:

printf("stdin->fd: %d\n", stdin->_fileno);
printf("stdout->fd: %d\n", stdout->_fileno);
printf("stderr->fd: %d\n", stderr->_fileno);

在这里插入图片描述
C 中的 stdin,stdout,stderr 也必定封装了 fd 这样的字段(即 _fileno),而三个默认打开的输入输出流,fd 依次就是 0 1 2。这就是我们打开的文件,fd 一定是从 3 开始的,不可能从 0 1 2 这几个下标开始(在不关闭文件描述符 0 1 2 的前提下)。

close(1);int ret = printf("stdin->fd: %d\n", stdin->_fileno);
printf("stdout->fd: %d\n", stdout->_fileno);
printf("stderr->fd: %d\n", stderr->_fileno);fprintf(stderr, "printf ret: %d\n", ret);

在这里插入图片描述

当我们 close(1) 关闭输出流之后,printf 确实就无法使用了。而把 1 号文件描述符关了,还有 2 号指向显示器文件,所以stderr 依旧可以正常写入。

我们要知道,一个文件是可以被多个进程打开的;一个进程中,不同的文件描述符中 struct file* 也可以指向同一个文件。在显示器文件中会维护一个引用计数 count,当有一个 struct file* 指向显示器文件,count++。当我们调用 close(1) 时,操作系统就对该进程的 1号文件描述符的指向置空,显示器文件中的 count - 1,再判断 count == 0 ? 如果还没有等于 0,就说明还有其它的 文件描述符指向该文件,等于 0 时,就回收该文件对象。

所以一句话总结:任何编程语言,都会默认打开三个输入输出(因为进程会默认打开)一个用于输入,两个用于输出,并且这些输入输出流一定直接或间接的包含 fd 字段,如果没有包含 fd,那么就无法访问文件。


如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

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



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

相关文章

多路转接之select(fd_set介绍,参数详细介绍),实现非阻塞式网络通信

目录 多路转接之select 引入 介绍 fd_set 函数原型 nfds readfds / writefds / exceptfds readfds  总结  fd_set操作接口  timeout timevalue 结构体 传入值 返回值 代码 注意点 -- 调用函数 select的参数填充  获取新连接 注意点 -- 通信时的调用函数 添加新fd到

Android中使用eBPF跟踪 FD打开与关闭

我们知道在Android系统中 fd 泄露,可以通过 google 开发的 fdtrack来进行排查,但是有些情况下我们想在外发release版本去监控fd泄露情况,fdtrack就不能很好的满足需求了。可以用eBPF去监控fd泄露。 Android中使用eBPF跟踪 FD打开与关闭

Detection简记2-DAFE-FD: Density Aware Feature Enrichment for Face Detection

创新点 1.使用密度估计模型增强检测中的特征图 总结 整个流程还是很清晰的。 conv1-3的特征图经过密度估计模块由检测器D1进行检测。 D2-4分别是四个检测器。 FFM是特征融合模块,将不同层不同大小的特征融合。 FFM网络结构如下: 首先使用1X1的卷积减少两组特征的厚度到128,然后使用双线性插值统一两组特征图的尺寸,然后相加。类似于cvpr2017的SSH。 多尺度检测器的网

虹科技术|全新Linux环境PCAN驱动程序发布!CAN/CAN FD通信体验全面升级!

全新8.17.0版本的PCAN-Linux驱动程序正式发布,专为CAN和CAN FD接口量身打造。无论是CAN 2.0 a/b还是CAN FD的PCAN硬件产品,都能在我们的新驱动下“驰骋自如”。想要体验字符模式设备驱动接口(chardev)的便捷,还是SocketCAN设备驱动接口(netdev)的高效?都由您说了算! 新版本驱动包概览 1、设备驱动程序模块源代码及Makefil

Linux | 文件描述符fd详解及重定向技术的应用

多谢梅花,伴我微吟。 - 《高阳台·除夜》(韩疁) 2024.8.23 目录 1、文件描述符fd 文件操作符概念(简单带过) 重点:如何理解文件操作符使得系统实现了设备无关性?(使得操作系统无需关心具体的硬件细节) 示例代码:标准输入、标准输出和标准错误 文件描述符的分配规则 注意 2、重定向 重定向的简单例子 思考 重点:使用dup2函数进行重定向 重定向的重要应用 文件描述符,一个看似

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

文章目录 前言:回顾一下文件提炼一下关于文件的理解: 理解文件:通过系统调用操作文件:理解标志位传参:打开文件 open写入信息 write 理解文件描述符:对于open的返回值:==文件描述fd的本质是什么呢?== 如何理解Linux中一切皆文件?打通系统调用和C语言函数 前言: ​ 现在我们对进程的总体概念也有了了解,下面我们进入新的模块学习。关于Linux如何操作文件。其关

【Linux】系统文件IO·文件描述符fd

前言  C语言文件接口  C 语言读写文件 1.C语言写入文件 2.C语言读取文件 stdin/stdout/stderr 系统文件IO 文件描述符fd: 文件描述符分配规则: 文件描述符fd: 前言 我们早在C语言中学习关于如何用代码来管理文件,比如文件的输入和文件的输出,一些文件的接口,如何深入学习文件的知识,在Linux下一切皆文件,今天我们探讨Linux的基础I/

【车载测试】CAN协议、CAN- FD协议和FlexRay协议 区别

【上半场电动化,下半场智能化】 一、CAN协议 和 CAN- FD协议的区别 CAN(Controller Area Network)协议是一种广泛用于汽车和工业控制系统等领域的现场总线协议。CAN- FD(Flexible Data Rate)协议是对CAN协议的扩展,旨在提高CAN总线的数据传输速率和数据量。 1.数据速率不同 CAN协议是基于ISO 11898标准,通常支持最高1Mbps的

【每日一库】fd - 文件搜索神器

find 命令,是我们 Linuxer 经常会用到的命令行工具之一,频率可能会比昨天介绍的 grep/ripgrep 工具稍微低一点。它的作用是在文件系统目录中搜索符合指定文件名模式的文件。这里介绍的 fd 是 find 的 Rust 替代实现。目前在 github 上 star 数量接近 10000。 fd 的语法与 find 有一些不同,这是这个工具的取舍。 先来看看这个工具的效果。

当TCP和UDP使用的fd描述符是同一个时,会发生什么?

最近在一个项目中遇到了一个非常奇葩的问题,简单来说就是程序会创建一条tcp客户端连接与tcp服务端通信,另外还有一个udp用来组播发送报文。 现在遇到的问题是不知道什么原因,tcp和udp通过socket接口返回的描述符竟然是一样的,结果就会导致不管是tcp调用send还是udp调用sendto,实际上报文都是发送到了tco服务端,最终就会导致tcp服务端认为tcp客户端存在问题,会主动将