嵌入式Linux之文件IO

一、标准IO库 

1.1 打开/关闭文件 

fopen

新建 fopen_test.c,写入以下内容:

#include 
int main()
{
    /* 打开文件
    函数:FILE *fopen (const char *__restrict __filename,const char *__restrict __modes)

    参数:
        char *__restrict __filename: 字符串表示要打开文件的路径和名称
        char *__restrict __modes: 字符串表示访问模式
            (1)"r": 只读模式 没有文件打开失败
            (2)"w": 只写模式 存在文件写入会清空文件,不存在文件则创建新文件
            (3)"a": 只追加写模式 不会覆盖原有内容 新内容写到末尾,如果文件不存在则创建
            (4)"r+": 读写模式 文件必须存在 写入是从头一个一个覆盖
            (5)"w+": 读写模式 可读取,写入同样会清空文件内容,不存在则创建新文件
            (6)"a+": 读写追加模式 可读取,写入从文件末尾开始,如果文件不存在则创建
        
    return: FILE * 结构体指针 表示一个文件
    
    */
    char *filename = "io.txt";
    FILE *ioFile = fopen(filename, "a+");
    if (ioFile == NULL)
    {
        printf("FAILED, a+不能打开不存在的文件\n");
    }
    else
    {
        printf("SUCCESS, a+能打开不存在的文件\n");
    }
}

新建 Makefile,写入以下内容:

CC:=gcc
fopen_test: fopen_test.c
    -$(CC) -o $@ $^
    -./$@
    -rm ./$@

说明:

1. 有时编译器不只是 gcc,我们将编译器定义为变量 CC,当切换编译器时只需要更改该变量的定义,而无须更改整个 Makefile。
2. $@相当于当前 target 目标文件的名称,此处为 fopen_test。
3. $^相当于当前 target 所有依赖文件列表,此处为 fopen_test.c
4. ./$@的作用是执行目标文件
5. rm ./$@的作用是在执行完毕后删除目标文件,如果没有这个操作,当源文件fopen_test.c 未更改时就无法重复执行,会提示: 
    make:“fopen_test”已是最新。
    此处删除目标文件,使得我们在不更改源文件的情况下可以多次执行。
6. 所有命令前都添加了“-”符号以忽略错误,确保即便上面的命令执行失败,仍然会向下执行。
    这样做是为了在发生错误时,确保删除目标文件,使得再次执行相同 target时
    不会提示: make:“fopen_test”已是最新,可以重新执行 target 下的命令。
fclose

创建 fclose_test.c 文件,写入以下内容:

#include 
int main()
{
    char *filename = "io1.txt";
    FILE *ioFile = fopen(filename, "r");
    if (ioFile == NULL)
    {
        printf("r 不能打开不存在的文件\n");
    }
    else
    {
        printf("r 能打开不存在的文件\n");
    }
    /*
    函数:int fclose (FILE *__stream)
        FILE *__stream: 需要关闭的文件
    return: 成功返回 0 失败返回 EOF(负数) 通常失败会造成系统崩溃
    */
    int result = fclose(ioFile);
    if (result != 0)
    {
        printf("关闭文件失败");
        return 1;
    }
    return 0;
}

Makefile 中补充以下内容:

fclose_test: fclose_test.c
    -$(CC) -o $@ $^
    -./$@
    -rm ./$@

1.2 向文件中写入数据

fputc 函数

创建 fputc_test.c 文件,写入以下内容:

#include 
int main()
{
    char *filename = "io.txt";
    FILE *ioFile = fopen(filename, "a+");
    if (ioFile == NULL)
    {
        printf("a+不能打开不存在的文件\n");
    }
    else
    {
        printf("a+能打开不存在的文件\n");
    }
    /*写入文件一个字符
    函数:int fputc (int __c, FILE *__stream)
        int __c: 写入的 char 按照 AICII 值写入 可提前声明一个 char
        FILE *__stream: 要写入的文件,写在哪里取决于访问模式
    return: 成功返回 char 的值 失败返回 EOF
    */
    int putcR = fputc(97, ioFile);
    if (putcR == EOF)
    {
        printf("写入字符失败\n");
    }
    else
    {
        printf("写入字符成功:%c\n", putcR);
    }

    int result = fclose(ioFile);
    if (result != 0)
    {
        printf("关闭文件失败");
        return 1;
    }
    return 0;
}

在 Makefile 中补充以下内容:

fputc_test: fputc_test.c
    -$(CC) -o $@ $^
    -./$@
    -rm ./$@
fputs 函数

创建 fputs_test.c 文件,写入以下内容:

#include 
int main()
{
    char *filename = "io.txt";
    FILE *ioFile = fopen(filename, "a+");
    if (ioFile == NULL)
    {
        printf("a+不能打开不存在的文件\n");
    }
    else
    {
        printf("a+能打开不存在的文件\n");
    }
    /*写入文件一个字符串
    函数:int fputs (const char *__restrict __s, FILE *__restrict __stream)
        char *__restrict __s: 需要写入的字符串
        FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问模式
    return: 成功返回非负整数(一般是 0,1) 失败返回 EOF
    */
    int putsR = fputs(" love letter\n", ioFile);
    if (putsR == EOF)
    {
        printf("写入字符串失败\n");
    }
    else
    {
        printf("写入字符串成功:%d\n", putsR);
    }

    int result = fclose(ioFile);
    if (result != 0)
    {
        printf("关闭文件失败");
        return 1;
    }
    return 0;
}
fprintf 函数

创建 fprintf_test.c 文件,写入以下内容:

#include 
int main()
{
    char *filename = "io.txt";
    FILE *ioFile = fopen(filename, "a+");
    if (ioFile == NULL)
    {
        printf("a+不能打开不存在的文件\n");
    }
    else
    {
        printf("a+能打开不存在的文件\n");
    }
    /*
    函数:fprintf (FILE *__restrict __stream, const char *__restrict
        FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问模式
        char *__restrict __fmt: 格式化字符串
        ...: 变长参数列表
    return: 成功返回正整数(写入字符总数不包含换行符) 失败返回 EOF __fmt, ...)
    */
    char *name = "大海";
    int fprintfR = fprintf(ioFile, "今天是2025年1月12号!/n我是guuilin", name);
    if (fprintfR == EOF)
    {
        printf("写入字符串失败");
    }
    else
    {
        printf("写入字符串成功:%d\n", fprintfR);
    }

    int result = fclose(ioFile);
    if (result != 0)
    {
        printf("关闭文件失败");
        fprintf(stderr, "%s\n", filename);
        return 1;
    }
    return 0;
}

1.3 从文件中读取数据

fgetc 函数

创建 fgetc_test.c 文件,写入以下内容:

#include 
int main()
{
    // 打开文件
    FILE *ioFile = fopen("io.txt", "r");
    if (ioFile == NULL)
    {
        printf("不能读不存在的文件");
    }
    /*
    函数:int fgetc (FILE *__stream)
        FILE *__stream: 需要读取的文件
    return: 读取的一个字节 到文件结尾或出错返回 EOF
    */
    char c = fgetc(ioFile);
    while (c != EOF)
    {
        printf("%c", c);
        c = fgetc(ioFile);
    }
    int result = fclose(ioFile);
    if (result != 0)
    {
        printf("关闭文件失败");
        return 1;
    }
    return 0;
}
fgets 函数

创建 fgets_test.c,写入以下内容:

#include 
int main()
{
    // 打开文件
    FILE *ioFile = fopen("io.txt", "r");
    if (ioFile == NULL)
    {
        printf("不能读不存在的文件");
    }
    /*
    函数:fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
        char *__restrict __s: 接收读取的数据字符串
        int __n: 能够接收数据的长度
        FILE *__restrict __stream: 需要读取的文件
    return: 成功返回字符串 失败返回 NULL(可以直接用于 while)
    */
    char buffer[100];
    while (fgets(buffer, sizeof(buffer), ioFile))
    {
        printf("%s", buffer);
    }
    int result = fclose(ioFile);
    if (result != 0)
    {
        printf("关闭文件失败");
        return 1;
    }
    return 0;
}
fscanf 函数

新建文件 user.txt,写入以下内容:

罗密欧 18 朱丽叶
贾宝玉 14 薛宝钗
梁山伯 16 祝英台
--2025.1.12 guilin

创建 fscanf_test.c,写入以下内容:

#include 
int main()
{ 
    /*
    函数:int fscanf (FILE *__restrict __stream, const char *__restrict__format, ...)
        FILE *__restrict __stream: 读取的文件
        char *__restrict __format: 读取的匹配表达式
        ...: 变长参数列表 用于接收匹配的数据
    return: 成功返回参数的个数 失败返回 0 报错或结束返回 EOF
    */
    FILE *userFile = fopen("user.txt", "r");
    if (userFile == NULL)
    {
        printf("不能打开不存在的文件");
    }
    char name[50];
    int age;
    char wife[50];
    int scanfR;
    while (fscanf(userFile, "%s %d %s\n", name, &age, wife) != EOF)
    {
        printf("%s 在%d 岁爱上了%s\n", name, age, wife);
    }
    int result = fclose(userFile);
    if (result != 0)
    {
        printf("关闭文件失败");
        return 1;
    }
    return 0;
}

1.4 标准输入/输出/错误

        读写文件通常用于代码内部操作,如果想要和用户沟通交流,就需要使用标准输入、输出和错误了。

创建文件 stdin_out_err_test.c,写入以下内容:

#include 
#include 
int main(int argc, char const *argv[])
{
    // malloc 动态分配内存 也可以用 char ch[100]接收数据
    char *ch = malloc(100);
    // char ch1[100];
    /*
    stdin: 标准输入 FILE *
    */
    fgets(ch, 100, stdin);
    printf("你好:%s", ch);
    /*
    stdout: 标准输出 FILE * 写入这个文件流会将数据输出到控制台
    printf 底层就是使用的这个
    */
    fputs(ch, stdout); /*
     stderr: 错误输出 FILE * 一般用于输出错误日志
     */
    fputs(ch, stderr);
    return 0;
}

二、系统调用

        系统调用是操作系统内核提供给应用程序,使其可以间接访问硬件资源的接口。

2.1 常见系统调用

open()系统调用用于打开一个标准的文件描述符:

int open(const char *__path, int __oflag, ...);
/*
const char *__path: 文件路径
int __oflag: 用于指定打开文件的方式,可以是以下选项的组合:
    (1) O_RDONLY: 以只读方式打开文件
    (2) O_WRONLY: 以只写方式打开文件
    (3) O_RDWR: 以读写方式打开文件
    (4) O_CREAT: 如果文件不存在,则创建一个新文件
    (5) O_APPEND: 将所有写入操作追加到文件的末尾
    (6) O_TRUNC: 如果文件存在并且以写入模式打开,则截断文件长度为 0
    还有其他标志,如 O_EXCL(当与 O_CREAT 一起使用时,只有当文件不存在时才创建新文件)、
        O_SYNC(同步 I/O)、 O_NONBLOCK(非阻塞 I/O)等可选参数: mode -> 仅在使用了 O_CREAT 标志且文件尚不存在的情况下生效,
        用于指定新创建文件的权限位 权限位通常由三位八进制数字组成,分别代表文件所有者、同组用户和其他用户的读写执行权限

return: 
    (1) 成功时返回非负的文件描述符。
    (2) 失败时返回-1,并设置全局变量 errno 以指示错误原因。
*/

