【Linux取经路】基础I/O之被打开的文件——文件描述符的引入 ...

打印 上一主题 下一主题

主题 537|帖子 537|积分 1611



  
一、明白基本共识



  • 文件等于内容加属性,内容和和属性都是数据,不管是内容还是属性都要在磁盘中保存。
  • 文件分为打开的文件和没打开的文件。
  • 打开的文件本质是进程打开的,要研究打开的文件,本质是研究进程和文件的关系。
  • 对文件的所有操纵(打开文件、读取文件、向文件写入)等,都是通过代码来实现的,而代码最终是由 CPU 去执行的,根据冯诺依曼结构体系,CPU 不能直接和外设打交道,因此一个被打开的文件,第一步肯定是先将其加载到内存。
  • 一个进程能够打开多个文件,以是在操纵系统内部肯定存在大量的被打开的文件,操纵系统还是通过先描述,再构造的方式对打开的文件举行管理。每个被打开的文件都必须有本身的文件打开对象,其中一肯定包含了文件的很多属性,将这些文件对象以某种特殊的数据结构构造起来,最终对文件的管理,就变成了对某种数据结构的维护(增删查改)。
  • 没打开的文件一般都是在磁盘上放着,对于没打开的文件,由于没打开的文件非常多,以是对于没打开的文件我们最关心文件如何被分门别类的放置好,分门别类的放置好是为了快速的举行增删查改。
二、C语言文件接口回顾

2.1 文件的打开操纵

  1. // 文件打开接口
  2. FILE *fopen(const char *path, const char *mode);
复制代码
第一个参数 path ,表示要打开的文件路径,或者文件名。如果只有文件名前面没写路径,表示打开当前路径下的文件。这里又涉及到当前路径,在前一篇文章中实现 cd 指令的时候就讲过什么是当前路径。总的来说,当前工作路径是一个进程 PCB 中维护的一个属性。一个可执行步伐在被加载到内存成为进程创建出对应的 PCB 对象的时候,PCB 对象中就维护了一个叫做 cwd 的属性,该属性就表示进程当前的工作路径。

如果 fopen 函数的第一个参数只通报了文件名,最终在打开文件的时候,操纵系统会去 cwd 指向的工作路径下查找该文件。
第二个参数 mode,这个参数有很多可选项,本日只先容个别选项,关于所有选项的详细先容请看我之前的文章【C语言进阶】文件操纵。


  • w选项:只要是以 w 选项打开的文件,在写入之前都会对文件做清空处置惩罚,然后从头开始写入。
  • a选项:在文件结尾举行追加写。
小Tips:我们之前先容的重定向,> 本质上就对应利用的是 w 选项,>> 本质上就对应利用的是 a 选项。
2.2 文件的读取写入操纵

和文件读取写入的干系接口,以及利用方法,本日也不外多先容,详细先容请看我之前写的文章【C语言进阶】文件操纵。本日只想通过 fwrite 接口跟各人明白一件事情。
  1. // fwrite 接口声明
  2. size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
复制代码
  1. int main()
  2. {
  3.     FILE *fp = fopen("log.txt", "w");
  4.     if (fp == NULL)
  5.     {
  6.         // 打开失败
  7.         perror("fopen");
  8.         return errno;
  9.     }
  10.     // 打开成功,对文件进行相关的操作
  11.     // ...
  12.     char* str = "Hello Linux!";
  13.     fwrite(str, strlen(str), 1, fp);
  14.     // 操作结束,关闭文件
  15.     fclose(fp);
  16.     return 0;
  17. }
复制代码
fwrite 接口的第二个参数 size 表示每一个要写入的对象的大小。在向文件写入字符串的时候,该参数是字符串的长度还是字符串的长度加一呢?因为 strlen 计算出来的字符串长度是不包含结尾的 \0,加一的小伙伴以为要把 \0 也写到文件里面,但是 \0 真的需要写入文件嘛?其实 \0 并不需要写入文件中,因为字符串以 \0 结尾只是 C 语言这么规定的,我们把一个字符串写入文件后,可能通过其它的语言去读取该文件,我们并不希望读到与该字符串无关的内容 \0。下面是加一的结果:

\0 也是字符,只不外不可显,在被写入到文件后,vim 编辑器会把它识别成 ^@,对 Hello Linux 来说,^@ 就是多余的无用字符。我们不希望它在文件中出现。
2.3 三个尺度输入输出流

C步伐在启动时候,默认会打开三个尺度流文件:


  • stdin:尺度输入流——键盘文件
  • stdout:尺度输出流——显示器文件
  • stderr:尺度错误流——显示器文件
三、文件有关的系统调用

