【Linux】:文件fd

朋友们、伙计们,我们又见面了,本期来给大家带来关于文件fd的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个 人 主 页 :stackY、

C + + 专 栏 :C++

Linux 专 栏 :Linux

目录

[1. 文件基础介绍](#1. 文件基础介绍)

[1.1 文件 = 内容 + 属性](#1.1 文件 = 内容 + 属性)

[1.2 文件操作](#1.2 文件操作)

[1.3 文件数据](#1.3 文件数据)

[1.4 访问文件](#1.4 访问文件)

[1.5 描述文件](#1.5 描述文件)

[2. 语言级文件接口](#2. 语言级文件接口)

[3. 系统级文件接口](#3. 系统级文件接口)

[3.1 open返回值](#3.1 open返回值)

[3.2 如何理解一切皆文件](#3.2 如何理解一切皆文件)

[3.3 文件数据读写本质](#3.3 文件数据读写本质)

[3.4 文件fd的分配规则](#3.4 文件fd的分配规则)


1. 文件基础介绍

当我们建立一个文件时,此时的文件是一个空文件,那么它要不要占据空间呢?

1.1 文件 = 内容 + 属性

在我们的计算机内部存在许许多多的文件,这些文件默认都是在磁盘中保存的,那么在磁盘中保存的不仅仅是文件的内容,还有一些描述文件的属性,所以文件通常由两部分构成:

① 文件内容 ② 文件属性

因此我们建立的空文件也是需要占据内存空间的 ,占据的空间用于存储文件的属性

1.2 文件操作

文件 = 内容 + 属性,因此对文件进行操作的方式有两种:

  • ① 对文件内容操作
  • ② 对文件属性操作

1.3 文件数据

文件是由两部分构成:内容 + 属性,所以一个文件来说,文件的内容和属性都属于文件的数据,所以要能完整的存储一个文件必须既存储该文件的内容又存储文件的属性,它默认是存储在磁盘中的。

1.4 访问文件

我们要访问一个文件,通常是使用代码进行访问,所以要能访问到文件的数据,首先得打开这个文件,所以我们的代码必须运行起来,换句话说我们访问文件其实就是进程访问文件。由于文件在磁盘中存储,所以打开文件之后就会将文件从磁盘加载到内存中,加载磁盘上的文件,就一定会涉及到访问磁盘设备,这个过程由OS来做。

1.5 描述文件

一个进程很有可能打开不止一个文件,有可能打开多个文件,又或者说多个进程打开多个文件,将这些文件打开就是加载到内存,在操作系统运行时,可能会打开很多的文件,因此为了防止打开的文件混乱,操作系统也是要将这些打开的文件管理起来的,如何管理呢?先描述、再组织!

一个文件要被打开,一定要先在内核中形成被打开的文件对象:


所以文件按照是否被打开可以分为两类:被打开文件、未被打开文件;打开的文件在内存中,未被打开的文件在磁盘中 ,所以我们本次研究文件操作的本质就是:进程与被打开文件之间的关系。

2. 语言级文件接口

在这里我们主要来回顾一下C语言中的文件接口:

打开文件接口:fopen

打开方式:

  • r:只读方式打开文件,文件不存在则打开失败;
  • w:写入的方式打开文件,文件不存在则创建;
  • a:追加式写入的方式打开文件,文件不存在则创建。

小细节:

以"w"方式打开文件,首先会清空文件内容,再写入;

以"a"方式打开文件,会在文件结尾开始写入,不清空。
关闭文件接口:fclose

写入接口:fputs、fwrite

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

int main()
{
    // 打开
    FILE* fp = fopen("log.txt", "w");
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    const char * msg = "hello\n";
    int cnt = 10;
    while(cnt--)
    {
        fputs(msg, fp);  // 写入
    }
    // 关闭
    fclose(fp);
    return 0;
}

3. 系统级文件接口

我们上面写的C语言打开文件的相关接口,在底层一定封装了系统调用接口!
一个进程通过操作系统打开文件,所以操作系统一定要给我们提供系统调用接口。
接下来就来看一看文件操作相关的系统调用接口:

打开文件接口:open

这里来说一下这个标志位,标志位的常用选项有:

  • O_WRNOLY:写入
  • O_CREAT:创建
  • O_TRUNC:清空
  • O_APPEND:追加

这些标志位可以通过 | 来组合在一起使用。

文件权限我们设置为默认权限0666即可。
写入文件接口:write

关闭文件接口:close

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    // 打开文件                    写入 + 创建 + 清空
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    const char *msg = "Hello Linux";
    // 写入
    write(fd, msg, strlen(msg));
    // 关闭
    close(fd);
    return 0;
}

在往文件写入内容时不需要在最后面加上'\0'。

3.1 open返回值

我们一次打开多个文件,看看它们的返回值有何特点:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    // 打开文件                    写入 + 创建 + 清空
    int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    
    printf("fd1: %d\n", fd1);
    printf("fd2: %d\n", fd2);
    printf("fd3: %d\n", fd3);
    printf("fd4: %d\n", fd4);
    // 关闭
    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);
    return 0;
}

可以看到这些返回值都是一个个连续的小整数 ,很类似于数组的下标
接下来就从系统的层面来理解一下文件fd:

在磁盘中存在众多的文件,每打开一个文件就需要在内核中形成对应的文件结构体对象 ,OS既然要管理这些被打开的文件,久而久之当被打开的文件越来越多,那么OS怎么知道哪几个文件是哪个进程打开的呢?

所以在OS中就会有一个struct files_struct 的结构体对象,而这个结构体中存在一个struct file *fd_array\[\] 的结构体指针数组,该数组被我们亲切的称为进程文件描述符表 。每一个进程PCB都有一个指向该数组的指针,在这个数组中,记录了进程与文件结构体对象的对应关系,所以每一个进程都可以找到自己打开的文件。

整个过程就是open先给文件创建对应的结构体对象,再将对应的文件与文件描述符表联系起来,再通过返回之将下标返回给外部。

所以文件描述符fd本质就是数组的下标,OS访问文件只认文件描述符。
那么C语言的FILE是什么呢?

它是C语言提供的一个结构体类型,它的底层必定封装了文件描述符,接下来就通过代码的方式验证一下:

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

int main()
{
    // 打开文件       
    FILE *fp1 = fopen("log.txt1", "w");
    FILE *fp2 = fopen("log.txt2", "w");
    FILE *fp3 = fopen("log.txt3", "w");

    printf("fp1->fd: %d\n", fp1->_fileno);
    printf("fp2->fd: %d\n", fp2->_fileno);
    printf("fp3->fd: %d\n", fp3->_fileno);
    fclose(fp1);
    fclose(fp2);
    fclose(fp3);
    return 0;
}


那么文件描述符表中的0、1、2是什么呢?

在我们进程运行的时候会默认打开三个文件流:

  • 标准输入流:stdin (键盘) 0
  • 标准输出流:stdout(显示器)1
  • 标准错误流:stderr(显示器) 2

OS/C语言为什么要默认把这三个流打开呢?

最主要的原因是为了方便程序员进行默认的输入输出的代码编写。

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

int main()
{
    printf("stdin->fd: %d\n", stdin->_fileno);
    printf("stdout->fd: %d\n", stdout->_fileno);
    printf("stderr->fd: %d\n", stderr->_fileno);
    return 0;
}

3.2 如何理解一切皆文件

磁盘、键盘、显示器、网卡等,这些外设统一按照文件的方式去看待,如何理解呢?

在底层的这些外设,站在方法的角度去考虑,每一种设备都有对应的读写方法 ,例如键盘,它具有读方法,写方法认为是空,显示器具有写方法,读方法认为是空,所以,这些设备的读写方法均不同,为了提升使用效率,在他们各自对应的文件结构体对象中会设置读写方法的函数指针,用于指向各自设备的具体读写方法,在访问这些设备时,直接去各自的文件结构体对象中调用读写方法,不需要关注底层,所以访问外设这一工作只需要访问各自对应的文件结构体,统一把它们当成文件对待,所以Linux下一切皆文件。

3.3 文件数据读写本质

文件一般是存储在磁盘外设上的,struct file是在内核中创建,专门用来管理被打开文件的。

我们对文件进行读写操作的时候,并不能直接操作文件数据,所以在struct file存在一个字段,指向一块空间,叫做文件缓冲区,无论是我们读写文件数据,都需要先把数据加载到文件缓冲区中,这个加载的过程是由OS来完成的。所以我们在应用层进行数据读写的本质就是将内核缓冲区中的数据来回拷贝。

3.4 文件fd的分配规则

1. 进程默认已经打开了文件描述符0、1、2,可以直接使用0、1、2进行数据访问:

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

int main()
{
    char buffer[1024];
    ssize_t s = read(0, buffer, 1024);  // 从0号fd(键盘)中读取数据至buffer
    if(s > 0)
    {
        buffer[s - 1] = 0; // 将读取的'\n'置为'\0'
    } 
    write(1, buffer, strlen(buffer)); // 将buffer内容写入到1号fd(显示器)
    return 0;
}

2. 文件描述符分配规则:寻找最小的,没有被使用的位置,分配给指定的打开文件!

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    // 先关闭2号fd
    close(2);
    // 重新分配
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    printf("fd: %d\n", fd); // 2
    close(fd);
    return 0;
}

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!

相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式