read()系统调用用于读取已经打开的文件描述符:

ssize_t read(int __fd, void *__buf, size_t __nbytes);
/*
int __fd:一个整数,表示要从中读取数据的文件描述符
void *__buf:一个指向缓冲区的指针,读取的数据将被存放到这个缓冲区中
size_t __nbytes:一个 size_t 类型的整数,表示要读取的最大字节数 系统调用将尝试读取最多这么多字节的数据,、
                但实际读取的字节数可能会少于请求的数量。

return: 
    (1) 成功时,read()返回实际读取的字节数 这个值可能小于__nbytes,如果遇到了文件结尾(EOF)或者因为网络读取等原因提前结束读取
    (2) 失败时,read()将返回-1
*/

write()系统调用用于对打开的文件描述符写入内容:

ssize_t write(int __fd, const void *__buf, size_t __n);
/*
参数:
    int __fd:一个整数,表示要写入数据的文件描述符
    void *__buf:一个指向缓冲区的指针,写入的数据需要先存放到这个缓冲区中
    size_t __n:一个 size_t 类型的整数,表示要写入的字节数 write()函数会尝试写入__n 个字节的数据,但实际写入的字节数可能会少于请求的数量

return: 
    (1) 成功时,write()返回实际写入的字节数 这个值可能小于__n,如果写入操作因故提前结束,例如: 磁盘满、网络阻塞等情况
    (2) 失败时,write()将返回-1
*/

close()系统调用用于在使用完成之后,关闭对文件描述符的引用:

int close(int __fd);
/*
参数:
    int __fd:一个整数,表示要关闭的文件描述符

return: 
    (1) 成功关闭时 返回 0
    (2) 失败时 返回-1
*/
exit 和_exit()

系统调用_exit():立即终止当前进程,且不进行正常的清理操作,如关闭文件、释放内存等。

库函数 exit():终止当前进程,但是在此之前会执行 3 种清理操作:
        (1) 调用所有通过 atexit()注册的终止处理函数(自定义);
        (2) 刷新所有标准 I/O 缓冲区(刷写缓存到文件);
        (3) 关闭所有打开的标准 I/O 流(比如通过 fopen 打开的文件)。

使用场景:

1.通常在父进程中使用 exit(),以确保程序在退出前能执行清理操作,如关闭文件和刷新输出。
2.在子进程中,特别是在 fork()之后立即调用了一个执行操作(如 exec())但执行失败时,
      推荐使用_exit()或_Exit()来确保子进程的快速、干净地退出,避免执行标准的清理操作,
      这些操作可能会与父进程发生冲突或不必要的重复。

2.2 综合案例

创建文件 system_call_test.c,写入以下内容:

#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    int fd = open("io.txt", O_RDONLY);
    if (fd == -1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }
    char buffer[1024]; // 创建一个缓冲区来存放读取的数据
    ssize_t bytes_read;
    while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0)
    {
        // 将读取的数据写入标准输出
        write(STDOUT_FILENO, buffer, bytes_read);
    }
    if (bytes_read == -1)
    {
        perror("read");
        close(fd);
        exit(EXIT_FAILURE);
    }
    close(fd); // 使用完毕后关闭文件描述符
    return 0;
}

三、文件描述符

        在 Linux 系统中,当我们打开或创建一个文件(或套接字)时,操作系统会提供一个文件描述符(File Descriptor, FD),这是一个非负整数,我们可以通过它来进行读写等操作。然而,文件描述符本身只是操作系统为应用程序操作底层资源(如文件、套接字等)所提供的一个引用或“句柄”。在 Linux 中,文件描述符 0、 1、 2 是有特殊含义的。

        ➢ 0 是标准输入(stdin)的文件描述符

        ➢ 1 是标准输出(stdout)的文件描述符

        ➢ 2 是标准错误(stderr)的文件描述符

3.1 文件描述符关联的数据结构

