《Linux系统编程》12.基础IO

💡Yupureki:个人主页

✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》《MySQL数据库》

《个人在线OJ平台》


🌸Yupureki🌸的简介:


目录

[1. 重谈文件](#1. 重谈文件)

[1.1 狭义理解](#1.1 狭义理解)

[1.2 广义理解](#1.2 广义理解)

[2. 重看C语言文件接口](#2. 重看C语言文件接口)

[2.1 打开文件](#2.1 打开文件)

[2.2 写入文件](#2.2 写入文件)

[2.3 读文件](#2.3 读文件)

[3. 系统文件I/O](#3. 系统文件I/O)

[3.1 接口介绍](#3.1 接口介绍)

[3.2 open和fopen的理解](#3.2 open和fopen的理解)

[3.3 文件描述符](#3.3 文件描述符)

[3.3.1 初识文件描述符](#3.3.1 初识文件描述符)

[3.3.2 文件描述符的本质](#3.3.2 文件描述符的本质)

[3.4 三个默认的文件描述符](#3.4 三个默认的文件描述符)

[3.5 文件描述符分配规则](#3.5 文件描述符分配规则)

[3.6 重定向](#3.6 重定向)

[3.7 理解 "一切皆文件"](#3.7 理解 "一切皆文件")

[3.7.1 具体体现](#3.7.1 具体体现)

一、硬件设备也是文件

二、进程与内核信息也是文件

三、网络通信也是文件

[3.7.2 这种设计带来的好处](#3.7.2 这种设计带来的好处)

[4. 缓冲区](#4. 缓冲区)

[4.1 什么是缓冲区](#4.1 什么是缓冲区)

[4.2 为什么需要缓冲区?](#4.2 为什么需要缓冲区?)

[4.3 标准 I/O 的用户态缓冲区](#4.3 标准 I/O 的用户态缓冲区)

[4.4 内核缓冲区](#4.4 内核缓冲区)

[4.5 缓冲区的层级关系](#4.5 缓冲区的层级关系)

全流程


1. 重谈文件

1.1 狭义理解

文件在磁盘里

  • 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
  • 磁盘是外设(即是输出设备也是输入设备)
  • 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出简称IO

1.2 广义理解

Linux下一切皆文件(键盘、显示器、网卡、磁盘··这些都是抽象化的过程)

这句话并不是说系统里所有的东西都是普通的文本文件,而是指:系统将所有的资源都通过"文件"这个统一接口来呈现和访问。

在 Linux 中,文件系统是一个巨大的树状结构。无论是:

  • 你的普通文本、图片

  • 你的键盘、鼠标、显示器、硬盘

  • 进程信息、内存

  • 网络连接、管道

它们都存在于这个树状结构的某个位置,并且都支持 打开、读写、关闭 这同一套标准的操作。

2. 重看C语言文件接口

2.1 打开文件

cpp 复制代码
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

int main() {
    FILE* fp = fopen("test","w");
    if(!fp)
    {
        printf("fopen error!\n");
        return 1;
    }
    fclose(fp);
    return 0;
}

然而fopen指定的只是文件的名字,我怎么知道这个文件在哪个路径下?->一般在当前路径

但问题又来了,进程怎么知道当前路径是什么?

可以通过ls /proc/进程pid -l查看进程的属性

其中:

  • cwd:指向当前进程运行目录的一个符号链接。
  • exe:指向启动当前进程的可执行文件(完整路径)的符号链接。

打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。

2.2 写入文件

cpp 复制代码
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

int main() {
    FILE* fp = fopen("my.txt","w");
    if(!fp)
    {
        printf("fopen error!\n");
        return 1;
    }
    char buffer[] = "hello world";
    fwrite(buffer,sizeof(buffer),1,fp);
    fclose(fp);
    return 0;
}

2.3 读文件

cpp 复制代码
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

int main() {
    FILE* fp = fopen("my.txt","r");
    if(!fp)
    {
        printf("fopen error!\n");
        return 1;
    }
    char buffer[1024];
    fread(buffer,sizeof(buffer),1,fp);
    printf("my.txt: %s",buffer);
    fclose(fp);
    return 0;
}

3. 系统文件I/O

打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案

3.1 接口介绍

pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"或"运算,构成flags

参数:

  • O_RDONLY:只读打开
  • O_WRONLY:只写打开
  • O_RDWR:读,写打开

这三个常量,必须指定一个且只能指定一个

  • O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
  • O_APPEND:追加写

返回值:

成功:新打开的文件描述符

失败:-1

先看示例:

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

int main() 
{
    int fd = open("my.txt",O_WRONLY,0644);
    if(fd < 0)
    {
        printf("open error!\n");
        return 1;
    }
    char buffer[] = "hello linux";
    write(fd,buffer,sizeof(buffer));
    return 0;
}

3.2 open和fopen的理解

本质:库函数 vs. 系统调用

  • open/read/write

    是 Linux 内核提供的系统调用 (System Call)。它们直接向内核发起请求,由内核完成真正的硬件操作(磁盘、终端、网络等)。

    返回值是文件描述符int),一个非负整数,代表内核中打开的文件的索引。

  • fopen/fread/fwrite

    C 标准库(libc)提供的函数。它们内部会调用相应的系统调用,但在这之上做了封装:

    • 维护一个 FILE 结构体,包含文件描述符、缓冲区指针、当前读写位置等信息。

    • 返回值是 FILE *(文件流指针)。

可以理解为,C语言库函数fopen,fread这种封装了各种操作系统底层的系统调用,再根据条件编译选择性地裁剪代码,这样就保证了可移植性。而Linux的系统调用直接由操作系统操作底层,也只限于Linux系统可以使用,其他的系统无法使用

3.3 文件描述符

3.3.1 初识文件描述符

fopen返回的是一个FILE*的指针,我们都知道这是一个指向文件的指针

那么open返回的一个整型,叫文件描述符是什么东西?

一个直观类比

想象你去图书馆:

  • 图书馆是内核,藏书是各种资源(文件、设备、管道、网络连接......)

  • 你走进图书馆,说要借一本书,管理员给你一张借书证号码(文件描述符)

  • 你后续要阅读、归还,只需出示这个号码,管理员就知道是哪本书、读到第几页了

这个号码就是 fd

3.3.2 文件描述符的本质

文件描述符是一个 非负整数 (通常 0、1、2、3...),它是进程用来标识一个已打开文件的唯一索引

在 Linux 内核中,每个进程都有一个 文件描述符表struct files_struct)。这张表就像一个数组,下标就是 fd,每个条目指向内核中一个 打开文件描述struct file)。

这个 struct file 包含:

  • 当前文件偏移量(lseek 移动的就是它)

  • 文件状态标志(只读、只写、非阻塞等)

  • 指向实际文件元信息的指针(inode / vnode)

而这个fd_array[]是一个数组,其下标就相当于是文件描述符fd。通过文件描述符,可以找到对应的文件

Linux源码:

3.4 三个默认的文件描述符

每个进程启动时,内核会自动打开三个 fd:

fd 宏名 含义
0 STDIN_FILENO 标准输入(默认从键盘读)
1 STDOUT_FILENO 标准输出(默认往屏幕写)
2 STDERR_FILENO 标准错误(默认往屏幕写)

因此,如果我们在write函数内指定文件描述符1,就会往显示屏上写

cpp 复制代码
int main() 
{
    char buffer[1024] = "hello linux\n";
    write(1,buffer,sizeof(buffer));
    return 0;
}

同样的,如果我们在read函数内指定文件描述符0,就会从键盘拿数据

3.5 文件描述符分配规则

文件描述符的分配取决于文件描述符的空位情况,他会从0开始,一直找,直到找到一个没被占有的文件描述符

3.6 重定向

文件描述符1默认是与显示屏相关的,然而这也只是默认行为。我们可以修改文件描述符1,将其强行与其他文件关联

dup2函数
int dup2 ( int oldfd, int newfd);

我们可以使用dup2函数来重定向文件描述符

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

int main() 
{
    int fd = open("my.txt",O_WRONLY | O_TRUNC,0644);
    if(fd < 0)
    {
        printf("open error!\n");
        return 1;
    }
    dup2(fd,1);
    char buffer[] = "123456";
    write(fd,buffer,sizeof(buffer));
    return 0;
}

同理,我们也可以重定向其他的文件描述符,如0,我们也可以重定向到其他文件中,从文件中拿取数据

3.7 理解 "一切皆文件"

首先,在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东

西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。

3.7.1 具体体现

一、硬件设备也是文件

这是最直观的体现。当你插入一个U盘,系统不会给你一个"可移动磁盘"的图标让你双击,而是在 /dev/sdb1 位置生成了一个设备文件

  • 硬盘/dev/sda ------ 往这个文件里写数据,就是往硬盘写数据。

  • 鼠标/dev/input/mouse0 ------ 读取这个文件,就能捕获鼠标的移动轨迹。

  • 显卡/dev/fb0 (帧缓冲)------ 往这个文件写入RGB数据,屏幕就会显示对应的颜色。

例子 :你可以直接用 cat 命令读取鼠标文件:

bash 复制代码
sudo cat /dev/input/mouse0
二、进程与内核信息也是文件

Linux 的 /proc 目录是驻留在内存中的虚拟文件系统,它把内核和进程的信息"伪装"成了文件。

  • CPU信息cat /proc/cpuinfo ------ 虽然它是一个文件,但它其实是内核实时生成的。

  • 进程状态/proc/1234/ ------ 这是一个目录,里面存放着 PID 为 1234 的进程的所有信息。

  • 系统配置/proc/sys/net/ipv4/ip_forward ------ 你往这个文件里写 1,内核就开启了 IP 转发(路由功能)。

三、网络通信也是文件

网络通信通常需要复杂的 Socket 编程接口,但在 Linux 里,它也沿用了文件描述符(FD)的概念。

  • 管道| 命令的本质,就是将一个进程的输出文件,直接连接到另一个进程的输入文件。

  • Socket :你在 /proc/<pid>/fd/ 下可以看到,网络连接被抽象成了编号为 0、1、2(标准输入/输出/错误)之后的文件描述符。

3.7.2 这种设计带来的好处

程序员学习 Linux 编程时,只需要掌握五个函数:open, read, write, close, ioctl

无论操作的是真实的硬盘文件、串口数据、还是屏幕显示,代码逻辑都是一样的。这极大地降低了驱动开发和上层开发的复杂度。

4. 缓冲区

4.1 什么是缓冲区

缓冲区就是一块内存区域,用于临时存放数据,等待合适时机再统一处理。

在 I/O 过程中,缓冲区可以出现在两个层级:

  • 用户态缓冲区 :由 C 标准库管理(如 FILE 结构体中的缓冲区)。

  • 内核态缓冲区:由操作系统内核管理(如页缓存、Socket 缓冲区等)。

4.2 为什么需要缓冲区?

最直接的原因:减少系统调用次数

  • 每次 read/write 系统调用,程序都要从用户态陷入内核态,上下文切换开销很大。

  • 如果每次只读写 1 字节,做 1000 次,就 1000 次系统调用,效率极低。

  • 缓冲区将多次小数据合并成一次大数据块,一次系统调用处理一整块数据,大幅提升性能。

4.3 标准 I/O 的用户态缓冲区

C 标准库(fopen/fread/fwrite/printf 等)在用户态维护了一个缓冲区。

缓冲类型

类型 行为 常见场景
全缓冲 填满缓冲区才进行系统调用(write 普通磁盘文件
行缓冲 遇到换行符 \n 就刷新(或缓冲区满) 终端交互(标准输出)
无缓冲 立即进行系统调用 标准错误(stderr

验证

运行后,会看到"Hello"不会立即出现,而是等待2秒后一起输出"Hello World"。因为标准输出是行缓冲,第一个 printf 只是把数据放进缓冲区,直到遇到 \n 或程序结束才真正调用 write

控制用户态缓冲区

C 标准库提供了函数来改变缓冲模式:

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

// 设置全缓冲,缓冲区大小为 BUFSIZ
setbuf(fp, NULL);          // 关闭缓冲(无缓冲)
setvbuf(fp, buffer, _IOLBF, size);  // 行缓冲
setvbuf(fp, buffer, _IOFBF, size);  // 全缓冲
setvbuf(fp, buffer, _IONBF, 0);     // 无缓冲

4.4 内核缓冲区

即使标准 I/O 调用了 write,数据也未必立即写到磁盘。内核也会维护一个页缓存 (Page Cache)或缓冲区缓存

  • write 系统调用将数据从用户态拷贝到内核缓冲区,然后立即返回(除非用 O_SYNC 标志)。

  • 内核随后在合适时机(如缓冲区满、时间到期、显式 fsync)将数据真正写入磁盘。

这样做的好处是:

  • 应用程序不必等待慢速的磁盘操作,可以继续执行。

  • 内核可以合并多次写入,减少磁盘 I/O。

强制落盘

如果需要确保数据已写入持久存储,可以调用:

cpp 复制代码
#include <unistd.h>
int fsync(int fd);          // 同步文件数据
int fdatasync(int fd);      // 只同步数据,不同步元数据
void sync(void);            // 全局同步(不常用)

4.5 缓冲区的层级关系

复制代码
用户程序
   │
   │ fwrite / printf
   ▼
┌─────────────────────┐
│  用户态缓冲区        │  ← 由 stdio 管理,减少系统调用
│  (FILE 结构体)       │
└─────────────────────┘
   │
   │ write 系统调用
   ▼
┌─────────────────────┐
│  内核缓冲区          │  ← 页缓存,减少磁盘 I/O
│  (Page Cache)       │
└─────────────────────┘
   │
   │ 磁盘驱动程序
   ▼
   磁盘/硬件

全流程

  1. printf("hello") → 数据进入用户态缓冲区。

  2. 缓冲区满或遇 \n → 调用 write → 数据进入内核页缓存。

  3. 内核异步将页缓存写入磁盘。

  4. 如果调用 fflush,只会把用户态缓冲区的数据推到内核缓冲区(不保证落盘)。

  5. 如果调用 fsync,才会强制内核将数据刷到磁盘。

相关推荐
淮北4941 小时前
bash下好用的快捷键以及linux常用指令
linux·开发语言·ubuntu·bash
Jordannnnnnnn1 小时前
追赶32名
c++
炸膛坦客1 小时前
单片机/C/C++八股:(十八)C/C++ 中 sizeof 和 strlen 的区别
c语言·c++
瀚高PG实验室1 小时前
nginx中配置数据库连接
运维·数据库·nginx·瀚高数据库
一个天蝎座 白勺 程序猿2 小时前
Oracle替换工程实践深度解析:从迁移挑战到金仓“零改造”实践
数据库·学习·oracle·kingbasees
薛定谔的猫喵喵2 小时前
卸载 Python 3.8 报错 “Could not set file security” 的终极解决方案
开发语言·python
顶点多余2 小时前
深度剖析Linux 线程概念
java·linux·jvm
Sahas10192 小时前
安装 Redis 为系统服务
数据库·redis·缓存