当前位置: 首页 > article >正文

【Linux】文件IO--open/close/文件描述符(详)

strace命令:shell中使用strace命令跟踪程序执行,查看调用的系统函数。

open/close函数

头文件:<fcntl.h>

open函数:

函数原型

int open(const char *pathname,int flags);
int open(const char *pathname,int flags,mode_t mode);

//mode_t:八进制的整型

常用参数

:O_RDONLY(readonly-只读)、O_WRONLY(writeonly-只写)、O_RDWR(read&write-读写)

:O_APPEND、O_CREAT、O_EXCL、O_TRUNC、O_NONBLOCK

  1. O_APPEND:在文件的末尾追加数据。如果与写操作一起使用,写入的数据将会被添加到文件的末尾,而不是覆盖现有内容。

  2. O_CREAT:如果指定的文件不存在,则创建一个新文件。这个标志通常与 O_WRONLY 或 O_RDWR 一起使用,以便在创建文件的同时打开它。

  3. O_EXCL:与 O_CREAT 一起使用时,确保如果文件已经存在,则打开操作失败。也就是说, 只有在新建文件时不会覆盖已存在的文件,如果用 O_CREAT | O_EXCL 组合打开一个已存在的文件,将返回错误。

  4. O_TRUNC:如果打开的文件已经存在且是以写入模式打开(如 O_WRONLY 或 O_RDWR),则将其长度截断为零,相当于清空文件的内容。

  5. O_NONBLOCK:以非阻塞模式打开文件。对于网络套接字操作,这表示读取和写入操作不会等待资源就绪,而是立即返回。

简单使用

再见umask:

①新建文件时不设置权限:

②新建文件时设置权限为0664:

③新建文件时设置权限为0666:

此时并不按照预期的结果设置权限,这是因为创建文件时,指定访问权限,权限同时还受掩码影响。使用umask查看掩码:(掩码默认0002)

对于上述的0666权限,我们将8进制转化为2进制数为:110 110 110

掩码写为二进制数为:000 000 010,而我们看到的显示的结果为0664,转化为二进制为:110 110 100。对于这三个数,我们的运算法则是这样的:

110 110 100 = 110 110 110 & ~000 000 010(&:按位与,~:按位取反)

也许你以为这是异或的结果,但是其实不是,根据真值表我们可以得出下表的结果

根据该结果我们只需设置一个:0664(110 110 100),根据异或的结果,得出显示权限为:110 110 110(即0666),根据 (a)与(非b)的结果,得出显示结果为:110 110 110(即0664)。设计程序验证一下:(也就是上面的第②个示例)。所以我们知道了文件实际(显示)权限不是 设置权限 异或上 掩码的结果。

结论为:文件权限 = mode & ~umask

umask命令初识 在后面的传送文章的末尾处:【Linux】命令杂谈--补充总结-CSDN博客

我们之前使用的fopen和fclose是用户级的库函数,这次我们要掌握的是系统级的open和close函数。使用man 2 open查看手册:

第一步,看函数与原型;第二步,看description描述,读到第二行,我们说一个特别重要的东西:file descriptor--文件描述符(这玩意即将伴随你学习linux的一生,哈哈),在这里我可以明确的下个定义,文件描述符就是个整数,但是具体是什么我们稍后再说,现在记住open返回一个整数就行了。 

close函数:

原型:int close(int fd);用于关闭文件,传入的参数为文件描述符。返回值:成功返回0,失败返回-1,并设置errno值。需要说明的是:

当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。

int fd=open("./dict.txt",O_RDONLY);
int ret= close(fd);
printf("ret=%d\n",ret);

常见错误:

1.打开方式不存在

在该示例中,当前目录下,没有名为dict.txt的文件,但是我们的open.c文件试图去以只读的方式打开该文件,并且没有传入其它标识符来预防问题。那么open将打开失败,返回fd=-1,并设置errno的值,这个值就是<errno.h>头文件下的一个"全局变量"errno(实际上是与环境变量相关的),代表的含义为<string.h>头文件下的strerror(errno)返回的一个字符串“No such file or directory”--没有这个文件或目录。 

2.以写的方式打开只读文件(打开文件没有对应权限)

创建一个文件dict.txt,设置权限为0444,编辑open.c文件,尝试以只写的方式打开该文件。输出结果仍然为fd=-1,代表打开失败。并且设置的errno值为13,经过strerror函数查看该值代表的含义,获取到“Permission denied”--权限被拒绝。说明打开的文件没有对应的权限,在该例中即 以只写方式 尝试打开 只有 读权限 的文件。

3.以只写的方式打开目录

