0%

Linux文件IO使用

Linux 文件IO

简单的实例

#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;
}

文件描述符

  • 调用 open 函数会有一个返回值,譬如示例代码 2.1.1 中的 fd1 和 fd2,这是一个 int 类型的数据,在 open 函数执行成功的情况下,会返回一个非负整数,该返回值就是一个文件描述符(file descriptor),这说明文 件描述符是一个非负整数;对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引。
  • 当调用 open 函数打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符,用于指 代被打开的文件,所有执行 IO 操作的系统调用都是通过文件描述符来索引到对应的文件,譬如示例代码 2.1.1 中,当调用 read/write 函数进行文件读写时,会将文件描述符传送给 read/write 函数
  • 所以对于一个进程来说,文件描述符是一种有限资源,文件描述符是从 0 开始分配的,譬如说进程中第 一个被打开的文件对应的文件描述符是 0、第二个文件是 1、第三个文件是 2、第 4 个文件是 3……以此类推,所以由此可知,文件描述符数字最大值为 1023(0~1023)。每一个被打开的文件在同一个进程中都有 一个唯一的文件描述符,不会重复,如果文件被关闭后,它对应的文件描述符将会被释放,那么这个文件描 述符将可以再次分配给其它打开的文件、与对应的文件绑定起来。

一切皆文件

  • Linux 系统下,一切皆文件,也包括各种硬件设备,使用 open 函数打开任何文件成功情况下便会 返回对应的文件描述符 fd。每一个硬件设备都会对应于 Linux 系统下的某一个文件,把这类文件称为设备文 件。所以设备文件对应的其实是某一硬件设备,应用程序通过对设备文件进行读写等操作、来使用、操控硬 件设备,譬如 LCD 显示器、串口、音频、键盘等。
  • 标准输入一般对应的是键盘,可以理解为 0 便是打开键盘对应的设备文件时所得到的文件描述符;标 准输出一般指的是 LCD 显示器,可以理解为 1 便是打开 LCD 设备对应的设备文件时所得到的文件描述符; 而标准错误一般指的也是 LCD 显示器。

打开文件

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

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

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

文件权限(rwx)

前面的博客中已经详细介绍过了,略

写入文件

  • 调用 write 函数可向打开的文件写入数据,其函数原型如下所示
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • fd:文件描述符。关于文件描述符,前面已经给大家进行了简单地讲解,这里不再重述!我们需要将进 行写操作的文件所对应的文件描述符传递给 write 函数。

  • buf:指定写入数据对应的缓冲区。

  • count:指定写入的字节数。

  • 返回值:如果成功将返回写入的字节数(0 表示未写入任何字节)如果此数字小于 count 参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。

针对写入文件的开始地址

  • 读写操作都是从文件的当前位置偏移量处开始,当然当前位置偏移量可以通过 lseek 系统 调用进行设置,关于此函数后面再讲;默认情况下当前位置偏移量一般是 0,也就是指向了文件起始位置, 当调用 read、write 函数读写操作完成之后,当前位置偏移量也会向后移动对应字节数

读文件

  • 调用 read 函数可从打开的文件中读取数据
#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(文件末尾)。

关闭文件

  • close
#include <unistd.h>

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

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

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

更改偏移量位置

  • lseek
#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。可以用此函数获取文件此时的偏移量

Linux文件系统简单讲述

  • 磁盘空间包括两个部分,一个是真正存储文件的区域,另一个是存储文件inode的区域(inode见前面的博客)

  • image-20220112130444097

  • windows的快速格式化不会真正删除存储问文件内容的区域,只是删除了inode表的区域

打开文件的过程

  • 系统找到这个文件名所对应的 inode 编号;

  • 通过 inode 编号从 inode table 中找到对应的 inode 结构体

  • 根据 inode 结构体中记录的信息,确定文件数据所在的 block,并读出数据。

  • 文件打开的时候内核会申请一段内存(一段缓冲区),并且将静态文件的数 据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内 核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作。

  • 因为磁盘、硬盘、U 盘等存储设备基本都是 Flash 块设备,因为块设备硬件本身有读写限制等特征,块 设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动 也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的 读写操作非常不灵活;而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活, 所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文 件,而不是直接去操作磁盘中的静态文件,不但操作不灵活,效率也会下降很多,因为内存的读写速率远比 磁盘读写快得多。

  • 在 Linux 系统中,内核会为每个进程(关于进程的概念,这是后面的内容,我们可以简单地理解为一个 运行的程序就是一个进程,运行了多个程序那就是存在多个进程)设置一个专门的数据结构用于管理该进 程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写 PCB)。

    • PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文 件状态标志、引用计数、当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等,进程打开 的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表
    • image-20220112131919826

程序出错

略,详见原子教程pdf

程序退出

基本方法

  • return
    • return 0表示程序正常结束
    • return -1表示程序异常退出

Linux 下的其他方法

  • 进程正常退出除了可以使用 return 之外,还可以使用exit()_exit()以及_Exit()

  • _exit()

  • 调用_exit()函数会 清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将 控制权交给操作系统。

#include <unistd.h>

void _exit(int status);
  • 其中的status含义与上面return的相同,0代表正常,其他数值代表异常

  • _Exit()_exit()等价,不再介绍

#include <stdlib.h>

void _Exit(int status);
  • exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。
    执行 exit()会执行一些清理工作,最后调用_exit()函数。
#include <stdlib.h>

void exit(int status);
  • 不管你用哪一种都可以结束进程,但还是推荐大家使用 exit()

