软硬链接
先看现象:先创建一个文件,并写入内容
建立软链接:
ln -s file_target1.txt file_soft.link
inode不一样,所以都是独立的文件:
建立硬链接:
ln file_target2.txt file_hard.link
发现硬链接的inode一样,且中间的数字为2
总结特征:
1.软链接是一个独立的文件,因为有独立的inode number
2.硬链接不是一个独立的文件,因为没有独立的inode number,用的是目标文件的inode
3.属性中有一列硬链接数。
软链接
软连接的内容:目标所指的路径
这跟windows下的快捷方式一样:
用路径也可以运行
把目标文件删掉后,软链接也跑不起来了
软链接有什么用?
路径比较多的话,可以迅速找到一个文件
这里的myls是ls的软链接,可以运行./myls运行ls
平常使用的各种库就是软链接
综上所述:与windows的快捷方式相似
硬链接
先看现象:会发现这个数字就是引用计数(磁盘级的引用计数:有多少个文件名字符串指向同一个inode)
根据inode一样可知:硬链接就是在指定的目录下,添加一个新的文件名与inode number的映射!(跟指针差不多,两个指针指向同一块空间)
硬链接有什么用?
为什么新创建的目录文件的引用计数是2、普通文件的引用计数是1呢?
每个目录文件里面都有".“和”. ."
一个".“指向当前路径,发现dir中的”.“的inode和dir目录本身的inode一样
在dir中创建一个文件,发现引用计数变成了3:dir本身,dir目录下的”.“,otherdir目录下的”. ."
这就是为什么cd . . 就是返回上级路径
看现象:发现Linux系统中,不能给目录建立硬链接
原因:root.hard就是"/"
Linux可以给自己建立硬链接,因为".“与”. .“名字是固定内置好的。(删不掉”.“与”. .“,是因为系统维护好的)
还有一个作用:备份。先建立硬链接,后删除源文件,但依旧能访问到源文件的内容(跟某软件删不掉的原理相似)
综上所述,硬链接的作用是:
1.构建Linux的路径结构,让我们可以使用”.“和”. ."来进行路径定位
2.一般用硬链接来做文件备份
动静态库
我们常用的都是C、C++的标准库:strcpy、string、STL…这些函数或类必须有实现
ldd可以看见所调用的库
查看C标准库就是软链接
一个程序运行不仅需要二进制可执行文件(a.out),还需要库。
Linux中的动静态库
Linux中 .so(动态库) .a(静态库)
windows中 .dll(动态库) .lib(静态库)
Linux中的指令基本都是C语言写的,都用一个C语言库
库
以下面两个所写的库为例子
mymath.h
#pragma once
int Add(int,int);
int Sub(int,int);
mymath.c
#include "mymath.h"
int Add(int x,int y)
{
return x+y;
}
int Sub(int x,int y)
{
return x-y;
}
下面两个是在fopen、fwrite、fclose、fflush简易版库
mystdio.h
#pragma once
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define uint32_t unsigned int
#define LINE_SIZE 1024 //缓冲区大小
//124是标记位
#define FLUSH_NOW 1 //直接刷新
#define FLUSH_LINE 2 //行刷新
#define FLUSH_FULL 4 //全缓冲
struct _myFILE
{
uint32_t flags; //标记位
int fileno;
//缓冲区
char cache[LINE_SIZE];
int cap; //容量大小
int pos; //下次写的位置
};
typedef struct _myFILE myFILE;
myFILE* my_fopen(const char* path,const char* flag);
ssize_t my_fwrite(myFILE* fp,const char* data,int len);
void my_fflush(myFILE* fp);
void my_fclose(myFILE* fp);
mystdio.c
#include "mystdio.h"
myFILE* my_fopen(const char* path,const char* flag)
{
int flag1 = 0;
int iscreate = 0; // 是否要创建文件
mode_t mode = 0666; // 权限
if(strcmp(flag,"r") == 0)
{
flag1 = (O_RDONLY);
}
else if(strcmp(flag,"w") == 0)
{
flag1 = (O_WRONLY | O_CREAT | O_TRUNC);
iscreate = 1; //需要创建文件
}
else if(strcmp(flag,"a") == 0)
{
flag1 = (O_WRONLY | O_CREAT | O_APPEND);
iscreate = 1; //需要创建文件
}
int fd = 0;
if(iscreate)
fd = open(path,flag1,mode);
else
fd = open(path,flag1);
if(fd < 0) return NULL;
myFILE* fp = (myFILE*)malloc(sizeof(myFILE));
if(!fp) return NULL;
fp->flags = FLUSH_LINE;
fp->fileno = fd;
fp->cap = LINE_SIZE;
fp->pos = 0;
return fp;
}
ssize_t my_fwrite(myFILE* fp,const char* data,int len) //ssize_t->有符号整数
{
memcpy(fp->cache+fp->pos,data,len); //假设直接拷贝成功
fp->pos+=len;
if((fp->flags&FLUSH_LINE)&&fp->cache[fp->pos-1] == '\n')
{
write(fp->fileno,fp->cache,fp->pos);
fp->pos = 0;
}
return len;
}
void my_fflush(myFILE* fp)
{
write(fp->fileno,fp->cache,fp->pos);
fp->pos = 0;
}
以一个故事为线:
老师布置了一个写库的作业,你已经写完了,舍友不会想要你的源代码。
你:因为命名风格一样,老师可能发现抄袭,所以你直接把所有文件编成.o(所有方法的实现),发给舍友。
默认形成同文件.o
gcc -c [文件名.c]
你舍友表示不会用,.o全是二进制文件,不知道如何用。于是你就把头文件给发过去了。头文件中包含着所有的函数声明。
但是你舍友还是不会用。
你:实现在.o里,.h跟实现手册一样。
于是又根据.h的方法写了main.c
#include "mymath.h"
#include "mystdio.h"
#include <stdio.h>
#include <string.h>
int main()
{
int a = 10;
int b = 20;
printf("%d+%d=%d\n",a,b,Add(a,b));
myFILE* fp = my_fopen("./myfile.txt","w");
if(fp == NULL) return 1;
const char* message = "hello Linux\n";
my_fwrite(fp,message,strlen(message));
my_fclose(fp);
return 0;
}
发现报错信息是没有找到函数实现,因为只编译了你的文件,舍友的文件没有被编译
把main.c->main.o
总结:头文件是一个手册,提供函数的声明,告诉用户怎么用。.o提供实现,我们只需要补上一个main,调用头文件提供的方法,然后和.o进行链接,就能形成可执行
故事进入第二阶段:
老师看你和你的舍友能力挺强,于是安排新任务,需要写100个源文件。
于是你故技重施,把100个.o发给舍友,然后舍友误删了几个,编不过了。
可以打包一起发送,但是舍友不会解包。你说行吧,用ar命令
r是replace,c是create,相当于把所有的.o文件打包,并且舍友不需要解包。
ar -rc [文件名].a *.o
可以直接编译,因为系统自动帮助你把main.c->main.o。
综上所述:所谓的库就是打包*.o
库的作用:提高开发效率。(你舍友只拿着函数说明书就能运行,不用自己编写源代码)
静态库 && 安装库
依旧使用ar命令,ar是gnu归档工具
ar -rc [文件名].a [文件名].o
故事进入第三阶段:
把.h文件放进include中,把.a文件放进lib中
给舍友发过去了lib,舍友想直接用这个库,需要安装到系统里(只需要把头文件拷贝到头文件的搜索里,.a文件也是一样)。
sudo cp mylib/include/*.h /usr/include/
sudo cp mylib/lib/*.a /lib64
安装后,就跟下载的安装包类似,发给舍友的lib可以不要了
这样头文件可以用<>
#include <mymath.h>
#include <mystdio.h>
发现依旧找不到函数
因为我们平常使用的是C/C++的库,gcc/g++默认是认识C/C++库。
libmyc.a --》别人写的(第三方库) --》gcc/g++不认识 --》> -l[库名] 表示链接某库
发现找不到库(-l后面可以带空格 -l libmyc.a)
依旧找不到库,gcc认为去前缀,去掉后缀,剩下来的才是库的名字
这样就可以运行了。
以上就完成了库的安装,不推荐把自己的库放进去。因为可能会污染库。
不安装,如何用库呢?
因为main.c与.h和.a不处在同一路径下,所以.c文件找不到.h和.a文件
-I是include的意思(这里是大写的i),程序员自己指定头文件的路径。
gcc也是进程,在运行的时候,pwd拼上后面的路径,找到了头文件所在地
gcc [文件名].c -I [路径]
运行不了,链接报错
gcc表示我只认系统默认的库文件/lib64 /usr/lib64,你的库(第三方的库)在哪不知道
-L是lib的意思,告诉gcc我的库在哪
gcc [文件名].c -I [路径] -L [路径]
还是链接报错
因为还没有告诉gcc你要找哪个库,就跟/lib64目录下有很多库,你得告诉gcc找哪个库
库名依旧是去掉前缀和后缀
gcc [文件名].c -I [路径] -L [路径] -l[库名]
总结:
问题来了:
头文件为什么不用指定具体名称?
头文件已经指定了,你的.c文件里面指定的
不指明-I也可以在.c文件里指明
注意<>表示在系统的路径下去找,现在要在当前的路径下找所以得用""
#include "mylib/include/mymath.h"
#include "mylib/include/mystdio.h"
还有一个问题:我们用的libmyc.a库,为什么没有显示出来?
编译时只指明静态库(libmy.c)时,形成可执行程序时能动态链接的就动态链接,只能静态链接的,就把实现拷贝到可执行程序(a.out)中。
-static的表示全部强制静态链接
动态库
想要实现动态库要加个选项fPIC
gcc -fPIC -c 文件名.c
动态库在发开中用的最频繁的,编译器自己支持形成动态库,直接使用gcc/g++
gcc *.o -o [库名].so
报错了,因为库中不能存在main函数
告诉gcc我要形成动态库:-shared
gcc -shared *.o -o [库名].so
也把动态库拷贝到库中
gcc main.c -I mylib/include/ -L mylib/lib -l myc
编译能编过,但是无法运行。
发现libmyc.so找不到
因为只告诉了gcc/g++,形成a.out是gcc的工作。但没有告诉操作系统,./a.out运行操作系统不知道库在哪。
综上:
动态库:动态库要在程序运行的时候,要找到动态库加载并运行!
静态库:编译期间,已经将库中的代码拷贝到我们的可执行程序的内部了!加载和库就没有关系了!
系统默认在/lib64下寻找,链接和运行时都在/lib64下寻找。
第一种方法:把动态库拷贝到/lib64路径下(不建议)
第二种方法:在/lib64中建立一个软链接
第三种:操作系统中有个环境变量LD_LIBRARY_PATH,其中存储的就是动态库的搜索路径(不推荐)
echo $LD_LIBRARY_PATH
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[需要添加的路径]
第四种:ld的意思为Load,so的意思是库,conf的意思是配置文件,.d的意思是目录
ls /etc/ld.so.conf.d
这个目录当中存储的是在系统当中的配置文件。
把当前要用的库的路径添加到其中,就永久生效了
首先是root才能修改
其次在目录中命名一个文件,命名必须要以.conf结尾
再把路径写入其中
在root的权限下让配置文件生效:
ldconfig
就可以运行了
动态库 VS 静态库
动静态库的大小差很多
-static 的意义是强制将我们的程序静态链接,这就要求我们链接的任何库都必须提供对应的静态库版本。
用第三方库
用别的人库,应该提供什么呢?
sudo yum install ncurses
下载开发版本:
sudo yum install ncurses-devel
下面的代码具体在这个网址C语言实现简易文本编辑器(新手向)
#include <ncurses.h>
int main(){
int key=0;
initscr();
noecho();
keypad(stdscr,TRUE);
key=getch();
while(key!='$'){
printw("%d\n",key);
key=getch();
refresh();
}
endwin();
}
因为是第三方库,所以编译使用命令时需要链接
gcc test.c -l ncurses
动态库加载
下面是调用动态库的简略版过程。
动态库被加载之后,要映射到当前进程的堆栈之间的共享区。
如果其他进程也需要该动态库,是不用重复加载的,都用这一份动态库。也称共享库
执行可执行程序,编译成功,没有加载运行时,二进制代码中有"地址"吗?
看现象:
#include <stdio.h>
int Sum(int top)
{
int i = 1;
int ret = 0;
for(;i<=top;i++)
{
ret += 1;
}
return ret;
}
int main()
{
int top = 100;
int result = Sum(top);
printf("result:%d\n",result);
return 0;
}
看反汇编
objdump -S a.out > code.s
可以看出是有地址的。函数名也变成了地址
在可执行形成的时候,也规划好了地址所用的空间
Linux中的可执行程序都是ELF格式的可执行程序,形成的二进制时是有自己的固定格式的。固定格式中有elf可执行程序的头部,里面有很多可执行程序的属性。
如何编制呢?
假设编址范围是0000 0000 ~ FFFF FFFF (虚拟地址,又称为逻辑地址)
地址种类:绝对编址(从0000 0000依次往后排) 又称为平坦模式。上图就是绝对编址
相对编址:相对地址+偏移量 (所有函数的起始地址都为0,里面存的都是偏移量0,1,2…)
elf头部信息
下图画红线的是Linux中的加载器:先让操作系统把库加载到内存中,执行库里面的方法,把可执行程序拷贝到内存中。拷贝时有虚拟地址。程序中有头部信息,一定要被加载器进行解释、扫描代码、找到main的入口,并且在elf头文件中加载各个区(程序被划分各个区的)。
ELF+加载器:可以让我们找到各个区域区域的起始和结束地址与main函数的入口地址
第二个问题:
进程 = 内核数据结构 + 代码和数据。先有内核数据结构后再有代码和数据添加到内存中。mm_struct 是结构体对象,成员变量是由code_start、code_end…组成,那么成员变量的初始值从哪里来呢?
首先肯定不是操作系统固定规划的,你写了一个正文5行的hello world,他写了50w行的CSGO,正文段的结束肯定不一样。
只有可执行程序自己知道有多大,在elf头部信息中有代码段多大的信息,填入到mm_struct中。
综上:虚拟地址空间由OS、编译器、加载器共同组成。
故事:你在学校的宿舍比如是513的2号床(物理地址),学号是111(虚拟地址),线下找你肯定是拿着宿舍的地址找你,校方线上查看档案肯定是拿着学号找。所以你在学校有你的学号(虚拟地址),占有宿舍空间(物理地址),形成了映射关系。
CPU存在一个寄存器:pc指针,会保存要执行下一条指令的地址。(pc指向哪里,CPU就要执行哪里的代码)
CPU开始运行:CPU问pc指针我接下来要运行哪条指令。pc指针给出地址,因为是虚拟地址要根据进程查页表,然后找到了真实的物理地址去运行,同时把下一条指令的虚拟地址读到pc指针中,重复上述过程。
库函数与上述可执行文件的编址也一样,除了没有main函数
磁盘中的地址都是虚拟地址
只有物理地址是无法使用的,需要mm_struct中的栈和堆之间开辟一段空间(因为物理内存中已经有库了,所以开辟多少空间的大小也是确定的)填入库的虚拟地址,然后与页表建立映射关系。
假设虚拟地址是00000000~FFFFFFFF
在磁盘中,库中每一个函数都可以表示为myc:偏移量(lib库的地址+偏移量)
磁盘中的偏移量,加载到mm_struct中的偏移量是不变的。
综上:库被映射到地址的什么位置是不重要的,只要找到库的起始的地址,就能通过偏移量找到函数。
库函数调用,是在地址空间内来回调用。
上面说的库被加载进内存,那如何知道库有没有被加载呢?
首先我们要知道库不止一个,不止一个说明要管理,管理:先组织,再描述
内存中有一个描述库的结构体:什么时候被加载进去,谁映射的…
当有其他程序需要用到这个库的时候,遍历列表发现其有没有被加载,有加载直接映射。
总结动态库加载的过程:可执行程序先加载内存中,运行到需要调用库的地方,如果库不在,就缺页中断,根据路径找到库加载进来,通过映射得到我的库的起始地址,库的地址+偏移量找到方法(函数)。
静态库直接放进正文代码中。
-fPIC表示:让动态库独立加载,不和可执行程序拷贝在一起。在物理地址中任意加载,在地址空间中任意映射