[Linux]文件描述符(万字详解)

[Linux]文件描述符

文件系统接口

在学习文件描述符前,首先要了解一下Linux系统常用的文件系统接口。

open函数

//open函数所在的头文件和函数声明
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • open函数有两个接口,三个参数的接口是在两个参数的接口的基础上添加了控制创建文件的权限功能,更适合写文件时使用
  • pathname参数 – 要打开的文件所在的路径
  • flags参数 – 打开文件的方式
  • mode参数 – 如果要创建文件,文件的权限
  • 函数成功返回一个新的文件描述符,函数失败返回-1,错误码被设置。

其中flags参数采用位图结构来接收要操作文件的打开模式,位图结构在这里的作用是只使用一个参数就可以传入多个标志信息,位图结构的具体形式是将flags参数的这样一个32位的整形变量中的每一个比特位都看作为一个个体,每个比特位都代表一个标志信息,位图结构的示意图如下:

image-20230901110937034

假设flags参数的倒数第一比特位表示是否清空文件,如果该比特位为1表示需要清空文件,如果为0则表示不需要清空文件。

为了测试open函数编写如下代码:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  int fd = open(LOG, O_WRONLY);
  if (fd == -1)
  {
    printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    exit(1);
  }
  return 0;
}

编译代码运行并查看结果:

image-20230901112138630

由于当前路径下不存在log.txt文件,O_WRONLY打开模式是以读的方式打开文件,因此open函数使用失败,从错误信息中也可以看到失败原因是文件不存在。

再编写如下代码测试open函数:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  int fd = open(LOG, O_WRONLY | O_CREAT);
  if (fd == -1)
  {
    printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    exit(1);
  }
  return 0;

编译代码运行并查看结果:

image-20230901112558183

执行程序后,我们能够发现由于文件权限是存在问题的,因此文件名被高亮显式了,由于本次使用的是O_WRONLY | O_CREAT 模式打开 O_CREAT模式是如果文件不存在就创建文件,但是由于没有设置权限,因此文件权限存在问题。

再编写如下代码测试open函数:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  umask(0);
  int fd = open(LOG, O_WRONLY | O_CREAT, 0666);
  if (fd == -1)
  {
    printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    exit(1);
  }
    else  printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
  return 0;
}

编译代码运行并查看结果:

image-20230901122558019

三参数的open函数接口第三个接口提供了创建文件是设置文件权限的功能,另外本段代码中使用了umask函数,设置了该进程所使用的umask值,保证所创建的文件权限是想获得的。

//umask函数所在的头文件和函数声明
#include <sys/types.h>
#include <sys/stat.h>

mode_t umask(mode_t mask);

close函数

//close函数所在的头文件和函数声明
#include <unistd.h>

int close(int fd);
  • close函数关闭文件能够避免文件内容出现问题
  • close函数要关闭对应文件要传入使用open函数打开该文件时的返回值(文件描述符)
  • 成功返回0,失败返回-1并且设置错误码

编写如下代码测试close函数:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  umask(0);
  int fd = open(LOG, O_WRONLY | O_CREAT, 0666);
  if (fd == -1)
  {
    printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    exit(1);
  }
  else  printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));

  close(fd);
  return 0;
}

编译代码运行并查看结果:

image-20230901123637228

write函数

//write函数所在的头文件和函数声明
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • write函数能够将数据写入相应的文件中
  • fd参数 – 用open函数打开该文件时的返回值(文件描述符)
  • buf参数 – 要写入文件的数据
  • count参数 – 要写入到文件中的数据字节数
  • 成功返回写入的数据字节数,失败返回-1并且设置错误码

编写如下代码测试write函数:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  umask(0);
  int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
  if (fd == -1)
  {
    printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    exit(1);
  }
  else  printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));

  const char* msg = "hello world\n";
  write(fd, msg, strlen(msg)); 

  close(fd);
  return 0;
}

编译代码运行并查看结果:

image-20230901124130335

说明:

  • O_TRUNC模式是在打开文件时先清空文件
  • 向文件写入数据时不需要将'\0'写入,其作用是在语言上表示字符串的结尾,在文件中不需要。

