linux.6 基础IO

1.C语言文件接口的复习

我们fopen创建文件时,是进程在当前目录下创建的文件,当前目录是运行程序所在的目录,不是可执行程序所在的目录。一般情况下直接 ./ 可执行程序得到的文件在可执行文件目录下是因为当前目录恰好就是可执行文件你的目录。如果 cd … 到上级目录,在上级目录下运行 ./10-12/testout ,文件就会被创建在10-12所在的目录下。需要掌握的三种文件打开方式 w , r , a(追加写入)。

打开文件,一定是进程运行的时候打开的。读写,关闭都是进程完成的。在Linux中,一切皆文件,那么我们的显示器,键盘也是文件。既然我们的显示器和键盘都是文件,但是想要往屏幕打印数据和用键盘写入数据的时候,我们并没有打开任何的文件,那我们为什么可以直接printf,scanf ?那是因为任何的进程在运行的时候,都会默认打开三个输入输出流:stdin ,stdout , stderr 。

如何理解这三个输入输出流?实际上这三个输入输出流就是三个FILE*的文件指针,所以有着键盘显示器也是文件的理解。如何操作?我们可以把下面的fputs中的file换成stdout,那么我们想要写入的字符串就会写到显示器中;如果我们把fgets中的file换成stdin,就会从输入的字符串中读取字符串打印到显示器上。在这里插入图片描述

输入字符串到文件中fputs
在这里插入图片描述

从文件中获取字符串fgets
在这里插入图片描述

2.系统文件的IO

2.1 系统调用接口open和close

在这里插入图片描述

系统调用open有三个参数:分别是打开文件的路径文件名,打开方式,默认的文件权限。其中打开文件的方式是由宏代替的,实际上是系统函数参数传参标志位,有32个比特位,所以理论上可以传递32中标志位,在二进制序列中,只有一个比特位是1,假如O_RDONLY 对应的二进制是0001,O_WRONLY对应的二进制是0010,O_CREAT对应的二进制是0100,如果想要创建文件并只读,可以O_RDONLY | O_CREAT 使得标志位为0101而达成效果。如果想要检查是否设置完成某种方式,可以在open的程序中判断。例如 way = O_RDONLY | O_CREAT ,想要检查是否只读可以 if(way & O_RDONLY) 来判断结果。mode 是默认打开文件的权限,一般用8进制数表示,如果想要mode的值就是文件对应的权限,需要在open之前将系统 umask设置为0 ,umask(0)。
在这里插入图片描述

在这里插入图片描述

open的返回值是文件描述符,实际上就是数组下标。当我们在进程中打开多个文件时,可以发现这些open的返回值都不同,都是从3开始递增的,因为0,1,2对应的是输入输出流的文件描述符。如果返回-1就表示打开失败。打开文件之后,要用close关闭文件,要注意的是,close的参数是文件描述符。
在这里插入图片描述

2.2 系统调用接口write和read

可以发现系统调用接口对应着C语言的一些接口,例如open和fopen,write和fwrite,read和fread,close和fclose。这时C语言封装了这些系统接口,在底层用的其实就是这些系统接口,使得用户用起来更加的方便,也使得语言具有了跨平台性。

write
write有三个参数,第一个参数是要写入文件的文件描述符,第二个是要写入的数据的指针,第三个是要写入的大小。最终写入的大小由ssize_t决定。写入成功,返回写入的字节大小,返回0表示没有写入内容,返回-1表示写入失败。
在这里插入图片描述
在这里插入图片描述

read
read由三个参数,第一个参数是要读取的文件的文件描述符,第二个是读取到buf中,第三个是读取的字节大小。如果读取成功,返回读取的字节数,如果返回0表示文件结束,如果返回-1表示读取失败。
在这里插入图片描述
在这里插入图片描述

3.深入理解文件描述符

系统文件分为内存文件和磁盘文件。一个进程可以打开多个文件,那么多个进程就可以打开很多个文件,所以在任何时刻,都可能存在着大量的已经打开的文件,这时就需要操作系统对文件进行管理,这就是操作系统的文件管理。和操作系统的进程管理类似,既然要管理,就需要先描述再组织。操作系统对在内存中的内存文件描述成一个 struct file 的结构体,然后以双链表的形式对每个文件的结构体进行管理。文件由内容和属性组成,在磁盘文件中保存了文件的内容和属性,当文件加载到内存中变成内存文件时(类似程序加载为进程),加载到内存中的更多的是文件的属性信息,延后式的慢慢加载数据。

