linux文件与文件描述符的介绍(代码片段)

蓝乐 蓝乐     2022-12-11     477

关键词:

基础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中的进程其实就是一个数据结构,顺带可以理解文件描述符、重定向、管道命令的底层工作原理,最后我们从操作系统的角度看看为什么说线程和进程基本没有区别。一、进程是什么首先,抽象地来说,我们的计算机就... 查看详情

linux--io(代码片段)

目录一.C语言当中文件操作接口(文件输入/输出)1.1.fopen()1.2fread()1.3fwrite()二.系统调用文件接口2.1open()2.2read()2.3write()2.4lseek()2.5close()2.6代码三.文件描述符3.1文件描述符的本质3.2文件描述符的分配方式3.3文件描述符和文件... 查看详情

linux基础io:文件描述符,文件流指针,重定向(代码片段)

...润菜菜📖专栏:Linux系统编程这是目录重新认识文件系统内部的文件操作我们C语言的文件操作fopen,fread,fwrite,fseek,fclose等函数的使用系统内部的文件操作OS一般会如何让用户给自己传递标志位的?多个标志位怎么实现... 查看详情

linux----io(初级)(代码片段)

Linux----IO(初级)1)C文件I/O2)系统文件I/O①Linux一切皆文件②IO系统接口③站在系统角度理解④filedescriptor(文件与进程)⑤分配文件描述符的规则⑥C中的FILE⑦重定向3)文件系统4)动态库和静态... 查看详情

linux中进程间传递文件描述符的方法(代码片段)

...于子进程会拷贝父进程的资源,所以父进程中打开的文件描述符在子进程中仍然保持着打开,我们很容易的就将父进程的描述符传递给了子进程。但是除了这种情况下,如果想将某个父进程在子进程创建后才打开的描... 查看详情

文件描述符的复制(代码片段)

文件描述符的复制dup和dup2是两个非常有用的系统调用,都是用来复制一个文件的描述符,使新的文件描述符也标识旧的文件描述符所标识的文件。intdup(intoldfd);intdup2(intoldfd,intnewfd);dup和dup2经常用来重定向进程的stdin、stdou... 查看详情

[os-linux]详解linux的基础io-------文件描述符fd(代码片段)

 本文由文件IO相关操作的一些操作,进一步详解了文件描述符fd,重定向,FILE结构体。目录一、C语言中的文件I/O操作二、系统文件I/O1.接口介绍2.open函数返回值三、文件描述符fd四、文件描述符的分配规则六、dup2系... 查看详情

[os-linux]详解linux的基础io-------文件描述符fd(代码片段)

 本文由文件IO相关操作的一些操作,进一步详解了文件描述符fd,重定向,FILE结构体。目录一、C语言中的文件I/O操作二、系统文件I/O1.接口介绍2.open函数返回值三、文件描述符fd四、文件描述符的分配规则六、dup2系... 查看详情

文件描述符的复制——实现输出重定向(代码片段)

...  dup2(2)#include<unistd.h>intdup(intoldfd);功能:复制文件描述符参数:oldfd:指定源文件的描述符返回值:错误-1errno被设置成功返回新的文件描述符新的文件描述符使用未使用的、最小的文件描述符 intdup2(intoldfd,intnewfd);... 查看详情

在进程间传递文件描述符(代码片段)

  由于fork调用之后,父进程中打开的文件描述符在子进程中仍然保持打开,所以文件描述符可以很方便地从父进程传递到子进程。需要注意的是,传递一个文件描述符并不是传递一个文件描述符的值,而是要在接收进程中创... 查看详情

linux下的基础io(代码片段)

目录一.C语言文件IO操作(库函数)1.打开文件fopen 2.写入二进制文件fwrite和读取二进制文件fread3.关闭文件fclose 二.系统文件IO(系统调用接口)1.打开文件open系统调用2.读文件read和写文件write3.关闭文件close  三.文件描述符1.系统管理... 查看详情

《linux从0到99》九基础io(代码片段)

基础IO1.回顾c语言文件操作接口2.系统调用文件操作系统01open函数02read函数03write函数04lseek函数05close函数3.文件描述符0&1&2文件描述符的分配规则(最小未分配原则)4.文件描述符与文件流指针的区别01文件流指针的本... 查看详情

[linux]基础io(代码片段)

文件想大家都不陌生吧,计算机中所有的数据都存在文件中,但是前面是一个普通人对文件的理解,那么现在我就带你看看程序员眼中的文件文章目录1语言层面上的文件(C语言)1.2C语言接口介绍1.2系统接口介绍1.3三个... 查看详情

linux基础io(代码片段)

文章目录C语言文件IOC语言文件接口汇总什么是当前路径?默认打开的三个流系统文件I/Oopenopen的第一个参数open的第二个参数open的第三个参数open的返回值closewriteread文件描述符fd文件描述符的分配规则重定向重定向的原理dup2... 查看详情

系统文件io操作与文件描述符(代码片段)

系统文件IO操作与文件描述符IO操作语言级IO操作系统级IO操作openclosewriteread文件描述符文件描述符的分配规则IO操作语言级IO操作在C语言中,我们首先需要打开一个文件,使用的函数就是fopen,fopen可以指定以何种方式... 查看详情

linux入门基础io(代码片段)

基础IO✔回顾C文件的接口✔系统文件I/O✔文件描述符文件描述符的分配规则重定向✔FILE缓冲区fclose和close✔dup2系统调用✔理解文件系统inode硬链接软链接文件的三个时间✔回顾C文件的接口在学习C语言时我们了解了一些C语言的对... 查看详情

linux入门基础io(代码片段)

基础IO✔回顾C文件的接口✔系统文件I/O✔文件描述符文件描述符的分配规则重定向✔FILE缓冲区fclose和close✔dup2系统调用✔理解文件系统inode硬链接软链接文件的三个时间✔回顾C文件的接口在学习C语言时我们了解了一些C语言的对... 查看详情

io多路复用之poll(代码片段)

...行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的... 查看详情