Linux编程入门-文件I/O

碎碎念

获取glibc版本:

c++
1
2
#include <gnu/libc-version.h>
const char *gnu_get_libc_version(void);

错误处理:

c++
1
2
3
void perror(const char *s); //打印错误信息
#include <string.h>
char *strerror(int errnum); //根据errnum错误码获取描述字符串

通用I/O模型

C语言标准中的文件操作这里略。

open

打开一个已存在的文件或创建并打开一个新文件:

c++
1
2
3
4
5
6
7
#include <sys/stat.h>
#include <fcntl.h>
int open(
const char *pathname, //要打开的文件 或符号链接
int flags, //访问模式
...
); //成功返回一文件描述符 错误-1 错误标志errno

新建文件的访问权限可能还受进程umask和父目录默认访问控制列表的影响。常用文件访问标志如下,其中前三个只能指定一种。

标志 含义
O_RDONLY 只读方式打开
O_WRONLY 只写方式打开
O_RDWR 读写方式打开
O_CLOEXEC 设置close-on-exec标志
O_CREAT 文件不存在则创建
O_DIRECTORY 若不是目录则失败
O_EXCL 当与O_CREAT结合使用时,若文件已存在则不打开文件并返回错误
O_NOCTTY 若打开的文件为中断设备,则使其不要称为控制终端
O_NOFOLLOW 不对符号链接进行解引用
O_TRUNC 清空文件
O_APPEND 文件尾追加数据
O_ASYNC I/O操作可行时产生信号通知进程,略
O_DIRECT 无系统缓冲
O_DSYNC 提供同步的I/O数据完整性
O_NOATIME read时不修改文件最近访问时间
O_NONBLOCK 非阻塞方式打开
O_SYNC 同步方式写文件
O_PATH

常见errno错误如下:

错误码 含义
EACCES 无法访问文件
EISDIR 打开目录进行写操作
EMFILE 进程已打开的文件描述符数达到进程资源限制上限
ENFILE 文件打开数量达到系统允许的上限
ENOENT 文件不存在且未指定创建标志,或指定路径目录不存在,或为空符号链接
EROFS 文件隶属于只读文件系统,企图写打开文件
ETXTBSY 所指定文件为正在运行的可执行文件,终止后即可

大文件支持(LFS)指对2GB以上文件进行读写,此时open应添加选项O_LARGEFILE标志,否则返回错误。

creat

创建并打开一个新文件,已被open替代。

c++
1
2
3
4
5
int creat( //注意没有“e”
const char *path,
mode_t mode
);
//等同于O_WRONLY|O_CREAT|O_TRUNC的open

read

从文件描述符指代的打开文件中读取数据:

c++
1
2
3
4
5
6
#include <unistd.h>
ssize_t read(
int fd, //文件描述符
void buf[.count], //存放输入数据的缓冲区地址
size_t count //最多读取的字节数
); //成功返回实际读取字节数 EOF返回0 错误-1

write

将数据写入一个已打开的文件中:

c++
1
2
3
4
5
ssize_t write(
int fd,
const void buf[.count],
size_t count
);

close

关闭一个打开的文件描述符,释放回调用进程,供该进程继续使用。进程终止时自动关闭已打开的所有文件描述符。

c++
1
2
3
int close(
int fd
);

lseek

调整文件偏移量。若某文件包含N字节数据,从0到N-1,则SEEK_SET为0,SEEK_END为N。

c++
1
2
3
4
5
off_t lseek(
int fd, //文件描述符
off_t offset, //偏移量
int whence //参照基点
); //成功返回新偏移量

常用参照基点如下:

基点 含义
SEEK_SET 文件头部
SEEK_CUR 当前偏移量
SEEK_END 文件尾部

当whence为SEEK_SET时offset必须为非负数,其他时可正可负。常见errno错误码有ESPIPE,表示lseek不能应用于管道、FIFO、套接字或终端。

当往文件结尾后一段距离处写入数据时,中间的空间称为文件空洞,读取时返回空字节。

ioctl

用于通用模型以外的操作:

c++
1
2
3
4
5
6
#include <sys/ioctl.h>
int ioctl(
int fd, //某文件或设备的文件描述符
unsigned long op, //控制操作
... //参数
);

文件描述符与文件控制

fcntl

对一个打开的文件描述符执行一系列控制操作:

c++
1
2
3
4
5
int fcntl(
int fd,
int op,
...
);

例如获取或修改打开文件的访问模式和状态标志,即在open时设置的。

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int flags=fcntl(fd,F_GETFL),accessMode;
if(flags==-1){
//...
};
if(flags&O_SYNC){
//...
};
accessMode=flags&O_ACCMODE;
if((accessMode==O_WRONLY)||(accessMode==O_RDWR)){
//...
};
flags|=O_APPEND;
if(fcntl(fd,F_SETFL,flags)==-1){
//...
};

dup/dup2/dup3

复制一个打开的文件句柄。其中0为标准输入流,1为标准输出流,2为标准错误流。系统保证返回新描述符一定是编号值最低的未用文件描述符。对于dup2,当目标文件描述符已被占用,则自动先关闭再复制。当dup2的fd无效,则返回EBADF,不关闭fd2。dup3的flags常用值有O_CLOEXEC,含义为为新文件描述符设置close-on-exec标志。

c++
1
2
3
4
5
6
7
8
9
10
11
12
int dup(
int fildes //被复制的打开的文件描述符
); //返回新描述符
int dup2(
int fildes, //被复制的
int fildes2 //目标
); //返回fd2
int dup3(
int oldfd,
int newfd,
int flags
); //系统调用行为

例如:

c++
1
2
3
4
5
6
7
8
9
10
//法一
newfd=dup(1); //获得3
close(2);
newfd=dup(1); //获得2

//法二
dup2(1,2);

//法三
newfd=fcntl(oldfd,F_DUPFD,startfd); //返回大于等于startfd的最小未用操作符编号

特殊I/O

pread/pwrite

再指定偏移量位置进行文件I/O操作,不改变文件当前偏移量:

c++
1
2
3
4
5
6
7
8
9
10
11
12
ssize_t pread(
int fd,
void buf[.count],
size_t count,
off_t offset
);
ssize_t pwrite(
int fd,
const void buf[.count],
size_t count,
off_t offset
);

readv/writev

分别实现分散输入和集中输出,这俩都原子操作。iov数组成员个数上限通过sysconf的_SC_IOV_MAX获取。

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/uio.h>
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Size of the memory pointed to by iov_base. */
};
ssize_t readv(
int fd,
const struct iovec *iov, //缓冲区数组
int iovcnt //iovec成员个数
); //成功返回读取字节数 EOF则0
ssize_t writev(
int fd,
const struct iovec *iov,
int iovcnt
);

preadv/pwritev

在指定文件偏移量处进行分散-集中I/O:

c++
1
2
3
4
5
6
7
8
9
10
11
ssize_t preadv(
int fd,
const struct iovec *iov,
int iovcnt,
off_t offset);
ssize_t pwritev(
int fd,
const struct iovec *iov,
int iovcnt,
off_t offset
);

truncate/ftruncate

将文件大小设为指定值,不修改文件偏移量。若文件当前长度大于length,则丢弃超出部分,小于则在文件尾部添加一系列空字节或称一个文件空洞。其中truncate需要路径名字符串,要求组成路径名的各目录拥有可执行权限。ftruncate需要文件描述符,只需文件写权限。

c++
1
2
3
4
5
6
7
8
int truncate(
const char *path, //路径名字符串 要求有写权限
off_t length
);
int ftruncate(
int fd, //写方式打开的文件描述符
off_t length
);

临时文件

mkstemp

mkstemp生成一个唯一文件名并使用O_EXCL标志,保证调用者以独占方式访问文件。文件拥有者对该函数建立的文件有读写权限,其他用户没有任何权限。

c++
1
2
3
int mkstemp(
char *template //路径名 最后6个字符必须为“XXXXXX”
); //成功返回文件描述符 否则-1

例子有:

c++
1
2
3
4
5
6
7
8
9
10
int fd;
char template[]="/tmp/somestringXXXXXX";
fd=mkstemp(template);
if(fd==-1)
errExit("mkstemp");
printf("%s\n",template); //文件名
unlink(template); //删除名字
//各种文件I/O
ifclose(fd)==-1) //关闭并删除文件
errExit("close");

tmpfile

创建一个名称唯一的临时文件,用O_EXCL标志以读写方式打开。返回的文件流在关闭后自动删除临时文件。

c++
1
FILE *tmpfile(void); //成功返回文件流

