解码Linux文件IO之系统IO

系统 IO 与标准 IO 基础

核心思想:Linux "一切皆文件"

Linux 中所有资源(普通文件、目录、设备、套接字等)都以 "文件" 形式抽象,内核通过统一的 "文件描述符" 管理这些资源,系统 IO 就是内核提供的、直接操作这些 "文件" 的函数接口。

系统 IO 与标准 IO 的区别

对比维度 系统 IO(内核提供) 标准 IO(ANSI C 标准库提供)
缓冲区 无内置缓冲区 有缓冲区(全缓冲、行缓冲、无缓冲)
调用层级 直接调用内核接口,属于 "系统调用" 基于系统 IO 封装,属于 "库函数"
效率 频繁调用时效率低(每次切换内核态) 减少系统调用次数,效率更高
支持文件类型 所有文件(普通文件、设备、套接字) 仅支持普通文件
适用场景 实时性要求高的场景(如 LCD、触摸屏) 普通文件读写(如文本、配置文件)

通俗理解:标准 IO 像 "快递代收点"------ 先把用户要写的内容存到代收点(缓冲区),攒够一批再一次性交给内核(系统 IO),减少跑内核的次数;系统 IO 像 "直接送快递"------ 每次有内容都直接跑内核,实时但麻烦(切换内核态耗时)。

如:写 100 字节到文件

  • 标准 IO(fwrite):调用 1 次 fwrite,缓冲区存满 100 字节后,仅调用 1 次 write(系统 IO),快;
  • 系统 IO(write):若循环 100 次写 1 字节,需调用 100 次 write,慢。

文件描述符(fd):打开文件的 "身份证号"

本质

文件描述符(file descriptor,简称 fd)是进程内一个名为fd_array的数组的下标 。内核为每个进程维护一个file_struct结构体,其中的fd_array数组存储指向 "打开文件结构体(file)" 的指针,fd 就是通过这个下标找到对应的文件信息。

关键特性

  • 类型:非负整数(>=0);

  • 分配规则:每次打开文件,分配当前进程中 "最小未使用" 的 fd;

  • 默认 fd:进程启动时默认打开 3 个 fd,不可手动关闭(除非重定向):

    • 0:STDIN_FILENO(标准输入,如键盘);
    • 1:STDOUT_FILENO(标准输出,如终端);
    • 2:STDERR_FILENO(标准错误,如终端);
  • 独立性 :同一文件可多次打开,每次生成不同 fd,各自对应独立的file结构体(如读写偏移量独立)。

fd 的最大限制

  • 默认限制:每个进程最多打开 1024 个 fd(可通过ulimit -n查看);
  • 临时修改:ulimit -n 4096(仅当前终端有效);
  • 永久修改:修改/etc/security/limits.conf,添加soft nofile 4096hard nofile 8192

核心系统 IO 函数详解

open:打开或创建文件

功能

打开已存在的文件,或创建新文件,返回操作该文件的 fd。

函数

c 复制代码
#include <fcntl.h>
/**
 * 打开已存在文件,或创建新文件(需配合O_CREAT)
 * @param pathname目标文件的路径+文件名(如"./a.txt",绝对/相对路径均可)
 * @param flags   打开文件的选项(必选1个访问模式,可选多个创建/状态标志,用|连接)
 *                必选访问模式(三选一):
 *                  O_RDONLY:只读模式;O_WRONLY:只写模式;O_RDWR:可读可写模式
 *                可选创建/状态标志:
 *                  O_CREAT:文件不存在则创建(需配合第三个参数mode);
 *                  O_EXCL:与O_CREAT连用,若文件已存在则报错(避免覆盖);
 *                  O_TRUNC:打开普通文件时,清空文件原有内容;
 *                  O_APPEND:写操作前,自动将偏移量移到文件末尾(追加模式);
 *                  O_NONBLOCK:非阻塞模式(读写不等待,无数据时立即返回);
 *                  O_CLOEXEC:进程执行exec系列函数时,自动关闭该fd(避免泄露)
 *                  O_TMPFILE:创建一个无文件名的临时文件
 * @param mode    仅当flags含O_CREAT/O_TMPFILE时有效,指定新文件的初始权限(八进制)
 *                权限规则:owner(u)、group(g)、other(o)各有r(4)、w(2)、x(1),如:
 *                  0644:owner读+写(6=4+2),group读(4),other读(4);
 *                  0755:owner读+写+执行(7=4+2+1),group读+执行(5),other读+执行(5)
 *                注意:实际权限 = mode & ~umask(umask是系统默认权限掩码,默认0022)
 * @return        成功:返回非负整数(文件描述符fd);失败:返回-1,设置errno
 * @note          O_CREAT必须配合mode,否则新文件权限不确定;
 *                不可同时指定O_RDONLY和O_WRONLY,需三选一访问模式;
 *                设备文件(如/dev/tty)不支持O_TRUNC(清空无意义)
 */
