欢迎光临
我们一直在努力

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

目录

一、理解 "文件"

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

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

1.3 文件操作的归类认知

1.4 系统角度

二、回顾 C 文件接口

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

2.2 写文件:fwrite

2.3 读文件:fread 与简易 cat

2.4 输出到显示器的方法

2.5 stdin & stdout & stderr

2.6 打开文件的方式

三、系统文件 I/O

3.1 传递标志位的方法

3.2 写文件(系统接口版)

3.3 读文件(系统接口版)

3.4 open 接口详解

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

四、文件描述符 fd

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

4.2 fd 的本质:fd_array 的下标

4.3 文件描述符的分配规则

五、重定向

5.1 输出重定向的本质

5.2 使用 dup2 系统调用

小结(上篇)

一、理解 "文件"

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

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

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

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

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

1.3 文件操作的归类认知

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

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

    二、回顾 C 文件接口

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

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

    #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]:

    [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

    #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

    先看 "有坑" 的写法:

    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;

    坑有三处:

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

    #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 输出到显示器的方法

    #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 返回类型)。

    #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 传递标志位的方法

    #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 写文件(系统接口版)

    #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 读文件(系统接口版)

    #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 接口详解

    #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 → 系统调用接口。

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

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

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


    四、文件描述符 fd

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

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

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

    可直接读写 0/1/2:

    #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 的下标

    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

    struct task_struct { … struct files_struct *files; … };
    struct files_struct { … struct file **fd_array; … };

    4.3 文件描述符的分配规则

    #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 再打开新文件:

    #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 系统调用

    #include <unistd.h>

    int dup2(int oldfd, int newfd);

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

    #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 有完整实现。


    小结(上篇)

  • 文件:磁盘永久存储,本质是 IO;一切皆文件。
  • C 文件接口:fopen/fwrite/fread/fclose,打开文件本质是进程打开(/proc/[pid]/cwd 定位)。
  • 系统文件 I/O:open/close/read/write,标志位(位运算)传参。
  • 系统调用 vs 库函数:f* 是对系统调用的封装。
  • 文件描述符 fd:从 0 开始的小整数,本质是 fd_array 下标,分配规则 = 最小未使用下标。
  • 重定向:改变 fd 指向的文件对象,用 dup2 实现。
  • 赞(0)
    未经允许不得转载:171主机测评 » 【Linux系统】13 基础IO(上)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址