在该例中,我们创建一个mydir目录,编辑open.c文件,尝试以只写的方式打开该目录,编译运行:返回fd=-1,说明没有打开成功,那么就进一步看errno的值:errno=21;并查看该值对应的信息:“Is a directory”--是一个目录文件。说明open并不能以只写的方式打开一个目录文件。 

文件描述符

基本认识:

文件描述符(File Descriptor, FD)

文件描述符是一个非常重要的概念,它用于表示对文件、套接字、管道等输入/输出(I/O)资源的引用。

定义:文件描述符是Unix/Linux系统中进程与文件、设备之间的接口,是操作系统中分配给每个打开文件或资源的一个非负整数。

作用:文件描述符用于高效管理已打开的文件,是进程访问文件的途径。通过文件描述符,进程可以对文件进行读写、控制和管理等操作。

默认:在程序刚启动时,默认有三个文件描述符,分别是0(代表标准输入stdin)、1(代表标准输出stdout)和2(代表标准错误stderr)。

程序的本质是一个进程。而操作系统为每个进程维护一个文件描述符表,新打开文件返回文件描述符表中未使用的最小文件描述符,调用open函数可以打开或创建一个文件,得到一个文件描述符。

PCB进程控制块:

可以使用命令:locate sched.h查看位置:(loacte命令需要先下载)

下面给大家看个原理图,左侧大部分都认识,就是虚拟空间分布模型,然后我们此时即将详细查看内核空间中PCB进程控制块里面的文件描述符表。因为涉及到了PCB进程控制块,我们在下面给出它的概念。

 进程控制块(Process Control Block,简称 PCB)是操作系统中用于管理进程的重要数据结构,它包含了一个进程的所有关键状态信息。PCB 本质上是每个进程的“身份证”,用于操作系统调度和管理进程的运行。主要包括:

  1. 进程标识符(Process ID,PID):唯一标识一个进程的编号。

  2. 进程状态:指示进程当前的状态,通常包括新建(New)、就绪(Ready)、运行(Running)、等待(Blocked)和终止(Terminated)。

  3. 程序计数器(Program Counter,PC):指向进程下一条将要执行的指令的地址。

  4. CPU 寄存器:存储 CPU 状态的寄存器,包括累加器、索引寄存器等,这些寄存器的值在进程上下文切换时需要保存和恢复。

  5. 内存管理信息:包括基址寄存器和界限寄存器,或页表等信息,用于管理该进程的内存空间。

  6. 调度信息:包含调度优先级、调度队列指针等,用于进程的调度和管理。

  7. I/O 状态信息:记录该进程所占用的 I/O 设备和正在进行的 I/O 操作的信息。

  8. 进程间通信信息:如果进程与其他进程有通信或同步需求,则需要保存相关的信息。

它在计算机中是以结构体的形式存在的:

struct task_struct{
    int process_id;//进程标识符
    int state;//进程状态(就绪、运行、等待等)
    int priority;//进程优先级
    int register_set[16];//寄存器集合(如通用寄存器、程序计数器等)
    int memory_allocated;//分配的内存大小
    struct task_struct *parent;//父进程的PCB指针
    struct task_struct *next;//指向下一个PCB的指针(用于链式形式管理进程)

    //其他信息
    int cpu_time;//进程使用的CPU的时间
    int io_time;//进程使用的I/O的时间
    //可能还会又其他的同步、调度、信号等信息。
}

而文件描述符表是PCB进程块这个结构体中的 一个指针成员,这个指针指向一个表,就是文件描述符表。 

文件描述符表:

文件描述符表的内容通常包括:

  • 文件描述符:非负整数,如 0、1、2(分别代表标准输入、标准输出和标准错误)。
  • 指向打开文件表的指针:每个条目中会有指针,指向具体的打开文件表(open file table)。
  • 文件状态标志:如只读、只写、可读写等。

在现代操作系统中,通过这种结构,进程可以方便地管理和使用文件。

在文件描述符表中:存储的是一个个文件描述符,文件描述符本质上是一个整数,它作为一个索引,用于指向进程中的文件表中的一个条目。实际上,这个文件描述符并不直接包含文件的信息,而是指向一个内核内部的数据结构--通常称为“文件描述符表”中的相应条目。这个条目包含了有关打开文件的具体信息。例如:文件类型、文件位置、权限信息、文件状态(是否为阻塞模式,阻塞我们会在下一篇文章中提到)。文件描述符的“值”可以被视为一个指向这些结构体的引用,用于在用户空间与内核空间之间进行文件操作。通常使用文件描述符时,它们的类型是int,但在操作的底层,它们的含义更加复杂,需要与操作系统提供的API结合使用,以访问或修改底层资源。