struct file:

        每个文件描述符都关联到内核一个 struct file 类型的结构体数据,结构体定义位于 Linux 系统的/usr/src/linux-hwe-6.5-headers-6.5.0-27/include/linux/fs.h文件中,从 992 行开始。

struct file
{
    ...... 
    atomic_long_t f_count; // 引用计数,管理文件对象的生命周期
    struct mutex f_pos_lock;      // 保护文件位置的互斥锁
    loff_t f_pos;                 // 当前文件位置(读写位置)
    ...... 
    struct path f_path;    // 记录文件路径
    struct inode *f_inode;        // 指向与文件相关联的 inode 对象的
    指针,该对象用于维护文件元数据,如文件类型、访问权限等
    const struct file_operations *f_op; // 指向文件操作函数表的指针,定义
    了文件支持的操作,如读、写、锁定等
    ...... 
    void *private_data; // 存储特定驱动或模块的私有数据
    ......
} __randomize_layout
    __attribute__((aligned(4)));
struct path:
struct path
{
    struct vfsmount *mnt;    // 是虚拟文件系统挂载点的表示,存储有关挂载文件系统的信息
    struct dentry *dentry;   // 目录项结构体,代表了文件系统中的一个目录项。目录项是文件系统中的一个实体,通常对应一个文件或目录的名字。通过这个类型的属性,可以定位文件位置。
} __randomize_layout;
 struct inode:
struct inode
{
    umode_t i_mode;     // 文件类型和权限。这个字段指定了文件是普通文件、目录、字符设备、块设备等,以及它的访问权限(读、写、执行)。
    unsigned short i_opflags;
    kuid_t i_uid;       // 文件的用户 ID,决定了文件的拥有者。
    kgid_t i_gid;       // 文件的组 ID,决定了文件的拥有者组。
    unsigned int i_flags;
    ...... 
    unsigned long i_ino; // inode 编号,是文件系统中文件的唯一标识。
    ...... 
    loff_t i_size;       // 文件大小
} __randomize_layout;

3.2 文件描述符表关联的数据结构 

struct files_struct 是用来维护一个进程(下文介绍)中所有打开文件信息的。

struct files_struct
{
    ...... 
    struct fdtable __rcu *fdt;                    // 指向当前使用的文件描述符表(fdtable)
    ...... 
    unsigned int next_fd;                         // 存储下一个可用的最小文件描述符编号
    ...... 
    struct file __rcu *fd_array[NR_OPEN_DEFAULT]; // struct file 指针的数组,大小固定,用于快速访问。
};

打开文件描述符表底层的数据结构是 struct fdtable。

struct fdtable {
    unsigned int max_fds;     // 文件描述符数组的容量,即可用的最大文件描述符
    struct file __rcu **fd;   // 指向 struct file 指针数组的指针
    unsigned long *close_on_exec;
    unsigned long *open_fds;
    unsigned long *full_fds_bits;
    struct rcu_head rcu;
}

fd_array 和 fd

fd_array 是一个定长数组,用于存储进程最常用的 struct file。

fd 是一个指针,可以指向任何大小的数组,其大小由 max_fds 字段控制。它可以根据需要动态扩展,以容纳更多的文件描述符。

3.3 文件描述符引用图解

嵌入式Linux之文件IO_第1张图片 图:文件描述符引用图解

        總結:当我们执行 open() 等系统调用时,内核会创建一个新的 struct file,这个数据结构记录了文件的元数据(文件类型、权限等)、文件路径、支持的操作等,然后分配文件描述符,将 struct file 维护在文件描述符表中,最后将文件描述符返回给应用程序。我们可以通过后者对文件执行它所支持的各种函数操作,而这些函数的函数指针都维护在struct file_operations 数据结构中。文件描述符实质上是底层数据结构 struct file 的一个引用或者句柄,它为用户提供了操作底层文件的入口。

你可能感兴趣的:(linux,c++,c语言)