Linux之基础IO

目录

一、回顾C文件接口

向文件中写入w

向文件中读取r

向文件中读取追加a

输出信息到显示器

stdin & stdout & stderr 

C++版本的文件操作演示:

二、系统调用接口

open接口

close接口

用系统调用接口实现文件操作 

flag参数的原理

返回值

采用系统调用接口实现C语言文件的相关操作操作实例

read接口

三、文件描述符

深入理解open的返回值

OS如何管理文件? 

如果我创建一个空文件,该文件要不要占用磁盘空间呢?

文件描述符是什么?

四、深入理解一切皆文件

如何做到一切皆文件呢?

C语言如何做到让我们可以调用同一个方法,将来指向不同的对象时执行不同的方法?也就是C语言如何实现多态这样的方法呢?

用户调用用IO的完整线路

如何证明?

总结

什么叫做文件描述符?

如何理解一切皆文件?

五、再谈文件描述符

验证0

验证1

验证2

文件描述符的分配规则  

验证分配规则

重定向

输出重定向

Linux上输出重定向的原理

追加重定向

输入重定向

证明stdin里面有0,stdout里有1,stderr里有2 

stdout和stderr都是向显示器上打印,两者有啥区别呢?

重定向的本质 

使用 dup2 系统调用

输出重定向

输入重定向

追加重定向

问题:执行exec*程序替换的时候会不会影响我们曾经打开的所有文件?

Linux中重定向的原理

子进程会共享文件描述符吗?

为什么我们所有的进程都会默认打开标准输入,输出,错误?

写时拷贝

六、缓冲区

首先连看两个实例

eg1:

eg2:

什么是缓冲区

为什么要引入缓冲区

缓冲区的类型

这两个例子说明了什么?

缓冲区的基本工作原理

C语言的缓冲区数据是如何写到内核缓冲区中的呢?

C缓冲区在哪里?

用户到OS的刷新策略

eg1解释 

eg2解释

深入理解缓冲区

FILE结构体源码 

总结

七、理解文件系统

了解磁盘工作原理

OS眼中的磁盘 

inode

inode的工作原理 

如何在inode table 申请一个inode?难道是将inode table的inode遍历一遍找到空的inode吗?

block group中其余对应的名词解释 

总结

Linux下inode实操

目录是文件吗?

目录的数据块里面放什么呢?

请解释下下面这些操作在系统层面上做了什么?

​编辑

恢复文件的原理

八、软硬链接

软链接

什么时候使用这个软链接呢?

硬链接

软硬链接区别

硬链接数

硬链接的作用

九、ACM

Access  

Modify    

Change

但我们还发现一个现象就是在修改文件内容的时候,发现change也会发生改变,这又是为什么呢?

Makefile的是否进行编译的原理

那么编译器怎么知道你的源文件被修改过呢?也就是说Makefile是怎么判断源文件和可执行文件谁更新谁更旧呢?

十、动态库和静态库

库的分类

​编辑库文件的命名

​编辑C++的库

静态编译

十一、制作动静态库 

静态库的制作

​编辑我们在C/C++中,为什么有时候写代码的时候,有时候是.h里面放入声明,.c/.cpp里面放入实现呢?为什么要这么设计呢?

制作一个简单的加减库

正常编译链接

Makefile

如果我们想把我们自己写的方法给被人使用该怎么办呢?

如何打包静态库?

静态库制作

Makefile

查看静态库

发布静态库

静态库的使用

mytest.c 

-I (大写i)选项告诉编译器去lib目录下寻找头文件

-L 选项告诉编译器我的库路径在哪里

-l (小写L)+库名称(l空格可带可不带)

可是我么之前写的C/C++代码也同样用了库,为什么就没有用了这些选项呢?

Makefile

动态库的制作

生成动态库

Makefile

发布动态库

动态库的使用 

mytest.c

Makefile

如何链接动态库?


一、回顾C文件接口

向文件中写入w

执行结果:

向文件中读取r

执行结果:

log.txt中的内容就被我们读取并且打印到显示器上了

向文件中读取追加a

执行结果:

 

如果是w则会先将原文件内容清理掉,在写入

执行结果:

输出信息到显示器

stdin & stdout & stderr 

如果学习文件操作只停留在语言方面,是很难对文件有一个比较深刻的理解的。

C程序默认会打开三个输入输出流,stdin,stdout,stderr。

stdin对应的硬件设备键盘 stdout对应的硬件设备显示器 stderr对应的硬件设备:显示器

fputs既然可以向文件写入,文件打开类型是file* ,而这些in,out,err也都是file*, 随意我们可以直接将这里的参数改成out,所以我们就可以像显示器输出打印。因为显示器也是文件

      

这里既然是文件,那么我们就可以进行重定向。把本该显示到stdout里面的内容显示到文件里面。这就是输出重定向。

这里我们发现这里有两个显示器。stdout和stderr

stderr同样可以打印在显示器上

 

但是我们在执行重定向的时候,发现并没有把对应的内容写到文件里面,而是依旧将内容打印到显示器上,所以输出重定向本质是把stdout的内容重定向到文件中,所以out与err是不一样的。

C++中也会有 cin,cout, cerr,C++上也称之为标准输入流,标准输出流,标准错误流。

fputs向一般文件或者硬件设备都能进行写入!一般文件其实是在磁盘上的,本质也是硬件。所以这里可以反映出一切皆文件。

不关你是读取键盘,还是把内容写到文件,最终都是访问硬件(键盘,显示器,文件(硬盘)) OS是硬件的管理者。

所有语言上的对“文件”的操作,都必须贯穿OS。OS不相信任何人 ,访问OS,需要通过系统调用接口。几乎所有的语言的fopen,fclose,fwrite, fgets, fputs,fgetc,fputc等底层一定需要使用OS提供的系统调用。

C++版本的文件操作演示:

二、系统调用接口

所有语言的都有文件操作,他们的文件操作都还不一样,但是这个“不一样”指的是他们在语言上做封装了,但是在OS上万变不离其宗,所有语言在底层上采用的文件操作都是我们的系统调用接口。

open接口

我们发现这个接口的头文件就有三个,还有一系列参数,使用起来成本太高了,所以几乎所有语言都要进行封装。

pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行 运算,构成 flags
参数 :
  • O_RDONLY: 只读打开
  • O_WRONLY: 只写打开
  • O_RDWR : 读,写打开
  • 这三个常量,必须指定一个且只能指定一个
  • O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
  • O_APPEND: 追加写

mode:可以帮我们设置对应新建文件的权限信息。

返回值是一个整数。返回一个文件描述符,-1就是返回失败。

close接口

用于关闭文件

用系统调用接口实现文件操作 

  

此时我们发现该程序创建的log.txt的权限都是乱的,这个T是粘滞位,给目录设置就是t,给普通文件设置就是T.这个不重要,重要的是这里的权限都是乱的,原因就是没有文件创建这个文件的时候,在OS层面你就必须告诉OS这个文件创建的权限是多少!

更改以后在进行运行,此时形成的log.txt就是644 .

C语言调用的fopen底层就是调用的open

flag参数的原理

flag是整数,需要传递标志位,int有32个比特位,所以既然是传标志位,我是不是可以让一个比特位代表一个标志!所以我一次就可以通过位操作的方式向我们的系统传递多个标志。eg:如果用整数,一次只能传一个,但是如果我们用位,我们一次就可以传递多组标志位。这样做的好处就是一次可以传多组标志位,另一个就是快,因为位操作在OS里本来就是非常高效的。

所以每一个标志位都代表不同的含义,我们以8个标志位为例子。

所以我们要传这两个标志位,这要把对应位置的标志位设为1,也就是0000 1001。其中OS就在内部做判定 O_WRONLY&flag  。 O_WRONLY就是一个可以让我们去检测的一个值,我们用与的操作就可以检测这个标志位是否设置进而决定这个比特位是否曾经被添加过。

所以这里的O_WRONLY,O_RDONLY,O_CREATE 等这些都是只有一个比特位是1的数据,而且不重复。

举个栗子:

这就叫做flag的原理。

系统当中是以8进制为进行表示的。

返回值

我们来打印下这个返回值。

  

这里我们再多测试几组。

我们发现最终打出来的文件描述符是3 4 5 6.貌似是连续的,但是既然小于0是出错的,那么0 1 2 在哪呢?为什么默认打开的是3呢?

实际上这里的 0 就是标准输入,1就是标准输出, 2就是标准错误。刚才我们说到C会默认代开stdin,stdout,stderr .所以这里的0就是键盘默认的设备,1就是显示器默认的设备,2显示器默认的设备。

我们这里打印出来的没有 0 1 2意味着它不让我们用,也就意味着要么它们是被别人用着,要么就是已经被占用。换句话说这些标准输入输出流是默认打开的,那么 0 1 2就是被占用了,所以连续打开多个文件,分配的底层文件描述符就是 0 1 2 3 4 5 6 7 8....这些值。一旦看到一些数字是连续且从0开始,我们就会联想到数组下标。这里我们先卖个关子,稍后我们给出具体解答。

采用系统调用接口实现C语言文件的相关操作操作实例

read接口

从文件描述符中读取指定的内容,一次读到的内容全部存放在用户层的缓冲区中(也就是你将来要定义一个缓冲区buf),期望读多少个字节由count决定。返回值代表实际读到多少个字节。

eg1: 

这里为什么要sizeof()-1; 我从这里的文件中期望一次读1023个,为什么要-1呢?

当你向文件当中写入的时候,你并不会将\0写进去,因为文件内并不需要\0标定字符串结束,换言之当你读文件的时候你也不会读到\0,一切的前提是你将你读到的内容要当做字符串来看待,所以当我们成功后,要手动的在字符串最结尾加上\0.

其二:写入和读取不一样,写入是写多次,写多行,我就想无脑式的一次把你文件的内容一次就放进来数组,把它当成一个常字符串看就可以,只不过字符串子区域部分都是以\n结尾,所以我们这里就一次把它读到了,然后直接输出就可以。输出的就是buffer内容.

直接将log.txt中原本的内容读出来了。ps:我们自己加了一个\n,所以多了一个空行。

eg2: 

三、文件描述符

深入理解open的返回值

当我们的程序运行起来之后,变成了进程,默认情况下,OS会帮助我们进程打开三个标准输入输出!

0:标准输入 ,键盘

1:标准输出 ,显示器

2:标准错误 ,显示器

C会默认代开stdin,stdout,stderr .也是对于键盘,显示器,显示器

0,1,2,3等想到下标,open的返回值是OS给我们的,那么此时的数组一定是在OS内部,是OS帮我们维护的一个数组。

OS如何管理文件? 

所有的文件操作表现上都是进程执行对应的函数!所以文件操作换句说法就是进程对文件的操作。我们要操作文件必须得打开文件!打开文件的本质就是将文件相关的属性信息加载到内存!系统中是会存在大量的进程,进程:打开的文件的=1:n。所以系统中一定存在更多的,打开的文件!那么操作系统就会把打开的文件在内存(系统)中管理起来。OS又通过先描述,再组织管理打开的文件。linux是由C语言写的,我们就通过struct file描述文件,里面包含文件的相关属性息。那这个文件的属性信息又从哪里来呢?打开文件之前,文件就有属性了,打开之后无非就是把磁盘上文件相关的属性加载到内存,如果有必要可能会加载一些在内存中特有的属性,不过肯定能做到帮我们维护或者描述这个文件。如果文件不打开,它就一直在磁盘上,由文件系统做管理。同样的如果一个进程没有被打开或者创建,这个进程也在磁盘上,它就是磁盘上的一个可以执行文件。

如果我创建一个空文件,该文件要不要占用磁盘空间呢?

答案是必须占用,因为文件是有属性的,属性也是数据。 磁盘文件=文件内容(文件里放的什么东西)+文件属性(文件名,文件类型,文件大小,文件拥有者,所属组,权限等)。我们之前所学的文件操作本质可以分两类:一类是对文件内容的操作(fread,fgets),还有一类是对文件属性的操作.

文件描述符是什么?

当我们实际打开文件时,一定是把进程创建出来,当我有了进程,这个进程可能会打开多个文件,内核中OS就会帮助每个打开的文件创建一个struct file结构,这个结构体包含文件相关的属性信息,如果一个进程打开多个文件,系统里就会存在多个大量的struct file结构。当操作系统有这么多struct file,那么OS就以双链表的形式,把所有的文件链接起来。

那么系统里打开的成百上千的文件,哪些是属于我们进程的呢?

所有OS为了能让进程和文件之间产生关系,进程就在内核中包含了一个结构叫做struct files_struct ,这个结构内又包含了一个叫做数组的结构,这个数组的数组名称之为struct file *fd_array[ ] ,struct file* 就是描述文件的结构体的地址,这个数组就是个指针数组,PCB里包含一个struct files_struct *fs的结构,fs指向struct files_struct,进程和文件就通过struct files_struct里面的指针数组关联起来,把对应描述文件的结构体变量的地址写入到特定的下标里,就相当于fd_array指向一个文件,分别用0,1,2指向三个描述文件的结构体,这三个文件就是标准输入,输出,错误。这也相当于在OS层面,0,1,2文件描述符分别被申请成键盘文件,显示器文件,显示器文件,保存这三个文件各自的地址,然后上层再用时就用0,1,2,当你在打开一个新的文件时,OS就会帮你形成一个struct file结构,然后从你的众多文件描述符中把3分配给你,也就是把你这个文件的地址填入到3号这个文件描述符内,然后再把3返回给用户。所以文件描述符从3开始,本质上就是因为0,1,2倍占用了。

我们发现我们的写入,读取等操作,第一个参数都是文件描述符fd,执行对应的write,read 都是你的进程在执行的,你把fd传进来了,这个进程通过自己的PCB,打开自己的文件列表,根据fd索引这个数组,找到fd文件描述符里面的内容,找到对应的文件,至此就可以对文件进行相关操作了。

所以,fd本质是内核中进程和文件关联的数组的下标。在进程中每打开一个文件,都会创建有相应的文件描述信息struct file,这个描述信息被添加在pcb的struct files_struct中,以数组的形式进行管理,随即向用户返回数组的下标作为文件描述符,用于操作文件。

四、深入理解一切皆文件

一个文件是要被OS管理的,那么它就要被OS创建相关的结构体。

键盘读取能理解,键盘还能写入?虽然这些外设统一都有IO ,但是并不代表非得把读写都要实现,比如键盘就么有读方法,也就是写方法为空。显示器的写入就是把数据刷新到显示器中,但是显示器是不能读的,所以显示器的读方法为空。磁盘,网卡读写就都有。这些接口所对应的方法一定是对应的不同的软硬件设备,这些方法在底层的实现一定是不一样的。

eg:我读取键盘,磁盘,网卡在底层上的方法一定是不一样的,因为大家的硬件结构都是不一样的,所以在读写方法上一定是不一样的。这些方法全部是在各自硬件的驱动层完成的。驱动程序在底层都有对应的读写方法帮助我们完成对各种硬件的读写。

如何做到一切皆文件呢?

实际上在linux当中做了一层软件的虚拟层叫做vfs(virtual file system)。刚的struct file 就是vfs的一个概念。linux下一切皆文件,所以底层是驱动还是硬件与我没关系,我知道的是在OS层面上要打开键盘,OS就要给你维护一个struct file 结构。这个结构里面包含所有文件的属性,每一个硬件设备都被打开,或者是磁盘上某一个具体的文件被打开,OS创建struct file就完事了。然后OS在用双链表组织起来。所以这个vfs在和进程关联起来将我们就能操作了。

C语言如何做到让我们可以调用同一个方法,将来指向不同的对象时执行不同的方法?也就是C语言如何实现多态这样的方法呢?

我们就可以在file 里面定义上对应的函数指针。如果我们站在struct file 这些结构的上层去看的时候,我们就会认为所有的文件都叫做vfs这一层的文件,你的属性我都知道,方法就是读和写。当你文件对应的是键盘,就让这个文件的read方法指向底层键盘的read方法,如果这个文件代表的是显示器,那我就让这个文件的read指向显示器的底层的read方法等等一一对应。站在struct file的上层看来所有文件我要读就调用你这个文件的read方法,我要写就调用你这个文件的write方法,我根本就不关心你到底是什么文件!!!eg:有一个动物的基类,被猫,狗等各种子类都继承了,这个基类有个方法是eat,当你在调用对应的方法时,如果你对应的指针指向的是猫,调用的就是猫吃东西的方法,对应的指针指向的是狗,调用的就是狗吃东西的方法。我们就可以认为一切皆动物,只要你将来继承了这个动物基类,只要实现对应的方法就可以了,我将来用我的基类指针指向你。同样的在OS层面,这就只是文件读写,某个文件调用硬件读写方法的时候,上层根本不需要关心,它只知道如果你要访问某个文件你就直接调用read或者write就可以写到对应的设备上。

用户调用用IO的完整线路

用户层调用read/write开始->进程->进程找到文件描述符表->文件描述符表找到对应的文件->对应的文件找到具体的操作方法->具体的操作方法数显到具体的硬件上。这就是用户调用的这一整条链。

如何证明?

部分源代码展示 :

总结

什么叫做文件描述符?

进程和文件产生关联时对应的fd_array[ ]数组的下标。

fd本质是内核中进程和文件关联的数组的下标。在进程中每打开一个文件,都会创建有相应的文件描述信息struct file,这个描述信息被添加在pcb的struct files_struct中,以数组的形式进行管理,随即向用户返回数组的下标作为文件描述符,用于操作文件。

如何理解一切皆文件?

文件在OS层面有个vfs会包含每一个被打开文件的struct file这个struct包含了一批函数指针,这批函数指针帮我们直接去指向底层方法的,那么在上层看待文件就可以以统一的struct file 的方式看待所有文件,所以一切皆文件指的是在vfs这一层看待文件。

五、再谈文件描述符

文件描述符fd,证明默认的0,1,2也是可以进行读写的

验证0

0代表标准输入,read借助0就可以直接从显示器上读取数据

这里多的空行,就是你在输入完后,按的回车键。read把\n也读进来了。只要减1在赋值0就没问题

  

验证1

1代表标准输出

验证2

2代表标准错误

文件描述符的分配规则  

我们知道对于新创建的文件fd默认从3开始,如果我们关闭掉0呢? 

eg1:如果我们把0关闭,我们发现对应的fd就是0了。

eg2:如果我们把2关掉,发现fd就成了2.

所以实际上在文件描述符中,如果把0关掉,就申请0,把2关掉就申请2,如果我有0,1,2,3,4我把3关掉,在申请就是3.

文件描述符的分配规则:给新文件分配的fd,是从fd_array中找一个最小的,没有被使用的,作为新的fd。

验证分配规则

eg1:0,2都关掉分配的就是0。

重定向

输出重定向

如果我们关掉1,我们会发现fd变成1,并且本来应该打印到显示器上的内容,现在出现在了文件里

 

再次关闭1验证 

 

本来应该显示到显示器中,但是却被“显示”到文件内部---输出重定向!

原理解释:

这个代码被编译运行起来后,将来就成了进程,内核中就会有task_struct,所有文件的类别都是struct file。close(1)就是把1对应的文件描述符关掉了,去掉1号文件描述符的数组和对应的显示器文件的关联。1号就不在指向对应的显示器文件了,这个显示器文件没人指向OS就给他关闭了。然后open打开了一个文件,也就是新增了个log.txt文件,根据分配规则,这个log.txt就与1链接起来,1就指向这个位置。

这个printf叫做C语言中的打印,本质就是向标准输出打印,C语言上标准输出就是stdout,stdout就是File* ,fopen的返回值就是File*,那么File又是个啥呢?File就是C语言层面上的结构体!

printf,fprintf,cin---语言层封装的函数或者对象或者类。所有这些接口都要进行系统调用:最典型的就是open,write。只要用系统调用就一定要使用fd---这就是系统层。所以语言层和系统层天然的就是上下层关系。

所以调用printf的时候最终是向stdout进行打印,既然向stdout打印,就是向File*里面写,File*是个结构体指针,File是个结构体,因为底层必须使用系统层的接口,所以这个File里面一定包含了一个整数,是对应在系统层面的,这个文件的打开对应的fd。

所以结论就是:系统上的0,1,2,C语言上的stdin,stdout,stderr,C++的cin,cout,cerr他们之间的对应关系是一对一的,stdin里面一定包含了一个0,stdout一定包含1,stderr一定包含2。这样语言和系统就产生了关系。

所以我们以前所写的C语言操作,当你调用fwrite的时候,好像就在调用File

好像在向文件流里写,实际上在写的时候,这个库函数在底层实际上是把你要写的数据通过文件描述符写到磁盘上。因为OS提供fd你就必须得用。

回到开始的问题,所以printf它是向stdout里面写,这个stdout封装了一个fd,这个fd是1,也就是stdout只关系1这个数字,当我们进行printf写入时,1由显示器文件指向log.txt文件,1这个下标是没变的,printf只认识1所以就向1打印了,本来应该打印到显示器上,现在打印到了文件里面。这就是输出重定向的原理。

下面我们用fprintf进行验证

eg:不关闭1,fprintf也是向显示器上打印。 

eg2:关闭1

 

所以printf和fprintf向stdout打印没有差别。所以我们要意识到printf和fprintf是向stdout打印的,stdout是个File*,File是个结构体,这个结构体一定有个整数和系统层面的0,1,2一一对应,所以调用printf等一定是通过fd操控底层文件。

C++ cin ,cout,cerr, 还有其他语言的文件操作,如果他们能在Linux上跑的时候,这些语言文件操作的底层必须都得包含一个fd。所有语言成为进程访问文件都是通过fd做的。

Linux上输出重定向的原理

把echo这个进程的1关掉,然后把log文件打开,然后你输出的所有内容都会打印到log里面。

追加重定向

C语言中有个选项叫做a选项。append:追加。系统里面就是O_APPEND

  

如果是正常的写入就会把原始内容清掉,如果是追加就是每次在前一次的基础上追加内容。

输入重定向

Linux上的输入重定向

cat啥也不带就是从键盘中获取内容

cat 输入重定向log.txt 就是从log.txt中获取内容

语言上的输入重定向

log.txt所存在的内容

stdin标准输入,本来应该从键盘获取的值,但是现在我们关掉了,我们有打开了一个新的log.txt此时这个新的文件就是0,所以stdin就变成我们从文件(log.txt)中获取。它只知道从0读,读的键盘还是文件完全不关心。 

  

证明stdin里面有0,stdout里有1,stderr里有2 

 同样我们也C语言的文件操作创建一个文件,在打印出它的文件描述符,可以预料的是答案是3

所以语言层和系统层一定是互相包含的关系,语言上的文件操作都必须通过OS完成。

stdout和stderr都是向显示器上打印,两者有啥区别呢?

区别:当重定向的时候,我们把内容写到log.txt里,发现只有标准输出写到了log.txt里,因为重定向叫做输出重定向,所谓的输出重定向只是把本来应该显示到1号文件描述符的内容,直接显示到指定的文件中,但是2号并没有变,照样执行标准错误。我们是两个文件描述符,但是你最终重定向只能重定向一个,我们重定向的就是标准输出。

我们如何将标准错误也重定向到log.txt文件中呢?

这种写法就可以将标准输出和标准错误都进行重定向,具体解释:

重定向的本质 

每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件 

使用 dup2 系统调用

也不是每次重定向都得关闭0或1dup2系统调用接口就可以帮助我们完成这个工作。

       #include <unistd.h>

       int dup2(int oldfd, int newfd);

假设要输出重定向,我们要改的文件描述符肯定是1,我们打开的文件的描述符是3(也就是log.txt),我们如何在文件已经打开的情况下进行输出重定向呢?

我们可以把3里面的内容拷贝到1里面,因为上层只关心数字,不关心内容,所以dup2的底层原理就是把进程的文件描述符表中需要重定向的内容做一下相关的拷贝。

dup2的描述是让newfd变成oldfd的拷贝,我们这里要做的是将3的内容拷贝给1,也就是1就成为了3的拷贝,所以1就是newfd,3就是oldfd. 

输出重定向

  

输入重定向

所谓“ 只读 ”( Read-Only )表示这个 文档 只能打开来观看,不能修改也不能储存

运行的时候,你不能向平时一样输入,我们要像log.txt里输入内容,再次运行就会打印到显示器上 

scanf中空格代表结束,所以证明成功重定向。 

追加重定向

以上就是dup2的使用。

问题:执行exec*程序替换的时候会不会影响我们曾经打开的所有文件?

答案:不会,因为文件的这些结构是进程相关的数据内核结构,替换的时候只会替换代码和数据。

Linux中重定向的原理

如果我现在执行echo  "hello  world" > file.txt 。

这个代码在被翻译过来就是 让fork执行exec系列的函数,替换echo命令,我们可以在替换的时候识别到它有 > 符号,可以先把子进程的文件描述符dup(fd, 1),然后在替换,所以最终打印结果的时候就直接打印到文件里了。因为程序替换并不替换曾经打开的文件,这个就是实现命令行式重定向的原理。

echo "hello world" > file.txt
fork->child->duo2(fd, 1)->exec*("echo", "echo",...)

子进程会共享文件描述符吗?

答案:如果我们创建子进程的时候首先回形成一个新的task_struct,新的地址空间,新的页表,同时也要形成份和父进程一模一样的files_struct,子进程创建的时候很大一部分数据都是源自父进程的但是曾经打开的各种文件是不会再形成的,所以我们就会出现父子进程的文件指针指向同一个文件。如果我们的父进程曾经打开了标准输入,标准输出,标准错误,同样子进程也会继承下去!!

为什么我们所有的进程都会默认打开标准输入,输出,错误?

原因就是我们命令行上所有的进程的父进程都是bash,bash是命令行解释器,它一定要打开标准输入,输出,错误。因为用户需要使用。

写时拷贝

写时拷贝,是通过页表实现,拷贝的是物理内存中的内容,而对于内核数据结构是OS直接去修改的,1个进程可以打开多个文件,实际上一个文件也可以被多个进程打开,那这个进程什么时候被关闭呢?引用计数--在我们的struct file本身存在一个int cnt ;这个cnt就代表有多少个进程指向我,如果有一个进程指向我,cnt++,再有一个指向我,cnt++...关闭一个进程cnt--,只有cnt为0的时候,这个文件才被释放。这个计数器就可以帮助我们让多个进程指向同一个文件。

六、缓冲区

首先连看两个实例

eg1:

我们在演示重定向的时候,最后是没有关闭fd的

进行关闭fd 

先把log.txt 内容清空

执行结果:

我们惊奇的发现这个文件里啥也没有。归根结底就是因为这个close。

eg2:

不进行close(1),我们进行重定向的时候符合我们的预期,除了标准错误都重定向到了log.txt

进行close(1),我们发现重定向后,log.txt中只有一个标准输出 

什么是缓冲区

缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

为什么要引入缓冲区

我们为什么要引入缓冲区呢?

比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。现在您基本明白了吧,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

缓冲区的类型

缓冲区 分为三种类型:全缓冲、行缓冲和不带缓冲。

1、全缓冲

在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。

2、行缓冲

在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。

3、不带缓冲

也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。

这两个例子说明了什么?

首先说eg1:eg1的根本原因就在于C语言给我们提供了缓冲区,我们曾经说的缓冲区都叫做用户级缓冲区,在语言层面上给我们实现的 .

C语言的printf和fprintf都是向stdout写入的,stdout是FILE*,FILE是一个对应的结构体。当你调用printf和fprintf并没有直接写到操作系统中,因为在C语言上也有缓冲区,叫做C缓冲区,这个缓冲区是C语言给你提供的。比如说你想写“hello world”,你是直接把他的数据写入到了C语言的缓冲区,然后你的printf就执行完了。所以字符串就是从用户角度直接写到C语言对应的缓冲区里就完了,然后定期的,C语言缓冲区把C语言的数据再刷新到OS,OS再把数据更新到硬件上。

缓冲区的基本工作原理

我们清楚的知道到当调用printf或者fprintf,字符串会写入到stdout中,本质是把字符串写到了C缓冲区里,这个缓冲区是C语言提供的,C语言的缓冲区是在用户层提供的,再由我们的系统调用接口,把数据从用户缓冲区拷贝到内核缓冲区,然后由操作系统把数据定期刷新到磁盘中

C语言的缓冲区数据是如何写到内核缓冲区中的呢?

C语言的缓冲区数据是如何写到内核缓冲区中就相当于是把一个数据写到文件中,一定需要fd,也就是你用户层的数据拷贝到用户层的缓冲区中,通过fd来把数据刷新到OS文件对应的缓冲区中,在OS中打开文件就有文件对应的内核缓冲区。

C缓冲区在哪里?

FILE是个结构体,它里面封住了fd,还维护了与C缓冲区相关的内容。stdin,stdout,stderr,和你打开的文件都是FILE,所以C语言上的printf和fprintf打印的字符串全部写入到了文件的缓冲区里,也就是FILE结构的内部。也就是说调用的printf或者fprintf最终所写的所有消息最终并没有显示出来,并没有直接刷新到外设里面,而是暂时将数据保存到了FILE结构体内与缓冲区相关的内容。也就是说FILE这个结构体,除了定义了fd,还定义了缓冲区。

进程退出的时候,会刷新FILE内部的数据到OS缓冲区。

用户到OS的刷新策略

1.立即刷新(不缓冲)

2.行刷新(行缓冲)实际扫描时碰到\n就把数据刷新出去,比如显示器打印,所以如果你写完一行消息没有带\n,没有立即显示出来,根本原因就是你没有行刷新

3.全缓冲 缓冲区满了才刷新,比如,往磁盘文件中写入。

往显示器上写就是行刷新,往普通文件写就是全缓冲

OS刷新到硬件上这三个策略也是同样适用的。

eg1解释 

平常我们是将printf的内容写入到了C缓冲区,然后碰到\n,进行行刷新,最终显示到显示器上。但是现在我们进行了重定向操作,显示器定向到了log.txt.由于重定向了,缓冲方式也改变了,由行缓冲变成了全缓冲。全缓冲的时候数据还在C语言的缓冲区中(缓冲区还没满),还没有被刷新到OS的文件内核缓冲区中,所以close(fd),就把OS的文件描述符关了,所以就没有刷新的地方了,所以数据就没办法进行刷新了。但是如果我们使用fflush接口强制刷新,在进行关闭fd就解没有问题了。

eg2解释

我们发现关不关闭1是不会影响数据刷新到显示器上的,因为显示器是行刷新。当我们重定向后printf和fprintf不会显示是因为他们从行缓冲变成了全缓冲,这两条消息本质上都是向C语言的缓冲区中写入,数据会暂存到C缓冲区中,但是他变成了全缓冲,不会立即刷新,当他准备刷新的时候又把文件描述符关闭了。这就是他俩不存在的问题,标准输出存在是因为它调用的是系统调用接口,直接刷新到了文件的内核缓冲区中,所以关闭fd对他没影响。

深入理解缓冲区

eg:

重定向后,重复出现的是使用C接口的时候,系统调用接口并不收影响!

每条消息都有\n,向显示器上打印,在fork之前就打印到显示器上了,也就是在fork之前缓冲区里就没有任何的数据了。所以打印的时候fork就没啥用了。当重定向以后,重定向后潜台词就是刷新策略变了,行缓冲变成全缓冲,printf,fprintf,fputs是先写到缓冲区buffer中,它是由C语言提供,在用户层,数据拷到缓冲区中,而这几条消息并没有将缓冲区写满,数据就暂时存在缓冲区,严格上来说就是父进程的缓冲区,因为父进程调用了C的接口,当fork的时候(父进程的缓冲区也是父进程的空间),我们就要发生我们的代码共享,数据各自拷贝一份以写时拷贝的方式,而缓冲区里的数据最终会被直接刷新出去,子进程也要刷新对应的数据就发生写时拷贝,因此数据出现了两次,这里重复出现两次的本质就是写时拷贝问题而导致的重复刷新。根本原因就是刷新策略变了。

解决方法:在fork前,进行fflush

原因:当他的若干消息打印到C语言缓冲区的时候,虽然重定向到文件里刷新策略变了,但是我进行了fflush让其立马刷新出去,再fork的时候,缓冲区里没数据了,就不发生写时拷贝了,就不会重复出现了。

FILE结构体源码 

在/usr/include/libio.h
struct _IO_FILE {
 int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
 //缓冲区相关
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
 char* _IO_read_ptr; /* Current read pointer */
 char* _IO_read_end; /* End of get area. */
 char* _IO_read_base; /* Start of putback+get area. */
 char* _IO_write_base; /* Start of put area. */
 char* _IO_write_ptr; /* Current put pointer. */
 char* _IO_write_end; /* End of put area. */
 char* _IO_buf_base; /* Start of reserve area. */
 char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base; /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */
 struct _IO_marker *_markers;
 struct _IO_FILE *_chain;
 int _fileno; //封装的文件描述符
#if 0
 int _blksize;
#else
 int _flags2;
#endif
 _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
 /* 1+column number of pbase(); 0 is unknown. */
 unsigned short _cur_column;
 signed char _vtable_offset;
 char _shortbuf[1];
 /* char* _save_gptr; char* _save_egptr; */
 _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

总结

stdout,cin/cout,iostream,fstream.流也是类,它里面包含缓冲区,这个缓冲区以字节不断向里面拷,让别人拿走。所以endl就是刷新C++流当中的数据到显示器当中。相当于行刷新。 

七、理解文件系统

文件=文件内容+文件属性,文件没有被打开它的内容+属性在磁盘放着

了解磁盘工作原理

磁盘是计算机中的一个机械设备(例外:SSD,FLASH卡,USB)这里我们讨论的是机械硬盘

磁盘有机械的,机械磁盘里面有各种硬件,其中有个磁盘的盘片,数据就在这个盘片上放着。

我的盘片是需要被定位的,我们要找到某个面,找到某个同心圆中某个扇区我们就能把数据写进去。磁盘写入的基本单位是:扇区!扇区的大小是512字节,但是一般磁盘和OS进行IO数据交换时采用的是4KB。

将来我想找到磁盘中某个扇区的位置该如何去找呢?

磁头->磁道->扇区。

OS眼中的磁盘 

我们可以将磁盘的盘片想象成线性结构(将盘片和磁带类比一下)OS就是将磁盘相像成了一个线性的数组,LBA就是数组的下标,LBA实际就是一个抽象出来的地址,很像进程地址空间,OS如果要把数据写到物理磁盘上就要把LBA地址转化成磁盘地址。

inode

为了能解释清楚inode我们先简单了解一下文件系统

 一个文件没有被打开,是在磁盘里的,现在磁盘被我们想象成了一个线性结构。我们的磁盘是很大的,因此它的管理成本也是很高的(类比一下我们国家进行行政管理,我们国家就把土地划分成各种省,然后每个省都有自己的管理人员,然后对应到各个省又进行划分...区域变小是有利于我们进行维护的)。我们把磁盘想象成国家,就要把他们划分成小区域,在磁盘这里就叫做分区,例如你大多数的电脑是只有一个硬盘的,然后将他们分区,形成D盘,C盘,E盘...

所以文件系统的第一步就是分区:大磁盘变成小空间。向分区写入文件系统(eg:向某个省分配政府领导人员)。如果你想更换你分区里的文件系统,你就可以将它格式化掉。所以使用磁盘要经历两个步骤:1.分区 2.格式化(格式化本质就是写入文件系统)

现在已经完成大空间拆成小模块的过程,所以现在的管理成本变成我们可控的了,我们再以一个小空间为例例如拿出C盘,如果我把这个盘管理好了,那么其他的区域直接进行复制这套管理就可以把其他区域也都管好了,因为磁盘和OS在计算机上是可以做成标品的,不想我们现实中对不同的省市实行不同的管理政策。但是如果你就想让不同的分区写不同的文件系统,也是可以的,我们现在的OS都支持多文件系统(但是我们今天不讨论该情况)。

C盘管理示意图: 

boot block是与启动相关的,boot block是每个分区都有的,不过一般都是在第一个分区的第一号。意思就是C盘一般来说是一定有的,其他盘可能有也可能没有。有了这个boot block供我们启动时查找分区。eg:计算机开机时,OS就是从boot block知道你有几个分区的。 或者启动时OS从boot block找到OS的相关组件等。然后又将其他的区域拆分成很多个Block group。同理我还是只要把这个Block group0管好,其他的Block group也就都管好了。所以最终成了研究Block group0。         

文件内容放在Data blocks中,文件属性放在inode Table中 

Data blocks是一个大空间,这个大空间里有很多数据块,这些数据块都有他们的编号。同样的inode Table也是一个对应的表,它里面也包含类似的数据块,只不过它的数据块保存的是文件的属性,每一个数据块小空间我们把它叫做一个Inode.

比如我们今天touch file.txt这个文件的大小,权限,拥有者,所属组这些信息都在Inode table里面找一个没有被使用的Inode把这些属性都填进去;后来我又向这个文件里写了一个字符串,所以又在Data blocks里申请一个没有人用到的块,然后把字符串放进去,此时文件就被创建好了。因此就需要对应的iNode是和对应的数据块进行关联,方便我们后序查找。我们访问文件的时候看似我们都是在根据文件名进行访问文件,实际在系统层面上,标定一个文件的并不是文件名。也就是说:Linux下,文件名(包括后缀)在系统层面没有意义!是给用户使用的!Linux中真正标识一个文件的是通过文件的iNode编号!也就是每一个文件都有一个文件的标号标识这个文件,一个文件有一个iNode

eg:ls -i 可以查看inode

        

inode的工作原理 

你的block也有编号,所以inode属性里面可以通过数组的方式来吧和文件的inode关联的block的编号写到数组里。比如block的编号是99,100.那么这个数组里面的0下标1下标就可以填上99,100.当我们访问文件时先根据inode编号找到对应的inode属性,然后通过属性找到文件和数据块的映射关系,找到对应的块,然后把数据读出来。

简单来讲:inode Table里面保存的是一个个inode结构体,这个结构体变量也是数据,只不过以结构体方式呈现,里面包含了这个文件的所有属性,包括文件的大小,权限,拥有者,所属组等信息然后还包括文件相关的inode编号和block group提供的data block所提供数据块的对应关系。我们只要找到inode就可以直接找到它的所有属性,再根据inode中block的映射关系找到所有的数据块。

ps:我们这里数组虽然写了个32并不代表文件大小的上限是32个数据块,实际上数据块可以设计成链表结构,不够了就可以去找下一个。

如何在inode table 申请一个inode?难道是将inode table的inode遍历一遍找到空的inode吗?

如果里面的inode有10万个这样做是不是效率就太低了。因此就有了inodebitmap。它叫做位图。eg:以0000 1010为例

  • 从右向左第一位叫做inode编号为0,第二位叫做inode编号为1,第三位叫做inode编号为2...一共能标识8个inode。
  • 第一个比特位是0,第二个比特位是1,这就是比特位的内容。

inodebitmap是位图,是2进制数字这样的结构,从右向左,比特位的位置的含义:inode编号
比特位的内容的含义:为0还是为1代表特定的inode"是否"被占用! 所以我要创建一个文件,我们只需要没有被使用的inode,只需要将inodebitmap加载到内存里,用位操作直接去找,找到0就代表它没有被使用,没有被使用就直接把创建的文件的属性填到这个空间中去就行。

对应的blockmap也是一样。申请空间的时候变量blockmap,假如需要5个就找到5个为0的位置,把为0的位置填入到inode的block位置中,就构建了一个映射关系。下次向文件写入的时候就跟据他的inode和对应block的映射关系找到数据块,就可以向磁盘对应的位置进行写入数据。

block group中其余对应的名词解释 

  • Group Descriptor Table:描述的是组的情况包括组的编号,再包括组中blockmap,inodebitmap的使用情况,比如说已经使用了多少个inode了,还有多少没有被使用,已经使用了多少个block了,还有多少没有被使用。
  • Super Block:描述的是整个分区的相关的文件系统信息,基本上每个block group里面都有Super Block。描述整个分区有多少个组,每个组是什么情况,整个分区被用了多少,inode还有多少,block还有多少,还有文件系统类型,这个东西有多份就是相当于做了备份,有备无患。

总结

我们直接将块组管好,这样整个块组都可以采用同样的方式把所有的文件管理好,这样分区就管理好了,然后把分区管理的思想推广到每一个分区,所有的分区就管理好了,最终磁盘的数据也就管理好了。
这就是linux特有的EXT系列的文件系统的核心内容

Linux下inode实操

虽说文件是inode标识的但是我作为用户我只知道文件名。

目录是文件吗?

是。 目录也有自己的inode,放目录的文件的大小,权限,拥有者,所属组等信息
stat查看目录属性

目录有数据吗?必须的

目录的数据块里面放什么呢?

linux中你所创建的所有文件全部一定在一个特定的目录下,从根目录开始,你要操作linux就必须有一个目录,一个进程有自己的当前目录,因为用户要用文件名,文件名也是一个字符串,我们的系统要的是inode,所以目录数据块里存放的是文件名和inode编号的映射关系。

请解释下下面这些操作在系统层面上做了什么?

创建和写入: 创建文件的时候首先在inodebitmap里面找一个没被占用的inode,然后把文件相关信息放到inode中,然后向文件通过inode找到block写入数据,然后在目录下把hello.c和hello.c的inode建立一个映射关系。

cat是查看:实际上cat的时候是在当前目录下运行cat hello.c的时候先在通过lesson15这个目录的inode里面访问它的数据块,找到hello.c和hello.c的inode的映射关系(这里以hello.c的inode是1234为例),然后通过1234访问1234的blocks,在找到对应的数据块最后将数据块的内容打印出来。

lesson15->data block->1234:hello.c->1234->inode->blocks[]->打印文件内容

ls -al 同cat一样。

rm hello.c :只需要找到inodebitmap由1置为0就可以了。对blockbitmap也是如此操作。对于文件属性和内容是不做处理的。所以这也就是为什么在Linux中拷贝文件要花两三分钟,但是删除的时候1秒钟就删除了,因为删文件的时候并不删除文件的属性和数据,而只是将它是否有效删掉就可以了,下次在创建文件只要覆盖到原来的数据块就可以了。 

恢复文件的原理

恢复文件的原理就是把曾经的inodebitmap由0置为1blockbitmap由0置为1就可以了。所以最终就是你如何知道你误删文件的inode编号,如何恢复它?debugfs 恢复工具。

同样windows中的删除根本就没有删除,我们可以把回收站当成一个目录,只不过是把文件从一个目录移到另一个目录。清空回收站才等价于rm。

如果我误操作删除了文件最好的操作就是啥也不干,你会就尝试一下,不会就找人。

八、软硬链接

软链接

软连接,我们建立了一个软链接叫做log_s,它指向的是log.txt.这是一种文件形态叫做软链接

什么时候使用这个软链接呢?

比如我们创建了一串目录路径

 我们进到这个test这个目录

在test这个目录下写简单的一个可执行程序test.cc

假如我们想在lesson15这个目录下执行刚才的可执行程序,是不是只能下下图一样带上大量路径呢?

这样太麻烦了,所以我们可以用一个myexe软链接一下

运行的时候直接./myexe即可

软链接就特别想windows中桌面上的快捷方式

eg:通过点击桌面端的浏览器快捷方式和这个程序所在路径下的浏览器都可以打开它。 

硬链接

ln不带选项建立硬链接

软硬链接区别

ls -i命令查看inode

通过比对我们发现软链接是有自己独立的inode的,就证明软链接是一个独立的文件,有自己的inode属性,也有自己的数据块,只不过它的数据块里面保存的是指向文件的所在路径+文件名(如果有的话)

硬链接我们发现他俩的inode是相同的,硬链接本质上根本就不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为自己没有独立的inode!所以创建硬链接本质就是在特定的目录下填写一对文件名和inode的映射关系。也就是说硬链接可以进行文件的重命名。

eg:我们对log.txt进行建立一个硬链接log.hard

删除log.txt后我们发现log.hard和log.txt就是完全一样的,只是名字不同

硬链接数

建立一个file.txt的硬链接 

同样的这个数字保存在inode的属性里面,就比如说是inode结构体里又有个ref,表示我们当前的inode被多少个人指向了。所以建立硬链接的本质就是ref++;

硬链接的作用

我们发现创建普通文件的数字是1,但是创建目录数字就是2,

我们发现dir的inode和dir中隐藏文件. 的inode是一样的。所以为什么说 .指向当前目录呢?原因就是这两个文件名指向的inode是一样的,指向同一个文件。

我们再在dir下创建一个tmp,发现现在的数字就变成3,第三个新增的就是tmp下的..这也就是为什么..代表上级目录。

九、ACM

下面解释一下文件的三个时间:
  • Access  文件最近被访问的时间
  • Modify   最近一次修改文件内容的时间
  • Change 最近一次修改文件属性的时间

Access  

我们以file.txt为例,sata命令可以查看文件的属性我们首先访问这个文件但是我们发现实践操作下来这个ACCESS的时间并没有改变 ,这是为什么呢?

在较新的Linux内核中,Access时间不会立即刷新,而是有一定的时间间隔,OS才会自动进行更新时间。因为在我们读取文件的时候,modify和change是一个叫低频的事件,Access是一个高频的如果我们频繁的去更新我们的时间,就会导致系统中出现大量的刷盘问题导致你的linux特别慢,所以就在新的linux内核中优化了,防止和磁盘有过度频繁的交互而导致系统效率降低。

Modify    

Modify:我们通过echo向文件写入,发现modify时间确实改变了

Change

Change 改变文件属性就会发生改变,这里我们进行改变文件的权限

我们发现其他的两个时间还是老的时间,修改文件的属性后,change的时间确实发生改变了。

但我们还发现一个现象就是在修改文件内容的时候,发现change也会发生改变,这又是为什么呢?

当我们修改文件内容的时候有可能修改文件的属性,比如对文件内容修改的时候可能会修改文件带大小的属性。所以他俩同时发生改变是很正常的 。

Makefile的是否进行编译的原理

Makefile在生成可执行程序的时候,每一次都需要对比老旧的源文件,如果你的源文件没有修改,那么再次进行make的时候,就不会再给你形成新的可执行程序了。

那么编译器怎么知道你的源文件被修改过呢?也就是说Makefile是怎么判断源文件和可执行文件谁更新谁更旧呢?

当你进行修改源文件的内容时,文件的modify时间是会被修改的。当进行编译文件的时候,mytest的时间是比test.c是时间更新的,因为在形成mytest之前,test.c就形成了。

当我们在进行对tset.c进行修改的时候发现现在test.c的时间就比mytest的时间要新了

我们再次进行make就发现可以编译了,相应的mytest的时间也被更新了

touch命令还可以用来更新时间(对已经创建好的文件再次进行touch)所以尽管我们没有进行修改源文件的内容,但是只要更新时间后,照样可以进行make

总结:makefile会根据可执行程序和源文件的修改时间进行判断是否可以进行重新编译。如果可执行程序文件的时间比源文件的时间新,就不能再次进行编译了。如果可执行程序文件的时间比源文件的时间旧,就代表源文件进行了修改,可以再次进行编译。 相应的我们makefile里面的.PHONY叫做伪目标,他总是可以被执行的,原因就是它不关心时间谁新谁旧的问题,所以他总是可以被编译。

十、动态库和静态库

库的分类

从我们开始接触语言我们就在使用库,例如C标准库。

在linux下如果想查看我们写的程序运用了那些库,可以用ldd + 可执行程序命令

我们查看下这个库,发现它是一个软链接,链接的是libc-2.17.so版本,这就是我们的库

所以所谓的库是在你linux下真正存在的一个文件,这个文件当中包含C语言当中使用的大部分库的内容。

eg:库的inode

库文件的命名

库文件的命名:libXXXX.so 或者libYYYY.a

库的真实名字:去掉lib前缀,去掉.a-,.so-(包含的内容)后缀,剩下的就是库名称。

C++的库

C++源文件的后缀可以有三个:.cc、.cpp、.cxx。我们最常用也最推荐就是.cc、.cpp

这里我们写一个C++的程序并查看它的库,发现就是C++库

静态编译

makefile中假如静态选项用的就是静态编译的

ps:一般的服务器,可能没有内置语言的静态库,而只有动态库,所以进行静态编译的时候就需要手动安装静态库。安装静态库的yum命令如下

sudo yum install glibc-static
sudo yum install libstdc++-static

实际上很多的linux命令就是C语言写的

eg:ls命令

十一、制作动静态库 

静态库的制作

我们先查看一个动态库,发现里面全是乱码(也就都是二进制)

库本身就是二进制的文件,那么我们如何得知,一个库给我们提供了什么方法? 

一套完整的库要包含:1.库文件本身 2.头文件 3.说明文档,库文件虽然是二进制,但是头文件却不是二进制的,它会说明库中暴露出的方法的基本使用!头文件安装在usr/include/目录下。

eg:打开stdio.h,它里面会包含很多方法的基本使用

我们在C/C++中,为什么有时候写代码的时候,有时候是.h里面放入声明,.c/.cpp里面放入实现呢?为什么要这么设计呢?

因为我们要制作库,库是二进制的,.h里面没有实现只有声明是方便我们制作库的。库的存在一方面是方便使用,另一方面是私密。

制作一个简单的加减库

正常编译链接

我们在test_lib目录下实现头文件和源文件

在lesson16实现我的可执行程序的源文件mytest.c

直接对mytest.c编译是不行的,因为你只能让我看到声明。gcc是不知道这俩函数的实现在哪里的

 我们必须指明sub.c和add.c的路径才能编译成功

执行结果:

Makefile

%叫做通配符,作用是把当前目录下所有的.o文件给我们展开,.o就是目标文件。所有目标文件链接实现可执行。%.o依赖所有的%.c,这里的$<意思是把%.c的内容一次展开,一个一个的进行编译

所有的.o链接形成可执行,所以需要三个.o,因为我们有三个源文件分别是你当前目录下的mytest.c,test_lib目录下的add.c与sub.c。所以我们要将这三个源文件先编译成.o,然后再进行统一链接就可以了。

编译及运行结果

如果我们想把我们自己写的方法给被人使用该怎么办呢?

1.提供源代码+头文件。但如果我们不想给他提供源代码+头文件并且还得让他用,我们就可以将我们的程序打包成静态库 。

如何打包静态库?

1.将源文件全部变成.o   2.用ar命令实现打包

因为我们的可执行程序是由.o链接而成的,所以我们只提供.o也可以,所以将所有的.o文件打包形成的一个文件就是库文件。

静态库制作

我要打包的是add.o和sub.o,是不用将main函数也打包的,因为main函数是用户在用。所以我们在test_lib目录下也实现一个Makefile帮助我们形成.o文件

基于这四个文件实现

Makefile

libmymath.a依赖这两个.o 。ar 命令就是把当前的.o文件全部打包。ar -rc $@ $^ 的意思是把目标文件libmymath.a用打包好的.o生成。

查看静态库

同时我们可以查看下系统的静态库,这里也是一堆的.o文件(只列出一部分)

现在我就把我的源文件隐藏起来了,因为.o文件也都是二进制

将来我们给别人的时候还要给他们头文件让他们明白库里有什么方法,所以我们可以在Makefile中发布一下我们的库

发布静态库

output就是发布我们自己的静态库

发布库

所以从现在开始,如果有人想用你的代码,但是你不想让他知道你代码的内容,你就可以把你的代码制作成一个静态库给他。

静态库的使用

我们以这个friend目录为例,首先把我们的静态库拷贝到friend目录下,并改名为lib

mytest.c 

根本原因在于,编译器是找不到你的头文件的,你的lib和mytest.c虽然在同一级目录下,但是头文件不是和mytest.c在同级目录,头文件是在lib目录下的。编译器默认是不会找你当前目录下的目录的,所以也就编译失败。因此你要告诉编译器要在我的当前目录下的lib目录下寻找头文件。

-I (大写i)选项告诉编译器去lib目录下寻找头文件

         

但是此时又发生报错,链接时报错,因为编译器没办法去找你的库在哪里,所以你就需要告诉编译器我的库路径在哪里

-L 选项告诉编译器我的库路径在哪里

 

但是此时仍然报错,因为编译器不知道应该在lib目录下链接那一个库,隐藏就需要告诉编译器链接那个库。

-l (小写L)+库名称(l空格可带可不带)

 但是现在还不行,因为这个是库的文件名,我们要的是库的名字。所以库的实际名字是mymath 此时就可以编译通过了

可是我么之前写的C/C++代码也同样用了库,为什么就没有用了这些选项呢?

原因就是之前的库,在系统的默认路径下:/lib64, /usr/lib,/usr/include等。编译器是能识别到这些路径的。换句话说,如果我不想带这些选项,我就可以把对应的库和头文件拷贝到默认路径下。但是我们严重不推荐!该过程也就是一般软件的安装过程! 

Makefile

动态库的制作

 基于这四个文件实现

生成动态库

  • shared: 表示生成共享库格式
  • fPIC:产生位置无关码(position independent code)fPIC产生.o目标文件,程序内部的地址方案是:与位置无关也就是库文件可以在内存的任何位置加载,而不影响和其他程序的关联性。
  • 库名规则:libxxx.so

Makefile

发布动态库

动态库的使用 

首先将我们的动态库拷贝到friend_dynamic目录下

mytest.c

Makefile

告诉编译器我们的库路径,库的名字,库的头文件

编译通过了但是运行的时候发生错误,原因就是没有链接上我们的动态库

原因是什么呢?

原因就是你的makefile只是告知编译器头文件库路径在哪里,可是当程序编译好的时候此时已经和编译器无关了,当程序要运行的时候,加载器进一步告知系统我们的库在哪里。刚刚的静态库没这个报错是因为静态库直接拷贝到我的程序里了,不需要链接。动态库则是编译,运行都要链接。

如何链接动态库?

  • 1、拷贝.so文件到系统共享库路径下, 一般指/usr/lib严重不推荐,会污染官方的库
  • 2、更改 LD_LIBRARY_PATH
  • 3ldconfifig 配置/etc/ld.so.conf.d/ldconfifig更新

更改 LD_LIBRARY_PATH

我们有个环境变量LD_LIBRARY_PATH,指明程序启动后,库的搜索路径。这个环境变量在系统是默认没有配置的,也就是说默认是没有的。但博主经过配置过所以是有的。我们就可以把我们自己的lib添加到该环境变量下,这样就可以解决问题了。

ps:如果默认没有的话,这样做即可

export LD_LIBRARY_PATH=/home/pxl/lesson16/friend_dynamic/lib

接着我们就发现我们的程序可以跑了,链接的还是我们自己的库

ldconfifig 配置/etc/ld.so.conf.d/ldconfifig更新

/etc/ld.so.conf.d是系统在搜索动态库的时候的一个系统路径。
进入这个目录就需要超级用户

在这里就可以添加配置文件, 我在这里创建一个pxl.conf,将自己实现的动态库的路径拷贝进去

 然后切回我们自己的用户,执行sudo ldconfig命令更新下

这下程序也可以正常运行,并且永久有效,因为你已经配置了全局的库路径的搜索路径

删除该conf

切换到超级用户,进入/etc/ld.so.conf.d/目录,删除pxl.conf即可

如果只提供静态库,意味着我们只能将我们的库,静态链接到我们的程序中。(但是一个可执行程序不仅仅会依赖我们的库,所以有些情况下我们的代码是既可以静态链接又可以动态链接的)

如果只提供动态库,我们只能将我们的库,采用动态链接。如果加上static呢?

我们发现如果此时你只有动态库,是无法进行动态链接的。如果你既想动态链接又想静态链接,我一般就需要提供两种版本的库文件。

如果我们静态库和动态库都有,那么gcc/g++优先链接哪一个呢?

一定是优先链接动态库,默认链接的就是动态库。所以你想静态链接就必须手动添加 -static。 

         

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值