3.1 进程和文件的对应关系

当系统内存中存在着大量的文件,每个文件都会被描述成一个struct file 被操作系统用双链表管理起来,那么进程在打开或者更改文件的时候是通过什么样的对应关系找到相应的文件的呢?
当程序运行起来变成进程,相应的在内存中操作系统会创建 task_struct ,而在 task_struct 中有一个指针指向 struct files_struct 的结构体,这个结构体内包含了一个指针数组 struct file* fd_array[32]和其他的内容,而 struct files_struct 包含的指针数组内的指针就是指向各个文件的 struct file 的。所以当进程对文件进行操作时,例如open时,系统会将磁盘中的问价的内容和属性加载到内存中,形成,文件的 struct file ,然后用链表将 struct file 管理起来,然后系统再将进行操作的文件进行分配,最后将文件的 struct file 的地址填入到 struct file* fd_array[32]中,然后再将该位置数组的下标返回给调用方,这时用户就拿到了文件描述符,这也就是为什么文件描述符就是数组下标的原因。
在这里插入图片描述

进而现在可以理解为什么open,write,read的时候,参数都含有文件描述符了,正是进程要通过文件描述符找到指针数组相应下标中存放的地址进而找到文件的struct file,才能对文件进行操作。那么进程默认打开0,1,2实际上就是系统检测到键盘显示器后自动创建了0,1,2三个struct file将它们自动分配到数组下标0,1,2的位置。文件描述符的分配是有规则的,是从没有被占用的最小的下标开始依次分配

fopen 究竟在做什么?1.给调用的用户申请 struct FILE 结构体变量FILE* fp = fopen("log.txt","w");,并返回地址 FILE* 2.在底层通过open打开文件,并返回fd,把 fd 填入 FILE 变量中的 fileno。

3.2 深入理解重定向

重定向的本质时修改文件描述符fd的下标。当没有close(1)的时候,write(1)就直接将字符串写入到了显示器上,如果close(1),那么fd_array[1]所存放的就不再是显示器文件的地址,根据文件描述符的分配规则,后来open的test.txt的struct file 的地址将会放到 fd_arrray[1]中,所以后来再write(1)后,字符串就被写入到test.txt中。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从上面的代码中可以看到,表面上 open 和下面的 printf 和 fputs 是没有关系的,因为 printf 和 fputs 是往输出流也就是显示器中显示数据的,不管上面对文件进行怎样的操作,它们都是互不干涉的,但是我们 close(1),发现结果影响到了最后输出的结果,也就是说 fd=1 和 FILE* stdout 是存在着某一种联系的。stdout 本质上是一个指向 FILE结构体 的指针,而在这个 FILE 的结构体中就包含了 fd 的信息。而 stdout 的 FILE 中,它的 fd 被指定为了 1 ,如果不 close(1) ,那么 fputs("…",stdout) 就找到 fd_array[1] 的位置从而找到显示器文件的地址。但是如果 close(1) ,只是说 fd_array[1] 存放的不再是显示器文件的地址,open之后存放的是 test.txt 文件 struct file 的地址,这样想要显示的字符串就 fputs 到了 test.txt 中,所以这些C语言的文件接口只认 stdout ,而 stdout 的 fd永远等于1,至于 fd_array[1] 指向的是谁它们是不管的,这样就造成了输出重定向,这也是输出重定向的原始原理。
在这里插入图片描述

输入重定向,从文件输入
在这里插入图片描述

追加重定向
在这里插入图片描述

那么总的来说,输出重定向 > 实际上就是先 close(1) ,然后用 O_WRONLY 的方式打开文件,追加重定向 >> 实际上就是先 close(1),然后用 O_WRONLY | O_APPEND 的方式打开文件,输入重定向 < 实际上就是先 close(0), 然后用 O_RDONLY 的方式打开文件。

3.3 dup2

在这里插入图片描述
我们也可以通过 dup2 来进行重定向, fd_array[ oldfd ] 存放的 file* 指针拷贝到了 fd_array[ newfd ] 中,完成了重定向。
在这里插入图片描述
在这里插入图片描述

4.缓冲区

