Linux文件与文件描述符的介绍

Posted 蓝乐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux文件与文件描述符的介绍相关的知识,希望对你有一定的参考价值。

基础IO(上)

本文将介绍Linux系统下的文件操作并从底层了解文件的相关知识。

前言

在开始介绍之前,我们先带着下面这几个问题去思考:

  1. 如何理解“Linux下一切皆文件”
  2. 进程启动同时会默认打开3个文件,这3个文件是什么
  3. 什么是文件描述符,为什么说有了文件描述符,就可以找到打开文件的所有细节
  4. 从语言和系统层面分别理解文件描述符fd与FILE的关系
    那么接下来我们将开始介绍文件及文件描述符

C语言文件IO相关

操作接口回顾

       #include <stdio.h>

       FILE *fopen(const char *path, const char *mode);
       int fclose(FILE *fp);//成功返回0,否则返回EOF并报错
       size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);//将nmemb个size大小的成员从stream中读到ptr所指向的内存中,调用成功返回成功读取的个数,调用失败返回0
       size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);//将nmemb个size大小的成员从ptr所指向的内存写入到stream中,调用成功返回被成功写入的成员个数,调用失败返回0
       int fprintf(FILE *stream, const char *format, ...);//用法与printf类似,只不过是从stream所指向的文件中读取信息打印

写文件:

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

int main()

  FILE* fp = fopen("log.txt", "w");

  if(fp == NULL)
  
    perror("fopen");
    return 1;
  

  const char* msg = "never give up\\n";
  //将msg长度个1字节的信息从msg写入到fp所指向的文件中
  fwrite(msg, 1, strlen(msg), fp);
  fclose(fp);
  return 0;


读文件:

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

int main()

  FILE* fp = fopen("log.txt", "r");
  if(fp == NULL)
  
    perror("fopen");
    return 1;
  
  const char* msg = "never give up\\n";
  char buf[1024];
  //将msg长度个1字节的内容从fp指向的文件中读取到buf数组
  size_t s = fread(buf, 1, strlen(msg), fp);
  //返回s为成功读取的个数
  buf[s] = '\\0';//在下标s处标记\\0,表示字符串末尾
  printf("%s\\n", buf);
  fclose(fp);
  return 0;


三个默认打开的文件

在介绍接下来的内容之前,首先说明:任何C语言程序,都会默认打开3个文件,即:标准输入(stdin),标准输出(stdout)以及标准错误(stderr),分别对应着键盘文件,显示器文件,显示器文件。
在这之前,我们插入一个话题。这里开始引入“Linux下一切皆文件”的概念了。
首先,我们知道键盘和显示器都是硬件,为什么要在其后加上文件二字呢?这就要说到所有的外设无外乎读和写操作。
不同硬件的读写操作一般是不同的,由于Linux底层是由C语言写的,因此在系统底层,不同文件对应着不同的结构体,C语言是如何在结构体内定义方法的呢?那就是通过函数指针来解决。
对于这么多的文件,操作系统(OS)要不要管理呢?当然,那么管理的六字箴言:先描述,在组织,就如下图所示一般:

由此,我们就可以直接通过C语言接口,对stdin,stdout,stderr进行读写了:

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

int main()

  const char* msg = "Linux so easy!\\n";
  //往显示器上写入msg的内容
  fwrite(msg, 1, strlen(msg), stdout);
  char buf[1024];
  //从键盘读取10个字节的内容到buf数组中
  size_t s = fread(buf, 1, 10, stdin);
  buf[s] = '\\0';//字符串以\\0结束
  printf("%s\\n", buf);
  //由于stderr也是显示器文件,因此效果与往stdout上打印一样
  fprintf(stderr, "never give up\\n");
  return 0;

回顾:打开文件的方式

文件使用方式含义如果指定文件不存在
“r”(只读)为了输入数据,打开一个已经存在的文本文件出错
“w”(只写)为了输出数据,打开一个文本文件建立一个新的文件
“a”(追加)向文本文件尾添加数据建立一个新的文件
“r+”(读写)为了读和写,打开一个文本文件出错
“w+”(读写)为了读和写,建议一个新的文件建立一个新的文件
“a+”(读写)打开一个文件,在文件尾进行读写建立一个新的文件

系统文件IO相关

操作接口

其实除了上述C语言接口,系统也提供了文件相关的调用接口。

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接口的第一个参数与fopen一样都是文件名
第二个参数为打开的方式,选项如下:
O_RDONLY:只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_APPEND:追加
O_CREAT:创建
若选项有多个则可以用|号进行“或”运算。
第三个参数mode可以联系之前介绍的chmod来理解,就是三种访问者的访问权限。
返回值:
·成功:新打开的文件描述符
·失败:-1

close

       #include <unistd.h>

       int close(int fd);

与fclose类似,只不过fclose传入的参数为open成功调用的返回值fd。
返回值:成功调用返回0,否则返回-1并报错。

write

       #include <unistd.h>

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

与fwrite类似,将buf指针指向的内容写入fd所标识的文件,写入count个字节
返回为成功写入的字节。

read

       #include <unistd.h>

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

read将fd所标识的文件的内容读取到buf指针所指向的内存中,读取count个字节,返回值为成功读取的字节。

代码调用

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

