Linux应用编程(文件I/O)

embedded/2024/11/27 8:14:19/

目录

1. 文件I/O与标准I/O的区别

1.1 概念

1.2 缓冲方式

1.3 使用便捷性

1.4 可移植性

1.5 性能特点

2. 文件描述符

2.1 定义

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

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

2.4 引用图解

2.5 小结

3.标准I/O

3.1 打开/关闭文件 

3.2 向文件写入数据

3.3 从文件读取数据

3.4 标准输入/输出/错误

4.文件I/O

4.1 数据类型说明

4.2 open

4.3 read 

4.4 write

4.5 close

5. exit和_exit()

5.1 系统调用_exit()

5.2 库函数exit()

5.3 使用场景

6.综合案例

6.1 标准I/O

6.2 系统调用


1. 文件I/O与标准I/O的区别

1.1 概念

  • 文件 I/O(Input/Output):也称为低级 I/O,它是操作系统提供的基本 I/O 操作接口,直接对文件描述符进行操作。文件描述符是一个非负整数,用于标识一个打开的文件。在 Unix/Linux 系统中,像openreadwriteclose等系统调用都属于文件 I/O 操作。
  • 标准 I/O:也称为高级 I/O,它是在文件 I/O 的基础上构建的一种更为方便的 I/O 库。标准 I/O 提供了缓冲机制,它会自动为输入输出操作分配缓冲区,减少系统调用的次数,提高效率。在 C 语言中,stdio.h头文件中定义的函数(如printfscanffgetsfputs等)都属于标准 I/O。

1.2 缓冲方式

  • 文件 I/O:通常没有自带的缓冲机制。每次readwrite操作都会直接调用系统调用,将数据从用户空间缓冲区复制到内核缓冲区或者反之。这意味着频繁的小数据读写可能会导致大量的系统调用开销。例如,使用write系统调用向文件写入一个字节的数据,就会触发一次系统调用。
  • 标准 I/O:带有缓冲机制。有三种类型的缓冲区:全缓冲、行缓冲和无缓冲。
    • 无缓冲:数据直接写入文件或者设备,不经过缓冲区。例如标准错误输出(stderr)通常是无缓冲的,这样可以确保错误信息能够及时显示,不会因为缓冲区的原因而延迟。
    • 行缓冲:当遇到换行符\n或者缓冲区满时进行 I/O 操作。比如在标准输出(stdout)中,默认是行缓冲的。所以当使用printf输出带有换行符的数据时,数据会立即显示在屏幕上;如果没有换行符,数据可能会暂时存放在缓冲区中,直到缓冲区满或者遇到下一个换行符才输出。
    • 全缓冲:当缓冲区填满时才进行实际的 I/O 操作。例如,向一个文件使用标准 I/O 写入数据,数据会先存放在缓冲区中,直到缓冲区满了,才会将数据一次性写入文件。这种方式适用于文件等大多数情况,能够有效减少系统调用次数。

1.3 使用便捷性

  • 文件 I/O:使用相对复杂。因为它是直接和操作系统的底层接口打交道,需要手动管理文件描述符,处理错误返回值等。例如,在使用open系统调用打开文件时,需要检查返回的文件描述符是否为负数,以判断打开文件是否成功。而且在进行读写操作时,需要明确指定读取或写入的字节数等参数。
  • 标准 I/O:使用起来更加方便。它提供了更高级的接口,隐藏了很多底层细节。例如,使用fgets函数可以很方便地从文件中读取一行数据,不需要考虑像文件 I/O 那样的文件描述符管理和字节数计算等复杂问题。同时,标准 I/O 函数的参数形式通常更符合人们的编程习惯。

1.4 可移植性

  • 文件 I/O:在不同的操作系统上,系统调用的实现细节和参数可能会有所不同。这就导致使用文件 I/O 编写的程序在跨平台移植时可能需要进行较多的修改。例如,open系统调用在 Unix/Linux 和 Windows 系统下的参数和返回值可能有差异。
  • 标准 I/O:具有更好的可移植性。因为标准 I/O 库在不同的操作系统上都有相应的实现,并且尽可能地提供了统一的接口。只要是遵循标准 C 语言规范的编译器,基本都支持标准 I/O 函数,这样在不同平台之间移植程序时,使用标准 I/O 的部分代码通常不需要太多修改。

