抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

文件 I/O 指的是对文件的输入/输出操作,说白了就是对文件的读写操作; Linux 下一切皆文件,文件作为 Linux 系统设计思想的核心理念,在 Linux 系统下显得尤为重要。

1 简单的文件 IO 示例

一个通用的 IO 模型通常包括打开文件、读写文件、关闭文件这些基本操作,主要涉及到 4 个函数: open()、 read()、 write()以及 close()。

下面是一个简单地文件读写示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
char buff[1024];
int fd1, fd2;
int ret;

/* 打开源文件 src_file(只读方式) */
fd1 = open("./src_file", O_RDONLY);
if (-1 == fd1)
return fd1;

/* 打开目标文件 dest_file(只写方式) */
fd2 = open("./dest_file", O_WRONLY);
if (-1 == fd2) {
ret = fd2;
goto out1;
}

/* 读取源文件 1KB 数据到 buff 中 */
ret = read(fd1, buff, sizeof(buff));
if (-1 == ret)
goto out2;

/* 将 buff 中的数据写入目标文件 */
ret = write(fd2, buff, sizeof(buff));
if (-1 == ret)
goto out2;
ret = 0;

out2:
/* 关闭目标文件 */
close(fd2);

out1:
/* 关闭源文件 */
close(fd1);
return ret;
}

从源文件 src_file 中读取 1KB 数据,然后将其写入到目标文件 dest_file 中(这里假设当前目录下这两个文件都是存在的);

在进行读写操作之前,首先调用 open 函数将源文件和目标文件打开,成功打开之后再调用 read 函数从源文件中读取 1KB 数据,然后再调用 write 函数将这 1KB 数据写入到目标文件中,至此,文件读写操作就完成了,读写操作完成之后,最后调用 close 函数关闭源文件和目标文件。

2文件描述符

调用 open 函数会有一个返回值,譬如示例代码 中的 fd1 和 fd2,这是一个 int 类型的数据,在 open函数执行成功的情况下,会返回一个非负整数该返回值就是一个文件描述符(file descriptor),这说明文件描述符是一个非负整数;对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引

当调用 open 函数打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符,用于指代被打开的文件,所有执行 IO 操作的系统调用都是通过文件描述符来索引到对应的文件,譬如示例代码中,当调用 read/write 函数进行文件读写时,会将文件描述符传送给 read/write 函数,所以在代码中, fb1 就是源文件 src_file 被打开时所对应的文件描述符,而 fd2 则是目标文件 dest_file 被打开时所对应的文件描述符

一个进程可以打开多个文件,但是在 Linux 系统中,一个进程可以打开的文件数是有限制,并不是可以无限制打开很多的文件。一个进程最多可以打开 1024 个文件

对于一个进程来说,文件描述符是一种有限资源,文件描述符是从 0 开始分配的,譬如说进程中第一个被打开的文件对应的文件描述符是 0、第二个文件是 1、第三个文件是 2、第 4 个文件是 3……以此类推,所以由此可知,文件描述符数字最大值为 1023(0~1023)。

当我们在程序中,调用 open 函数打开文件的时候,分配的文件描述符一般都是从 3 开始, 0、 1、 2 这三个文件描述符已经默认被系统占用了,分别分配给了系统标准输入(0)、标准输出(1)以及标准错误(2)。

  • 标准输入一般对应的是键盘,可以理解为 0 便是打开键盘对应的设备文件时所得到的文件描述符;
  • 标准输出一般指的是 LCD 显示器,可以理解为 1 便是打开 LCD 设备对应的设备文件时所得到的文件描述符;
  • 标准错误一般指的也是 LCD 显示器。

3open打开文件

在 Linux 系统中要操作一个文件,需要先打开该文件,得到文件描述符,然后再对文件进行相应的读写操作(或其他操作),最后在关闭该文件; open 函数用于打开文件,当然除了打开已经存在的文件之外,还可以创建一个新的文件,函数原型如下所示:

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

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

在 Linux 系统下,可以通过 man 命令(也叫 man 手册)来查看某一个Linux系统调用的帮助信息, man命令可以将该系统调用的详细信息显示出来,譬如函数功能介绍、函数原型、参数、返回值以及使用该函数所需包含的头文件等信息;man更像是一份帮助手册,所以也把它称为man 手册,当我们需要查看某个系统调用的功能介绍、使用方法时,不用在上网到处查找,直接通过 man 命令便可以搞定, man命令用法如下所示:

1
man 2 open  #查看 open 函数的帮助信息

man 命令后面跟着两个参数,数字 2 表示系统调用, man 命令除了可以查看系统调用的帮助信息外,还可以查看 Linux 命令(对应数字 1)以及标准 C 库函数(对应数字 3)所对应的帮助信息;最后一个参数 open 表示需要查看的系统调用函数名。

