【Linux】从C语言文件操作 到Linux文件IO | 文件系统调用

news2025/1/19 3:43:37

文章目录

  • 前言
  • 一、C语言文件I/O复习
    • 文件操作:打开和关闭
    • 文件操作:顺序读写
    • 文件操作:随机读写
    • stdin、stdout、stderr
  • 二、承上启下
  • 三、Linux系统的文件I/O
    • 系统调用接口介绍
      • open()
      • close()
      • read()
      • write()
      • lseek()

Linux文件相关重点

  • 复习C文件IO相关操作
  • 认识文件相关系统调用接口
  • 认识文件描述符,理解重定向
  • 对比fd和FILE,理解系统调用和库函数的关系
  • 理解文件系统中inode的概念
  • 认识软硬链接,对比区别
  • 认识动态静态库,学会结合gcc选项,制作动静态库

前言

  1. 文件 = 内容 + 属性

  2. 所以对文件的所有操作被分为:
    a. 对内容操作
    b. 对属性操作

  3. 我们要访问一个文件的时候,都是要先把这个文件打开的:
    打开前:是普通的磁盘文件
    打开后:文件被加载到内存中
    打开的步骤是由操作系统来做的!

  4. 一个进程可以打开很多文件,所以操作系统运行时被打开的文件是很多的,操作系统当然要对这些被打开的文件做管理,管理的方式是:先描述,再组织。因此,一个文件要被打开,一定要先在内核中,形成被打开的文件对象。请添加图片描述

  5. 本文研究的文件操作的本质是:进程内存中被打开(被加载)的文件 的关系


一、C语言文件I/O复习

文件操作:打开和关闭

函数签名描述
FILE fopen(const char path, const char* mode)打开文件并返回指向文件的指针
int fclose(FILE *stream)关闭文件
模式描述
“r”读取:打开文件进行输入操作。文件必须存在。
“w”写入:创建一个空文件进行输出操作。如果同名文件已存在,其内容将被丢弃,文件被视为新的空文件。
“a”追加:打开文件进行输出操作,将数据追加到文件末尾。输出操作总是在文件末尾写入数据,扩展文件大小。重新定位操作(fseek、fsetpos、rewind)将被忽略。如果文件不存在,则创建文件。
“r+”读取/更新:打开文件进行更新操作(既可读又可写)。文件必须存在。
“w+”写入/更新:创建一个空文件并打开它进行更新操作(既可读又可写)。如果同名文件已存在,其内容将被丢弃,文件被视为新的空文件。
“a+”追加/更新:打开文件进行更新操作(既可读又可写),所有输出操作都在文件末尾写入数据。重新定位操作(fseek、fsetpos、rewind)影响下一次的输入操作,但输出操作将位置移回文件末尾。如果文件不存在,则创建文件。

[!Attention] 不带+号的模式在文件打开时会对原文件进行擦除(覆盖)操作,具体来说:

  • "w"模式: 如果使用 “w” 模式打开一个文件,它会创建一个空文件,如果同名文件已存在,则会清空该文件的内容。换句话说,打开文件时,如果文件已经存在,原文件的内容将被抹掉。
  • "a"模式: 如果使用 “a” 模式打开一个文件,文件指针会移动到文件末尾,写入的数据将追加到文件的末尾。如果文件不存在,则会创建一个新文件。原文件的内容不会被清空,而是保留在文件中。

这是在不带+号的写入模式下的行为。要同时进行读写而不清空文件内容,可以考虑使用带+号的模式,如 “r+” 或 “a+”。

实验一下:

#include <stdio.h>

int main() 
{
    FILE *filePointer;
	
    // 打开文件
    filePointer = fopen("test.txt", "w");
    if (filePointer == NULL) {
        printf("文件打开失败。\n");
        return 1;
    }
	
    printf("文件打开成功,执行其他文件操作...\n");
	
    // 执行其他文件操作...
	
    // 关闭文件
    if (fclose(filePointer) == 0) {
        printf("文件关闭成功。\n");
    } else {
        printf("文件关闭失败。\n");
    }
	
    return 0;
}