文件最初是在磁盘上的,磁盘是外部装备,访问磁盘文件其实是访问硬件,在计算机层状结构中,硬件是处于最底层的,操纵系统帮我们把这些硬件管理起来,并且操纵系统是不相信用户的,因此操纵系统不允许我们直接去访问硬件,而是给我们提供了系统调用接口,几乎所有的库只要是访问硬件装备,必定要封装系统调用。也就是说我们平常在C语言里面利用的 fopen、printf、fprintf、fscanf等函数都肯定是封装了系统调用。
3.1 open

  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. int open(const char *pathname, int flags);
  5. int open(const char *pathname, int flags, mode_t mode);
复制代码


  • 第一参数 pathname:表示要打开或创建的目标文件
  • 第二个参数 flags:标志位选项。O_RDPPNLY:只读打开;O_WRONLY:只写打开;O_RDWR:读,写打开。这三个常量,必须指定一个且只能指定一个。O_CREAT:若文件不存在,则创建它。需要利用 mode 参数,来指明新文件的访问权限。O_APPEND:追加写;O_TRUNC:文件打开的时候先清空。
  • 第三个参数 mode:新创建文件的默认权限,要考虑权限掩码,可以配合 umask 系统调用接口来设置本身想要的结果。umask 系统调用产生的结果就只对当前进程创建的文件有关。
  • 返回值:成功,返回新打开的文件描述符,关于文件描述符是什么,将在后文为各人先容;失败,返回-1。
小Tips:open 函数具体利用哪个,和具体的应用场景有关,如目标文件不存在,需要 open 创建,则第三个参数表示创建文件的默认权限。如果不需要创建新文件,利用两个参数的 open。
3.1.1 比特位级别的标志位通报方式

  1. #define ONE (1<<0) // 1
  2. #define TWO (1<<1) // 2
  3. #define FOU (1<<2) // 4
  4. #define EIG (1<<3) // 8
  5. void show(int flags)
  6. {
  7.     if(flags & ONE) printf("function1\n");
  8.     if(flags & TWO) printf("function2\n");
  9.     if(flags & FOU) printf("function3\n");
  10.     if(flags & EIG) printf("function4\n");
  11.     return;
  12. }
  13. int main()
  14. {
  15.     printf("--------------------------------------\n");
  16.     show(ONE);
  17.     printf("--------------------------------------\n");
  18.     show(ONE | TWO);
  19.     printf("--------------------------------------\n");
  20.     show(ONE | TWO | FOU );
  21.     printf("--------------------------------------\n");
  22.     show(ONE | TWO | FOU | EIG);
  23.     printf("--------------------------------------\n");
  24.     return 0;
  25. }
复制代码

小Tips:这种比特位级别的标志位通报方式,利用户可以在函数调用的时候采用按位或的方式通报多个选项实现差别的功能。open 函数的第二个参数就是采用这种方式就是如许。
3.2 write

  1. #include <unistd.h>
  2. ssize_t write(int fd, const void *buf, size_t count);
复制代码


  • 第一个参数 fd:表示待写入文件的文件描述符。
  • 第二个参数 buf:指向待写入的文件内容。
  • 第三个参数 count:待写入内容的大小,单元是字节。
  • 返回值:实际上写入的字节数。
3.2.1 模拟实现 w 选项

  1. int main()
  2. {
  3.     umask(0); // 将权限掩码设置成全0
  4.     int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // 以读的方式打开,若文件不存在就创建,打开文件时清空
  5.     if(fd < 0)
  6.     {
  7.         printf("open file\n");
  8.         return errno;
  9.     }
  10.     const char* str = "aaa";
  11.     ssize_t ret = write(fd, str, strlen(str));
  12.     close(fd);
  13.     return 0;
  14. }
复制代码

3.2.2 模拟实现 a 选项

  1. int main()
  2. {
  3.     umask(0); // 将权限掩码设置成全0
  4.     int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 以读的方式打开,若文件不存在就创建,以追加的方式进行写入
  5.     if(fd < 0)
  6.     {
  7.         printf("open file\n");
  8.         return errno;
  9.     }
  10.     const char* str = "aaa";
  11.     ssize_t ret = write(fd, str, strlen(str));
  12.     close(fd);
  13.     return 0;
  14. }
复制代码

3.3 read

  1. #include <unistd.h>
  2. ssize_t read(int fd, void *buf, size_t count);
复制代码


  • 第一个参数 fd:要读取文件的文件描述符。
  • 第二个参数 buf:指向一段空间,该空间用来存储读取到的内容。
  • 第三个参数 count:参数二指向空间的大小。
四、访问文件的本质