1.5 性能特点

  • 文件 I/O:对于少量数据的频繁读写,由于没有缓冲机制,系统调用开销较大,性能可能较差。但是在某些特定场景下,比如对实时性要求极高,不希望数据被缓冲的情况下,文件 I/O 可以直接操作文件,避免缓冲带来的延迟。
  • 标准 I/O:通过缓冲机制可以减少系统调用次数,对于大量数据的读写或者频繁的小数据读写(只要缓冲区大小合适)能够提高性能。不过,如果缓冲区设置不合理,可能会导致数据延迟写入或者占用过多内存等问题。

2. 文件描述符

2.1 定义

在Linux系统中,当我们打开或创建一个文件(或套接字)时,操作系统会提供一个文件描述符(File Descriptor,FD),这是一个非负整数,我们可以通过它来进行读写等操作。

文件描述符本身只是操作系统为应用程序操作底层资源(如文件、套接字等)所提供的一个引用或“句柄”。

在Linux中,文件描述符0、1、2是有特殊含义的。

  • 0是标准输入(stdin)的文件描述符
  • 1是标准输出(stdout)的文件描述符
  • 2是标准错误(stderr)的文件描述符

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

1.struct file

每个文件描述符都关联到内核一个struct file类型的结构体数据,该结构体的部分关键字段如下:

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)));

这个数据结构记录了与文件相关的所有信息,其中比较关键的是f_path记录了文件的路径信息,f_inode,记录了文件的元数据。

2.struct path

结构体定义如下:

struct path {struct vfsmount *mnt;struct dentry *dentry;
} __randomize_layout;
  • struct vfsmount:是虚拟文件系统挂载点的表示,存储有关挂载文件系统的信息。
  • struct dentry:目录项结构体,代表了文件系统中的一个目录项。目录项是文件系统中的一个实体,通常对应一个文件或目录的名字。通过这个类型的属性,可以定位文件位置。

3.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;

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

1.打开的文件表数据结构

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指针的数组,大小固定,用于快速访问。
};

fdt维护了文件描述符表,其中记录了所有打开的文件描述符和struct file的对应关系。

2.打开文件描述符表

打开文件描述符表底层的数据结构是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;
};

3.fd_array和fd

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

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

当打开文件描述符的数量不多于NR_OPEN_DEFAULT时,fd指向的通常就是fd_array,当文件描述符的数量超过NR_OPEN_DEFAULT时,会发生动态扩容,会将fd_array的内容复制到扩容后的指针数组,fd指向扩容后的指针数组。这一过程是内核控制的。

4.文件描述符和fd或fd_array的关系 

文件描述符是一个非负整数,其值实际上就是其关联的struct file在fd指向的数组或fd_array中的下标。

2.4 引用图解

2.5 小结

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

3.标准I/O

3.1 打开/关闭文件 

1.fopen

函数作用:打开文件

函数原型: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 * 结构体指针 表示一个文件

2.fclose

函数作用:关闭文件

函数原型:int fclose (FILE *__stream)

函数解析:

    FILE *__stream: 需要关闭的文件return: 成功返回0 失败返回EOF(负数) 通常失败会造成系统崩溃

3.2 向文件写入数据

1.fputc

函数作用:写入一个字符

函数原型:int fputc (int __c, FILE *__stream)

函数解析:

    int __c: 写入的char按照AICII值写入 可提前声明一个charFILE *__stream: 要写入的文件,写在哪里取决于访问模式return: 成功返回char的值 失败返回EOF

2.fputs

函数作用:写入一个字符串

函数原型:int fputs (const char *__restrict __s, FILE *__restrict __stream)

函数解析:

    char *__restrict __s: 需要写入的字符串FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问模式return: 成功返回非负整数(一般是0,1) 失败返回EOF

3.fprintf

函数作用:将格式化的数据输出到指定的文件流

函数原型:fprintf (FILE *__restrict __stream, const char *__restrict __fmt, ...)

函数解析:

    FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问模式char *__restrict __fmt: 格式化字符串...: 变长参数列表return: 成功返回正整数(写入字符总数不包含换行符) 失败返回EOF

3.3 从文件读取数据

1.fgetc

函数作用:读取一个字符

函数原型:int fgetc (FILE *__stream)

函数解析:

    FILE *__stream: 需要读取的文件return: 读取的一个字节 到文件结尾或出错返回EOF

2.fgets

