系统编程-文件I/O操作

我们都听过Linux下一切皆文件,实际上无论是普通的文件读写,还是网络IO读写,它们都有着类似的操作过程。本文通过基本文件IO操作,来了解Linux“一切文件”的读写。当然过程中穿插着很多其他内容。

文件I/O过程

在介绍具体的函数使用之前,我必须说明一下文件I/O的基本过程。它们类似过程如下:

  • 以某种模式打开文件,获取一个文件描述符
  • 对文件进行读写
  • 不需要时,关闭文件描述符

文件描述符是什么?你可以认为是一个对文件进行操作的凭据,你只有通过它才能对文件进行读写。它是一个非负整数。通常0是标准输入,1是标准输出,2是标准错误(参考《》)。正是有了它们,你的简单程序才可以从控制台读入数据,输出日志,输出错误打印等等。

记得很小的时候,家里连压水的工具都没有,需要用水的时候,都是用一个小点的桶从井里打水。

类比文件I/O操作,打开井盖,拿到绑着绳子的水桶,就像是打开文件,获取文件描述符;而打水的过程,就像对文件进行读写;最后需要的时候,又把桶放回去,并盖上井盖;而这就像关闭文件描述符。

当然了,如果嫌弃里面的小桶打水太慢,有的人可能会用一担大桶用来装水,装满一担后,再挑走使用。而这个过程就像使用了缓冲。(参考《不可不知的三种缓冲》)。

说了这么多废话,文件I/O到底怎么操作呢?本文介绍的是不带缓冲的I/O函数。

打开文件,获取文件描述符

主要函数:

1
2
#include<fcntl.h>
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
#include<unisdt.h>
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.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//来源:公众号【编程珠玑】
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include <errno.h>
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
2
perror("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
2
open ok
write failed: Bad file descriptor

以只读方式打开,却尝试写,自然是会写失败了。因此对应的操作要设置对应的标志位,否则会失败。

总结

以上就是文件I/O的基本操作。关键就三个步骤:

  • 以某种模式打开
  • 操作
  • 关闭

错误处理原则:
返回-1,则出错,会设置errno,可通过perror或者strerror打印错误信息。

守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!