int open(const char *pathname, int flags);          // 打开已存在文件,无mode
int open(const char *pathname, int flags, mode_t mode); // 创建新文件,需mode

示例:创建并打开文件

c 复制代码
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int main() {
    // 创建a.txt,初始权限0644
    int fd = open("a.txt", O_RDWR | O_CREAT | O_EXCL, 0644);
    if (fd == -1) {
        perror("open failed"); // 打印错误:open failed: File exists(若文件已存在)
        return -1;
    }
    printf("成功打开文件,fd=%d\n", fd); // 首次运行fd=3(默认0/1/2已占用)
    return 0;
}

close:关闭文件

功能

释放 fd 对应的内核资源(file结构体等),fd 变为 "未使用",可被后续 open 复用。

函数

c 复制代码
#include <unistd.h>
/**
 * 关闭文件描述符,释放内核资源
 * @param fd 要关闭的文件描述符(必须是当前进程已打开的有效fd)
 * @return   成功:返回0;失败:返回-1,设置errno(如fd无效时errno=EBADF)
 * @note     多次关闭同一fd:仅第一次成功,后续关闭返回-1(fd已无效);
 *           关闭fd=0/1/2(标准输入/输出/错误)后,若再open,新fd可能为0/1/2;
 *           进程退出时,内核会自动关闭所有未关闭的fd,但建议手动关闭(避免资源泄漏)
 */
int close(int fd);

示例:关闭文件

c 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main() {
    int fd = open("a.txt", O_RDWR);
    if (fd == -1) {
        perror("open failed");
        return -1;
    }

    // 第一次关闭:成功
    int ret = close(fd);
    if (ret == 0) {
        printf("第一次关闭成功\n");
    }

    // 第二次关闭:失败(fd已无效)
    ret = close(fd);
    if (ret == -1) {
        perror("第二次关闭失败"); // 输出:第二次关闭失败: Bad file descriptor
    }
    return 0;
}

read:从文件读取数据

功能

从 fd 对应的文件中,读取指定字节数的数据到用户提供的缓冲区。

函数

c 复制代码
#include <unistd.h>
/**
 * 从文件描述符读取数据到用户缓冲区
 * @param fd    已打开的文件描述符(需有读权限,如O_RDONLY/O_RDWR)
 * @param buf   用户缓冲区地址(需可写,存储读取到的数据)
 * @param count 期望读取的字节数(不能超过buf的实际大小,避免缓冲区溢出)
 * @return      成功:返回实际读取的字节数(可能小于count);
 *              0:到达文件末尾(EOF),无数据可读;
 *              -1:失败,设置errno(如fd无读权限时errno=EBADF)
 * @note        1. 实际读取字节数<count的场景:
 *                - 接近文件末尾(如文件剩50字节,count=100,返回50);
 *                - 读取管道/终端(如终端输入仅30字节,count=100,返回30);
 *                - 被信号中断(读取部分数据后,返回已读字节数);
 *              2. buf需提前分配空间(如char buf[1024];),不可为NULL
 */
ssize_t read(int fd, void *buf, size_t count);

示例:读取文件内容

c 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main() {
    int fd = open("a.txt", O_RDONLY);
    if (fd == -1) {
        perror("open failed");
        return -1;
    }

    char buf[1024] = {0}; // 初始化缓冲区(避免脏数据)
    // 读取最多1023字节(留1字节存'\0',方便打印)
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    if (n == -1) {
        perror("read failed");
        close(fd);
        return -1;
    } 
    else if (n == 0) {
        printf("文件为空\n");
    } 
    else {
        printf("读取到%d字节:%s\n", (int)n, buf);
    }

    close(fd);
    return 0;
}

write:向文件写入数据

功能

将用户缓冲区中的数据,写入到 fd 对应的文件中。

函数

c 复制代码
#include <unistd.h>
/**
 * 从用户缓冲区向文件描述符写入数据
 * @param fd    已打开的文件描述符(需有写权限,如O_WRONLY/O_RDWR)
 * @param buf   存储待写入数据的缓冲区(const修饰,避免被修改)
 * @param count 期望写入的字节数(通常是strlen(buf),若buf是字符串)
 * @return      成功:返回实际写入的字节数(可能小于count);
 *              -1:失败,设置errno(如磁盘满时errno=ENOSPC)
 * @note        实际写入字节数<count的场景:
 *                - 磁盘空间不足(无法写入全部数据);
 *                - 超过文件大小限制(RLIMIT_FSIZE,需用setrlimit修改);
 *                - 被信号中断(写入部分数据后返回);
 *              O_APPEND模式:每次write前,内核自动将偏移量移到文件末尾(原子操作,避免多进程追加冲突);
 *              普通文件写入后,文件偏移量会自动增加"实际写入字节数"
 */
ssize_t write(int fd, const void *buf, size_t count);

示例:向文件写入数据

c 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main() {
    // 打开文件,若存在则追加,不存在则创建(权限0644)
    int fd = open("a.txt", O_WRONLY | O_APPEND | O_CREAT, 0644);
    if (fd == -1) {
        perror("open failed");
        return -1;
    }

    const char *msg = "Hello, System IO!\n";
    // 写入msg的全部字节(strlen(msg)计算长度)
    ssize_t n = write(fd, msg, strlen(msg));
    if (n == -1) {
        perror("write failed");
        close(fd);
        return -1;
    }
    printf("成功写入%d字节\n", (int)n);

    close(fd);
    return 0;
}

lseek:设置文件读写偏移量

功能

调整 fd 对应的文件的 "读写位置(偏移量)",仅改变位置,不实际读写数据。偏移量是从文件开头计算的字节数。

函数

c 复制代码
#include <sys/types.h>
#include <unistd.h>
/**
 * 设置文件描述符的读写偏移量
 * @param fd     已打开的文件描述符(需支持seek,普通文件/设备支持,管道/套接字不支持)
 * @param offset 偏移量(可正可负,具体含义由whence决定)
 * @param whence 偏移量的参考基准:
 *                SEEK_SET:以文件开头为基准,offset=0表示文件开头;
 *                SEEK_CUR:以当前偏移量为基准,offset=5表示向后移5字节,offset=-3表示向前移3字节;
 *                SEEK_END:以文件末尾为基准,offset=0表示文件末尾,offset=-10表示末尾前10字节
 * @return       成功:返回调整后的偏移量(从文件开头计算的字节数);
 *               -1:失败,设置errno(如管道不支持seek时errno=ESPIPE)
 * @note         文件空洞:若offset超过文件当前大小,后续write会在"空洞部分"填'\0',但空洞不占用磁盘空间;
 *               文本文件建议用SEEK_SET(避免换行符导致的偏移计算错误);
 *               off_t是长整型(32/64位),不是int,需包含<sys/types.h>
 */
off_t lseek(int fd, off_t offset, int whence);

示例:指定偏移量读取

c 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main() {
    int fd = open("a.txt", O_RDWR);
    if (fd == -1) {
        perror("open failed");
        return -1;
    }

    // 定位到文件开头(偏移量0)
    off_t pos = lseek(fd, 0, SEEK_SET);
    printf("当前偏移量(开头):%ld\n", pos);

    // 定位到文件末尾,获取文件大小(偏移量=文件大小)
    pos = lseek(fd, 0, SEEK_END);
    printf("文件大小:%ld字节\n", pos);

    // 定位到末尾前10字节,读取这10字节
    pos = lseek(fd, -10, SEEK_END);
    char buf[11] = {0};
    ssize_t n = read(fd, buf, 10);
    if (n > 0) {
        printf("末尾前10字节:%s\n", buf);
    }

    close(fd);
    return 0;
}

dup/dup2:复制文件描述符

功能

复制已有的 fd,新 fd 与原 fd 共享同一个file结构体(即共享读写偏移量、文件状态等)。

函数(dup)

