一、复习文件相关知识
我们前面已经学习过文件方面的知识了,接下来就进行一下总结:
1、空文件在磁盘也是要占用空间的(空文件也是有属性信息的,只不过空文件内容为0字节罢了)
2、文件=内容+属性
3、文件操作=对内容操作+对属性操作+对内容和属性的操作
4、标定一个文件必须使用:文件路径+文件名,因为具有唯一性
5、没有指明对应文件路径,默认在当前路径进行文件访问。当前路径:进程的工作目录(进程从根目录到当前所在位置的路径)
6、我们前面学习C语言的时候,学习了fopen、fclose、fread、fwrite等接口,当我们写完这些代码之后,进行编译就形成了二进制可执行程序。但是,如果我们没有运行代码,文件对应的操作就没有被执行。所以,对文件的操作,本质上就是:对进程的操作
7、一个文件没有被打开,可以直接进行文件访问吗?肯定是不能的。一个文件要被访问,必须先被打开!(操作就是用户进程+OS)
8、磁盘那么多文件,是不是所有文件都被打开了呢?这也肯定不是的。
文件也就分为两种:1、被打开的文件;2、没有被打开的文件
综合上面几点结论,代码得出一个最终结论:
文件操作的本质:进程和被打开文件的关系
二、复习C文件相关操作
1、复习知识点
我们既然学习了C语言和C++语言,那么应该可以想到,两个语言都有对应的文件操作,但是操作的函数接口不一样。不知如此,java、php、python、go等等语言都有自己对应的文件操作接口,也就是说:每种语言的文件操作都不一样
接下来我们就来仔细分析:
文件在哪里呢?
在磁盘上面
磁盘属于硬件
所以,磁盘是归OS(操作系统)管理的
那么所有人想要访问磁盘/文件就都不能绕过OS
那么,想要对文件进行操作,就必须使用OS提供的接口,也磁盘/文件归OS管理
OS为了方便磁盘/文件被访问,就提高了文件级别的系统调用接口
可是,语言有很多种,但操作系统只有一个。每个语言的底层都是操作系统
所以得出结论:
上层语言不管怎么变化
1、库函数底层必须调用系统调用接口
2、库函数可以千变万化,但是底层不会改变
所以,我们想要学好文件操作,将来快速了解任何一门语言,就要学好底层的知识,我们学习不会改变的知识,以不变应万变
2、复习操作
这里我就先用表格将操作列举出来:
操作符 | 作用 |
---|---|
w | 向文件写入内容,文件不存在会自动创建文件。w单纯的打开文件不写入时,C语言是会清空原文件的全部内容 |
w+ | 可读可写,但是不会创建文件 |
r | 读取文件内容,文件必须存在,否则报错 |
r+ | 可读可写,但是不存在报错 |
a | 在文件原有内容的末尾追加新的内容,文件不存在会自动创建 |
a+ | 可读可写,读取内容可以在文件任意地方读取,写入只能是追加在末尾写入 |
wb | b是二进制的意思,以二进制的方式写入 |
rb | b是二进制的意思,以二进制的方式读取 |
这里就说这么多,其他的操作有兴趣可以深入了解
操作1:“w“创建不存在文件
1 #include <stdio.h>
2 int main()
3 {
4 FILE* fp=fopen("log.txt","w");
5 if(fp==NULL)
6 {
7 perror("fopen");
8 return 1;
9 }
10 fclose(fp); //我没做任何处理,直接关闭
11 return 0;
12 }
操作2:”w“向文件写入
1 #include <stdio.h>
2 int main()
3 {
4 FILE* fp=fopen("log.txt","w");
5 if(fp==NULL)
6 {
7 perror("fopen");
8 return 1;
9 }
10 int cnt=5;
11 while(cnt)
12 {
13 fprintf(fp,"%s:%d\n","hello!",cnt--); //向文件写入
15 }
16 fclose(fp);
17 return 0;
18 }
操作3:”r“读取文件内容
1 #include <stdio.h>
2 #include <string.h>
3 int main()
4 {
5 FILE* fp=fopen("log.txt","r");
6 if(fp==NULL)
7 {
8 perror("fopen");
9 return 1;
10 }
11 char buffer[64];
12 while(fgets(buffer,sizeof(buffer)-1,fp)!=NULL)//因为fgets是C语言的库函数,其中的s表示string字符串,所以文件内容的结尾会有\0,这里-1去掉\0
13 {
14 buffer[strlen(buffer)-1]=0;//这里把buffer的最后换行置0,因为puts可能打印的时候带了\n,而这里再加一个\n就会多空处一行
15 puts(buffer);
16 }
17 }
操作4:”a“向文件追加内容
1 #include <stdio.h>
2 int main()
3 {
4 FILE* fp=fopen("log.txt","a"); //因为log.txt文件存在的原因,这里直接把w改成a就行了
5 if(fp==NULL)
6 {
7 perror("fopen");
8 return 1;
9 }
10 int cnt=5;
11 while(cnt)
12 {
13 fprintf(fp,"%s:%d\n","hello!",cnt--);
15 }
16 fclose(fp);
17 return 0;
18 }
那么,C语言的操作就复习到这里了,接下来我们就学习新内容了
三、文件的系统调用接口
1、open
我们已经知道了,上面C语言使用的库函数接口是封装底层系统调用接口的,那么我们就来学习一下底层的系统调用接口
C语言的fopen就是再open的基础之上进行封装得来的
大部分文件的系统调用接口,所需要的头文件都是这三个
open作用:打开或者创建一个文件设备
三个参数
pathname:文件名/路径->只有文件名就是默认当前路径
flags:文件操作所需要对应的宏操作选项(下面仔细讲),按标记位传参
mode:文件的权限
接下来看看返回值:
这里就要提出新概念:
文件描述符
:文件描述符是一个整数,文件操作成功就返回文件描述符,失败就返回-1
这个下面也会仔细讲
接下来我们就仔细研究一下flags:
上面的三个是flags最常用的三个宏选项,分别是:可读,可写,即可读又可写。下面的宏选项操作我们用到再讲
接下来我们先不用open选项,先来了解一个新内容——标记位
C语言传标记位,是一个int表示一个标记位。但是,int有32个bit位,那么可以通过比特位来传递选项,达到不同的目的
注意:一个比特位对应一个选项。而且比特位的位置不能重复
举例:
7 // 我没有指明路径
8 #define FILE_NAME(number) "log.txt"#number
9
10 // 每一个宏,对应的数值,只有一个比特位是1,彼此位置不重叠
11 #define ONE (1<<0) //0x1 这种注释的16进制宏定义的方法是系统内部实现的方法,而许多人认为这样不如位操作方法方便
12 #define TWO (1<<1) //0x2
13 #define THREE (1<<2) //0x4
14 #define FOUR (1<<3) //0x8
15
16 void show(int flags)
17 {
18 if(flags & ONE) printf("one\n");
19 if(flags & TWO) printf("two\n");
20 if(flags & THREE) printf("three\n");
21 if(flags & FOUR) printf("four\n");
22 }
23 int main()
24 {
25 show(ONE);
26 printf("-----------------------\n");
27 show(TWO);
28 printf("-----------------------\n");
29 show(ONE | TWO);
30 printf("-----------------------\n");
31 show(ONE | TWO | THREE);
32 printf("-----------------------\n");
33 show(ONE | TWO | THREE | FOUR);
34 printf("-----------------------\n");
35 }
接下来就来使用open系统调用接口
我们先采用open的两个参数方法,先不考虑mode参数
1 #include <stdio.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <assert.h>
8 int main()
9 {
10 int fd=open("log.txt",O_WRONLY); //O_WRONLY表示以只写的方式进行文件操作
11 assert(fd!=-1);
12 (void)fd;//防止警告
13 close(fd);
14 return 0;
15 }
我们先删除log.txt文件,然后来看看结果:
我们可以看到,open居然没有创建文件,为什么会这样呢?我们以只写的方式进行文件操作不应该创建文件吗?
其实,底层如果是这样实现的话,是不会创建文件的,C语言能够只读创建是做了封装的!
我们想要系统调用也能够创建文件,就需要加点东西了
1 #include <stdio.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <assert.h>
8 int main()
9 {
10 int fd=open("log.txt",O_WRONLY | O_CREAT);//O_WRONLY表示以只写的方式进行文件操作 | O_CREAT表示文件不存在自动创建
11 assert(fd!=-1);
12 (void)fd;//防止警告
13 close(fd);
14 return 0;
15 }
文件的权限是乱的!!!
所以,我们在创建文件的时候,必须指定文件权限,不然很危险!!!
1 #include <stdio.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <assert.h>
8 int main()
9 {
10 int fd=open("log.txt",O_WRONLY | O_CREAT,0666);//O_WRONLY表示以只写的方式进行文件操作 | O_CREAT表示文件不存在自动创建
11 assert(fd!=-1);
12 (void)fd;//防止警告
13 close(fd);
14 return 0;
15 }
这里我们就进行uamsk的更改:
1 #include <stdio.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <assert.h>
8 int main()
9 {
10 umask(0);
11 int fd=open("log.txt",O_WRONLY | O_CREAT,0666);//O_WRONLY表示以只写的方式进行文件操作 | O_CREAT表示文件不存在自动创建
12 assert(fd!=-1);
13 (void)fd;//防止警告
14 close(fd);
15 return 0;
16 }
晤,做了这么多的准备工作,那么我们就开始用open系统调用接口进行写入了
2、write
终于可以写入了,我们来看看这个函数的文档:
老规矩,三个参数
fd:我们向哪一个文件写
buf:我们写入的时候,缓存区数据在哪里
count:缓存区中,数据的字节个数
返回值其实就是count的值,但是我们这里不考虑返回值,后面的章节再来讲这个ssize_t
注意:buf的返回值是void*类型的。我们前面说的文本类和二进制类是C语言提供给我们的;而系统可不管什么文本,二进制,在操作系统看来,都是二进制!!!
1 #include <stdio.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <assert.h>
8 int main()
9 {
10 umask(0);
11 int fd=open("log.txt",O_WRONLY | O_CREAT,0666);//O_WRONLY表示以只写的方式进行文件操作 | O_CREAT 表示文件不存在自动创建
12 assert(fd!=-1);
13 (void)fd;//防止警告
14 char buffer[64];//我们自己定义的缓存区
15 int cnt=5;
16 while(cnt)
17 {
18 sprintf(buffer,"%s:%d\n","hello:",cnt--);//sprintf把数据格式化成字符串
19 write(fd,buffer,strlen(buffer));//系统调用接口,这里不能+1带上\0!!!我们要都就是字符串有效内容,不需要\0
19 //这是文件,不是C语言,C语言的规则关我文件什么事,这样做会产生乱码等错误
20 }
21 //printf("fd:%d\n",fd);
22 close(fd);
23 return 0;
24 }
我们继续
接下来我们在换内容打印:
打印aaaaa
这里居然还有上次剩下的数据。说明了open函数并没有帮我们清空文件内部,也就是说:C语言不仅仅帮我们创建文件,还帮我们清空了内容
那么我们系统调用函数也想清空内容也得加点内容了:
1 #include <stdio.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <assert.h>
8 int main()
9 {
10 umask(0);
11 int fd=open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
11 //O_WRONLY表示以只写的方式进行文件操 | O_CREAT表示文件不存在自动创建 | O_TRUNC对文件内容做清空处理
12 assert(fd!=-1);
13 (void)fd;//防止警告
14 char buffer[64];//我们自己定义的缓存区
15 int cnt=5;
16 while(cnt)
17 {
18 sprintf(buffer,"%s:%d\n","aaaaa",cnt--);//sprintf把数据格式化成字符串
19 write(fd,buffer,strlen(buffer));
20 }
21 //printf("fd:%d\n",fd);
22 close(fd);
23 return 0;
24 }
所以,我们C语言一个简简单单的”w“选项,底层操作系统做了O_WRONLY | O_CREAT | O_TRUNC等等操作
我们已经完成了写入操作,那么接下来就趁热打铁,学习一下怎么追加内容:
接下来就是读取操作了
3、read
这里的参数就不多介绍了,和上面的write参数意义是一样的
1 #include <stdio.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <assert.h>
8 int main()
9 {
10 umask(0);
11 int fd = open("log.txt",O_RDONLY);
12 assert(fd!=-1);
13 void(fd);
14 char buffer[1024];
15 ssize_t ret = read(fd,buffer,sizeof(buffer)-1);
16 if(ret > 0) buffer[ret]=0;//设置\0
17 printf("%s",buffer);
18 close(fd);
19 return 0;
20 }
4、小结
C语言库函数 | 系统接口调用 |
---|---|
fopen | open |
fclose | close |
fwrite | write |
fread | read |
fseek | lseek |
… | … |
任何语言的文件操作库函数都是对底层的系统调用接口进行封装
四、文件描述符
我们前面知道了
文件操作的本质:进程和被打开文件的关系
那么进程可以打开多个文件吗?肯定是可以的
既然进程可以打开多个文件,那么系统一定会存在大量被打开的文件
被打开的文件要不要被OS管理呢?也是要的
怎么管理呢?先描述,再组织
所以,OS为了管理对应的打开文件,必须要为打开文件创建对应的内核数据结构,这个内核数据结构就是struct file{}
,这个结构体包含了文件的大部分属性
1、初步认识
前面我们打开一个进程的时候,打印fd的值是3,我们在来仔细研究:
1 #include <stdio.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <assert.h>
8 int main()
9 {
10 umask(0);
11 int fd0=open("log0.txt",O_WRONLY|O_CREAT);
12 int fd1=open("log1.txt",O_WRONLY|O_CREAT);
13 int fd2=open("log2.txt",O_WRONLY|O_CREAT);
14 int fd3=open("log3.txt",O_WRONLY|O_CREAT);
15 printf("fd0:%d\n",fd0);
16 printf("fd1:%d\n",fd1);
17 printf("fd2:%d\n",fd2);
18 printf("fd3:%d\n",fd3)
19 }
2、两个问题
问题1:文件描述符fd从3开始,一直是连续的小整数,但是0、1、2去哪里了呢?
问题2:文件描述符fd从3开始,一直是连续的小整数,我们学习的知识中,只有数组的下标是连续的小整数,fd是数组下标吗?
我们依次来解决,先来看问题1:为什么fd的值从3开始,而不是0
我们前面学习过,操作系统会打开三个默认的输入输出流:
stdin:标准输入->键盘
stdout:标准输出->显示器
stderr:标准错误->显示器
知识点
我们C语言文件操作用的是FILE*是一个结构体
而底层函数接口调用用的是文件描述符
我们又知道C语言库函数在底层上面
所以得出结论:C语言的FILE*结构体内部一定有一个字段是底层的文件描述符,因为库函数是对系统接口调用的封装,既然系统调用接口用了文件描述符,那么上层一定要调用文件描述符
这三个文件被默认打开,那么会不会是这三个文件占用了0、1、2文件描述符呢?
我们来看看:
printf("stdin->fd: %d\n", stdin->_fileno);//_fileno可以拿到我们想要的文件描述符
printf("stdout->fd: %d\n", stdout->_fileno);
printf("stderr->fd: %d\n", stderr->_fileno);
所以,综上所述:
OS默认打开了三个标准输入输出流文件,这三个文件占用这文件描述符0、1和2。我们在不动这三个文件描述符/文件的前提下,创建任何文件的文件描述符都是从3开始的,一直递增
接下来,我们来解决问题2
解决上面的问题1之后,我们对问题2有了另外的疑惑:为什么文件描述符是从0开始的呢?
我们知道了文件操作是:进程与被打开文件的关系
那么进程是要创建task_struct(进程控制块),所以task_struct里有一个struct files_struct* files
指针,这个指针指向一个struct files_struct
结构体,这个结构体是专门构成进程和结构对应关系的结构体
而struct files_struct结构体里面有一个struct file* fd_array[]
的数组(也叫做:文件描述符表),这个指针数组里面存放着指针,从数组下标0开始,数组的每一个下标位置存放着一个文件的地址。因为标准输入输出流被打开,所以数组下标0、1、2的里面存放的就是这三个流的地址。
这也证明了,文件描述符的本质就是:数组的下标
。所以,我们创建的新文件,默认文件描述符是3,因为3表示struct file* fd_array[]数组下标为3的位置,数组的这个位置就存放着我们新创建的文件。每创建一个文件,文件描述符就对应跟着变化
3、文件描述符的分配规则
我们直接来举几个例子,任何通过例子观察出结论:
正常情况下,就是上面的情况,但是,如果我们关闭0、1、2这三个文件描述符对应的文件,也就是关闭那三个输入输出流会怎么样呢?我们来看看:
所以我们可以总结出文件描述符的分配规则:
文件描述符分配规则:数组从小到大,按照顺序寻找最小的,并且没有被占用的位置(下标)
接下来我们再来看看特殊情况:
这里很好理解:
五、重定向
上面的例子中,本来应该打印到显示器的内容,却打印到了文件里面,这叫做输出重定向
这就是重定向操作,我们接下来就来学习一下重定向操作
在此之前,先来学习/总结前面的知识点
1、重定向的分类:输出重定向(>);输入重定向(<);追加重定向(>>);
2、重定向的本质:上层的文件描述符不变,在内核(文件描述符表)中更改文件描述符对应位置中存放的struct file* 地址
在学习重定向之前,我们先来学习一下一个函数
1、 dup2函数
我们上面先关闭,再创建新文件的操作是不是太麻烦了呢?
dup2函数就是来简化这个操作的!
先来看文档:
makes newfd be the copy of oldfd, closing newfd first if necessary
int dup2(int oldfd, int newfd);
我们结合上面的例子来仔细看看这个函数:
dup2有两个参数,一个oldfd,一个newfd。其中前面的makes newfd be the copy of oldfd这句话的意思是:
newfd拷贝oldfd。结合我们上面的例子来看,我们要想让文件描述符1不指向stdout,而是指向我们的新建文件。所以:fd=1->新建文件,那么就是把fd的内容拷贝到1里面去(注意不能弄反了),所以,oldfd就是fd,而newfd就是1
我们还可以更加简单的理解为:
newfd和oldfd不管谁拷贝谁,最终剩下来的是oldfd。而我们上面的例子中,代码最后的结果就是fd和1的内容都是fd的,1也指向了fd指向的内容。所以oldfd就是上面的fd
更改上面例子的代码,采用dup2函数
这就是输出重定向
接下来我们来分别看看追加重定向和输入重定向
追加重定向:
可以看到,追加重定向和我们的文件描述符没有关系,和文件描述符指向的文件(对象)也没有关系,只是和我们的打开方式有关系
接下来看看输入重定向:
我们前面写过一个myshell,接下来再myshell里面实现我们的重定向功能
2、myshell里面实现重定向功能
1 #include <stdio.h>
2 #include <string.h>
3 #include <stdlib.h>
4 #include <unistd.h>
5 #include <ctype.h>
6 #include <sys/types.h>
7 #include <sys/wait.h>
8 #include <assert.h>
9 #include <fcntl.h>
10
11 #define NUM 1024
12 #define OPT_NUM 64
13
14 #define NONE_R 0//默认没有重定向
15 #define INPUT_R 1//输入重定向标记
16 #define OUTPUT_R 2
17 #define APPEND_R 3
18
19 int rtype = NONE_R;//默认没有重定向
20 char* rfile = NULL;//重定向文件名称
21
22 char lineCommand[NUM];
23 char *myargv[OPT_NUM]; //指针数组
24 int lastCode = 0;
25 int lastSig = 0;
26
27 #define jumpspace(start) do{ while(isspace(*start)) ++start; }while(0)//这里不带分号,因为这个宏我们可以直接拿来做if语句的判断条件
28 void commandCheak(char* ck)
29 {
30 assert(ck!=NULL);
31 char* start = ck;
32 char* end = ck + strlen(ck);//这里不用在最后加1,假设有4个字符,下标4就是最后的\0
33 while(start!=end)
34 {
35 if(*start == '>')
36 {
37 *start='\0';
38 ++start;
39 if(*start=='>')
40 {
41 rtype=APPEND_R;
42 ++start;//追加重定向要再加一次
43 }
44 else
45 {
46 rtype=OUTPUT_R;
47 }
48 jumpspace(start);
49 rfile=start;
50 break;
51 }
52 else if(*start == '<')
53 {
54 *start='\0';
55 ++start;
56 jumpspace(start);
57 rtype = INPUT_R;
58 rfile = start;
59 break;
60 }
61 else
62 {
63 ++start;
64 }
65 }
66 }
67 int main()
68 {
69 while(1)
70 {
71 rtype=NONE_R;
72 rfile=NULL;
73 // 输出提示符
74 printf("用户名@主机名 当前路径# ");
75 fflush(stdout);
76
77 // 获取用户输入, 输入的时候,输入\n
78 char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
79 assert(s != NULL);
80 (void)s;
81 // 清除最后一个\n , abcd\n
82 lineCommand[strlen(lineCommand)-1] = 0; // ?
83 //printf("test : %s\n", lineCommand);
84
85 // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
86 commandCheak(lineCommand);//将一个字符串分隔为两个字符串
87 // 字符串切割
88 myargv[0] = strtok(lineCommand, " ");
89 int i = 1;
90 if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
91 {
92 myargv[i++] = (char*)"--color=auto";
93 }
94
95 // 如果没有子串了,strtok->NULL, myargv[end] = NULL
96 while(myargv[i++] = strtok(NULL, " "));
97
98 // 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
99 // 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
100 if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
101 {
102 if(myargv[1] != NULL) chdir(myargv[1]);
103 continue;
104 }
105 if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
106 {
107 if(strcmp(myargv[1], "$?") == 0)
108 {
109 printf("%d, %d\n", lastCode, lastSig);
110 }
111 else
112 {
113 printf("%s\n", myargv[1]);
114 }
115 continue;
116 }
117 // 测试是否成功, 条件编译
118 #ifdef DEBUG
119 for(int i = 0 ; myargv[i]; i++)
120 {
121 printf("myargv[%d]: %s\n", i, myargv[i]);
122 }
123 #endif
124 // 内建命令 --> echo
125
126 // 执行命令
127 pid_t id = fork();
128 assert(id != -1);
129
130 if(id == 0)
131 {
132 switch(rtype)//进行重定向处理!
133 {
134 case NONE_R:
135 break;
136 case INPUT_R:
137 {
138 int fd = open(rfile,O_RDONLY);
139 assert(fd!=-1);
140 dup2(fd,0);
141 }
142 break;
143 case OUTPUT_R:
144 case APPEND_R:
145 {
146 int flags = O_WRONLY | O_CREAT;
147 if(rtype==APPEND_R) flags |= O_APPEND;
148 else flags |= O_TRUNC;
149 int fd = open(rfile,flags);
150 assert(fd!=-1);
151 dup2(fd,1);
152 }
153 break;
154 default:
155 printf("Is bug!!!\n");
156 break;
157 }
158 execvp(myargv[0], myargv);
159 exit(1);
160 }
161 int status = 0;
162 pid_t ret = waitpid(id, &status, 0);
163 assert(ret > 0);
164 (void)ret;
165 lastCode = ((status>>8) & 0xFF);
166 lastSig = (status & 0x7F);
167 }
168 }
3、知识点
第一点:
因为进程独立性的原因,子进程和父进程的操作互不影响,所以,子进程处理要拷贝父进程的pcb以外,还要拷贝父进程里面的文件描述符表!
然而文件不属于进程,所以子进程拷贝父进程不关文件什么事,因为文件不属于文件,文件属于文件系统。文件是被父子进程共享的!
第二点:
程序进程替换是不会影响曾经进程打开的重定向文件。文件描述符之前指向什么文件,程序进程替换之后还是指向什么文件,因为替换只是替换物理地址上面的代码和数据,与我pcb和files_struct无关。
六、如何理解linux一切皆文件
我们学linux的时候就常常听说linux一切皆文件,那么如何理解呢?我们怎么能够看到细节呢?
接下来我们就来综合前面内容来分析一下:
我们前面学习计算机的软硬件体系结构是时候,知道了计算机内部的结构是什么样的:
那么,今天就通过这张图来研究一下为什么linux一切皆文件
硬件部分有许多的硬件,比如磁盘、网卡、键盘、显示器等等。我们向这些硬件读取数据或者写入数据都方法都不一样,这是肯定的。而我们想要使用硬件就必须调用对应硬件的设备驱动,这个驱动层里面有这对应硬件的结构体,驱动层通过每一个硬件的结构体来管控对应的硬件,从而方便我们操作硬件。这个硬件结构体里面有读取函数和写入函数等对硬件操作的函数,那么我们想要对硬件做任何操作,就要通过驱动层对应的硬件结构体内部的函数来完成我们的操作
在驱动层上面就是操作系统层面了,OS里面有一个struct file
结构体,这个结构体里面存放在各种文件的属性,比如:文件的空间大小,文件描述符…不仅仅如此,这个文件的结构体里面还有驱动层每一个硬件结构体内部操作(read读、write写等等)函数的函数指针,通过struct file内部的函数指针可以直接调用驱动层硬件结构体内部的函数。这也就是说:OS不用管驱动层、硬件层是怎么样实现的,只需要调用OS自己内部struct file中对应的函数指针,就能够调用起来硬件,从而帮助我们进行任何的硬件操作。
所以,OS不管你底层是怎么实现的,他只需要struct file这个文件结构体就能够完成对底层硬件的调用
站在OS的视角来看,驱动设备和硬件都是struct file结构体(因为OS通过该结构体就完成了所有操作),这也就是为什么linux中一切皆文件!
图中的vfs就是
虚拟文件系统
七、缓冲区
我们先来看看一段代码:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <assert.h>
7 #include <string.h>
8 int main()
9 {
10 printf("hello printf\n");
11 fprintf(stdout,"hello fprintf\n");
12 fputs("hello fputs\n",stdout);
13
14 const char* buffer="hello writef\n";
15 write(1,buffer,strlen(buffer));
16 return 0;
17 }
我们在return前面加上fork函数看看:
我们明明是在程序最后进行fork的,为什么重定向之后C语言接口多打印了呢?我们没有加fork函数重定向到文件打印就是4行,但是加了fork函数之后重定向到文件打印是7行,这说明问题出在了fork函数
那么,带着这个问题我们继续往下面学习
1、理解缓冲区
缓冲区的本质:缓冲区的本质就是一块内存!
缓冲区的意义:缓冲区的意义就是节省进程进行数据IO的时间!
那么对于fwrite和write函数,与其理解这两个是写入到文件的函数,倒不如理解为是进行拷贝的两个函数。将我们的数据从进程拷贝到“缓冲区”或者外设当中
2、缓冲区刷新问题
缓冲区一定会结合具体的设备来定制合适的刷新策略
这是肯定的,因为我们常见或者默认情况下一次性刷新完缓冲区肯定比多次刷新完要高效一些
因为:一次刷完就只会进行一次IO,而多次刷新就会进行多次IO。而如果我们一次IO如果时间是1000秒,那么999秒的时间都在等IO的外设做准备,然后1秒将数据拷贝完。所以IO次数少,效率高
所以,针对不同的设备,缓冲区也有不同的刷新策略:
第一种:立即刷新——无缓冲(这个就和没有缓冲区一样,我们写什么,就直接打印在显示器上面)
这种就相当于我们代码每打印一行数据,就直接fflush刷新出去了,不用在缓冲区等待
第二种:行刷新——行缓冲——显示器(一行行打印到显示器,方便用户观看)
这个就是我们经常使用的缓冲,当打印的数据遇到\n时,就会进行行缓存,把这一行的数据给刷新出缓冲区
对比于下面的全缓冲,这里的行缓冲是为了方便给我们用户观看的,全缓冲一次性刷新完数据太多了,用户看不过来。所以既为了方便用户观察,又为了刷新效率不会太慢,就有了行缓冲
第三种:缓冲区满了——全缓冲——磁盘文件(向磁盘文件写入就会等缓冲区满了一次性刷新到磁盘文件)
这个就是效率最高的。缓冲区全部满了,一次性刷新到显示器或者我们指定的地点去
上面三种是上层语言给我们提供的三种策略。但是,是存在特殊情况的:
特殊情况1:用户强制刷新
我们用户直接调用fflush是可以强制刷新缓冲区的
特殊情况2:进程退出——一般都要进行缓冲区刷新
这里提一下:我们open和close等操作,并不是直接打开和关闭了文件,而是只是给操作系统发一个请求/信号,告诉操作系统,我要打开或者关闭一个文件了。然后操作系统会自动帮助我们打开或者关闭一个文件。所以,文件的打开/关闭不是我们直接操作的,而是OS完成的,我们只是告诉OS要使用而以。
所以,这就是为什么退出进程会刷新缓冲区,这就是OS自动完成的
3、缓冲区在哪里?
问题:我们上面说的缓冲区在哪里呢?这个缓冲区指的是什么缓冲区呢?
那上面的例子来说明:
根据这个结果我们可以确定俩个结论:
结论1:这个现象一定是和缓冲区有关的
结论2:这里的缓冲区一定是在用户语言层面的。如果在OS层中,write重定向之后也要被打印两次的根据前面OS会默认打开三个输入输出流,stdin、stdout、stderror。这三个文件都是FILE*类型的,而我们调用fflush和fclose的时候,fflush(文件指针)/fclose(文件指针)就可以完成刷新缓冲区的操作
所以,FILE结构体除了包含文件描述符,还包含了缓冲区,这就是为什么fflush(文件指针)/fclose(文件指针)我们强制刷新缓冲区和关闭文件会刷新缓冲区,就是因为文件指针是FILE*类型的,而FILE里面有缓冲区!
4、FILE结构体
我们只需要知道FILE结构体里面包含了:文件描述符、缓冲区、文件各种属性等内容就行
所以,我们以前说的缓冲区都是C语言(用户语言层面)的缓冲区,而我们前面一系列操作打印的数据都是先拷贝(这里说加载也可以,每个人的理解不一样,作用就是:把我们要打印的数据先存到缓冲区里面)到FILE*指针里面的缓冲区,然后要么我们调用fflush或者退出等进程自动刷新缓冲区,才在显示器打印出我们想要的结果!
现在解释一下上面例子为什么重定向之后出现了7行:
stdout默认使用的是行刷新,在进程fork之前,3条C语言函数就已经把数据打印到显示器(外设)上面了,我们的FILE结构体内部的缓冲区没有对应的数据了
如果我们进行了>重定向,打印的数据就不在是向显示器写入了,而是向普通文件写入,普通文件写入是采用的全缓冲方法,那么这3条C语言打印数据就不会被刷新
执行fork的时候,stdout是父进程的,创建子进程之后,直接退出。不管哪一个进程退出,都会自动刷新缓冲区,而刷新的本质就是修改!所以,一旦进程退出就会触发写时拷贝!数据也就显示两份了!
为什么write没有被打印两份?
因为上面的过程都与write无关,write没有使用FILE结构体,也就不存在C语言提供的缓冲区,那么刷新缓冲区就与write无关!
5、模拟实现基础的缓冲区
我们了解了那么多缓冲区知识,不如自己动手试一下
points.h:
1 #pragma once
2
3 #include <assert.h>
4 #include <stdlib.h>
5 #include <errno.h>
6 #include <string.h>
7 #include <unistd.h>
8 #include <sys/types.h>
9 #include <sys/stat.h>
10 #include <fcntl.h>
11
12 #define SIZE 1024
13 #define SYNC_NOW 1//无缓冲
14 #define SYNC_LINE 2//行缓冲,默认采用这个
15 #define SYNC_FULL 4//全缓冲
16
17 typedef struct _FILE{
18 int flags; //刷新方式
19 int fileno;//文件描述符
20 int cap; //buffer的总容量
21 int size; //buffer当前的使用量
22 char buffer[SIZE];//存放数据
23 }FILE_;//自己封装定义结构体,重命名为FILE_
24
25
26 FILE_ *fopen_(const char *path_name, const char *mode);//打开文件操作
27 void fwrite_(const void *ptr, int num, FILE_ *fp);//写入操作
28 void fclose_(FILE_ * fp);//关闭操作
29 void fflush_(FILE_ *fp);//强制刷新操作
points.c:
1 #include "points.h"
2
3 FILE_ *fopen_(const char *path_name, const char *mode)//打开文件操作
4 {
5 int flag = 0;//文件操作的宏选项
6 int permissions = 0666;//文件权限
7 if(strcmp(mode,"r")==0)//判断对文件的操作是什么
8 {
9 flag |= O_RDONLY;
10 }
11 else if(strcmp(mode,"w")==0)
12 {
13 flag |= O_WRONLY | O_CREAT | O_TRUNC;
14 }
15 else if(strcmp(mode,"a")==0)
16 {
17 flag |= O_WRONLY | O_CREAT | O_APPEND;
18 }
19 else
20 {
21
22 }
23 int fd = 0;
24 if(flag & O_RDONLY)
25 fd = open(path_name,flag);
26 else
27 fd = open(path_name,flag,permissions);
28 if(fd<0)
29 {
30 const char* err = strerror(errno);
31 write(2,err,strlen(err));//打印错误信息
32 return NULL;//这就是为什么C语言打开文件失败了会返回NULL
33 }
34 //到这里就是文件被打开了
35 FILE_* fp =(FILE_*)malloc(sizeof(FILE_));//申请空间
36 //下面进行初始化工作
37 fp->flags = SYNC_LINE;//设置默认行缓冲
38 fp->fileno = fd;//文件描述符
39 fp->cap = SIZE;//容量初始化
40 fp->size = 0;//有效数据个数初始化
41 memset(fp->buffer,0,SIZE);//空间初始化
42
43 return fp;//这里就是为什么C语言打开一个文件,返回FILE*指针
44 }
45 void fwrite_(const void *ptr, int num, FILE_ *fp)//写入操作
46 {
47 memcpy(fp->buffer+fp->size,ptr,num);//数据写入到缓冲区中
48 fp->size += num;
49 //判断是否刷新
50 if(fp->flags & SYNC_NOW)
51 {
52 write(fp->fileno,fp->buffer,fp->size);
53 fp->size=0;//清空缓冲区
54 }
55 else if(fp->flags & SYNC_LINE)
56 {
57 if(fp->buffer[fp->size-1] == '\n')//不考虑abcd\nadg
58 {
59 write(fp->fileno,fp->buffer,fp->size);
60 fp->size=0;
61 }
62 }
63 else if(fp->flags & SYNC_FULL)
64 {
65 if(fp->cap == fp->size)
66 {
67 write(fp->fileno,fp->buffer,fp->size);
68 fp->size=0;
69 }
70 }
71 else
72 {
73
74 }
75 }
76 void fclose_(FILE_ * fp)//关闭操作
77 {
78 fflush_(fp);//刷新
79 close(fp->fileno);//关闭
80 }
81 void fflush_(FILE_ *fp)//强制刷新操作
82 {
83 if(fp->size > 0)
84 write(fp->fileno,fp->buffer,fp->size);
85 fsync(fp->fileno);//强制OS把数据刷新到外设!
86 fp->size=0;
87 }
main.c:
1 #include "points.h"
2 #include <stdio.h>
3 int main()
4 {
5 FILE_* fp = fopen_("./bzh.txt","w");
6 if(fp == NULL)
7 {
8 return -1;
9 }
10 int cnt=10;
11 const char* sp = "hello";
12 while(cnt)
13 {
14 fwrite_(sp,strlen(sp),fp);
15 //fflush_(fp);
16 sleep(1);
17 printf("count = %d\n",cnt);
18 if(cnt==5)
19 fflush_(fp);
20 cnt--;
21 if(cnt==0)
22 break;
23 }
24 return 0;
25 }
6、缓冲区总结
上面我们谈的缓冲区是C语言里面FILE结构体里面的缓冲区,那么OS里面就真没有缓冲区吗?
其实OS也有缓冲区——内核缓冲区
1、我们平时写代码,执行程序之后,打印的结果通过fwrite函数被加载到C语言FILE结构体的缓冲区里面,而通过FILE内部封装的文件描述符,又把C语言缓冲区的数据通过write函数加载到内核缓冲区里面,最后从内核缓冲区中加载数据打印到显示器/存放到磁盘里面。而最后从内核缓冲区加载数据到外设是OS自主决定的,与用户毫无关系了!
2、我们上面说的缓冲区刷新策略,只是在用户语言也就是C语言缓冲区有用。内核缓冲区不是我们想的这么简单!
那么,如果我们要加载一批大量的数据到磁盘文件里面,如果OS突然宕机了,怎么办?
怎么来解决对数据丢失0容忍的机构呢?
这个时候就要用到fsync函数了
7、fsync函数
fysnc函数作用:不要加载数据到内核缓冲区了,强制把数据刷新到外设!实施同步更新!
八、理解文件系统
我们前面都是谈论的进程与被打开文件的关系,那么,没有被打开的进程呢?
没有被打开的进程会被放在磁盘上静静的躺着,这些没有被打开的文件也要被OS静态管理起来(这里的OS静态管理关闭文件和前面打开文件的管理都是由文件系统进行管理的!文件系统属于OS的一部分!),方便我们需要使用的时候打开
而这些关闭的文件也是由文件系统管理的,不仅仅只有前面打开的文件要被文件系统管理,关闭的文件也是需要文件系统管理的!
1、磁盘的物理结构
我们现在的电脑大多数都不使用磁盘了,都是ssd盘,但是在企业中主流还是采用的磁盘,因为磁盘的性价比对于企业来说比较高
磁盘是计算机中唯一的一个机械结构,加上磁盘又是外设,所以磁盘的访问对比于cpu来说很慢
—
1、每一个磁盘打开内部有多个盘片垂直、互不接触大体上呈圆柱体
2、每一个盘片都有两面可以用来读写
3、每一个盘面就有一个磁头,磁头也是有一摞的,并且磁头都是连在一起的
盘片n——盘面2n——磁头2n
注意:磁盘和磁头是没有接触的。那么就要要求磁盘不能够抖动,一旦抖动就会划花磁盘
当磁盘运作起来时,盘片会按照顺时针/逆时针高速旋转,而磁头也会左右摇摆
2、磁盘的存储结构
磁盘在寻址的时候基本单位是扇区(512byte)。所以磁盘是块设备
那么,怎么在盘面上,定位扇区呢?
1、先确定磁道。磁头在来回摆动的时候就是确定在哪一个磁道
2、确定在对应磁道的哪一个扇区。盘片旋转就是让磁头定位扇区
所以,我们想要在磁盘定位任何一个扇区:
先定位在哪一个磁道(cylinder),然后定位磁头(head),最后定位扇区(sector)
磁盘定位任何一个扇区/多个扇区,采用的硬件基本定位方式:CHS定位法
3、磁盘的逻辑结构
我们先来回忆一下磁带是什么样的:
这里的磁带是圆形的,卷起来就像我们上面的磁盘里面的盘面一样
而当我们把磁带扯出来就变成了线性结构!
那么,我们能不能把上面的磁盘内部的盘面也扯成线性结构呢?
答案是可以的,那么对磁盘进行管理,就变成了对数组进行管理!!
这个时候,要找到一个扇区又该怎么办呢???
这里我们直接来举例:
盘面:4
磁道/柱面:10
扇区:100
求LBA地址123号磁盘的位置?
总容量=4*10*100*512
下标范围:[0,4*10*100]
盘面:10*100=1000扇区
123/1000=0->位于0号盘面->H
123/100=1->位于1号磁道->C
123%100=23->位于23号扇区->S
提问:
那么,就有人问,为什么要使用LBA地址,不直接使用CHS呢?
1、便于管理(管理数组明显方便于管理立体结构)
2、不想让OS的代码和硬件强耦合(不管是磁盘、ssd还是其他的结构,OS都只用LBA地址。所以底层的硬件改变不会影响操作系统)
4、进一步理解
虽然磁盘访问的基本单位是512byte,但是依然很小!OS内的文件系统定制的进行多个扇区的读取->1KB、2KB、4KB为基本单位(一般4KB用的多)
也就是说:我们哪怕是修改或者读写1bit,都必须将4KB加载(load)到内存,有必要再写回磁盘!这就是局部性原理
内存被划分成为了4KB大小的空间——
页框
磁盘的可执行文件,按照4KB大小划分好——页帧
当我们执行磁盘的可执行程序时,在进行外设到内存的加载时,就是把数据放到页框里面!
5、逻辑结构的进一步抽象
举个例子:磁盘有500GB
所以,我们管理好这5GB就相当于大体上管理好了磁盘
接下来我们就来仔细分析分析上面那所谓5GB里面的内容:
Boot Block:OS开机加电启动的时候,相关的信息全部都在这个区域(我们只需要了解,不需要深入研究)
Super Block:简称SB,保存整个文件系统的信息
这里Super Block这么重要,为什么不是在分区里面,而是在分组里面呢?
其实,我们Super Block在有些组里面有,有些组里面没有。所以SB对于分组来说不是必须的
那为什么要放在分组里面,还要有多份?
这是因为多个存在就意味着备份,我们以后如果是分组出问题了是小问题,容易解决;但是分区出问题了那就是大问题了
所以,我们把SB保存在分组里面,每一个SB内容/数据都是一样的,同步更新也是一起的。如果有一天我们的SB坏了,那么直接拷贝其他的SB过来就行了,这就是文件恢复。所以,多个分组里面有Super Bloke就是有效做备份
把Super Bloke讲完了,我们继续来看看剩下的5个分组内容:
前面我们知道:
文件 = 内容 + 属性
linux中的文件属性和文件内容是分批存储的!
其中,保存文件属性主要用到
inode
,大小为固定的字节。
一个文件对应一个inode
inode基本上包含了文件的所有属性,文件名除外
文件内容在磁盘中是采用Data blocks数据块的形式存储的
inode为了进行区分彼此,每一个inode都有自己的ID:
inode table:保存了分组内部所有可用(已经使用+未被使用)inode
Date blocks:保存的是分组内部所有文件的数据块
inode Bitmap:inode对应的位图结构。位图中比特位的位置和当前文件对应的inode的位置是一一对应的(0000 1101:比特位的第几个位置,表示是第几个inode;比特位为0表示该inode没有被占用,为1表示该inode被占用了)所以删除一个文件就在inode Bitmap把文件对应的inode改成0,在Block Bitmap把数据块的位图改成0,这样就删除了;然后改成1就恢复文件了
Block Bitmap:数据块对应的位图结构。位图中比特位的位置和当前Data blocks对应的数据块的位置是一一对应的
Group Descriptor Table:简称GDT。块组描述表:对应分组的宏观属性信息(比如:有多少个inode,数据块,哪些inode和数据块被占用了…)
所以,查找一个文件的时候,统一使用:inode编号。inode编号可用跨分组使用,但是不能跨分区使用!!
那么,我们通过inode编号,就可用通过inode Bitmap找到对应的 inode table,查看是否正确,如果是1,那么我们就可以拿到对应的文件属性了
但是,我怎么通过inode编号拿到我文件的内容也就是文件对应Date blocks里面的数据块呢?
inode内部除了有文件属性以外,还有一个data blocks arr[n]
数组,数组里面放的就是对应的Data blocks内部数据块的编号
我们通过inode先拿到文件的属性之后,在属性内部找到数组对应的表,通过表来拿到对应的数据块
但是,一个数组能够有多大,要是我们Data blocks里面的数据块过多,那么一个数组(一张表)不就对应不过来了吗?
所以,在
data blocks arr[n]
数组里面,数组最后几个位置对应的数据块内部并不是存放的数据,而是存放其他的数据块的ID。二级索引、三级索引…这样一来空间肯定就够了
但是,我们在linux中使用ls -inode编号是不行的,要使用ls -文件名才能够成功,这是为什么呢?
目录的inode里面存放的是目录的属性,而目录的数据块里面存放的是当前目录下面的文件名和inode的映射关系
所以当我们创建一个文件的时候要有写权限,因为我们要在当前目录的内容里面去写入创建的文件名和inode的映射关系
当我们要罗列文件名的时候要有读权限,因为我们要根据文件名找到对应的inode,然后才能读取出下面文件的所有属性。因为我们用是文件名,所以必须访问目录的数据块,所以需要读权限
九、软硬链接
1、基本概念与使用
我们直接来看看什么是硬链接,什么是软连接:
所以,软连接和硬链接的区别就在于:
是否独立的inode
那么,硬链接究竟干了什么呢?
所以我们确定了:
创建一个硬链接,根本没有创建新文件,因为硬链接没有被分配独立的inode;没有文件,那么硬链接就没有直接的内容集合和属性集合,用的一定是别人的inode和内容
样例:
263563 -rw-r--r--. 2 root root 0 9月 15 17:45 abc
261678 lrwxrwxrwx. 1 root root 3 9月 15 17:53 abc.s -> abc
263563 -rw-r--r--. 2 root root 0 9月 15 17:45 def
创建硬链接本质就是:在指定路径下,新增文件名与inode映射关系
inode里面有一个引用计数
,我们也可以称之为硬链接数
。这个数就表示有多少,就表示inode被多少个文件所指向
所以,怎么样才算成功删除一个文件?
答案就是:文件的硬链接数变成0的时候,就算删除文件成功了
我们可以把软连接理解为windows下面的快捷方式!
2、应用场景
软连接:
硬链接:
但是,为什么empty目录的硬链接数是2呢?
3、问题
为什么linux不允许普通用户给目录建立硬链接呢?
这里先解释一下.和. .是操作系统系统帮我们创建的,操作系统相信自己,不相信用户
4、三个时间
我们前面讲stat提到过,这里再来学习一下:
Access 最后访问时间
Modify 文件内容最后修改时间
Change 属性最后修改时间
十、动静态库
1、基本概念
1、静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
2、动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
3、一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
4、在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
5、动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
库里面是没有main函数的,不然我们每次写代码的时候写main函数会与库直接冲突
2、静态库的生成和使用
我们举最简单的例子
add.c:
#include "add.h"
#pragma once
extern int Add(int x,int y);
add.h:
#include <stdio.h>
int Add(int x,int y)
{
printf("%d - %d =\n",x,y);
return x+y;
}
前面我们说了,库函数里面是没有main函数的,所以我们把main函数移出去
如果我们不想给对方我们的源代码,我们可以给对方提供.o可重定位二进制文件,让对方用代码进行链接就行!
我们可以给对方提供.o(方法的实现)和.h(有什么方法)
那么,当有无数个.c文件的时候,我们要把无数个.o文件依次给对方吗?
这是肯定不行的,因为效率太低了。那么我们可以将这些.o文件都打一个包,给对方提供库文件即可
库文件就是由多个.o文件组成,组成的打包文件就叫做库,而打包库的工具与方法的不同就形成了动态库和静态库
库的本质:就是.o文件的集合
接下来我们继续:
生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar(archive)是gnu归档工具,rc表示(replace and create)
那么,我们是不是把这个静态库拷贝过去就可以直接使用了呢?
答案是不行的,因为linux里面有我们写代码时,对应路径下面有我们需要的头文件和库文件:
所以直接拷贝过去是不行的
未来我们交付给别人库,要把库文件.a/.so和匹配的头文件都要给别人!
我们继续对makefile文件做修改:
1 libbzh.a:add.o
2 ar -rc $@ $^
3 add.o:add.c
4 gcc -c add.c -o add.o
5 .PHONY:otuput
6 output:
7 mkdir -p mylib/include
8 mkdir -p mylib/lib
9 cp *.a mylib/lib
10 cp *.h mylib/include
11 .PHONY:clean
12 clean:
13 rm -rf add.o libbzh.a
这里的mylib就是我们打包之后的静态库,我们继续把.o和.h文进行打包
所谓的安装:本质就是拷贝!
我们使用-I 来指定头文件
使用-L来指定库所在路径
使用-l指定库名称
到此,我们自主进行打包静态库文件和使用库文件就完成了,但是还有一些需要收尾的工作
而且没有我们自己的库
这里要注意几点:
1、gcc默认的是动态链接
2、形成一个可执行程序,不仅仅只依赖一个库!!!
对于一个库来说:gcc默认是动态链接的(建议行为),对于特定的一个库,究竟是动,还是静,取决于你提供的究竟是动态库还是静态库。我们也知道了一个可执行程序要依赖多个库,当我们把动态库和静态库都给gcc时,gcc对于动态库就进行动态链接,对于静态库就进行静态链接。而==只要有一个动态库,那么我们的程序/软件就是动态链接的!==
当然,我们也可以把我们写的库和头文件直接安装(cp)到系统的默认路径下面:
/usr/include
/lib64
这样就不用带上面那么多的选项了,可以直接编译运行程序
3、动态库的生成和使用
我们采用静态库的代码样例
我们虽然告诉库文件路径和库名称了
但是我们告诉的是gcc,当我们把程序编译完,和gcc没有半毛钱关系了
这个时候程序运行起来,os和shell也是需要知道库在哪里的,而我们的库不在默认系统路径下面,os无法找到
那么,这里有4种解决方法:
方法一:将头文件和库文件安装到系统默认路径下面,然后再指明库名称
这种方法我就不实现了,我们来看看其他方法
方法二:更改环境变量
LD_LIBRARY_PATH
方法三:ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
方法四:软连接
4、动静态库的加载
静态库的加载
动态库的加载
所以,就算有再多的程序和库也不怕了,我们能够通过偏移量找到指定需要的函数!
5、小结
生成静态库:
[root@localhost linux]# ls
add.c add.h main.c sub.c sub.h
[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o
生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar是gnu归档工具,rc表示(replace and create)
查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t:列出静态库中的文件
v:verbose 详细信息
[root@localhost linux]# gcc main.c -L. -lmymath
-L 指定库路径
-l 指定库名
测试目标文件生成后,静态库删掉,程序照样可以运行
库搜索路径
从左到右搜索-L指定的目录。
由环境变量指定的目录 (LIBRARY_PATH)
由系统指定的目录
/usr/lib
/usr/local/lib
生成动态库:
shared: 表示生成共享库格式
fPIC:产生位置无关码(position independent code)
库名规则:libxxx.so
示例: [root@localhost linux]# gcc -fPIC -c sub.c add.c [root@localhost linux]# gcc -shared -o libmymath.so
*.o [root@localhost linux]# ls add.c add.h add.o libmymath.so main.c sub.c sub.h sub.o
使用动态库
编译选项
l:链接动态库,只要库名即可(去掉lib以及版本号) L:链接库所在的路径.
示例: gcc main.o -o main –L. -lhello
运行动态库
1、拷贝.so文件到系统共享库路径下, 一般指/usr/lib
2、更改 LD_LIBRARY_PATH
3、ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
4、软链接