请添加图片描述

文件操作:顺序读写

函数签名描述
int fputc(int c, FILE *stream)将一个字符写入文件
int fgetc(FILE *stream)从文件中读取一个字符
char *fgets(char *s, int size, FILE *stream)从文件中读取一行内容,并存储到字符串 s 中
int fputs(const char *s, FILE *stream)将字符串 s 写入文件
size_t fread(void *ptr, size_t size, size_t count, FILE *stream)从文件中读取二进制数据
size_t fwrite(void *ptr, size_t size, size_t count, FILE *stream)向文件中写入二进制数据
#include <stdio.h>

int main() {
    FILE *filePointer;
    char ch;

    // 写入文件
    filePointer = fopen("test.txt", "w");
    if (filePointer == NULL) {
        printf("文件打开失败。\n");
        return 1;
    }

    fputc('A', filePointer);

    fclose(filePointer);

    // 读取文件
    filePointer = fopen("test.txt", "r");
    if (filePointer == NULL) {
        printf("文件打开失败。\n");
        return 1;
    }

    ch = fgetc(filePointer);
    printf("读取的字符:%c\n", ch);

    fclose(filePointer);

    return 0;
}

请添加图片描述

文件操作:随机读写

函数签名描述
int fseek(FILE *stream, long offset, int whence)设置文件指针偏移量,用于定位读写位置
long ftell(FILE *stream)返回当前文件指针的位置
void rewind(FILE *stream)将文件指针重置到文件开头
int feof(FILE *stream)检测是否到达文件末尾
#include <stdio.h>

int main() {
    FILE *filePointer;

    // 写入文件
    filePointer = fopen("test.txt", "w");
    if (filePointer == NULL) {
        printf("文件打开失败。\n");
        return 1;
    }

    fputs("Hello, World!", filePointer);

    fclose(filePointer);

    // 随机读取文件
    filePointer = fopen("test.txt", "r");
    if (filePointer == NULL) {
        printf("文件打开失败。\n");
        return 1;
    }

    fseek(filePointer, 7, SEEK_SET); // 移动到文件第8个字符的位置

    char ch = fgetc(filePointer);
    printf("随机读取的字符:%c\n", ch);

    fclose(filePointer);

    return 0;
}

请添加图片描述

stdin、stdout、stderr

  • C默认打开的三个输入输出流是:stdinstdoutstderr
  • 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
  • 这三个流通常在程序开始运行时就已经打开,并且不需要使用fopen等函数手动打开。它们分别用于标准输入、标准输出和标准错误输出。
  1. stdin(标准输入流):

    • stdin代表标准输入流,通常与键盘输入相关联。
    • 对应的文件指针是FILE* stdin
    • 你可以使用scanf等函数从stdin中读取输入数据。
  2. stdout(标准输出流):

    • stdout代表标准输出流,通常与屏幕输出相关联。
    • 对应的文件指针是FILE* stdout
    • 你可以使用printf等函数将输出写入到stdout中。
  3. stderr(标准错误输出流):

    • stderr代表标准错误输出流,通常也与屏幕输出相关联。
    • 对应的文件指针是FILE* stderr
    • stdout相比,stderr通常用于输出错误消息,以便在程序发生错误时将错误信息与正常输出区分开。

这些标准流的使用使得C程序能够在不同环境中运行,而不用关心具体的输入和输出设备。在程序中,你可以直接使用这些流,而无需显式打开或关闭它们。例如,可以通过fprintf将输出写入到文件,而不仅仅是屏幕,或者通过fscanf从文件而不是键盘读取输入。


二、承上启下

  • 上面的fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
  • 而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
  • 回忆一下我们讲【Linux】从冯诺依曼体系结构到操作系统 时,画的一张图!请添加图片描述

系统调用接口和库函数的关系,就是库函数封装了系统调用接口。
所以可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
如何封装?
fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。