c 复制代码
#include <unistd.h>
/**
 * 复制文件描述符(自动分配最小未用fd)
 * @param oldfd 已打开的源文件描述符(必须有效)
 * @return      成功:返回新的文件描述符(当前进程最小未用fd);
 *              -1:失败,设置errno(如oldfd无效时errno=EBADF)
 * @note        新fd与oldfd共享:读写偏移量、文件权限、状态标志(如O_APPEND);
 *              关闭新fd不影响oldfd,反之亦然,但关闭任意一个,另一个仍可操作文件
 */
int dup(int oldfd);

函数(dup2)

c 复制代码
#include <unistd.h>
/**
 * 复制文件描述符(指定新fd的数值)
 * @param oldfd 已打开的源文件描述符(必须有效)
 * @param newfd 期望的新文件描述符数值
 * @return      成功:返回newfd(若newfd已打开,先自动关闭newfd再复制);
 *              -1:失败,设置errno(如oldfd无效或newfd=oldfd时无操作)
 * @note        若newfd已打开且≠oldfd,dup2会先关闭newfd(即使关闭失败,仍会继续复制);
 *              常用场景:重定向(如将stdout(fd=1)重定向到文件);
 *              若newfd=oldfd,直接返回newfd(无复制操作)
 */
int dup2(int oldfd, int newfd);

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

c 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main() {
    // 打开日志文件(追加模式)
    int log_fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT, 0644);
    if (log_fd == -1) {
        perror("open log failed");
        return -1;
    }

    // 将stdout(fd=1)重定向到log_fd:关闭fd=1,再复制log_fd为fd=1
    dup2(log_fd, 1);

    // 此时printf会写入log.txt(而非终端)
    printf("这是一条日志,时间:%s\n", __TIME__);

    close(log_fd);
    return 0;
}

fcntl:文件控制(灵活操作 fd)

功能

对已打开的 fd 进行多样化控制,如获取 / 设置文件状态、设置非阻塞、复制 fd 等(功能最灵活的系统 IO 函数)。

函数

c 复制代码
#include <fcntl.h>
/**
 * 文件描述符控制函数(变参函数,参数由cmd决定)
 * @param fd  已打开的文件描述符
 * @param cmd 控制命令(决定函数功能),常用命令:
 *            F_GETFL:获取文件状态标志(如O_APPEND、O_NONBLOCK);
 *            F_SETFL:设置文件状态标志(仅支持O_APPEND、O_NONBLOCK等部分标志);
 *            F_DUPFD:复制fd,类似dup,参数为"最小允许的新fd";
 *            F_GETFD:获取fd的标志(如FD_CLOEXEC);
 *            F_SETFD:设置fd的标志(如FD_CLOEXEC)
 * @param ... 可选参数,由cmd决定(如F_SETFL需传"新状态标志")
 * @return    成功:返回值由cmd决定(F_GETFL返回状态标志,F_DUPFD返回新fd);
 *            -1:失败,设置errno
 * @note       变参函数:cmd不同,后续参数不同,需严格匹配;
 *             F_SETFL不能修改所有标志(如O_RDONLY/O_WRONLY/O_RDWR不能改,需重新open);
 *             常用场景:设置非阻塞IO、获取文件打开模式
 */
int fcntl(int fd, int cmd, ... /* 可选参数 */);

示例 1:设置非阻塞模式

阻塞与非阻塞 IO

  • 阻塞 IO:默认模式,读写时若无数据 / 资源,进程会 "等待"(阻塞),直到有数据 / 资源;
  • 非阻塞 IO:读写时若无数据 / 资源,立即返回 - 1,errno=EAGAIN/EWOULDBLOCK,进程不等待;
  • 适用场景:网络编程(如服务器同时处理多个客户端)、设备操作(如读取传感器数据)。
c 复制代码
#include <fcntl.h>
#include <stdio.h>
int main() {
    // 打开标准输入(fd=0,键盘)
    int fd = 0;

    // 获取当前状态标志
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        perror("fcntl F_GETFL failed");
        return -1;
    }

    // 添加非阻塞标志(O_NONBLOCK)
    flags |= O_NONBLOCK;

    // 设置新状态标志
    if (fcntl(fd, F_SETFL, flags) == -1) {
        perror("fcntl F_SETFL failed");
        return -1;
    }

    printf("标准输入已设置为非阻塞模式\n");
    return 0;
}