再编写如下代码测试write函数:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  umask(0);
  int fd = open(LOG, O_WRONLY | O_CREAT | O_APPEND, 0666);
  if (fd == -1)
  {
    printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    exit(1);
  }
  else  printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));

  const char* msg = "hello world i love you\n";
  write(fd, msg, strlen(msg));

  close(fd);
  return 0;
}

编译代码运行并查看结果:

image-20230901125840962

说明: O_APPEND模式是采用追加的方式,需要和O_WRONLY模式配合使用才能实现追加写入。

read函数

//read函数所在的头文件和函数声明
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • read函数能够读取文件中的数据
  • fd参数 – 要读取的文件的文件描述符
  • buf参数 – 接收文件中数据的字符串变量地址
  • count参数 – buf能接收的最大数据量,单位为字节
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  int fd = open(LOG, O_RDONLY);
  if (fd == -1)
  {
    printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    exit(1);
  }
  else  printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));

  char buffer[256];
  ssize_t n = read(fd, buffer, sizeof(buffer)-1);
  if (n > 0)
  {
    buffer[n] = '\0';
  }
  printf("buffer: %s", buffer);
  close(fd);
  return 0;
}

编译代码运行并查看结果:

image-20230901132135484

说明:

  • O_RDONLY模式是以读取的方式打开文件
  • 用于接收read函数读取到的数据的字符串要在末尾预留一个元素存储'\0'

系统接口与编程语言库函数的关系

由于计算机的体系结构中,操作系统是对上向用户提供服务,向下管理硬件的部分,并且文件的操作是会涉及到硬件的使用,导致编程语言语言库函数中的文件操作一定是对系统接口的封装,无论任何编程语言都是如此,因此无论何种编程语言其文件操作的本质都是相同的。计算机体系结构示意图如下:

image-20230901133244418

文件描述符

文件描述符的概念

在学习文件描述符前要知道,观察Linux操作系统的文件系统接口,能够看出文件操作和文件描述符强相关,open函数的返回值,close函数、write函数、read函数的参数都是文件描述符。

首先,进行文件操作时,操作系统不会将磁盘中的文件加载到内存中,而是在内存中创建一个描述文件的结构(如下图struct file),该结构会管理一个缓冲区,内存和磁盘文件的数据交换都是通过这个缓冲区。示意图如下:

image-20230903133235469

其次,由于文件操作是由进程调用了系统接口完成的,因此进程控制块(如下图struct task_struct)需要记录这些描述文件的结构,因此进程控制块会申请一块创建一个结构(如下图struct files_struct)描述进程操作的文件,然后在进程控制块中使用一个变量(如下图struct files_struct*)记录这个进程操作的文件的地址。示意图如下:

image-20230903134916356

最后,由于一个进程可以操作的文件数量众多,因此描述进程操作的文件的结构(如下图struct files_struct)中,会用一个数组(如下图struct file* fd_array[])记录所有该进程操作的文件的描述结构(如下图struct file)的地址,而这个数组的下标就是文件描述符。示意图如下:

在这里插入图片描述

说明: 文件描述符的存在使得进程管理和文件管理处于轻耦合的状态,二者之间只是使用了文件描述结构的指针联系在了一起。

在这里插入图片描述

文件数据交换的原理

  • 进程调用系统接口write之后,操作系统会将要传入到磁盘文件的数据拷贝至该文件描述结构中管理的缓冲区,然后操作系统会根据自身的刷新策略,在合适的时候将数据刷新到磁盘文件中。

  • 进程调用系统接口read之后,操作系统会将磁盘文件中的数据拷贝至该文件描述结构中管理的缓冲区,然后将该缓冲区中的内容拷贝至进程指定的位置中。

理解“一切皆文件”

在Linux操作系统中,一切的外设都被看作是文件,因此说“Linux下一切皆文件”。关于“一切皆文件”的理解如下:

首先,操作系统在管理硬件时,并不是直接管理硬件设备的,二者之间要通过驱动程序这一中间软件来完成交互,外设与计算机的交互方式就是数据写入和读取,因此在各个硬件对应的驱动程序中会存在该硬件的读写方法。示意图如下:

image-20230903143346572

虽然驱动程序都设计了对应硬件的读写方法声明,但并不是都有具体实现的,比如键盘只有读取方法,无法向键盘写入。

其次,Linux操作系统会使用描述文件的结构(如下图struct file)来描述每一个外设,并且该结构中会使用函数指针的形式来记录驱动程序所提供的读写方法,然后操作系统要进行外设数据的写入时,只需要将数据写入至文件对应的缓冲区,然后使用函数指针调用对应的函数方法将数据写入写入外设,操作系统读取外设数据时,只需要调用函数指针调用对应的方法将数据写入文件对应的缓冲,然后从缓冲区中读取数据。示意图如下:

image-20230903144604590

由于所有的外设都被统一的使用了描述文件的结构描述,并且进程都是利用缓冲区交换数据,调用对应的读写方法进行读写,其操作方式和文件的操作方式相同,因此从进程的角度看外设和磁盘文件是一样的,因此才说“Linux下一切皆文件”。

进程默认文件描述符

进程在运行时,会默认打开三个文件,分别是标准输入、标准输出、标准错误。为了验证这编写如下代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
  printf("fd:%d\n", fd);
  close(fd);
  return 0;
}

编译代码运行并查看结果:

image-20230903151231601

文件描述符代表的是记录进程操作文件的结构的数组的下标,文件描述符的使用规则是从数组起始位置开始,找到第一个没被使用的位置,进程运行时默认打开了标准输入(对应文件描述符为0)、标准输出(对应文件描述符为1)、标准错误(对应文件描述符为2),因此再打开的文件的文件描述符是从3开始。所以能够看到上面的代码打开文件获取的文件描述符为3。

文件描述符和编程语言的关系

在C语言标准库中使用了struct FILE描述文件信息,将标准输入命名为stdin,标准输出命名为stdout,标准错误命名为stderr,由于Linux操作系统的提供的文件操作接口都需要使用文件描述符,因此struct FILE需要包含文件描述符字段。C语言进行进程文件操作和struct FILE强相关,C语言库函数中提供的文件操作函数都会利用struct FILE中记录的文件描述符来调用Linux系统接口来完成。在Linux系统下C语言的struct FILE会有一个变量 _fileno记录文件描述符。为了验证这编写如下代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  printf("stdin:%d\n", stdin->_fileno);
  printf("stdout:%d\n", stdout->_fileno);
  printf("stderr:%d\n", stderr->_fileno);
  FILE* fp = fopen("LOG", "w");
  printf("fp:%d\n", fp->_fileno);
  fclose(fp);
  return 0;
}

编译代码运行并查看结果:

image-20230903154929366

在Linux操作系统中,无论何种编程语言,只要使用文件操作必然需要对文件描述符进行一定的封装来使用,才能完成Linux操作系统下的文件操作。

重定向

输出重定向

输出重定向是将进程原本应该打印到显示器上的数据,输出到文件中。

实现输出重定向的原理: 输出函数是将文件描述符1作为参数调用系统接口函数实现的,修改文件描述符表中1号位置指向的文件,即可完成输出重定向。输出函数的封装中只是使用文件描述符1,无法知晓输出位置的改变。落实到C语言中就是printf是向stdout输出数据,但是stdout也只是封装了文件描述符1,修改文件描述符表中的指向的文件,printf也无法知晓。

为了模式实现输出重定向编写如下代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  int fd = close(1);
  open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
  printf("hello world\n");
  printf("hello world\n");
  printf("hello world\n");
  close(fd);
  return 0;
}

编译代码运行查看结果:

image-20230903163810294

输入重定向

输入重定向是将进程原本应该从键盘中获取数据,变为从文件中获取数据。

实现输入重定向的原理: 输入函数是将文件描述符0作为参数调用系统接口函数实现的,修改文件描述符表中0号位置指向的文件,即可完成输入重定向。输入函数的封装中只是使用文件描述符0,无法知晓输入位置的改变。落实到C语言中就是scanf是从stdin获取数据,但是stdin也只是封装了文件描述符0,修改文件描述符表中的指向的文件,scanf也无法知晓。

