本文主要是介绍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在执行我们的代码。 -
操作文件,本质是进程在操作文件。
- 文件在没有被打开的时候是存在于磁盘中的。
- 一个进程可以打开多个文件
在很多情况下,操作系统内部一定存在大量被打开的文件,因此操作系统必须对打开的文件进行管理!!!
谈及管理永远六个字:“先描述,在组织”因此我们猜测,未来估计会有一个类似于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");}
最后的运行结果为:
这需要我们理解按位或的操作,才能理解上述操作,接下来我们就来介绍系统调用函数
打开文件 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;
}
写入信息 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;
}
在这里肯定也存在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;
}
诶,在这里我们发现为什么我们创建新的文件后,对于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;
}
结果也确实如此,这也说明了我们后面创建的文件标识符都是从3开始的。
文件描述fd的本质是什么呢?
问题就是凭什么我们可以直接对一个整形1写入,就可以在显示器显示?
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件 。
原理说白了就是:先描述,再组织
简单来说,open是在干嘛呢?
- 创建struct file
- 开辟文件缓冲区的空间,加载文件数据(这个过程可以延后)
- 查看进程的文件描述符
- 将struct file地址填入对应的表下标中
- 返回下标
所以,本质是文件映射关系的数组的下标。无论读写,都必须在合适的时候让OS把文件的内容读到文件缓冲区中。
如何理解Linux中一切皆文件?
现在我们只是知道0 && 1 && 2 代表着键盘和显示器,可是这些是硬件啊,我们又该如何向这些硬件中写入和读取呢?
-
如何使用C语言创建类?
了解过C++的都知道,C++是一门面向对象的编程语言,而在C++中一切皆对象已经熟的不能再熟。为什么我们使用C++那么爱”类“?无非就是方便管理,正如老板肯定想的是如何赚大钱,那赚大钱的基础肯定是需要管理好手下的员工。正如使用C++目的是为了创建维护良好的项目,基础就是将数据代码管理好,因此也是需要管理!谈及管理,永远绕不开的六个字 —— ”先描述,再组织“。
类的出现就完美的实现了这六个字,不同于C语言创建的struct 对象,C++通过类创建出来的class 对象拥有灵活的“组织“功能,关键就是类能“描述”函数,函数就是方法,每个对象都有自己对应的方法!
所以知道区别后,C语言想要实现C++的类,需要的正是“描述”函数,来创建动作。
对于不同struct对象的函数,我们便可以使用函数指针来找到对应的函数,从而实现类的操作。 -
深入剖析:
对于每个硬件来说,都有自己独特的函数,例如read和write方式,但是硬件与硬件之间,甚至说型号与型号之间的read函数肯定不一样,然而我们并不需要关系这些区别,我们只负责使用就好!至于read函数是如何实现的,不必关心,因为在每个硬件都会收到对应的struct file进行管理,每个struct file内部都会有对应硬件的函数的函数指针,通过函数指针就能直接使用专门对应的函数。
不必关系底层实现,只负责使用。便是一切皆文件的意义!
而我们又会发现,以上这种通过函数指针调用不同函数的方式,不就是我们在C++学习的多态吗!!!!
打通系统调用和C语言函数
现在我们知道,系统在访问文件时,OS只认文件标识符fd。
-
那对于系统调用和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; }
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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!