示例 2:获取文件打开模式

c 复制代码
#include <fcntl.h>
#include <stdio.h>
int main() {
    int fd = open("a.txt", O_RDWR | O_APPEND);
    if (fd == -1) {
        perror("open failed");
        return -1;
    }

    // 获取状态标志
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        perror("fcntl failed");
        close(fd);
        return -1;
    }

    // 判断打开模式
    if (flags & O_RDWR) {
        printf("文件打开模式:可读可写\n");
    }
    if (flags & O_APPEND) {
        printf("文件状态:追加模式\n");
    }

    close(fd);
    return 0;
}

/*
结果:
文件打开模式:可读可写
文件状态:追加模式
*/

原子操作

  • 定义:操作要么完全执行,要么完全不执行,无中间状态;
  • 示例:O_APPEND 模式的 write(偏移到末尾 + 写入一步完成,避免多进程追加冲突);
  • 非原子操作:手动 lseek 到末尾再 write(多进程时可能出现数据覆盖)。

错误处理:定位系统 IO 的 "问题"

核心工具:errno 与错误函数

  • errno :全局变量(定义在<errno.h>),系统调用失败时自动设置(成功时值不确定,不可用);
  • strerror :将 errno 转为人类可读的错误信息字符串(<string.h>);
  • perror :直接打印错误信息(格式:"自定义信息:错误信息",<stdio.h>)。

示例:错误处理实战

c 复制代码
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
int main() {
    int fd = open("nonexist.txt", O_RDONLY);
    if (fd == -1) {
        // 方法1:用perror打印
        perror("open nonexist.txt failed");

        // 方法2:用strerror打印
        fprintf(stderr,"open failed: errno=%d, info=%s\n", errno, strerror(errno));
        return -1;
    }

    close(fd);
    return 0;
}

输出

复制代码
open nonexist.txt failed: No such file or directory
open failed: errno=2, info=No such file or directory

文件权限与 umask:控制谁能操作文件

权限组成(r/w/x)

文件权限分为三类用户,每类用户有 3 种权限:

用户类型 含义 权限符号 权限数值
owner(u) 文件所有者(创建者) r/w/x 4/2/1
group(g) 文件所属组的用户 r/w/x 4/2/1
other(o) 其他用户 r/w/x 4/2/1

示例

  • 0644:owner(6=4+2,读 + 写),group(4,读),other(4,读);
  • 0755:owner(7=4+2+1,读 + 写 + 执行),group(5=4+1,读 + 执行),other(5,读 + 执行)。

umask:系统权限掩码

  • 作用:限制新文件的默认权限(避免权限过宽);
  • 计算方式:实际权限 = mode(open 的第三个参数) & ~umask;
  • 默认值:Linux 默认 umask=0022(八进制),即屏蔽 group 和 other 的 "写权限";
  • 修改方式
    • shell 临时修改:umask 0002(仅当前终端有效);
    • 程序中修改:umask(0002)(需包含<sys/stat.h>)。

示例:open 时 mode=0666,umask=0022:实际权限 = 0666 & ~0022 = 0644(group 和 other 的写权限被屏蔽)。

相关推荐
Konwledging15 分钟前
kernel-devel_kernel-headers_libmodules
linux
Web极客码17 分钟前
CentOS 7.x如何快速升级到CentOS 7.9
linux·运维·centos
一位赵37 分钟前
小练2 选择题
linux·运维·windows
代码游侠1 小时前
学习笔记——Linux字符设备驱动开发
linux·arm开发·驱动开发·单片机·嵌入式硬件·学习·算法
Lw老王要学习2 小时前
CentOS 7.9达梦数据库安装全流程解析
linux·运维·数据库·centos·达梦
Kaede62 小时前
提示dns服务器未响应,需要做哪些事?
运维·服务器
CRUD酱2 小时前
CentOS的yum仓库失效问题解决(换镜像源)
linux·运维·服务器·centos
We....2 小时前
鸿蒙与Java跨平台Socket通信实战
java·服务器·tcp/ip·arkts·鸿蒙
zly35002 小时前
VMware vCenter Converter Standalone 转换Linux系统,出现两个磁盘的处理
linux·运维·服务器
珠海西格3 小时前
1MW光伏项目“四可”装置改造:逆变器兼容性评估方法详解
大数据·运维·服务器·云计算·能源