目录
一、文件IO的概念
Linux 应用编程中最基础的知识,即文件 I/O(Input、 Outout) , 文件 I/O 指的是对文
件的输入/输出操作,即对文件的读写操作。
Linux 下一切皆文件,文件作为 Linux 系统设计思想的核心理念,在 Linux 系统下显得尤为重要,所以对文件的 I/O 操作既是基础也是最重要的部分。
一个通用的 IO 模型通常包括打开文件、读写文件、关闭文件这些基本操作, 主要涉及到 4 个函数: open()、 read()、 write()以及 close()。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd1, fd2;
char buf[1024];
int ret;
// 打开当前目录下的 src_file 文件
if((fd1 = open("./src_file", O_RDONLY)) < 0) {
printf("open src_file fail ! \n ");
return 0;
}
printf("fd1 = %d\n", fd1);
// 打开当前目录下的 dest_file 文件
if ((fd2 = open("./dest_file", O_WRONLY)) < 0) {
printf("open dest_file fail ! \n");
return 0;
}
printf("fd2 = %d\n", fd2);
// 将 src_file 内容读到 buf 中
if ((read(fd1, buf, sizeof(buf))) < 0) {
printf("read src_file fail ! \n");
return 0;
}
// 将 buf 内容写到 dest_file 中
if ((write(fd2, buf, sizeof(buf))) < 0) {
printf("write dest_file fail ! \n");
return 0;
}
// 关闭文件
close(fd1);
close(fd2);
return 0;
}
代码说明:从源文件 src_file 中读取 1KB 数据,然后将其写入到目标文件 dest_file 中(这里假设当前目录下这两个文件都是存在的)。
二、文件描述符
调用 open 函数会有一个返回值, 这是一个 int 类型的数据,在 open函数执行成功的情况下, 会返回一个非负整数, 该返回值就是一个文件描述符(file descriptor)这说明文件描述符是一个非负整数。
调用 open 函数打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符, 用于指代被打开的文件,所有执行 IO 操作的系统调用都是通过文件描述符来索引到对应的文件,当调用 read/write 函数进行文件读写时,会将文件描述符传送给 read/write 函数。
一个进程可以打开多个文件, 但是在 Linux 系统中,一个进程可以打开的文件数是有限制,并不是可以无限制打开很多的文件,如果超过进程可打开的最大文件数限制,内核将会发送警告信号给对应的进程,然后结束进程。
Linux 系统下,可以通过 ulimit 命令来查看进程可打开的最大文件数,用法如下所示:
该最大值默认情况下是 1024,也就意味着一个进程最多可以打开 1024 个文件。
文件描述符是一种有限资源, 文件描述符是从 0 开始分配的进程中第一个被打开的文件对应的文件描述符是 0、第二个文件是 1、第三个文件是 2、第 4 个文件是 3…,文件描述符数字最大值为 1023(0~1023)。
每次给打开的文件分配文件描述符都是从最小的没有被使用的文件描述符(0~1023)开始。
每一个被打开的文件在同一个进程中都有一个唯一的文件描述符,不会重复,如果文件被关闭后,它对应的文件描述符将会被释放,那么这个文件描述符将可以再次分配给其它打开的文件、与对应的文件绑定起来。
当我们在程序中,调用 open 函数打开文件的时候,分配的文件描述符一般都是从 3 开始,这里大家可能要问了,上面不是说从 0 开始的吗,确实是如此,但是 0、 1、 2 这三个文件描述符已经默认被系统占用了,分别分配给了系统标准输入(0)、 标准输出(1)以及标准错误(2)。
例子中的fd1 和 fd2 的文件描述符如下:
三、open函数(打开文件)
在 Linux 系统中要操作一个文件,需要先打开该文件,得到文件描述符,然后再对文件进行相应的读写操作(或其他操作),最后在关闭该文件。
open 函数用于打开文件,当然除了打开已经存在的文件之外,还可以创建一个新的文件。
函数原型如下所示:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
通过 man 命令查看open函数用法:
man 2 open #查看 open 函数的帮助信息
1.pathname参数
- pathname: 字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径) 信息。
2.flags参数
- 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 参数是一个符号链接,会对其进行解 引用。 |
open 函数的 flags 标志并不止这些。
3.mode参数
- mode: 此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志时才有效(O_TMPFILE 标志用于创建一个临时文件)。
touch 命令新建一个文件,此时文件会有一个默认的权限,可通过 chmod 命令对文件权限进行修改,可以使用"ls -l"命令来查看到文件所对应的权限。
调用 open 函数去新建一个文件时,也需要指定该文件的权限,而 mode 参数便用于指定此文件
的权限,mode 参数的类型是 mode_t,是一个 u32 无符号整形数据,权限表示方法如下所示:
从低位从上看,每 3 个 bit 位分为一组,分别表示:
- O---这 3 个 bit 位用于表示其他用户的权限。
- G---这 3 个 bit 位用于表示同组用户(group)的权限,即与文件所有者有相同组 ID 的所有用户。
- U---这 3 个 bit 位用于表示文件所属用户的权限,即文件或目录的所属者。
- S---这 3 个 bit 位用于表示文件的特殊权限,文件特殊权限一般用的比较少。
权限的概述
3 个 bit 位中,按照 rwx 顺序来分配权限位(特殊权限除外) :
- 最高位(权值为 4)表示读权限,为 1 时表示具有读权限,为 0 时没有读权限。
- 中间位(权值为 2)表示写权限,为 1 时表示具有写权限,为 0 时没有写权限。
- 最低位(权值为 1)表示执行权限,为 1 时表示具有可执行权限,为 0 时没有执行权限。
最高权限表示方法: 111111111(二进制表示)、 777(八进制表示)、 511(十进制表示)。
- 111000000(二 进制表示):表示文件所属者具有读、写、执行权限,而同组用户和其他用户不具有任何权限。
- 100100100(二进制表示):表示文件所属者、同组用户以及其他用户都具有读权限,但都没有写、执行权限。
open 函数文件权限(mode)宏
宏定义 | 说明 |
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(特殊权限) |
这些宏既可以单独使用,也可以通过位或运算将多个宏组合在一起:
S_IRUSR | S_IWUSR | S_IROTH
4.返回值
- 返回值: 成功将返回文件描述符,文件描述符是一个非负整数;失败将返回-1。
5.open 函数使用示例
1.使用 open 函数打开一个已经存在的文件(例如当前目录下的 app.c 文件),使用只读方式打开:
int fd = open("./app.c", O_RDONLY)
if (-1 == fd)
return fd;
2.使用 open 函数打开一个已经存在的文件(例如当前目录下的 app.c 文件),使用可读可写方式打开:
int fd = open("./app.c", O_RDWR)
if (-1 == fd)
return fd;
3.使用 open 函数打开一个指定的文件(譬如/home/dengtao/hello) , 使用可读可写方式,如果该文件是一个符号链接文件,则不对其进行解引用,直接返回错误:
int fd = open("/home/dengtao/hello", O_RDWR | O_NOFOLLOW);
if (-1 == fd)
return fd;
4.使用 open 函数打开一个指定的文件(譬如/home/dengtao/hello),如果该文件不存在则创建该文件,创建该文件时,将文件权限设置如下:
- 文件所属者拥有读、写、执行权限。
- 使用可读可写方式打开。
- 同组用户与其他用户只有读权限。
int fd = open("/home/dengtao/hello", O_RDWR | O_CREAT, S_IRWXU | S_IRGRP | S_IROTH);
if (-1 == fd)
return fd;
四、write函数(写文件)
write()
函数是用于向文件描述符(文件、设备、套接字等)写入数据。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
1.参数说明
-
fd
: 文件描述符,指定要写入的文件或设备。-
例如,标准输出是
1
,标准错误是2
。
-
-
buf
: 指向要写入的数据的指针。 -
count
: 要写入的字节数。
2.返回值
- 成功时返回实际写入的字节数。
- 失败时返回
-1
,并设置errno
。
五、read函数(读文件)
read()
是用于从文件描述符(文件、设备、管道等)中读取数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
1.参数说明
-
fd
: 文件描述符,表示从哪里读取数据。-
例如,标准输入是
0
,也可以是通过open()
打开的文件描述符。
-
-
buf
: 指向用于存储读取数据的缓冲区的指针。 -
count
: 要读取的最大字节数。
2.返回值
-
成功时返回读取的字节数(可能小于
count
)。 -
到达文件末尾时返回
0
。 -
失败时返回
-1
,并设置errno
。
write和read函数都会使文件位置发生偏移。
六、close函数(关闭文件)
close()
是用于关闭打开的文件描述符(文件、设备、管道等)。关闭文件描述符可以释放相关的系统资源,避免资源泄漏。
#include <unistd.h>
int close(int fd);
1.参数说明
-
fd
: 要关闭的文件描述符,由open()
或其他系统调用返回。
2.返回值
-
成功时返回
0
。 -
失败时返回
-1
,并设置errno
。
七、lseek函数(设置文件偏移位置)
lseek()
是用于在文件中重新定位文件偏移量。它可以用于随机访问文件内容,通过设置文件偏移来控制后续 read()
或 write()
的起始位置。
#include <unistd.h>
#include <fcntl.h>
off_t lseek(int fd, off_t offset, int whence);
1.参数说明
-
fd
: 文件描述符,由open()
或其他相关系统调用返回。 -
offset
: 偏移量,根据whence
的值进行解释。 -
whence
: 指定偏移的基准位置,常用值包括:-
SEEK_SET
: 从文件开头偏移offset
字节。 -
SEEK_CUR
: 从当前位置偏移offset
字节。 -
SEEK_END
: 从文件末尾偏移offset
字节(offset
可为负值)。
-
2.返回值
-
成功时返回新的文件偏移量。
-
失败时返回
-1
,并设置errno
。
3.lseek函数使用示例
1.将读写位置移动到文件开头处:
off_t off = lseek(fd, 0, SEEK_SET);
if (-1 == off)
return -1;
2.将读写位置移动到文件末尾:
off_t off = lseek(fd, 0, SEEK_END);
if (-1 == off)
return -1;
3.将读写位置移动到偏移文件开头 100 个字节处:
off_t off = lseek(fd, 100, SEEK_SET);
if (-1 == off)
return -1;
4.获取当前读写位置偏移量:
off_t off = lseek(fd, 0, SEEK_CUR);
if (-1 == off)
return -1;
八.练习示例
将src_file文件(以只读方式打开)偏移500个字节之后1k字节写入到dest_file文件(以创建方式打开)中:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
char buffer[1024];
int fd1, fd2;
int ret;
/* 打开 src_file 文件 */
fd1 = open("./src_file", O_RDONLY);
if (-1 == fd1) {
printf("Error: open src_file failed!\n");
return -1;
}
/* 新建 dest_file 文件并打开 */
fd2 = open("./dest_file", O_WRONLY | O_CREAT | O_EXCL,
S_IRWXU | S_IRGRP | S_IROTH);
if (-1 == fd2) {
printf("Error: open dest_file failed!\n");
ret = 0;
goto err1;
}
/* 将 src_file 文件读写位置移动到偏移文件头 500 个字节处 */
ret = lseek(fd1, 500, SEEK_SET);
if (-1 == ret)
goto err2;
/* 读取 src_file 文件数据,大小 1KByte */
ret = read(fd1, buffer, sizeof(buffer));
if (-1 == ret) {
printf("Error: read src_file filed!\n");
goto err2;
}
/* 将 dest_file 文件读写位置移动到文件头 */
ret = lseek(fd2, 0, SEEK_SET);
if (-1 == ret)
goto err2;
/* 将 buffer 中的数据写入 dest_file 文件,大小 1KByte */
ret = write(fd2, buffer, sizeof(buffer));
if (-1 == ret) {
printf("Error: write dest_file failed!\n");
goto err2;
}
printf("OK: test successful\n");
ret = 0;
err2:
close(fd2);
err1:
close(fd1);
return ret;
}