文件IO基础
- 一、简单的文件 IO 示例
- 二、文件描述符
- 三、open 打开文件
- 1. 函数原型
- 2. 文件权限
- 3. 宏定义文件权限
- 4. 函数使用实例
- 四、write 写文件
- 五、read 读文件
- 六、close 关闭文件
- 七、Iseek
绍 Linux 应用编程中最基础的知识,即文件 I/O(Input、Outout)
一、简单的文件 IO 示例
一个通用的 IO 模型通常包括打开文件、读写文件、关闭文件这些基本操作,主要涉及到 4 个函数:open()、read()、write()以及 close()。
#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 函数关闭源文件和目标文件。
二、文件描述符
看上面代码中的 open 函数会有一个返回值。然后会赋值给 fd 中,这是一个 int 类型数据,在 open 函数执行成功的情况下,会返回一个非负函数,而这个返回值就是一个文件描述符。对 Linux 内核,所有打开的文件都会通过文件描述符进行索引。
当调用 open 函数打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符,用于指代被打开的文件,所有执行 IO 操作的系统调用都是通过文件描述符来索引到对应的文件。当调用 read/write 函数进行文件读写时,会将文件描述符传送给 read/write 函数,所以在代码中,fd1 就是源文件 src_file 被打开时所对应的文件描述符,而 fd2 则是目标文件 dest_file 被打开时所对应的文件描述符。
在 Linux 系统下,我们可以通过 ulimit 命令来查看进程可打开的最大文件数,用法如下所示:
ulimit -n
当我们在程序中,调用 open 函数打开文件的时候,分配的文件描述符一般都是从 3 开始,是 0、1、2 这三个文件描述符已经默认被系统占用了,分别分配给了系统标准输入(0)、标准输出(1)以及标准错误(2)。
标准输入一般对应的是键盘,可以理解为 0 便是打开键盘对应的设备文件时所得到的文件描述符;
标准输出一般指的是 LCD 显示器,可以理解为 1 便是打开 LCD 设备对应的设备文件时所得到的文件描述符;
而标准错误一般指的也是 LCD 显示器。
三、open 打开文件
1. 函数原型
open 函数的函数原型为:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
- pathname:字符串类型,用于标识需要打开或创建的文件,可以包含路径。
- flag:调用 open 函数时需要提供的标志,包括文件访问模式标志以及其它文件相关标志,这些标志使用宏定义进行描述,都是常量。
- mode:此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志时才有效(O_TMPFILE 标志用于创建一个临时文件)。
flags 参数时既可以单独使用某一个标志,也可以通过位或运算(|)将多个标志进行组合:
open("./src_file", O_RDONLY) //单独使用某一个标志
open("./src_file", O_RDONLY | O_NOFOLLOW) //多个标志组合
2. 文件权限
当我们调用 open 函数去新建一个文件时,也需要指定该文件的权限,而 mode 参数便用于指定此文件的权限。首先 mode 参数的类型是 mode_t,这是一个 u32 无符号整形数据,权限表示方法如下所示:
们从低位从上看,每 3 个 bit 位分为一组,分别表示:
- O:表示其他用户的权限
- G:表示同组用户(group)的权限
- U:表示文件所属用户的权限
- S:表示文件的特殊权限
3 个 bit 位中,按照 rwx 顺序来分配权限位(特殊权限除外)。
- 最高位(权值为 4)表示读权限,为 1 时表示具有读权限,为 0 时没有读权限。
- 中间位(权值为 2)表示写权限,为 1 时表示具有写权限,为 0 时没有写权限。
- 最低位(权值为 1)表示执行权限,为 1 时表示具有可执行权限,为 0 时没有执行权限。
对于最高权限:
1FF(hex)、111 111 111(bin)、777(oct)、511(dec)
几个权限例子:
111 000 000:表示文件所属者拥有 读、写、执行权限。而同组用户和其他用户并不具有任何权限。
100 100 100:表示三类都有 读权限,但没有 写、执行权限。
3. 宏定义文件权限
在实际编程中,我们可以直接使用 Linux 中已经定义好的宏,不同的宏定义表示不同的权限。而宏定义其实可以根据英文来判断是什么权限。以下是几个例子:
USR文件所属者
S_IRUSR
:IR读权限、
S_IWUSR
:IW写权限
S_IXUSR
:IX执行权限
S_IRWXU
:RWX 读、写、执行权限。U代表USR
GRP同组用户和OTH其他用户就不介绍了,一个道理。
4. 函数使用实例
使用 open 函数打开一个已经存在的文件,使用只读方式打开:
int fd = open("./app.c",O_RDONLY)
if(-1==fd)
return fd;
使用 open 函数打开一个指定的文件,使用可读可写方式,如果该文件是一个符号链接文件,则不对其进行解引用,直接返回错误:
int fd = open("/home/prover/hello",O_RDWR|O_NOFOLLOW);
if(-1==fd)
return fd;
使用 open 函数打开一个指定的文件,如果该文件不存在则创建该文件,创建该文件时,将文件权限设置如下:
- 文件所属者拥有读、写、执行权限;
- 同组用户与其他用户只有读权限
- 使用可读可写方式打开:
int fd = open("/home/prover/hello",O_RDWR|O_CREAT,S_IRWXU|S_IRGRP|S_IROTH);
if(-1==fd)
return fd;
四、write 写文件
函数原型
ssize_t write(int fd, const void *buf, size_t count);
- fd:文件描述符
- buf:指定写入数据对应的缓冲区
- count:指定写入的字节数
- 返回值:如果成功将返回写入的字节数。如果此数字小于 count 参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。
对于普通文件,默认情况下当前位置偏移量一般是 0,也就是指向了文件起始位置,当调用 read、write 函数读写操作完成之后,当前位置偏移量也会向后移动对应字节数。
隔壁 CTF 的 Pwn 二进制安全,可以利用 write() 函数进行漏洞利用,填充缓冲区,填入 system 函数地址和 /bin/sh 的地址,从而达到漏洞利用。
五、read 读文件
函数原型
ssize_t read(int fd, void *buf, size_t count);
- fd:文件描述符。与 write 函数的 fd 参数意义相同。
- buf:指定用于存储读取数据的缓冲区。
- count:指定需要读取的字节数。
- 返回值
- 如果读取成功将返回读取到的字节数,实际读取到的字节数可能会小于 count 参数指定的字节数。
- 也有可能会为 0,譬如进行读操作时,当前文件位置偏移量已经到了文件末尾。
- 实际读取到的字节数少于要求读取的字节数,譬如在到达文件末尾之前有 30 个字节数据,而要求读取 100 个字节,则 read 读取成功只能返回 30;而下一次再调用 read 读,它将返回 0(文件末尾)。
六、close 关闭文件
函数原型
int close(int fd);
- fd:文件描述符,需要关闭的文件所对应的文件描述符。
- 返回值:如果成功返回 0,如果失败则返回-1。
除了使用 close 函数显式关闭文件之外,在 Linux 系统中,当一个进程终止时,内核会自动关闭它打开的所有文件,也就是说在我们的程序中打开了文件,如果程序终止退出时没有关闭打开的文件,那么内核会自动将程序中打开的文件关闭。很多程序都利用了这一功能而不显式地用 close 关闭打开的文件。
七、Iseek
上面说到,对于每个打开的文件,系统都会记录它的读写位置偏移量,我们也把这个读写位置偏移量称为读写偏移量,记录了文件当前的读写位置,当调用 read()或 write()函数对文件进行读写操作时,就会从当前读写位置偏移量开始进行数据读写。
读写偏移量用于指示 read()或 write()函数操作时文件的起始位置,会以相对于文件头部的位置偏移量来表示,文件第一个字节数据的位置偏移量为 0。
当打开文件时,会将读写偏移量设置为指向文件开始位置处,以后每次调用 read()、write()将自动对其进行调整,以指向已读或已写数据后的下一字节,因此,连续的调用 read()和 write()函数将使得读写按顺序递增,对文件进行操作。
函数原型
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。
使用示例
将读写位置移动到文件开头处:
off_t off = lseek(fd,0,SEEK_SET);
if(-1==off)
return -1;
将读写位置移动到文件末尾:
off_t off = lseek(fd,0,SEEK_END);
if(-1==off)
return -1;
将读写位置移动到偏移文件开头 100 个字节处:
off_t off = lseek(fd,100,SEEK_SET);
if(-1==off)
return -1;
获取当前读写位置偏移量
off_t off = lseek(fd,0,SEEK_CUR);
if(-1==off)
return -1;