函数作用:读取一个字符串

函数原型:fgets (char *__restrict __s, int __n, FILE *__restrict __stream)

函数解析:

    char *__restrict __s: 接收读取的数据字符串int __n: 能够接收数据的长度FILE *__restrict __stream: 需要读取的文件return: 成功返回字符串 失败返回NULL(可以直接用于while)

3.fscanf

函数作用:从指定的文件流中读取格式化的数据

函数原型:int fscanf (FILE *__restrict __stream,  const char *__restrict __format, ...)

函数解析:

    FILE *__restrict __stream: 读取的文件char *__restrict __format: 读取的匹配表达式...: 变长参数列表 用于接收匹配的数据return: 成功返回参数的个数  失败返回0 报错或结束返回EOF

3.4 标准输入/输出/错误

stdin: 标准输入FILE *

stdout: 标准输出FILE * 写入这个文件流会将数据输出到控制台

stderr: 错误输出FILE * 一般用于输出错误日志

4.文件I/O

4.1 数据类型说明

1. ssize_t

ssize_t相关的宏定义如下:

typedef __ssize_t ssize_t;
__STD_TYPE __SSIZE_T_TYPE __ssize_t;
# define __STD_TYPE     typedef
#define __SSIZE_T_TYPE      __SWORD_TYPE
# define __SWORD_TYPE       long int

ssize_t是__ssize_t的别名,后者是long int的别名,long是long int的简写,因此,ssize_t实际上是long类型的别名。

2. size_t

size_t相关的宏定义如下:

typedef __SIZE_TYPE__ size_t;
#define __SIZE_TYPE__ long unsigned int

unsigned long是long unsigned int的简写,size_t实质上是unsigned long。

4.2 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以指示错误原因。

4.3 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

4.4 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

4.5 close

函数作用:在使用完成之后,关闭对文件描述符的引用

函数原型:int close (int __fd);

函数解析:

    int __fd:一个整数,表示要关闭的文件描述符return: (1) 成功关闭时 返回0(2) 失败时 返回-1

5. exit和_exit()

5.1 系统调用_exit()

_exit()是由POSIX标准定义的系统调用,用于立即终止一个进程,定义在unistd.h中。这个调用确保进程立即退出,不执行任何清理操作。

_exit()在子进程终止时特别有用,这可以防止子进程的终止影响到父进程(比如,防止子进程意外地刷新了父进程未写入的输出缓冲区)。

_exit和_Exit功能一样。

#include <unistd.h>/*** 立即终止当前进程,且不进行正常的清理操作,如关闭文件、释放内存等。这个函数通常在程序遇到严重错误需要立即退出时使用,或者在某些情况下希望避免清理工作时调用。* * int status: 父进程可接收到的退出状态码 0表示成功 非0表示各种不同的错误*/
void _exit(int status);
void _Exit (int __status) ;

5.2 库函数exit()

exit()函数是由C标准库提供的,定义在stdlib.h中。

#include <stdlib.h>/*** 终止当前进程,但是在此之前会执行3种清理操作* (1) 调用所有通过atexit()注册的终止处理函数(自定义)* (2) 刷新所有标准I/O缓冲区(刷写缓存到文件)* (3) 关闭所有打开的标准I/O流(比如通过fopen打开的文件)* * int status: 父进程可接收到的退出状态码 0表示成功 非0表示各种不同的错误*/
void exit(int status);

5.3 使用场景

① 通常在父进程中使用exit(),以确保程序在退出前能执行清理操作,如关闭文件和刷新输出。

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

6.综合案例

6.1 标准I/O

1.程序

