文件描述符

本文详细介绍了C语言文件I/O操作,包括C文件接口、系统文件I/O接口(如open、read、write)、文件描述符(fd)的使用、分配规则、重定向以及dup2系统调用。此外,文章还探讨了硬链接和软链接的概念,以及文件系统中inode和超级块的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文件描述符

  1. 预备知识:
  • 文件 = 文件内容 + 属性(也是数据)
  • 文件的所有操作,无外乎是 对内容对属性 的操作
  • 文件在磁盘(硬件)上放着,我们访问文件先写代码 — 编译 — exe — 运行 — 访问文件:
  • 问:本质是谁在访问文件呢? 答: 进程!!
  • 要向硬件写入,只有操作系统有权力!!普通用户也想要写入呢? 必须让os提供接口! 文件类的系统调用接口!!
  • 显示器是硬件吗?printf向显示器打印(也是一种写入),你为什么不觉得奇怪呢?----->>> 和磁盘写入到文件,没有本质区别 !!
  1. linux认为,一切皆文件:
  • 感性认识:
  • 对于文件而言: 曾经理解的文件:read、write; 显示器:printf/cout ----> 一种write 键盘:scanf/cin ----> 一种read

  • 普通文件 —>> fopen/fread —>> 你的进程的内存(内部) —>> fwrite —>> 文件中

  • 那么什么叫做文件呢?

    答:站在系统的角度,能够被input读取,或者能够output写出的设备就叫做文件!

    ​ 狭义文件:普通的磁盘文件。
    ​ 广义上的文件:显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有的外设都可以称之为文件。

1. C文件IO相关操作

先来段代码回顾C文件接口

  • func.c写文件 :
#include <stdio.h>
#include <string.h>

int main()
{
    FILE *fp = fopen("log.txt", "w");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    //进行文件操作
    const char *s1 = "hello fwrite\n";
    fwrite(s1, strlen(s1), 1, fp);
    const char *s2 = "hello fprintf\n";
    fprintf(fp, "%s", s2);
    const char *s3 = "hello fputs\n";
    fputs(s3, fp);
    fclose(fp);
    return 0;
}

在这里插入图片描述

其中strlen不需要加 1, \0 结尾是c语言的规定,文件要保存的是有效数据, \0 只是结束标识符。

当以w模式打开文件时,第一件事是先清空文件然后再打开!

若以a模式打开文件,实则是在结尾追加内容!

  • func.c读文件
#include <stdio.h>
#include <string.h>
int main()
{
    FILE *fp = fopen("myfile", "r");
    if (!fp)
    {
        printf("fopen error!\n");
    }
    char buf[1024];
    const char *msg = "hello solity!\n";
    while (1)
    {
        // 注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
        ssize_t s = fread(buf, 1, strlen(msg), fp);
        if (s > 0)
        {
            buf[s] = 0;
            printf("%s", buf);
        }
        if (feof(fp))
        {
            break;
        }
    }
    fclose(fp);
    return 0;
}
  • 输出信息到显示器,你有哪些方法
#include <stdio.h>
#include <string.h>
int main()
{
    const char *msg = "hello fwrite\n";
    fwrite(msg, strlen(msg), 1, stdout);  //指定字符串向stdout输出
    printf("hello printf\n");  //直接打印
    fprintf(stdout, "hello fprintf\n");  //直接向stdoout输出流打印
    return 0;
}

显示结果:

在这里插入图片描述

什么叫做当前路径?

  • 当一个进程运行起来时,这个进程都会记录自己当前所处的工作路径!

  • ps axj查看进程

    在这里插入图片描述

  • ls /proc/ 进程 查看cwd就是当前路径的变化

    在这里插入图片描述

    将查看到的cwd路径与当前pwd路径先对比,是完全一样的!!

2. stdin & stdout & stderr

  1. C默认会打开三个输入输出流,分别是

    • stdin
    • stdout
    • stderr
  2. 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

总结

打开文件的方式,使用man手册查看:man fopen

在这里插入图片描述

如上,是我们之前学的文件相关操作。还有 fseek ftell rewind 的函数,在C部分已经有所涉猎,这里便不再举例。

3. 系统文件I/O

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。

在使用系统接口之前,先来了解一下open函数:

接口介绍 :open

int open(const char *pathname, int flags, mode_t mode);

介绍几个常用的常量:

  • O_CREAT:在文件打开过程中创建新文件
  • O_RDONLY:以只读方式打开文件。
  • O_WRONLY:以只写方式打开文件。
  • O_RDWR:以读写方式打开文件。
  • O_APPEND:在文件末尾追加数据,而不是覆盖现有内容。
  • O_TRUNC:如果文件已经存在,将其截断为空文件。
  • O_EXCL:与 O_CREAT 一起使用时,如果文件已经存在,则 open() 调用将失败。
  • O_SYNC:使文件写操作变为同步写入,即将数据立即写入磁盘。
  • O_NONBLOCK:以非阻塞方式打开文件,即使无法立即进行读写操作也不会被阻塞。

这些常量可以通过按位或( | )操作组合使用,以同时指定多个常量,例如,O_RDWR | O_CREAT 可以同时指定读写和创建。

这几个常量被定义在头文件fcntl.h中

fcntl 是 “file control” 的缩写。它是由 “file”(文件)和 “control”(控制)两个单词组合而成的。都是配合open函数使用。

先来直接以代码的形式,实现和上面一模一样的代码:

func.c 写文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    umask(0);
    int fd = open("myfile", O_WRONLY | O_CREAT, 0644);  //0644表示创建的文件的权限
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    int count = 5;
    const char *msg = "hello solity!\n";
    int len = strlen(msg);
    while (count--)
    {
        write(fd, msg, len); 
        // msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
    }
    close(fd);
    return 0;
}

写入结果:

在这里插入图片描述

hello.c读文件

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    int fd = open("myfile", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    const char *msg = "hello solity!\n";
    char buf[1024];
    while (1)
    {
        ssize_t s = read(fd, buf, strlen(msg)); // 类比write
        if (s > 0)
        {
            printf("%s", buf);
        }
        else
        {
            break;
        }
    }
    close(fd);
    return 0;
}

读取结果:

在这里插入图片描述

还有write read close lseek都是类比C文件的相关接口。

open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用库函数

上面的 fopen、fclose、fread、fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。而 open、close、read、write、lseek 都属于系统提供的接口,称之为 系统调用接口

回忆一下操作系统概念的相关图:

在这里插入图片描述

系统调用接口和库函数的关系,一目了然。所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

4. 文件描述符fd

通过对open函数的学习,我们知道了文件描述符就是一个小整数 0 & 1 & 2

Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.

这三个整数对应的物理设备一般是:

  • 0:键盘 (输入)
  • 1:显示器 (输出)
  • 2:显示器 (错误)

所以输入输出还可以采用如下方式:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
    char buf[1024];
    ssize_t s = read(0, buf, sizeof(buf));  //0代表上述的stdin
    if (s > 0)
    {
        buf[s] = 0;
        write(1, buf, strlen(buf));  //1代表stdout
        write(2, buf, strlen(buf));  //2代表stderr
    }
    return 0;
}

输出结果:

在这里插入图片描述

在这里插入图片描述

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

5. 文件描述符的分配规则

直接看代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    int fd = open("myfile", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    close(fd);
    return 0;
}

输出发现是 fd: 3

在这里插入图片描述

如果关闭关闭0或者2呢?

再看:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    close(0);
    // close(2);
    int fd = open("myfile", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    close(fd);
    return 0;
}

发现是结果是: fd: 0

在这里插入图片描述

或者fd为2可见:

在这里插入图片描述

可见文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

6. 重定向

那如果关闭1呢?看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
    close(1);
    int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    fflush(stdout);
    close(fd);
    exit(0);
}