缓冲

setvbuf/setbuf/setbuffer

setvbuf控制stdio库使用缓冲的形式。当buf不为NULL,,则其指向size大小的内存块作为stream的缓冲区,其中缓冲区需要用malloc等函数以动态或静态方式在堆中分配空间。当buf为NULL时忽略size参数,stdio库为stream自动分配一个缓冲区,除非选择非缓冲I/O。

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int setvbuf(
FILE *restrict stream, //要修改的文件流
char *restrict buf,
int type,
size_t size
); //成功0 失败非0
void setbuf(
FILE *restrict stream,
char *restrict buf
);
void setbuffer(
FILE *restrict stream,
char buf[restrict .size],
size_t size
);

type参数指定缓冲类型,常用值如下:

含义
_IONBF 不缓冲。每个stdio库函数立即调用writeread,忽略buf和size参数。stderr默认该类型。
_IOLBF 行缓冲。对于输出流,在输出一个换行符前缓冲数据,除非缓冲区已满。对于输入流,每次读一行数据。终端设备流默认该类型。
_IOFBF 全缓冲。单次读写数据大小与缓冲区相同。磁盘流默认该类型。

setbufsetbuffer基于setvbuf实现,分别相当于:

c++
1
2
setvbuf(stream,buf,(buf!=NULL)?_IOFBF:_IONBF,BUFSIZ); //BUFSIZ在stdio中一般定义为8192
setvbuf(stream,buf,(buf!=NULL)?_IOFBF:_IONBF,size);

fflush

对于输出流,强制将某stdio输出流中的数据刷新到内核缓冲区中。当stream参数为NULL,则该函数刷新与输出流相关的所有stdio缓冲区。对于输入流,则丢弃已缓冲的输入数据。

c++
1
2
3
int fflush(
FILE *_Nullable stream
); //成功0 失败EOF

例如当stdin和stdout指向一终端,则从stdin读入输入时,都将先隐含调用一次fflush

fsync/fdatasync

同步I/O完成指的是某一I/O操作要么已完成到磁盘的数据传递,要么不成功。同步I/O完成分为同步I/O数据完整性完成和同步I/O文件完整性完成。前者对于读操作而言意味着被请求的文件数据已被从磁盘传递给进程,写操作而言意味着写请求的所有数据和获取数据所需的所有文件元数据属性已传递至磁盘完毕。后者区别在于将所有发生更新的文件元数据都传递到磁盘上,无论在后续读操作中是否需要。

fsync将使缓冲数据和与打开的文件描述符fd相关的所有元数据刷新到磁盘上,强制使文件处于同步I/O文件完整性完成状态,并只在传递完成后返回。fdatasync运作类似fsync,但强制处于同步I/O文件数据完整性完成状态。sync_file_range当刷新文件时还可指定待刷新的文件区域和标志,以及控制在写磁盘时是否阻塞,这里不讲。

c++
1
2
3
4
5
6
7
#include <unistd.h>
int fsync(
int fd
); //成功0 失败-1
int fdatasync(
int fd
);

sync

sync使包含更新文件信息的所有内核缓冲区刷新到磁盘上,包含数据块、指针块和元数据等,仅在所有数据已传递到磁盘或高速缓存时返回。

c++
1
void sync(void);

若内容发生变化的内核缓冲区30秒内未经显式方式同步到磁盘上,则一条长期运行的内核线程会确保将其刷新到磁盘上。下面该文件规定了该内核线程刷新前脏缓冲区必须达到的年龄,单位为1%秒。

plaintext
1
2
$ cat /proc/sys/vm/dirty_expire_centisecs
3000

open时指定O_SYNC标志,则使所有后续输出同步,写操作遵从同步I/O文件完整性完成,类似fsync,即每个write会自动将文件数据和元数据刷新到磁盘上,此时性能影响极大。不过现代磁盘驱动器内置大型高速缓存,若用以下命令禁用磁盘高速缓存,则性能影响更为极端。

bash
1
hdparm -W0

使用O_DSYNC标志时,写操作遵从同步I/O数据完整性完成,类似fdatasync。O_RSYNC标志与O_DSYNC或O_SYNC标志配合使用,表示将对写操作的作用同样应用到读操作上。

posix_fadvise

允许进程就自身访问文件数据时可能采取的模式通知内核,内核可根据该函数提供的信息优化对缓冲区高速缓存的使用,提高进程和整个系统的性能。