int main()

  //打开文件,以只写的方式,若文件不存在则创建之,创建的权限为664
  int fd = open("block.txt", O_WRONLY | O_CREAT, 0664);
  if(fd == -1)
  
    perror("open");
    return 1;
  
  const char* msg = "never give you up.\\n";
  write(fd, msg, strlen(msg));
  close(fd);
  fd = open("block.txt", O_RDONLY);
  if(fd == -1)
  
    perror("open");
    return 1;
  
  char buf[64];
  ssize_t s = read(fd, buf, strlen(msg));
  if(s > 0)
  
    buf[s] = '\\0';
  
  printf("%s\\n",buf);
  close(fd);
  return 0;


文件描述符fd

我们先来看下面这段代码:

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

int main()

  int fd = open("test.txt", O_WRONLY|O_CREAT, 0644);
  printf("fd : %d\\n", fd);
  close(fd);
  return 0;



可以看到运行结果为3,那么问题来了:为什么这个创建的文件描述符是3,而不是0,1,2呢?这就和前面所介绍的3个文件联系起来了,前面说过C语言程序中,会默认打开标准输入、标准输出及标准错误三个文件,而其对应的文件描述符就是0、1、2。
默认打开的三个文件并非C语言独有,应该说是所有语言在创建进程时都会打开的。

文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体,表示一个已经打开的文件对象。而在进程控制块(PCB)中有一个指针指向存放file*的数组,数组中的每个成员都指向一块file结构体。
而文件描述符本质上就是fd_array数组的下标,由fd则可以找到对应的file结构体。

分配规则

文件描述符fd与系统的对应关系

我们已经知道创建一个新的文件,为其分配的文件描述符是从3开始的,那么我们再执行如下代码:

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

int main()

  close(0);
  int fd = open("log.txt", O_WRONLY|O_CREAT, 0644);
  printf("log.txt fd:%d\\n", fd);
  close(fd);
  return 0;


可以看到结果为0,那么此时我们得出结论,fd的分配规则为第一个最小的未使用的fd下标。

初步理解重定向

我们再试试关闭fd为1的文件:

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

int main()

  close(1);
  int fd = open("log.txt", O_WRONLY|O_CREAT, 0644);
  const char* msg = "never give up\\n";
  write(fd, msg, strlen(msg));
  printf("fd:%d\\n", fd);
  close(fd);
  return 0;


可以看到调用printf后屏幕上并没有打印出我们想要的内容,这是因为1号fd标识的为标准输出,也就是说本来我们往显示器上打印的内容此时写入了修改后的1号文件log.txt。但此时我们打开log.txt文件,发现写入了msg的内容,却没有调用printf后的内容。

此时我们修改代码,在原printf函数后加上

fflush(stdout);

此时运行程序后再打开log.txt文件:

发现printf打印的内容出现再log.txt文件中了。
而原本要写入标准输出文件的内容却写入到log.txt文件中,这种现象就叫做重定向,这便是我们初步认识输出重定向。

深入理解文件描述符与FILE

其实在C语言底层代码中FILE是一个结构体,而在这个结构体中有:

int _fileno; //封装的文件描述符

也就是说,C语言将文件描述符进行了二次封装,实际上C语言也是通过文件描述符来操作文件的。为什么C语言要这么做呢?这是因为不同系统下的文件管理不一定相同,这次介绍的Linux是如此,但到了windows下又不一定了,因此语言层需要对此进行封装,保证平台的可移植性。

理解数据在文件层面的流动过程

数据在写入文件之前会先写到语言层的缓冲区中,当遇到\\n或者通过fflush函数接口强制刷新时,才会写入系统层的文件中。

缓冲区的理解

在上面理解输出重定向时,我们明明在printf函数中加入了\\n换行,为什么一开始没有写入到文件中呢?这是因为对于缓冲区而言是行缓冲,也就是说如果遇到\\n或则fflush强制刷新,就会清空缓存区,将数据写入文件中;而文件的缓冲是全缓冲,只有写满文件才会清空缓冲区。
而我们在一开始关闭1号fd时只是将数组中指针的链接关系改变了,并未关闭stdout标准输出文件,也就是说调用printf函数仍是将数据写入了语言层的缓冲区,但是由于系统已经知晓fd所指向的为普通文件,因此缓冲规则却是执行的全缓冲,因此只是靠\\n并不会清空语言层的缓冲区,而需要通过fflush强制刷新缓冲区才能够将数据写入log.txt文件中。

小结

回顾一开始所提出的问题,我们现在就有了更深层次的理解。
1.“Linux下一切皆文件”:系统对于硬件进行读写方法的封装,保证了可以通过系统IO接口调用进行操作,因此Linux下一切皆文件
2. 进程启动同时会默认打开3个文件,这3个文件是:标准输入、标准输出及标准错误
3. 什么是文件描述符,为什么说有了文件描述符,就可以找到打开文件的所有细节:文件描述符是文件指针数组的下标,数组中的指针指向了描述文件的file结构体
4. 从语言和系统层面分别理解文件描述符fd与FILE的关系:语言层面,FILE对fd进行了封装,保证可移植性;系统层面,fd可以管理上层语言接口的文件操作。

以上是关于Linux文件与文件描述符的介绍的主要内容,如果未能解决你的问题,请参考以下文章

[Linux] 基础 IO

[Linux] 基础 IO

[Linux] 基础 IO

Linux基础优化与安全小结

Linux详解 --- 系统文件IO操作与文件描述符

「图文结合」Linux 进程、线程、文件描述符的底层原理