文件I/O

文件I/O

人们常说Linux上一切皆文件,所以文件操作是Linux系统编程中重要的部分之一

文件的操作顺序一般为:打开(创建)、读/写、关闭文

打开文件 open()

通过系统调用open(),可以打开文件并获取文件描述符

1
2
3
4
5
6
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open (const char *name, int flags);
int open (const char *name, int flags,mode_t mode);

如果调用成功则返回指向所打开文件的文件描述符,文件打开方式根据flags的值来确定

flags参数

flags 参数由一个或多个标志位的按位或组合。它支持三种访问模式:

参数 描述
O_RDONLY 以只读方式打开
O_WRONLY 以只写方式打开
O_RDWR 以读写方式打开

flgs 还可以和下面这些值进行按位或运算,修改打开文件的行为:

参数 描述
O_APPEND 文件以追加模式打开。也就是说,每次写操作都从文件尾开始
O_CREAT 当文件不存在时,自动创建文件
O_DIRECT 打开文件用于直接I/O
O_CLOEXEC 在执行新的进程时,文件会自动关闭
O_DIRECTORY 如果name不是目录则调用失败(带此参数内部调用opendir())
O_NOATIME+ 读文件时,不会更新文件的最后访问时间
O_NONBLOCK 文件以非阻塞模式打开
O_SYNC 打开文件用于同步I/O
O_TRUNC 如果文件存在,且为普通文件,并且有写权限,则文件会被截断为0
1
2
3
int fd;
//以读写方式打开文件test,并将其截断为0
fd = open("test", O_RDWR | O_TRUNC);

mode参数

mode 参数只有在创建文件时才有意义,及给定O_CREAT参数时一定要添加mode参数,如果没有提供,结果则是未定义的

mode 参数通常是UNIX权位集合,比如八进制数0644,0777等

读文件 read()

最常见的读取文件时调用read(),定义如下:

1
2
#include <unistd.h>
ssize_t read (int fd, void *buf, size_t len);

每次调用read()函数,会从fd指向的文件的当前偏移开始读取len字节到buf中,成功返回读取的字节数,失败返回-1,并设置errno,下面是一个简单的实现:

1
2
3
4
5
6
7
8
char buf[64] = {0};
int ret = 0;

ret = read (fd, buf, sizeof(buf));
if (ret == -1)
{
/*error*/
}

这个实现存在两个经常被人忽略的问题,以至于有很多人这么使用read()

  1. 可能还没读取len字节,调用就返回了
  2. 可能产生某些可操作的错误,但是没有检查

返回值

使用read()时,还需考虑返回值为0的情况。当到达文件末尾时,read()返回0,在这种情况下,没有读到任何字节。EOF并不表示出错,它仅仅表示文件位置已经到达文件结尾,因此没有数据可读了

但是,如果调用要读取len个字节,但是没有一个字节刻度,调用就会阻塞

实际上,调用read()有很多可能的结果:

  • 调用返回值等于len。结果和预期一致
  • 调用返回值小于len,大于0。可读字节大于0小于len,在读取len之前到达EOF或是信号中断或读取出错
  • 没有可读数据,调用阻塞
  • 调用返回EOF没有更多可读数据
  • 调用返回-1,errno设为EINTR。表示在读取任何字节前收到阻塞信号,调用可以重新执行
  • 调用返回-1,errno设为EAGAIN。当前没有数据可用,读操作会阻塞(非阻塞模式才会发生)
  • 调用返回-1,errno为EINTR和EAGAIN之外的值,这表示更严重的错误

根据上面的返回值可以优化read()调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int ret;

while (len != 0 && ret = read (fd, buf, len) != 0)
{
if (ret == -1)
{
if (errno == EINTR)
{
continue;
}

perror ("read");
break;
}

len -= ret;
buf += ret;
}

写文件 write()

最基础常见的系统调用时write(),定义如下:

1
2
#include <unistd.h>
ssize_t write (int fd, const void *buf, size_t count);

write()调用会从文件描述符fd指向的文件的当前位置开始。将buf中至多count个字节写入文件中。不支持seek的文件总是从起始位置开始写

write()执行成功时,会返回写入的字节数并更新文件位置,出错时返回-1,并设置errno值,下面是简单实现:

1
2
3
4
5
6
7
8
const char *buf = "Hello World!";
ssize_t ret;

ret = write (fd, buf, strlen (buf));
if (ret == -1)
{
/* error */
}

追加模式

当以追加模式(参数设置O_APPEND)打开文件描述符时,写操作则是从文件尾开始的

多个进程写同一文件时此模式能保证数据不会被覆盖而造成数据丢失,追加模式可以理解成每次写请求之前都会更新文件位置,指向写入的数据末尾,这和write()无关,因为更新位置是自动完成的

关闭文件 close()

当完成对某个文件的操作后,可以通过系统调用close()取消文件描述符到对应文件的映射:

1
2
3
#include <unistd.h>

int close (int fd);

调用后,先前给定的文件描述符fd将不再有效,它的用法也很简单:

1
2
3
4
if (close (fd) == -1)
{
perror ("close");
}

文件I/O
https://carl-5535.github.io/2021/02/01/Linux系统编程/文件I-O/
作者
Carl Chen
发布于
2021年2月1日
许可协议