【Linux篇】基础IO – 文件描述符的引入
一. 理解文件
我们以前就说过 文件 = 文件内容 + 文件属性
1.1 侠义理解
文件是在磁盘上的,磁盘本质上是个外设,我们访问文件其实就是在系统和磁盘上进行IO
磁盘是永久性的存储介质,文件在磁盘上的存储是永久性的
对文件的所有操作,本质上是对外设的输入输出 简称 IO
1.2 广义理解
Linux下一切皆文件(键盘,显示器,网卡,磁盘… )
1.3 文件操作的归类认知
对于0KB的空文件,是占据磁盘的空间的
文件 = 文件内容 + 文件属性
所有对文件的操作本质上是对文件内容和文件属性的操作
1.4 系统角度
对文件的操作本质上是进程对文件的操作
磁盘的管理者是操作系统
文件的读或写本质上不是通过 C语言/C++的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口实现的
文件
“内存级(被打开)”文件
磁盘级文件
二. 回顾C语言文件接口
2.1 打开文件
// 文件打开接口
FILE *fopen(const char *path, const char *mode);
path:表示要打开文件的路径,或者文件名,只有文件名而没有路径表示打开当前路径下的文件。
mode:表示打开的方式,比如只读r,只写w,追加a等。
📌 Tips: 我们之前介绍的重定向,>本质上就对应使用的是w选项,>>本质上就对应使用的是a选项。
2.2 对文件进行写入
#include<stdio.h>
int main()
{
FILE* fp = fopen("myfile.txt","w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
while (1);
fclose(fp);
return 0;
}
打开的myfile文件在哪个路径下呢?
在程序的当前路径下
那系统怎么知道程序的当前路径在哪里呢?
可以使用ls /proc/[进程id] -l命令查看当前正在运行进程的信息:
cwd:进程当前的工作路径
exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
2.3 输出信息到显示器,有哪些方法
printf()
fprintf()
fwrite()
当我们向显示器打印本质上就是向显示器文件写入,Linux下一切皆文件
2.4 stdin & stdout & stderr
c语言会默认打开三个输入输出流,分别是stdin,stdout,stderr
这三个流的类型都是FILE*
为什么要帮我们把这几个流自动打开呢?
我们传统上写的程序是做数据处理的
2.5 打开文件的方式
以w的方式打开文件时,文件首先会被清空,然后从0开始写
我们以前说过的重定向,比如echo aaaaa > log.txt,把打印到显示器上的内容写入文件里,前提是我们先得把文件打开。我们的输出重定向>log.txt为什么会把文件内容清空呢?因为我们输出重定向第一步要打开文件,而打开文件,而打开文件第一步先要把文件清空
以a的方式打开文件,这种方式叫做追加,它一般写的时候会向文件结尾进行写入,不存在的话就创建它。>>叫做追加重定向,以a的方式进行写入本质上也是先要把文件打开,然后再进行写入。
当我们向文件里写入一段字符串时,我们需不需要在字符串后面加\0呢?答案是不需要,因为\0是c语言的规定,与我文件又有什么关系呢。
三. 系统文件I/O
我们对文件操作的是时候,文件是在磁盘上面的,而真正对文件进行操作的其实是操作系统,操作系统对磁盘文件进行读写访问,我们以前使用c语言对文件的访问其实是c语言封装了系统调用。比如说访问文件得先打开,那么就得先有open
flags有众多选项O_RDONLY表示只读, O_WRONLY表示只写, O_RDWR表示读写,O_TRUNC表示
int open(const char *pathname,int flags,mode_t mode);
1
如果我们今天要打开一个文件,并且这个文件不存在要新建的话,就用上面的这个open,必须指定权限,如果不指定的话这个权限就是乱码。如果打开一个已经存在的文件,就用两个参数的。
open的返回值:如果打开成功的话返回一个新的文件描述符,如果失败的话-1被返回,并且错误码就被设置了。
flags是一种整形标志位,一共有32个bit位,,如果用O_RDONLY这种选项直接传参的话会很麻烦,所以选用位图的方式来传递。
3.1 位图传递标志位
#include<stdio.h>
#define ONE_FLAG (1<<0)// 000000....00000001
#define TWO_FLAG (1<<1)// 000000....00000010
#define THREE_FLAG (1<<2)// 000000....00000100
#define FOUR_FLAG (1<<3)// 000000....00001000
void Print(int flags)
{
if(flags & ONE_FLAG)
{
printf("One!\n");
}
if(flags & TWO_FLAG)
{
printf("Two!\n");
}
if(flags & THREE_FLAG)
{
printf("Three!\n");
}
if(flags & FOUR_FLAG)
{
printf("Four!\n");
}
}
int main()
{
Print(ONE_FLAG); //打印one
printf("\n");
Print(ONE_FLAG | TWO_FLAG); //打印one two
printf("\n");
Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);//打印one two three
printf("\n");
Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);//打印one two three four
printf("\n");
return 0;
}
3.2 open
我们open打开文件的时候绝对相对路径都可以,因为在哪个路径下创建文件是由进程决定的,进程记录了自己的cwd,说明我们新建文件是在指定路径下建还是在其他路径下建和cd,fopen都没有关系,它是系统的行为。
我们接下来验证一下log.txt它默认会在当前路径下去创建,因为它进程的路径就在当前路径下。
我们对文件进行写入write接口
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
第一个参数: 是open的返回值,这个返回值叫文件描述符
第二个参数: 是我们要写入的buffer
第三个参数: 是我们要写的数据的长度,返回值是实际写入的长度。写入失败返回-1。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
umask(0);//在新建文件之前将umask权限眼掩码设置为0
int fd = open("log.txt",O_CREAT | O_WRONLY,0666);//不存在就创建,而且以写入的方式打开
if(fd < 0)
{
perror("open");
return 1;//进程的退出码设为1
}
printf("fd = %d/n",fd);
const char *msg = "hello world\n";//定义一个字符串
int cnt = 5;
while(cnt)
{
write(fd,msg,strlen(msg));//向指定文件描述符里写,写的内容是msg,写的长度
//我们在写入时是当做字符来写,所以这里不需要strlen(msg)+1,因为\0是c语言规定的,如果写\0的话在我们的文件中就会出现@^乱码现象
cnt--;
}
close(fd);//关闭文件
return 0;
}
当我们把要写的内容该为abcd并且只让它写一行。
现象是在我们往文件里写的时候应当是先清空再进行写入,而现在是覆盖写,文件内原来的内容并没有清空,为什么没有清空呢?答案是我们再创建文件的时候,只填了创建和写入,而并没有要清空。
所以我们加上O_TRUNC
💦结论:如果我们要打开文件,并且将它清空,若要用系统级的函数,我们就需要传递这几个标志位。
清空并写入:
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);
如果O_CREAT(不存在就新建),O_WRONLY(以只写入的方式),O_TRUNC(清空)
追加并写入:
int fd = open("log.txt",O_CREAT | O_WRONLY | O_APPEND,0666);
所以C语言中的fopen中的w选项和a选项就会被分别转化为上面的那样。
3.3 write
write这个接口在写的时候参数不是char*,而是void*
ssize_t write(int fd, const void *buf, size_t count);
说明write在进行写入的时候,进行二进制写入也可以,做字符串写入也可以。
文本写入 vs 二进制写入
我们在往显示器上打印12345的时候是往显示器打印的是1字符2字符3字符4字符和5字符。而不是一万两千三百四十五。
📌小tips: 我们往显示器上打印和我们往文件里写入其实是一摸一样的,因为Linux下一切皆文件。
我们往文件里以清空写的方式写入1234567
查看log.txt
我们会发现log.txt是4个字节里面的内容是乱码,这是为什么呢?因为这种写入叫做二进制写入,在实际写的时候把a这个整形变量写到了文件里面,所以文件的大小是4字节,因为整数的大小是4字节,而我们写入的1234567不可显,它是把1234567这个二进制数字写在了磁盘上。而我们想看到的是1234567,那可怎么办呢?
将a这个整形格式化处理成字符串,然后将字符串写入到要写的文件中。
此时的log.txt中就是1234567了。
💦结论: 在系统层面上并不存在所谓的文本写入和二进制写入,系统并不关心你写入的类型,文本写入还是二进制写入其实是语言层 提供的概念。所以我们在c语言中有fpus文本写入fwrie二进制写入,这两个底层最终调用的都是这个接口。
ssize_t write(int fd, const void *buf, size_t count);
格式化输入输出其实就是将内存里的二进制数据转成字符串,在使用write接口将数据写进去。所以格式化输入输出,文本式的写入全都是语言层的概念。这个格式化工作要么是语言来做,要么是我们自己来做。
3.4 read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:如果成功,则返回读的字节数(0表示文件结束),如果错误返回-1
第一个参数:是open的返回值,这个返回值叫文件描述符
第二个参数: 指向一段空间,该空间用来存储读取到的内容。
我们读取log.txt中的内容
四. 访问文件的本质
我们一次性打开四个文件,并观察它的fd
打出来的文件描述符是3,4,5,6问题是0,1,2去哪里了呢?
0:标准输入
1:标准输出
2:标准错误
这三个叫做默认的文件流,因为它默认的把三个文件打开了,012已经被占了。
C语言中有三个标准输入,标准输出,标准错误的文件流
C++中也有标准输入(cin),标准输出cout,标准错误cerr的文件流
C语言中我们打开文件叫做FILE*,*是指针,可是FILE是什么呢?FILE是C语言提供的一个结构体,它是被typedef出来的,结构体里包含了文件的属性。
在OS接口层面上只认fd文件描述符,所以这个结构体里一定封装了文件描述符。
所以我们以前学到的文件操作,在类型层面的文件对象FILE封装了文件描述符,在接口层面打开文件是封装了对应的选项。
所以任何语言,底层只认文件描述符,C语言/C++会把市面上的各种平台的代码各自实现一份,然后采用条件编译,代码裁剪的方式,把不同的系统需要的库编到不同的系统里,给用户提供的是同一个语言型接口,这样写出来的代码具有可移植性。
要了解可移植性就需要知道不可移植性,不可移植性是由于平台不一样,平台不一样系统调用的接口不一样。
4.1 文件描述符
操作系统要把被打开的文件管理起来,怎么管理呢?先描述,再组织
创建一个进程的时候,首先在内核当中创建的了一个task_struct,我们称之为进程控制块。操作系统在打开文件时需要在内核当中创建一个数据结构struct file,打开很多文件就会创建很多的struct file,然后用指针连接起来。那么找一个文件的所有内容或者任何一个属性就都能通过struct file找到。也就是说未来想访问文件就只需要找到对应的struct file结构体队对象就可以了。
在文件中,每一个文件的struct file都会提供一个文件级缓冲区,将来文件的内容就会加载到文件缓冲区当中,文件的属性会用来初始化我们的struct file以及将来的inode结构体。今天我们如果是想读取文件里面的内容,一定先是我们把文件打开,创建struct file结构体,通过file内部的指针操作找到文件缓冲区,然后操作系统把文件的内容给我们加载或者预加载到缓冲区里面,加载之后我们读写文件的本质就是从缓冲区里面把内容拷贝出去。
可是在进程中怎么快速找到我们自己打开的文件呢?被连起来的文件有可能属于进程a,也有可能属于进程b,哪个文件是和你的进程相关的呢?在我们的进程的PCB当中,当一个进程被创建,除了地址空间页表,它还要创建一个struct files_struct文件描述符表,文件描述符表中包含一个数组,这个数组是可大可小的,一般的Linux系统是32或者64,在云服务器上它可以支持内核扩展,这个struct files_struct中还包含了其他属性,包括打开文件的个数,其他的一些属性信息。这个数组叫做struct file *fd array[],这是一个指针数组,在PCB中会存在一个struct files_struct *files指向文件描述符表。这个数组中放的就是这个进程打开的文件,把操作系统打开的struct file对象填充到我们数组对应的指定下标当中。此时那些进程有哪些文件就被关联起来了,建立被打开的文件和进程之间的映射关系。所以进程要访问任意一个被打开的文件就可以通过下标来访问了。
所以文件描述符的本质就是数组下标
当我们的用户层在进行open调用的时候,就会在操作系统里创建一个新的struct file,然后在当前进程的文件描述符表里面找到一个没有被使用的下标,然后把struct file的地址填进去,此时进程和文件就关联了。下来读取数据(read)时要传fd,当前进程调用read,进程拿着fd去文件描述符中的数组中找,就能找到对应的文件,每一个文件都有对应的文件缓冲区,操作系统把磁盘中的内容预加载到缓冲区当中,然后把文件缓冲区当中的数据拷贝到用户层的buffer中。
文件描述符的分配原则:最小的没有被使用的作为新的fd分配给用户。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/2301_81290732/article/details/146403330
阅读剩余
版权声明:
作者:SE_Wang
链接:https://www.cnesa.cn/4226.html
文章版权归作者所有,未经允许请勿转载。
THE END