【Linux】文件描述符

简介: 【Linux】文件描述符

思维导图

学习目标

      这篇博客学习文件描述符,对文件描述符进行进一步的学习,在了解一下硬件如何与文件联系起来。

一、回顾一下文件系统

      我们在C语言中学习了文件系统,连接了一下关于文件的一些函数:例如:fopen函数,fclose函数等……

字符输入函数 fgetc 所有输入流
字符输出函数 fputc 所有输入流
文本行输入函数 fgets 所有输入流
文本行输出函数 fputs 所有输入流
格式化输入函数 fscanf 所有输入流
格式化输出函数 fprintf 所有输入流
二进制输入 fwrite 文件
二进制输出 fread 文件

1.1 介绍一下文件的打开模式:

r(只读) 为了输入数据,打开一个已经存在的文本文件 失败
w(只写) 为了输出数据,打开一个文本文件 建立了一个新的文本文件
a(追加) 向文本文件末尾添加数据 建立了一个新的文本文件

      我们来写一段代码进行文件的一些基本操作,我们可以通过fopen函数来打开一个文件,并利用fwrite函数进行数据的写入,还有很多的写入操作;最后我们可以利用fclose函数进行文件的关闭。我们在进行文件操作时,我们需要先将程序跑起来,这样文件的打开和关闭是在CPU上进行运行的。

#include <stdio.h>
#include <errno.h>
#include <string.h>
 
int main()
{
  FILE* fp = fopen("log.txt", "w");
  if(fp == NULL)
  {
    perror("fopen:");
    return 1;
  }
  char* tmp = "Hello, Linux!\n";
  fwrite(tmp, strlen(tmp), 1,fp);
  fclose(fp);
  return 0;
}

      之后,我们来看一些现象:在之前我们学习了两个符号:> 和 >> 。我们需要将这个fopen函数和这两个函数有一定的关系。

fopen以读的形式打开文件和 > 的关系

      当我们以读的形式打开文件时,当文件不存在时,就需要进行创建文件;当文件存在时,我们需要将文件进行清空,然后再进行写入操作。

      当我们使用echo > 指令时,会出现同一个现象:

      所以,fopen函数以读的形式打开文件和 > 指令追加到文件的操作是一样的。

fopen以追加的形式打开文件和 >> 的关系

      当我们以追加的形式打开文件,如果文件不存在,我们需要重新创建一个文件;如果文件存在,我们不需要进行刷新,直接将数据写入文件的末尾。

在使用echo >> 指令进行文件数据的追加:

1.2 提炼一下对文件的理解

      我们的文件的打开,本质是将文件的打开和写入都是交给进程进行操作的,文件在没有打开之前,在磁盘中存放,在一个进程中,我们会打开很多文件,因此操作系统将会把文件进行统一管理,先描述后组织。 文件 = 内容 + 属性。

1.3 什么叫做当前路径?

      我们使用fopen函数时,需要将要打开文件的路径进行传入,我们大多数人都只是利用当前路径进行创建文件,并进行文件的操作,我们需要将当前路径给大家进行解释:

      当我们在使用fopen函数进行文件操作时,出现了未存在的文件路径,在执行完程序后,我们可以看出文件创建在当前可执行程序的路径下。

所以,当前路径是否为可执行程序的路径??

      在我们获取到进程的pid,进行查询该进程信息,我们可以看到两个软链接文件cwd和exe,cwd就是进程运行时我们所处的路径,而exe就是该可执行程序的所处路径。

      实际上,我们这里所说的当前路径不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。

1.4 stdin & stdout & stderr

  • C语言会默认打开三个输入输出流,分别是stdin、stdout、stderr;
  • 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

二、文件的系统调用接口

      我们在进行文件的操作时,如果需要向文件中进行写入,我们需要访问磁盘,磁盘是一种硬件,因此,向文件中进行写入,本质就是向硬件中进行写入。我们用户没有权利向硬件进行写入操作,需要操作系统进行写入,操作系统必须要提供系统调用(OS不相信任何人的话),在之前的函数,就是在语言层面中对系统调用函数的封装。

下面,来介绍一下常用的系统调用函数:

open函数的原型:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);

函数的参数部分:

  • 第一个参数:我们需要进行写入的文件路径,我们需要将其传入
  • 第二个参数:一些标志位,我们需要认识一些标志位,这些标志位通过按位与进行传参,我们需要通过位图的知识点来将每一个标志位进行分开,分别进行不同函数的操作。open函数的一些标志位的写法和用途:
标志位的写法 标志位的功能
O_RDONLY 只读模式,打开文件用于读取,文件必须存在
O_WRONLY  写模式,打开文件用于写入,如果文件已存在则清空文件内容,如果文件不存在则创建新文件。
O_RDWR 读写模式,打开文件用于读取和写入,文件必须存在。
O_APPEND 追加模式,打开文件用于写入,在文件末尾添加数据,如果文件不存在则创建新文件。
O_CREAT 创建新文件,如果文件不存在则创建新文件,如果文件已存在则不做任何操作。
O_EXCL 与O_CREAT一起使用,用于创建新文件,如果文件已存在则返回错误。
O_TRUNC 与O_WRONLY或O_RDWR一起使用,打开文件用于写入时清空文件内容。

      下面,我们来讲解一下标志位是如何进行传参的,这种传参方式只需要一个整数就能发挥出好几个整数的作用。

      我们先将int整数有32个比特位,每一个比特位,我们都可以表示一个信息,所以最多我们可以将每一个比特位上都放置一个函数,来进行函数的操作。这样传参的好处是:将函数的参数不需要进行堆积。下面进行代码:

#define O_one   1  // 0000 0001
#define O_two   2  // 0000 0010
#define O_three 4  // 0000 0100
#define O_four  8  // 0000 1000
 
void solve(int n)
{
  if(n & O_one)
  {}
  if(n & O_two)
  {}
  if(n & O_three)
  {}
  if(n & O_four)
  {}
}
  • 第三个参数:表示创建文件的默认权限。在进行创建时,我们有可能创建出的文件的默认权限不是我们想要的,我们需要进行修改起始默认权限,这样就可以将我们想要的文件默认权限给求出。

这里来简要介绍一下umask函数:

umask函数原型:

#include <sys/stat.h>
 
mode_t umask(mode_t cmask);

umask函数的用途:是在创建文件时设置或者屏蔽掉文件的一些权限,使用时是遵循就近原则。

write函数的原型:

ssize_t write(int fd,const void*buf,size_t count);

函数的参数部分:

  • 第一个参数:文件描述符,我们之后的文件在进程中都是以文件描述符来进行标识,将我们要写入的文件描述符带入其中。
  • 第二个参数:要写入文件中的数据,在一个缓冲区中存储。
  • 第三个参数:写入数据的大小。
  • 本质是拷贝函数

open函数在干嘛??

  1. 创建文件file;
  2. 开辟文件缓冲区的空间,加载文件数据;
  3. 查看进程的文件缓冲区表;
  4. 将file地址填入文件缓冲区表的下标中;
  5. 返回下标。

read函数的原型:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

read函数的参数部分:

  • 第一个参数:fd是文件描述符,指明了我们从哪一个文件进行读取数据。
  • 第二个参数:buf是接收数据的缓冲区地址,我们将读取的数据放在缓冲区中。
  • 第三个参数:count表示期望读取的字节数。
  • 本质是拷贝函数

read函数的返回值:返回实际读取到的字节数

总结:

      fd是文件描述符,buf是接收数据的缓冲区地址,count表示期望读取的字节数。read函数会从指定的文件中读取count个字节到buf中,并返回实际读取到的字节数。在读取过程中,文件指针会根据读取的字节数偏移。

close函数的原型:

#include <unistd.h>
 
int close(int fd);

close函数的参数部分:

  • 参数fd是要关闭文件描述符。当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。

由open返回的文件描述符一定是该进程尚未使用的最小的描述符。

三、文件描述符

      在上述函数的使用中,我会发现文件描述符其实是一个小整数。

3.1 0 && 1 &&& 2

  • 这三个数就是C语言互默认打开的三个输入输出流:stdin、stdout、stderr。
  • 在Linux系统中,我们会默认打开这三个描述符,分别是标准输入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));
 if(s > 0){
 buf[s] = 0;
 write(1, buf, strlen(buf));
 write(2, buf, strlen(buf));
 }
 return 0;
}

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

总结:文件描述符的本质就是:就是在文件结构体中的数组的下标。

3.2 文件描述符的分配规则

#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数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