空洞文件

  • 什么是空洞文件(hole file)?在上一章内容中,笔者给大家介绍了 lseek()系统调用,使用 lseek 可以修 改文件的当前读写位置偏移量,此函数不但可以改变位置偏移量,并且还允许文件偏移量超出文件长度,这 是什么意思呢?譬如有一个 test_file,该文件的大小是 4K(也就是 4096 个字节),如果通过 lseek 系统调 用将该文件的读写偏移量移动到偏移文件头部 6000 个字节处,大家想一想会怎样?如果笔者没有提前告诉 大家,大家觉得不能这样操作,但事实上 lseek 函数确实可以这样操作。

  • 接下来使用 write()函数对文件进行写入操作,也就是说此时将是从偏移文件头部 6000 个字节处开始写 入数据,也就意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形 成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件

  • 文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它 分配对应的空间,但是**空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的**,这点需要注意。

  • 空洞文件对多线程共同操作文件是及其有用的,有时候我们创建 一个很大的文件,如果单个线程从头开始依次构建该文件需要很长的时间,有一种思路就是将文件分为多 段,然后使用多线程来操作每个线程负责其中一段数据的写入

测试

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
int fd;
int ret;
char buffer[1024];
int i;
/* 打开文件 */
fd = open("./hole_file", O_WRONLY | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 将文件读写位置移动到偏移文件头 4096 个字节(4K)处 */
ret = lseek(fd, 4096, SEEK_SET);
if (-1 == ret)
{
perror("lseek error");
goto err;
}
/* 初始化 buffer 为 0xFF */
memset(buffer, 0xFF, sizeof(buffer));
/* 循环写入 4 次,每次写入 1K */
for (i = 0; i < 4; i++) {
ret = write(fd, buffer, sizeof(buffer));
if (-1 == ret)
{
perror("write error");
goto err;
}
}
ret = 0;
err:
/* 关闭文件 */
close(fd);
exit(ret);
}
  • 上面的代码从文件的4k位置开始,一次写入1k,写入4次。

  • 下面查看文件的大小

  • image-20220112143912056

  • 利用ls察看文件大小的时候,虽然文件只有4k有数据,但是文件大小查出来逻辑大小,也就是8k。

  • 但是使用du查看的时候,只有文件占用的实际存储的大小,也就是4k。

O_APPEND 和 O_TRUNC 标志

在上一章给大家讲解 open 函数的时候介绍了一些 open 函数的 flags 标志,譬如 O_RDONLY、 O_WRONLY、O_CREAT、O_EXCL 等,本小节再给大家介绍两个标志,分别是 O_APPEND 和 O_TRUNC, 接下来对这两个标志分别进行介绍。

O_TRUNC 标志

  • O_TRUNC 这个标志的作用非常简单,如果使用了这个标志,调用 open 函数打开文件的时候会将文件 原本的内容全部丢弃,文件大小变为 0;这里我们直接测试即可!测试代码如下所示:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int fd;
/* 打开文件 */
fd = open("./test_file", O_WRONLY | O_TRUNC);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 关闭文件 */
close(fd);
exit(0);
}

O_APPEND 标志

  • 如果 open 函数携带了 O_APPEND 标志,调用 open 函数打开文件, 当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末 尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
char buffer[16];
int fd;
int ret;
/* 打开文件 */
fd = open("./test_file", O_RDWR | O_APPEND);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 初始化 buffer 中的数据 */
memset(buffer, 0x55, sizeof(buffer));
/* 写入数据: 写入 4 个字节数据 */
ret = write(fd, buffer, 4);
if (-1 == ret) {
perror("write error");
goto err;
}
/* 将 buffer 缓冲区中的数据全部清 0 */
memset(buffer, 0x00, sizeof(buffer));
/* 将位置偏移量移动到距离文件末尾 4 个字节处 */
ret = lseek(fd, -4, SEEK_END);
if (-1 == ret) {
perror("lseek error");
goto err;
}
/* 读取数据 */
ret = read(fd, buffer, 4);
if (-1 == ret) {
perror("read error");
goto err;
}
printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1],
buffer[2], buffer[3]);
ret = 0;
err:
/* 关闭文件 */
close(fd);
exit(ret);
}

  • image-20220112145240871

  • 通过控制台可知,读出的内容确实是最后四个字节为0x55

  • O_APPEND 标志并不会影响读文件当读取文件时,O_APPEND 标志并不会影响读位置偏移量,即使使用了 O_APPEND 标志,读文件位置偏移量默认情况下依然是文件头,关于这个问题大家可以自己进行测试,编程是一个实践 性很强的工作,有什么不能理解的问题,可以自己编写程序进行测试。

  • 使用 lseek 函数来改变 write()时的写位置偏移量,其实这种做法并不会成功,这就是笔 者给大家提的第二个细节,使用了 O_APPEND 标志,即使是通过 lseek 函数也是无法修改写文件时对应的 位置偏移量(注意笔者这里说的是写文件,并不包括读),写入数据依然是从文件末尾开始,lseek 并不会 该变写位置偏移量

  • 测试

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
int fd1;
char readBuff[1024];
char wrBuff[1024];
char * wrCont = "Hello World";
int i = 0;
while(wrCont[i]!=0)
{
wrBuff[i] = wrCont[i];
i++;
}
wrBuff[i] = 0;
fd1 = open("./test1.txt", O_RDWR|O_APPEND);
for(int j = 0;j<4;j++)
{
sprintf(wrBuff,", 0x%02x", j);
printf(wrBuff);
write(fd1, wrBuff, 6);
}

close(fd1);
}
  • 此处函数不对文件偏移位置进修修改的前提下,在文件写入4次, 0xXX的字符,观察效果

  • image-20220112151123797

  • 可以看出,在文件最后多出了四个字符串,可见每次写入的时候,文件的偏移量都自动移动到了文件的最后,即使文件在这个过程中并没有被保存。