凡是显示到显示器上的内容,都是字符,凡是从键盘读取的内容也都是字符,所以,键盘和显示器一般称之为“字符设备”。缓冲分为三种,分别是无缓冲,行缓冲,全缓冲。常见的行缓冲有对显示器进行刷新数据,在我们printf 一段内容时,该内容被保存到内存的行缓冲区,只有当输入 \n ,或者程序结束 ,或者行缓冲区满了的时候内容才会被刷新到显示器上。全缓冲一般是对磁盘文件写入的时候会用到,必须要将全缓冲区写满或者进程退出才会刷新到磁盘中。观察以下的代码和现象:
在这里插入图片描述

在运行程序的时候,打印了正常的三行字符串,但是我们把输出的结果重定向到文件中时,结果发生了变化。./testout 和 ./testout > log.txt 存在差异性的原因是 ./testout 运行程序后,打印用的是行缓冲,遇到 \n 马上被刷新出来,所以三行数据都被刷新出来;但是 ./testout > log.txt 是在写入文件,用的是全缓冲,\n 就不发挥作用了,即使打印了也不会刷新出来,要等程序结束才会刷新出来。而打印结果不同的原因是 ./testout 行缓冲区的数据被立马刷新,fork() 得到的子进程就拿不到缓冲区数据了所以子进程没有打印;而 ./testout > log.txt 之后全缓冲区中的数据没有被刷新出去,在程序结束前进行了 fork() ,使得子进程拿到父进程的缓冲区数据,父进程和子进程都要结束时,父进程的全缓冲区和子进程的全缓冲区都要刷新,但是进程具有独立性,这时子进程就要发生写时拷贝,从而父进程和子进程分别刷新得到两个 printf 和 fprintf 。但是为什么 write 数据没有打印两份呢?那是因为 write 没有缓冲区。

那么缓冲区是谁提供的?如果缓冲区是操作系统提供的,那么上面的所有接口数据都要打印两次,这就说明缓冲区是语言自带的,是由 struct FILE 维护的,这是用户缓冲区。我们打开文件,文件的 struct file 就维护了这一块用户缓冲区。如果要写入内容到显示器或者磁盘中,光有用户缓冲区是不行的,因为软件与硬件要进行交互一定要经过操作系统,所以在 kernal 区有内核缓冲区,用户缓冲区的内容会刷新到内核缓冲区中,然后在刷新到显示器或者磁盘文件中。

FILE结构体
在这里插入图片描述

struct FILE
{
   int fd;
   ...
   //缓冲区
};

在这里插入图片描述

再观察下面的代码:
如果我们在程序中不使用 fflush 进行手动刷新,最后在进程结束之前进行 close(fd) ,因为写入文件是全缓冲,所以在进程结束时才会刷新缓冲区,但是因为我们把该文件描述符 fd 给关了,即使 FILE 中还维护着缓冲区,最后进程想刷新该文件的缓冲区也写入不到文件中去了,因为已经不能通过 fd 找到这个文件了,所以就没有内容刷新到文件中。如果我们用的是 fclose(stdout) ,那么系统知道要关闭文件,就会提前在关闭文件之前要进行刷新缓冲区(这个时候还可以通过文件描述符 fd 找到文件的 struct file,就可以写入文件),再关闭文件,所以内容刷新到了文件中。
在这里插入图片描述

再次理解Linux下一切皆文件:一切硬件的读和写的方法实现不尽相同,每个硬件都有它们的 write() ,read() 等等,但是它们都可以被描述成一个 struct file ,其中有一些函数指针例如 void (*read) () 和 void (*write) () ,这是一些通用接口,当 struct file 具体描述的是哪一个硬件,就将这些函数指针指向了硬件自己的接口函数,在操作系统看来,这些硬件就变成了一个个的 struct file ,可以通过 struct file 中的函数指针去对硬件进行操作,而底层硬件实现的接口是怎么样的,系统并不关心。

接下来我们可以完善之前的简易shell的代码,使得程序可以完成输出重定向。子进程是会继承父进程打开的文件信息的,进程替换不会影响打开的文件信息。在这里插入图片描述

5.理解文件系统