四、如何理解硬件在系统中是以文件的形式存在?

      在上述过程中,我们知道了文件描述符0, 1和2对应的是键盘,显示器和显示器。那么我们应该怎么进行理解这个硬件和文件描述符进行关联起来的?Linux中一切皆文件!!!!

      首先,在iOS设备上建立一层驱动层,我们只需要将不同的设备建立不同的驱动层,用函数指针区调用函数的使用,在驱动层上面,使用文件将每一个设备的属性和方法建立在文件中。我们可以使用函数指针来进行调用函数。因此,将硬件设备和文件练习起来。

我们可以在原码进行验证:

五、进行进一步理解上面的东西(打通)

5.1 写入文件的操作进行串联一遍

      进程在打开文件时,进程会打开文件描述符表。文件描述表会指向一个文件,文件中有文件缓冲区和方法列表,我们使用write函数指定文件描述符和所要写入的数据,将数据写入文件缓冲区中,经过刷新将会刷新到磁盘中。

5.2 如何理解C语言通过FILE*访问文件呢??

      在系统中,系统只认文件描述符。但是,我们在使用C语言进行文件操作时,只使用FILE*进行文件操作。fopen、fclose、fread等函数是库函数, 而标准输入、标准输出和标准错误都是以FILE*为类型的。

      FILE 是一个C语言提供的结构体类型,在结构体中的属性有fd。fd = (FILE*)fp->_fileno;C语言的接口在底层实现是系统调用。

C语言为什么要这样做?

      我们以后可以使用系统调用,也可以使用语言提供的文件方法,但是不推荐使用系统调用,在系统不同的情况下,系统调用的接口是不同的,代码不具有跨平台性,但是所有语言的代码是具有跨平台性的,所有语言要对不同的平台的系统调用进行封装,但是函数接口就有区别了。

相关文章
|
1月前
|
Unix Linux Shell
【探索Linux】P.12(文件描述符 | 重定向 | 基础IO)
【探索Linux】P.12(文件描述符 | 重定向 | 基础IO)
22 0
|
1月前
|
存储 Linux 开发工具
【Linux】基础 IO(文件描述符)-- 详解(下)
【Linux】基础 IO(文件描述符)-- 详解(下)
|
1月前
|
存储 Linux C语言
【Linux】基础 IO(文件描述符)-- 详解(上)
【Linux】基础 IO(文件描述符)-- 详解(上)
|
1月前
|
Linux 程序员 C语言
【linux基础I/O(一)】文件描述符的本质&重定向的本质
【linux基础I/O(一)】文件描述符的本质&重定向的本质
|
1月前
|
安全 Linux Shell
为什么在 linux system service 启动服务,最大文件描述符变成了默认的 4096
修改系统或用户文件描述符限制可能未生效,需确保执行系统重启、systemd 重启或服务重启以加载新配置。注意服务运行账户的权限和配置文件中的限制,检查服务 unit 文件是否覆盖默认限制。临时 `ulimit` 调整不适用于服务启动,应修改配置文件。还要确认内核版本和配置是否允许更高的限制。
30 0
|
1月前
|
Unix Linux 索引
Linux 基础解惑:Linux 下文件描述符标志和文件描述符状态标志,文件状态标志,文件状态之间的区别
Linux 基础解惑:Linux 下文件描述符标志和文件描述符状态标志,文件状态标志,文件状态之间的区别
80 0
|
1月前
|
存储 Unix Linux
Linux文件描述符和打开文件之间的关系
文件描述符和打开的文件之间似乎呈现出一一对应的关系。然而,实际并非如此。多个文件描述符指向同一打开文件,这既有可能,也属必要。这些文件描述符可在相同或不同的进程中打开。要理解具体情况如何,需要查看由内核维护的 3 个数据结构。进程级的文件描述符表。系统级的打开文件表。文件系统的 i-node 表。上述讨论揭示出如下要点。两个不同的文件描述符,若指向同一打开文件句柄,将共享同一文件偏移量。
25 0
Linux文件描述符和打开文件之间的关系
|
1月前
|
Unix Linux API
Linux文件描述符和文件指针互转
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。FILE *指针变量标识符;其中FILE应为大写,它实际上是由系统定义的一个结构,该结构中含有文件名、文件状态和文件当前位置等信息。在编写源程序时不必关心FILE结构的细节。
33 0
|
6月前
|
Linux
linux文件描述符fd
linux文件描述符fd
32 0
|
10月前
|
监控 Linux
Linux显示文件描述符命令:fd
Linux显示文件描述符命令:fd
340 0
Linux显示文件描述符命令:fd