目录
文件路径及shell的完善
文件基本操作及其C语言的文件操作接口
stdin & stdout & stderr
总结文件的基本操作:
系统文件I/O
接口介绍:
文件描述符fd
文件描述符的分配规则
重定向
使用 dup2 系统调用
FILE
文件操作本质
文件缓冲区
文件系统
inode
软硬链接
acm 3个文件时间
动态库和静态库
生成静态库
生成动态库
动静态库的加载
在上一次的进程控制中,我们模拟了一个shell,但是我们会发现下面这个问题:
我们可以通过ls /proc/进程编号 -al来查看进程的当前路径。
为什么这里不能退出到上级路径呢?
因为我们使用的是子进程帮我们执行,子进程改变了路径并不影响父进程,当子进程退出的时候,我们使用的还是父进程的路径,这也就是为什么我们使用pwd看的时候路径是没有改变的。
那么如果解决呢?还有其他指令也会这样吗?
那肯定是有一些指令也是这样的,而且导致的原因都是一样的。既然我们想要改变的是父进程的路径,那么我们肯定要在创建子进程之前完成,并退出这一次循环。这种操作叫内建命令。echo也是一样的,我们要看到的是父进程的返回。下面看看如何完善:
在解决cd的问题时,我们应该要使用到这个系统调用接口:chdir,可以帮助我们改变路径
看看代码:
结果:
结果正如我们所料的那样,那么echo命令其实也是大致一样的操作而已:
下面直接看代码和结果:
结果:
这个shell虽然还有点bug,但是基本满足需求了。
在C语言中,我们学习了文件的简单操作,就是通常的三步走:打开文件fopen,对文件进行写入或者读出,关闭文件fclose。
下面我们通过代码来回忆一下这些操作:
文件的写操作:
文件的读操作:
结果:
stdin & stdout & stderr
C 默认会打开三个输入输出流,分别是 stdin, stdout, stderr仔细观察发现,这三个流的类型都是 FILE*, fopen 返回值类型,文件指针
r,w,a: r+(读写,不存在出错),w+(读写, 不存在创建), a(append, 追加), a+()
提一下文件的权限问题,之前在讲权限的时候已经有详细的说明,这里简单回忆:
在我们创建文件的时候可以设置文件的权限,而这个文件的权限就等于文件初始值&(~umask)
头文件:#include #include #include 使用方式:int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);pathname: 要打开或创建的目标文件flags: 打开文件时, 可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。下面通过代码的方式看看其中的原理:// 每一个宏,对应的数值,只有一个比特位是1,彼此位置不重叠 #define ONE (1<<0) #define TWO (1<<1) #define THREE (1<<2) #define FOUR (1<<3) void show(int flags) { if(flags & ONE) printf("one\n"); if(flags & TWO) printf("two\n"); if(flags & THREE) printf("three\n"); if(flags & FOUR) printf("four\n"); } int main() { show(ONE); printf("-----------------------\n"); show(TWO); printf("-----------------------\n"); show(ONE | TWO); printf("-----------------------\n"); show(ONE | TWO | THREE); printf("-----------------------\n"); show(ONE | TWO | THREE | FOUR); printf("-----------------------\n"); return 0; }
看看结果:
参数:O_RDONLY: 只读打开O_WRONLY: 只写打开O_RDWR : 读,写打开这三个常量,必须指定一个且只能指定一个O_CREAT : 若文件不存在,则创建它。需要使用 mode 选项,来指明新文件的访问权限O_APPEND: 追加写
写文件:
结果:
但是如果我们写重新一些数据:
结果:
我们可以看到这里出现了很大的不同的结果,结果多了一些东西,为什么呢?
因为这是系统,在语言层次上,语言库中的接口是重新写入的话就会把数据清空,但是在系统层面就不会,系统是以二进制的方式写入,它不会关心里面有没有数据,当然如果你调用系统接口(也就是在刚刚的打开文件的步骤加多一个选项TRUNC的选项就可以先清空再写入。)
为什么会这样呢?原因很简单,那就是语言是在系统之上的。在之前的进程概念的时候就已经很详细的说明了这个观点了
所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发
读文件:
我们刚刚已经接触到了文件描述符了,看下这段代码:
我们可以看到结果是3,那为什么是3呢?0,1,2是什么呢?
在上面已经提到了stdin & stdout & stderr,这3个流,
输入一次,输出两次:
文件描述符就是从0开始的小整数 。当我们打开文件时, 操作系统在内存中要创建相应的数据结构来描述目标文件(操作系统要管理文件,就要先描述,后组织) 。于是就有了fifile 结构体。表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*fifiles, 指向一张表 fifiles_struct, 该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以, 本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
所谓的分配规则就是低处分配,就好像刚刚我们打印文件的fd时发现它等于3,如果我们观点0或者2那么相应的fd就会变成0或者2,看看结果:
我们可以看到确实如我们所见结果为0;
如果close2呢?结果如下:
结果验证了正如刚刚所说的,这确实是一个数组,然后从小到大开始分配。
刚刚那个代码,如果close的是1呢?也就是标准输出,结果如何呢?
如果我们把代码改成这样呢?
结果:
我们就会发现原来我们想在1号标准输出输出的,结果最后输出到了我们创建的文件中了。这就完成了一次重定向。
那重定向的本质是什么呢?
重定向的本质是上层的fd不变,在内核中改变文件指针的指向,下面使用画图的方式去解释:
下面我们来讨论一下这两个参数是怎么回事,
如果我们要调用dup2这个接口:我们应该是这样使用dup2(fd,1)还是dup2(1,fd)呢?也就是哪个是新的哪个是旧的?
从上面的解释我们可以知道我们应该是fd->1,也就是应该使用第一种方式去调用。
看看大致的使用方法:输出重定向:
输入结果没问题,但是顺序为什么是乱的呢,这个后面会提到。
输入重定向:
我们把fd重定向到0号,也就是输入重定向,这样的话我们就可以不用自己输入,从而读取文件中的信息,按行来读,知道文件信息全部读完。
学习了重定向之后,我们可以把我们之前实现的shell再加一个重定向的功能:
首先重定向肯定是由子进程来完成的,因为进程的独立性所有对父进程没有影响,但是父进程在进行指令分析的时候要分析出其中是否要重定向,是输入重定向>还是输出重定向<或者是追加重定向>>。
我们可以专门写一个函数来判断是否要求重定向,然后通过一个变量来记录重定向,最后通过子进程来实现重定向。
下面看代码:
子进程执行:
全部代码:
我们都知道文件本身存放在磁盘中,如果我们要打开并操作文件,那么我们就要加载到内存,操作系统就要对其进行管理,管理的本质就是先使用结构进行描述,然后采用特定的数据结构进行组织。这里我们先谈论一下打开的文件,然后再来谈论文件系统。
首先我们要认识到文件操作的本质就是进程和被打开文件的关系。
我们知道OS要对打开的文件进行管理就要先组织再描述,我们就可以使用struct file的结构体来描述它。同时我们使用子进程帮我们完成一些事情的时候,我们是要复制父进程的代码和数据的。从而实现OS对子进程的管理。那么文件是否需要复制呢?
答案是不需要:
其实所谓的关闭文件并没有真的关闭文件,只是把指向发生了改变而已:
下面我们来理解一些在当时讲指令的时候提到的Linux下一切皆文件的思想。
我们该怎么理解这种思想呢?
首先我们知道硬件是由驱动程序进行管理的,然后我们的OS又会对驱动程序进行管理,我们有各种的struct file的结构体去描述组织,而struct file中又有函数指针,指向驱动程序的驱动方法,所以站在上层来看,所以的设备和文件都是struct file,这也就是为什么说Linux下一切皆文件了。
首先我们要解释一下文件缓冲区是什么:
我们知道计算机为了追求效率,会尽量减少和硬件的IO过程因为这样会使效率大大降低。
所以就有了文件的缓存区,对于一般的文件来说都是等到缓存区全部放满然后再一次性的加载到磁盘中,这样的效率是最高的。(全缓冲)
有一个特殊的硬件需要行缓存,那就是显示器,因为显示器是给用户提供服务的,所以行缓冲是最适合的(行缓冲)
还有一种几乎见不到的就是立即刷新(无缓冲)
当然还有两种特殊情况:
1.进程退出的时候会刷新缓冲区。2.强制刷新
下面这段代码就和缓冲区有关:
在没有创建子进程的时候,这个代码这样运行是正常的,但是创建子进程之后就会有这种结果:
通过结果我们可以知道这个文件缓冲区一定在OS之后,因为这里的write只打印了一次,其他的C语言的接口都打印了两次。
那怎么解释呢:1.当我们我们进行了>, 写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲,之前的3条c显示函数,虽然带了\n,但是不足以stdout缓冲区写满!数据并没有被刷新!!!
2.执行fork的时候,stdout属于父进程,创建子进程时, 紧接着就是进程退出!谁先退出,一定要进行缓冲区刷新,就会写时拷贝!!数据最终会显示两份,也就看到了C接口是两份。
3.. write为什么没有呢?上面的过程都和wirte无关,wirte没有FILE,而用的是fd,就没有C提供的缓冲区
总结:也就是我们C语言使用的FILE其实是一个结构体:里面封装了缓冲区,这些操作都是在语言层面的,对于操作系统来说并没有FILE,就自然没有这个缓冲区了,而进程退出是会刷新缓冲区的同时要进行不同的操作,所以会发生写时拷贝。最终有两份代码。
下面就让我们模拟实现C语言中的fopen,fclose,fwrite来进一步加深理解缓冲区的概念:
首先时fopen:
实现步骤:1.根据传进来的打开方式进行flags的选择。2.根据不同的flags来进行不同的打开
fclose:
1.先刷新缓冲区 2.调用系统接口关闭文件
fwrite:
我们要意识到fwrite本质其实是拷贝,因为我们需要把提供的字符串拷贝到文件中
1.拷贝字符串到文件中 2.判断是否需要刷新缓冲区
下面是代码实现:
我们先看看lib.h:
lib.c:
最后就是我们使用main.c来操作文件了:
当然不可以忘记的是makefile的书写:一个目标文件怎么对应两个源文件:
最后我们可以看到我们想要的结果:
最后画图再理解:
前面我们都在谈论打开文件与进程的关系,我们知道磁盘中存在很多文件,那么这些文件该怎么被 管理呢?答案是静态管理,方便我们随时使用。而这些文件就构成了文件系统。
首先说说磁盘:我们都知道磁盘是一个硬件,磁盘IO过程是很慢的(这里的慢是相对的,相对于cpu,内存而言)
我们来看看磁盘的存储结构:
磁盘的存储是以扇区(512字节)为单位的。
这里所谓的柱面就相当于磁道,也就是这里的一圈一圈的东西。
那么存储的方式是怎么样的,以及我们应该如何获得磁盘中的信息呢?
我们应该先定位到哪一个磁道/柱面(track,cylinder),然后定位磁头(head),最后确定扇区(sector)
这就是硬件的基本定位法:CHS定位法
我们再来谈谈磁盘的逻辑结构:
我们可以把圆形结构抽象成线性结构:(和使用磁带的方式很像)
如果是线性结构,那么就可以想象成数组了。那么OS对磁盘的管理也就是变成了对数组的管理了.
那么如何寻址呢?
我们可以让对应的数组的下标和CHS3个物理量的关系就可以得到地址,我们称这种地址就叫做LBA地址(就是逻辑地址)。
那操作系统为什么不直接使用CHS定位法来直接对磁盘进行管理呢?
有两个原因:1.便于管理 2.不让代码和硬件有强耦合。这是为了统一进行管理。
刚刚提到磁盘的存储是以扇区为单位的。但是512字节显然太小了。所以我们在寻址的时候一次性访问4KB,这就是局部性原理(这个和我们当时讲vector和list的命中率那里对应上)
对磁盘管理大化小的分治思想:
inode
在上面不断分区中,最后得到下面一样的分组:
那么这些分别对应什么意思呢?
Block Group : ext2 文件系统会根据分区的大小划分为数个 Block Group 。而每个 Block Group 都有着相同的结构组成。政府管理各区的例子超级块( Super Block ):存放文件系统本身的结构信息。记录的信息主要有: bolck 和 inode 的总量,未使用的block 和 inode 的数量,一个 block 和 inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。 Super Block的信息被破坏,可以说整个文件系统结构就被破坏了(所以有的时候使用电脑时会让你恢复文件,就是将其他的分区的Super Block的内容复制到当前的分区)GDT , Group Descriptor Table :块组描述符,描述块组属性信息。块位图 (Block Bitmap ): Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用inode位图 ( inode Bitmap ):每个 bit 表示一个 inode 是否空闲可用。i节点表 : 存放文件属性 如 文件大小,所有者,最近修改时间等数据区 : 存放文件内容文件 = 文件的内容 + 属性;那怎么分别存放呢?
文件的内容和属性是分开存储的。分别存储在data blocks和inode Table中。
那么我们创建一个文件的过程又是怎么样的呢?
1. 存储属性内核先找到一个空闲的 i 节点(也是就是inode )。内核把文件信息记录到其中。2. 存储数据假设该文件需要存储在三个磁盘块,内核找到了三个空闲块: 300,500 , 800 。将内核缓冲区的第一块数据复制到300 ,下一块复制到 500 ,以此类推。3. 记录分配情况文件内容按顺序 300,500,800 存放。内核在 inode 上的磁盘分布区记录了上述块列表。4. 添加文件名到目录 (从而建立inode和文件名的映射关系)实际上用户根本不需要知道inode,用户只需要正常的创建文件即可,后面的事情都是交给操作系统完成。最后如果你不小心误删了该文件,那么你首先要做的一件事情就是不做任何事,然后想办法恢复这个inode,因为你一旦做了其他事情就有可能导致其中的数据被覆盖从而导致丢失,这也就是为什么我们在下载的时候非常慢,但是在删除的时候很快。(因为下载的时候是真实的把数据拷贝到数据区的,但是删除并不需要,只需要把对应的inode和文件的映射关系删除,然后把inode记录数据是否存在的bit位从1置为0即可实现删除。
其实我们讲解了这么久的文件系统也是为了后面的软硬链接做准备。
通过ls -li我们可以看到:
对于硬链接来说他们的inode的是相同的。对于软链接来说inode则是不同。
这相当于有没有创建新的文件。
那个数字又是什么意思呢?
数字其实是硬链接数。
下面通过画图来讲解:
其实所谓的软链接就相当于我们使用的快捷方式,不会因为文件的大小而改变自身的大小,而硬链接就是文件真实的大小,就算我们把原来的文件删除,但是我们依旧可以看到硬链接文件里面和它一样的数据,这就是上面说的引用计数原因。
这里我们给文件加上内容,发现硬链接确实和原来的文件是一样大小,内容也一样。
如果删除原来的文件,对硬链接没有影响,但是软链接就失效了(源文件删除了,那么快捷方式自然不起作用):
文件是如此,那么目录呢?
我们可以看到这里的目录的硬链接是两个,为什么呢?
我们进入该目录下可以看到原因:
我们会发现该目录里面的.和当前目录的inode竟然是相等的,同时..的inode竟然和上级目录是相同的。尤其可见,我们使用的cd ..命令之所以能返回上级路径其实就是因为..的inode和上级路径下的目录的inode是相同的。
. 和 .. 就是给目录进行硬链接。这样能让我们更好的访问。
我们发现一个问题:Linux是不允许普通用户给目录进行硬链接。为什么呢?
Linux的硬链接不能链接到目录是因为引入了对目录的硬连接就有可能在目录中引入循环,在目录遍历的时候系统就会陷入无限循环当中,这样导致无法定位到访问目录。Linux的目录结构是一棵以“/目录”为根节点的树,如果允许自定义硬连接,则很有可能会破坏这个结构,甚至形成循环;而一旦形成循环,对于需要遍历目录树的命令,是致命的。
acm 3个文件时间
在之前我们就提到过文件的3个时间:
Access: 最后访问时间 (因为访问是比较频繁的,所以Linux不一定在访问之后就刷新这个时间,而是 经过一定的访问次数才会刷新这个时间 ,这同时也提高了效率。)Modify 文件内容最后修改时间 (文件内容修改后立即生效)Change 属性最后修改时间 (文件内容修改了,这个时间也会变,因为文件的大小改变了,文件的大小也是属性的一部分)
动静态库的概念:
静态库( .a ): 程序在编译链接的时候把库的代码链接到可执行文件中 。程序运行的时候将不再需要静态库动态库( .so ): 程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码 。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking )动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间 。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
我们简单的写了个加减法函数,并封装给其他人使用:
写好之后我们就可以使用tar指令进行打包压缩并上传,这样别人想使用的话就可以通过yum来下载我们写的程序进行使用。下载的本质其实就是拷贝。然后我们就可以正常的使用了。
在这边我们想要生成图中的math的可执行文件,我们就需要让gcc帮我们做下面这条指令:
gcc -o math main.c -I ./mylib/include -L ./mylib/lib -lmath
-I 指定头文件
-L 指定库路径-l 指定库名
shared: 表示生成共享库格式fPIC :产生位置无关码 (position independent code)库名规则: libxxx.so和生成静态库的方法类似:需要以下指令:
原因是找不到这个库的位置,为什么呢?
在我们进行编译的时候我们已经告诉了gcc所以它想要的信息,但是在编译结束之后,是由OS和shell来创建进程来运行你的程序,而OS和shell并不知道你的库的路径,你只告诉了gcc而没有告诉OS
解决方法:
然后我们运行一下:
重新登录:
因为我们自己添加的环境变量只在你本次登录的时候有效,下个再运行就有重新添加。
2.拷贝.so文件到系统共享库路径下, 一般指/usr/lib,这个直接添加即可。
3.ldconfig 配置/etc/ld.so.conf.d/,ldconfifig更新(永久生效)
首先我们进入配置文件的路径,然后创建文件,写入更新,即可
4.建立软链接(永久生效)
这样我们就建立了和库的软链接,就可以运行了。
其实动静态库的加载过程可以使用相对位置和绝对位置这个概念来区别:
对于静态库来说,在加载过程中就是程序和库一起被加载进来了,但是这个库函数只能被一个进程使用,如果是多个进程那么就需要加载相同的库到不同的进程中,所以为什么静态库所需要的空间很多,但是动态库就不需要动态库的加载过程: