【Linux系统编程】文件IO 函数篇

一,对于Linux系统编程的学习指南

学习Linux系统编程,我们将花费1-2个月的时间进行学习,然后过程中需要去学习makefile的操作和一些于linux系统编程的基本工具,这里在我的makefile专栏和Linux驱动基本知识+准备工作都有写。

我们将参照unix环境高级编程这本书来参考学习

文件I/O: 3,5,14章节

文件系统:4,6,7章节

并发:(信号:10章,多进程:10,11章节)

IPC(进程间的通信):8章进程守护(涉及到多进程),13章守护进程,15,16章

二,标准I/O介绍

1,user和kernel态之间的区别

特性 用户态(user) 内核态(kernel)
权限等级 低(Ring 3) 最高(Ring 0)
执行代码 普通用户应用程序 内核、驱动程序、中断处理程序
资源访问 受限,仅能访问用户空间 无限制,可访问所有系统资源
典型指令 普通算术运算、逻辑判断 特权指令、硬件操作指令

这是操作系统最基础的安全隔离机制,几乎所有现代操作系统(包括 Linux、Windows、macOS)都采用了类似的设计理念。

1. user 用户态

  • 本质:CPU 的低权限执行模式,用于运行普通用户程序(如浏览器、文本编辑器、终端命令等)
  • 核心限制
    • 无法直接访问硬件资源(如磁盘、网卡、I/O 端口)
    • 不能执行特权指令(如修改内存管理单元配置)
    • 只能操作自身虚拟地址空间,无法直接访问内核数据结构
  • 工作机制 :需通过系统调用(System Call) 请求内核协助完成敏感操作(如文件读写、网络通信)
  • 典型场景:所有用户安装的应用程序都运行在用户态下,确保系统安全性和稳定性

2. kernel 内核态

  • 本质:CPU 的最高权限执行模式,用于运行操作系统内核、驱动程序和处理中断
  • 核心能力
    • 完全访问系统所有硬件资源和内存空间
    • 可执行 CPU 支持的任何指令(包括特权指令)
    • 管理进程调度、内存分配、设备驱动等核心系统功能
  • 工作机制:内核态代码运行在内核空间,拥有对整个系统的完全控制权
  • 典型场景:系统启动、硬件中断处理、系统调用执行时,CPU 会从用户态切换到内核态

2,user于kernel之间的文件流

这里表示的是,user在进行文件传输的时候,是通过sysio系统IO的,stdio标准IO是依赖于系统IO的,两者都可以进行文件调用,区别在于:

系统IO移植性差,标准IO的移植性好,每一个操作系统对应的sysio系统IO是不一样的,但是对于stdio标准IO在不同的操作系统下都是一样的,stdio会自动调用该操作系统对应的操作

比如 fopen 在 windows下调用的系统IO的是openfile函数,在 Liunx下调用的系统IO的是open函数

3,对于FILE的学习

这里不需要针对FILE结构体进行学习,该结构体都是在后续慢慢累积的经验,自然就可以看懂FILE这个结构体里面每一个变量的含义

三,fopen函数和fclose函数

1,fopen的使用

打开man手册观察fopen函数的定义,这里先讲解第一个,第二个第三个需要知道系统调用IO才可以理解

第一个fopen的 第一个参数表达的是打开文件的路径,第二个参数表达的是打开的方式

2,什么是errno?

复制代码
RETURN VALUE
Upon successful completion fopen(), 
fdopen() and freopen() return a FILE pointer. 
Otherwise, NULL is returned and errno is set to indicate the error.

fopen函数如果调用成功的话,那就是返回对应的FILE指针,如果调用失败的话就会返回NULL指针,然后还会返回errno值,对于这个返回情况非常好理解,但是这个errno是什么玩意?

到errno的头文件里面去

(1) 早期的errno

早期的errno是一个全局变量,当报错的时候,errno会接收到对应的宏,然后这个宏对应的数字会映射到对应的报错信息,比如ENOSYS:3 对应的报错信息是Invalid system call number这个错误

