文章目录

    • 0. 前言
    • 1. C文件接口
      • 文件打开
      • 文件写入
    • 2. 系统文件接口
      • open && write && close
      • open的返回值

本章gitee代码仓库:文件描述符

0. 前言

基础原理知识:

  1. 文件 = 内容 + 属性

  2. 文件分为:打开的文件(本章重点讲解)和没打开文件

    打开的文件,本质上是进程将其打开

    没打开的文件,存储在磁盘

  3. 要访问这个文件,根据冯诺依曼体系结构,这个文件必须先加载到内存当中,文件又是内容+属性,那么第一步肯定是要先将文件的属性先加载进来,然后根据要不要对这个文件进行读取修改,从而决定是否将内容加载。

  4. 一个进程可以打开多个文件,操作系统内部一定是存在者大量的被打开的文件,操作系统要对这些文件进行管理,肯定也是先描述,再组织。在内核当中,一个被打开的文件,都必须有自己的打开对象,这里面包含了文件的很多属性。而Linux内核是用C语言写的,所以描述这个文件,用的是struct结构体:

    struct xxx{文件属性;struct xxx *next;}

    这样对文件的管理就变成了对链表的增删查改。

1. C文件接口

文件打开

打开文件的接口fopen,头文件也是stdio.h

#include#includeint main(){printf("pid:%d\n",getpid());FILE *fp = fopen("log.txt","w");if(fp == NULL){perror("fopen");return 1;}fclose(fp);sleep(1000);return 0;}

如果不指定文件路径,则默认在当前工作目录创建或者访问这个文件

这个工作目录就是该进程的工作目录,指令ll /proc/进程pid,可查看进程的工作目录,cwdcurrent working directory

如果我们修改当前进程的工作目录,则这个创建的文件,则会创建到修改的工作目录里面

chdir("/home/Pyh")

文件写入

这里采用fwrite作为演示

const char *str = "hello linux\n";fwrite(str,strlen(str),1,fp);

这里的strlen(str),不需要再加上1('\0'),因为字符串末尾加'\0'是C语言的规定,而这个和文件并没有关系,文件只关心这个内容

我们这里发现,这里写入的时候,都会将文件的内容进行清空然后再写入,而>这个重定向符号,也是会将文件进行清空再写入。

这本质上就因为"w"操作,会将原始文件的内容进行清空再写入,所以>打开文件的方式肯定是"w" 方式。

如果想要对文件不进行清空,那么可以采用"a"操作进行打开a就就是append追加,而追加重定向>>,就是以"a"方式打开文件。

C语言程序在启动的时候,会默认打开三个输入输出流(文件):

  • stdin:标准输入,键盘文件
  • stdout:标准输出,显示器文件
  • stderr:标准错误,显示器文件

那么我们可以直接向这些文件里面写入,例如:

fwrite(str,strlen(str),1,stdout);

2. 系统文件接口

文件是存储在磁盘上的,而磁盘是外设,所以访问文件就是访问硬件。而硬件是在操作系统之下,是被操作系统管理的,而我们的普通用户,是没有资格去直接访问硬件的,所以需要通过操作系统提供的接口来访问。而这就是系统调用,C语言的库函数,类似printffprintffscanffwrite这基本上都封装了系统调用。

open && write && close

在系统调用中,我们可以采用open接口打开文件

int main(){umask(0);//修改权限掩码int fd = open("log.txt",O_WRONLY|O_CREAT,0666);//读取文件|创建,0666设置权限if(fd < 0)//创建失败返回-1{perror("open file error\n");return 1;}return 0;}

因为涉及到创建文件,所以权限是必须要告诉系统的,不然会出现乱码。

这里我们还修改了权限掩码,具体知识,之前在此篇文章讲到过,有兴趣可查看——Linux权限

  • O_RONLY:只读打开
  • O_WRONLY:只写打开
  • O_RDWR:读写打开

上面这三个必须指定一个且只能指定一个

  • O_CREAT:若文件不存在则创建,但必须要使用mode选项,指定文件访问的权限
  • O_APPEND:追加写入
  • O_TRUNC:清空文件内容

有了这些组合,我们就能推断出C语言库函数中的这些"w""a"封装的是哪些

#include#include#include#include#include#includeint main(){umask(0);//int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);if(fd < 0){perror("open file error\n");return 1;}const char *str = "abc";write(fd,str,strlen(str));close(fd);return 0;} 

open的返回值

在操作系统内部描述一个被打开的文件,会直接或者间接包含这些信息:

  • 在磁盘的位置
  • 文件的基本属性
  • 文件的内核缓冲区信息
  • 文件结构体指针struct file *next

在进程的task_struct结构体中,里面文件结构体指针struct files_struct *files,它指向了一个文件描述符表struct files_struct,这个表里面包含一个指针数组struct file *fd_array[],它里面存放的就是struct file *

当一个文件打开的时候,系统会创建一个文件对象,进程指向的文件描述表里面,就会给它分配一个没有占用的下标来指向这个对象。

所以这个open的返回值,其实就是返回这个数组的下标。

#include#include#include#include#includeint main(){umask(0);//int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);int fd1 = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);int fd2 = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);int fd3 = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);int fd4 = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);if(fd1 < 0){perror("open file error\n");return 1;}const char *str = "abc";write(fd1,str,strlen(str));printf("fd1:%d\n",fd1);printf("fd2:%d\n",fd2);printf("fd3:%d\n",fd3);printf("fd4:%d\n",fd4);close(fd1);return 0;}

我们发现这个open返回的就是这个文件描述符对应数组的下标

但是这里是从3开始的,这就是以为C语言程序会默认打开三个输入输出流文件,这里我们也可以验证一下

C语言FILE这个结构体就包含了这个文件描述符

printf("stdin->%d\n",stdin->_fileno);printf("stdout->%d\n",stdout->_fileno);printf("stderr->%d\n",stderr->_fileno);

我们发现,这三个输入输出流的文件描述符分别是0、1、2

这里程序启动打开三个输入输出流,并不单是C语言的特性,而是操作系统的特性

因为在计算机开机的时候,我们的屏幕和键盘就被默认打开了,所以我们启动进程的时候,只需要将它们的文件对象地址填到进程的pcb里面

我们再来看一个现象:

close(1);int n = printf("hello linux\n");fprintf(stderr,"printf %d\n",n);

我们这里已经关闭的了标准输出,可是这里显示器上还是输出了信息,这是因为每个文件结构体对象中都包含了一个count引用计数,标准输出和标准错误都是指向的显示器文件,这里的计数就是2,所以我们close(1)其实是将该文件计数减一,然后再将1号位置置空,如果这个这个count计数不为零,则不管它,如果为零了,则系统回收这个文件对象。