三、Linux系统的文件I/O

系统调用接口介绍

先介绍一个小技巧

关于Linux常用的传参方式:函数传入标志位的小技巧


C语言常通过一个整形来传递选项,但是当选项较多时,每一个选项都用一个整形太浪费空间,所以有人想出了办法 – 使用一个比特位来传递一个选项,这样一个整形就可以传递32种选项,大大节省了空间,具体案例如下:

#include <stdio.h>

#define Print1 1      // 0001
#define Print2 (1<<1) // 0010
#define Print3 (1<<2) // 0100
#define Print4 (1<<3) // 1000

void Print(int flags)
{
   if(flags&Print1) printf("hello 1\n");
   if(flags&Print2) printf("hello 2\n");
   if(flags&Print3) printf("hello 3\n");
   if(flags&Print4) printf("hello 4\n");
}


int main()
{
   Print(Print1);
   Print(Print1|Print2);
   Print(Print1|Print2|Print3);
   Print(Print3|Print4);
   Print(Print4);
   return 0;
}

请添加图片描述

如上,我们将宏与比特位对应,然后在 Print 函数中编写每一个宏对应的功能,之后我们就可以在其他函数中通过调用 Func 函数并传递对应的选项来达到我们想要的效果,并且我们可以通过按位或来实现同时传递几个选项。

open()

  • 系统调用open:用于打开或创建一个文件。open函数是一个系统调用,用于打开文件或创建新文件,返回值是一个文件描述符,后续的文件操作可以使用这个文件描述符。请添加图片描述

  • pathname: 要打开或创建的目标文件

  • flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“按位或”运算,构成flags。

  • 常用的flags的可选参数:

    文件打开方式含义如果指定文件不存在
    O_RDONLY以只读形式打开出错
    O_WRONLY以只写形式打开出错
    O_RDWR以读写形式打开出错
    O_APPEND向文本文件尾添加数据出错
    O_CREAT如果文件不存在,创建新文件建立一个新的文件
    O_TRUNC打开文件时清空文件中之前的数据出错

close()

  • 系统调用close:关闭一个文件请添加图片描述

read()

  • read:从文件中读数据 请添加图片描述

  • 返回值
    如果成功,则返回读取的字节数(0表示文件结束),并将文件位置提前该字节数。如果这个数字小于请求的字节数,则不会报错;例如,发生这种情况可能是因为现在实际可用的字节数减少了(可能是因为接近文件末尾,或者因为我们正在从管道或终端读取数据),或者因为read()被信号中断。发生错误时,返回-1,并适当地设置errno。在这种情况下,文件位置(如果有的话)是否改变是未指定的。

下面用read.c来测试read()系统调用:

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

#define FILE_NAME "file1.txt"

int main() {
    int fd = open(FILE_NAME, O_RDONLY);
    if(fd == -1) {
        perror("open");
        return 1;
    }

    char buf[1024];
    //C语言字符串以'\0'结尾,所以留一个位置来放置
    int ret = read(fd, buf, sizeof(buf) - 1);
    //read读到文件末尾返回0
    while(ret != 0) {
        buf[ret] = '\0';
        printf("%s", buf);
        ret = read(fd, buf, sizeof(buf) - 1);
    }

    close(fd);
}

现象:
请添加图片描述

write()

  • write:向文件中写数据请添加图片描述

下面用write.c来测试write()系统调用:

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

#define FILE_NAME1 "file1.txt"  //已存在
#define FILE_NAME2 "file2.txt"  //不存在

