一、文件描述符
调用 open 函数会有一个返回值,该返回值就是一个文件描述符(
file descriptor),这说明文件描述符是一个
非负整数;对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引。
当调用 open 函数打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符,用于指代被打开的文件,所有执行 IO
操作的系统调用都是通过文件描述符来索引到对应的文件。
一个进程能打开的文件是有限的,所以对进程来说,文件描述符也是有限的,文件描述符是从 0 开始分配的,譬如说进程中第一个被打开的文件对应的文件描述符是 0、第二个文件是 1、第三个文件是 2、第 4 个文件是 3……以此类推,所以由此可知,文件描述符数字最大值为 1023(0~1023)。每一个被打开的文件在同一个进程中都有一个唯一的文件描述符,不会重复,如果文件被关闭后,它对应的文件描述符将会被释放,那么这个文件描述符将可以再次分配给其它打开的文件、与对应的文件绑定起来。
当我们在程序中,调用 open
函数打开文件的时候,分配的文件描述符一般都是从
3
开始,这里大家可能要问了,上面不是说从 0
开始的吗,确实是如此,但是
0
、
1
、
2
这三个文件描述符已经默认被系统占用 了,分别分配给了系统标准输入(0
)、标准输出(
1
)以及标准错误(
2
)
(注:标准输入一般对应的是键盘,可以理解为 0 便是打开键盘对应的设备文件时所得到的文件描述符;标 准输出一般指的是 LCD 显示器,可以理解为 1 便是打开 LCD 设备对应的设备文件时所得到的文件描述符; 而标准错误一般指的也是 LCD 显示器。)
二、open打开文件
在
Linux
系统中要操作一个文件,需要先打开该文件,得到文件描述符,然后再对文件进行相应的读写操作(或其他操作),最后在关闭该文件;open
函数用于打已经存在的文件之外,
还可以创建一个新的文件,函数原型如下所示:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
从上图可知,open函数有两种原型,但C语言又不支持重载,所以open应为可变参函数:
所以由此可知,在应用程序中调用
open
函数即可传入
2
个参数(
pathname
、
flags
)、也可传入
3
个参数(pathname
、
flags
、
mode
),但是第三个参数
mode
需要在第二个参数
flags 满足条件时才会有效。
open
函数的第
3
个参数只有在使用
了
O_CREAT
或
O_TMPFILE
标志
时才有效。
参数说明:
pathname
:
字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径)信 息,譬如:"./src_file"
(当前目录下的
src_file
文件)、
"/home/dengtao/hello.c"
等;如果
pathname
是一个符号链接,会对其进行解引用。
flags
:
调用
open
函数时需要提供的标志,包括文件访问模式标志以及其它文件相关标志,这些标志使 用宏定义进行描述,都是常量,open
函数提供了非常多的标志,我们传入
flags
参数时既可以单独使用某一 个标志,也可以通过位或运算(|)将多个标志进行组合。比如O_RDONLY 、
O_WRONLY
mode
:
此参数用于指定新建文件的访问权限,只有当
flags
参数中包含
O_CREAT
或
O_TMPFILE
标志 时才有效(O_TMPFILE
标志用于创建一个临时文件)
在实际编程中,我们可以直接使用
Linux
中 已经定义好的宏,不同的宏定义表示不同的权限,如下所示:
三、Linux如何管理文件
1、静态文件与inode
文件在没有被打开的情况下一般都是存放在磁盘中的,譬如电脑硬盘、移动硬盘、U
盘等外部存储设备,文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为静态文件。
文件储存在硬盘上,硬盘的最小存储单位叫做“扇区”(Sector
),每个扇区储存
512
字节(相当于
0.5KB
), 操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次 性读取一个“块”(block
)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是 4KB
,即连续八个
sector
组成一个
block
。
所以由此可以知道,静态文件对应的数据都是存储在磁盘设备不同的“块”中,我们的磁盘在进行分区、
格式化的时候会将其分为两个区域,一个是数据区,用于存储文件中的数据;另一个是 inode 区,用于存放 inode table(inode 表),inode table 中存放的是一个一个的 inode(也称为 inode节点),不同的 inode 就可以表示不同的文件,每一个文件都必须对应一个 inode,inode 实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、文件类型、文件数据存储的 block(块)位置等等信息。
在
Linux
系统中,内核会为每个进程设置一个专门的数据结构用于管理该进 程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block
,缩写 PCB)。
PCB
数据结构体中有一个指针指向了文件描述符表(
File descriptors
),文件描述符表中的每一个元素 索引到对应的文件表(File table
),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文 件状态标志、引用计数、当前文件的读写偏移量以及 i-node
指针(指向该文件对应的
inode
)等,进程打开 的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表, 其示意图如下所示:
系统内部会将这个过程分为三步:
1)
系统找到这个文件名所对应的
inode
编号(do_file_open函数内执行);
2)
通过
inode
编号从
inode table
中找到对应的
inode
结构体;
3)
根据
inode
结构体中记录的信息,确定文件数据所在的
block
,并读出数据。
2、文件打开时的状态
当我们调用
open
函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作, 而并不是针对磁盘中存放的静态文件。
3、多次打开同一个文件
- 一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的时候也需要调用 close 依次关闭各个文件描述符。
- 一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。
- 一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的。
4、原子操作与竞争冒险
操作共享资源的两个进程(或线程),其操作之后的所得到的结果往往是不可预期的,因为每个进程(或线程)去操作文件的顺序是不可预期的,即这些进程获得 CPU
使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争状态。既然存在竞争状态,那么该如何规避或消除这种状态呢?可以执行
原子操作:
所谓原子操作,是有多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有 步骤,不可能只执行所有步骤中的一个子集。
(1)O_APPEND 实现原子操作
进程
A
和进程
B
都对同一个文件进行追加写操作,导致进程
A
写入 的数据覆盖了进程 B
写入的数据,解决办法就是将“先定位到文件末尾,然后写”这两个步骤组成一个原 子操作即可,那如何使其变成一个原子操作呢?答案就是 O_APPEND
标志。
当
open
函数的
flags
参数中包含了
O_APPEND
标志,每次执行
write
写入操作时都会将文件当前写位置偏移量移动到文件末尾,然后再写入数据,这里“移动当前写位置偏移量到文件末尾、写入数据”这两个操作步骤就组成了一个原子操作,加入 O_APPEND
标志后,不管怎么写入数据 都会是从文件末尾写,这样就不会导致出现“进程 A
写入的数据覆盖了进程
B
写入的数据”这种情况了。
(2)pread()和 pwrite()
pread()
和
pwrite()
都是系统调用,与
read()
、
write()
函数的作用一样,用于读取和写入数据。区别在于, pread()和
pwrite()
可用于实现原子操作,调用
pread
函数或
pwrite
函数可传入一个位置偏移量
offset
参数, 用于指定文件当前读或写的位置偏移量,所以调用 pread
相当于调用
lseek
后再调用
read
;同理,调用
pwrite 相当于调用 lseek
后再调用
write
。所以可知,使用
pread
或
pwrite
函数不需要使用
lseek
来调整当前位置偏 移量,并会将“移动当前位置偏移量、读或写”这两步操作组成一个原子操作。
(3)创建一个文件
O_EXCL
可以用于测试一个文件是否存在,如果不存在则创建此文件,如果存在则返回错误,这使得测试和创建两者成为一个原子操作。
5、fcntrl函数
fcntl()
函数可以对一个已经打开的文件描述符执行一系列控制操作,譬如复制一个文件描述符(与
dup
、 dup2 作用相同)、获取
/
设置文件描述符标志、获取
/
设置文件状态标志等,类似于一个多功能文件描述符管理工具箱。
6、ioctrl函数
ioctl()
可以认为是一个文件
IO
操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件 或硬件外设。