ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【Linux 系统篇(二十五)】文件(二) 系统文件I/O详解

【Linux 系统篇(二十五)】文件(二) 系统文件I/O详解 大家好欢迎来到 huangjin007_ 的博客⭐个人主页huangjin007_文章收录专栏Linux 内功修炼手册(系统篇)总会有一些坚持能从冰封的土地里培育出十万朵怒放的蔷薇Linux 系统篇(二十五) —— 系统文件I/O文章目录Linux 系统篇(二十五) —— 系统文件I/O一、从标志位说起1.1 问题如何给函数传递多个开关1.2 位运算基础1.3 示例代码与解析1.4 为什么系统调用也这么用二、系统文件 I/O 初体验2.1 写文件hello.c2.2 读文件hello.c三、open 函数详解3.1 函数原型与头文件3.2 flags 参数打开模式与附加选项3.3 mode 参数与 umask3.4 返回值文件描述符3.5 语言层与系统调用的映射3.6 文本写入 vs 二进制写入四、系统调用与库函数4.1 库函数fopen, fclose, fread, fwrite4.2 系统调用open, close, read, write4.3 关系图4.4 为什么要有库函数五、文件描述符 fd5.1 fd 是什么5.2 默认打开的 0、1、25.3 文件描述符的分配规则最小未使用5.4 文件描述符的分配规则总结六、重定向6.1 输出重定向close(1) open6.2 重定向的本质6.3 dup2 系统调用6.4 输入重定向dup2(fd, 0)七、深入内核7.1 进程控制块 task_struct7.2 files_struct 与 fd_array7.3 struct file打开的文件对象7.4 inode磁盘文件的元信息7.5 一个进程可以打开多个文件一个文件可以被多个进程打开7.6 引用计数 ref_cnt结语一、从标志位说起1.1 问题如何给函数传递多个开关假设你要写一个函数它支持三种功能ONE、TWO、THREE。用户可以单独使用某一个也可以组合使用。怎么设计参数一种笨办法是传三个布尔值func(bool one, bool two, bool three)。但这样不优雅而且如果功能增加到几十个参数列表会爆炸。更好的办法是用一个整数的不同二进制位来表示不同的功能。这就是位掩码bitmask。1.2 位运算基础先复习一下位运算按位与两个位都为 1 时结果为 1否则为 0。|按位或两个位只要有一个为 1结果就为 1。左移把二进制位向左移动右边补 0。例如#defineONE(10)// 0000 0001#defineTWO(11)// 0000 0010#defineTHREE(12)// 0000 0100#defineFOUR(13)// 0000 1000这样每个宏只占一个二进制位互不干扰。1.3 示例代码与解析代码#includestdio.h#defineONE0001// 0000 0001#defineTWO0002// 0000 0010#defineTHREE0004// 0000 0100voidfunc(intflags){if(flagsONE)printf(flags has ONE! );if(flagsTWO)printf(flags has TWO! );if(flagsTHREE)printf(flags has THREE! );printf(\n);}intmain(){func(ONE);func(THREE);func(ONE|TWO);func(ONE|THREE|TWO);return0;}解析ONE、TWO、THREE分别是八进制数0001、0002、0004对应二进制的001、010、100。func(ONE)传入0001flags ONE非零打印 ONE。func(ONE | TWO)0001 | 0010 0011所以 ONE 和 TWO 都会被打印。通过运算可以检测某个位是否被设置通过|运算可以组合多个标志。1.4 为什么系统调用也这么用因为系统调用是 C 函数参数是整数。用位掩码可以在一个整数里塞下几十个开关既节省参数又灵活高效。二、系统文件 I/O 初体验语言层的fopen、fwrite我们很熟悉。现在看看如何用系统调用实现同样的功能。2.1 写文件hello.c#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#includeunistd.h#includestring.hintmain(){umask(0);// 设置文件创建时的权限掩码为 0这样创建的文件权限就是我们指定的 0644intfdopen(myfile,O_WRONLY|O_CREAT,0644);if(fd0){perror(open);return1;}intcount5;constchar*msghello Linux!\n;intlenstrlen(msg);while(count--){write(fd,msg,len);// fd: 文件描述符msg: 缓冲区首地址len: 期望写入的字节数// 返回值实际写入的字节数}close(fd);return0;}解释umask(0)umask是文件权限掩码。系统在创建文件时会用mode ~umask计算实际权限。把umask设为 0文件权限就完全由open的第三个参数决定。这里我们指定0644即rw-r--r--。open(myfile, O_WRONLY | O_CREAT, 0644)O_WRONLY只写打开。O_CREAT如果文件不存在则创建。0644创建文件时的权限。返回值fd是文件描述符是一个非负整数。失败返回 -1。write(fd, msg, len)把msg指向的len个字节写入文件。返回值是实际写入的字节数。close(fd)关闭文件描述符释放资源。2.2 读文件hello.c#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#includeunistd.h#includestring.hintmain(){intfdopen(myfile,O_RDONLY);if(fd0){perror(open);return1;}constchar*msghello Linux!\n;charbuf[1024];ssize_ts;while(1){sread(fd,buf,strlen(msg));if(s0){buf[s]\0;// 读到s个字节在s下标位置补字符串结束符printf(%s,buf);}else{// s0 读到文件末尾s0 读取出错if(s0){perror(read);}break;}}close(fd);return0;}三、open 函数详解3.1 函数原型与头文件#includesys/types.h#includesys/stat.h#includefcntl.hintopen(constchar*pathname,intflags);intopen(constchar*pathname,intflags,mode_tmode);pathname要打开或创建的目标文件路径。flags打开方式由多个常量按位或|组成。mode创建文件时的权限仅在使用了O_CREAT时有效。返回值成功返回新的文件描述符非负整数失败返回 -1 并设置errno。3.2 flags 参数打开模式与附加选项必须指定且只能指定一个的访问模式O_RDONLY只读打开。O_WRONLY只写打开。O_RDWR读写打开。常用的附加选项O_CREAT若文件不存在则创建它。需要使用mode参数指明新文件的访问权限。O_APPEND追加写。每次写操作前文件偏移量自动移到文件末尾。O_TRUNC截断。如果文件已存在且以只写或读写方式打开则将其长度截断为 0清空。O_EXCL与O_CREAT一起使用如果文件已存在则open失败。用于确保创建的是新文件。示例intfdopen(log.txt,O_CREAT|O_WRONLY|O_TRUNC,0666);// 类似 wintfdopen(log.txt,O_CREAT|O_WRONLY|O_APPEND,0666);// 类似 aintfdopen(log.txt,O_RDONLY);// 类似 r3.3 mode 参数与 umaskmode是一个八进制数表示文件权限。比如0644表示所有者读、写6组读4其他读4实际创建的文件权限是mode ~umask。umask是进程的文件权限掩码通常默认为022。所以0644 ~022 0644。如果umask是002那么0644 ~002 0644不变如果umask是077那么0644 ~077 0600。可以用umask命令查看当前 shell 的掩码也可以用umask()函数在程序中设置。3.4 返回值文件描述符open成功时返回一个文件描述符它是一个小的非负整数。失败返回 -1。文件描述符是本文的核心概念后面会详细讲。3.5 语言层与系统调用的映射C 标准库的fopen有几种模式r只读打开对应O_RDONLY。w只写打开文件存在则清空不存在则创建对应O_WRONLY | O_CREAT | O_TRUNC。a追加写文件不存在则创建对应O_WRONLY | O_CREAT | O_APPEND。r读写打开对应O_RDWR。w读写打开清空或创建对应O_RDWR | O_CREAT | O_TRUNC。a读写追加对应O_RDWR | O_CREAT | O_APPEND。所以fopen内部就是根据这些字符串拼出flags然后调用open。3.6 文本写入 vs 二进制写入系统调用不关心你写的是文本还是二进制。write只认字节流你给它一个缓冲区它就把里面的字节原样写入文件。所谓的“文本模式”和“二进制模式”是 C 标准库在 Windows 等平台上的概念比如换行符转换。在 Linux 上文本和二进制没有区别系统只认字节。因此你可以用write写入整数、结构体、图片等任何数据只要把它们转成字节序列即可。四、系统调用与库函数4.1 库函数fopen, fclose, fread, fwrite这些是 C 标准库libc提供的函数我们称之为库函数。它们运行在用户态提供了更高级、更可移植的接口。4.2 系统调用open, close, read, write这些是操作系统内核提供的接口我们称之为系统调用。它们直接操作文件描述符是底层的。4.3 关系图fopen等库函数内部会调用open等系统调用。比如fwrite最终会调用write。4.4 为什么要有库函数可移植性不同操作系统的系统调用可能不同但 C 标准库的接口是统一的。程序用fopen可以在 Linux、Windows、macOS 上编译运行而open是 Linux 特有的。缓冲库函数自带用户态缓冲区减少系统调用次数提高效率。比如fwrite可能先写到缓冲区攒够了一定数量再调用write。格式化printf、fprintf等提供了格式化输出系统调用write只能写原始字节。五、文件描述符 fd5.1 fd 是什么通过open返回的fd是一个小的非负整数。比如 3、4、5……它到底是什么本质文件描述符是进程的文件描述符表中的数组下标。操作系统为了管理进程打开的文件在每个进程的 PCB进程控制块中维护了一个文件描述符表。这个表是一个指针数组每个元素指向一个打开文件对象struct file。数组的下标就是文件描述符。所以你拿着fd3操作系统就能通过files_struct找到对应的struct file从而找到文件的所有信息。5.2 默认打开的 0、1、2Linux 进程在启动时默认会打开三个文件描述符0标准输入stdin通常对应键盘。1标准输出stdout通常对应显示器。2标准错误stderr通常对应显示器。在 C 语言中stdin、stdout、stderr是FILE*类型的指针它们内部封装了对应的文件描述符。示例用 read/write 操作标准输入输出#includestdio.h#includeunistd.h#includestring.hintmain(){charbuf[1024];ssize_tsread(0,buf,sizeof(buf));// 从标准输入读if(s0){buf[s]0;write(1,buf,strlen(buf));// 写到标准输出write(2,buf,strlen(buf));// 写到标准错误}return0;}这段代码从键盘读取输入然后分别输出到显示器和标准错误。因为0、1、2已经默认打开所以可以直接使用。5.3 文件描述符的分配规则最小未使用当进程打开一个新文件时操作系统会在files_struct的数组中找到当前没有被使用的最小的下标作为新的文件描述符。验证代码#includestdio.h#includefcntl.h#includeunistd.hintmain(){intfdopen(myfile,O_RDONLY);if(fd0){perror(open);return1;}printf(fd: %d\n,fd);close(fd);return0;}因为 0、1、2 已经被占用所以新打开的fd通常是 3。如果关闭 0close(0);intfdopen(myfile,O_RDONLY);printf(fd: %d\n,fd);// 输出 0如果关闭 2close(2);intfdopen(myfile,O_RDONLY);printf(fd: %d\n,fd);// 输出 2这就验证了分配规则最小未使用。5.4 文件描述符的分配规则总结默认打开 0、1、2。新打开的 fd 是当前未被使用的最小下标。关闭一个 fd 后它的下标可以被重新分配。六、重定向6.1 输出重定向close(1) open看这段代码#includestdio.h#includefcntl.h#includeunistd.h#includestdlib.hintmain(){close(1);// 关闭标准输出intfdopen(myfile,O_WRONLY|O_CREAT,0644);if(fd0){perror(open);return1;}printf(fd: %d\n,fd);fflush(stdout);close(fd);exit(0);}现象本来应该输出到显示器的内容现在输出到了myfile文件中而且fd是 1。原因我们关闭了 1 号文件描述符标准输出然后open新文件时根据“最小未使用”原则新文件占用了 1 号。此时fd_array[1]指向的不再是显示器而是myfile。printf默认往stdout输出而stdout底层就是 1 号 fd所以内容写到了myfile。这就是输出重定向。常见的 shell 重定向、、就是基于这个原理。6.2 重定向的本质重定向的本质更改文件描述符表的指针指向。fd_array的下标如 1没有变但它指向的struct file对象变了。所有往这个 fd 写数据的行为都会写到新的文件。6.3 dup2 系统调用手动closeopen实现重定向有点麻烦而且容易出错。Linux 提供了dup2系统调用专门用来复制文件描述符。函数原型#includeunistd.hintdup2(intoldfd,intnewfd);功能让newfd成为oldfd的副本。如果newfd已经被打开会先自动关闭它。成功返回新的描述符即newfd失败返回 -1。官方描述dup2()makesnewfdbe the copy ofoldfd, closingnewfdfirst if necessary.示例输出重定向#includestdio.h#includeunistd.h#includefcntl.hintmain(){intfdopen(./log,O_CREAT|O_RDWR,0644);if(fd0){perror(open);return1;}// close(1); // dup2 会自动关闭 newfd所以这行可以省略dup2(fd,1);// 让 1 号 fd 指向 fd 所指向的文件for(;;){charbuf[1024]{0};ssize_tread_sizeread(0,buf,sizeof(buf)-1);if(read_size0){perror(read);break;}printf(%s,buf);fflush(stdout);}return0;}过程fd是新打开文件的描述符比如 3。dup2(fd, 1)让 1 号描述符指向fd指向的struct file。原本 1 号指向显示器现在指向log文件。显示器的struct file引用计数减 1如果减到 0 就关闭。之后printf往stdout即 1 号写就写到了log文件。注意dup2会自动关闭newfd所以不需要手动close(1)。6.4 输入重定向dup2(fd, 0)类似地可以重定向标准输入intfdopen(input.txt,O_RDONLY);dup2(fd,0);// 让 0 号 fd 指向 input.txtclose(fd);// 关闭多余的 fdcharbuffer[64];while(fgets(buffer,sizeof(buffer),stdin){printf(%s,buffer);}这样fgets从stdin读取时实际是从input.txt读取。七、深入内核要真正理解文件描述符必须看看内核中的数据结构。我们以 Linux 内核为例版本不同细节可能略有差异但思想一致。7.1 进程控制块 task_struct每个进程在内核中都有一个task_struct结构体它包含了进程的所有信息。其中有一个成员structfiles_struct*files;它指向该进程的文件描述符表。7.2 files_struct 与 fd_arrayfiles_struct是文件描述符表内部最重要的部分是一个指针数组structfile*fd_array[NR_OPEN_DEFAULT];这个数组的每个元素都是一个指向struct file的指针。数组的下标就是文件描述符。7.3 struct file打开的文件对象struct file代表一个已经打开的文件。注意它不代表磁盘上的文件本身而是代表“进程与文件的一次打开会话”。它包含int ref_cnt;引用计数。有多少个文件描述符指向它。文件读写位置偏移量。文件操作函数指针如read、write的具体实现。指向inode的指针磁盘文件的元信息。文件缓冲区内核态。7.4 inode磁盘文件的元信息inode是磁盘上文件的元信息包括文件大小、权限、创建时间、数据块位置等。每个文件在磁盘上都有一个唯一的inode。struct file通过指针关联到inode从而找到文件的实际数据。7.5 一个进程可以打开多个文件一个文件可以被多个进程打开一个进程可以打开多个文件files_struct的数组中有多个struct file指针。一个文件可以被多个进程打开每个进程的open会创建不同的struct file对象但它们都指向同一个inode。struct file中的ref_cnt记录有多少个这样的对象。同一个进程也可以多次打开同一个文件每次open都会创建新的struct file返回不同的 fd。7.6 引用计数 ref_cnt当dup2或fork等操作导致多个 fd 指向同一个struct file时ref_cnt增加。当close一个 fd 时ref_cnt减少。减到 0 时内核才会释放这个struct file对象。图示进程 A task_struct files (files_struct) fd_array[0] -- struct file (键盘) fd_array[1] -- struct file (显示器) fd_array[2] -- struct file (显示器) fd_array[3] -- struct file (myfile) -- inode (myfile)当close(1)后fd_array[1]置空。再open新文件新文件占用 1 号fd_array[1]指向新的struct file。结语今天的内容到这里就结束了希望你能有所收获~干货整理到手抖觉得有用的话赏个三连回回血__(:ᗤ」ㄥ)_ _
返回列表