文件IO
Unix系统中的文件I/O通常只用到5个函数:open
、read
、write
、lseek
和close
。这一节描述的函数都是不带缓冲的I/O,不带缓冲是指每个read
和write
都会执行一个系统调用,而不是从缓冲中读取数据。
1.文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负数,范围0~OPEN_MAX-1。默认情况下0,1,2对应进程标准输入、输出和错误,这些魔法数宏定义在<unistd.h>
。
2.函数open和openat
调用open
或openat
函数可以打开或创建一个文件。
#include<fcntl.h>
int open(const char *path, int oflag,.../* mode_t mode */);
int openat(int fd, const char *path, int oflag,.../* mode_t mode */);
path
是打开或创建的文件名字,oflag
是函数模式(比如只读),通过位与|
操作设定,mode
用于指定插入模式(比如从尾端追加)。两个函数的区别在于fd
,共3种可能:
-
path
指定的是绝对路径,fd
会被忽略,两个函数相同 -
path
指定的相对路径,fd
给出相对路径名在文件系统中的开始地址,fd
是通过打开相对路径名所在的目录来获取 -
path
参数指定了相对路径,fd
参数具有特殊值AT_FDCWD,路径名在当前工作目录中获取。
openat
的提出主要是解决:
- 线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录
- 避免TOCTTOU错误:两个基于文件的调用,由于两个调用不是原子操作,函数调用之间文件可能发生改变,导致第一个调用结果不再有效
3.函数creat
creat
同样用于创建一个新文件:
#include<fcntl.h>
int creat(const char *path, mode_t mode);
/* 返回值:成功返回文件描述符,失败返回-1 */
函数等效于:
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
creat
的缺陷是它以只写方式打开创建的文件。
4.函数close
close
函数用于关闭打开的文件:
#include<unistd.h>
int close(int fd);
/* 返回值:成功0,失败-1 */
关闭文件时,会释放该进程加在该文件上的所有记录锁。当一个进程终止时,内核自动关闭它所有打开的文件,所以很多程序并没显式调用close
。
5.lseek
lseek
用于指定读写操作的偏移量处(不指定默认为0):
#include<unistd.h>
off_t lseek(int fd, off_t offset, int whence);
/* 返回值:成功返回偏移量,失败返回-1 */
whence有3个值:
-
SEEK_SET,则偏移量为距离文件开始处
offset
字节 -
SEEK_CUR,则偏移值设置为当前值+
offset
(可正可负) -
SEEK_END,则偏移值设置为文件长度+
offset
(可正可负)
利用lseek
测试输入是否能使用偏移值:
#include<apue.h>
int main()
{
if(-1 == lseek(STDIN_FILENO, 0, SEEK_CUR))
printf("cannot seek\n");
else
printf("seek OK\n");
exit(0);
}
管道,FIFO或网络套接字都不能使用lseek,所以结果为(只测试了管道和普通文件):
注意:
- 由于偏移量可能是负值,因此比较
lseek
返回值时应当测试它是否等于-1,而不是是否小于0。 - 文件空洞问题,文件偏移量可以大于文件长度,因此下一次写将加长该文件,在文件中构成空洞,文件空洞并不要求占用磁盘上的存储区,具体处理方式与文件系统有关。
空洞文件测试:
#include "apue.h"
#include <fcntl.h>
char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
int
main(void)
{
int fd;
if ((fd = creat("file.hole", FILE_MODE)) < 0)
err_sys("creat error");
if (write(fd, buf1, 10) != 10)
err_sys("buf1 write error");
/* offset now = 10 */
if (lseek(fd, 16384, SEEK_SET) == -1)
err_sys("lseek error");
/* offset now = 16384 */
if (write(fd, buf2, 10) != 10)
err_sys("buf2 write error");
/* offset now = 16394 */
exit(0);
}
运行该程序可以得到:
6.read和write
read函数用于打开文件中读取数据:
#include<unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
/* 返回值:读取的字节数,若到尾部返回0;若出错返回-1 */
write函数用于向打开文件写数据:
#include<unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
/* 返回值:若成功,返回写入字节数;出错返回-1 */
7.文件共享
内核使用三种数据结构表示打开文件,它们间关系决定了文件共享方面一个进程对另一个进程的影响。
从上图可以看出,每个进程都有一个进程表项,里面存放文件表项的指针;文件表项包含当前文件状态(读、写、同步、阻塞等),偏移值和v-node表项指针;v-node表项包括v-node信息(文件类型和操作函数),以及inode(索引节点),索引节点包含文件信息(文件所有者、长度、指向实际磁盘位置等),Linux中没有v-node只有索引节点。
如果两个独立进程各种打开同一个文件,则有下图关系:
假定第一个进程在文件描述符3上打开该文件,另一个在文件描述符4上打开该文件,这样每个进程各自获得一个文件表项(这样做,可以让每个进程拥有自己的偏移值),但是这两个文件表项指向相同的v-node。
8.原子操作
Unix为文件IO提供原子操作,例如在打开文件时设置O_APPEND,这样使得内核每次写操作时都将当前偏移量设置为文件尾端。
同时提供pread
和pwrite
函数,他们分别执行的是lseek后调用read和write,但是在调用函数时,无法中断其定位和读写操作。
9.dup和dup2
下面两个操作可以用来复制一个现有文件描述符:
#include<unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
/* 返回值:成功返回文件描述符;失败返回-1*/
dup2是一个原子操作。
10.fcntl
fcntl函数可以改变已经打开文件的属性:
#include<fcntl.h>
int fcntl(int fd, int cmd, ... /* int arg */)
/* 返回值:若成功,则依赖于cmd,若出错,返回-1 */
fcntl函数有以下5个功能(cmd控制):
- 复制一个已有描述符
- 获取/设置文件描述符标志
- 获取/设置文件状态标志
- 获取/设置异步I/O所有权
- 获取/设置记录锁
11.其他函数
其余函数ioctl
和/dev/fd
没怎么见过,前者可以实现各种功能,后者通过打开文件的方式复制文件描述符
小结
这节内容挺多的,主要是对文件I/O进行介绍,很多函数功能介绍,不太容易记住,所以需要多实验。