linux基础知识总结

125 阅读8分钟

文件读写

问题一: 在linux系统里面,open,read,write函数都不带缓冲吗

在 Linux 中,openreadwrite 这三个系统调用本身不带用户态缓冲,但内核会通过 “页缓存(Page Cache)” 提供内核态缓冲;而 C 标准库的文件操作函数(如 fopenfreadfwrite)才会在用户态额外加一层缓冲。

一、核心结论:系统调用 vs 标准库函数的缓冲差异

函数类型函数举例是否带用户态缓冲是否依赖内核态缓冲(页缓存)
Linux 系统调用openreadwrite不带依赖(默认开启)
C 标准库函数fopenfreadfwrite带(默认开启)依赖(底层仍调用系统调用)
  • 关键区分:“无缓冲” 仅指用户态没有额外缓存,内核的页缓存始终存在(除非主动关闭),用于减少磁盘 IO 次数。

二、详细解析:系统调用的 “无缓冲” 本质

1. open:仅负责打开文件,不涉及缓冲

open 的作用是向内核申请一个 “文件描述符(fd)”,记录文件的打开模式(读 / 写)、权限等元信息,不分配任何用户态缓存空间。例如:

int fd = open("test.txt", O_RDWR); // 仅获取文件描述符,无缓冲

2. read:直接从内核态读取,无用户态缓冲

read 的工作流程是:

  1. 接收用户传入的 “用户态缓冲区(如 char buf[1024])” 和读取长度。

  2. 切换到内核态,检查目标数据是否在页缓存中:

    • 若缓存命中:直接从页缓存拷贝数据到用户传入的 buf(1 次 CPU 拷贝)。
    • 若缓存未命中:触发磁盘 IO,将数据读入页缓存后,再拷贝到 buf
  3. 切换回用户态,返回实际读取的字节数。

核心特点read 不会在用户态维护额外缓存,每次调用都需要用户主动提供 buf,且数据仅在 buf 中临时存储(用户不处理就会丢失)。例如:

char buf[1024];
ssize_t n = read(fd, buf, sizeof(buf)); // 数据直接读入 buf,无用户态缓冲

3. write:直接写入内核态,无用户态缓冲

write 的工作流程类似:

  1. 接收用户传入的 “用户态数据地址(如 buf)” 和写入长度。
  2. 切换到内核态,将数据拷贝到页缓存(1 次 CPU 拷贝)。
  3. 内核会在合适的时机(如页缓存满、调用 fsync、进程退出),将页缓存中的数据刷写到磁盘(延迟写,默认开启)。
  4. 切换回用户态,返回实际写入的字节数。

核心特点write 也不维护用户态缓存,每次调用都需要用户主动传入数据地址,且数据先到页缓存,并非直接刷盘(除非用 O_DIRECT 或 fsync 强制刷盘)。例如:

char buf[] = "hello";
ssize_t n = write(fd, buf, strlen(buf)); // 数据先写入页缓存,无用户态缓冲

三、特殊场景:如何让系统调用 “完全无缓冲”(跳过页缓存)

默认情况下,read/write 依赖内核页缓存(属于内核态缓冲);若需彻底跳过页缓存(直接读写磁盘),可在 open 时加 O_DIRECT 标志:

// 打开文件时跳过页缓存,read/write 直接操作磁盘
int fd = open("test.txt", O_RDWR | O_DIRECT);
  • 注意:O_DIRECT 要求用户态缓冲区的地址和大小必须是 “磁盘块大小(如 4KB)” 的整数倍,否则会报错(EINVAL)。
  • 适用场景:数据库、文件系统等需要自己管理缓存的场景,避免内核页缓存的双重缓存开销。

四、对比:C 标准库的 “带缓冲” 如何实现

C 标准库的 fopen 会创建一个 FILE 结构体,其中包含:

  • 底层关联的文件描述符(fd)。
  • 用户态缓存缓冲区(如 4KB 大小)。
  • 缓存的读写位置、缓冲模式(全缓冲 / 行缓冲 / 无缓冲)等。

例如,fread 的工作流程:

  1. 先检查 FILE 结构体的用户态缓存是否有数据:

    • 若有:直接从缓存中返回数据,无需调用 read
    • 若无:调用 read 从内核页缓存读取数据,填满用户态缓存,再返回部分数据。

这种设计的目的是减少系统调用次数(系统调用切换用户态 / 内核态开销较大),例如读取 1000 字节,fread 可能只需 1 次 read(读 4KB 到缓存),而直接用 read 可能需要多次调用。

总结

  • open/read/write 作为 Linux 系统调用,不带用户态缓冲,但默认依赖内核的页缓存(内核态缓冲)来优化性能。
  • “无缓冲” 不等于 “直接读写磁盘”:除非用 O_DIRECT 跳过页缓存,否则数据仍会经过页缓存,只是用户态没有额外缓存。
  • 若需要减少系统调用次数,可使用 C 标准库的 fopen/fread/fwrite(带用户态缓冲);若需精确控制 IO 过程(如数据库),可直接用系统调用,甚至跳过页缓存。