为了模式实现输入重定向编写如下代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  close(0);
  int fd = open(LOG, O_RDONLY);
  int a = 0;
  int b = 0;
  scanf("%d %d", &a, &b);
  printf("%d %d\n", a, b);
  close(fd);
  return 0;
}

编译代码运行查看结果:

image-20230903164454740

追加重定向

追加重定向是将进程原本应该追加打印到显示器上的数据,变为追加打印到文件中。

实现追加重定向的原理: 将输出函数使用的文件描述符1指向改为指定文件,并且在打开文件时使用追加的方式。

为了模式实现追加重定向编写如下代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  close(1);
  int fd = open(LOG, O_WRONLY | O_CREAT | O_APPEND, 0666);
  printf("hello world\n");
  printf("hello world\n");
  printf("hello world\n");
  close(fd);
  return 0;
}

编译代码运行查看结果:

image-20230903174215021

说明: 在实现重定向时也可以选择如下的输入或输出方式:

fscanf(stdin, ...);		//等效于scanf
fprintf(stdout, ...);	//等效于printf

使用指令完成重定向

Linux操作系统中可以使用指令完成重定向,为了验证编写如下代码:

#include <stdio.h>

int main()
{
  printf("hello->printf\n");
  fprintf(stdout, "hello->fprintf stdout\n");

  fprintf(stderr, "hello->fprintf stderr\n");
  return 0;
}

编译代码运行查看结果:

image-20230903175329455

./myfile > log.txt将./myfile的输出重定向到 log.txt文件中,也就是将 log.txt的文件信息写入到该进程的文件描述符1中,2>&1是将该进程的文件描述符1的文件信息写入到文件描述符2中。

重定向系统调用

Linux操作系统中提供了能够重定向的系统调用dup2

//dup2函数所在的头文件和声明
#include <unistd.h>

int dup2(int oldfd, int newfd);
  • dup2函数会将输出到文件描述符newfd的数据改为输出到文件描述符oldfd对应的文件中。
  • dup2函数会将文件描述符oldfd的文件信息写入到文件描述符newfd的文件信息中。

为了验证dup2编写如下代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
  int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC);
  dup2(fd, 1);
  printf("this is printf\n");
  fprintf(stdout, "this is fprintf->stdout\n");
  close(fd);
  return 0;
}

编译代码运行查看结果:

image-20230903182422548

C语言文件缓冲区

在C语言标准库中提供的struct FILE中包含了缓冲区字段,使用C语言库函数中的文件操作向文件写入数据,C语言标准库只会将数据写入到对应文件的struct FILE中的缓冲区,然后结合一定的刷新策略将缓冲区中的内容刷新到Linux操作系统中的文件缓冲区中。示意图如下:image-20230903191243869

缓冲区刷新策略:

  • 无缓冲 – 数据不经过缓冲区直接写入操作系统

  • 行缓冲 – 数据写入缓冲区后遇到'\n',将'\n'及以前的数据写入操作系统

  • 全缓冲 – 缓冲区写满后将数据写入操作系统

  • 显示器采用的刷新策略:行缓冲

  • 普通文件采用的刷新策略:全缓冲

将缓冲区中的数据刷新到操作系统需要使用系统调用,而系统调用的使用要花费大量时间,因此采用缓冲区刷新策略,可以提高效率。

为了验证C语言struct FILE中的缓冲区编写如下代码:

#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define LOG "log.txt"

int main()
{
  fprintf(stdout, "hello fprint->stdout\n");

  const char* msg = "hello write\n";
  write(1, msg, strlen(msg));

  fork();
  return 0;
}

编译代码运行查看结果:

image-20230903193030324

由于不进行重定向时,打印数据的位置是显示器,采用的是行缓冲的策略,因此数据会在创建子进程前被刷新到操作系统,进行重定向后打印数据的位置是普通文件,采用的是全缓冲的策略,但数据不够写满缓冲区,因此数据会在进程解说,也就是创建子进程之后刷新,而无论父子进程哪一个先刷新都会发生写时拷贝,导致最终数据被刷新到操作系统两次。调用系统调用wrire是直接写入操作系统因此不受影响。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

好想写博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值