缓冲区的本质就是一段用作缓存的内存。
节省进程进行数据IO的时间。进程使用fwrite等函数把数据拷贝到缓冲区或者外设中。
情况很少,比如调用printf后,手动调用fflush刷新缓冲区。
显示器需要满足人的阅读习惯,故采用行刷新的策略而不是全缓冲的策略。
虽然全缓冲的刷新方式,可以大大降低数据IO的次数,节省时间。但若数据暂存于缓冲区,等缓冲区满后再刷出,当人阅读时面对屏幕中出现的一大堆数据,很难不懵逼。所以显示器采用行刷新的策略,既保证了人的阅读习惯,又使得数据IO效率不至于太低
用户强制刷新或进程退出。
#include
#include
#include
#include
#include
int main()
{
printf("hello printf\n");//先打印至stdout缓冲区中
fprintf(stdout,"hello fprintf\n");
fputs("hello fputs\n",stdout);
const char* msg="hello write\n";
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提供的缓冲区。
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__
#include
#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
#include "MyStdio.h"
#include
#include
#include
#include
#include
#include
#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);
}
#include "MyStdio.h"
#define myfile "test.txt"
#include
#include
#include
#include
#include
int main()
{
_FILE* fp = _fopen(myfile,"a");
if(fp == NULL) return 1;
const char *msg = "hello world\n";
int cnt = 5;
while(cnt)
{
_fwrite(fp,msg,strlen(msg));
sleep(1);
cnt--;
}
_fclose(fp);
return 0;
}
main.c中msg指向的字符串有无\n,这个程序对应的刷新策略不同。
前面我们学习到的东西,全部都是在内存当中,但并不是所有的文件都被打开,大量的文件就在磁盘上静静的躺着,这批文件非常多,杂,乱,我们必须要对这些磁盘文件进行管理,我们把做这部分管理工作的操作系统模块称之为文件系统。现在我们把视角从内存迁移到磁盘上来看。
磁盘是计算机主要的存储介质,可以存储大量的二进制数据,并且断电后也能保持数据不丢失。早期计算机使用的磁盘是软磁盘(Floppy Disk,简称软盘),如今常用的磁盘是硬磁盘(Hard disk,简称硬盘)。
硬盘结构包括: 盘片、磁头、盘片主轴、控制电机、磁头控制器、数据转换器、接口、缓存等几个部份。. 所有的盘片 (一般硬盘里有多个盘片,盘片之间平行)都固定在一个主轴上。盘片的表面涂有磁性物质,这些磁性物质用来记录二进制数据。因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面。在每个盘片的存储面上都有一个磁头,磁头与盘片之间的距离很小 (所以剧烈震动容易损坏),磁头连在一个磁头控制器上,统一控制各个磁头的运动。. 磁头沿盘片的半径方向动作,而盘片则按照指定方向高速旋转,这样磁头就可以到达盘片上的任意位置了。
扇区(sector)
:盘片被分成许多扇形的区域磁道(track)
:盘片上以盘片中心为圆心,不同半径的同心圆柱面(cylinder)
:硬盘中,不同盘片相同半径的磁道所组成的援助磁头(head)
:每个磁盘都有两个面,每个面都有一个磁头磁盘上存储的基本单位是扇区,一般是512字节,数据是在扇区上存储的。在读写磁盘的时候,磁头找的是某一个面(哪一个磁头)的某一个磁道(哪一个柱面——距离圆心的半径**)的某一个扇区**(磁道上的一段)。只要我们能够找到磁盘上的盘面,柱面(磁道),扇区,即CHS地址,我们就能找到磁盘上的任意一个存储单元。
理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,想想磁带:
磁带被卷起来时,就像磁盘一样是圆形的,里面存储的是数据,当我们把磁带拉直后,其就是线性的。我们把盘片想象成为线性的结构,就可以把盘片当成是数组,定位有关sector(扇区),只要找到下标LBA(逻辑块地址)就可以了。因此对磁盘的管理,就转化成为了对数组空间的管理,如图:
因此内存中的数据想要往磁盘里写入,在内存中只需要知道有关地址叫LBA(逻辑块地址),然后将LBA地址映射转换为CHS地址,再将内存中的数据配合CHS写到磁盘里,即可完成磁盘的写入。
问:如何将LBA地址转化为CHS地址?
现在假设磁盘有2片(4个面),一个面能存1000个数据,每个面有20个磁道,已知LBA地址是3234,那么写入的过程如下:
综上:
C
:11H
:3S
:14上述磁盘的每一个扇区的大小是512字节,但是有一个问题,OS表示每一次访问512字节很小,效率差,因此OS对进进行再一次抽象,以8个扇区为单位,整合为一个OS所认为的存储单元,所以大小就变成了4KB。OS在读写数据的时候,就会去这个存储单元中找。(IO的基本单位是4KB)
上述这样操作有两个好处:
在Linux操作系统中,文件的元信息和内容是分离存储的,其中保存元信息的结构称之为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一的编号,即inode号。也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了inode编号。
在命令行当中输入ls -i
,即可显示当前目录下各文件的inode编号。
注意:
每个组块都有着相同的组成结构,每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。
①、Boot Block
:与开机有关,里面包括了各种开机信息,有分区表,以及软件的位置信息。
②、Block Group
:整个时文件系统所划分的不同的组,每个组的结构构成都相同。
③、Date blocks
:以块为单位,进行文件的保存(所占的空间最大,80%左右)。
④、inode Table
:以128字节为单位,进行inode属性的保存。inode属性里面有一个inode编号,一般而言,一个文件,一个inode,一个inode编号。
⑤、Block Bitmap
:这里按位记录着Date Block(数据块)哪个被占用,哪个没被占用,每个bit位为0表示没被占用,为1表示被占用。
⑥、inode Bitmap
:这里按位记录着inode的使用情况。每个bit位为0表示没被占用,为1表示被占用。
⑦、Group Descriptor Table(GDT)
:对块组进行描述,包含了有多少inode,起始的inode编号,有多少个inode被使用,有多少block被使用,还剩多少等待信息。
⑧、Super Block
:就是我们文件系统的顶层数据结构,记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
问4:当我们创建一个文件时,OS操作系统做了什么?
创建一个文件的时候,一定是在一个目录下。
问5:删除一个文件,OS操作系统做了什么呢?
找到自己的目录的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即可。
问6:为什么拷贝文件的时间耗费很长,而删除文件却很快?
我们可以通过以下命令创建一个文件的软链接。
ln -s my.txt my.txt.soft
通过 ls -i -l 命令可以看到,软连接的inode号和源文件的inode号是不同的,并且软连接文件的大小比源文件的大小要小的多。
软连接又叫符号链接,软连接文件相当于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软连接文件的大小要比源文件小得多。软连接就类似于Windows操作系统当中的快捷方式。
但是软连接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软连接的内容了。
问:既然软连接是一个独立文件,inode是独立的,那么软连接的文件内容是什么呢?
我们通过以下命令创建一个文件的硬链接。
硬链接就是单纯的在Linux指定的目录下,给指定的文件新增文件名和inode编号的映射关系。
与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。看如下的示例:
问1:什么是硬连接数?
问2:硬链接有什么用呢?
看如下我重新创建了一个目录和文件:
为什么文件被创建出来,默认的硬连接数是1?
为什么目录被创建出来,默认的硬连接数是2呢?
我们cd进入创建的目录,会发现目录中自动创建两个文件 . 和 …,仔细看这个inode编号,会发现 . 和mydir的inode编号是一样的,综上,自己本身的目录名mydir和自己本身的inode有一个映射关系,且任何一个目录里头都有一个 . ,它通过自己所处的目录和inode建立一个硬链接,所以目录的默认硬链接数是2。
所以我们也可以根据系统的硬连接数,不进入文件,从而估算出文件的目录数(一个目录下相邻的子目录数 = 该目录的硬连接数 - 2)。因此,硬链接的一个作用就是进行路径切换。
区别如下:
建议用unlink来删除软硬连接的文件(unlink也可以删除普通文件,与rm没什么区别)
在Linux中,我们可以使用命令 stat 文件名来查看对应文件的信息:
这其中包含了文件的三个时间信息:
当我们修改文件内容时,文件的大小一般会随之改变,所以Modify的改变会带动Change一起改变,但丢该文件属性一般不会影响文件内容,所以一般情况下Change的改变不会带动Modify的改变。此外,我们可以使用touch命令把这三个时间都更新到最新状态。(当一文件存在时使用touch命令,此时touch命令的作用变为更新文件信息)。
使用ldd可以显示可执行程序依赖的库。
查看程序是动静态的方法:
file 可执行程序
一套完成的库包含1、库文件本身(二进制文件,人看不懂)2、头文件(文本类型,暴露库文件中的接口)3、说明文档。
/lib64 库文件的存放目录(有些是/usr/lib)
/usr/include 头文件的存放目录
1、将所有库文件编译为.o(可重定向二进制目标文件),用户拿到每个模块的.o文件,自行链接即可。将所有的.o打包就是库。
使用ar -rc对多个.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查看静态库中的内容:
将打包好的静态库文件给到用户,用户自己写一个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头文件的存放目录。
可以将静态库和头文件放入这些目录或其他相关目录下,这就是一般软件的安装过程。但是不推荐(自己写的库什么水平没点数吗?放进去会污染标准库)。
同样的,将所有.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
makefile:
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只是告诉编译器头文件和库的路径,编译能通过,但是运行又不是编译器来运行,当然不知道详细库路径!
静态库能运行是因为静态链接是将所有内容全部拷贝到源文件。动态库编译/运行都需要这些路径,运行时需要通过加载器,告诉操作系统库路径在哪里。
方案一:将动态库和头文件拷贝至对应的系统库路径(拷贝至/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就可以了
优点:
更加节省内存并减少页面交换;
库文件与程序文件独立,只要输出接口不变,更换库文件不会对程序文件造成任何影响,
因而极大地提高了可维护性和可扩展性;
不同编程语言编写的程序只要按照函数调用约定就可以调用同一个库函数;
适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
缺点:
制作动静态库:
1、将所有的源文件编译为.o可重定向目标文件;
2、制作动静态库的本质就是将所有.o和头文件“打包”,静态库使用ar -rc,动态库使用-shared和gcc -fPIC
3、使用:include+.a或.so文件
静态库只能静态链接,动态库只能动态链接。一般需要提供动静态两种版本的库,gcc和g++优先默认使用动态库进行链接,想要静态链接,需要手动在编译指令后添加-static选项。
Linux操作系统中一定会存在动态库,操作系统中有很多命令是由C语言写的,它们采用动态链接。
无论是采用动态链接还是静态链接,程序在预编译的时候,都会把所包含的头文件进行展开,这里展开的仅仅是库中的声明;当程序在链接的时候,静态链接会将库函数的定义拷贝一份到程序的代码段中,而动态链接会将动态库中所需的定义通过地址偏移量的方式加载到内存而不是可执行程序中,可执行程序运行时将这些定义通过页表映射至共享区,所以动静态库的体积存在巨大的差距。