在我们输入ls -a -i -l 的时候,左边第一列代表的是 inode 编号。文件是由文件属性(元信息)和文件内容组成。而Linux把文件内容与文件属性进行了分开存储。而 inode 是任何一个文件的属性信息的集合,Linux 中每个文件都有一个 inode ,那么系统中就存在着大量的 inode ,所以就需要一个 inode 编号来进行区分。inode 和 文件内容都是在磁盘中存储的。实际上我们的 ls 是用来查看文件的属性信息, cat 来查看文件的内容。
在这里插入图片描述

5.1 磁盘

磁盘是一个效率很低的外设,寻址方式是磁面+柱面+扇区。磁盘可以理解为线性结构,为了管理磁盘要对磁盘进行分区,至于对每个分区的管理方法是由文件系统决定的,这也叫对分区的格式化。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。

GDT,Group Descriptor Table:块组描述符,描述块组属性信息。

块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用,用一个二进制的序列表示,如果该位置下Block 被占用,那么这个比特位会置1。
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。同样的也是有一个二进制序列被占用则该比特位为1,没有占用该比特位为0。

inode节点表:其中存放的是一个一个的 inode 结构体,分区之后 inode 的个数就是确定的,结构体内存放了 inode号,与数据区对应的 block 数组,文件属性。
在这里插入图片描述

数据区:存放文件内容 。

总的来说,inode 和数据区有没有被占用,占用了多少,还剩多少是在超级块中体现出来,而哪些占用了,哪些没有被占用,是在两个位图中体现出来。inode Table 里的 inode 结构体存放着文件的 inode 号以及文件的属性信息,还有和数据区的对应关系,可以通过 inode 结构体里的数组找到文件在数据区的数据。所以文件的属性信息和数据信息是分离存储的。

那么回过头来,创建一个文件的过程是怎么样的呢?想要创建文件,就得到 inode bitmap 中去遍历找剩下的 inode ,在 inode Table中申请一个 struct inode,然后存放该文件的属性信息。创建完成之后,如果我们想要往文件中写入1kb的数据,例如往test.c中写入printf(“hello world”); 首先得通过文件名 test.c 找到该文件的 inode,在通过 inode 找到描述该文件数据区的数组,然后再通过到 block bitmap 中遍历找到可用的数据区 block 的位置,然后申请空间再将数据区与数组对应起来,然后再对数据区中的block进行写入,这样数据就被写入到文件中,然后再将文件的文件名和 inode 的映射关系填入到目录的内容当中。删除文件只要修改两个位图即可,将刚刚申请的比特位为1的位置为0,目录内容中的该文件名和inode的映射关系进行清除就完成了删除,而不会对该 inode 和 block 中的数据进行清空,如果下次有文件的创建,如果需要用到这块空间,那么就会就行覆盖式的写入,所以不需要清空。这也是为什么拷贝的时候比较慢,删除的时候比较快的原因。这种方式也造成了删除的文件是可以恢复的。
在这里插入图片描述
在这里插入图片描述

那么如何理解目录?既然Linux下一切皆文件,所以目录也可以看作是文件,那么目录就应该也有自己的 inode,和文件一样,目录的 inode 里存放了目录的属性信息,但是目录的内容存放的是当前目录下的文件名和文件名对应的 inode 指针,是一种映射关系,所以得到结论:文件名并没有在 inode 中保存,包括目录本身,当前目录下的所有文件的文件名都保存在该目录的数据区中,对于单个文件,它只有 inode 编号,并没有存储文件名。也就是说操作系统认识一个文件是通过 inode 编号认识的,而不是通过文件名认识的。目录也是文件,也有inode属性,也有内容。
在这里插入图片描述
目录的内容存放的是文件名和inode的映射关系在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

对一个目录ls -i ,实际上就是将该目录的内容打印出来;
对一个目录ls ,实际上就是显示目录内容中的文件名;
对一个目录ls -l,实际上就是显示目录内容中的文件名,并通过文件名与各个文件的inode的映射关系找到各个文件的属性信息并显示出来
在目录下对一个文件进行cat,实际上就是在目录内容中找到该文件的文件名,并通过文件名和inode的映射关系找到该文件的inode,在通过该文件的inode找到该文件的内容数据,并显示出来。

5.2 软链接和硬链接

