【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
O_APPEND:在文件的末尾追加数据。如果与写操作一起使用,写入的数据将会被添加到文件的末尾,而不是覆盖现有内容。
O_CREAT:如果指定的文件不存在,则创建一个新文件。这个标志通常与
O_WRONLY
或O_RDWR
一起使用,以便在创建文件的同时打开它。O_EXCL:与
O_CREAT
一起使用时,确保如果文件已经存在,则打开操作失败。也就是说, 只有在新建文件时不会覆盖已存在的文件,如果用O_CREAT | O_EXCL
组合打开一个已存在的文件,将返回错误。O_TRUNC:如果打开的文件已经存在且是以写入模式打开(如
O_WRONLY
或O_RDWR
),则将其长度截断为零,相当于清空文件的内容。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 本质上是每个进程的“身份证”,用于操作系统调度和管理进程的运行。主要包括:
进程标识符(Process ID,PID):唯一标识一个进程的编号。
进程状态:指示进程当前的状态,通常包括新建(New)、就绪(Ready)、运行(Running)、等待(Blocked)和终止(Terminated)。
程序计数器(Program Counter,PC):指向进程下一条将要执行的指令的地址。
CPU 寄存器:存储 CPU 状态的寄存器,包括累加器、索引寄存器等,这些寄存器的值在进程上下文切换时需要保存和恢复。
内存管理信息:包括基址寄存器和界限寄存器,或页表等信息,用于管理该进程的内存空间。
调度信息:包含调度优先级、调度队列指针等,用于进程的调度和管理。
I/O 状态信息:记录该进程所占用的 I/O 设备和正在进行的 I/O 操作的信息。
进程间通信信息:如果进程与其他进程有通信或同步需求,则需要保存相关的信息。
它在计算机中是以结构体的形式存在的:
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语言中,标准函数(如fopen
、fread
、fwrite
、fclose
等)会通过FILE
指针间接操作文件。
运行示例:
因为程序的本质是一个进程,而操作系统会为每个进程都维护一个文件描述符表。所以我们将根据下面一些示例来对返回值fd的值进行深入地探究。
①一个程序在两个终端运行:
在该示例中,两个终端属于不同的进程,所以程序是在两个不同的进程执行的。那么每个进程除了默认维护 012三个文件描述符外,都将新维护一个fd=3的文件。所以此时是两个fd=3。
② 两个程序在两个终端运行:
在该示例中,两个不同的程序在不同的终端进行,也就是在两个进程中运行,那么得到的结果依旧是两个fd=3。在同一个进程中,并没有多维护第五个值为4的文件描述符。
③一个程序打开两个文件:
在该示例中,一个进程中,运行两个不同的open,打开两个文件,因此第一个fd的值是012除外的3,而第二个fd2的值就是在此基础上的第五个描述符:4。
通过上述三个示例,我们更加清晰的体会到了,“每个进程维护一个文件描述符表”,这句话。
感谢大家!