【Linux系统】13 基础IO(上)

目录

一、理解 "文件"

[1.1 狭义理解:文件在磁盘里](#1.1 狭义理解:文件在磁盘里)

[1.2 广义理解:Linux 下一切皆文件](#1.2 广义理解:Linux 下一切皆文件)

[1.3 文件操作的归类认知](#1.3 文件操作的归类认知)

[1.4 系统角度](#1.4 系统角度)

[二、回顾 C 文件接口](#二、回顾 C 文件接口)

[2.1 打开文件:进程在哪里?](#2.1 打开文件:进程在哪里?)

[2.2 写文件:fwrite](#2.2 写文件:fwrite)

[2.3 读文件:fread 与简易 cat](#2.3 读文件:fread 与简易 cat)

[2.4 输出到显示器的方法](#2.4 输出到显示器的方法)

[2.5 stdin & stdout & stderr](#2.5 stdin & stdout & stderr)

[2.6 打开文件的方式](#2.6 打开文件的方式)

[三、系统文件 I/O](#三、系统文件 I/O)

[3.1 传递标志位的方法](#3.1 传递标志位的方法)

[3.2 写文件(系统接口版)](#3.2 写文件(系统接口版))

[3.3 读文件(系统接口版)](#3.3 读文件(系统接口版))

[3.4 open 接口详解](#3.4 open 接口详解)

[3.5 open 返回值:系统调用 vs 库函数](#3.5 open 返回值:系统调用 vs 库函数)

[四、文件描述符 fd](#四、文件描述符 fd)

[4.1 标准输入 0 & 标准输出 1 & 标准错误 2](#4.1 标准输入 0 & 标准输出 1 & 标准错误 2)

[4.2 fd 的本质:fd_array 的下标](#4.2 fd 的本质:fd_array 的下标)

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

五、重定向

[5.1 输出重定向的本质](#5.1 输出重定向的本质)

[5.2 使用 dup2 系统调用](#5.2 使用 dup2 系统调用)

小结(上篇)

一、理解 "文件"

1.1 狭义理解:文件在磁盘里

  • 文件存储在磁盘 上,磁盘是永久性存储介质 ,所以文件在磁盘上的存储是永久性的(断电不消失,区别于内存)。
  • 磁盘是外设------ 既是输入设备(读文件 = 从磁盘读入),也是输出设备(写文件 = 写到磁盘)。
  • 因此,磁盘上的文件本质是:对文件的所有操作,都是对外设的输入和输出,简称 IO。

结论:对文件的操作,本质就是 IO。

1.2 广义理解:Linux 下一切皆文件

Linux 里 "一切皆文件"------ 键盘、显示器、网卡、磁盘...... 这些硬件设备都被抽象成了文件。这是 Linux 的核心哲学,具体抽象实现见下篇。

1.3 文件操作的归类认知

  1. 即使是一个 0KB 的空文件,也要占用磁盘空间(要记录属性 / 元数据)。
  2. 文件 = 文件属性(元数据) + 文件内容。
  3. 所有文件操作,本质是内容操作 和属性操作两大类。

1.4 系统角度

  • 对文件的操作,本质是进程对文件的操作;
  • 磁盘的管理者是操作系统(OS),用户程序不能直接碰磁盘。

二、回顾 C 文件接口

关键认知:文件读写的本质并不是通过 C/C++ 库函数实现的(库函数只提供方便),真正干活的是文件相关的系统调用接口。

2.1 打开文件:进程在哪里?

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

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

fopen("myfile", "w") 打开的文件在哪个路径?------在程序的当前路径 。系统怎么知道?通过 /proc/[进程id]:

cpp 复制代码
[hyb@VM-8-12-centos io]$ ps ajx | grep myProc
506729 533463 533463 506729 pts/249  533463 R+  1002  7:45 ./myProc
[hyb@VM-8-12-centos io]$ ls /proc/533463 -l
total 0
...
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 cwd -> /home/hyb/io
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 exe -> /home/hyb/io/myProc
  • cwd:指向当前进程运行目录的符号链接;
  • exe:指向启动当前进程的可执行文件(完整路径)的符号链接。

结论:打开文件本质是进程打开。进程知道自己在哪里,即便文件不带路径,OS 也知道文件放哪。

2.2 写文件:fwrite

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

int main()
{
    FILE *fp = fopen("myfile", "w");
    if(!fp){
        printf("fopen error!\n");
    }
    const char *msg = "hello bit!\n";
    int count = 5;
    while(count--){
        fwrite(msg, strlen(msg), 1, fp);
    }
    fclose(fp);
    return 0;
}

2.3 读文件:fread 与简易 cat

先看 "有坑" 的写法:

cpp 复制代码
while(1){
    // 注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
    ssize_t s = fread(buf, 1, strlen(msg), fp);   // 坑:用 strlen(msg) 当读取长度
    if(s > 0){
        buf[s] = 0;
        printf("%s", buf);
    }
    if(feof(fp)){
        break;
    }
}
fclose(fp);
return 0;

坑有三处:

  1. 读取长度用错 :fread(buf, 1, strlen(msg), fp) 每次最多读 strlen(msg) 字节,文件若比它长就永远读不完,应用 sizeof(buf);
  2. 返回类型 :fread 返回 size_t,不是 ssize_t;
  3. feof 时机:只有 "尝试读过且到末尾" 才为真,更稳的是读不满就认为读完。

修正后的简单 cat:

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

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        printf("argv error!\n");
        return 1;
    }
    FILE *fp = fopen(argv[1], "r");
    if(!fp){
        printf("fopen error!\n");
        return 2;
    }
    char buf[1024];
    while(1){
        int s = fread(buf, 1, sizeof(buf), fp);   // 读满缓冲区
        if(s > 0){
            buf[s] = 0;
            printf("%s", buf);
        }
        if(feof(fp)){
            break;
        }
    }
    fclose(fp);
    return 0;
}

2.4 输出到显示器的方法

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

int main()
{
    const char *msg = "hello fwrite\n";
    fwrite(msg, strlen(msg), 1, stdout);
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    return 0;
}

2.5 stdin & stdout & stderr

C 默认打开三个输入输出流:stdin、stdout、stderr ,类型都是 FILE*(同 fopen 返回类型)。

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

extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

2.6 打开文件的方式

表格

模式 含义
r 只读,流定位在文件开头
r+ 读写,流定位在文件开头
w 截断到零长度或创建,流定位在文件开头
w+ 读写,不存在则创建、存在则截断
a 追加写,不存在则创建,流定位在文件末尾
a+ 读、追加写,读位置在开头,输出总追加到末尾

另有 fseek/ftell/rewind 三个定位函数,请自行复习。


三、系统文件 I/O

语言层(fopen/ifstream)不是最底层,系统(内核)才是打开文件最底层的方案 。先学一个小技巧 ------如何给函数传递标志位。

3.1 传递标志位的方法

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

#define ONE   0001  //0000 0001
#define TWO   0002  //0000 0010
#define THREE 0004  //0000 0100

void func(int flags) {
    if (flags & ONE)   printf("flags has ONE! ");
    if (flags & TWO)   printf("flags has TWO! ");
    if (flags & THREE) printf("flags has THREE! ");
    printf("\n");
}

int main() {
    func(ONE);
    func(THREE);
    func(ONE | TWO);
    func(ONE | THREE | TWO);
    return 0;
}

用位 表示选项,用按位或 | 拼参数,用按位与 & 判存在。open 的 O_* 就是这套设计。

3.2 写文件(系统接口版)

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    umask(0);
    int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
    if(fd < 0){
        perror("open");
        return 1;
    }
    int count = 5;
    const char *msg = "hello bit!\n";
    int len = strlen(msg);
    while(count--){
        write(fd, msg, len);   // fd: 文件描述符;msg: 数据首地址;len: 期望写入字节数
    }
    close(fd);
    return 0;
}

write 返回值 = 实际写入的字节数。perror 根据 errno 打印错误描述。实际创建权限 = mode & ~umask。

3.3 读文件(系统接口版)

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    int fd = open("myfile", O_RDONLY);
    if(fd < 0){
        perror("open");
        return 1;
    }
    const char *msg = "hello bit!\n";
    char buf[1024];
    while(1){
        ssize_t s = read(fd, buf, strlen(msg));
        if(s > 0){
            printf("%s", buf);
        }else{
            break;   // s==0 读到末尾,s<0 出错
        }
    }
    close(fd);
    return 0;
}

read/write 返回值语义 :write 返回实际写入字节数,失败 -1;read 返回实际读到字节数,0 表示 EOF,-1 表示出错;都可能 "部分读写"。

3.4 open 接口详解

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • pathname:目标文件;
  • flags:一个或多个常量 "或" 运算;
  • mode:新文件访问权限(配 O_CREAT 用)。

表格

常量 含义
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读写
O_CREAT 不存在则创建(配合 mode)
O_APPEND 追加写
O_TRUNC 截断为 0

O_RDONLY/O_WRONLY/O_RDWR 必须且只能指定一个。 mode_t 直接 man。0644=rw-r--r--,0666=rw-rw-rw-。用两参还是三参看是否创建文件。

返回值:成功 = 文件描述符(非负小整数),失败 =-1。

3.5 open 返回值:系统调用 vs 库函数

  • fopen/fclose/fread/fwrite → 库函数(libc);
  • open/close/read/write/lseek → 系统调用接口。

分层图(回忆操作系统概念课):

cpp 复制代码
用户部分:     用户操作接口 / shell / lib 库 / 部分指令
系统软件部分:   system call(系统调用接口)
             操作系统(内存管理·进程管理·文件管理·驱动管理)
             驱动(网卡·硬盘·其他)
硬件部分:      底层硬件(网卡 / 硬盘 / 其他)

可以认为 f* 系列都是对系统调用的封装。 补充:每次系统调用都要用户态↔内核态切换,有开销,所以库函数用缓冲区攒批写入(见下篇缓冲区)。


四、文件描述符 fd

通过 open 知道:文件描述符就是一个小整数。

4.1 标准输入 0 & 标准输出 1 & 标准错误 2

Linux 进程默认有 3 个缺省打开的文件描述符:标准输入 0、标准输出 1、标准错误 2。 物理设备一般是:键盘、显示器、显示器。

可直接读写 0/1/2:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    char buf[1024];
    ssize_t s = read(0, buf, sizeof(buf));
    if(s > 0){
        buf[s] = 0;
        write(1, buf, strlen(buf));
        write(2, buf, strlen(buf));
    }
    return 0;
}

4.2 fd 的本质:fd_array 的下标

cpp 复制代码
task_struct                  files_struct                   file
   │  *files  ───────────▶   file **fd_array[]   ────▶   下标0→file* 标准输入
                            (指针数组)                   下标1→file* 标准输出
                                                          下标2→file* 标准错误
                                                          下标3→file* myfile(新打开)

打开文件时,OS 创建 file 结构体(已打开文件对象,含 inode 元信息、位置、引用计数)。进程 *files 指向 files_struct,其核心是指针数组 fd_array[] ,每个元素指向一个打开的文件。所以文件描述符就是该数组的下标 ------ 拿着 fd 就能找到对应文件。

内核源码验证:

  • task_struct:/usr/src/kernels/<版本>/include/linux/sched.h
  • files_struct:/usr/src/kernels/<版本>/include/linux/fdtable.h
  • file:/usr/src/kernels/<版本>/include/linux/fs.h
cpp 复制代码
struct task_struct { ... struct files_struct *files; ... };
struct files_struct { ... struct file **fd_array; ... };

4.3 文件描述符的分配规则

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd = open("myfile", O_RDONLY);
    if(fd < 0){ perror("open"); return 1; }
    printf("fd: %d\n", fd);
    close(fd);
    return 0;
}

输出 fd: 3(0/1/2 被占)。先 close(0) 或 close(2) 再 open,则 fd: 0 或 fd: 2。

结论:分配规则 = 在 fd_array 中找到当前未被使用的最小下标,作为新文件描述符。


五、重定向

5.1 输出重定向的本质

关闭 1 再打开新文件:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
    close(1);
    int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
    if(fd < 0){ perror("open"); return 1; }
    printf("fd: %d\n", fd);
    fflush(stdout);
    close(fd);
    exit(0);
}

本应输出到显示器的内容输出到了 myfile,fd=1,这就是输出重定向 。常见符号:>、>>、<。

本质:close(1) 后 fd_array[1] 空出,open 按最小未使用规则把 myfile 放到下标 1。于是往 1 号 fd 写的内容都进了 myfile。重定向 = 改变 fd_array 某下标指向的文件对象。

5.2 使用 dup2 系统调用

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

int dup2(int oldfd, int newfd);

把 oldfd 复制到 newfd,让 newfd 指向 oldfd 指向的文件:

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

int main() {
    int fd = open("./log", O_CREAT | O_RDWR);
    if (fd < 0) { perror("open"); return 1; }
    close(1);
    dup2(fd, 1);   // 让 1 指向 fd 指向的文件
    for (;;) {
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0) { perror("read"); break; }
        printf("%s", buf);
        fflush(stdout);
    }
    return 0;
}

为什么 printf 进文件? printf 往 stdout 写,stdout 底层找 fd:1;此时 fd:1 已变成 ./log 的地址,不再是显示器,所以输出都进文件。追加(>>→O_APPEND)和输入(<→dup2 到 0)同理,下篇自定义 shell 有完整实现。


小结(上篇)

  1. 文件:磁盘永久存储,本质是 IO;一切皆文件。
  2. C 文件接口 :fopen/fwrite/fread/fclose,打开文件本质是进程打开(/proc/[pid]/cwd 定位)。
  3. 系统文件 I/O :open/close/read/write,标志位(位运算)传参。
  4. 系统调用 vs 库函数 :f* 是对系统调用的封装。
  5. 文件描述符 fd :从 0 开始的小整数,本质是 fd_array 下标,分配规则 = 最小未使用下标。
  6. 重定向 :改变 fd 指向的文件对象,用 dup2 实现。
相关推荐
CCCCCCCCharlie1 小时前
从0到1:一个字节的磁盘之旅
linux·开发语言·c++
羑悻的小杀马特1 小时前
超越Bridge:全面掌握Docker网络隔离与通信的底层逻辑!
运维·网络·docker·容器
运维小斌1 小时前
麒麟 V10 ARM64 离线部署 OceanBase:8 步实施手册
linux·数据库·oceanbase·麒麟
着迷不白1 小时前
生产环境标准的Linux系统安装到服务器
linux·云计算
A-刘晨阳1 小时前
Windows硬盘里的媒体太乱?我用WSL+Plex把本地文件整理成一个统一媒体中心
运维·cpolar
吹什么轩1 小时前
linux系统复习:进程优先级与O(1)调度
java·linux·算法
古法老登瞎折腾1 小时前
大数据智能运维实战(六):任务一直在等——排查调度队列与执行资源
java·大数据·运维·spring ai·mcp
开开心心就好2 小时前
安卓PDF处理工具,免费无广告零弹窗
android·服务器·开发语言·算法·pdf·ocr
电梯界知识分子2 小时前
江西赣州赣县三层设计师自建房:1700mm中空极限利用,全观光铝合金电梯的精度与防震处理
大数据·服务器·网络·人工智能·家用电梯