在这里插入图片描述
软链接和硬链接的区别:
1.软链接是一个独立的文件,有自己独立的 inode ,文件内容存放的是 testout 的路径,硬链接没有独立的 inode。
2.软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的 inode 的一个映射关系,并写入当前目录。例如 . 就是当前目录的一个别名, … 就是上级目录的一个别名,这样方便了目录之间通过相对路径进行跳转。上面的代码中发现 … 对应的硬链接数是 14 个,我们进入上级目录发现,上级目录中有12个目录,这12个目录里的 … 对应的是上级目录的硬链接,加上 . 和目录本身刚好是14个。

硬链接的本质实际上是多个文件名对应了一个inode,而这个硬链接数就是引用计数,只有当引用计数为1的时候删除该文件该文件才会真正被删除。
在这里插入图片描述

文件的ACM时间:
Access:最后一次访问的时间
Modify:文件内容最后一次修改的时间
Change:文件属性最后一次修改的时间

在这里插入图片描述

6.动态库与静态库

6.1 动静态库的基本原理

动静态库的本质是可执行程序的“半成品”,像 printf , scanf 这些函数使用者只要会用就行,函数的具体实现是在哪里呢?C语言的处理分为4个部分,预处理,编译,汇编,链接,经过汇编之后,所有的 .c 文件都变成了二进制的 .o 文件, .o 文件里包含的是大量的方法,这些 .o 文件可以与 其他的 main.o 文件进行链接,最后形成可执行程序。所有库的本质是一堆 .o 文件的集合,不包含 main ,但是包含了大量的方法
在这里插入图片描述

6.2 认识动态库与静态库

我们在普通的test.c文件中调用 printf 函数时,实际上是调用了在 C 库中的 printf 函数方法。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在Linux中,动态库后缀为 .so ,静态库后缀为 .a
在Windows中,动态库后缀为 .dll ,静态库后缀为 .lib

6.3 动静态库各自的特征

当一个可执行程序使用的室静态库时,当进程运行起来,进程会直接把库的内容直接拷贝到该进程的正文代码段,当系统存在大量的进程,也要一次又一次的拷贝。而动态库则不一样,库文件是通过地址空间进行共享的,进程如果需要库中的某一个模块,就会利用进程空间的共享区和内存中的库文件通过页表的映射关系,要多少就给多少。

静态库的缺点:占空间(磁盘+内存)
静态库的优点:与库无关了,不需要库(就算是程序运行起来之后库被删了,进程也照样运行,因为已经拷贝了一份)

动态库的缺点:必须依赖库,没有库就无法运行(如果库被删,就不能通过页表找到所需要库的内容了)
动态库的优点:节省空间(内存空间)
在这里插入图片描述

6.4 动静态库的制作

6.4.1 静态库的打包与使用

在打包之前得有若干个 .h 文件和 .c 文件
1.用 gcc -c 把 .c 文件编译成 .o 目标文件
2.用 ar -rc 把所有的 .o 目标文件打包成一个 .a 的库文件
3.要想使用库,首先得创建一个库的目录,子目录分别为包含 .h 文件的子目录和包含 .a 库文件的目录,即 libdir/include 和 libdir/lib 。
给别人库的时候,本质是给别人一份头文件(库的使用说明书)+一份库文件(.a/.so库的实现)
在这里插入图片描述

方法一:用指令
在这里插入图片描述

方法二:拷贝到系统的头文件和库文件的目录下(实际上就是安装库的过程)
在这里插入图片描述
在这里插入图片描述

6.4.2 动态库的打包与使用

1.用 gcc -fPIC -c 把 .c 文件编译成 .o 目标文件
2.用 gcc -share 把所有的目标文件进行打包
3.创建一个库的目录,子目录分别为包含 .h 文件的子目录和包含 .so 库文件的目录。
在这里插入图片描述

方法一:用指令,如果生成的可执行程序不能运行,再配置环境变量LD_LIBRARY_PATH
在这里插入图片描述
在这里插入图片描述

方法二:直接拷贝到系统路径中,不做演示。

总结

  1. 先转化成 .o 文件:
    静态库: gcc -c
    动态库: gcc -fPIC -c
  2. 打包
    静态库: ar -rc
    动态库: gcc -shared
  3. 使用
    给别人的是包含头文件集合和库文件集合的文件
    静态库:-I -L -l 链接
    动态库:-I -L -l 链接
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:
  4. 安装库,本质是把头文件和库文件拷贝到系统文件中
  5. 第三方库使用的时候一般都要知名库名称
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RanieMiss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值