【Linux】基础IO(二):系统文件IO

【Linux】基础IO(二):系统文件IO
在这里插入图片描述

✨道路是曲折的,前途是光明的!

📝 专注C/C++、Linux编程与人工智能领域,分享学习笔记!

🌟 感谢各位小伙伴的长期陪伴与支持,欢迎文末添加好友一起交流!

在这里插入图片描述


一、IO操作的层级调用关系

简单来说:C/C++程序(标准库) → 调用 → 系统调用 → 调用 → 操作系统 → 调用 → 硬件驱动 → 操作 → 硬件
 应用程序 (App) ↓ C/C++ 标准库 (Libc) ↓ 系统调用接口 (Syscall) ↓ 操作系统内核 (Kernel) ↓ 硬件驱动程序 (Driver) ↓ 硬件 (Hardware) 
  • 操作系统为保证安全,仅通过系统调用对外开放硬件访问接口,任何程序(包括C标准库)都需通过系统调用才能自上而下访问操作系统→硬件驱动→硬件;
  • printf/fprintf/fscanf/fwrite/fread/fgets/gets等文件操作库函数,本质是对文件类系统调用的封装,其底层均依赖系统调用实现对硬件的读写。

二、open

系统接口中使用open函数打开文件,open函数的函数原型如下:

intopen(constchar*pathname,int flags,mode_t mode);

2.1 第一个参数

open函数的第一个参数是pathname,表示要打开或创建的目标文件。
  • 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
  • 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)

2.2 第二个参数

open函数的第二个参数是flags,表明打开文件的方式。

我们要告诉操作系统:“我要读写模式打开”、“如果文件不存在就创建”、“每次写都追加到末尾”

  • 如果按照常规思维,这需要 3 个布尔类型的参数(isReadWrite, isCreate, isAppend)。如果有 10 种操作模式,难道要写 10 个参数吗?

显然不是。Linux 大神们只用了一个 int 类型(32位)就搞定了。这背后的核心魔法,就是比特位传递标志位


2.2.1 核心原理:把整数当成“32 个开关的面板”

我们可以把一个 int 类型的变量,想象成一个拥有 32 个独立开关 的控制面板。

  • 一个开关(比特位):只有两种状态,0(关)或 1(开)。
  • 一个整数:就是这 32 个开关的集合。

通过操作这些开关,我们就能用这一个整数,同时传递 32 个“是/否”的指令。


