1.先回忆一下c语言的文件接口:
fopen,fwrite:
对应的代码如下:
注意fopen和fwrite的用法:
代码结果:
对应的strlen不需要+1吗,不需要\0对应的是c语言的存储方式,和操作系统无关。
可以看到已经在对应的文件中写入了.
可以看到在fopen中只写了文件名没写路径。对应在当前文件路径下创建
如果改变路径呢?
对应的结果:
可以看到直接写进/home/msb里面了。
-
对应的"w"是在写入之前,对文件进行清空,再写入:
-
对应的"a"也是写入,对应直接在文件后面追加写入:
2.认识文件系统调用:
认识:
文件其实是在磁盘上的,磁盘是外部设备,所以访问磁盘文件其实就是访问硬件。
对应系统的文件调用函数:
open,write:
open对应的使用方法:
open的返回值是int,对应的flags接口:
旗标的解释如下:
-
O_RDONLY 以只读方式打开文件
-
O_WRONLY 以只写方式打开文件
-
O_RDWR 以可读写方式打开文件.
-
// 上述三种旗标是互斥的, 也就是不可同时使用,
-
// 但可与下列的旗标利用 OR(|)运算符组合.
-
O_CREAT 若欲打开的文件不存在则自动建立该文件.
-
O_EXCL 如果 O_CREAT 也被设置, 此指令会去检查文件是否存在. 文件若不存在则建立该文件, 否
-
O_NOCTTY 如果欲打开的文件为终端机设备时, 则不会将该终端机当成进程控制终端机.
-
O_TRUNC 若文件存在并且以可写的方式打开时, 此旗标会令文件长度清为 0, 而原来存于该文件的资源
-
O_APPEND 当读写文件时会从文件尾开始移动, 也就是所写入的数据会以附加的方式加入到文件后面
-
O_NONBLOCK 以不可阻断的方式打开文件, 也就是无论有无数据读取或等待, 都会立即返回进程之中
-
O_NDELAY 同 O_NONBLOCK.
-
O_SYNC 以同步的方式打开文件.
-
O_NOFOLLOW 如果参数 pathname 所指的文件为一符号连接, 则会令打开文件失败.
write对应的使用方法:
不难想到fd对应的应该就是open的返回值。
写代码加深了解:
对应的flags是
O_WRONLY:对应打开的文件的方式为只写方式
O_CREAT:对应如果该文件不存在,则创建一个该文件
O_TRUNC:如果文件存在并以可写的方式打开的,那么该旗标会直接将文件中原来右的内容清0
结果如下:
我们试一下 O_TRUNC的效果:
在此文件存在并写入的情况下,写入此代码:
对应的结果如下:
再试一下O_APPEND旗标:
在log,tx为123的情况下追加:
代码如下:
结果:
可以看到与预期结果一样的。
那么对应的fd(文件描述符)到底是什么意思,有什么用?
3.对应访问文件的本质:
先对应多写几个文件打印出来看看:
发现这个fd都是从3开始的,那么为这么是3呢?
还记得c语言中的头文件<stdio.h>
对应里面有几个文件吗?
-
stdin
-
stdout
-
stderror
所以这三个文件其实不是c语言的,本质上是操作系统的,c语言只是使用者。
我们可以在系统中验证:
结果如下:
所以更加说明了:
不管是哪个语言,只要有对应的文件的操作函数,输入输出流,其底层一定会调用系统的对应接口,
所以任何语言在文件操作和输入输出的底层都是保持一致的,都对应操作系统堆其实现。
每个语言文件操作函数必须封装有文件操作符:
所以对应fopen都是通过open封装的。
结合上述只是再来学习一下read系统调用接口:
代码:
s返回的是到最后的数组下标。
对应的输出:
对应在键盘上输出就会打印。
再来深度讨论一下访问文件的本质:
对应文件存在磁盘中,通过双向列表的形式在存储
而在test_struct中也存在一个struct_file_struct用来存储文件信息:
只是大概,细节会在后面再说。
文件重定向:
先看下面一段代码:
对应的运行结果:
至于fd为什么等于3,和为什么写在文件里,已经讲得很清楚了。
在这之前先说明一下文件描述符对应的分配规则:
从0下标开始,寻找最小的没有使用的数组位置(上面讲到的文件本质),它的下标就是新文件的描述符。
那么如果我们在上述代码中把对应的stdout1号文件关了,并在显示器上写入呢:
对应的结果如下:
对应的hello Linux还是打印到了文件中,这其实是与预期相符的,因为此时的1号文件对应的就是指向log.txt的文件
而上述的一系列操作其实就实现了文件重定向。
难道在系统中的文件重定向也是这么实现的吗,当然不是,有对应的接口函数:
dup:
这里主要看dup2:
这个接口的命名比较绕,需要更好的理解,对应的含义是让newfd成为oldfd的一份拷贝,最终意味着olded是最终保存在newfd的,所以如果我们要实现上述的操作,
对应的函数的书写应该是:
dup2(fd,1);
close(fd);
这才是正确的,代码验证:
对应的结果:
可以看到,就实现了文件的重定向。
那么其他的printf,fprintf这些标准输出是在哪里输出的呢:
对应的结果:
可以看到是直接将log.txt的文件指针替换到标准输出的文件指针去了。
所以说,标准输入应该也是相同的:
结果:
会直接输出文件中的内容。
如果在自制的shell中写入重定向,进程替换会不会影响文件访问?
不会,对应的进程具有独立性。
补充重定向的一些其他玩法:
先写出如下代码:
这是对应之前的玩法,将输出stdout重定向到notmal.log里面。
其中error正常输出在显示器上。
对应两个输出重定向。
那么如果我就想把这两种输出放在一个文件中呢?
对应将2号文件重定向到一号文件地址了。
如何理解一切皆文件?
外设是如何被操作系统识别的?
只要是外设,虽然肯定会有接口种类的不同,但一定都存在读接口和写的接口
而对应操作系统中的文件虚拟系统与这些外设的读写接口构成多态,从而使
操作系统通过进程访问文件从而访问到这些输入输出设备。
文件虚拟系统中存在具有函数指针的结构体,从而可以很好的接收输入输出设备的
读写函数接口。
所以对于操作系统而言,无论访问什么,都可以通过对应的进程用对应的open,write,read,来实现,
所以,对于操作系统而言,一切皆文件:
用户缓冲区:
先来看一段代码:
对应的输出:
对应的输出没问题。
再来看下一段代码:
只添加了一个fork创建子进程。
对应的结果:
说明一定是fork的作用,再看一段代码:
对应的结果:
对应在显示器上没有打印。
接下来我们来说明缓冲区的使用规则:
对应我们在调用c语言接口的时候使用的一定是c语言对应的缓冲区。
同理调用系统接口的时候一定是内核的缓冲区。
缓冲区的刷新方式:
-
无缓冲 —— 直接刷新
-
行缓冲 —— 不刷新,直到碰到\n才刷新
-
全缓冲 —— 缓冲区满了,才刷新
对应的显示器文件的刷新方案是行刷新,
用户刷新的本质,就是将数据通过1 + write写入到内核中
如果重定向到文件中,会变为全缓冲,等缓冲区满了才会输出,对应如下:
结果:
对应只有write打印出来,说明其他都还在缓冲区中。
补充问题:
1.缓冲区在进程退出的时候也会刷新。
就是说如果上面的代码不写close是没有这种效果的,对应还是会全部打印出来,因为在退出的时候也会清理缓冲区。
2.为这么会有缓冲区?
-
提高效率
-
配合格式化
3.缓冲区在哪里?
对应在FILE的结构体中。
也就是在c语言中,对应malloc(FILE)的时候对应都会创建出来一个缓冲区。
4.我们目前认为:
只要将数据刷新到对应的内核中,数据就可以直接被输出了。
接下来解决这个问题:
对应结果:
为什么会是这样呢?
现在应该是可以理解了:
对应将输出重定向到了文件中,对应的缓冲方式由行缓冲转变为了全缓冲。
所以c接口的函数先不会打印,而是存在缓冲区中,
而对应到了系统接口的时候直接打印。
最或在函数的返回的时候才把缓冲区冲刷了。
打印两次的原因是创建子进程发生了写时拷贝。
为了有更好的理解,模拟实现一下C语言标准库:
innode和软硬链接:
认识磁盘:
磁盘的构造:
磁盘的存储构成:
将磁盘的物理结构逻辑化:
对应成线性的:
以及在第几面,第几个磁道,第几个扇区:
文件系统:
软硬链接:
datablocks:存文件的内容,以块的形式呈现。常见的是4KB大小。
innodetable:对应单个文件的所有属性,128字节,一个文件对应一个inode:
对应的inode结构体:
上面所有信息都存在inode结构体中。
inodebitmap:
比特位的位置和inode编号映射起来,用来设置对应inode的有效性。
blockbitmap:
用来将比特位的位置和对应的块号映射起来,比特位中的1,0来表示文件内容,块有没有被使用,在删除文件的时候,直接将对应的比特位置0即可。
groupdescribetable:
组描述表,用来描述组中文件内容和对应inode的使用情况。
superblock:
对应文件系统的基本信息:
-
一共有多少个组
-
每个组的大小
-
每个组的block数量
-
每个组的起始inode
-
文件的类型与名称等
但是在linu文件属性中,并不包含文件的名称,而在其中表示的都是对应的inode。
那么我们怎么知道一个文件的inode呢?
对目录的加深理解:
目录也是文件,也有自己的inode。目录也有自己的属性。
目录中也有内容,也有对应的数据块。
在数据块中存放的是文件的文件名和对应文件的inode映射关系!
对应的一些常用的inode在linux下有对应的缓存,所以找起来能快一些。
接下看对应的软硬链接:
这就是对应的软硬链接:
从inode中就可以看出:
软链接是一个独立的文件,具有独立的inode,该如何理解软链接?
软链接也有独立的数据块,其数据块里保存的是指向文件的路径:
其实相当于Windows中的快捷方式。
软链接的应用场景:
对应在有些情况下,我们要执行的运行文件或着安装的软件对应的绝对路径很长,这时候建立一个软链接就可以很好的解决这个问题。
如何理解硬链接:
没有独立的inode,没有独立的文件:
-
所谓的建立硬链接,本质就是在特定目录的数据块中新增文件名
和执行文件的incode的映射关系。
-
每一个inode内部,都有一个叫做引用计数器
应用场景:
目录中的. ..对应都是目录的硬链接。
但硬链接是必不可少的,应为在操作系统中,返回上级和当前目录对应的. .. 是不能改变的。
问题linux为什么不允许对目录建立硬链接?
因为会产生无限递归的情况。
因为每个目录都会有是上层目录的地址和自己的地址,
如果建立硬链接之后,对应要在该目录中找对应的文件,
当我们好不容易从根目录找找到了该目录,可是发现对应有
两个inode完全相同的两个目录,那么就会进入到一个环路中
一直无限循环下去,所以操作系统直接禁止了这一操作。
动态库,静态库:
静态库:
目的是不想让用户知道源码,将.c文件对应转换为.o二进制文件,然后再将这些二进制文件打包
最后形成静态库:
例子:
对应的.h.c文件:
对应生成二进制文件以及生成静态库的方法:
下面的output是对应静态库放在一个目录里面从而方便其他用户使用。
对应创建了一个新目录,把对应的lib目录拷贝过去;
在test底下写一个main.c:
注意运行代码是只能这么运行:
-I指定头文件路径,-L指定.c库路径,-l指定的是对应的静态库名
本来的静态库是:libmymath.a
对应真正的库名是去头去尾,最后就是mymath
所以对应文件执行起来了:
所以静态库就是,将对应的.h文件和被打包成库的源代码形成库。
动态库:
与静态库作用相同,也是不让看原码:
例子:
对应的代码以及Makefile文件:
现在已经转到vscode里面了:
Makefile文件:
框柱的是形成动态库目录。
shared:表示生成动态库格式。
fPIC:产生位置无关码
对应后面解释。
在终端的命令行与静态库的一样:
可以看到我们已经生成了可执行文件但是运行不了,显示不能打开对应的文件,
再来看看库的链接情况:
可以看到我们想要链接的动态库是找不到的。
对应的解决办法有四种,但第一种最常用:
1.将动态库拷贝到系统默认的库路径:user/lib64/
可以看到动态库直接链接上了,而且可执行文件也跑起来了。
2.在默认库路径下建立软连接:
也可以看到动态库链接上了。
3.将自己的库的所在路径添加到系统的环境变量中LD_LIBRARY_PATH:
对应也产生了链接。
4./etc/ld.so.conf.d建立自己的动态库路径配置文件,然后重新ldconfig即可:
对应也能找到。
ncurses --- 基于终端的图形界面库
总结:
动态库在进程运行的时候,是要被加载的(静态库没有,直接放在可执行文件中)
常见的动态库被左右的可执行程序,都要使用,动态库 ———— 共享库
所以动态库在系统中加载之后,会被所有进程共享。
3.动态库是怎么加载的?
通过每个进程的PCBtesk_struct中的进程地址空间中执行代码,要调用动态库的时候将要建立链接的共享区中的虚拟地址取出通过页表在映射到物理地址中的共享库中。
再加载到可执行程序中的。
结论:
-
从此以后,我们执行的任何代码,都是我们在进程地址空间中执行的。
-
系统在运行时,一定会存在多个动态库,对应要用操作系统管理起来,管理方式:先描述,再组织。
4.再谈地址空间:
什么是虚拟地址,什么是物理地址?
程序在编译好之后,内部也有地址的概念。
在学习c++的时候一定看过对应一些代码的反汇编,其实反汇编后面的函数,还是指令对应都已经转化成了地址,而且是虚拟地址,对应方便我们在进程地址空间中找到它,并映射到真正的物理地址,并执行操作。
那么如果是执行第一条指令呢?
对应的页表就会发生缺页中断从而在物理地址中取寻找。
所以CPU接受到的其实全部都是虚拟地址:
5.动态库的地址:
关键问题:共享库大了,具体映射到哪里呢?
动态库加载到固定的地址空间位置是不可能的。
解决方法:逻辑地址+偏移量的方法:
让自己库中的内部函数不要再用绝对编址,值表示在库中的偏移量即可
所以来解释fPIC的含义:
产生位置无关码,其实就是产生对应的偏移量的: