【俩万字解析】linux基础IO/文件系统inode/动静态库详解

文章目录


前言


一、什么是当前路径?

在学习c语言的时候,fopen如果以写的方式打开文件如果这个文件存在那么文件的内容将会被清空,如果不存在会在当前路径下创建这个文件,那什么是当前路径呢?

我们使用以下代码创建一个文件
在这里插入图片描述

我们第一次在程序的所处目录执行该程序,并使用PS命令查看进程,获取进程的pid:
在这里插入图片描述
然后我们可以在/proc/中查看进程:
在这里插入图片描述
在这里插入图片描述

第二次我们在/home/LZH目录下执行该可执行程序

在这里插入图片描述

于是我们可以得到如下信息:

  • cwd:指进程的工作目录(进程在哪一个目录下执行的)。
  • exe:指该进程对应可执行程序所处的目录。

二、系统调用接口

操作系统的底层其实给我们提供了文件IO系统调用接口,有的write,read,close和seek登一套系统调用接口,不同的语言会对齐进行封装,封装成对应语言的一套操作文件的库函数,不需要知道底层的调用关系,降低使用者的学习成本。

1. open函数

函数原型如下:
在这里插入图片描述

  • 参数:pathname
    open的第一个参数表示打开或者创建目标文件的文件名称
    1.如果以路径的形式给出那么当需要创建文件的时候,会在你提供的这个路径下创建。
    2.如果只给了文件名,那么会在当前路径下创建。
  • 参数:flags
    open的第二个参数表示以什么样的方式打开或创建文件,常见的打开方式如下:
    在这里插入图片描述
    1.O_RDONLY:只读打开
    2.O_WRONLY:只写打开
    3.O_RDWR:可读可写
    4.O_CREAT:文件不存在,则创建。需要使用mode选项,指明新文件的访问权限
    5.O_APPEND:追加写,即打开时将文件流指针移动到文件末尾为止。

这些选项前三个只可以选择一个,其余的俩个可以与前三个选项通过“|”进行组合。举个例子:我们想要以只写的方式打开一个文件如果文件不存在就创建O_WRONLY|O_CREAT

而这些选项实际上是一个32位大小的数据,这些选项中32位只有1位为1,这一位称为标志位,其余均为0;这样就可以通过“|”来组合选项。(与单片机操作寄存器的方式相同)

我们使用vim打开/usr/include/asm-generic/fcntl.h这个目录下的文件看一看:在这里插入图片描述

  • 参数;mode
    open的第三个参数表示创建文件时新文件的默认权限在这里插入图片描述结果如下:
    在这里插入图片描述
    发现我们设置的权限为110 110 110,而实际上全是110 110 100;这是因为umask的值影响了最终权限,所以要先设置umask的值,通过umask()来设置。
    在这里插入图片描述
    在这里插入图片描述
    ·
  • 返回值:返回文件标志符

2. read函数

系统接口中使用read函数从文件读取信息,read函数的函数原型如下:
在这里插入图片描述

  • 参数:fd
    read的第一个参数表示要读文件的对应文件描述符
  • 参数:buf
    read的第二个参数表示将读取的数据存放在buf指向的空间中。
  • 参数:count
    read的第三个参数表示最大可以读多少字节的数据
  • 返回值:表示实际读取到的字节数,读取失败则返回 -1。

3. write函数

系统接口中使用write函数向文件写入相关信息,write函数的函数原型如下:在这里插入图片描述

  • 参数:fd
    write的第一个参数表示对应文件的文件描述符。
  • 参数:buf
    write的第二个参数表示指向要向文件中写入数据中空间。
  • 参数:count
    第三个参数表示最大写多少个字节(写入字符串不需要包含“\0”)。
  • 返回值:实际写入的字节数。

在这里插入图片描述

4. close函数

系统中使用close关闭一个文件。对应函数原型

#inlcude <unistd.h>

int close(int fd);

关闭文件只需要将对应的文件描述符传入即可,如果关闭文件成功则返回0失败返回-1


三、stdout&&stderr&&stdin

1. 理解什么是文件?

从用户的角度看,文件可分为普通文件和设备文件两种

普通文件是指驻留在磁盘或其它外部介质上的一个有序数据集,可以是源文件、目标文件、可执行程序; 也可以是一组待输入处理的原始数据,或者是一组输出的结果。对于源文件、目标文件、 可执行程序可以称作程序文件,对输入输出数据可称作数据文件。

设备文件是指与主机相联的各种外部设备,如显示器、打印机、键盘等。在操作系统中,把外部设备也看作是一个文件来进行管理,把它们的输入、输出等同于对磁盘文件的读和写。 通常把显示器定义为标准输出文件,一般情况下在屏幕上显示有关信息就是向标准输出文件输出。如前面经常使用的printf,putchar 函数就是这类输出。键盘通常被指定标准的输入文件, 从键盘上输入就意味着从标准输入文件上输入数据。scanf,getchar函数就属于这类输入。

2. 进程启动时默认打开的三个流

当一个c语言程序运行起来时,会默认打开三个流即stdout(标准输出流),stdin(标准输入流)以及stderr(标准错误流)。这3个可以称为终端(Terminal)的标准输入(standard input),标准输出( standard out)和标准错误输出(standard error)当linux开始执行程序的时候,程序默认会打开这3个文件流,这样就可以对终端进行输入输出操作。其对应的设备分别为:显示器,键盘,显示器。下面我们通过man手册查看一下:

#inlcude <stdio.h>

extern FILE* stdin;
extern FILE* stdout;
extern FILE* stderr;

我们来查看一个进程运行起来时它所打开的文件:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
我们查看的fd目录下出现的数字被称为文件描述符,每个文件描述都对应一个文件。其中0、1、2文件描述符对应的分别是stdin,stdout,stderr,他们所对应的终端是相同的,对该终端进行输入输出。其中文件描述符3对应的是新打开的文件。

于是我们就可以改变该进程中文件信息(struct FILE结构体中包含的信息)中文件描述符号对应的文件,从而实现重定向。(后面详细解释)

3. stdout和stderr的区别

printf()其实就是向stdout中输出,等同于fprintf(stdout,“*”);
perror()其实就是向stderr中输出,相当于fprintf(stderr,“ *”);
那到底stdout,和stderr有什么区别和作用呢?

我们在写程序时用printf()是为了我们能监控我们的程序运行状况,或者是说debug,如果我们的程序是一直运行,不停下来,我们不可能时刻盯着屏幕去看程序输出,这时我们就可以用文件重定向。将输出到一文件中,我们以后就可以看这文件就行。

举例:

#include<stdio.h>  
int main()  
{  
     printf("Stdout Helo World!!\n");  
     fprintf(stdout,"Stdout Hello World!!\n");  
     perror("Stderr Hello World!!\n");  
     fprintf(stderr,"Stderr Hello World!!\n");  
       
     return 0;  
} 

编译过后,我们./test > test.txt(默认是将stdout里的内容重定向到文件中),这样就把test程序输出的内容输出到test.txt文件中。还有一种更明晰的写法./test 1>test.txt,这里的1就代表stdout。说到这你应该知道stderr该怎样处理了。

在这里插入图片描述

编译过后,./test,屏幕上是四条输出,如果./test > test.txt ,结果是屏幕上输出两条Stderr Hello World!!,Stdout Helo World!!在文件test.txt中,基于上面说的很容易理解现在的结果,于是我们可以随便处理我们想要的输出,例如:

./test 1>stdout.txt 2>stderr.txt,我们将stdout输出到文件stdout.txt中,将stderr输出到stderr.txt文件中;
./test 1>stdout.txt,将stdout输出到文件stdout.txt 中,stderr输出到屏幕上;
./test 2>stderr.txt,将stderr输出到文件stderr.txt中,stdout输出到屏幕上;
./test > test.txt 2>&1,这是将stdout和stderr重定向到同一文件test.txt文件中

并且:stderr,和stdout还有重要一点区别,stderr是没有缓冲的,它立即输出,而stdout默认是行缓冲,也就是它遇到‘\n’,才向外输出内容,如果你想stdout也实时输出内容,那就在输出语句后加上fflush(stdout),这样就能达到实时输出的效果。(缓冲区后面详解)

PS:凡是显示到显示器上的内容都是字符,凡是从键盘读取的内容都是字符,所以键盘和显示器一般被称为字符设备。格式化输入输出就是将字符转换为其它类型或者其它类型转换为字符。
在这里插入图片描述

4. Linux下一切皆文件的理解

我们所用的一些外设,键盘鼠标显示器等,他们的文件输入输出打开等的操作的实现源码肯定是不同的,比如进程默认打开的三个文件 0 1 2 号文件,他们的open write 等的方法肯定是不同的,而我们打开一个文件,也就是创建一个文件的file结构体,那么不同的设备对文件的操作也该存到这个结构体内部,但是C语言的结构体内只能存变量,不能存函数,所以在结构体file当中有一个file_operations(文件操作)结构体,用来指向硬件提供的一些底层的驱动代码,用来实现对不同外设的文件读写等的操作,这样一来一种结构体的不同的结构体对象就能保存不同外设的文件操作驱动代码。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


四、文件描述符

通过刚刚对stdout&&stderr&&stdin的讨论,我们可以知道存在下面这样的语法:

echo log > /dev/null 2>&1

表示将输出结果重定向到哪里,例如:echo “123” > /home/123.txt
/dev/null :表示空设备文件
所以 echo log > /dev/null 表示把日志输出到空文件设备,也就是将打印信息丢弃掉,屏幕上什么也不显示。

1 :表示stdout标准输出
2 :表示stderr标准错误
& :表示等同于的意思

所以 2>&1 表示2的输出重定向等同于1,也就是标准错误输出重定向到标准输出。因为前面标准输出已经重定向到了空设备文件,所以标准错误输出也重定向到空设备文件。

这个用法平时很常见,重点是为什么这里是用 2 和 1 ,不是3456等等,这要从 Linux 中的文件描述符说起。

1. 理解Linux中的文件描述符

(1)OS系统如何管理文件

文件是由进程打开,而一个进程是可以打开多个文件。系统中也存在着大量的进程那么也就意味着系统中任何时刻都可能存在大量的进程。而我们打开一个文件,需要将文件的相关属性加载到内存当中,操作系统是做管理工作的软件。那么OS系统需要对应这些数据进行管理如何进行管理为了管理打开的文件,操作系统会给每个打开的文件创建一个结构体struct_file,并以某种数据结构的方式将其组织起来。OS的打开文件的管理也就变成了对数据结构的增删查改等操作。

(2)进程和文件如何进行关联

那么进程怎么知道,那些文件是我打开的了?为了区分文件是那个进程打开的,还需要建立进程和文件之间的对应关系。我们在学习进程的时候,当我们的程序跑起来会将对应的代码和数据加载到内存,并为之创建相关的数据结构(task_struct ,mm_struct,页表)。并通过页表建立虚拟地址和物理地址之间的映射关系。

在这里插入图片描述

而为了管理该进程打开的文件,task_struct 有一个指针指向了一下结构体,这个结构体叫做files_struct,结构体里面有一个数组fd_array,而这个数组的下标就是我们说的文件描述符

我么首先在 linux-5.6.18\include\linux\sched.h 头文件中找到task_struct结构体,在task_struct结构体中可以找到这样一个结构体指针,它指向的是另一个结构体 files_struct 。

在这里插入图片描述

里面存放着打开文件的相关属性,文件锁等,都是用来管理文件的,也就是管理file结构体。在末尾有一个数组fd_array[],这个数组是一个结构体指针数组,数组内存放的就是要管理的文件描述结构体也就是file结构体的指针,通过访问这个数组中的地址信息,进程就能对文件进行管理。数组的大小是一个宏定义,转到定义,我们发现这个宏的值是32,也就是通过这个数组,我们可以访问最多32个文件

在这里插入图片描述

当进程打开Test.py文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置(0、1、2的位置已经被占用),使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。

在这里插入图片描述

在这里插入图片描述

02283470650d5197249.png)

(3)重新理解系统调用接口

  • read:进程通过文件描述符和进程的PCB结构体,先找到fd数组,然后通过文件描述符找到对应的struct file,然后就可以找到磁盘中文件的位置,然后将文件中的数据加载到内存中的缓冲区中,将数据拷贝给进程就完成了读取。
  • write:找到文件后,将数据写入文件对应的缓冲区中,然后在适当的时候将缓冲区数据刷新到文件中。

以上我们讨论的文件描述符表是每一个进程都有的,是进程级的文件描述符表,然后我们还会学习文件系统级别的i-node表。

2. 进一步理解文件FILE结构体和文件描述符

在"stdio.h"头文件中的搜索"FILE",查看结果如下:
在这里插入图片描述
从这里可以看出,文件流指针FILE本质上就是_IO_FILE,即C标准库中的一个结构体;再查找"struct _IO_FILE"结构体的定义:
在这里插入图片描述

最终在libio.h头文件中找到_IO_FILE结构体的定义:

在这里插入图片描述
这里"_IO_FILE"结构体的成员变量"_fileno"保存的正是文件描述符的数值;通过程序也可以验证这一点在这里插入图片描述

用一句话概括文件流指针与文件描述符的关系就是:文件流指针指向的结构体_IO_FILE内部的成员变量_fileno保存了文件描述符的数值。

知道了FILE结构体和文件描述符的关系后,现在我们来理解一下C的文件接口究竟是如何完成工作的?

以fopen()为例:
(1)给调用者申请struct FILE结构体变量,并返回首地址(FILE*)。
(2)进入系统内核,执行系统调用接口,通过open()打开文件,并返回fd,将fd填充进FILE变量中的fileno。

3. 文件描述符的分配规则

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{             
    close(0);
    close(2); 
                                                                                                                                  
    int fd1=open("./log1.txt",O_WRONLY|O_CREAT,0664);
    int fd2=open("./log2.txt",O_WRONLY|O_CREAT,0664);
    int fd3=open("./log3.txt",O_WRONLY|O_CREAT,0664);
    int fd4=open("./log4.txt",O_WRONLY|O_CREAT,0664);
    printf("%d\n",fd1);
    printf("%d\n",fd2);
    printf("%d\n",fd3);
    printf("%d\n",fd4);
    
    close(fd1);
    close(fd2);
    close(fd3);close(fd4);
    
    return 0;
}

运行结果如下:
在这里插入图片描述
我们发现0和2也被用起来了。现在我们就明白了文件描述符的分配规则是从最小的未被使用的下标开始的。

4. 文件描述符及重定向

(1)输出重定向原理

从文件描述符的分配规则我们可以将文件描述符1分配给新打开的文件,此时使用printf()函数和fput()函数会出现什么现象呢?该如何解释?
在这里插入图片描述

运行结果如下:
在这里插入图片描述

  1. 没有关闭文件描述符1对应的文件时,printf()和fput( #,stdout)就会向显示器上打印;因为未关闭该文件时,printf()底层的实现就是向文件描述符1对应文件(显示器)中打印的(通过write封装),而对于fput( #,stdout)会向stdout对应的文件中打印,stdout指向的FILE中的文件描述符默认就是1,所以会向stdout对应文件(显示器)中打印
  2. 关闭文件描述符1对应的文件的话,根据文件描述符的分配规则新打开的文件就会占用1的位置,从而新文件的文件描述符是1printf固定向文件描述符1的文件中写入数据;而stdout指向的FILE中的文件描述符始终是1,所以向stdout中写入数据就变成了向新文件中写入数据。
  3. 这就是重定向;下图可以帮助理解。
    在这里插入图片描述

(2)输入重定向

输入重定向就是,将我们本应该从一个键盘上读取数据,现在重定向为从另一个文件读取数据。
在这里插入图片描述

我们的scanf函数是从标准输入读取数据,现在我们让它从log1.txt当中读取数据,我们在scanf读取数据之前close(0)。这样键盘文件就被关闭,这样一样log1.txt的文件描述符就是0。运行结果如下:
在这里插入图片描述

举例:
在这里插入图片描述

(3)追加重定向

追加重定向和输出重定向的区别是追加重定向不是覆盖数据。
原理其实就只比输出重定向多了一个O_APPEND选项。
在这里插入图片描述

(4)使用dup和dup2系统调用完成重定向

A. dup函数

1、 dup函数
头文件及函数定义:

#include <unistd.h>
int dup(int oldfd);

dup用来复制参数oldfd所指的文件描述符。当复制成功是,返回最小的尚未被使用过的文件描述符,若有错误则返回-1。

代码示例:
在这里插入图片描述
在这里插入图片描述

B. dup2函数

头文件及其定义:

 #include <unistd.h>
 int dup2(int oldfd, int newfd);

dup2与dup区别是dup2可以用参数newfd指定新文件描述符的数值。若参数newfd已经被程序使用,则系统就会将newfd所指的文件关闭,若newfd等于oldfd,则返回newfd,而不关闭newfd所指的文件。

返回值:
若dup2调用成功则返回新的文件描述符,出错则返回-1。

代码示例:
在这里插入图片描述
在这里插入图片描述

(5)总结

在这里插入图片描述


五、Linux文件缓冲区

1. 缓冲区机制

根据应用程序对文件的访问方式,即是否存在缓冲区,对文件的访问可以分为带缓冲区的操作和非缓冲区的文件操作

  1. 带缓冲区文件操作:高级标准文件I/O操作,将会在用户空间中自动为正在使用的文件开辟内存缓冲区。
  2. 非缓冲区文件操作:低级文件I/O操作,读写文件时,不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),当然用于可以在自己的程序中为每个文件设定缓冲区。

两种文件操作的解释和比较

1、非缓冲的文件操作访问方式,每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响

2、ANSI标准C库函数 是建立在底层的系统调用之上,即C函数库文件访问函数的实现中使用了低级文件I/O系统调用,ANSI标准C库中的文件处理函数为了减少使用系统调用的次数,提高效率,采用缓冲机制,这样,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,即需要少量的CPU状态切换,提高了效率

2. 缓冲类型

标准I/O提供了3种类型的缓冲区。

(1)全缓冲

全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。第一次执行I/O操作时,ANSI标准的文件管理函数通过调用malloc函数获得需要使用的缓冲区,默认大小为8192。

//come from /usr/include/stdio.h
 /* Default buffer size. */
#ifndef BUFSIZ
#define BUFSIZ _IO_BUFSIZ        //BUFSIZ 全局宏定义
#endif
//come from /usr/include/libio.h
#define _IO_BUFSIZ _G_BUFSIZ
//come from /usr/include/_g_config.h
#define _G_BUFSIZ 8192        //真实大小

(2)行缓冲

行缓冲区:在这种情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。

(3)无缓冲

无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

注:①标准输入和标准输出设备:当且仅当不涉及交互作用设备时,标准输入流和标准输出流才是全缓冲的。②标准错误输出设备:标准出错绝不会是全缓冲方式的。

对于任何一个给定的流,可以调用setbuf()和setvbuf()函数更改其缓冲区类型。

3. 修改缓冲区类型

(1) setbuf

在这里插入图片描述

此函数第一个参数为要操作的流对象,第二个参数buf 必须指向一个长度BUFSIZ 的缓冲区。如果将buf 设置为NULL,则关闭缓冲区。如果执行成功,将返回0,否则返回非0 值。

(2)setvbuf

在这里插入图片描述

此函数第一个参数为要操作的流对象;第二个参数buf 必须指向一个长为BUFSIZ 的缓冲区;第三个参数为缓冲区类型,分别定义如下:

//come from /usr/include/stdio.h
/* The possibilities for the third argument to 'setvbuf'. */
#define _IOFBF 0 /* Fully buffered.*/        //全缓冲
#define _IOLBF 1 /* Line buffered. */        //行缓冲
#define _IONBF 2 /* No buffering. */        //无缓冲

第四个参数为该buf的大小。如果指定一个不带缓冲区的流,则忽略buf和size参数。

如果指定全缓冲区或行缓冲区,则buf 和size 可选择地指定一个缓冲区及其长度。如果出现指定该流是带缓冲区的,而buf 是NULL,则标准I/O 库将自动为该流分配适当长度的缓冲,适当长度指的即是由文件属性数据结构(struct stat)的成员st_blksize 所指定的值,如果系统不能为该流决定此值(例如若此流涉及一个设备或一个管道),则分配长度BUFSIZ 的缓冲区。

此函数如果执行成功,将返回0,否则返回非0 值。

4. 重定向与缓冲方式

要知道Linux下对于向显示器写入采用的是行刷新策略,而写入其它文件时采用的是全刷新策略,为什么要这样设置呢?
在这里插入图片描述
因此我们也容易知道:重定向会改变缓冲区的刷新策略。比如说输出重定向,将原来的输出到显示器上策略是行缓冲,现在要将其输出到文件当中采用策略的是全缓冲:下面我们来看一个例子:

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

我们发现为什么只有系统调用fwrite只打印了一次,而printf和fwrite都打印了两次了?这是为什么?
在这里插入图片描述

综上: printf、fwrite 库函数会自带缓冲区而 write 系统调用没有带缓冲区。另外,这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。(刷新数据时,先将数据刷新到内核缓冲区,再由内核刷新到外设)
那这个缓冲区谁提供呢? printf、fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

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