结果终端无任何输出内容:

在这里插入图片描述

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中:

在这里插入图片描述

其中,fd=1,这种现象叫做输出重定向。常见的重定向有: >>><

那重定向的本质是什么呢?

  • 操作的文件描述符/文件流指针并没有改变,但是实际操作的文件却改变了
  • 实现方式就是替换这个描述符对应的文件描述信息
  • 实际是描述符的重定向,改变描述符所指向的文件,就改变了数据的流向

在这里插入图片描述

7. 使用 dup2 系统调用

函数原型如下:

#include <unistd.h>
int dup2(int oldfd, int newfd);

解读:

  • 让 newfd 这个描述符也指向oldfd所指向的文件,这时候 oldfd 和 newfd 都能够操作 oldfd 所指向的文件,两个描述符操作的都是 oldfd 指向的信息
  • 因为newfd本身有可能已经打开了文件,但是现在要让它保存oldfd指向的信息,因此就需要先释放掉newfd当前的信息

示例代码

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
    int fd = open("./log", O_CREAT | O_RDWR);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    close(1);
    dup2(fd, 1);
    for (;;)
    {
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0)
        {
            perror("read");
            break;
        }
        printf("%s", buf);
        fflush(stdout);
    }
    return 0;
}

printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1

但此时,fd:1下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址

所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

8. FILE

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。

所以C库当中的FILE结构体内部,必定封装了fd。

来段代码在研究一下:

#include <stdio.h>
#include <string.h>
int main()
{
    const char *msg0 = "hello printf\n";
    const char *msg1 = "hello fwrite\n";
    const char *msg2 = "hello write\n";
    printf("%s", msg0);
    fwrite(msg1, strlen(msg0), 1, stdout);
    write(1, msg2, strlen(msg2));
    fork();
    return 0;
}

运行出结果:

在这里插入图片描述

但如果对进程实现输出重定向呢?

./hello > file , 我们发现结果变成了:

在这里插入图片描述

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。

为什么呢?肯定和fork有关!

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
  • 但是进程退出之后,会统一刷新,写入文件当中。
  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
  • write 没有变化,说明没有所谓的缓冲。

综上: printf、fwrite库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。

那这个缓冲区谁提供呢?

printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

如果有兴趣,可以看看FILE结构体

typedef struct _IO_FILE FILE; 在/usr/include/stdio.h

查看指令: ls /usr/include/stdio.h

9. 理解文件系统

我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。

在这里插入图片描述

每行包含7列:

  • 模式
  • 硬链接数
  • 文件所有者
  • 大小
  • 最后修改时间
  • 文件名

ls -l 读取存储在磁盘上的文件信息,然后显示出来

在这里插入图片描述

其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息

在这里插入图片描述

上面的执行结果有几个信息需要解释清楚:inode

为了能解释清楚inode我们先简单了解一下文件系统

在这里插入图片描述

Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,

  • Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子。
  • 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
  • inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
  • i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
  • 数据区:存放文件内容

将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。

在这里插入图片描述

为了说明问题,我们将上图简化:

在这里插入图片描述

创建一个新文件主要有一下4个操作

  1. 存储属性

    内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。

  2. 存储数据

    该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。

  3. 记录分配情况

    文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。

  4. 添加文件名到目录

    新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

10. 理解硬链接

我们看到,真正找到磁盘上文件的并不是文件名,而是inode。

其实在linux中可以让多个文件名对应于同一个inode。

在这里插入图片描述

  • abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 37080904 的硬连接数为2。

  • 我们在删除文件时干了两件事情:

    1.在目录中将对应的记录删除,

    2.将硬连接数-1,如果为0,则将对应的磁盘释放。

11. 软链接

硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法

在这里插入图片描述

在这里插入图片描述

下面解释一下文件的三个时间:

  • Access 最后访问时间
  • Modify 文件内容最后修改时间
  • Change 属性最后修改时间
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值