一、缓冲区
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>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
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接口函数被打印了两份的现象。
5、仿写File
5.1、MyStdio.h
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__
#include <string.h>
#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(&fp->outbuffer[fp->out_pos],s,len);
fp->out_pos += len;
if(fp->flag & FLUSH_NOW)
{
write(fp->fileno,s,len);
fp->out_pos = 0;
}
else if(fp->flag & 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 & 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;
}
二、文件系统
前面我们学习到的东西,全部都是在内存当中,但并不是所有的文件都被打开,大量的文件就在磁盘上静静的躺着,这批文件非常多,杂,乱,我们必须要对这些磁盘文件进行管理,我们把做这部分管理工作的操作系统模块称之为文件系统。现在我们把视角从内存迁移到磁盘上来看。
1、什么是磁盘
磁盘是计算机主要的存储介质,可以存储大量的二进制数据,并且断电后也能保持数据不丢失。早期计算机使用的磁盘是软磁盘(Floppy Disk,简称软盘),如今常用的磁盘是硬磁盘(Hard disk,简称硬盘)。
2、磁盘的物理结构
硬盘结构包括: 盘片、磁头、盘片主轴、控制电机、磁头控制器、数据转换器、接口、缓存等几个部份。. 所有的盘片 (一般硬盘里有多个盘片,盘片之间平行)都固定在一个主轴上。盘片的表面涂有磁性物质,这些磁性物质用来记录二进制数据。因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面。在每个盘片的存储面上都有一个磁头,磁头与盘片之间的距离很小 (所以剧烈震动容易损坏),磁头连在一个磁头控制器上,统一控制各个磁头的运动。. 磁头沿盘片的半径方向动作,而盘片则按照指定方向高速旋转,这样磁头就可以到达盘片上的任意位置了。
3、磁盘的存储结构
扇区(sector)
:盘片被分成许多扇形的区域磁道(track)
:盘片上以盘片中心为圆心,不同半径的同心圆柱面(cylinder)
:硬盘中,不同盘片相同半径的磁道所组成的援助磁头(head)
:每个磁盘都有两个面,每个面都有一个磁头
磁盘上存储的基本单位是扇区,一般是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
:11H
:3S
:14
上述磁盘的每一个扇区的大小是512字节,但是有一个问题,OS表示每一次访问512字节很小,效率差,因此OS对进进行再一次抽象,以8个扇区为单位,整合为一个OS所认为的存储单元,所以大小就变成了4KB。OS在读写数据的时候,就会去这个存储单元中找。(IO的基本单位是4KB)
5、inode
5.1、inode说明
在Linux操作系统中,文件的元信息和内容是分离存储的,其中保存元信息的结构称之为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一的编号,即inode号。也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了inode编号。
在命令行当中输入ls -i
,即可显示当前目录下各文件的inode编号。
5.2、Linux ext2文件系统
- 计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于分区的其它区域,文件系统会根据分区的大小将其划分为一个个的块组(Block Group)。
注意:
- 启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。
- 文件 = 内容 + 属性,二者都是数据,都要存储。Linux采用的是将内容和属性数据分开存储的方案,内容在block中(4KB),内容是可以无限增多的。属性数据在inode中(128字节),文件的属性是稳定的。
每个组块都有着相同的组成结构,每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。
-
④、
inode Table
:以128字节为单位,进行inode属性的保存。inode属性里面有一个inode编号,一般而言,一个文件,一个inode,一个inode编号。 -
⑤、
Block Bitmap
:这里按位记录着Date Block(数据块)哪个被占用,哪个没被占用,每个bit位为0表示没被占用,为1表示被占用。 -
⑦、
Group Descriptor Table(GDT)
:对块组进行描述,包含了有多少inode,起始的inode编号,有多少个inode被使用,有多少block被使用,还剩多少等待信息。 -
⑧、
Super Block
:就是我们文件系统的顶层数据结构,记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- 先查找inode Bitmap,找到一个比特位没有被使用,把此比特位由0置为1,在inode Table中找到对应的inode节点,向里面写入对应的属性,并且为它分配数据块,把数据写到数据块中,同时修改block Bitmap,并且建立inode和block的映射关系,最终返回该文件的inode。
-
找到自己的目录的inode,再找到自己目录的blocks,然后根据文件名的唯一性,以及它与inode的映射关系,找到对应的inode编号,然后再根据inode编号找到它对应的Block group,然后将该文件所对应的inode Bitmap和block Bitmap由1置0,就完成了文件的删除。最后在文件所处的,目录中,把文件名和inode所对应的映射关系去掉,此时这个文件就被删掉了。
-
所以平时拷贝文件所花费的时间很长,而删除东西几秒钟就完事了,原因在于删除只是把标记该文件对应的属性和数据块相关的位图由1置0即可。
-
因此,Linux并没有真正的清除数据,只是将inode Bitmap和block Bitmap由1置0,就相当于删除了。此外,想要恢复整个删除的数据也很容易,只要知道了这个inode,通过一些工具,把这个inode对应的inode Bitmap和block Bitmap由0恢复成1即可。
- 因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请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。
与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。看如下的示例:
问1:什么是硬连接数?
问2:硬链接有什么用呢?
看如下我重新创建了一个目录和文件:
为什么目录被创建出来,默认的硬连接数是2呢?
我们cd进入创建的目录,会发现目录中自动创建两个文件 . 和 …,仔细看这个inode编号,会发现 . 和mydir的inode编号是一样的,综上,自己本身的目录名mydir和自己本身的inode有一个映射关系,且任何一个目录里头都有一个 . ,它通过自己所处的目录和inode建立一个硬链接,所以目录的默认硬链接数是2。
- 仔细看上图,会发现mydir目录下的 … 文件的inode和上级目录date22的inode是一样的,而mydir目录下的 . 文件和当前目录mydir的inode是一样的,综上,. 和 … 分别对应当前路径和上级路径。
所以我们也可以根据系统的硬连接数,不进入文件,从而估算出文件的目录数(一个目录下相邻的子目录数 = 该目录的硬连接数 – 2)。因此,硬链接的一个作用就是进行路径切换。
6.3、软硬链接的区别
区别如下:
- 软连接是一个独立文件,有自己独立的inode和inode编号。硬链接不是一个独立文件,它和它的目标文件使用的是同一个inode。
- 软连接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。
6.4、软硬链接的删除
建议用unlink来删除软硬连接的文件(unlink也可以删除普通文件,与rm没什么区别)
6.5、文件的三个时间
在Linux中,我们可以使用命令 stat 文件名来查看对应文件的信息:
当我们修改文件内容时,文件的大小一般会随之改变,所以Modify的改变会带动Change一起改变,但丢该文件属性一般不会影响文件内容,所以一般情况下Change的改变不会带动Modify的改变。此外,我们可以使用touch命令把这三个时间都更新到最新状态。(当一文件存在时使用touch命令,此时touch命令的作用变为更新文件信息)。
三、动静态库
file 可执行程序
1、静态库的制作
一套完成的库包含1、库文件本身(二进制文件,人看不懂)2、头文件(文本类型,暴露库文件中的接口)3、说明文档。
/lib64 库文件的存放目录(有些是/usr/lib)
/usr/include 头文件的存放目录
1.1、静态库的生成
1、将所有库文件编译为.o(可重定向二进制目标文件),用户拿到每个模块的.o文件,自行链接即可。将所有的.o打包就是库。
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
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)
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
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 路径下建立软链接
用于指定查找共享库(动态链接库)时除了默认路径(./lib和./usr/lib)之外的其他路径。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sqy/108-linux/MyLib/test/mylib/lib
当然这个环境变量在下次重新登录就没了,如果想让这个环境变量永久生效,可以把这个环境变量添加到登录相关的启动脚本里,下面两个都行,但是不建议,如果真要改,多开几个终端,防止改了之后登不上Linux:
vim ~/.bash_profile
vim ~/.bashrc
然后创建文件,写入库的路径
2.5、动态库的优缺点
优点:
缺点:
3、动静态库的总结
制作动静态库:
1、将所有的源文件编译为.o可重定向目标文件;
2、制作动静态库的本质就是将所有.o和头文件“打包”,静态库使用ar –rc,动态库使用-shared和gcc -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进行投诉反馈,一经查实,立即删除!