引言:
我们常常说的打开文件打开文件,真的就是我们打开的吗?一个文件如果被打开了没有进行任何操作岂不是白白浪费资源吗?站在操作系统的角度,没有什么文件是直接被用户打开的,用户只是给了一个触发条件,打开文件这个操作是操作系统来处理的,将范围缩小,打开文件肯定是某一个进程需要读取数据时所用到的,因此也可以说进程打开文件。
文件操作:
C语言标准库为我们提供了各式各样的文件操作函数,例如fopen、fclose、fread、fwrite等一系列函数。这些函数内部封装了系统调用open、write、close等等,之所以要进行封装,一方面是为了让用户可以低成本地操作文件,另一方面是为了保证程序可移植性。
文件管理:
一个进程或许会同时打开多个文件,那么操作系统就必须对这些隶属于一个进程的文件进行管理,Linux下通常把被进程打开的文件用名为file的结构体描述。进程控制块(PCB)中会存有一个指针,该指针指向一个指针数组,数组内部存储了每一个file结构体的地址,当进程要读取某一个文件时,就可以通过访问此数组找到目标文件。
文件描述符:
上面提到的指针数组下标是有特殊称谓的,叫做文件描述符,在底层一个进程要寻找目标文件就是通过文件描述符fd来进行寻找的,我们在C语言阶段学到的FILE*类型实质上是一个结构体指针,FILE结构体内部封装了fd。
文件描述符是从0开始的整数,为一个文件分配描述符的时默认情况选择未使用的最小下标。
图示:
C程序开始运行时默认打开三个文件,标准输入stdin、标准输出stdout、标准错误stderr,系统给它们分配的文件描述符分别是0、1、2
标准输出和标准错误区别:
虽然标准输出和标准错误所对应的硬件都是显示器,但是它们相互独立,互不影响。
你肯定知道Linux命令行中的>重定向操作符吧,它其实就是把标准输出重定向到指定目标文件。
//c程序————————编译后可执行成为名为test
int main(){
printf("printf\n");
perror("perror");
return 0;
}
//命令行
./test>txt
如果只是单纯地运行./test文件,显示器上会显示printf和perror信息,这没有什么问题,但是一旦加入了重定向操作符,你会发现显示器上只有perror的信息,printf的内容跑到txt文件里去了。这就说明>默认情况只对标准输出有效。同时也就证明了标准错误和标准输出所对应的显示器是不冲突的。
如果想要将标准错误也重定位到txt中去,可以在命令行中输入
./test>txt 2>txt
缓冲区:
IO操作的速度很大程度上取决于IO的次数,而不是数据量的大小,用户常常会通过键盘输入数据,如果一旦输入就马上将数据输出到外设中,其实效率是很低下的,因为用户每一次输入的数据对计算机来说是很小的,频繁的IO会导致用户端的反应变慢,造成不好的体验,因此引出了缓冲区技术,所谓缓冲区就是数据的暂存区,负责帮助输入设备将数据给到输出设备。
将缓冲区中的内容给到输出设备的这一步操作叫做缓冲区刷新,缓冲区刷新有三种策略,分别是立即刷新、按行刷新、全满刷新,如果用户强制刷新或是进程退出,缓冲区也会立即刷新。
显示器设备按行刷新,磁盘文件全满刷新
int main(){
printf("hello\n");
fork();
return 0;
}
运行输出一行hello,没有疑问,这里的fork没有任何意义。
int main(){
close(1);
FILE* fp=fopen(……);
printf("hello\n");
fork();
close(fp);
return 0;
}
运行输出了两行hello,你肯定感觉到奇怪吧。为什么这里就是输出两行呢,原因就是上面提到的缓冲区针对显示器和磁盘的刷新策略不同。
第一版代码片段由于缓冲区针对显示器,是行刷新,printf中\n被检测到后就立刻刷新了,最后当子进程创建时缓冲区里什么都没有,当然就只是输出一行hello了。
第二版代码片段的缓冲区针对磁盘,是全满刷新,当子进程创建时缓存区中有hello内容,父子进程结束时需要都需要刷新缓冲区,但是先后顺序未知,又要保证进程的独立性,子进程会拷贝缓冲区的内容在进行刷新,就输出两行hello了。其实每一个进程都有自己的缓冲区来保证进程独立性。
文件系统:
计算机工作时会使用到大量的文件,文件常常存储与磁盘上,因此想要更好地理解文件系统,需要先简单的了解磁盘的逻辑存储结构。
我们可以把磁盘想象成一个很大的数组,数据以数组的方式被管理。
由于磁盘空间非常大,直接管理起来效率低下。因此操作系统会对磁盘进行分区,分成大小不等的几块区域,每一个分区内部也分为不同的区块,我们这里重点需要知道Data blocks和inode Table,分别用于存储文件的内容和文件的属性,其中inode中每一个块区都有inode编号,作为一个文件的唯一标识(一个文件,一个inode,一个inode编号)。
在操作系统的视角下,对文件的一切操作都需要先找到文件,怎么找呢?就是通过文件的inode编号进行搜索。
ls -li可以查看文件的inode编号
软链接:
是一个独立文件,具有自己的inode编号,内容是指向对应文件的路径,可以理解为windows下的快捷方式。
使用***ln -s [sour] [des]***命令创建一个软链接
(第一列即inode编号)
硬链接:
不是一个独立文件,用的是目标文件的inode编号,相当于起了一个别名
使用***ln [sour] [des]***命令创建一个硬链接
(ls -li的第三列数字即该文件的硬链接数)
动静态库:
在学习C语言的过程,我们其实一直在用库函数来实现目的,如printf就是最简单的库函数,库函数是人在C标准下写的,那么我们也可以编写库来给别人使用。
静态库:
编写了2个头文件和源文件
//hello.h
//hello.c //输出hello world
//print.h
//print.c //输出print
编写makefile
//
libmine.a:hello.o print.o //只需要打包目标文件即可
ar -rc libmine.a hello.o print.o
/*静态库的名称必须前缀为lib后缀为.a
ar -rc 的意思是将两个目标文件打包成为一个libmine.a库*/
hello.o:hello.c
gcc -c hello.c -o hello.o
print.o:print.c
gcc -c print.c -o print.o
//以上所有库文件就已经打包好了,还缺少所对应的头文件
.PHONY:mine
mine:
mkdir -p mine/include // 存头文件
mkdir -p mine/lib //存库文件
cp -rf *.h mine/include
cp -rf *.a mine/lib
.PHONY:clean
clean:
rm -rf *.o libmine.a
这样就可以发布我们的静态库了,
我们可以查看到生成的mine库文件中含有include和lib两个目录,一个存有hello.h、print.h,另一个存有libmine.a
现在就是要安装我们编写的静态库了,这里有两种方式,一种是将库安装到系统路径下,一种是在当前路径强制寻找,更推荐第二种(安装到系统路径可能会造成污染)。
第一种方法需要将include下的文件拷贝到/usr/include下,将lib下的文件拷贝到/lib64下,在使用gcc编译的时候需要输入gcc main.c -lmine标识使用mine库(去掉前后缀加l)
第二种方法
gcc main.c -I ./mine/include -L ./mine/lib -lmine
-I标识头文件路径,-L标识库文件路径
虽然我们认为此时生成的可执行文件所依赖的库是静态库,但是我们使用ldd命令查看时会发现并没有以.a为后缀的文件,取而代之的是一个动态库,原因是因为gcc默认编译是链接动态库的,如果所依赖的文件只有静态库,那么会将静态库的内容加载到一个系统动态库中。
因此要真正形成静态库链接的可执行文件,需要添加**-static选项**(加与不加文件大小相差巨大)
动态库:
如果说静态库链接可以理解为编译过程中把所依赖的库文件的内容拷贝到可执行程序的代码区中,那么动态库链接可以理解为编译时把所依赖的库文件地址存储在地址空间的共享区中,在运行时通过地址跳转到相应的方法再跳转返回。
相比于静态库,动态库的优点在于提高内存的利用率(可被多个进程共享),避免重复内容的出现,大大减小了可执行文件的大小。
如何创建一个自己的动态库呢?
//hello.h print.h
//hello.c print.c
Makefile
libmine.so:hello.o print.o
gcc -shared -fPIC hello.o print.o -o libmine.so
hello.o:hello.c
gcc -c hello.c -o hello.o
print.o:print.c
gcc -c print.c -o print.o
//-shared 告诉gcc生成一个动态库
//-fPIC表示位置无关代码(大致了解)
.PHONY:dynamic
dynamic:
mkdir -p dynamic/include
mkdir -p dynamic/lib
cp *.so dynamic/lib
cp *.h dynamic/include
.PHONY:clean
clean:
rm -rf *.o *.so
dynamic就是我们最后要发布的动态库
按照ILl告诉gcc进行编译,可以发现编译通过但是运行失败
会出现文件找不到的错误
原因:
静态库链接后可执行文件就与静态库文件完全没有联系了,静态库中的方法均已拷贝至可执行文件中,直接运行可执行文件不会出错。
但是动态库不同,动态库链接的可执行文件编译好后仍然与库文件保持联系,之所以编译能过是因为所填写得选项是面向gcc的,程序变为进程之后就有操作系统控制了,操作系统并不知道你所编写的第三方库在什么位置,就当然无法运行啦。
解决办法有三种
1、添加环境变量LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:【path】
2、设置配置文件/etc/ld.so.conf.d
在此目录下创建一个保存动态库文件的路径的普通文件,最后在root权限下ldconfig更新
3、将.so文件拷贝到系统共享库文件下/lib64