我们都听过Linux下一切皆文件,实际上无论是普通的文件读写,还是网络IO读写,它们都有着类似的操作过程。本文通过基本文件IO操作,来了解Linux“一切文件”的读写。当然过程中穿插着很多其他内容。
文件I/O过程
在介绍具体的函数使用之前,我必须说明一下文件I/O的基本过程。它们类似过程如下:
- 以某种模式打开文件,获取一个文件描述符
- 对文件进行读写
- 不需要时,关闭文件描述符
文件描述符是什么?你可以认为是一个对文件进行操作的凭据,你只有通过它才能对文件进行读写。它是一个非负整数。通常0是标准输入,1是标准输出,2是标准错误(参考《》)。正是有了它们,你的简单程序才可以从控制台读入数据,输出日志,输出错误打印等等。
记得很小的时候,家里连压水的工具都没有,需要用水的时候,都是用一个小点的桶从井里打水。
类比文件I/O操作,打开井盖,拿到绑着绳子的水桶,就像是打开文件,获取文件描述符;而打水的过程,就像对文件进行读写;最后需要的时候,又把桶放回去,并盖上井盖;而这就像关闭文件描述符。
当然了,如果嫌弃里面的小桶打水太慢,有的人可能会用一担大桶用来装水,装满一担后,再挑走使用。而这个过程就像使用了缓冲。(参考《不可不知的三种缓冲》)。
说了这么多废话,文件I/O到底怎么操作呢?本文介绍的是不带缓冲的I/O函数。
打开文件,获取文件描述符
主要函数:1
2
int open(const char *pathname, int flags, mode_t mode);
参数解释:
- pathname 文件名
- flags 打开选项
这里的文件名应该不用过多解释,但是flags需要做一些说明,
它须指定以下五个中的一个:
- O_RDONLY 只读
- O_WRONLY 只写
- O_RDWR 可读可写
- O_EXEC 执行打开
- O_SEARCH 搜索打开(针对目录)
而下面的选项是可选的:
- O_APPEND 写时追加到文件末尾
- O_CREAT 文件不存在时创建,且必须指定文件访问权限位
- O_TRUNC 文件存在时,且以只写,或者读写方式打开,则截断长度为0
- ……
当打开成功时返回文件描述符,否则返回-1,并且设置errno。
读写操作
读写操作主要有两个函数:1
2
3
ssize_t read(int fd, void *buf,size_t nbytes);
ssize_t write(int fd, const void *buf,size_t nbytes);
参数说明:
- fd 文件描述符
- buf 要读写的内容
- nbytes 读写的内容大小
这里的fd就是前面拿到的文件描述符。篇幅有限, 本文暂不涉及具体的读写介绍。
关闭文件
调用close函数即可,它的参数是前面打开的时候获得的文件描述符1
2#include <unistd.h>
int close(int fd);
成功返回0,失败则返回-1,并且会设置errno。
实例
以上都太过理论化了,那么理论结合实际来看看,究竟是怎样的。
打开一个不存在的文件
这是最简单的情况,现在假设,当前目录下没有test.txt1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//来源:公众号【编程珠玑】
int main(void)
{
int fd = open("test.txt",O_WRONLY);
if(-1 == fd)
{
//perror("open failed:");
printf("open failed:%s\n", strerror(errno));
return -1;
}
printf("open ok\n");
char test[] = "wechat:shouwangxiansheng\n";
ssize_t len = write(fd,test,sizeof(test));
if(-1 == len)
{
perror("write failed");
}
close(fd);
}
运行报错:1
open failed: No such file or directory
还记得前面说的,如果出错就会设置errno吗?当open返回-1(很多系统接口类似)时,就会设置errno,这个时候就可以调用perror接口打印对应的错误信息。便于我们定位问题。即:1
2perror("open failed:");
printf("open failed:%s\n", strerror(errno));
上面两种方式都可以打印出错误信息,区别在于,前者输出到标准错误,后者输出到标准输出。
还记得在《不可不知的三种缓冲》中说的吗?标准错误通常是不带缓冲的。
打开一个文件,不存在时创建
既然不存在时,会打开失败,那么不存在就创建好了,这就用到了O_CREATE标志。因此修改open函数那一行:1
int fd = open("test.txt",O_WRONLY | O_CREAT);
运行结果:1
open ok
并且会在test.txt发现写入的内容。
注意到,多个标志使用|构成flags参数。
打开一个文件,存在时截断
好了,前面已经实现了文件不存在时,创建,存在时也可以正常打开,如果存在时,又不想要原先的内容?那就需要用到O_TRUNC标志。
修改open行如下:1
int fd = open("test.txt",O_RDONLY | O_CREAT | O_TRUNC);
现在假设test.txt文件存在,且里面有内容,再次运行后,发现打开文件正常,且内容只有新加入的,而没有之前存在的。
在打开的文件后追加内容
如果想在打开的文件后追加内容,那么可以使用O_APPEND标志:1
int fd = open("test.txt",O_RDONLY | O_CREAT | O_APPEND);
这样如果原来test.txt中有内容,则可以往文件中追加内容。
只读打开的文件进行写操作
前面提到了5个打开标志,如果以只读方式尝试写会怎样?
修改open行:1
int fd = open("test.txt",O_RDONLY);
你会发现:1
2open ok
write failed: Bad file descriptor
以只读方式打开,却尝试写,自然是会写失败了。因此对应的操作要设置对应的标志位,否则会失败。
总结
以上就是文件I/O的基本操作。关键就三个步骤:
- 以某种模式打开
- 操作
- 关闭
错误处理原则:
返回-1,则出错,会设置errno,可通过perror或者strerror打印错误信息。