这样做的缺点就是,你需要立马打印这个报错信息,否则会被其他的报错信息给占用,因为这是一个全局变量

(2) 如今的errno

如今的errno被私有化了

  • 私有化的核心概念:通过限制资源的访问范围(如仅归某个线程 / 类 / 模块所有),避免冲突和非法修改,提升代码 / 系统的稳定性;
  • errno 的私有化 :将原本全局共享的errno改为线程局部存储(TLS)变量,让每个线程拥有独立的errno副本,解决多线程下错误码错乱的问题;
  • 关键效果 :私有化后的errno对开发者透明(使用方式不变),但底层实现了线程隔离,保证多线程错误处理的准确性。

我们来测试一下errno是否被私有化

这里不再是变量,而是一个宏了,所以被私有化了。

3,fclose的使用

这里返回值为int变量,参数是一个流,也就是对应一个fopen的返回的FILE指针,我们通过这个指针将已开放的流给释放掉

复制代码
RETURN VALUE
       Upon  successful completion, 0 is returned.  Otherwise, 
EOF is returned and errno is set to indicate
       the error.  In either case,
any further access (including another call to fclose())  to  the  stream
       results in undefined behavior.

对应的返回值就是,返回成功就是返回0,返回不成功就是EOF和一个errno

四,内存模型

这是我们的内存分布图,然后需要思考的是,fopen和fclose启动的时候,这个流是开到哪里的

1. 栈区

这个不可能,比如我们在栈区返回一个FILE指针,然后等这个fopen函数结束之后,由于这个是局部变量,栈会立马销毁,因此不对

2,静态区

这个也不可能,因为这个常年在静态区,这个FILE是被共用的,比如我开启一个文件,到打开第二个文件的时候,就会覆盖第一个文件,因此也不合理

3,堆区

这个是合理的

如何快速分辨该函数是否在堆区

可以看这个有没有对立函数

比如fopen肯定是开的,就是malloc,fclose则是释放的,因此就是free

五,fgetc函数和fputc函数

1,fgetc函数

这里是对于fgetc的一些相关定义函数

不难看到这里的返回值是一个int类型
成功之后unsigned char转换为int类型,所以以后我们要用int类型来进行接收
失败则返回EOF类型

(1) 为什么 fgetc() 要返回 int 而非 char

fgetc() 的返回值设计成 int,核心原因是为了区分有效字符和文件结束符(EOF)

  1. 字符的存储范围char 类型通常是 1 字节(8 位),如果是 signed char,范围是 -128 ~ 127;如果是 unsigned char,范围是 0 ~ 255
  2. EOF 的定义 :EOF 是宏定义(通常值为 -1),用来表示 "文件读取结束" 或 "读取失败"。
  3. 关键矛盾 :如果 fgetc() 返回 char,当读取到 ASCII 码为 255 的字符(比如扩展 ASCII 字符)时,若编译器把 char 当作 signed char,255 会被解析成 -1此时无法区分 "读取到 255 字符" 和 "读取到 EOF"
  4. 解决方案 :把读取到的 unsigned char 转换为 int(保留 0~255 的值),再返回。这样:
    • 有效字符:返回 0 ~ 255 的整数值;
    • 文件结束 / 失败:返回 -1(EOF),两者不会混淆。

fput函数会把传入的整数值转换回字符类型,然后写入到文件中

(2) getc和fgetc的区别

这里不难看到这两个函数的返回类型和里面的参数都是一样的,这两个有什么区别呢?

其实在原始的定义

getc是一个宏定义的"函数",fgetc是一个正儿八经的函数

为什么呢?

因为由于效率的需求,宏定义的函数是要比普通函数更快的,普通函数是后续需要不停的调用,但是宏函数直接替换的,只需要在编译进行替换

场景 选宏函数 选普通函数
简单的数值计算(如 MAX/MIN) ✅(追求效率,无调用开销) ❌(开销没必要)
有复杂逻辑 / 多语句 ❌(代码膨胀 + 易出错) ✅(结构清晰)
需要类型安全 ❌(无类型检查) ✅(严格类型检查)
频繁调用的小功能 ✅(减少调用开销) ❌(多次调用开销累积)
参数有自增 / 自减操作 ❌(多次求值踩坑) ✅(安全)

