本篇文章为总结使用Linux文件I/O的心得体会
文件描述符(file descriptor, fd)
文件描述符为一非负整数, 所有执行I/O操作的系统调用都文件描述符来指代打开的文件。而在Unix中文件不仅仅指Windows中的文件, 它包括以下类型:
- 普通文件(regular file)
- 目录
- 符号链接
- 面向块的设备文件
- 面向字符的设备文件
- 管道(pipe)和命名管道(named pipe)
- 套接字(socket)
其中设备文件与I/O设备以及集成到内核中的设备驱动程序相关。而管道和套接字是用于进程间通信的特殊文件。
标准文件描述符:
文件描述符 | 用途 | POSIX名称 | stdio流 |
---|---|---|---|
0 | 标准输入 | STDIN_FILENO | stdin |
1 | 标准输出 | STDOUT_FILENO | stdout |
2 | 标准错误 | STDERR_FILENO | stderr |
以上三种文件描述符, shell启动时将自动打开, 凡是利用shell启动的程序将继承文件描述符的副本, 通常这三个文件描述符始终都是打开的。
文件描述符与打开文件之间的关系
多个文件描述符可同时指向一个打开文件, 且这些文件描述符可在不同或相同的进程中打开。
操作文件
需要强调的是, 对于已打开的每个人间, 内核都维护有一个文件偏移量, 这决定了下一次读或写操作的起始位置。 读和写操作会隐式修改文件偏移量。
创建文件
int fd_create = open("fileio.log", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (-1 == fd_create){
printf("create file failed!");
}
读文件
以下代码展示了读取一个已经存在的文件:
int fd_create = open("fileio.log", O_RDONLY, S_IRUSR | S_IWUSR);
if (-1 == fd_create){
printf("create file failed!");
}
char szRead[128] = {0};
read(fd_create,szRead, sizeof(szRead));
另外,对于普通文件, 一般情况下,一次read()调用所读取的字节数小于请求的字节数, 很有可能时因为当前读取位置靠近文件尾部
。
如果需要在指定位置读取, 可使用lseek, 基本用法如下:
// 在文件开始第三个字节处开始读取(当whence为SEEK_SET时, offset不能为负值)
lseek(fd_create, 3, SEEK_SET);
// 在文件末尾处开始读取
lseek(fd_create, 0, SEEK_END);
// 从文件倒数第三个字节开始读取
lseek(fd_create, -3, SEEK_END);
// 在当前位置倒数第三个字节开始读取
lseek(fd_create, -3, SEEK_CUR);
char szRead[128] = {0};
read(fd_create,szRead, sizeof(szRead));
注意,使用lseek时, offset为负数仅仅用于whence为SEEK_END或SEEK_CUR时, 不能用于SEEK_SET,SEEK_SET时, offset必须为正整数。
同时可以利用pread在指定位置进行读取, 基本用法如下:
char szRead[128] = {0};
pread(fd_create, szRead, sizeof(szRead), 3);
pread仅仅在指定位置读取, 并未更改其当前偏移位置。
pread 相当于将以下代码纳入了同一原子操作:
off_t orig = lseek(fd, 0, SEEK_CUR);
lseek(fd, offset, SEEK_SET);
read(fd, buf, len);
lseek(fd, orig, SEEK_SET);
此API为多线程调用提供了用武之地。 相似的接口还有pwrite()
写文件
以下代码演示了打开已有文件后, 将其清空(O_TRUNC)后往文件中写入内容:
int fd_create = open("fileio.log", O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
if (-1 == fd_create){
printf("create file failed!");
}
char szRead[128] = "I Love Programming!";
write(fd_create,szRead, strlen(szRead));
在指定位置进行写文件类似于在指定位置读文件, 可利用lseek结合write, 也可使用pwrite()。
需要强调一点的是, 在指定了O_APPEND标志位使用lseek然后调用write, 将不会如预期在lseek指定位置进行写入操作。为什么会出现这样的情况呢? 因为当指定O_APPEND标志位后, Linux将会把文件偏移量的移动和数据写操作纳入同一原子操作。
其他标志位
如何以独占地方式创建一个文件?
当同时指定O_EXCL 与 O_CREATE 作为open()的标志位时,如果open的文件已经存在将返回一个错误。这保证了对文件是否存在的检查和创建文件属于同一原子操作
。
正确使用读写标志位
O_RDWR 不等同 O_RDONLY | O_WRONLY
打开文件时清空文件
使用O_TRUNC标志位
以非阻塞的方式打开文件
在打开文件时指定O_NONBLOCK标志, 此标志仅能用于 管道、FIFO、套接字、设备(比如终端、伪终端)都支持非阻塞模式。对于管道和套接字因其文件描述符不是由open得到, 所以设置其O_NONBLOCK只能使用fcntl()。普通文件指定O_NONBLOCK将会被忽略, 因为普通文件的操作因为内核缓冲区保证了其不会阻塞
。
删除文件
unlink接口提供删除文件的一种系统调用, 此调用是为了减少文件链接数, 删除了相应的目录项。 只有当链接数为0时, 文件才被真正删除。
文件控制操作: fcntl()
fcntl() 系统调用对一个打开的文件描述符执行一系列控制操作
#include <fcntl.h>
// Return on success depends on cmd, or -1 on error
int fcntl(int fd, int cmd, ...);
获取文件打开标志
int flags = fcntl(fd, F_GETFL);
if (flags == -1){
printf("occur error!");
}
// 判断文件是否以同步方式打开
if (flags & O_SYNC){
printf("writes are synchronized\n");
}
int accessMode = flags | O_ACCMODE;
if (accessMode == O_WRONLY || accessMode == O_RDWR){
printf("file is writable\n");
}
可以使用fcntl() 的 F_SETFL 命令来修改打开文件的某些状态标志。 允许更改的标志有O_APPEND、O_NONBLOCK、O_NOATIME、O_ASYNC和O_DIRECT。 系统将忽略对其他标志的修改操作。
例如, 对打开的文件添加O_APPEND标志:
int flags = fcntl(fd, f_GETFL);
if (flags == -1){
printf("occur error!\n");
}
flags |= O_APPEND;
fcntl(fd, F_SETFL, flags);
临时文件
创建临时文件常用的两个系统调用为mkstemp() 和 tmpfile()。
基于调用者提供的模板, mkstemp()函数生成唯一文件名并打开该文件, 并返回文件描述符:
#include <stdlib.h>
// Returns file descriptor on success, or -1 on error.
int mkstemp(char * template);
mkstemp() 函数的示例代码:
char templates[] = "tmp/somestringXXXXXX";
int fd = mkstemp(templates);
if (fd ==-1){
printf("mkstemp failed!\n");
}
printf("Generated filename was: %s\n", templates);
// 文件名将马上消失, 但是文件需要等到调用close()后才会被删除
// 同时进程消亡时, fd亦会被自动关闭, 文件自然被删除。
unlink(templates);
close(fd);
另一个函数tmpfile()也会创建一个名称唯一的临时文件, 并以读写的方式打开。 并且返回一个文件流供fread、fwrite等接口调用。 文件流关闭后将自动删除临时文件。 因为tmpfile()在内部打开文件后调用了接口unlink()来删除文件名。
应尽量避免使用tmpnam()、tempnam()、mktemp()。尽管这些接口有也能生成唯一的文件名, 但存在安全漏洞。