2.2.2 第一步:定义开关(宏定义与左移 <<

操作系统需要先定义好,哪个开关代表什么意思。这就是 <fcntl.h> 头文件中那些宏定义的由来。

为了保证每个开关互不干扰,我们使用 1 << n(1 左移 n 位)的方式来定义:

  • O_RDWR(读写):定义在第 1 位 → 1 << 1 → 二进制 000...0010
  • O_CREAT(创建):定义在第 6 位 → 1 << 6 → 二进制 000...1000000
  • O_APPEND(追加):定义在第 10 位 → 1 << 10 → 二进制 000...10000000000

为什么要这么做?

因为左移操作保证了每一个宏对应的二进制数中,只有某一位是 1,其他位全是 0。这就像给每个开关贴上了唯一的标签,按下 O_CREAT 绝对不会误触 O_RDWR。

2.2.3 第二步:按下开关(传参与按位或 |

当我们在代码中调用 open 时,我们需要告诉系统:“我要同时按下 读写创建 这两个开关”。

这时候我们使用 按位或| 运算符。它的规则是:只要有一个是 1,结果就是 1

场景模拟:
我们要传递 O_RDWR | O_CREAT

 O_RDWR: 000...0000 0010 | O_CREAT: 000...0100 0000 ---------------------------- 结果: 000...0100 0010 

看!结果整数中,第 1 位和第 6 位都变成了 1。我们成功地把两个指令“打包”进了一个整数里,传给了内核。


2.2.4 第三步:检查开关(解析与按位与 &

open 函数的内核源码收到这个整数后,怎么知道你按下了哪些开关呢?

它使用 按位与& 运算符。它的规则是:两个都是 1,结果才是 1

内核逻辑模拟:

  1. 检查是否要创建文件?
    传入的整数 & O_CREAT
    • 如果结果不为 0,说明第 6 位是 1 → 执行创建逻辑
    • 如果结果为 0,说明第 6 位是 0 → 跳过创建逻辑
  2. 检查是否要追加写入?
    传入的整数 & O_APPEND
    • 同理,判断第 10 位是否为 1。

通过这种方式,内核就能精准地解析出我们想要的所有操作模式。


这种设计模式不仅存在于 open 函数,在 socketfcntl 等系统调用中无处不在。掌握了“比特位传递标志位”,你就掌握了阅读 Linux 源码的一把金钥匙。

2.2.5 常见的选项如下

参数选项含义对应数值(1<<n)二进制(简化)
O_RDONLY以只读的方式打开文件000000000
O_WRONLY以只写的方式打开文件1(1<<0)00000001
O_APPEND以追加的方式打开文件1024(1<<10)10000000000
O_RDWR以读写的方式打开文件2(1<<1)00000010
O_CREAT当目标文件不存在时,创建文件64(1<<6)01000000

2.3 第三个参数

mode 参数仅在使用 O_CREAT 标志创建文件时生效,用于指定文件的默认权限;若无需创建文件,该参数可省略。

2.3.1 基础用法示例

当将 mode 设置为 0666 时,期望创建的文件权限为:

  • 所有者(user):读、写(6 → rw-
  • 所属组(group):读、写(6 → rw-
  • 其他用户(other):读、写(6 → rw-
  • 权限表示:-rw-rw-rw-

2.3.2 umask(文件默认掩码)的影响

文件实际创建的权限并非直接等于 mode,而是受系统 umask (默认掩码)约束,计算公式为:

实际权限 = mode & (~umask) 

默认场景示例

  • 系统默认 umask0002(二进制:000 000 010
  • 设置 mode = 0666(二进制:110 110 110
  • 计算过程:0666 & (~0002) = 0664
  • 最终权限:-rw-rw-r--(所有者/组可读可写,其他用户仅可读)

2.3.3 取消umask影响的方法

若希望文件权限完全按 mode 设置,不受 umask 干扰,可在调用 open 前通过 umask 函数将掩码置0:

umask(0);// 将文件默认掩码设置为0,后续创建文件权限完全遵循modeint fd =open("test.txt", O_CREAT | O_RDWR,0666);// 实际权限为0666

注意事项

  • mode 的值需以 0 开头(八进制),如 0666 而非 666(十进制);
  • 即使设置 mode = 0777,若 umask = 0022,实际权限仍为 0755
  • 无需创建文件时(未使用 O_CREAT),open 无需传入第三个参数。
open函数的返回值是新打开文件的文件描述符。

2.4 实例测试

我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符。

#include<stdio.h>#include<sys/stat.h>#include<sys/types.h>#include<fcntl.h>intmain(){umask(0);int fd1 =open("log1.txt", O_RDONLY | O_CREAT,0666);int fd2 =open("log2.txt", O_RDONLY | O_CREAT,0666);int fd3 =open("log3.txt", O_RDONLY | O_CREAT,0666);int fd4 =open("log4.txt", O_RDONLY | O_CREAT,0666);int fd5 =open("log5.txt", O_RDONLY | O_CREAT,0666);printf("fd1:%d\n", fd1);printf("fd2:%d\n", fd2);printf("fd3:%d\n", fd3);printf("fd4:%d\n", fd4);printf("fd5:%d\n", fd5);return0;}

运行程序后可以看到,打开文件的文件描述符是从3开始连续且递增的

我们再尝试打开一个根本不存在的文件,也就是open函数打开文件失败。

#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){int fd =open("test.txt", O_RDONLY);printf("%d\n", fd);return0;}

运行程序后可以看到,打开文件失败时获取到的文件描述符是-1。

总结

  1. 文件描述符(File Descriptor,简称 fd)是 Linux 系统操作文件的核心标识,它的本质并非随机数字,而是进程内一个指针数组的下标。Linux 进程会维护一个专门的指针数组,数组中每个元素(指针)都指向一个“已打开文件的信息结构体”,这个结构体包含了文件路径、读写位置、权限等所有文件相关信息,通过文件描述符这个下标,就能精准找到对应的文件信息。
  2. 当使用 open 函数成功打开文件时,系统会在这个指针数组中新增一个指向该文件信息的指针,随后将这个指针在数组中的下标作为文件描述符返回;若文件打开失败,则直接返回 -1。正因为数组下标是连续分配的,所以成功打开多个文件时,获得的文件描述符会呈现连续且递增的特点。
  3. Linux 进程在默认情况下会预先打开 3 个缺省的文件描述符,分别是代表标准输入的 0、代表标准输出的 1、代表标准错误的 2,这三个下标会被系统占用,这也是为什么我们手动调用 open 函数成功打开文件时,得到的文件描述符总是从 3 开始分配的原因。

三、close

原函数如下:

intclose(int fd);

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。


四、write

原函数如下:

ssize_twrite(int fd,constvoid*buf,size_t count);

系统接口中使用write函数向文件写入信息。

我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。

  • 如果数据写入成功,实际写入数据的字节个数被返回。
  • 如果数据写入失败,-1被返回。

实例测试:

#include<stdio.h>#include<string.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){int fd =open("ceshi.txt", O_WRONLY | O_CREAT,0666);if(fd <0){perror("open");return1;}constchar* message ="hello linux!\n";for(int i =0; i <5; i++){write(fd, message,strlen(message));}close(fd);return0;}

五、read

系统接口中使用read函数从文件读取信息,read函数的函数原型如下:

ssize_tread(int fd,void*buf,size_t count);

我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。

  • 如果数据读取成功,实际读取数据的字节个数被返回。
  • 如果数据读取失败,-1被返回。

实例测试:

#include<stdio.h>#include<string.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){int fd =open("ceshi.txt", O_RDONLY);if(fd <0){perror("open");return1;}char ch;while(1){ssize_t s =read(fd,&ch,1);if(s <=0){break;}write(1,&ch,1);//向文件描述符为1的文件写入数据,即向显示器写入数据}close(fd);return0;}

✍️ 坚持用清晰易懂的图解+可落地的代码,让每个知识点都简单直观!💡 座右铭:“道路是曲折的,前途是光明的!”

Read more

【Linux系统】C/C++的调试器gdb/cgdb,从入门到精通

【Linux系统】C/C++的调试器gdb/cgdb,从入门到精通

各位读者大佬好,我是落羽!一个坚持不断学习进步的学生。 如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步! 也欢迎关注我的blog主页:落羽的落羽 文章目录 * 一、调试前的预备知识 * 二、gdb/cgdb的使用 * 1. 启动,查看代码 * 2. 基础调试命令 * 3. 监视变量相关命令 * 4. 设置条件断点 一、调试前的预备知识 程序发布的方式有两种,debug模式和release模式。 * debug模式:生成的可执行程序中会包含程序的调试信息,便于程序员进行调试代码。 * release模式:会剥离或不生成这些调试信息。这使得文件更小,但也意味着调试器几乎无法工作,release版本程序无法进行调试。 Linux的gcc/g++,按照我们之前的写法gcc -o $@ $^,默认生成的是release版本的程序,是无法进行调试的。要在命令后加-g选项,指定以debug方式发布,debug模式下的程序我们才能进行调试。 gcc -o $@ $^ -g 二、gdb/cgdb的使用

By Ne0inhk

C++:实现Sqrt开根号(附带源码)

一、项目背景详细介绍 开根号(Square Root,√x) 是计算机科学中一个极其基础、却又极具深度的问题。 在日常编程中,我们习惯直接调用: sqrt(x); 但在以下场景中,自己实现 sqrt 算法是必须的: * 操作系统 / 编译器 / 数学库开发 * 嵌入式系统(无标准库或浮点支持受限) * 算法竞赛 / 面试(禁止使用库函数) * 数值计算精度与性能优化 * 底层算法与计算机体系结构学习 很多开发者都会遇到这些问题: * sqrt 内部是如何实现的? * 为什么不能直接暴力枚举? * 浮点数误差如何控制? * 整数开根号与浮点开根号有何区别? * 牛顿迭代法为什么这么快? 因此,本项目的目标非常明确: 👉 用 C++ 从零实现多种开根号(sqrt)算法,并系统讲清数学原理、数值特性与工程实现。 二、项目需求详细介绍 本示例程序需要满足以下要求: 1️⃣ 功能需求 * 实现整数平方根 * 实现浮点平方根 * 不使用

By Ne0inhk
智能指针:告别内存泄漏的利器----《Hello C++ Wrold!》(27)--(C/C++)

智能指针:告别内存泄漏的利器----《Hello C++ Wrold!》(27)--(C/C++)

文章目录 * 前言 * 智能指针的作用 * 智能指针的实现和原理 * 库里面的智能指针 * std::auto_ptr * auto_ptr的模拟实现 * std::unique_ptr * unique_ptr的模拟实现 * std::shared_ptr * shared_ptr的模拟实现 * shared_ptr的一个弊端 * std::weak_ptr * weak_ptr的模拟实现 * 删除定制器 * 作业部分 前言 在 C++ 编程中,动态内存管理始终是开发者面临的核心挑战之一。手动使用new分配内存、delete释放内存的模式,不仅需要开发者时刻关注内存生命周期,更可能因疏忽导致内存泄漏(忘记调用delete)、二次释放(重复调用delete),或是在异常抛出时因执行流跳转跳过delete语句等问题 —— 这些隐患轻则导致程序性能退化,重则引发崩溃或不可预期的运行错误,成为项目中难以排查的 “隐形 bug”。 为解决这一痛点,C++ 标准库引入了智能指针这一核心工具。

By Ne0inhk
C++幻象:内存序、可见性与指令重排

C++幻象:内存序、可见性与指令重排

C++ 井发的假象:内存序、可见性与指令重排 写在前面:当你第一次把 std::atomic、memory_order 这些词读到手软时,可能会觉得这是 OS 或硬件工程师的专属领域。但其实理解内存模型并不需要掌握每一条 CPU 手册的汇编,只要抓住核心概念与工程实践,你就能写出既高效又安全的并发代码。 本文面向有一定 C++ 并发基础的读者(知道线程、互斥量、基本的 std::atomic 用法),但想把“为什么这样”弄清楚。我们会从 std::atomic 的语义出发,讲清 CPU cache coherence、内存屏障(fence)、指令重排 和 happens-before 的关系——不是空洞的定义,而是大量实战例子、容易踩的坑和调试技巧。文风尽量自然、通俗,

By Ne0inhk