在应用程序中调用 open 函数即可传入 2 个参数(pathname、 flags)、也可传入 3 个参数(pathname、 flags、 mode),但是第三个参数 mode 需要在第二个参数 flags 满足条件时才会有效;在应用程序中使用 open 函数时,需要包含 3 个头文件“ #include <sys/types.h>”、“#include <sys/stat.h>”、“#include <fcntl.h>”。

  • pathname: 字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径)信息,譬如: “./src_file”(当前目录下的 src_file 文件)、 **”/home/dengtao/hello.c”**等; 如果 pathname 是一个符号链接,会对其进行解引用。

  • flags: 调用 open 函数时需要提供的标志,包括文件访问模式标志以及其它文件相关标志,这些标志使用宏定义进行描述,都是常量, open 函数提供了非常多的标志,我们传入 flags 参数时既可以单独使用某一个标志,也可以通过位或运算(|)将多个标志进行组合。这些标志介绍如下:

    标志 用途 说明
    O_RDONLY 以只读方式打开文件 这三个是文件访问权限标志,传入的flags 参数中必须要包含其中一种标志,而且只能包含一种,打开的文件只能按照这种权限来操作,譬如使用了 O_RDONLY 标志,就只能对文件进行读取操作,不能写操作。
    O_WRONLY 以只写方式打开文件 同上
    O_RDWR 以可读可写方式打开文件 同上
    O_CREAT 如果 pathname 参数指向的文件不存在则创建此文件 使用此标志时,调用 open 函数需要传入第 3 个参数 mode,参数 mode 用于指定新建文件的访问权限,稍后将对此进行说明。open 函数的第 3 个参数只有在使用了 O_CREAT 或 O_TMPFILE 标志时才有效。
    O_DIRECTORY 如果 pathname 参数指向的不是一个目录,则调用 open 失败
    O_EXCL 此标志一般结合 O_CREAT 标志一起使用,用于专门创建文件。在 flags 参数同时使用到了 O_CREAT 和O_EXCL 标志的情况下,如果 pathname 参数指向的文件已经存在,则 open 函数返回错误。 可以用于测试一个文件是否存在,如果不存在则创建此文件,如果存在则返回错误,这使得测试和创建两者成为一个原子操作;关于原子操作,在后面的内容当中将会对此进行说明。
    O_NOFOLLOW 如果 pathname 参数指向的是一个符号链接,将不对其进行解引用,直接返回错误。 不加此标志情况下,如果 pathname参数是一个符号链接,会对其进行解引用。
  • mode: 此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志时才有效(O_TMPFILE 标志用于创建一个临时文件)。

我们调用 open 函数去新建一个文件时,也需要指定该文件的权限,而 mode 参数便用于指定此文件的权限,接下来看看我们该如何通过 mode 参数来表示文件的权限,首先 mode 参数的类型是 mode_t,这是一个 u32 无符号整形数据,权限表示方法如下所示:

从低位从上看,每 3 个 bit 位分为一组,分别表示:

  • O—这 3 个 bit位用于表示其他用户的权限;

  • G—这 3 个 bit 位用于表示同组用户(group)的权限,即与文件所有者有相同组 ID的所有用户;

  • U—这 3 个 bit位用于表示文件所属用户的权限,即文件或目录的所属者;

  • S—这 3 个 bit 位用于表示文件的特殊权限,文件特殊权限一般用的比较少

只有用户对该文件具有相应权限时,才可以使用对应的标志去打开文件,否则会打开失败!譬如,我们的程序对该文件只有只读权限,那么执行 open 函数使用 O_RDWR 或 O_WRONLY 标志将会失败。

在实际编程中,我们可以直接使用 Linux 中已经定义好的宏,不同的宏定义表示不同的权限,如下所示:

宏定义 说明
S_IRUSR 允许文件所属者读文件
S_IWUSR 允许文件所属者写文件
S_IXUSR 允许文件所属者执行文件
S_IRWXU 允许文件所属者读、写、执行文件
S_IRGRP 允许同组用户读文件
S_IWGRP 允许同组用户写文件
S_IXGRP 允许同组用户执行文件
S_IRWXG 允许同组用户读、写、执行文件
S_IROTH 允许其他用户读文件
S_IWOTH 允许其他用户写文件
S_IXOTH 允许其他用户执行文件
S_IRWXO 允许其他用户读、写、执行文件
S_ISUID S_ISGID S_ISVTX set-user-ID(特殊权限) set-group-ID(特殊权限) sticky(特殊权限)

返回值: 成功将返回文件描述符,文件描述符是一个非负整数;失败将返回-1。

使用 open 函数打开一个指定的文件(譬如/home/dengtao/hello),如果该文件不存在则创建该文件,创建该文件时,将文件权限设置如下:文件所属者拥有读、写、执行权限;同组用户与其他用户只有读权限。使用可读可写方式打开:

1
2
3
int fd=open("/home/dengtao/hello", O_RDWR|O_CREAT,S_IRWXU|S_IRGRP|S_IROTH); 
if (-1 == fd)
return fd;