在之后我们常用的描述为:文件描述符是一个指向 文件结构体(struct file) 的指针的索引。因为操作系统不想让你知道详细内容,这些东西是对你隐藏的,所以我们常用的是其索引值、也就是int整型。

最大打开文件数:

一个进程中的文件描述符表的大小是有上限的,也就是1024个,由于下标是从0开始的,所以,一个进程中,最大的 文件的描述符 的值为1023。如果你想改变这个值,也可以,你要去重新编译内核。不推荐这么去干,所以一般认为最大打开文件数为1024个。

这里还有一个概念,新打开的文件的文件描述符是文件描述符表中可用的最小的值。例如:打开一个文件后,那么这个文件使用的文件描述符为3(012被三个标准文件占用,之后就不提了,牢记于心)。再打开一个,就是4,此时关闭fd=3的文件,我再重新打开一个新文件,这个文件的描述符为5还是为3?答案是3!

之后再使用时,如果使用到标准输入、输出、错误。不推荐使用0、1、2这三个数,建议使用三个宏:STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。

命令查看:ulimit -a 查看open files对应值,默认为1024

可以使用ulimit -n 4096修改,也可以通过修改系统配置文件永久修改该值,但是不建议这样操作。

cat /proc/sys/fs/file-max可以查看该电脑最大可以打开的文件个数、受内存大小影响。

FILE结构体:

typedef struct {  
    int fd;                // 文件描述符  
    char *buf;             // 缓冲区指针  
    size_t bufsize;        // 缓冲区大小  
    size_t pos;            // 当前缓冲区位置  
    int flags;             // 状态标志  
    off_t offset;          // 文件位置指针  
    // ... 其他可能的字段  
} FILE;

由于FILE是一种抽象数据类型,用户通常不需要直接访问其内部结构。在C语言中,标准函数(如fopenfreadfwritefclose等)会通过FILE指针间接操作文件。 

 运行示例: 

因为程序的本质是一个进程,而操作系统会为每个进程都维护一个文件描述符表。所以我们将根据下面一些示例来对返回值fd的值进行深入地探究。 

①一个程序在两个终端运行:

在该示例中,两个终端属于不同的进程,所以程序是在两个不同的进程执行的。那么每个进程除了默认维护 012三个文件描述符外,都将新维护一个fd=3的文件。所以此时是两个fd=3。

② 两个程序在两个终端运行:

在该示例中,两个不同的程序在不同的终端进行,也就是在两个进程中运行,那么得到的结果依旧是两个fd=3。在同一个进程中,并没有多维护第五个值为4的文件描述符。

③一个程序打开两个文件:

 在该示例中,一个进程中,运行两个不同的open,打开两个文件,因此第一个fd的值是012除外的3,而第二个fd2的值就是在此基础上的第五个描述符:4。

通过上述三个示例,我们更加清晰的体会到了,“每个进程维护一个文件描述符表”,这句话。


感谢大家!


http://www.kler.cn/a/446812.html

相关文章:

  • 消息队列 Kafka 架构组件及其特性
  • 「Mac畅玩鸿蒙与硬件45」UI互动应用篇22 - 评分统计工具
  • React图标库: 使用React Icons实现定制化图标效果
  • YOLOv8目标检测(七)_AB压力测试
  • dolphinscheduler服务RPC框架源码解析(八)RPC提供者服务整合Spring框架实现
  • 【读书笔记】《论语别裁》学而有何乐
  • 【技术干货】移动SDK安全风险及应对策略
  • 【WPS安装】WPS编译错误总结:WPS编译失败+仅编译成功ungrib等
  • 在 Ubuntu 下通过 Docker 部署 MariaDB 服务器
  • 2024.12.18 周三
  • 对 MYSQL 架构的了解
  • PySide6如何使用自定义委托实现在TableWidget填充颜色
  • CTF 伪造ip的http请求头(学习记录)
  • sql server 查询对象的修改时间
  • 1. 深度学习介绍
  • winpcap抓包原理
  • 记忆组合数据知识
  • 基于LSTM和SSUN模型的高光谱遥感分类实现
  • PCL点云库入门——PCL库中点云数据拓扑关系之K-D树(KDtree)
  • 1、学习大模型总纲
  • FreeRTOS的任务调度
  • 全志H618 Android12修改doucmentsui鼠标单击图片、文件夹选中区域
  • Suno Api V4模型无水印开发「高清音频WAV下载」 —— 「Suno Api系列」第6篇
  • netcore 集成Prometheus
  • 大数据-环保领域
  • 【1.排序】