【Linux篇】基础IO - 文件描述符的引入

📌 个人主页: 孙同学_

🔧 文章专栏: Liunx

💡 关注我,分享经验,助你少走弯路!

文章目录

    • [一. 理解文件](#一. 理解文件)
      • [1.1 侠义理解](#1.1 侠义理解)
      • [1.2 广义理解](#1.2 广义理解)
      • [1.3 文件操作的归类认知](#1.3 文件操作的归类认知)
      • [1.4 系统角度](#1.4 系统角度)
    • [二. 回顾C语言文件接口](#二. 回顾C语言文件接口)
      • [2.1 打开文件](#2.1 打开文件)
      • [2.2 对文件进行写入](#2.2 对文件进行写入)
      • [2.3 输出信息到显示器,有哪些方法](#2.3 输出信息到显示器,有哪些方法)
      • [2.4 stdin & stdout & stderr](#2.4 stdin & stdout & stderr)
      • [2.5 打开文件的方式](#2.5 打开文件的方式)
    • [三. 系统文件I/O](#三. 系统文件I/O)
      • [3.1 位图传递标志位](#3.1 位图传递标志位)
      • [3.2 open](#3.2 open)
      • [3.3 write](#3.3 write)
      • [3.4 read](#3.4 read)
    • [四. 访问文件的本质](#四. 访问文件的本质)
      • [4.1 文件描述符](#4.1 文件描述符)

一. 理解文件

我们以前就说过 文件 = 文件内容 + 文件属性

1.1 侠义理解

  • 文件是在磁盘上的,磁盘本质上是个外设,我们访问文件其实就是在系统和磁盘上进行IO
  • 磁盘是永久性的存储介质,文件在磁盘上的存储是永久性的
  • 对文件的所有操作,本质上是对外设的输入输出 简称 IO

1.2 广义理解

  • Linux下一切皆文件(键盘,显示器,网卡,磁盘... )

1.3 文件操作的归类认知

  • 对于0KB的空文件,是占据磁盘的空间的
  • 文件 = 文件内容 + 文件属性
  • 所有对文件的操作本质上是对文件内容和文件属性的操作

1.4 系统角度

  • 对文件的操作本质上是进程对文件的操作
  • 磁盘的管理者是操作系统
  • 文件的读或写本质上不是通过 C语言/C++的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口实现的

文件

  1. "内存级(被打开)"文件
  2. 磁盘级文件

二. 回顾C语言文件接口

2.1 打开文件

cpp 复制代码
// 文件打开接口
FILE *fopen(const char *path, const char *mode);
  • path:表示要打开文件的路径,或者文件名,只有文件名而没有路径表示打开当前路径下的文件。
  • mode:表示打开的方式,比如只读r,只写w,追加a等。

📌 Tips: 我们之前介绍的重定向,>本质上就对应使用的是w选项,>>本质上就对应使用的是a选项。

2.2 对文件进行写入

cpp 复制代码
#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 打开文件的方式

  1. w的方式打开文件时,文件首先会被清空,然后从0开始写
  2. 我们以前说过的重定向,比如echo aaaaa > log.txt,把打印到显示器上的内容写入文件里,前提是我们先得把文件打开。我们的输出重定向>log.txt为什么会把文件内容清空呢?因为我们输出重定向第一步要打开文件,而打开文件,而打开文件第一步先要把文件清空
  3. a的方式打开文件,这种方式叫做追加 ,它一般写的时候会向文件结尾进行写入,不存在的话就创建它。>>叫做追加重定向,以a的方式进行写入本质上也是先要把文件打开,然后再进行写入。

  4. 当我们向文件里写入一段字符串时,我们需不需要在字符串后面加\0呢?答案是不需要,因为\0是c语言的规定,与我文件又有什么关系呢。

三. 系统文件I/O

我们对文件操作的是时候,文件是在磁盘上面的,而真正对文件进行操作的其实是操作系统,操作系统对磁盘文件进行读写访问,我们以前使用c语言对文件的访问其实是c语言封装了系统调用。比如说访问文件得先打开,那么就得先有open

flags有众多选项O_RDONLY表示只读, O_WRONLY表示只写, O_RDWR表示读写,O_TRUNC表示

cpp 复制代码
int open(const char *pathname,int flags,mode_t mode);

如果我们今天要打开一个文件,并且这个文件不存在要新建的话,就用上面的这个open,必须指定权限,如果不指定的话这个权限就是乱码。如果打开一个已经存在的文件,就用两个参数的。

open的返回值:如果打开成功的话返回一个新的文件描述符,如果失败的话-1被返回,并且错误码就被设置了。

flags是一种整形标志位,一共有32个bit位,,如果用O_RDONLY这种选项直接传参的话会很麻烦,所以选用位图的方式来传递。

3.1 位图传递标志位

cpp 复制代码
#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接口

cpp 复制代码
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • 第一个参数:open的返回值,这个返回值叫文件描述符
  • 第二个参数: 是我们要写入的buffer
  • 第三个参数: 是我们要写的数据的长度,返回值是实际写入的长度。写入失败返回-1
cpp 复制代码
#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

💦结论:如果我们要打开文件,并且将它清空,若要用系统级的函数,我们就需要传递这几个标志位。

清空并写入:

cpp 复制代码
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);

如果O_CREAT(不存在就新建),O_WRONLY(以只写入的方式),O_TRUNC(清空)
追加并写入:

cpp 复制代码
int fd = open("log.txt",O_CREAT | O_WRONLY | O_APPEND,0666);

所以C语言中的fopen中的w选项和a选项就会被分别转化为上面的那样。

3.3 write

write这个接口在写的时候参数不是char*,而是void*

cpp 复制代码
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.txt4个字节里面的内容是乱码,这是为什么呢?因为这种写入叫做二进制写入,在实际写的时候把a这个整形变量写到了文件里面,所以文件的大小是4字节,因为整数的大小是4字节,而我们写入的1234567不可显,它是把1234567这个二进制数字写在了磁盘上。而我们想看到的是1234567,那可怎么办呢?

a这个整形格式化处理成字符串,然后将字符串写入到要写的文件中。

此时的log.txt中就是1234567了。

💦结论 : 在系统层面上并不存在所谓的文本写入和二进制写入,系统并不关心你写入的类型,文本写入还是二进制写入其实是语言层 提供的概念。所以我们在c语言中有fpus文本写入fwrie二进制写入,这两个底层最终调用的都是这个接口。

cpp 复制代码
ssize_t write(int fd, const void *buf, size_t count);

格式化输入输出其实就是将内存里的二进制数据转成字符串,在使用write接口将数据写进去。所以格式化输入输出,文本式的写入全都是语言层的概念。这个格式化工作要么是语言来做,要么是我们自己来做。

3.4 read

cpp 复制代码
 #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是什么呢?FILEC语言提供的一个结构体,它是被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分配给用户。


👍 如果对你有帮助,欢迎:

  • 点赞 ⭐️
  • 收藏 📌
  • 关注 🔔
相关推荐
乐亦亦乐3 分钟前
如何将/dev/ubuntu-vg/lv-data的空间扩展到/dev/ubuntu-vg/ubuntu-lv的空间上
linux·数据库·ubuntu
kfhj3 小时前
负载均衡是什么,Kubernetes如何自动实现负载均衡
运维·kubernetes·负载均衡
MarkHD5 小时前
第八天 - paramiko/ssh模块 - 远程服务器管理 - 练习:批量服务器命令执行工具
运维·服务器·ssh
写代码的小王吧6 小时前
【安全】Web渗透测试(全流程)_渗透测试学习流程图
linux·前端·网络·学习·安全·网络安全·ssh
Tee xm7 小时前
清晰易懂的跨平台 MySQL 安装与配置教程
linux·windows·mysql·macos·安装
GalaxyPokemon7 小时前
MySQL基础 [一] - Ubuntu版本安装
linux·运维·ubuntu
musk12127 小时前
wsl2 配置ubuntu 固定ip
linux·tcp/ip·ubuntu
柳鲲鹏8 小时前
UBUNTU编译datalink
linux·运维·ubuntu
追随远方8 小时前
Ubuntu 64-bit 交叉编译 FFmpeg(高级用户指南)
linux·ubuntu·ffmpeg
GalaxyPokemon8 小时前
Muduo网络库实现 [七] - Connection模块
linux·服务器·网络