int main() {
    //以只写形式打开,并清空文件中之前的数据
    int fd1 = open(FILE_NAME1, O_WRONLY | O_TRUNC); 

    //创建文件并以只写形式打开,并指定文件的默认权限为0666(还受umask的影响)
    //同时,我们可以通过umask接口手动设置当前进程的文件掩码,而不使用从父进程继承过来的umask
    umask(0000); 
    int fd2 = open(FILE_NAME2, O_WRONLY | O_CREAT | O_TRUNC, 0666);
	
	//错误处理
    if(fd1 == -1 || fd2 == -1) {
        perror("open");
        return 1;
    }

    const char* buf1 = "hello file1\n";
    const char* buf2 = "hello file2\n";

    int cnt = 5;
    while(cnt--) {
        //注意:这里strlen求得的长度不用加1,因为字符串以'\0'结尾只是C语言的特性,而文件中并不这样规定
        write(fd1, buf1, strlen(buf1));
        write(fd2, buf2, strlen(buf2));
    }

    close(fd1);
    close(fd2);
}

现象:
请添加图片描述

[!Attention] 上面的文件操作的三个细节:

  1. 如果在向文件中写入数据时没有指定O_TRUNC选项,而是直接写入数据,新数据会从文件的当前位置开始写入,而不会影响文件中原有的数据。如果新数据的长度小于文件当前的大小,那么文件的尾部会保留原有的数据,就比如先写入五行 hello world,再写入五行 hello:请添加图片描述

  2. 创建 file2.txt 时我们通过 umask 系统调用将 umask 由默认的 0002 设置为了 0000(第一个0代表八进制),然后将 open() 系统调用的最后一个参数 mode 设置为 0666,所以 file2 的最终权限为 文件的默认权限 & ~umask0666 & ~0000 亦即 0666:请添加图片描述

  3. 在C语言中,字符串是以'\0'(空字符或Null字符)结尾的字符数组。但是,在文件中存储字符串时,并不要求在文件中以'\0'结尾。文件系统仅是按照写入的字节数来存储数据,而不关心字符串的结尾字符。因此,当你使用write函数将字符串写入文件时,不需要把字符串结尾的'\0'字符写入文件,只需写入字符串本身即可。
    如果在写入文件时将'\0'字符写入:
    请添加图片描述

  • 可能会导致一些乱码的问题:
    请添加图片描述

  • 所以说write函数的第三个参数 count 应该设置为 strlen(str),表示写入字符串的长度,而不包括字符串结尾的'\0'字符。


lseek()

lseek(“lseek"代表"long seek”)是Linux系统调用之一,用于在文件中移动文件指针的位置。它是对文件进行随机访问的关键系统调用之一。lseek可以用于设置文件偏移量,以便在文件中执行读取或写入操作。

函数原型如下:

请添加图片描述

  • fd 是文件描述符,表示要操作的文件。
  • offset 是文件偏移量,可以为正数、负数或零,用于指定相对于whence参数的偏移位置。
  • whence 指定了偏移量的基准位置,可以是以下值之一:
    • SEEK_SET:相对于文件的起始位置进行偏移。
    • SEEK_CUR:相对于当前文件指针的位置进行偏移。
    • SEEK_END:相对于文件的末尾位置进行偏移。

lseek函数的返回值是新的文件偏移量,如果调用出现错误,则返回 -1

使用示例:

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    int fd = open("example.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 使用 lseek 将文件指针移动到文件末尾
    off_t end_position = lseek(fd, 0, SEEK_END);
    if (end_position == -1) {
        perror("lseek");
        close(fd);
        return 1;
    }

    printf("当前文件大小:%lld 字节\n", (long long)end_position);

    // 使用 lseek 将文件指针移动到文件开头
    off_t start_position = lseek(fd, 0, SEEK_SET);
    if (start_position == -1) {
        perror("lseek");
        close(fd);
        return 1;
    }

    printf("文件指针已重置到文件开头\n");

    close(fd);
    return 0;
}

在上述示例中,lseek函数用于将文件指针移动到文件的末尾,获取文件的大小,然后将文件指针重新设置到文件开头。这演示了lseek在文件中移动文件指针的基本用法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1416362.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

南京观海微电子---如何减少时序报告中的逻辑延迟

