Linux 文件 IO一、两种 IO 方式的本质区别在 Linux 下操作文件主要有两套接口它们的核心区别在于是否带有用户态缓冲区类型接口函数族缓冲区位置适用场景C 标准库 IO带缓冲fopen/fread/fwrite/fgets/fputs/fprintf等用户态FILE结构体内部频繁读写小数据、文本处理、追求代码可移植性系统调用 IO无缓冲open/read/write/close/lseek等无用户态缓冲直接交给内核大数据块传输、网络通信、需要精确控制缓冲的场景核心记忆标准库 IO 是为了减少read/write系统调用次数从而提高效率系统调用是真正向内核“发号施令”的入口。二、C 标准库文件 IO 函数带用户态缓冲区1. 打开与关闭fopen/fcloseFILE *fopen(const char *pathname, const char *mode);返回值成功返回FILE*指针失败返回NULL并设置errno。mode打开模式r只读文件必须存在w只写存在则清空不存在则创建a追加存在则末尾追加不存在则创建r读写文件必须存在w读写存在则清空不存在则创建a读写追加存在则末尾追加不存在则创建int fclose(FILE *stream);返回值成功返回0失败返回EOF通常为 -1并设置errno。FILE*fpfopen(test.txt,w);if(fpNULL){perror(fopen 失败);// 自动打印错误原因return-1;}// ... 读写操作 ...if(fclose(fp)!0){perror(fclose 失败);}2. 文本行读写fgets/fputsint fputs(const char *s, FILE *stream);返回值成功返回非负整数失败返回EOF。注意不会自动添加换行符\n需要手动加。char *fgets(char *s, int size, FILE *stream);返回值成功返回s的指针读到文件尾EOF或出错返回NULL。注意最多读取size-1个字符会自动在末尾追加\0并且会保留换行符如果读到的话。// 写入两行fputs(Hello, 这是第一行\n,fp);// 加 \n 换行fputs(这是第二行,fp);// 不加 \n 则不会换行charbuffer[128];while(fgets(buffer,sizeof(buffer),fp)!NULL){printf(读到: %s,buffer);// 因为 fgets 会保留 \n所以 printf 不用再加 \n}3. 二进制块读写fread/fwrite适用于结构体、数组等size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);返回值成功返回实际写入的元素个数不是字节数。如果与nmemb不等则说明发生了写入错误。size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);返回值成功返回实际读取的元素个数。若小于nmemb可能遇到了文件尾feof或发生了读取错误ferror。intarr[3]{10,20,30};// 写 3 个 int每个 int 占 4 字节size_twrittenfwrite(arr,sizeof(int),3,fp);if(written!3){printf(警告只写入了 %zu 个元素\n,written);}// 重置文件指针到开头稍后详细讲fseek(fp,0,SEEK_SET);intread_arr[3];size_tread_numfread(read_arr,sizeof(int),3,fp);if(read_num3){printf(成功读到: %d, %d, %d\n,read_arr[0],read_arr[1],read_arr[2]);}4. 文件指针定位fseek/ftellint fseek(FILE *stream, long offset, int whence);返回值成功返回0失败返回非0通常为 -1。whence起始位置SEEK_SET文件开头offset 0SEEK_CUR当前位置offset可正可负SEEK_END文件末尾offset通常为 0 或负数long ftell(FILE *stream);返回当前文件指针距开头的字节数失败返回 -1L。fseek(fp,0,SEEK_SET);// 移到文件开头常用于写完再读fseek(fp,0,SEEK_END);// 移到文件末尾longsizeftell(fp);// 此时 size 就是文件总大小字节fseek(fp,-10,SEEK_CUR);// 从当前位置向前移动 10 个字节5. 强制刷新缓冲区fflushint fflush(FILE *stream);返回值成功返回0失败返回EOF。作用将用户态FILE缓冲区中未写入的数据立即通过write系统调用刷入内核。在调试或需要确保数据落盘时非常有用。三、系统调用文件 IO 函数无用户态缓冲区系统调用直接由内核提供服务每次调用都会陷入内核态。1. 打开与关闭open/closeint open(const char *pathname, int flags, ...);返回值成功返回最小未使用的文件描述符0失败返回-1并设置errno。必选标志flagsO_RDONLY、O_WRONLY、O_RDWR三选一。可选标志按位或|O_CREAT文件不存在则创建此时必须提供第三个参数mode指定权限。O_EXCL与O_CREAT配合若文件已存在则报错防止覆盖。O_TRUNC若文件存在且为普通文件打开时清空内容。O_APPEND每次write之前自动将文件偏移量移到末尾追加写入。O_NONBLOCK非阻塞模式常用于管道、设备文件。权限计算最终权限 mode ~umask。例如umask为0022mode0666→0666 ~0022 0644即rw-r--r--。int close(int fd);返回值成功返回0失败返回-1。#includefcntl.h#includeunistd.hintfdopen(test.txt,O_CREAT|O_RDWR|O_APPEND,0644);if(fd-1){perror(open);exit(1);}// ... 读写操作 ...close(fd);2. 读写read/writessize_t read(int fd, void *buf, size_t count);返回值成功返回实际读取的字节数0 表示读到文件尾。失败返回-1并设置errno。ssize_t write(int fd, const void *buf, size_t count);返回值成功返回实际写入的字节数可能小于count如磁盘满或信号中断。失败返回-1并设置errno。经典拷贝代码无缓冲循环读写#includestdio.h// perror#includestdlib.h// exit#includeunistd.h// read, write, close#includefcntl.h// open#defineBUFSIZE1024voidcopy_data(intfd_in){charbuf[BUFSIZE];ssize_tn;while((nread(fd_in,buf,sizeof(buf)))0){// 注意write 可能写不完必须循环保证写全ssize_twritten0;while(writtenn){ssize_tretwrite(STDOUT_FILENO,bufwritten,n-written);if(ret-1){perror(write);exit(1);}writtenret;}}if(n-1){perror(read);exit(1);}}intmain(intargc,char*argv[]){intfd;if(argc1){fdopen(argv[1],O_RDONLY);if(fd-1){perror(open);exit(1);}copy_data(fd);close(fd);}else{// 无参数时从标准输入fd0读取copy_data(STDIN_FILENO);}return0;}3. 文件偏移lseekoff_t lseek(int fd, off_t offset, int whence);返回值成功返回从文件开头到新位置的偏移字节数失败返回(off_t)-1。whence同fseekSEEK_SET、SEEK_CUR、SEEK_END。off_tposlseek(fd,0,SEEK_CUR);// 获取当前偏移printf(当前偏移量: %ld\n,pos);lseek(fd,0,SEEK_SET);// 移到文件头4. 高级控制fcntl与ioctl1.fcntl—— 文件控制本质对已打开的文件描述符执行各种控制操作如复制、获取/设置标志、加锁等。头文件#include fcntl.h和#include unistd.h函数原型intfcntl(intfd,intcmd,.../* arg */);参数fd要操作的文件描述符。cmd要执行的命令告诉内核要干什么。...可变参数可选的第三个参数通常是一个整数或指针具体含义取决于cmd。返回值成功时返回值取决于cmd具体见下表失败返回-1并设置errno。常用命令cmd命令cmd作用第三个参数arg成功返回值F_DUPFD复制描述符类似dup整数最小返回描述符值返回新描述符F_GETFL获取文件状态标志即open时的 flags忽略返回标志整数值如O_RDWR、O_APPENDF_SETFL设置/修改文件状态标志整数新的标志值成功返回0F_GETFD获取文件描述符标志仅FD_CLOEXEC忽略返回标志值F_SETFD设置文件描述符标志整数新标志值成功返回0注意F_SETFL虽然能改标志但它只能改一部分标志主要包括O_APPEND追加模式O_NONBLOCK非阻塞模式O_ASYNC异步通知它无法修改open时确定的读写模式O_RDONLY/O_WRONLY/O_RDWR。如果你试图通过F_SETFL把只读文件改成可写内核会直接忽略或报错因为磁盘文件的访问权限是打开时就固定死的。获取并添加非阻塞标#includefcntl.h#includestdio.h#includestdlib.h#includeunistd.h#includeerrno.hintset_nonblock(intfd){// 1. 先获取当前文件状态标志F_GETFL 不需要第三个参数intflagsfcntl(fd,F_GETFL);if(flags-1){perror(fcntl F_GETFL);return-1;}// 2. 判断是否已经是非阻塞避免重复设置if(flagsO_NONBLOCK){printf(文件描述符 %d 已经是非阻塞模式\n,fd);return0;}// 3. 添加非阻塞标志按位或flags|O_NONBLOCK;// 4. 写回新的标志F_SETFL 需要第三个参数新标志值if(fcntl(fd,F_SETFL,flags)-1){perror(fcntl F_SETFL);return-1;}printf(成功将 fd %d 设置为非阻塞模式\n,fd);return0;}intmain(){// 模拟stdin标准输入默认是阻塞的if(set_nonblock(STDIN_FILENO)0){// 此时 read(stdin) 如果没数据会立即返回 -1 并置 errnoEAGAINcharbuf[10];ssize_tretread(STDIN_FILENO,buf,sizeof(buf));if(ret-1errnoEAGAIN){printf(当前没有输入数据read 立即返回了非阻塞效果\n);}}return0;}2.ioctl—— 设备控制本质用于对特殊设备文件如终端/dev/tty、串口/dev/ttyS0、声卡、网卡等执行通用 read/write 无法覆盖的底层操作。它是设备驱动向用户态暴露“自定义功能”的主要入口。头文件#include sys/ioctl.h函数原型intioctl(intfd,unsignedlongrequest,.../* arg */);参数fd设备文件对应的文件描述符必须由open成功打开。request与设备相关的请求码告诉驱动程序“你想让它干什么”如调整音量、设置波特率、获取窗口大小。对于请求码可询问AI。...可变参数通常是一个指针指向存放数据的内存地址用于传出配置参数或传入要设置的参数。返回值成功通常返回0具体视驱动而定有些会返回数据长度失败返回-1并设置errno。fcntl与ioctl的核心区别对比表特性fcntlioctl全称File Control文件控制I/O Control输入输出控制操作对象任何文件描述符普通文件、管道、socket、设备均可特殊设备文件终端、串口、声卡、网卡等主要用途复制描述符、修改非阻塞/追加标志、文件锁、管道容量设置串口波特率/数据位、获取终端窗口大小、格式化磁盘、网卡混杂模式标准化程度高度标准化POSIX 规范不同系统行为一致设备强相关每个设备驱动的request号码和arg含义都不同可移植性高跨平台通用低换种硬件或驱动请求码就可能变了获取终端窗口大小TIOCGWINSZ这是ioctl经典的用法之一#includestdio.h#includestdlib.h#includeunistd.h#includesys/ioctl.h// ioctl 函数#includetermios.h// struct winsize 定义在这里或 sys/ttycom.hintmain(){structwinsizews;// 用于存放窗口大小行、列// STDOUT_FILENO 是标准输出通常是终端设备// TIOCGWINSZ 是请求码Get Window Size获取窗口大小if(ioctl(STDOUT_FILENO,TIOCGWINSZ,ws)-1){perror(ioctl TIOCGWINSZ可能当前不是终端设备);return1;}printf(当前终端窗口大小\n);printf( - 行数rows%d\n,ws.ws_row);printf( - 列数cols%d\n,ws.ws_col);return0;}注如果把标准输出重定向到普通文件ioctl会返回 -1因为普通文件不认识TIOCGWINSZ这个请求码3. 为什么要区分这两个函数fcntl管的是“内核文件描述符”的共性所有fd在内核里都有一个通用的struct file结构体里面包含了f_flags文件状态标志等通用属性。fcntl就是操作这一层的。ioctl管的是“具体设备驱动”的个性每个设备驱动如串口驱动、声卡驱动都有自己的私有数据结构和控制逻辑通用读写满足不了它们。于是内核给它们开了一个“后门”ioctl系统调用让设备驱动程序自己去解析request和arg从而实现千奇百怪的设备特有功能。四、传入传出参数C 语言指针语义在系统编程中指针参数常被用于传递数据传入参数被const修饰函数只读不写如strcpy(const char *src, ...)。传出参数函数内部对其指向的内存进行写操作调用后此内存包含有效数据如read(int fd, void *buf, ...)中的buf内核把数据填入其中。传入传出参数既读又写如select中的fd_set。五、文件描述符与 PCB1. PCB进程控制块内核为每个进程维护一个结构体struct task_struct定义在linux/sched.h。其中与文件相关的核心成员是struct files_struct *files它指向当前进程的文件描述符表。2. 文件描述符表本质上是一个指针数组数组下标就是文件描述符fd数组中存储的是内核文件对象struct file *的地址。分配规则新打开的文件总是占用当前最小可用的非负整数下标0、1、2 默认已被占用。三个默认描述符0STDIN_FILENO标准输入1STDOUT_FILENO标准输出2STDERR_FILENO标准错误3. 最大打开文件数限制进程级软限制默认1024。查看与临时修改ulimit-n# 查看ulimit-n4096# 临时修改当前 Shell 生效永久修改编辑/etc/security/limits.conf。系统级硬限制/proc/sys/fs/file-max记录系统全局最大文件数受内存影响进程无法逾越。六、用户态FILE与 内核态struct file层级结构体定义位置关键内容用户态FILEC 库stdio.h包含文件描述符fd、用户态 I/O 缓冲区、用户态读写位置。内核态struct filelinux/fs.h包含真实偏移量f_pos、权限f_mode、打开标志f_flags、内核缓存页地址、操作方法集f_op。用户态验证代码FILE*fpfopen(test.txt,w);intfdfileno(fp);// 获取 FILE* 内部隐藏的 fdprintf(fd %d\n,fd);// 通常为 3fprintf(fp,Hello);// 先写入用户态缓冲区fflush(fp);// 强制调用 write(fd, ...) 刷入内核fclose(fp);内核态关键成员与挂钩机制当我们在用户态调用write(fd, buf, count)时内核的操作路径如下fd → 当前进程 fdtable[] → struct file *file → file-f_op-write(...)f_op指向struct file_operations包含read、write、llseek、poll等函数指针。不同文件系统ext4、NFS、socket会注册自己专属的实现。七、性能分析strace命令strace可以跟踪程序运行过程中发生的所有系统调用是验证上述理论的绝佳工具。基本用法# 跟踪所有系统调用strace./my_test# 只跟踪文件相关的核心调用过滤输出strace-etraceopen,close,lseek,fcntl,write ./my_test# 统计各系统调用的耗时和次数性能分析strace-c./my_test# 保存日志到文件strace-ostrace.log ./my_test八、内核优化策略预读入缓输出预读入Read-ahead当应用程序发起read请求读某块数据时内核会“自作主张”地提前读取后续相邻的数据块存入内核页缓存Page Cache。好处下次你再读接下来的内容时数据早已在内核内存中直接拷贝给用户态即可极大减少磁盘寻道和延迟。缓输出Write-back / Delayed write当应用程序调用write时内核并不一定会立刻把数据写入物理磁盘而是先写入内核页缓存并标记该页为“脏页”。内核在后台异步将这些脏页刷入磁盘由pdflush或kworker线程负责。好处write系统调用可以极快返回不阻塞应用程序同时内核有机会合并多次小写入为一次大写入优化磁盘寿命和吞吐量。注意若想强制数据落盘如数据库场景可使用fsync、fdatasync或open时加O_SYNC标志。