本站总访问量 Unix环境高级编程-文件IO - Jerry的小站

Jerry Gao

上帝就是真理,真理就是上帝

文件IO

open read write lseek close

文件描述符

文件描述符是一个非负整数。当打开或者创建一个文件时,内核向进程返回一个文件描述符。当读写一个文件时,使用open或者create返回的

文件描述符变化范围是0~OPEN_MAX-1,早期的Unix文件系统实现采用上限值是19,即允许每个进程最多打开20个文件,但现在很多系统将其上限增加至63.

在Linux里面,每个进程最大打开数为1024,修改这个设置可以使用如下方法:

临时性修改

1
ulimit -n 30665

永久性修改需要更改配置文件

1
2
3
vim /etc/security/limits.conf
* soft nofile 30665 # 应用软件级别的限制
* hard nofile 30665 # 操作系统级别的限制

函数open和opennat

由open和opennat所返回的文件描述符一定是最小的为用的文件描述符数值。

fd参数把open和opennat分隔开,有三种可能性:

  • path指定绝对路径名,这时候fd参数被忽略
  • path参数指定相对路径名,fd指出了相对路径名在文件系统中的开始地址
  • path指定相对路径名,fd参数具有特殊值AT_FDCWD,路径名在当前工作目录中获取,opennat在操作上与open类似

总而言之,opennat希望让线程可以使用相对路径打开目录中的文件,而不再只能打开当前工作目录,其次可以避免time-of-check-to-time-of-use错误(TOCTTOU错误)。

文件名和路径截断

在Linux当中过长的文件名会出错。

在Linux当中,获取文件名的长度可以使用这个命令

1
2
3
4
getconf PATH_MAX /usr
1024
getconf NAME_MAX /usr
255

由上面可知,Linux当中默认文件名最大长度是255个字节,而路径最大长度是1024个字节。

函数creat

以只写方式打开所创建文件,如果要创建一个临时文件,并要写该文件,必须先调用creat、close然后调用open,现在可以直接用open方式实现。

函数close

关闭一个文件会释放该进程加在该文件上的记录锁,当一个进程终止时,内核自动关闭他所打开的所有文件,很多程序利用这一点并不是显式的调用close关闭文件。

函数lseek

当前文件偏移量用以度量一个文件从当前开始的字节数。按系统默认的情况,每打开一个文件,偏移量被设置为0。

如果lseek返回-1,表示文件描述符指向的是一个管道、FIFO或者网络套接字,lseek将errno设置为ESPIPE。

lseek仅将当前的偏移量记录在内核中,并不进行任何的IO操作,该偏移量用于下一个IO操作。

如果偏移量大于文件长度,是允许的,下一次写将加长文件长度,并在文件中形成一个空洞,没写过的字节都被读成0。空洞不需要分配磁盘块。

函数read

函数write

IO的效率

大多数文件系统为改善性能都采取某种预读技术,当检测到正进行顺序读取的时候,系统就试图写入比应用所要求的更多的数据,并假想应用会很快读取。

文件共享

内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。

  • 每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表(进程表项)包含:
    • 文件描述符标志
    • 指向一个文件表项的指针
  • 内核为所有打开文件维持一张文件表,包含:
    • 文件状态标志
    • 当前文件偏移量
    • 指向该文件v节点表项的指针
  • 每个打开文件都有一个v节点结构,v节点包含文件类型和对此文件进行各种操作的函数的指针。还包含i节点(包含文件所有者,文件长度,指向文件实际数据块在磁盘上所在位置的指针等)。

使用write方式导致当前文件偏移量发生改变与使用lseek方式导致当前文件偏移量发生改变的方式不同,lseek不会进行IO操作。

文件描述符标志和文件状态标志在作用范围的区别:

  • 前者只用于一个进程的一个描述符
  • 后者应用于指向该给定文件表项的任何进程中的所有描述符

原子操作

追加到一个进程