1. 引言 在FPGA逻辑电路设计中&#xff0c;FPGA设计能达到的最高性能往往由以下因素决定&#xff1a; ▪ 工作时钟偏移和时钟不确定性&#xff1b; ▪ 逻辑延迟&#xff1a;在一个时钟周期内信号经过的逻辑量&#xff1b; ▪ 网络或路径延迟&#xff1a;Vivado布局布线后引…

网络服务综合练习

综合练习&#xff1a;请给openlab搭建web网站 网站需求&#xff1a; 1.基于域名www.openlab.com可以访问网站内容为 welcome to openlab!!! 2.给该公司创建三个子界面分别显示学生信息&#xff0c;教学资料 和缴费网站&#xff0c;基于&#xff0c;www.openlab.com/data网…

什么是跳表,Java如何实现跳表?

1. 问题引入&#xff0c;相较于有序链表我们为什么需要跳表&#xff1f; 1.1 首先我们需要了解什么是有序链表 如图&#xff1a; 每个链表存在一个指向下一节点的指针&#xff0c;如果我们要对其任一节点进行增删改&#xff0c;都需要先使用迭代器进行查询&#xff0c;找到指…

【数字量采集1.28】数字信号采集

数字量采集-2024年01月27日-二刷 文章目录 分析考虑一个波形的六要素&#xff1a;项目需求分析&#xff1a;高低电平数字量采集电路设计RS485差分信号传输SP3485 芯片引脚RS485 转 TTL 电路 分析考虑一个波形的六要素&#xff1a; 高电平 低电平 上升时间 下降时间 频率/周期 …

Vue2 VS Vue3 生命周期

一、生命周期的概念 Vue组件实例在创建时要经历一系列的初始化步骤&#xff0c;在此过程中Vue会在合适的时机&#xff0c;调用特定的函数&#xff0c;从而让开发者有机会在特定阶段运行自己的代码&#xff0c;这些特定的函数统称为&#xff1a;生命周期钩子&#xff08;也会叫…

【C++中的STL】常用算法3——常用拷贝和替换算法

常用算法3 copyreplacereplace_ifswap 1、 copy容器内指定范围的元素拷贝到另一个容器中 2、 replace将容器内指定的旧元素修改为新元素 3、 replace_if容器内指定范围满足条件的元素替换为新元素 4、 swap互换两个容器的元素 copy 容器内指定范围的元素拷贝到另一个容器中…

布隆过滤器介绍及实战应用(防止缓存穿透)

布隆过滤器介绍 布隆过滤器&#xff08;Bloom Filter&#xff09;是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多&#xff0c;缺点是有一…

最全全国十七个数据入表和资产化案例深度解析

2024年1月1日起&#xff0c;财政部会计司发布的《企业数据资源相关会计处理暂行规定》正式施行&#xff0c;规定为数据资源的会计处理提供了明确的指导原则。这一里程碑事件也标志着我国在数据资产入表领域正式进入实际操作阶段&#xff0c;随后&#xff0c;数据资产入表在全国…

[BJDCTF 2020]Easy

运行之后是这个东西 我们直接IDA暴力打开 结果main函数啥也不是 &#xff08;看其他人的wp知道了照que函数&#xff09; 我也不知道咋找的&#xff0c;可能真要硬找吧 int ques() {int v0; // edxint result; // eaxint v2[50]; // [esp20h] [ebp-128h] BYREFint v3; // [e…

在vscode里面聊微信

### 1、源起 事情是这样的&#xff0c;某天下午&#xff0c;我在做项目的时候被人事叫去谈话&#xff0c;说些有的没得&#xff0c;但是我注意到她说我不要玩微信&#xff0c;“我好几次都看到你在和别人聊微信”之类的话&#xff0c;所以我打算在ide工具的命令行中聊微信&…

聊聊效能与敏捷的差异

这是鼎叔的第八十四篇原创文章。行业大牛和刚毕业的小白&#xff0c;都可以进来聊聊。 欢迎关注本专栏和微信公众号《敏捷测试转型》&#xff0c;星标收藏&#xff0c;大量原创思考文章陆续推出。 近期&#xff0c;TesterHome社区小道消息播客直播间邀请了鼎叔&#xff0c;与…