六、文件系统

1. 磁盘存储设备的基本概念

磁盘设备是一种相当复杂的机电设备。 磁盘设备可以包括一个或多个物理盘片,每个磁盘片分一个或两个存储面(如图(a)所示)。每个磁盘面被组织成若干个同心环,这种环称为磁道track,各磁道之间留有必要的间隙。每条磁道又被逻辑上划分成若干个扇区sectors。在不同扇区之间又保留必要的间隔,图(b)中显示了显示了一个有3个磁道,每个磁道又被分成8 个扇区的磁盘片的一个存储面

在这里插入图片描述

我们所划分出来的这些扇区就是磁盘的最小物理存储单位,同一个同心圆的扇区组合成的园就是磁道(track);由于磁盘里面会有多个碟片,因此在所以碟片上面的同一个磁道可用组合成柱面。

2. 存取信息的最小单位

  1. 从应用程序包括用户界面的角度来看,存取信息的最小单位是Byte(字节)。
  2. 从磁盘的物理结构来看存取信息的最小单位是扇区,一个扇区是512字节。
  3. 从操作系统对硬盘的存取管理来看,存取信息的最小单位是簇,簇是一个逻辑概念,一个簇可以是2、4、8、16、32或64个连续的扇区。一个簇只能被一个文件占用,哪怕是只有1个字节的文件,在磁盘上存储时也要占用一个簇,这个簇里剩下的扇区是无用的。例如用NTFS文件系统格式化的时候默认是8个扇区组成一个簇,即4096字节。所以你如果保存了一个只有1字节的文件,它在磁盘上实际也要占用4096字节(4KB),所以“簇”也可以理解为操作系统存取信息的最小单位。

在OS系统中,信息一般以扇区(sectors)的形式存储在硬盘上,而每个扇区包括512个字节的数据和信息(即一个扇区包括两个主要部分:存储数据地点的标识符和存储数据的数据段)。操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个块(blocks)这种由多个扇区组成的”块”,是文件存取的最小单位”块”的大小,最常见的是4KB,即连续八个 sectors组成一个 blocks。

3. 磁盘的分区与格式化

分区:一块磁盘是比较大的,它有许多柱面,OS系统为了管理它将的每一部分柱面作为一个分区。比如磁盘共有400个柱面,可用划分为4个分区,每个分区100个柱面(通常分区的“最小单位”是柱面);在windows系统下就是将磁盘分为C盘,D盘等等。并且分区会使得数据集中,有助于数据读取的速度与性能。

格式化:将管理信息填入一些管理信息,方便管理,以成为OS系统可以利用的文件系统格式(不同的文件系统写入的管理信息是不同的)

4. 文件属性和文件内容

使用ls -l可以得到如下数据:
在这里插入图片描述
每行包含7列:(1)模式(2)硬链接数(3)文件所有者
(4)组(5)大小(6)最后修改时间(7)文件名

我们还可以使用stat命令来获取更多的信息
在这里插入图片描述
这里便可以看到inode信息主要包括:

(1)文件的字节数,块数 (2)文件拥有者的User ID
(3)文件的Group ID (4)文件的读、写、执行权限
(5)文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
(6)链接数,即有多少文件名指向这个inode
(7)文件数据block的位置
(8)inode编号

文件除了文件的实际内容还有许多的文件属性,在Linux下就是文件的inode号,权限,大小,拥有者/所属组,时间参数,软硬链接等等。文件系统通常会将这俩部分数据分别存放在不同的块,文件属性放到inode中,实际数据则放在数据块中。

5. 文件系统

为了解释文件属性信息中的inode,我们需要先了解一下文件系统。

在这里插入图片描述
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的块组。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,

  • Block Group:ext2文件系统会根据分区的大小划分为数Block Group。而每个Block Group都有着相同的结构组成。
  • 超级块(Super Block):存放文件系统本身整体的结构信息。记录的信息主要有:(1)bolck 和 inode的总量,未使用的block和inode的数量。(2)一个block和inode的大小。(3)最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息。
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
  • inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
  • inode表:记录文件属性,一个文件占用一个inode,存放文件属性 如 文件大小,所有者,最近修改时间等信息,同时记录此文件数据所在的数据区块号码。
  • 数据区块:记录文件的内容,若文件太大,会同时占用数据区中的多个数据块。

(1)Super_Block

超级块非常重要,文件系统的基本信息都储存在这里,因此超级块损坏,系统就会奔溃。此外,并不是每一个块组都有超级块,事实上除了第一个块组内含有超级块之外,后续的块组中也可能含有超级块,后续的超级块主要是为第一个块组中的超级块做备份,这样当超级块损坏时可以快速恢复。

(2)Data_Blocks

数块是用来存放文件数据的地方,在EXT2文件系统下所支持的区块大小有1K,2K和4K共三种。区块的大小在格式化的时候就确定了;并且每个区块都有编号,以方便inode进行记录(inode记录一个区块需要4B)。

注意:每一个区块最多只能放置一个文件的数据,如果文件大于数据块的大小,则一个文件会占用多个数据块,如果文小于数据块,则该块的剩余容量就不能够再次被使用了(磁盘空间会被浪费)。

(3)inode_table

