一、缓冲区

1、缓冲区概念

缓冲区的本质就是一段用作缓存内存

2、缓冲区的意义

节省进程行数据IO的时间进程使用fwrite函数数据拷贝到缓冲区或者外设中。

3、缓冲区刷新策略

3.1、立即刷新(无缓冲)——ffush()

情况很少,比如调用printf后,手动调用fflush刷新缓冲区。

3.2、行刷新(行缓冲)——显示器

显示器需要满足人的阅读习惯,故采用行刷新的策略而不是全缓冲的策略

虽然全缓冲的刷新方式可以大大降低数据IO的次数节省时间。但若数据暂存于缓冲区,等缓冲区满后再刷出,当人阅读时面对屏幕中出现的一大堆数据,很难不懵逼。所以显示器采用行刷新的策略,既保证了人的阅读习惯,又使得数据IO效率不至于太低

3.3缓冲区满后刷新(全缓冲)——磁盘文件

3.4、特殊的刷新情况

用户强制刷新或进程退出

4、同一份代码输出屏幕或者文件上不同

#include <stdio.h>
#include <unistd.h&gt;
#include <sys/types.h&gt;
#include <sys/stat.h&gt;
#include <string.h&gt;
int main()
{
    printf("hello printfn");//先打印stdout缓冲区中
    fprintf(stdout,"hello fprintfn");
    fputs("hello fputsn",stdout);
 
    const char* msg="hello writen";
    write(1,msg,strlen(msg));
    fork();//生成子进程
    return 0;
}

在这里插入图片描述

运行上方代码生成可执行文件,在显示器上是正常打印,但是将运行结果重定向文本,会发现C接口的打印函数打印了两次

这个现象和缓冲区有关,从侧面说明了缓冲区并不存在内核中,否则write也会打印两次用户语言层面提供的缓冲区在FILE指向stdin/stdout/stderr中,FILE结构体会包含fd和缓冲区。需要强制刷新时,调用fflush(FILE);关闭文件时,调用fclose(FILE*)。参数为FILE就是为了刷新FILE指向的FILE结构体中的缓冲区。

上方代码现象的解释

1、stdout默认采用行刷新策略,每条打印函数都带了’n’,所以在fork之前,数据已经全部被打印到了显示器,缓冲区被并没有数据,当代码运行fork创建子进程时,子进程对应的缓冲区当然也是没有任何数据。

2、当写入的是磁盘文件时,采用的是全缓冲的刷新策略,程序运行fork时,缓冲区并没有被写满,数据仍存在于缓冲区中,当然被创建的子进程也拷贝了一份缓冲区的数据,当父子进程退出时,父子进程缓冲区中的数据将被刷新。所以出现了C接口函数被打印了两份的现象。

3、上面的过程系统调用write无关,write没有FILE,使用的是fd,当然就没有C提供的缓冲区。

5、仿写File

5.1、MyStdio.h

#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__

#include <string.h&gt;
#define SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4
typedef struct IO_FILE
{
    int fileno;
    char outbuffer[SIZE];//输出缓冲区
    int flag;//标记即时刷新,行刷新,还是全缓冲
    int out_pos;

}_FILE;

_FILE* _fopen(const char* filename, const char * flag);
int _fwrite(_FILE* fp,const char* s,int len);
void _fclose(_FILE* fp);

#endif 

5.2、MyStdio.c

#include "MyStdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>

#define FILE_MODE 0666
_FILE* _fopen(const char* filename, const char * flag)
{
    assert(filename);
    assert(flag);
    int f = 0;
    int fd = -1;
    if(strcmp(flag,"w") == 0) //只写方式打开
    {
        f = O_CREAT | O_WRONLY | O_TRUNC;
        fd = open(filename,f,FILE_MODE);
    }
    else if(strcmp(flag,"a") == 0)//追加方式打开
    {
        f = O_CREAT | O_WRONLY | O_TRUNC;
        fd = open(filename,f,FILE_MODE);
    }
    else if(strcmp(flag,"r") == 0)//只读方式打开
    {
        f = O_RDONLY;
        fd = open(filename,f);
    }
    else 
    {
        return NULL;
    }
    //文件打开失败
    if(fd == -1) return NULL;

    _FILE* fp = (_FILE*)malloc(sizeof(_FILE));
    //申请空间失败
    if(fp == NULL) return NULL;
    fp->fileno = fd;
    //fp->flag = FLUSH_LINE;
    fp->flag = FLUSH_ALL;
    fp->out_pos = 0;
    return fp;
 
}
int _fwrite(_FILE* fp,const char* s,int len)
{
    memcpy(&amp;fp->outbuffer[fp->out_pos],s,len);
    fp->out_pos += len;

    if(fp->flag &amp; FLUSH_NOW)
    {
        write(fp->fileno,s,len);
        fp->out_pos = 0;
    }
    else if(fp->flag &amp; FLUSH_LINE)
    {
        if(fp->outbuffer[fp->out_pos-1] == 'n')
        {
            write(fp->fileno,s,len);//目前先考虑结尾是斜杠n的情况,中间有斜杠n的不考虑
            fp->out_pos = 0;
        }
    }
    else if(fp->flag &amp; FLUSH_ALL)
    {
        if(fp->out_pos == SIZE)
        {
            write(fp->fileno,s,len);
            fp->out_pos = 0;
        }
    }
    
    return len;
}

void _fflush(_FILE* fp)
{
    if(fp->out_pos > 0)
    {
        write(fp->fileno,fp->outbuffer,fp->out_pos);
        fp->out_pos = 0;
    }
}
void _fclose(_FILE* fp)
{
    if(fp == NULL) return;
    _fflush(fp);
    close(fp->fileno);
    free(fp);
}

5.3、main.c

#include "MyStdio.h"
#define myfile "test.txt"
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    _FILE* fp = _fopen(myfile,"a");
    if(fp == NULL) return 1;
    const char *msg = "hello worldn";
    int cnt = 5;
    while(cnt)
    {
        _fwrite(fp,msg,strlen(msg));
        sleep(1);
        cnt--;
    }
    _fclose(fp);
    return 0;
}

main.c中msg指向字符串有无n,这个程序对应的刷新策略不同

二、文件系统

前面我们学习到的东西,全部都是在内存当中,但并不是所有的文件都被打开,大量的文件就在磁盘上静静的躺着,这批文件非常多,杂,乱,我们必须要对这些磁盘文件进行管理我们把做这部分管理工作操作系统模块称之为文件系统。现在我们视角内存迁移到磁盘上来看。

1、什么是磁盘

磁盘是计算机主要的存储介质,可以存储大量的二进制数据,并且断电后也能保持数据不丢失。早期计算使用的磁盘是软磁盘(Floppy Disk,简称软盘),如今常用的磁盘是硬磁盘(Hard disk,简称硬盘)。

2、磁盘的物理结构

在这里插入图片描述

硬盘结构包括: 盘片、磁头、盘片主轴控制电机、磁头控制器、数据转换器接口缓存等几个部份。. 所有的盘片 (一般硬盘里有多个盘片,盘片之间平行)都固定一个主轴上。盘片的表面涂有磁性物质,这些磁性物质用来记录二进制数据。因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面。在每个盘片的存储面上都有一个磁头,磁头与盘片之间的距离很小 (所以剧烈震动容易损坏),磁头连在一个磁头控制器上,统一控制各个磁头的运动。. 磁头沿盘片的半径方向动作,而盘片则按照指定方向高速旋转,这样磁头就可以到达盘片上的任意位置了。

3、磁盘的存储结构

在这里插入图片描述

磁盘上存储基本单位扇区,一般是512字节,数据是在扇区上存储的。在读写磁盘的时候磁头找的是某一个(哪一个磁头)的某一个磁道(哪一个柱面——距离圆心的半径**)的某一个扇区**(磁道上的一段)。只要我们能够找到磁盘上的盘面,柱面(磁道),扇区,即CHS地址我们就能找到磁盘上的任意一个存储单元

4、磁盘的逻辑抽象结构

理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,想想磁带:

在这里插入图片描述

磁带被卷起来时,就像磁盘一样是圆形的,里面存储的是数据,当我们把磁带拉直后,其就是线性的。我们把盘片想象成为线性的结构,就可以把盘片当成是数组定位有关sector(扇区),只要找到下标LBA(逻辑地址)就可以了。因此对磁盘的管理,就转化成为了对数组空间管理如图

在这里插入图片描述

因此内存中的数据想要往磁盘里写入,在内存中只需要知道有关地址叫LBA(逻辑地址),然后将LBA地址映射转换为CHS地址,再将内存中的数据配合CHS写到磁盘里,即可完成磁盘的写入

问:如何将LBA地址转化为CHS地址?

现在假设磁盘有2片(4个面),一个面能存1000个数据,每个面有20个磁道,已知LBA地址是3234,那么写入过程如下

  • 3234 / 1000 = 3 —— 在第3面(H是3)
  • 3234 % 1000 = 234
  • 234 / 20 = 11 —— 在第11个磁道(C是11)
  • 234 % 20 = 14 —— 在第14个扇区(S是14)

综上

  • C:11
  • H:3
  • S:14

上述磁盘的每一个扇区的大小是512字节,但是有一个问题,OS表示一次访问512字节很小,效率差,因此OS对进进行再一次抽象,以8个扇区为单位整合为一个OS所认为的存储单元,所以大小就变成了4KB。OS在读写数据的时候,就会去这个存储单元中找。(IO的基本单位是4KB)

在这里插入图片描述

上述这样操作两个好处

  1. 提高IO效率
  2. 不要让软件(OS)设计硬件(磁盘)具有相关性,换句话说,就是解耦合!

5、inode

5.1、inode说明

在这里插入图片描述

在这里插入图片描述

在Linux操作系统中,文件的元信息内容分离存储的,其中保存信息的结构称之为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一编号,即inode号。也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了inode编号

命令行当中输入ls -i即可显示当前目录下各文件的inode编号

在这里插入图片描述

  • 注意: 无论是文件内容还是文件属性,它们都是存储在磁盘当中的。

5.2、Linux ext2文件系统

在这里插入图片描述

注意

每个组块都有着相同的组成结构,每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。

在这里插入图片描述

问4:当我们创建一个文件时,OS操作系统做了什么

创建一个文件的时候,一定是在一个目录下。

问5:删除一个文件,OS操作系统做了什么呢?

问6:为什么拷贝文件的时间耗费很长,而删除文件却很快?

  • 因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的。

6、软硬链接

6.1、软链接

我们可以通过以下命令创建一个文件的软链接。

ln -s my.txt my.txt.soft

在这里插入图片描述

通过 ls -i -l 命令可以看到,软连接的inode号和源文件的inode号是不同的,并且软连接文件的大小源文件大小要小的多。

在这里插入图片描述

连接又叫符号链接,软连接文件相当于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含源文件路径名,所以软连接文件的大小要比源文件小得多。软连接就类似于Windows操作系统当中的快捷方式

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

但是软连接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软连接的内容了。

在这里插入图片描述

问:既然软连接是一个独立文件,inode是独立的,那么软连接的文件内容什么呢?

  • 软连接保存的内容就是指向的文件的所在路径!!!

6.2、硬链接

我们通过以下命令创建一个文件的硬链接。

在这里插入图片描述

硬链接就是单纯的在Linux指定的目录下,给指定的文件新增文件名和inode编号的映射关系

在这里插入图片描述

  • 通过ls -i -l命令我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是, 新创建的my.txt文件的硬链接数为1,可给my.txt文件建立硬链接后,其硬链接数变成了2。

与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。看如下示例

在这里插入图片描述

  • 我对可执行程序myfile建立硬链接,现在删除硬链接的源文件:

在这里插入图片描述

  • 总之,硬链接就是让多个不在或者同在一个目录下的文件名,同时能够修改同一个文件,其中一个修改后,所有与其有硬链接的文件都一起修改了。
    在这里插入图片描述

问1:什么是硬连接数?

问2:硬链接有什么用呢?

如下我重新创建了一个目录和文件:

在这里插入图片描述

为什么文件被创建出来,默认的硬连接数是1?

  • 如果硬链接数是0,那么就应该是被关闭的文件了,所以至少应该从1开始。此外,普通文件的文件名,本身就和自己的inode具有映射关系,且只有1个,所以文件的默认硬连接数是1。

为什么目录被创建出来,默认的硬连接数是2呢?

在这里插入图片描述

我们cd进入创建的目录,会发现目录中自动创建两个文件 . 和 …,仔细看这个inode编号,会发现 . 和mydir的inode编号是一样的,综上,自己本身的目录名mydir自己本身的inode有一个映射关系,且任何一个目录里头都有一个 . ,它通过自己所处的目录和inode建立一个硬链接,所以目录的默认硬链接数是2。

在这里插入图片描述

  • 仔细看上图,会发现mydir目录下的 … 文件的inode和上级目录date22的inode是一样的,而mydir目录下的 . 文件和当前目录mydir的inode是一样的,综上,. 和 … 分别对应当前路径和上级路径。

所以我们也可以根据系统的硬连接数,不进入文件,从而估算出文件的目录数(一个目录下相邻的子目录数 = 该目录的硬连接数 – 2)。因此,硬链接的一个作用就是进行路径切换

6.3、软硬链接的区别

区别如下

  1. 软连接是一个独立文件,有自己独立的inode和inode编号。硬链接不是一个独立文件,它和它的目标文件使用的是同一个inode。
  2. 软连接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。

6.4、软硬链接的删除

建议unlink来删除软硬连接的文件(unlink也可以删除普通文件,与rm没什么区别)

6.5、文件的三个时间

在Linux中,我们可以使用命令 stat 文件名来查看对应文件的信息:

在这里插入图片描述

这其中包含了文件的三个时间信息:

当我们修改文件内容时,文件的大小一般会随之改变,所以Modify的改变会带动Change一起改变,但丢该文件属性一般不会影响文件内容,所以一般情况下Change的改变不会带动Modify的改变。此外,我们可以使用touch命令把这三个时间都更新最新状态。(当一文件存在时使用touch命令,此时touch命令作用变为更新文件信息)。

三、动静态

使用ldd可以显示可执行程序依赖的库。

在这里插入图片描述

查看程序是动静态方法

file 可执行程序

在这里插入图片描述

1、静态库的制作

一套完成的库包含1、库文件本身(二进制文件,人看不懂)2、头文件文本类型暴露库文件中的接口)3、说明文档

/lib64        库文件的存放目录(有些是/usr/lib/usr/include  头文件的存放目录

1.1、静态库的生成

1、将所有库文件编译为.o(可重定向二进制目标文件),用户拿到每个模块的.o文件,自行链接即可。将所有的.o打包就是库

使用arrc多个.o进行打包ar是gun归档工具

gcc -c mymath.c
ar -rc libmymath.a  mymath.o

对应的makefile

libmymath.a:mymath.o
	ar -rc $@ $^
mymath.o:mymath.c
	gcc -c $^ 

.PHONE:clean
clean:
	rm -rf *.o *.a lib 

.PHONY:output
output:
	mkdir -p lib/include
	mkdir -p lib/mymathlib
	cp *.h lib/include 
	cp *.a lib/mymathlib

先把.c生成.o,再把.o打包成.a静态库。

使用ar -tv查看静态库中的内容:

在这里插入图片描述

1.2、用户如何使用静态

在这里插入图片描述

打包好的静态库文件给到用户,用户自己写一个main函数,即可使用output中的.h文件。

不过在编译时,需要执行如下命令

gcc -o main main.c -I ./lib/include/ -L ./lib/mymathlib/ -lmymath
-I:告诉编译器在./lib/include路径中找头文件
-L:告诉编译器在./lib/mymathlib路径找库
-l:跟库名称去掉前缀lib,去掉后缀.so或.a)

makefile

main:main.c 
	gcc -o main main.c -I ./lib/include/ -L ./lib/mymathlib/ -lmymath
.PHONY:clean
clean:
	rm -f main

使用编译器提供的库并行不需要带这些选项,是因为编译器有自己的环境变量,能够找到位于/lib64库文件的存放目录和/usr/include头文件的存放目录。

可以将静态库和头文件放入这些目录或其他相关目录下,这就是一般软件安装过程。但是不推荐(自己写的库什么水平没点数吗?放进去会污染标准库)。

2、动态库的制作

2.1、动态库的生成

同样的,将所有.o进行打包

makefile:

dy-lib=libmymethod.so 
static-lib=libmymath.a

.PHONY:all
all: $(dy-lib) $(static-lib)

$(static-lib):mymath.o
	ar -rc $@ $^
mymath.o:mymath.c
	gcc -c $^


$(dy-lib):mylog.o Printf.o
	gcc -shared -o $@ $^
mylog.o:mylog.c
	gcc -fPIC -c $^
Print.o:Printf.c
	gcc -fPIC -c $^
 

.PHONE:clean
clean:
	rm -rf *.o *.a  *.so lib  

.PHONY:output
output:
	mkdir -p mylib/include
	mkdir -p mylib/lib 
	cp *.h mylib/include 
	cp *.a mylib/lib 
	cp *.so mylib/lib

注意:这里小编动态库与静态库的制作放在一起了,可以只看动态库的制作

这样就得到了一个动态库libmymethod.so

2.2、将动态库和头文件合并

makefile:

在这里插入图片描述

2.3、用户如何使用动态

makefile:

main:main.c
	gcc -o main main.c -I ./mylib/include/ -L ./mylib/lib/ -lmymath -lmymethod
.PHONY:clean
clean:
	rm -rf main

动静态库的使用方式是一样的。

但是运行可执行程序报错:没有这个文件或目录

在这里插入图片描述

使用ldd命令发现缺少了自己写的动态库:因为makefile只是告诉编译器头文件和库的路径,编译通过,但是运行又不是编译器来运行,当然不知道详细库路径!

在这里插入图片描述

静态库能运行是因为静态链接是将所有内容全部拷贝到源文件。动态库编译/运行都需要这些路径,运行时需要通过加载器,告诉操作系统库路径在哪里。

2.4、解决加载找不到动态库的四种方法

方案:将动态库和头文件拷贝至对应的系统库路径(拷贝至/lib64)和头文件(拷贝至/usr/include)路径下(自己写的库不推荐,成熟的库可以推荐拷贝进去,否则会污染人家的库)

方案:在系统的默认的库路径/usr/lib64 路径下建立软链接

方案更改环境变量LD_LIBRARY_PATH

用于指定查找共享库(动态链接库)时除了默认路径(./lib和./usr/lib)之外的其他路径。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sqy/108-linux/MyLib/test/mylib/lib

当然这个环境变量在下次重新登录就没了,如果想让这个环境变量永久生效,可以把这个环境变量添加登录相关启动脚本里,下面两个都行,但是不建议,如果真要改,多开几个终端,防止改了之后登不上Linux:

vim ~/.bash_profile
vim ~/.bashrc

方案ldconfig

使用root进入 /etc/ld.so.conf.d

然后创建文件,写入库的路径

然后执行ldconfig就可以了

2.5、动态库的优缺点

优点

缺点

  • 运行时依赖,否则找不到库文件就会运行失败
  • 运行加载速度相较静态库慢一些
  • 需要对库版本之间的兼容性做出更多处理

3、动静态库的总结

制作动静态库:

1、将所有的源文件编译为.o可重定向目标文件;

2、制作动静态库的本质就是将所有.o和头文件打包”,静态库使用ar –rc,动态库使用-sharedgcc -fPIC

3、使用:include+.a或.so文件

静态库只能静态链接,动态库只能动态链接。一般需要提供动静态两种版本的库,gcc和g++优先默认使用动态库进行链接,想要静态链接,需要手动在编译指令后添加static选项

Linux操作系统中一定会存在动态库,操作系统中有很多命令是由C语言写的,它们采用动态链接。

无论是采用动态链接还是静态链接,程序在预编译的时候,都会把所包含的头文件进行展开这里展开的仅仅是库中声明;当程序在链接的时候,静态链接会将库函数的定义拷贝一份到程序代码段中,而动态链接会将动态库中所需的定义通过地址偏移量的方式加载到内存而不是可执行程序中,可执行程序运行时将这些定义通过页表映射至共享区,所以动静态库的体积存在巨大的差距。

原文地址:https://blog.csdn.net/VHhhbb/article/details/134501358

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_29926.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注