程序员的基本素养之——R语言起源、特点以及应用

R语言是一种功能强大的数据分析、统计建模、可视化、 免费、开源且跨平台的编程语言 作为用于数据统计的必备技能语言&#xff0c;博主目前正在对R语言进行基本的学习&#xff0c;这也是生物信息学领域进行统计分析的必备语言之一。下面跟我来一起看看吧&#xff01; R语言是一…

产品解读 | 新一代湖仓集存储,多模型统一架构,高效挖掘数据价值

星环科技TDH一直致力于给用户带来高性能、高可靠的一站式大数据基础平台&#xff0c;满足对海量数据的存储和复杂业务的处理需求。 同时在易用性方面持续深耕&#xff0c;降低用户开发和运维成本&#xff0c;让数据处理平民化&#xff0c;助力用户以更便捷、高效的方式去挖掘数…

【PyTorch】n卡驱动、CUDA Toolkit、cuDNN全解安装教程

文章目录 GPU、NVIDIA Graphics Drivers、CUDA、CUDA Toolkit和cuDNN的关系使用情形判断仅仅使用PyTorch使用torch的第三方子模块 安装NVIDIA Graphics Drivers&#xff08;可跳过&#xff09;前言Linux法一&#xff1a;图形化界面安装&#xff08;推荐&#xff09;法二&#x…

第十三章认识Ajax(四)

认识FormData对象 FormData对象用于创建一个表示HTML表单数据的键值对集合。 它可以用于发送AJAX请求或通过XMLHttpRequest发送表单数据。 以下是FormData对象的一些作用&#xff1a; 收集表单数据&#xff1a;通过将FormData对象与表单元素关联&#xff0c;可以方便地收集表…

【GitHub项目推荐--建一个 ChatGPT 机器人】【转载】

建一个 ChatGPT 机器人 bot-on-anything 它可以将 ChatGPT 等算法模型应用于各类平台。目前&#xff0c;它已经可以接入到个人微信、公众号、QQ、Telegram、Gmail邮箱、Slack 等待&#xff0c;并计划接入Web、企业微信、钉钉等。 通过使用该开源项目&#xff0c;开发者可以通…

一天吃透面试八股文

内容摘自我的学习网站&#xff1a;topjavaer.cn 分享50道Java并发高频面试题。 线程池 线程池&#xff1a;一个管理线程的池子。 为什么平时都是使用线程池创建线程&#xff0c;直接new一个线程不好吗&#xff1f; 嗯&#xff0c;手动创建线程有两个缺点 不受控风险频繁创…

机器学习之numpy库

机器学习之numpy库 numpy库概述numpy库历史numpy的核心numpy基础ndarray数组内存中的ndarray对象ndarray数组对象的特点ndarray数组对象的创建ndarray对象属性的基本操作数组的维度元素的类型数组元素的个数数组元素索引(下标) ndarray对象数组的自定义类型切片操作一维数组切片…

【LTSpice】导入第三方元件库 之 subckt文件类型

LTSpice想要导入第三方的元件库&#xff0c;网上教程非常多。这里记录一下一种不用include命令实现、以后可以直接在component里面添加的 subckt文件的导入。过程比较复杂。 本文只讲解subckt文件&#xff01;如果发现文件里有.SUBCKT这样的文字&#xff0c;说明可以用本文的方…

[UI5 常用控件] 03.Icon, Avatar,Image

文章目录 前言1. Icon2. Avatar2.1 displayShape2.2 initials2.3 backgroundColor2.4 Size2.5 fallbackIcon2.6 badgeIcon2.7 badgeValueState2.8 active 3. Image 前言 本章节记录常用控件Title,Link,Label。 其路径分别是&#xff1a; sap.m.Iconsap.m.Avatarsap.m.Image 1…