inode表中有许多inode,inode的数量和大小在格式化的时候就已经固定了,inode大小固定为128B(EXT2系统下);没一个文件都只会占用一个inode,所以文件系统能够建立的文件数量与inode数量有关。系统读取文件的时候需要先找到inode,并分析inode所记录的权限与用户要求的是否符合,符合才可以读取内容。

一个inode的大小为128B,而储存一个数据块好吗需要4B,假设一个文件400MB每个数据块4KB,那么有十万个数据块需要被记录,一个128B大小多空间无法记录这么多数据块;为此inode记录区块号码的区域被定义为12个直接,一个间接,一个双重间接,一个三重间接。

在这里插入图片描述

inode有12个直接指向数据块,而一个间接就可以找到一个数据块,而这个数据块被当作记录数据区号码记录区;双重三重间接同理。

这样一个inode就可以指向许多数据块,数据块的大小为16GB(数据块大小为1K时:12K + 256K + 256^2 K+ 256^3K = 16GB)

此时我们知道文件系统将数据块格式化为1K时,能容纳最大文件为16GB。

(4)理解文件的创建与写入

在这里插入图片描述
在这里插入图片描述
而删除一个文件时,只需要在俩个位图中的对应位置置为“未使用”即可。


五、目录文件

目录也是一种文件,它也有读写可执行三个权限。打开目录,实际上就是打开目录文件。 所以创建一个目录和创建普通文件时,文件系统都是进行相同的操作:文件系统分配一个inode与至少一个数据块给该目录文件。其中inode记录该目录的相关权限与属性,还记录对应的数据块号码;而数据块记录了这个目录下的文件名与该文件名对应的inode号码。使用ls -i可以得到目录内容:

在这里插入图片描述

inode本身并不会记录文件名称,文件名是由该文件所在的目录的数据块记录。因此目录下文件的新增,删除,修改文件名都与目录的r和w权限有关。由于目录文件内只有文件名和inode号码,所以如果只有读权限,只能获取文件名,无法获取其他信息,这主要是因为其他信息都储存在文件的inode中,而读取inode内的信息需要目录文件的执行权限(x)。

在这里插入图片描述

1. 重新在目录创建文件

新增文件,首先申请inode,将文件信息填入inode,然后为文件分配数据块,建立inode和数据块的映射关系,将数据块号码填入inode,然后将文件名和对应的inode号填写懂啊当前的目录的数据块中,在当前目录下添加映射关系,就完成了对文件的添加。

2. ls,cat操作该如何理解

在这里插入图片描述

3. 软硬链接

创建命令:

ln 【选项】 原文件 链接文件
-s 创建符号链接(软连接),如果不带这个参数,就是创建硬链接
-f 强制创建文件或目录的链接
-i 覆盖前先询问
-v 显示创建链接的过程

  1. 创建一个软链接
    ln -s test.txt test1.txt
  2. 创建一个硬链接
    ln test.txt test2.txt

(1)引例

我们来建立三个硬链接,俩个软链接:
在这里插入图片描述

我们仔细观察一下发现,test1.txt,test2.txt,test3.txt拥有一样的inode结点(显示结果的第一列),甚至于连权限属性都一模一样。而test4.txt,test5.txt拥有另外一个独立的inode。我们在前面曾经说过,每一个i结点对应一个实际的文件。所以,我们可以发现,建立的硬链接实际上跟我们的源文件是一样的。而软链接则是重新建立了一个独立的文件。

事实上,硬链接的本质就是在该目录下新创建一条新文件名和旧inode号的映射记录而已

另外,我们观察一下这几个文件的大小,由于我们的源文件是空文件,所以大小是0。那为什么两个硬链接也是0?而软链接却是9呢?

因为硬链接关联着我们的源文件,所以源文件的大小是多大,它们就是多大。至于软链接的大小为什么是9,大家观察一下软链接指向的源文件名的长度,就是9。我们的软链接会写上链接文件的文件名。一个字母一个字节,所以是9个字节,所以软链接的大小是9。

现在我们向test1.txt文件中写入数据,现象如下:
在这里插入图片描述

此时我们删除test1.txt观察现象:
在这里插入图片描述

当我们删除了源文件之后,发现硬链接还能正常显示原本的内容,而软链接则提示文件不存在。

因为软链接是建立了另一个新的独立的文件,它指向源文件,因为源文件没了,所以它就不能正常指向了;而硬链接实际是一条文件名与i结点的记录。所以,在删除源文件的时候,系统则将链接数减1,当链接数为0的时候,inode就会被系统回收,文件的内容才会被删除。

(2)软硬链接基本概念

  1. 软链接软连接创建的是一个独立的文件,有独立的inode。指的是指向原始文件的实际链接,有点类似于 Windows 的快捷方式。在软链接中存放的不是具体的文件数据,而是所链接的原始文件的路径名,当打开软链接的时候,则会根据这个路径去找到并打开所链接的原文件。编辑软链接的文本就是在编辑软链接所链接的原文件的文本,如果所链接的原文件被删除了,这个软链接就找不到当初所链接的原文件,就成了无效的链接,这时再次打开软链接就会提示将创建一个新的原文件。删除软链接不会影响到所链接的原文件。
  2. 硬链接本质没有创建文件,只是建立了一个文件名和已有inode的映射关系,并写入当前目录。由于硬链接文件与原始文件的inode是相同的,即一个 inode 节点对应两个不同的文件名,两个文件名指向同一个文件,所以不管是编辑硬链接文件还是原文件,保存之后两者显示的文本内容都是相同的,但是删除其中任何一个都不会影响另外一个的访问,因此只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。

