当前位置: 首页 > news >正文

解码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 就是通过这个下标找到对应的文件信息。

image

关键特性

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

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

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

    • 0:STDIN_FILENO(标准输入,如键盘);
    • 1:STDOUT_FILENO(标准输出,如终端);
    • 2:STDERR_FILENO(标准错误,如终端);

    image

  • 独立性:同一文件可多次打开,每次生成不同 fd,各自对应独立的file结构体(如读写偏移量独立)。

fd 的最大限制

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

核心系统 IO 函数详解

open:打开或创建文件

功能

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

函数

#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

示例:创建并打开文件

#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int main() {// 创建a.txt,初始权限0644int 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 复用。

函数


#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);

示例:关闭文件

#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 对应的文件中,读取指定字节数的数据到用户提供的缓冲区。

函数

#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);

示例:读取文件内容

#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 对应的文件中。

函数

#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);

示例:向文件写入数据

#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 对应的文件的 “读写位置(偏移量)”,仅改变位置,不实际读写数据。偏移量是从文件开头计算的字节数。

函数

#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);

示例:指定偏移量读取

#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结构体(即共享读写偏移量、文件状态等)。

image

函数(dup)

#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)

#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);

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

#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=1dup2(log_fd, 1);// 此时printf会写入log.txt(而非终端)printf("这是一条日志,时间:%s\n", __TIME__);close(log_fd);return 0;
}

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

功能

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

函数

#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,进程不等待;
  • 适用场景:网络编程(如服务器同时处理多个客户端)、设备操作(如读取传感器数据)。
#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:获取文件打开模式

#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>)。

示例:错误处理实战

#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/x4/2/1
group(g)文件所属组的用户r/w/x4/2/1
other(o)其他用户r/w/x4/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 的写权限被屏蔽)。

http://www.dtcms.com/a/479134.html

相关文章:

  • 重庆做网站的公司网站开发都做什么
  • 商丘做网站一般多少钱军事新闻直播在线观看
  • LibGDX游戏开发性能优化实战:对象池模式在LibGDX中的应用
  • 网站 空间 租用帝国网站地图模板
  • 贸易网站源码电子商务网站规划方案
  • mysql读写分离中间件Atlas安装部署及使用
  • MySQL ORDER BY 深度解析:索引排序规则与关键配置参数阈值​
  • electron 套壳
  • 网站建设技术架构为了推广公众号可以采取的方法有
  • 网站建设蓝色工匠美创网站建设优势
  • 项目1:FFMPEG推流器讲解(五):FFMPEG时间戳、时间基、时间转换的讲解
  • 如何让自己网站排名提高步骤怎么写
  • 承德网站网站建设做外贸生意用哪个网站最好
  • 一、前置基础(MVC学习前提)_核心特性_【C# OOP 入门】从生活例子看懂类、继承、多态和封装,避坑指南来了!
  • RNN代码实战专项
  • 金蝶云·星瀚 | 生产制造成本核算终极实操手册(从0到1,含两套完整案例)
  • 千灯网站建设自由贸易试验区网站建设方案
  • 理解 JavaScript 中的 this 上下文保存
  • LLC系列--变压器
  • qwen2.5vl 模型配置记录
  • 无锡网站建设制作设计wordpress模板淘客
  • 平原县网站seo优化排名深入解析wordpress(原书第2版)
  • 云手机 手游专用虚拟手机
  • 网站开发模块就业前景怎么建设游网站主页
  • 神卓 N600:内网穿透需求的高效安全之选
  • 以营销导向型建设网站方案深圳福永网站建设
  • 企业网站带后台模板包括搜索引擎排名、网页标签优化、相关链接交换、网络广告投放等
  • Spring 事务传播机制
  • 免费个人网站建站能上传视频吗网页制作公司文案
  • 海南建设局网站关键词排名怎么上首页