4write写文件

调用 write 函数可向打开的文件写入数据,其函数原型如下所示(可通过”man 2 write”查看):

1
2
3
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • fd: 文件描述符。
  • buf: 指定写入数据对应的缓冲区。
  • count: 指定写入的字节数。
  • 返回值: 如果成功将返回写入的字节数(0 表示未写入任何字节),如果此数字小于 count 参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。

对于普通文件(我们一般操作的大部分文件都是普通文件,譬如常见的文本文件、二进制文件等),不管是读操作还是写操作,一个很重要的问题是:从文件的哪个位置开始进行读写操作?也就是 IO 操作所对应的位置偏移量,读写操作都是从文件的当前位置偏移量处开始,当然当前位置偏移量可以通过 lseek 系统调用进行设置,关于此函数后面再讲;默认情况下当前位置偏移量一般是 0,也就是指向了文件起始位置,当调用 read、 write 函数读写操作完成之后,当前位置偏移量也会向后移动对应字节数,譬如当前位置偏移量为 1000 个字节处,调用 write()写入或 read()读取 500 个字节之后,当前位置偏移量将会移动到 1500 个字节处。

5read读文件

调用 read 函数可从打开的文件中读取数据,其函数原型如下所示(可通过”man 2 read”查看):

1
2
3
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • fd: 文件描述符。与 write 函数的 fd 参数意义相同。
  • buf: 指定用于存储读取数据的缓冲区。
  • count: 指定需要读取的字节数。
  • 返回值: 如果读取成功将返回读取到的字节数,实际读取到的字节数可能会小于 count 参数指定的字节数,也有可能会为 0,譬如进行读操作时,当前文件位置偏移量已经到了文件末尾。实际读取到的字节数少于要求读取的字节数,譬如在到达文件末尾之前有 30 个字节数据,而要求读取 100 个字节,则 read 读取成功只能返回 30;而下一次再调用 read 读,它将返回 0(文件末尾)。

6close关闭文件

可调用 close 函数关闭一个已经打开的文件,其函数原型如下所示(可通过”man 2 close”查看):

1
2
3
#include <unistd.h>

int close(int fd);
  • fd: 文件描述符,需要关闭的文件所对应的文件描述符。

  • 返回值: 如果成功返回 0,如果失败则返回-1。

除了使用 close 函数显式关闭文件之外,在 Linux 系统中,当一个进程终止时,内核会自动关闭它打开的所有文件,也就是说在我们的程序中打开了文件,如果程序终止退出时没有关闭打开的文件,那么内核会自动将程序中打开的文件关闭。很多程序都利用了这一功能而不显式地用 close

关闭打开的文件。

显式关闭不再需要的文件描述符往往是良好的编程习惯,会使代码在后续修改时更具有可读性,也更可靠,进而言之,文件描述符是有限资源,当不再需要时必须将其释放、归还于系统。

7Iseek

对于每个打开的文件,系统都会记录它的读写位置偏移量,我们也把这个读写位置偏移量称为读写偏移量,记录了文件当前的读写位置,当调用 read()或 write()函数对文件进行读写操作时,就会从当前读写位置偏移量开始进行数据读写。

读写偏移量用于指示 read()或 write()函数操作时文件的起始位置,会以相对于文件头部的位置偏移量来表示,文件第一个字节数据的位置偏移量为 0。

当打开文件时,会将读写偏移量设置为指向文件开始位置处,以后每次调用read()、 write()将自动对其进行调整,以指向已读或已写数据后的下一字节,因此,连续的调用 read()和 write()函数将使得读写按顺序递增,对文件进行操作。

lseek 函数的原型,如下所示(可通过”man 2 lseek”查看):

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
  • fd: 文件描述符。

  • offset: 偏移量,以字节为单位。

  • whence: 用于定义参数 offset 偏移量对应的参考值,该参数为下列其中一种(宏定义):

    • SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算);
    • SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处, offset 可以为正、也可以为负,如果是正数表示往后偏移,如果是负数则表示往前偏移;
    • SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负,如果是正数表示往后偏移、如果是负数则表示往前偏移。
  • 返回值: 成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置;发生错误将返回-1。

使用示例:

(1)将读写位置移动到文件开头处:

1
2
3
off_t off = lseek(fd, 0, SEEK_SET); 
if (-1 == off)
return -1;

(2)将读写位置移动到文件末尾:

1
2
3
off_t off = lseek(fd, 0, SEEK_END); 
if (-1 == off)
return -1;

(3)将读写位置移动到偏移文件开头 100 个字节处:

1
2
3
off_t off = lseek(fd, 100, SEEK_SET);
if (-1 == off)
return -1;

(4)获取当前读写位置偏移量:

1
2
3
off_t off = lseek(fd, 0, SEEK_CUR); 
if (-1 == off)
return -1;

函数执行成功将返回文件当前读写位置。

评论