其余相关内容可参考个人博客
文件描述符
Linux系统将所有设备都当作文件来处理,而Linux用文件描述符来标识每个文件对象。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
标准文件描述符
每个进程都有一张文件描述符的表,进程刚被创建时,标准输入、标准输出、标准错误输出设备文件被打开,对应的文件描述符0、1、2 记录在表中。在进程中打开其他文件时,系统会返回文件描述符表中最小可用的文件描述符,并将此文件描述符记录在表中
文件描述符 | 缩写 | 描述 |
---|---|---|
0 | STDIN | 标准输入 |
1 | STDOUT | 标准输出 |
2 | STDERR | 标准错误 |
文件描述符的复制
dup
int dup(int oldfd);
功能:复制旧的文件描述符,自动分配一个可用的最小的文件描述符
成功返回新的文件描述符,失败返回-1
函数说明:
它们引用相同的打开文件描述,因此共享文件偏移量和文件状态标志。 例如,如果在旧的文件描述符之上使用lseek
修改了文件偏移,则新的也将更改。
关于文件描述符与打开文件、文件的关系在后续文章将会介绍,阅读后能更容易理解上述说明
dup2
int dup2(int oldfd, int newfd);
功能:复制旧的文件描述符,自动分配一个可用的最小的文件描述符
成功返回新的文件描述符,失败返回-1
函数说明:
dup2
是dup
函数的升级版本,可以指定生成的文件描述符(必须小于1024),如果这个指定的描述符已经打开了,那么会原子地关闭和复制。- 若
oldfd
是无效的,则newfd
不会被关闭 - 若
oldfd
是无效的,且newfd
和oldfd
相等,则dup2
函数什么也不干,直接返回newfd
修改标准文件描述符
标准文件描述符0,1,2一旦被改变了就无法使用了,所以在重定向之前需要把他们三个保存起来:
int new_stdout = dup(1);//在重定向之前保存起来
dup2(new_stdout,1);//这样就可以变回来了
写例子的时候还有个小问题,我们重定向之后,printf
到文件当中,然后把stdout
变回来,再printf
一句话,这个时候可以看到终端上两句话都打印出来了,那是因为重定向输出到文件的时候缓冲区是全缓冲的,所以数据还在缓冲区当中,没有写到文件当中呢,为了避免这类问题,可以选择使用系统调用(无缓冲区)
关于缓冲区的问题可继续阅读本文后续章节
exec后的文件描述符
无论是fork
还是system
出子进程,如果父进程里在open
某个文件后(包括socket fd
)没有设置FD_CLOEXEC
标志,就会引起各种不可预料的问题,特别是socket的fd
本身又包括了本机ip,端口号等信息资源,如果该socket fd
被子进程继承并占用,或者未关闭,就会导致新的父进程重新启动时不能正常使用这些网络端口,严重的就是设备掉线。
打开文件后默认未将该标志位置位,即默认在exec后不关闭文件描述符,可进行如下设置:
int flags;
flags = fcntl(fd, F_GETFD);//获得标志
flags |= FD_CLOEXEC; //打开标志位
flags &= ~FD_CLOEXEC; //关闭标志位
fcntl(fd, F_SETFD, flags);//设置标志
其实open函数的flag提供了
O_CLOEXEC
标志位,可直接设置(仅Linux 2.6.23后支持)。
文件描述符和打开文件的关系
- 每个文件描述符都指向一个打开的文件相对应
- 不同的文件描述符可能指向同一个打开的文件
- 相同的文件可能被不同的进程打开,也可以在被同一个进程打开多次
具体情况要具体分析,需要查看由内核维护的3个数据结构:
- **进程级的文件描述符表:**进程级的列表,也就是用户区的一部分,进程每打开一个文件就会新建一个文件描述符,同时只能通过文件描述符的函数访问
- **系统级的打开文件表:**系统级的列表,对当前系统的所有进程都共享.
- **文件系统的i-node表:**inode索引节点表。
文件描述符表 | 打开文件表 | i-node表 | |
---|---|---|---|
记录内容 | 文件描述符操作标志(目前内核仅定义了一个close-on-exec标志) | 当前文件偏移量 | 文件类型 |
对打开文件句柄的引用 | 打开文件时使用的状态标识(open的flags参数) | 文件锁 | |
文件访问模式(open时设置的O_RDONLY等标志) | 文件拥有者的UID,GID | ||
对该文件i-node对象的引用 | 文件的时间戳:ctime,mtime,atime | ||
文件类型(例如:常规文件、套接字或FIFO) | 链接数,即有多少文件名指向这个inode | ||
访问权限 | 读写执行权限 | ||
一个指针,指向该文件所持有的锁列表 | 文件数据block的位置 | ||
文件的各种属性,包括大小以及各种时间戳 | 文件的各种属性,包括大小以及各种时间戳 | ||
与信号驱动相关的设置 |
示例如下图所示:
- 在进程A中,文件描述符1和30都指向了同一个打开的文件句柄(标号23)。这可能是通过调用
dup、dup2
- 进程A的文件描述符2和进程B的文件描述符2都指向了同一个打开的文件句柄(标号73)。这种情形可能是在调用
fork
后出现的 - 进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向i-node表的相同条目(1976),发生这种情况是因为每个进程各自对同一个文件发起了
open
调用,同一个进程两次打开同一个文件,也会发生类似情况
文件描述符限制
系统级限制
查看方式:
sysctl -a | grep -i file-max --color
cat /proc/sys/fs/file-max
sysctl
命令和proc
文件系统中查看到的数值是一样的,这属于系统级限制,它是限制所有用户打开文件描述符的总和
用户级限制
每个进程的最大文件描述符限制:
ulimit -n
修改方式
-
修改用户级限制:
ulimit -SHn 10240
以上的修改只对当前会话起作用,是临时性的,如果需要永久修改,则要修改
/etc/security/limits.conf
文件:* soft nofile 100001 * hard nofile 100002
soft 指的是当前系统生效的设置值,hard 表明系统中所能设定的最大值
-
修改系统级限制:
[root@VM-0-4-centos ~]# cat /proc/sys/fs/file-max 350000 [root@VM-0-4-centos ~]# echo 50000 > /proc/sys/fs/file-max [root@VM-0-4-centos ~]# cat /proc/sys/fs/file-max 50000 [root@VM-0-4-centos ~]# sysctl -a | grep -i file-max --color fs.file-max = 50000
以上是临时修改,重启后失效,永久修改如下
把
fs.file-max=400000
添加到/etc/sysctl.conf
中,使用sysctl -p
即可
缓冲区
出于速度和效率考虑,系统IO调用和标准 C语言库的IO函数均会对数据进行缓冲,接下来将分类介绍:
系统IO调用缓冲
read
和write
在操作磁盘文件的时候不会直接发起磁盘访问,而是在用户空间缓冲区和内核缓冲区高速缓存之间复制数据。
write(fd,"abc",3);
上面的语句将3个字节的数据从用户空间内存传递到内核空间的缓冲区中,随后write
返回,在后续的某个时刻,内核会将其缓冲区中的数据写入(刷新至)磁盘,在此期间如果有另一进程访问这几个字节,直接从高速缓存中提供这些数据。对输入而言同理。
这一设计不需要read
和write
等待磁盘操作,也减少了内核进行磁盘传输的次数。例如:让磁盘写1000次,每次写入一个字节,还是一次写入1000个字节,内核访问磁盘的次数都是相同的,因为有缓冲区的存在,但是我们更趋向于后者,因为只有一次系统调用,所以这部分是程序员需要思考的,这部分的缓冲也就是下面提到的stdio库的缓冲了。
简单来说,就是在write系统调用和实际的磁盘之间还有一层由内核维护的缓冲。
stdio库的缓冲
在操作磁盘文件的时候,虽然有内核维护的缓冲来减少访问磁盘的次数以节省开销,但是还有一部分开销是由系统调用产生的,也就是程序中确定每次write
或者read
多少个字节,而stdio库的缓冲就是帮程序员干这件事的,分为以下三类:
-
无缓冲
每个stdio库函数立即调用write
或者read
-
行缓冲
只带终端设备的流默认为这一缓冲类型。对于输出流,在输出一个换行符(除非缓冲区已经填满)前将缓冲数据,遇到换行符会刷新缓冲区。对于输入流,每次读取一行数据 -
全缓冲
单次读写数据(通过write和read)的大小和缓冲区相同,只带磁盘的流默认采用此模式。
手动刷新stdio缓冲区
int fflush(FILE *stream);
- 使用该库函数强制将stdio输出流中的数据刷新到内核缓冲区中
- 应用于输入流时,这将丢弃已缓冲的输入数据。当程序下一次尝试从流中读取数据时,将重新装载缓冲区
- 若
stream
为NULL
,则将刷新所有的输出缓冲区 - 当关闭流时,自动刷新缓冲区
函数
open
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能:打开pathname所标识的文件,并返回文件描述符,flags可以指定文件的打开方式,mode指定了访问权限,如果flags中没有创建文件的标志,mode可以忽略
flag取值:
标志 | 用途 |
---|---|
O_RDONLY | 以只读方式打开 |
O_WRONLY | 以只写方式打开 |
O_RDWR | 以读写方式打开 |
O_CLOEXEC | 设置close-on-exec标志,默认关闭,即exec后文件描述符不关闭 |
O_CREAT | 若文件不存在则创建,需要指定mode |
O_EXCL | 结合O_CREAT标志使用,专门用于创建文件(若文件已存在,则直接返回错误) |
O_NONBLOCK | 以非阻塞方式打开 |
O_APPEND | 总在文件尾追加数据(若多个进程同时对同一文件追加数据,可能导致文件损坏) |
O_TRUNC | 截断已有文件,使其长度为0 |
O_SYNC | 以同步方式写入文件 |
O_ASYNC | 当IO操作可行时,产生信号通知进程(此特性仅适用终端、伪终端、socket和管道) |
O_DSYNC | 提供同步的IO数据完整性,即write返回后,数据均已输出到硬件 |
O_DIRECT | 无缓冲的输入输出 |
O_DIRECTORY | 如果pathname不是目录,则失败 |
O_LARGEFILE | 在32位系统中使用该标志打开大文件 |
O_NOATIME | 调用read时不修改文件最近访问时间 |
O_NOCTTY | 不要让pathname(所指向的终端设备)成为控制终端 |
O_NOFOLLOW | 对符号链接不予解引用 |
补充:
-
O_DIRECT和O_SYNC的区别
O_DIRECT:绕过内核的页面缓存将数据写入设备,但是设备本身也存在缓存所以并不能保证数据就一定固化到磁盘上
O_SYNC:文件数据和所有文件元数据同步写入磁盘 -
O_DSYNC、O_RSYNC、O_SYNC的区别
- Linux中无O_RSYNC,glibc定义O_RSYNC具有与O_SYNC相同的值
- 在写操作中,O_DSYNC和O_SYNC均保证数据同步更新到文件中,O_DSYNC将仅保证刷新对文件长度元数据的更新(而O_SYNC也刷新最后的修改时间戳记元数据)
read
ssize_t read(int fd, void *buf, size_t count);
功能:从fd文件中读取至多count字节的数据并保存到buf中。
返回值为实际读取到的字节数,如再无字节可读(例如读到文件结尾符EOF时),返回值为0
write
ssize_t write(int fd, const void *buf, size_t count);
功能:从buf中读取多达count字节的数据写入fd指代的已打开的文件中
返回值为实际写入文件中的字节数,有可能小于count
close
int close(int fd);
功能:释放文件描述符fd及相关的内核资源
成功返回0,失败返回-1
lseek
off_t lseek(int fd, off_t offset, int whence);
功能:改变文件偏移量
名称 | 说明 | |
---|---|---|
参数 | fd | 文件描述符 |
offset | 指定了一个以字节为单位的数值 | |
whence | 表示参照哪个基点来解释offset,取值如下: SEEK_SET 文件开头 SEEK_CUR 当前偏移量 SEEK_END 文件末尾 | |
返回值 | off_t | 成功返回距文件开头的偏移量,失败返回-1 |
lseek不适用于所有类型的文件,不能用于如管道、FIFO、socket和终端
文件空洞
如果程序的文件偏移量已经跨越了文件结尾,然后在执行I/O操作,将会发生read
调用返回0,表示文件结尾,write
可以正常写入数据。从文件结尾到重新用write
写入数据的这段空间被称为文件空洞,从编程角度看,文件空洞中是存在字节的,读取空洞将会返回以0(空字节)填充的缓冲区。然而文件空洞不会占用磁盘空间。
ls
命令可以查看文件在文件系统中的大小(逻辑大小),这个大小是包含文件空洞的空字节大小的.du
命令可以查看文件在磁盘中实际占用的空间,du -s test
结果表示的是多少个1024字节