
前言基本原理和问题聚焦文件 内容 属性文件分为打开的文件和没打开的文件打开的文件像“fopen“这样的代码也是存储在可执行程序中的所以打开文件这个操作一定是进程做的进程的操作对象是存储在内存中的代码和数据所以文件要被放入内存才能被进程打开而操作系统要对内存做管理就要按照“先描述再组织“的原则对被打开的文件进行管理所以研究已打开文件本质是研究进程和文件的关系。没打开的文件这些文件放在磁盘上且没被打开的文件非常多。需要关注文件如何被分门别类的放置好从而快速找到文件。回顾C文件接口fopen参数filename需指定路径文件路径mode是选择打开模式fwrite注如果文件名不存在且不指定地址会自动创建且路径和当前进程路径一致。如果进程运行时将进程更改到其他路径下chdir文件的创建路径就随之更改了注写入大小不需要把\0考虑进去也就是strlen后不用1因为\0是C语言的规定和文件没有关系代码示例下面演示fopen的“w”和“a”的使用#includestdlib.h #includestdio.h #include errno.h #include string.h int main(){ //创建并写入 char s[12]Hello File!; FILE*pfile; pfilefopen(test.txt,w); if(pfileNULL){ printf(文件打开失败错误码%d\n,errno); printf(错误信息%s\n,strerror(errno)); return 0; } fwrite(s,sizeof(char),strlen(s),pfile); fclose(pfile); //追加 char ss[13]Hello Linux!; pfilefopen(test.txt,a); if(pfileNULL){ printf(文件打开失败错误码%d\n,errno); printf(错误信息%s\n,strerror(errno)); return 0; } fwrite(ss,sizeof(char),strlen(ss),pfile); fclose(pfile); return 0; }运行结果如下文件系统调用系统不会让用户直接修改操作系统实际上C的文件接口是对系统调用接口的封装open打开方式flagsflags控制打开方式用宏定义按位或组合O_RDONLY 只读 O_WRONLY 只写 O_RDWR 读写 O_CREAT 文件不存在则创建需要指定 mode 权限 O_TRUNC 打开时清空文件内容 O_APPEND 追加写入每次写自动定位到文件末尾权限参数mode0666 → rw-rw-rw- 0644 → rw-r--r-- 0755 → rwxr-xr-xmode控制新建文件时的权限只在O_CREA方式下生效。各个语言的打开文件函数都是对open的封装写、关闭文件同理//C库函数 FILE *fp fopen(log.txt, a); //底层调用 int fd open(log.txt, O_WRONLY|O_CREAT|O_APPEND, 0666);//C库函数 FILE *fp fopen(log.txt, w); //底层调用 int fd open(log.txt, O_WRONLY|O_CREAT|O_TRUNC, 0666);closeint close(int fd);关闭文件描述符(fd释放 fd_array 中对应的槽位。对应文件的引用计数 -1降到 0 后可以释放资源,fclose是对close的封装writessize_t write(int fd, const void *buf, size_t count); fd文件描述符通过它在 fd_array 中找到对应的 struct file buf用户态缓冲区的起始地址 count写入字节数 返回值实际写入的字节数fcwrite是对write的封装访问文件的本质系统要管理被打开的文件根据“先描述再组织”的原则为文件创建文件控制块FCB并采用某种数据结构对所有FCB进行管理。而每个进程控制块里有一个为打开文件专门创立的struct对象files_struct内有一指针数组*fd_array存放指向已打开文件的控制块的地址的指针。综上当文件第一次被进程打开open函数被调用一方面操作系统创建了FCB另一方面这个FCB地址被存储到打开该文件进程fd_array空余的地方最后返回存储位置下标fd这就是文件描述符write接口要写的文件的位置就是通过fd来确定的。此外每个文件不是只能和单个进程挂钩多个进程可以打开同一个文件。而在 fork 的场景下子进程会继承父进程的 fd_array父子进程的 fd 指向同一个struct file 对象引用计数加 1。只有当所有引用该 struct file 的 fd 都关闭后计数归 0该对象才会被销毁。现已知open的返回的文件描述符为int类型而fopen是对open的封装那C接口的返回值FILE是什么为什么能接收文件描述符FILE是C库封装的一个结构体里面包含了文件描述符。重定向如果让某个文件描述符指向另一struct file就叫做重定向比如把stdoutfd 1从屏幕重定向到log.txt对应的其实就是我们在shell里面输入的命令echo log.txt重定向如何实现系统重定向的实现依赖于dup函数dup2是dup系列函数之一下面代码示例将文件log.txt重定向到显示屏后打印文字不再向显示屏输出而是向文件中输出这就叫做输出重定向。#includestdlib.h #includestdio.h #include errno.h #include string.h #include sys/types.h #include sys/stat.h #include fcntl.h #includeunistd.h int main(){ //实现重定向 char str[]hello linux1!!!\n; int oldfd open(log.txt, O_WRONLY|O_CREAT|O_TRUNC, 0666);//打开log.txt后oldfd已经加载到file_array中 //write(oldfd,str,strlen(str)); dup2(oldfd,1);//重定向:让原本屏幕的fd1指向log.txt的fd所指向的文件 printf(%s,str); return 0; }可以看到echo命令原本要将字符串打印到显示屏现在输出到文件中底层就是输出重定向。下面代码演示将程序打印的标准输出和标准错误通过重定向输出到两份文件中int main() { // 标准输出stdoutfd 1—— 会被 1normal.log 捕获 printf(这是一条正常日志信息\n); printf(程序正在运行...\n); printf(程序运行结束\n); // 标准错误stderrfd 2—— 会被 2err.log 捕获 fprintf(stderr, 这是一条错误信息\n); fprintf(stderr, 发生了一个警告\n); return 0; }shell如何处理重定向指令的呢当检测到命令行中有、、这些符号时在子进程中按前文所述对应方式重定向显示屏或键盘然后符号前面的指令正常执行因为已经重定向直接输出即可。可参考下面的shell模拟实现代码shell的简单模拟实现代码中有如下片段void normalcommand() { check_redirect();//判断是否使用重定向,并将重定向符号前后分割开 pid_t ret fork(); if (ret 0) { if(state1){//用状态表示发生重定向与否 int fd open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0666); dup2(fd,1);//重定向显示器 close(fd); } else if(state2){ int fd open(filename, O_WRONLY|O_CREAT|O_APPEND, 0666); dup2(fd,1); close(fd); } execvp(argv[0], argv);//这里不用‘p’,默认把环境变量传过去就好了 exit(EXIT_CODE); } else if (ret 0){ //... } }可以看到子进程发生重定向后程序替换并不会影响重定向因为进程替换掉的是数据和代码而打开文件后files_struct属于PCB不会发生替换。如何理解操作系统一切皆文件?学习了重定向后再来理解一下操作系统中一切皆文件:计算机由CPU、内存和各种I/O设备磁盘、显示器、网卡等组成每个设备在初始化的时候有自己的驱动方式存储在驱动文件中每个文件都有一个索引节点inode其中关联着一张操作方法表struct file_operations当进程打开文件时内核创建一个struct file对象并将其指针存入进程的files_struct-fd_array中进程通过文件描述符找到file对象再顺着其中的file_operations指针最终调用到具体的设备操作方法。同一个调用接口不同的底层实现这就是一切皆文件的本质int fd1 open(log.txt, O_WRONLY); // 写磁盘 write(fd1, buf, 100); int fd2 open(/dev/monitor, O_WRONLY); // 写显示器 write(fd2, buf, 100); int fd3 open(/dev/keyboard, O_RDONLY); // 读键盘 read(fd3, buf, 100);缓冲区缓冲区是什么在什么场景发挥作用语言层会提供一个用户级缓冲区这个缓冲区在FILE结构体中被定义。语言层的写入fwriteprintf读取freadscanf先暂存到缓冲区到一定时机缓冲区才被刷新所谓的刷新其实就是对write的封装这个时机取决于缓冲区采用的刷新策略。刷新后内容才实际被写入。缓冲区刷新策略无缓冲:输入后立即刷新如printf后直接调用ffush新行缓冲直到遇到\n才刷新显示器采用此方式刷新全缓冲缓冲区写满了才刷新文件写入采用另外进程退出时自动会对缓冲区刷新一次如果在进程退出前关闭文件相当于销毁了FILE中以及里面的缓冲区缓冲区就无法刷新例如代码1因提前将stdout文件关闭“Hello C”就没有打印出来。缓冲区具体在哪里?FILE结构体里包含对应文件的缓冲区字段和维护信息注意:FILE是C语言额外创建的所以第一次打开文件用FILE指针接收返回值后此后要对该文件进行操作都必须带上这个FILE指针不然无法找到缓冲区也就是说每个文件都有自己的缓冲区打开了10个文件就会产生10个缓冲区。为什么要有这个缓冲区一次write()系统调用的开销是很大的不如先把字符存入到缓冲区中积攒到一定程度再进行系统调用从而提高了效率C语言创建了FILE这个FILE属于用户还是操作系统FILE的创建相当于在语言层给我们开了一个存放FILE的空间这个不归操作系统管理而是用户因为它的创建销毁缓冲区的更新都是依托语言层的代码实现的。子进程赋值缓冲区缓冲区应用fork()打印两次的问题现象打印后创建子进程并通过重定向将会打印两次但按理子进程应该执行fork()后的代码所以只会打印一次。int main(){ char str[]看看这句话会打印几次; printf(%s\n,str); fork(); return 0; }原因因为重定向到文件从输出到屏幕改为输出到文件这时采用全缓冲方式所以即使带\n也不会刷新而缓冲区没写满只能等进程退出时才刷新进程退出前创建了子进程对代码和数据复制了一份父进程想退出的时候要刷新缓冲区也算是对数据malloc出来的缓冲区的写入此时发生写时拷贝物理内存就会有两份缓冲区了原本父子共享一份父子进程退出时都会刷新自己的缓冲区所以对文件做了两次write操作下面代码演语言层如何实现缓冲区简易缓冲区模拟实现文件系统前面记录的是有关“被打开文件”的知识而开篇提过针对没被打开的文件我们关注的是文件如何被分门别类的放置好从而快速找到文件。认识有关硬件以磁盘为例在磁盘上存储文件存储内容属性,linux下文件属性和内容是分开存储的。磁盘是电脑上唯一一个机械设备不是现在的电脑同时也是一个外设计算机叫电子计算机而磁盘是物理性的速度肯定不如电流磁盘部件简介盘片数量不定两面都是光滑的依赖主轴马达高速旋转盘面上存储的是数据磁头一面一个作为整体一起运动运转后来回摆动但和盘面不接触读写数据的原理写数据原理电流方向经磁头产生磁场改变盘片磁性粒子的极性来存储 0/1存数据原理内存是掉电易失介质而磁盘是永久性存储介质靠的不完全是电而是电磁特性电让磁极更改销毁数据的方法消磁高温 或者专门设计的擦除磁盘的接口磁盘的存储构成磁道以盘片中心为圆心向外扩散的每个同心圆环就是一个磁道扇区磁道被分成很多个扇区传统扇区为512字节现代硬盘已普遍采用4K字节扇区多个盘面的一个磁道构成柱面所以磁头的左右摆动就是在定位磁道和柱面的过程所以磁盘可被定义为由无数个扇区构成的存储介质如果要存储数据就要知道如何定位一个扇区。要知道是哪个磁头再知道是哪个磁道最后才能定位到某个扇区。上述过程根据各部件编号实现这就叫做CHS寻址方式。磁盘效率磁盘是机械部件机械运动越少效率越高除了转轴、磁头的速度等数据如何存放也是一个影响因素因此在软件设计上应该将相关联的数据尽量放在一起从而提高效率。磁盘的逻辑存储磁盘在物理上是圆形的但逻辑上是线性的。也就是按顺序从各个面到各个道再到各个扇区展开来。因此任意一个扇区都有自己的逻辑块地址LBA,LogicalBlockAddressing磁盘的寄存器不仅cpu有寄存器其他设备(包括磁盘)也有控制寄存器现在是要读还是写控制IO方向 数据寄存器暂存准备写进来/出去的数据 地址寄存器填入LBA 状态寄存器准备写出是否就绪写入是否成功文件系统正如虚拟地址空间一般磁盘同样被操作系统分成多个区并为每个分区定义结构体struct partion上面包含了起始地址和终止地址在每个分区的第一个扇区存有开机信息后续的扇区就被分为一个个block group进一步分治文件是可以跨组的而各个blockgroup里面再一次被分成多个区域接下来对这些区域做说明Datablocks用于存文件内容的区域以块的形式呈现就是不按照磁盘的基本单位扇区而是自己划分的块常见大小为4kb这就是文件系统的基本单元块大小确定所以每个块也有编号。注意文件并不是按顺序占据Datablocks的而是碎片化分布的所以一份文件如果只有1kb也会占据一个4kb的块。但通常文件相对于一个块来说都很大而每个文件所占据的最后一个块才可能产生浪费几乎可以忽略不计。inode table存放多个inodeinode用于存放文件属性一般为128字节每个inode也有自己的编号这个编号只在当前分区有效文件属性里并不包含文件名字在linux里用inode编号标识文件可data block是一整块的inode table里面的各个inode怎么和对应数据关联起来的呢inode基本构成如下#define NUM 15 struct inode { //inode number(编号) //文件类型 //权限 //引用计数 //拥有者 //所属组 //ACM时间 int blocks[NUM]; };每个文件被创建时开头自带inode编号所以会去inode block里面找到自己的inode再通过blocks[NUM]判断自己的大小从而确定了文件的开头和结尾。blocks数组存有直接索引、二级和三级索引以类似指数增长的方式指向一份文件各个不同的块但能保证文件所有块都被找到blockbitmap用等同于文件占据块的数量的比特位表示哪些块被使用比特位的位置和块编号映射起来而比特位的内容0/1表示块的使用与否所以删文件的时候不没必要把块清空把状态改一下后面覆盖就好了同理如果文件被删了还没被覆盖知道编号就可以马上恢复inodeBitmap同blockbitmapinodeBitmap就是有关inode的位图Super Block文件系统的全局配置卡Super Block 的内容不是全部列关键的 s_inodes_count整个文件系统共有多少个 inode s_blocks_count整个文件系统共有多少个 block s_inodes_per_group每个 Group 里有多少个 inode s_blocks_per_group每个 Group 里有多少个 block s_free_inodes_count目前空闲的 inode 总数 s_free_blocks_count目前空闲的 block 总数 s_magic魔数标识这是什么文件系统如 ext4 0xEF53 s_state文件系统状态是否正常挂载 s_log_block_sizeblock 大小4KB 等作用这个区域存放了文件系统的配置和现状有了它系统才知道怎么解读后续的所有结构存放位置:Group 0 有主份特定组1, 3, 5, 7, ...的幂次方有备份Group Descriptor Table(GDT)作用存放当前分区每个Group的管理结构bitmap、inode table的位置。注意大部分 Group 没有 Super Block 和 GDT,只有特定的 Group约十几到几十个有备份Group 0一定有主份总结Super Block管理整个文件系统的全局配置 Group Descriptor Table管理每个 Group 的位置和空闲情况 Block Bitmap Inode Bitmap管理每个 Group 内部哪些资源被占用 Inode Table Data Block是被管理的最终资源如何理解目录系统如何通过文件名拿到inode编号的目录也是文件 也有inode,也有属性它的数据块放的是该目录下文件名和文件对应inonde的映射关系所以如果用户给文件名或目录名系统可以相应地找到该文件或目录的inode而根目录的inode一开始已经确定所以系统从根目录开始一层层往下找因此找文件需要传路径。此外每次进入到新的目录都会改变shell环境变量pwd。再谈目录权限对目录有x权限可以打开该目录的data block并定位到其中的某个文件如果知道该文件名的话拿到inode然后再视情况对该文件进行操作。对目录有r权限决定了可以看到该目录下的文件名字,但仅限于看到不能通过该目录的路径进一步访问文件对目录具有w权限可以在目录中创建和删除文件也就是在对应的data block中建立或删除文件名和文件的映射关系综合上面的文件系统知识删除一份文件a.txt流程如下通过目录data block 找到 a.txt得到inode number为305 目录里存放的读 inode 305找到blocks[] 数组 得到block 1, block 500, block 99999, block 3, block 8000Block Bitmap把 1, 500, 99999, 3, 8000... 对应的比特位置0Inode Bitmap把 305 对应的比特位置0删目录项a.txt → 305 这条记录删掉软硬链接软链接定义软链接是一个独立的文件拥有自己的inode其数据块存放的是指向文件的路径添加软链接ln [-s] 源文件路径最好用绝对路径 [目标名]操作演示在当前目录创建子目录linux_file下log.txt的软链接名为soft_link发现其内容和源文件一致软链接应用场景将常用目录/文件放到便捷路径省去反复输入长路径的麻烦硬链接定义硬链接是指向同一inode的新文件名它与原文件共享同一份数据没有主从之分。硬链接数代表有几个硬链接指向同一个个可执行程序添加硬链接ln 源文件路径最好用绝对路径 [目标名]操作演示为当前目录子目录linux_file下log.txt文件创建硬链接使用ls -l可看到此时log.txt硬链接数为2硬链接应用场景维持目录结构防止文件误删硬链接数变为0文件才算真正删除维持目录结构目录自带两个硬链接:.和..分别指向当前目录和上级目录没有它们文件系统的目录树就只能往下走不能往回走。所以当前目录创建出来的时候上级目录的硬链接数。注意不允许为目录建立硬链接假如目录拥有硬链接递归遍历文件的过程就可能产生环路。从本质上看根据路径访问文件就是去 block data 里找到下一个目录的 inode而 find 这类工具进入一个目录后会把所有子目录都探索一遍如果存在硬链接让某个子目录指向祖先目录工具每次进入后又会展开所有子目录永远无法停止。而..这个硬链接即使存在工具也不会主动进入所以只允许.和..这两个目录硬链接存在。内存管理简述内存管理有一套完善的体系但后续不会深入学习这里做简述操作系统用和页数量相当大小的数组表示内存被占用与否也就是位图所以数组下标就是页号每个页有自己的结构体记录了自身的信息struct page { unsigned long flags; // 状态标志是否被使用 int count; // 引用计数有进程在用这个页 // ... lru 链表指针 ... };这里的引用计数就是父子进程共享页面的机制。fork时不复制内存父子共用同一个页计数count1。谁要写谁才复制一份写时复制。LRU机制Least Recently Used当内存不足时要把一些页腾出来给新数据最近最少使用的页先被踢出去。文件与内存的关联件struct file除inode指针外里面还有一个page_tree指针这个指针就指向了文件的页缓存page_tree 是一棵树用来管理该文件有哪些页在内存中文件可能很大但不是所有页都被加载到了内存里。以偏移量作为 key通过基数树查询偏移量 X 处的数据在不在内存中。基数树就是用偏移量做索引的哈希表的树形版本可以快速定位文件的哪一部分已经在内存中了。所以应用层读文件调用read的时候给一个偏移量是为了查 page_tree偏移量对应的页是否在内存中。如果命中就直接从页缓存拿数据,如果没命中说明还没加载到内存中从磁盘读read参数大小的对应的数据到page上然后再返回数据。命中与否最后都是再从内核缓冲页拷贝到用户缓冲区再调用write()真正输出到屏幕上。写文件的时候调用write的时候同样在内核中先找到或加载对应的 page 把数据写入 page标记该页为脏页dirty,然后write()返回应用层到此结束而后台由内核择机把脏页刷回磁盘。所以在脏页里还没刷回磁盘时断电可能丢数据。