(3)软链接与硬链接的特征

软链接:

  1. 每个软链接都拥有自己的inode节点和block块。
  2. 软链接不保存实际的数据,只保存原始文件的路径。通过软链接打开或者编辑文件其实操作的就是所链接的原文件。
  3. 不管是通过编辑原文件还是通过其软链接去编辑文件,该原文件下的其他软链接打开的文件都随之改变。软连接的文件类型标志为:l,软链接的权限都为rwxrwxrwx。删除原文件,其对应的软链接都不能使用,将会称为无效链接。
  4. 删除其中任一的软链接文件,原文件以及该原文件下的其软链接文件依然可用。

硬链接:

  1. 同一原文件创建的多个硬链接inode节点和block块都与原文件一样,可以看做同一个文件,只是文件名不同。
  2. 编辑其中的任一文件,其它文件都会随之改变。删除其中任一的文件,都会不影响到其他文件,除非把相关联的所有文件都删除,那么这个文件才会被删除。

(4)硬链接在OS系统中的应用

在这里插入图片描述
当该目录下再创建一个目录,则该目录的链接数+1

在这里插入图片描述
一个目录下的“.”文件就是该目录的一个硬链接,而“…”文件则链接了该目录的上一级目录。

所以我们可以通过目录的硬链接数判断该目录下有几个目录(硬链接数 - 2)。


六、动静态库

1. 库文件

库文件的本质就是一堆". o"文件的集合(也就是可重定向二进制目标文件)每个目标文件存储的代码,并非完整的程序,而是一个个实用的功能模块

例如,C 语言库文件提供有大量的函数(如 scanf()、printf()、strlen() 等),C++ 库文件不仅提供有使用的函数,还有大量事先设计好的类。库文件的产生,极大的提高了程序员的开发效率。因为很多功能根本不需要从 0 开发,直接调取包含该功能的库文件即可。

调用库文件为什么还要牵扯到头文件呢?首先,头文件和库文件并不是一码事,它们最大的区别在于:头文件只存储变量、函数或者类等这些功能模块的声明部分,库文件才负责存储各模块具体的实现部分。即所有的库文件都提供有相应的头文件作为调用它的接口,库文件是无法直接使用的,只能通过头文件间接调用;所以我们在打包库文件的时候,即打包了库的头文件(库的使用说明书)也打包了一份库文件(库的实现)

头文件和库文件相结合的访问机制,最大的好处在于,有时候我们只想让别人使用自己实现的功能,并不想公开实现功能的源码,就可以将其制作为库文件,这样用户获取到的是二进制文件,而头文件又只包含声明部分,这样就实现了“将源码隐藏起来”的目的,且不会影响用户使用。

2. 动态链接库与静态链接库

(1)动静态库的基本原理

C或C++程序从源文件到生成可执行文件需经历 4 个阶段
分别为预处理、编译、汇编和链接。
链接阶段所要完成的工作,是将同一项目中各源文件生成的目标文件和程序中用到的库文件整合为一个可执行文件。

虽然库文件明确用于链接,但编译器提供了2种实现链接的方式,分别称为静态链接和动态链接。
采用静态链接方式实现链接操作形成的库文件称为静态链接库;
采用动态链接方式实现链接操作形成的库文件称为动态链接库。

(2)动静态库的区别

在 Linux 发行版系统中,静态链接库文件的后缀名通常用 .a 表示,动态链接库的后缀名通常用 .so 表示;
在 Windows 系统中,静态链接库文件的后缀名为 .lib,动态链接库的后缀名为 .dll。

静态库的扩展名:libxxx.a
动态库的扩展名:libxxx.so

其中的xxx表示库的名称。

下面我们来观察一下这两种方式生成的可执行文件的大小,并使用file命令查看俩种文件的属性:

在这里插入图片描述