总结:一个被打开的文件,加载到内存,会为该文件创建一个 struct file 结构体对象,操纵系统对文件的管理本质上就是对 struct file 结构体对象的管理,操纵系统会将当前所有被打开文件的 struct file 对象以双链表的情势构造起来。进程的 PCB 对象中有一个 struct files_struct 类型的指针,指向该类型的一个对象,该类型对象里面记载了当前进程所打开的所有文件新信息,其中中维护了一个 struct file* 类型的数组,数组的内容就指向了当前进程所打开的文件结构体对象,简言之就是指向了当前进程打开的文件。我们将这个数组就叫做文件描述符表,数组的下标就叫做文件描述符(因此文件描述符肯定大于0)。open 函数的返回值其实就是文件描述符,即只要当前进程打开一个新文件,操纵系统就会按照从前去后的顺序从该进程的文件描述符表中分配一个数组下标,该下标对应的内存空间中存储的就是该文件结构的地址。今后要对该文件举行任何操纵,只需要知道它对应的数组下标即可。
  1. int main()
  2. {
  3.     umask(0); // 将权限掩码设置成全0
  4.     int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 以读的方式打开,若文件不存在就创建,以追加的方式进行写入
  5.     int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
  6.     int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
  7.     int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
  8.     printf("fd1: %d\n", fd1);
  9.     printf("fd2: %d\n", fd2);
  10.     printf("fd3: %d\n", fd3);
  11.     printf("fd4: %d\n", fd4);
  12.     return 0;
  13. }
复制代码

小Tips:通过结果可以看出,进程新打开的文件,其下标只能从3,开始,这是因为 C 步伐在运行起来的时候操纵系统会默认帮我们打开三个流,尺度输入流 stdin 对应键盘文件,下标为0;尺度输出流 stdout 对应显示器文件,下标为1;尺度错误流 stderr 对应显示器文件,下标为2。从这里可以的出一个结论,默认打开三个尺度输入输出流并不是 C 语言的特性,而是操纵系统的特性,所有语言编写的步伐运行起来后都会打开。操纵系统为什么要帮我们打开呢?因为电脑在开机的时候,键盘和显示器就已经被打开了,我们在编程的时候,一般都会用键盘输入和通过显示器查看结果。
文件描述符对应的分配规则:从0下标开始,寻找最小的没有利用的数组位置,它的下标就是新打开文件的文件描述符。
4.1 再来认识 FILE

FILE 是 C 语言库中本身封装的一个结构体,在 C 语言中,通过 FILE 对象去描述文件。可以确定,FILE 中肯定封装了文件描述符。如下面代码,FILE 中的 _fileno 属性就是文件描述符。
  1. int main()
  2. {
  3.     printf("stdin->fd: %d\n", stdin->_fileno); // 标准输入
  4.     printf("stdout->fd: %d\n", stdout->_fileno); //标准输出
  5.     printf("stderr->fd: %d\n", stderr->_fileno); // 标准错误
  6.     return 0;
  7. }
复制代码

4.2 再来理解关闭文件

一个文件可以被多个进程同时打开,最常见的比如键盘文件,显示器文件。在 struct file 对象中有一个 f_count 字段,叫做当前文件的引用计数,记载了当前文件被多少个进程打开了,在进程视角关闭文件就是调用 close 系统调用,将对应下标里面的内容置为 NULL,这是进程系统需要执行的工作。置空后操纵系统会把该文件描述对应文件结构体对象中的 f_count 字段减减,然后判断 f_count 是否为0,如果不为0就什么也不干,如果为0,操纵系统才将对应的 struct file 对象回收,这是文件系统执行的工作。从这儿可以看出,文件描述符表的存在,将进程系统和文件系统举行了美满的解藕。这不禁让我想起了前面的假造地址(进程地址空间)和页表的存在将进程系统和内存系统举行解藕。Linux 操纵系统的计划真的让人拍案叫绝!
  1. int main()
  2. {
  3.     close(1); // 将 stdout 关闭
  4.     int ret = printf("stdin->fd: %d\n", stdin->_fileno);
  5.     printf("stdout->fd: %d\n", stdout->_fileno);
  6.     printf("stderr->fd: %d\n", stderr->_fileno);
  7.     fprintf(stderr, "printf ret: %d\n", ret);
  8.     return 0;
  9. }
复制代码

代码分析:close(1) 表示将尺度输出关闭,1下标指向显示器文件,printf 就是向尺度输出中举行写入,关闭后,三条 printf 函数都没有将内容成功打印到显示器上。根据上面的分析,固然把尺度输出关了,但是尺度错误也指向显示器,以是在调用 fprintf 向尺度错误中写入时,我们可以在显示器上看到打印结果。其次,printf 执行成功,返回值表示写入的字符个数,可以看出固然我们通过系统调用直接把尺度输出给关了,但是 printf 还是认为它写入成功。
五、结语

本日的分享到这里就竣事啦!如果以为文章还不错的话,可以三连支持一下,春人的主页另有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

悠扬随风

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表