宏函数是预处理阶段的文本替换 ,无调用开销但无类型检查、易踩参数求值坑;普通函数是运行阶段的代码调用,有类型安全但有调用开销。

2,fputc函数


成功的时候和未成功都是跟fgetc一样的

六,fopen,fclose,fgetc,fputc的练习

将文件1复制到文件2去

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>

int main(int argc, char* argv[]){

    FILE *fd, *fs;
    int ch;

    if(argc != 3){
        printf("Use erro.....%s <fd> <fs>",argv[0]);
        return 1;
    }

    fd = fopen(argv[1],"w");

    if(fd == NULL){
        perror("fopen()");
        return 1;
    }


    fs = fopen(argv[2], "r");

    if(fs == NULL){
        perror("fopen()");
        fclose(fd);
        return 1;
    }

    while((ch = fgetc(fs)) != EOF)
        fputc(ch, fd);

    fclose(fd);
    fclose(fs);

    return 0;
}

最后可以用diff来判断是否复制有问题

这里要考虑的是文件是否接受到,判断是否为NULL

还要考虑的这个是否文件读取完了用EOF来弄

七,fgets函数和fputs函数


这里表示的为成功则返回一个字符串,不成功返沪一个空指针和一个errno

这里表示的是出错返回EOF

1,对于fgets函数的解疑

为什么第三种情况会这么奇怪呢?

其实每次我们打开一个编辑器,都会有一个默认的'\n',就是有一个默认的换行,所以这里就要多一步,不是一步,而是两步。

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>

#define SIZE 1200

int main(int argc, char* argv[]){

    FILE *fd, *fs;
    char buffer[SIZE]; // this is a array yet is a string

    if(argc != 3){
        printf("Use erro.....%s <fd> <fs>",argv[0]);
        return 1;
    }

    fd = fopen(argv[1],"w");

    if(fd == NULL){
        perror("fopen()");
        return 1;
    }


    fs = fopen(argv[2], "r");

    if(fs == NULL){
        perror("fopen()");
        fclose(fd);
        return 1;
    }

    while(fgets(buffer, SIZE, fs) != NULL)
        fputs(buffer, fd);

    fclose(fd);
    fclose(fs);

    return 0;
}

八,fread函数与fwrite函数

这里是要地址,读取大小,块的大小,读取到的流

cpp 复制代码
RETURN VALUE
       On  success,  fread() and fwrite() return the number of 
items read or written.  This number equals the number of bytes 
transferred only when size is 1.  If an error occurs,
       or the end of the file is reached, the return value is a 
short item count (or zero).

       fread() does not distinguish between end-of-file 
and error, and callers must use feof(3) and ferror(3) to 
determine which occurred.

返回值是对象的个数,如果没有成功则返回的是0或者其他的个数

相关推荐
dinga198510263 小时前
linux上redis升级
linux·运维·redis
hzc09876543213 小时前
Linux系统下安装配置 Nginx 超详细图文教程_linux安装nginx
linux·服务器·nginx
RisunJan4 小时前
Linux命令-ltrace(用来跟踪进程调用库函数的情况)
linux·运维·服务器
阿乐艾官4 小时前
【 LVM 创建逻辑卷】
linux
予枫的编程笔记4 小时前
【Linux高级篇】搞定文件句柄+TIME_WAIT,Linux内核初步调优实操指南
linux·linux运维·ulimit·time_wait·sysctl.conf·内核调优·服务器优化
c***03234 小时前
linux centos8 安装redis 卸载redis
linux·运维·redis
柏木乃一4 小时前
Linux进程信号(2):信号产生part2
linux·运维·服务器·c++·信号处理·信号·异常
小义_5 小时前
【RH134知识点问答题】第13章 运行容器
linux·云原生
q***76566 小时前
ubuntu 安装 Redis
linux·redis·ubuntu