#include <stdio.h>
#include <stdlib.h> 
#include <unistd.h>
#include <fcntl.h>int main(int argc, char const *argv[])
{char *filename = "io.txt";FILE* file_fd;// 以追加读写模式打开文件,不存在则创建file_fd = fopen(filename,"a+");if (file_fd == NULL){printf("failed 打开文件失败\n");}else{printf("success 打开文件成功\n");}// 写入字符'a'int put_result = fputc(97,file_fd);if (put_result == EOF){printf("写入文件失败\n");}else {printf("写入%c成功\n",put_result);}// 写入字符串int putsR = fputs(" love letter\n",file_fd);if (putsR == EOF){printf("写入字符串失败\n");}else {printf("写入字符串成功\n");}// 格式化写入字符串char *name = "printfR";int printfR =  fprintf(file_fd,"我是 %s",name);if (printfR == EOF){printf("字符串写入失败\n");}else{printf("字符串写入成功\n");}// 将文件指针移动到文件开头fseek(file_fd, 0, SEEK_SET);// 读取字符char c = fgetc(file_fd);if(c != EOF){printf("读取字符成功:%c",c);}printf("\n");// 读取字符串char buffer[100];char *str;str = fgets(buffer,sizeof(buffer),file_fd);     if(str != NULL){printf("读取字符串成功:%s",str);}// 格式化读取字符串int scnafR;char na[50];char me[50];scnafR = fscanf(file_fd,"%s %s",na,me);if(scnafR != EOF){printf("读取字符串成功:%s %s",na,me);}// 关闭文件int closefd = fclose(file_fd);if (closefd == EOF){printf("关闭文件失败\n");}return 0;
}

2.运行结果

控制台输出:

文件显示内容:

6.2 系统调用

1.程序

#include <stdio.h>
#include <stdlib.h> 
#include <unistd.h>
#include <fcntl.h>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;
}

2.运行结果


http://www.ppmy.cn/embedded/140846.html

相关文章

基于Boost库的搜索引擎

本专栏内容为&#xff1a;项目专栏 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;基于Boots的搜索引擎 &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&#x1f69a; &#x1f339;&#x1f339;&#x1f339;关注我带你学习编程知识…

WPF 加载页面的三种方式(瞬时加载,延迟加载,异步行为)

在WPF中&#xff0c;瞬时加载、延迟加载和异步行为分别适用于不同的场景&#xff0c;它们各自的实现方式和用途如下&#xff1a; 1. 瞬时加载 定义&#xff1a; 在赋值或初始化时&#xff0c;视图和数据会立即加载&#xff0c;所有的逻辑在主线程上完成。视图在 UI 上的渲染…

从ChatGPT到代理AI:安全领域的新变革

生成式人工智能&#xff08;genAI&#xff09;和大型语言模型&#xff08;LLMs&#xff09;的出现&#xff0c;如ChatGPT&#xff0c;引发了一场波及每个行业和企业的变革。这些技术不仅带来新的能力、效率和可能性&#xff0c;还动摇了现有的结构、流程、治理和运营活动。许多…

Unreal从入门到精通之如何绘制用于VR的3DUI交互的手柄射线

文章目录 前言实现方式MenuLaser实现步骤1.Laser和Cursor2.移植函数3.启动逻辑4.检测射线和UI的碰撞5.激活手柄射线6.更新手柄射线位置7.隐藏手柄射线8.添加手柄的Trigger监听完整节点如下:效果图前言 之前我写过一篇文章《Unreal5从入门到精通之如何在VR中使用3DUI》,其中讲…

余弦相似度

余弦相似度&#xff08;Cosine Similarity&#xff09; 是一种常用的度量两个向量之间相似度的方法&#xff0c;尤其适用于文本分析、信息检索、推荐系统等领域。它的核心思想是通过计算两个向量夹角的余弦值来衡量它们的相似度&#xff0c;而不是直接计算向量的欧几里得距离。…

SpringBoot(9)-Dubbo+Zookeeper

目录 一、了解分布式系统 二、RPC 三、Dubbo 四、SpringBootDubboZookeeper 4.1 框架搭建 4.2 实现RPC 一、了解分布式系统 分布式系统&#xff1a;由一组通过网络进行通信&#xff0c;为了完成共同的任务而协调工作的计算机节点组成的系统 二、RPC RPC&#xff1a;远程…

18:(标准库)DMA二:DMA+串口收发数据

DMA串口收发数据 1、DMA串口发送数据2、DMA中断串口接收定长数据包3、串口空闲中断DMA接收不定长数据包 1、DMA串口发送数据 当串口的波特率大于115200时&#xff0c;可以通过DMA1进行数据搬运&#xff0c;以防止数据的丢失。如上图所示&#xff1a;UART1的Tx发送请求使用DMA1的…

搜维尔科技:研究人员如何使用SenseGlove Nova触觉反馈手套远程操作机器人手

研究人员如何使用SenseGlove Nova触觉反馈手套远程操作机器人手 搜维尔科技&#xff1a;研究人员如何使用SenseGlove Nova触觉反馈手套远程操作机器人手