c++
1
2
3
4
5
6
7
#include <fcntl.h>
int posix_fadvise(
int fd, //文件描述符
off_t offset, //文件区域起始偏移量
off_t size, //区域大小 单位字节 0表示从offset到文件结尾
int advice //访问模式
);

advice参数的常用值有:

含义
POSIX_FADV_NORMAL 无建议,默认行为。文件预读窗口大小设为默认值128KB。
POSIX_FADV_SEQUENTIAL 进程预计从低偏移量到高偏移量读取数据。将文件预读窗口置为默认值两倍。
POSIX_FADV_RANDOM 进程预计以随机顺序访问数据。禁用文件预读。
POSIX_FADV_WILLNEED 进程预计在不久将来访问指定文件区域。将指定文件区域文件数据预先填充到缓冲区高速缓存中。效果同readahead
POSIX_FADV_DONTNEED 进程预计在不久将来不访问指定文件区域。若底层设备未挤满排队的写操作请求,则内核对指定区域中已修改页面刷新并释放该区域高速缓存页面。建议该操作前用syncfdatasync以防设备拥塞而不成功。
POSIX_FADV_NOREUSE 进程预计一次性访问指定文件区域,不复用。内核对指定区域访问一次后即释放,Linux貌似没实现该功能。

缓冲I/O总结

在一般情况下,输出时首先通过stdio库函数调用(如printffputc等)将用户数据传递到stdio缓冲区(位于用户态内存区)。缓冲区填满时stdio库调用write将数据传递到内核高速缓冲区(位于内核她内存区)。最终内核发起磁盘操作,将数据传递到磁盘。

使用setbuf(stream,NULL);时,每个stdio库函数自动刷新到用write。使用fflush();时,强制将stdio缓冲区内容刷新到用write。使用open(path,flags|O_SYNC,mode);时,使每次write都直接由内核发起写操作。使用fsyncfdatasyncsync时,强制将内核缓冲区高速缓存内容刷新到内核发起写操作。

直接I/O

执行磁盘I/O时操作缓冲区高速缓存,从用户空间直接将数据传递到文件或磁盘设备,称为直接I/O或裸I/O。直接I/O使用方法是在open时指定O_DIRECT选项。有些非UNIX文件系统可能不支持,如VFAT等。

直接I/O执行时必须遵循一些规则,否则导致EINVAL错误,其中物理块大小通常为512B:

  • 文件系统支持直接I/O。
  • 缓冲区内存边界必须对齐为块大小整数倍。
  • 数据传输的开始点,即文件或设备的偏移量,必须是块大小整数倍。
  • 待传输数据长度必须是块大小整数倍。

例如:

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int fd;
ssize_t numRead;
size_t length, alignment;
off_t offset;
char* buf;
if (argc < 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s file length [offset [alignment]]\n", argv[0]);
length = getLong(argv[2], GN_ANY_BASE, "length");
offset = (argc > 3) ? getLong(argv[3], GN_ANY_BASE, "offset") : 0;
alignment = (argc > 4) ? getLong(argv[4], GN_ANY_BASE, "alignment") : 4096;
fd = open(argv[1], O_RDONLY | O_DIRECT);
if (fd == -1)
errExit("open");
buf = memalign(alignment * 2, length + alignment);
if (buf == NULL)
errExit("memalign");
buf += alignment;
if (lseek(fd, offset, SEEK_SET) == -1)
errExit("lseek");
numRead = read(fd, buf, length);
if (numRead == -1)
errExit("read");
printf("Read %ld bytes\n", (long)numRead);
exit(EXIT_SUCCESS);

fileno/fdopen

系统调用和标准C语言库函数可混合使用。fileno通过一个文件流返回相应文件描述符,随后可在readwritedupfcntl等系统调用中正常使用该文件描述符。fdopen通过一个文件描述符创建一个使用该描述符进行文件I/O的流。fdopen的mode参数含义与fopen的mode参数相同,且当该参数与文件描述符的访问模式不一致时失败。

c++
1
2
3
4
5
6
7
int fileno(
FILE *stream
);
FILE *fdopen(
int fildes,
const char *mode
);

时刻注意I/O系统调用直接将数据传递到内核缓冲区高速缓存,而stdio库函数等到用户空间流缓冲区填满后才传递。