问题2 dup和dup2的功能是什么?

在 Linux 系统中,dup 和 dup2 是两个用于复制文件描述符(file descriptor)的系统调用,它们的核心作用是创建一个已存在文件描述符的副本,使得多个文件描述符指向同一个打开的文件(或设备、管道等),共享同一套文件状态(如文件偏移量、文件状态标志等)。

一、dup 函数:创建文件描述符的副本

函数原型

c

运行

#include <unistd.h>
int dup(int oldfd);

作用

  • dup 会为参数 oldfd(已打开的文件描述符)创建一个新的文件描述符,新描述符是当前系统中最小的未被使用的文件描述符

  • 新、旧文件描述符指向同一个打开的文件,共享以下属性:

    • 文件偏移量(读写位置,对一个描述符执行 read/write 会影响另一个)。
    • 文件状态标志(如 O_RDONLYO_NONBLOCK 等)。
    • 文件锁(通过 fcntl 设置的锁)。
    • 引用计数(当所有副本都被关闭后,文件才真正被关闭)。

返回值

  • 成功:返回新的文件描述符(非负整数)。
  • 失败:返回 -1,并设置 errno(如 oldfd 无效时,errno 为 EBADF)。

二、dup2 函数:指定新文件描述符的数值

函数原型

#include <unistd.h>
int dup2(int oldfd, int newfd);

作用

  • dup2 与 dup 功能类似,也是创建 oldfd 的副本,但可以指定新文件描述符的数值为 newfd

  • 行为细节:

    1. 若 newfd 已被打开,dup2 会先自动关闭 newfd(除非 newfd 与 oldfd 相同,此时不做任何操作)。
    2. 新的 newfd 指向 oldfd 对应的文件,共享所有属性(同 dup)。

返回值

  • 成功:返回 newfd(即指定的新文件描述符)。
  • 失败:返回 -1,并设置 errno

三、核心区别:新文件描述符的取值

  • dup:新描述符是 “系统当前最小的未使用值”(无需手动指定)。
  • dup2:新描述符由用户指定为 newfd(若已被使用则先关闭)。

四、典型应用场景

1. 重定向标准输入 / 输出 / 错误(stdin/stdout/stderr

标准输入、输出、错误对应的文件描述符固定为 012。通过 dup2 可将其重定向到文件或管道。

示例:将标准输出重定向到文件

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    // 打开文件(创建新文件描述符 fd)
    int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open failed");
        return 1;
    }

    // 将 stdout(1)重定向到 fd 指向的文件
    // 即:后续 printf 会写入 output.txt 而非终端
    if (dup2(fd, 1) == -1) {
        perror("dup2 failed");
        close(fd);
        return 1;
    }

    // 关闭原 fd(已通过 dup2 复制到 1,无需保留)
    close(fd);

    // 此时 printf 输出到 output.txt
    printf("这行内容会写入文件而非终端\n");
    return 0;
}

2. 管道通信(进程间通信)

管道(pipe)的读写端通过文件描述符操作,dup2 可将管道描述符复制为标准输入 / 输出,简化进程间数据传递。

示例:子进程通过管道向父进程发送数据

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int pipefd[2];
    if (pipe(pipefd) == -1) { // 创建管道,pipefd[0] 读端,pipefd[1] 写端
        perror("pipe failed");
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        return 1;
    }

    if (pid == 0) { // 子进程:写数据到管道
        close(pipefd[0]); // 关闭读端(子进程只写)

        // 将标准输出(1)重定向到管道写端(pipefd[1])
        // 即:子进程的 printf 会写入管道
        if (dup2(pipefd[1], 1) == -1) {
            perror("dup2 failed");
            exit(1);
        }
        close(pipefd[1]); // 复制后关闭原写端

        printf("子进程发送的数据\n"); // 写入管道
        exit(0);
    } else { // 父进程:从管道读数据
        close(pipefd[1]); // 关闭写端(父进程只读)

        char buf[100];
        ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);
        if (n == -1) {
            perror("read failed");
            return 1;
        }
        buf[n] = '\0';
        printf("父进程收到:%s\n", buf); // 输出:子进程发送的数据

        close(pipefd[0]);
        return 0;
    }
}

3. 复制文件描述符以共享状态

例如,多个描述符共享文件偏移量,适合多线程 / 进程协作读写同一文件(需注意同步)。

五、注意事项

  1. 关闭描述符:副本和原描述符需分别关闭(close),只有当所有副本都关闭后,文件的引用计数才会归零,文件真正关闭。
  2. 错误处理oldfd 必须是有效的打开文件描述符,否则 dup/dup2 会失败。
  3. 原子操作dup2 关闭 newfd 和复制的过程是原子的,避免了手动关闭 newfd 可能导致的竞争条件(如 newfd 被其他线程复用)。

总结

dup 和 dup2 用于复制文件描述符,使得多个描述符指向同一文件并共享状态。dup 自动选择新描述符,dup2 可指定新描述符,二者在重定向、管道通信等场景中必不可少,是 Linux 中进程 IO 控制的核心工具