A和B两个进程都对同一个文件进行追加操作,A和B都以打开了该文件,未使用O_APPEND标志。这时候进程A调用了lseek将当前偏移量改变为1500,进程切换,B也用lseek将当前偏移量改变为1500,随即调用write从1500开始进行写操作,写完进程切换,A也调用write从1500开始进行写操作,这时候A的写就会覆盖B的写。

解决方法,将定位到文件尾端和写操作变成一个原子操作

函数pread和pwrite

pread相当于调用lseek后调用pread,但是又有区别

  • 调用praed时无法中断其定位和读操作
  • 不更新当前文件偏移量

创建一个文件

open函数的O_CREATE选项和O_EXCL选项

函数的dup和dup2

用来复制一个现有的文件描述符

dup返回的一定是当前可用文件描述符的最小数值,对于dup2可以用fd2指定新描述符的值。如果fd2已经打开,先将其关闭,如果等于fd,则返回fd2,不关闭它。

1
2
3
4
5
6
7
8
dup(fd);
# 等效于
fcntl(fd, F_DUPFD, 0);
而调用
dup2(fd, fd2);
等效于
close(fd2);
fcntl(fd, F_DUPFD, fd2);

后一种情况下,dup2并不完全等于close加上fcntl。区别如下:

  • dup2是一种原子操作,close和fcntl包含两个函数调用。
  • 有不同的errno

函数sync、fsync和fdatasync

Unix系统实现在内核中设有缓冲区高速缓存,或者页高速缓存,大多数磁盘IO都通过缓冲区进行。当我们向文件写入数据的时候,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写

当内核需要重用缓冲区来存放其他磁盘块中的数据时,会把所有延迟写数据写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX系统中提供了sync、fsync和fdatasync三个函数。

sync将所有修改过的块缓冲区排入写队列,然后就返回,并不等待实际写磁盘操作结束。称为updata的系统守护进程周期性的调用sync函数,保证定期冲洗内核的块缓冲区。

fsync函数只对由文件描述符fd指定的一个文件起作用,等待磁盘操作结束后才返回。

fsync可用于数据库这样需要确保修改过的块立即写到磁盘上的应用。

fdatasync之影响文件的数据部分,而fsync还会同步更新文件的属性。

函数fcntl

可以改变已经打开文件的属性

在修改文件描述符或者是文件状态标志值时需要注意,要先获取现在的标志值,然后按期望修改它,最后设置新的标志值。

通常write只是将数据排入队列,而实际的写磁盘操作则可能在以后的某个时刻进行。而数据库系统则需要使用O_SYNC

函数ioctl

/dev/fd

目录项是名为0、1、2等的文件,打开文件/dev/fd/n等效于复制文件描述符n

所以

1
fd = open("/dev/fd/0", mode)

等效于

1
fd = dup(0)

Linux中的/dev/fd是个例外。它把文件描述符映射成指向底层物理文件的符号链接。例如打开/dev/fd/0时,事实上正在打开与标准输入关联的文件,因此返回新文件描述符的模式与/dev/fd文件描述符的模式并不相关

在Unix上调用creat用/dev/fd/1作为路径名与open时调用O_CREAT作为第二个参数作用相同。但是Linux实现需要很小心,因为Linux实现使用指向实际文件的符号链接,在/dev/fd文件上使用creat会导致底层文件被截断。

很有意思的问题

1
2
3
./a.out > outfile 2>&1
./a.out 2>&1 > outfile
# 两者有什么不同
  • 第一种情况:标准输出1首先重定向到outfile,然后调用dup2(1, 2)将标准错误重定向到1,所以这时候2和1都指向同一个文件表项。
  • 第二种情况:标准输出一开始指向终端,这时候先调用dup2(1, 2)使标准错误也指向终端,然后又把标准输出重定向到outfile。
如果查看一个进程打开了哪些文件

先用ps命令查看进程pid,进入/proc/pid号/fd文件夹,查看文件描述符指向的文件。

评论