【Linux系统编程】基础IO第二讲——文件描述符

文章目录

  • [1. open的返回值](#1. open的返回值)
  • [2. 文件描述符](#2. 文件描述符)
    • [2.1 看看现象](#2.1 看看现象)
    • [2.2 谈谈原理+看源码(什么是文件描述符)](#2.2 谈谈原理+看源码(什么是文件描述符))
    • [2.3 补充了解(缓冲区)](#2.3 补充了解(缓冲区))
    • [2.4 文件描述符的分配规则](#2.4 文件描述符的分配规则)
  • [3. 代码](#3. 代码)

上一篇文章中,我们还遗留了一些问题,后面的文章会一一解决。

这篇文章,先来解决:文件描述符。

1. open的返回值

上一篇文章我们已经学过open系统调用的用法了,但是open的返回值我们并没有详细介绍


open的返回值:
成功:返回最小的未使用的文件描述符(非负整数)
失败:返回 -1,并设置 errno 以指示错误原因。

那这篇文章我们就要来解开这个疑惑了

到底什么是文件描述符呢?
又为什么是一个非负整数呢?

2. 文件描述符

2.1 看看现象

再来看我们上篇文章的那段代码:


open的返回值不就是给打开的文件分配的文件描述符吗?(后续的操作如write、close都要使用这个文件描述符)
那我们把这个返回值打印出来看看他是几?

运行

我们看到是3 ,确实是一个非负整数,但是为什么是3呢?

我们可以再多打开几个文件看看:

我们来重新写一个代码

依次打开4个文件,并打印它们的文件描述符
看看结果

3,4,5,6
目前来看,我们打开的文件,文件描述符好像是从3开始,依次递增的整数。

那为什么是这样呢?这些数字又到底是什么呢?

2.2 谈谈原理+看源码(什么是文件描述符)

一个进程可以打开多个文件,那在操作系统内,就可能同时存在多个进程,它们一共打开了很多的文件。并且文件和进程之间还有一定的从属关系。

那这么多的进程,要不要被操作系统管理起来呢?

当然!
如何管理?
先描述,再组织!
在 Linux 内核中,使用struct file结构体 来描述一个被进程打开的文件。

每个 open 系统调用成功时,内核都会创建一个 struct file 实例,并返回一个文件描述符(fd)与之关联。
struct file内部,会直接或间接地包含与被打开文件相关的属性和数据等信息。
多个struct file之间,用一个双向循环链表组织起来。
那么这样,对文件的管理,就转换成了 对链表的增删查改。

那这么多的文件,如果表示它们与进程之间的从属关系呢?(哪些文件属于哪个进程打开的)

在进程的task_struct中,有一个结构体指针------struct files_struct* files
该指针指向一个struct files_struct结构体,该结构体中有一个成员struct file * fd_array[];,是一个struct file *的结构体指针 数组,那里面的元素不就指向一个个的struct file 结构体嘛(就对应了该进程打开的所有文件)
那既然是数组的元素,就有下标 啊。
每个文件的struct file*指针 的下标就是该文件的文件描述符。
进程每打开(open)一个文件,就会把对应的struct file结构体的指针存入fd_array数组中(找一个最小的且没被占用的下标位置),这也解释了为什么文件描述符是非负整数
然后返回其对应的下标(即open的返回值),也是该文件的文件描述符。
所以操作系统内部,是使用文件描述符来唯一标识一个被特定进程打开的文件的,这也是为什么我们后面使用write、close这些系统调用必须要传文件描述符(这里也能推断出C语言中的FILE中必定封装了文件描述符fd)。

那为什么我们打开的文件,文件描述符是从3开始呢?也就明白了!


因为进程启动时候,默认打开了三个文件(标准输入、标准输出、标准错误),占用了0,1,2下标,所以我们再打开新的文件,下标就从3开始。

当然我们也可以打印出来标准输入、标准输出、标准错误对应的文件描述符来看看:

上面我们提到C语言中的FILE中必定封装了文件描述符fd
我们可以来看下FILE的定义

而:

它们的类型是FILE*,那我们通过->不就直接可以访问其中的文件描述符成员嘛!

所以,我们运行程序,前三个就应该是0,1,2

没有问题!
在C++中,cin、cout、cerr分别对应标准输入流对象、标准输出流对象、标准错误流对象,它们的类定义中,必定也有一个成员变量是文件描述符。

2.3 补充了解(缓冲区)

之前的文章中我们简单提过用户级缓冲区:

用户级缓冲区是在 FILE 结构体内部维护的。

那其实除了用户缓冲区还有内核缓冲区
同样这篇文章中我们提到:
我们使用printf打印一个字符串,并不是直接输出到显示器上的,而是先会暂存到缓冲区,等待合适的时机刷新到显示器"文件中"(inux下一切皆文件)。
现在应该再完善一点:
我们使用fwrite写入一个字符串到某个文件(以fwrite为例),并不是直接写入到文件的,而是先会暂存到用户缓冲区,即先从用户空间(比如一个字符数组存的字符串/一个常量字符串存在常量区,都在用户空间)到用户缓冲区,fwrite 最终会调用 write 系统调用,write系统调用会把数据从用户缓冲区拷贝到内核缓冲区,然后等待合适的时机,再刷新到文件中(比如内核缓冲区满了),这样可以减少磁盘IO次数,提高效率。
那如果使用的本身就是write这些系统调用,那就是先从用户空间拷贝到内核缓冲区,后面一样。
所以write这些接口,并不是直接把数据写入文件,其本质是一个拷贝函数,最终数据刷新到文件是合适的时机下由OS完成的。
两层缓存:C 库缓冲减少系统调用,内核页缓冲减少磁盘 I/O。当然这两层缓存都要放在内存中,内存是 CPU 可以直接访问的,这符合冯·诺依曼体系结构。

那同样地,读文或修改文件内容也都需要经过对应的缓冲区。
(先了解一下,后面我们还会谈缓冲区)

2.4 文件描述符的分配规则

其实上面已经讲完这个规则了:

新分配的文件描述符,永远是fd_array数组(也叫文件描述符表)中,最小的且没被占用的下标

我们也可以再来多做一些验证:

我们正常打开一个文件,文件描述符就从3开始分配,因为0,1,2被占用了。
那我们调用close可以关闭我们自己打开文件的描述符,当然也可以关闭0,1,2

那我现在把文件描述符0关闭,然后打开一个文件,分配的规则不是分配最小的且没被占用的下标嘛。
那现在我打开的这个文件就应该分配到0,因为现在0没被占用,并且最小

把2关掉

那就是2了。

那把1关掉呢?


那这次运行就应该打印1

但是!
这么什么都没打印呢?
🆗,1对应的是什么啊?
是标准输出(显示器)!
而printf为什么默认往显示器打印啊?
其实是因为printf底层固定是往文件描述符1对应的文件中打印的。

但是我们现在把1关闭了,然后新打开一个文件,所以

所以文件描述符1就不再指向标准输出,而是指向我们打开的log1.txt文件。
所以我们打印的信息就不会写入到显示器了,而是写到我们自己新打开的文件了。(我们把这个叫做重定向,后面会详细讲解重定向
我们来看一下

怎么回事,没有啊

但是,为什么这个文件里也没有呢?

修改代码

最后的close注释掉
然后重新运行

这次文件里面就被写入了1。
怎么回事?为什么close注释掉就有了
或者可以这样

把close放开,但是close之前调用一下fflush

也可以(现在是一个追加写的模式)
那出现这种现象的原因是什么呢?
这个问题先留着,我们后面会解决

这篇文章先到这里,下一篇------重定向

3. 代码

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    close(1);

    int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 

    printf("%d\n", fd1);

    // 关闭文件
    fflush(stdout);
    close(fd1);

    return 0;
}

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

//     int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写
//     int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写
//     int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写
//     int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写

//     printf("%d\n", fd1);
//     printf("%d\n", fd2);
//     printf("%d\n", fd3);
//     printf("%d\n", fd4);

//     // 关闭文件
//     close(fd1);
//     close(fd2);
//     close(fd3);
//     close(fd4);

//     return 0;
// }
相关推荐
wxytxdy1 小时前
Linux 自动化运维基础 —— 定时任务与日志轮转
linux
Cx330❀1 小时前
【Linux网络】高性能 TCP 服务器:从多线程到线程池的架构演进与落地实践
linux·运维·服务器·网络·c++·tcp/ip·架构
程序猿编码1 小时前
vmlinuz 到 vmlinux:不碰源码,徒手重建内核 ELF 符号表
linux·服务器·网络·内核·elf
Par@ish1 小时前
Ubuntu Apache日志存储周期变更
linux·ubuntu·apache
简单点好呀1 小时前
Valgrind 报告干干净净,内存却在涨——我用 GDB 揪出了 47000 个泄漏的 Lua 闭包
linux
闲猫1 小时前
从0到1完整开发Smartshell最后沉淀出的Cursor开发规则
linux·运维·堡垒机·cursor·vibecoding
炘爚2 小时前
Phase 4:业务线程池 — IO/计算解耦
linux·c++
AOwhisky2 小时前
MySQL 学习笔记(第七期):高可用架构进阶与综合项目实战
linux·运维·笔记·学习·mysql·高可用·mha
张小姐的猫2 小时前
【Linux】多线程 —— 线程池 | 单例模式 | 常见锁
linux·运维·服务器·c++·单例模式·设计模式·策略模式