通过ldd查看可以执行程序依懒的库(注意动态链接生成的可执行程序才有依赖库,静态链接生成的可执行程序是没有依赖库的,静态链接会将库文件的代码拷贝一份到可执行文件中。

在这里插入图片描述

(3)静态链接库

  • 编译操作:静态链接库在编译的时候,程序文件中哪里用到了库文件中的功能模块,GCC 编译器就会将该模板代码直接复制到程序文件的适当位置,生成可执行文件,所以利用静态库编译形成的文件会比较大。
  • 优势使用静态库文件的优势是可移植性强,可独立运行,即生成的可执行文件不再需要任何静态库文件的支持就可以独立运行。
  • 劣势劣势是代码冗余、可执行文件的体积大:如果程序文件中多次调用库中的同一功能模块,则该模块代码就会被复制多次,造成代码的冗余。和使用动态链接库生成的可执行文件相比,静态链接库生成的可执行文件的体积更大。
  • 库升级难度:由于函数库时直接整合到可执行文件中,所以函数库升级时,可执行文件必须需要重新编译才可以将升级后的库整合到程序中。

(4)动态链接库

  • 编译操作:动态链接就是在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去执行共享内存中已经加载的动态库可执行代码(不同进程执行相同代码时就不需要重复加载了),最终达到运行时连接的目的。
  • 优势:采用动态链接库的优势是生成的执行文件文件体积小,因为可执行文件中记录的是功能模块的地址,真正的实现代码会在程序运行时被载入内存,即便功能模块被调用多次,使用的都是同一份实现代码(这也是将动态链接库称为共享链接库的原因)。
  • 劣势:这样生成的可执行文件是无法独立运行的。并且由于库是在程序执行的时候才加载到内存中,所以会影响到程序初期的速度。
  • 库升级难度:这类可执行文件具有指向性功能,所以函数库升级后,指向文件不需要进行重新编译,因为执行文件会指向新的函数库文件(新旧文件名相同)

3. 静态链接库的创建与使用

(1)静态库的创建与发布

制作步骤

  1. 生成对应的.o二进制文件 .
  2. 将生成的.o文件打包,使用ar rcs + 静态库的名字(libMytest.a) + 生成的所有的.o

首先为了掩饰打包静态库的过程,我创建了几个文件:
在这里插入图片描述
在这里插入图片描述
ar 工具对形成的.o文件进行打包(打包的时侯带上 -rc 选项)其中 r 和c分别是replace和creat。在这里我们将库名设置为 mylib。
在这里插入图片描述

这样就形成了一个静态库:libcal.a
使用静态库,需要包含头文件;如果我们要给别人使用我们写的静态库,就需要将库文件和头文件一起打包:

利用make指令一键打包和make output发布:
将头文件放在mathlib/include中,库文件放在mathlib/lib中

在这里插入图片描述

  • include文件夹:存放头文件,提供给用户调用的接口API
  • lib文件夹:存放库文件,即:生成的静态库、动态库
  • src文件夹:存放源文件

执行make和make output:
在这里插入图片描述
现在我们以及打包成功了.现在我们就可以交给别人使用了。

(2)静态库的使用

下面我们将这个打包好的静态库放到一个目录下进行测试:

对应测试test.c内容:在这里插入图片描述

对应makefile:
在这里插入图片描述
执行结果:

在这里插入图片描述
我们发现出错了。我们在使用gcc采用静态链接时,我们需要告诉编译器库文件所在路径,头文件所在路径和你要链接那个库而这三个操作所对应的选项分别为:

参数说明:

  • -I参数:指定头文所在的文件夹名,文件夹名可以和参数贴着写在一起
  • -L参数:指定静态库的文件夹名
  • -l参数:指定静态库的名字,但名字要掐头去尾,eg:原静态库名字为libcal,在指定-l参数值的时候为:-l cal

修改Makefile:
在这里插入图片描述

执行结果:
在这里插入图片描述

4. 动态库的创建和使用

(1)动态库的创建与发布

制作步骤:

  1. 生成与位置无关的代码 (生成与位置无关的.o)
  2. 将.o打包成共享库(动态库)
  3. 发布和使用共享库:

注意:静态库生成的.o文件是和位置有关的用gcc生成和位置无关的.o文件,需要使用参数-fPIC(常用) 或 -fpic

在了解什么叫生成和位置无关的.o文件,我们就要联系之前学过的虚拟地址空间:

linux上打开一个运行的程序(进程),操作系统就会为其分配一个(针对32位操作系统)0-4G的地址空间(虚拟地址空间),虚拟地址空间不是在内存中。静态库生成与位置有关的二进制文件(.o文件)虚拟地址空间是从0开始的,生成的二进制文件(.o文件)会被放到代码段即.text代码区。生成的.o代码每次都被放到同一个位置,是因为使用的是绝对地址。动态库生成与位置无关的二进制文件(.o文件)动态库 / 共享库 在程序打包的时候并不会把.o文件打包到可执行文件中,只是做了一个记录,当程序运行之后才去把动态库加载到程序中,也就是加载到共享库空间,但是每次加载到共享库空间的位置可能不同。

在这里插入图片描述

(2)动态库的使用

参数说明:

  1. -PIC:生成和位置无关的.o文件
  2. -shared:共享,就是把.o文件,打包成动态库 / 共享库
  3. 上面就已经完成动态库的制作,然后把下面的两个文件发布给用户即可调用
  4. include/head.h: 头文件,定义接口API
  5. lib/libcal.so:动态库,封装了编译之后的源代码二进制文件

把mathlib放在另一个目录下进行测试:
在这里插入图片描述
在这里插入图片描述

发现在make的时候没有问题,但是在执行可执行程序的时候出现了找不到动态库的问题。这是因为我们使用makefile编译的时候只告知编译器头文件库路径在哪里,当程序编译好的时候,此时已经和编译无关了!加载器在运行的时候,需要进一步告知OS系统,库在哪里。

解决方法:

  1. 拷贝.so文件到系统共享库路径下, 一般指/usr/lib
  2. 更改 LD_LIBRARY_PATH: 导入环境变量LD_LIBRARY_PATH;
    export LD_LIBRARY_PATH=/home/ksy/BK/Test/mylib/lib/。这样之后我们就可以使用了:在这里插入图片描述
    在这里插入图片描述
    但是这个导入只是本次登录生效,如果你想永久生效,尝试第三种方法.
  3. ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
    /etc/ld.so.conf.d/这个路径下存放的都是.conf结尾的配置文件,系统在这里面会自动搜索动态库的路径,我们只需要把自己写的动态库,拷贝进去就可以永久生效了。
    我们首先将路径存入一个以.conf结尾的文件中,然后拷贝改文件到/etc/ld.so.conf.d/目录下在这里插入图片描述
    此时需要更新缓存才可以运行:在这里插入图片描述
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值