4.2w字,详细的带你认识基础I/O【Linux--基础IO】_i/o read modify write-程序员宅基地

技术标签: Linux  c语言  linux  开发语言  

前言  

        相信大家最开始都挺疑惑的,什么I/O。在计算机操作系统中,所谓的I/O实则就是输入(Input)和输出(Output),也可以理解为读(Read)和写(Write),那么这里基础的意思是我们需要基本掌握的I/O的知识。这篇文章主要讲解的是针对于磁盘I/O,我们会深入探究用户对磁盘进行读写操作的过程,也将涉及到的基本知识进行讲解。

首先对于I/O,我们在学C语言文件章节的时候就学过C文件IO相关操作,当初就是简单用c语言对应的接口,对文件进行读写操作。任何语言对文件操作的接口都是基于系统调用接口之上的,之后我们就会学习文件相关系统调用接口,后续内容是:文件描述符 ,理解重定向对比fd和FILE ,理解系统调用和库函数的关系理解文件系统中inode的概念,认识软硬链接,对比区别认识动态静态库,最后学会结合gcc选项 ,制作动静态库。


目录

前言  

一、重顾C文件接口

1.1不变的接口

1.2C语言文件操作

二、系统文件I/O

2.1用比特位传递选项

2.2接口介绍

2.3文件描述符fd(重)

2.4文件描述符的分配规则

2.5重定向

2.6使用 dup2 系统调用

 2.7再谈myshell

三、FILE-缓冲区

3.1抛出问题

3.2认识缓冲区

3.3缓冲区刷新策略

3.4解决缓冲区问题(重)

3.4myStdio-dome

四、文件系统

4.1磁盘的物理结构

4.2磁盘的存储结构 

4.3磁盘的逻辑结构

4.4理解文件系统(重)

五、软硬链接(重)

5.1软硬链接的创建

5.2软硬链接的区别

5.3软硬链接的应用 

六、动态库和静态库 

6.1动态库和静态库的理解

6.2静态库和静态链接的生成

6.3生成动态库

6.3使用外部库--ncurses库

6.4动态库的加载


2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png2f2f936c26a44559afc9da7431a05dbf.png


一、重顾C文件接口

1.1不变的接口

我们知道C语言有文件操作接口,那么当然C++,JAVA这些语言都有文件操作接口。这些语言拥有文件操作接口的目的找到文件,然后对文件进行操作。那么文件是在磁盘上,磁盘是属于硬件。对于硬件的访问只有操作系统才是进行。所有人想访问磁盘都不能绕开操作系统,C语言也好,其他语言也罢都是人表达出意思让操作系统理解我们想要干嘛,所以任何上层语言想要进行对磁盘进行操作,都会使用操作系统提供的接口

1.2C语言文件操作

下面我们通过代码进行回顾c文件接口

写文件

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

#include <stdio.h>    
#include <string.h>    
    
    
int main()    
{    
    FILE* fp=fopen("myfile","w");    
    if(!fp)    
    {    
        perror("open fail");    
        return (1);    
    }    
    
    const char *msg="hello world! \n";    
    int cnt=5;    
    while(cnt--)    
    {    
        fwrite(msg,strlen(msg),1,fp);    
               
    }                                                                                   
                                                             
    fclose(fp);                                                                                                                                                                            
                       
    return 0;                                         
}  

显示结果:

[hongxin@VM-8-2-centos 12-31-1]$ gcc -o test test.c
[hongxin@VM-8-2-centos 12-31-1]$ ./test
[hongxin@VM-8-2-centos 12-31-1]$ ll
total 24
-rw-rw-r-- 1 hongxin hongxin   73 Dec 31 23:26 makefile
-rw-rw-r-- 1 hongxin hongxin   70 Dec 31 23:27 myfile
-rwxrwxr-x 1 hongxin hongxin 8568 Dec 31 23:27 test
-rw-rw-r-- 1 hongxin hongxin  711 Dec 31 23:27 test.c
[hongxin@VM-8-2-centos 12-31-1]$ cat myfile 
hello world! 
hello world! 
hello world! 
hello world! 
hello world! 

细节问题:

1.当以"w"-写的方式单纯打开文件时,c会自动清理内部数据

2.当以"w"方式打开文件,它会建立一个新文件,它的默认权限是0664

 -rw-rw-r-- 1 hongxin hongxin  212 Jan  1 14:37 myfile

因为普通类文本创建时有自己的默认文件掩码 umask 0002

0666 &  ~umask ->0664

[hongxin@VM-8-2-centos 12-31-1]$ umask
0002

如果我们想去文件的默认掩码,建立文件前输入代码umask(0);

读文件

size_t fread( void *ptr, size_t size, size_t count,FILE *stream );

#include <stdio.h>    
#include <string.h>    
    
    
int main()    
{    
    FILE* fp=fopen("myfile","r");    
    if(!fp)    
    {    
        perror("open fail");    
        return (1);    
    }    
    char buf[1024];
    const char *msg="hello world\n";

    while(1)
    {
        size_t s=fread(buf,1,sizeof(buf)-1,fp);
        if(s>0)
        {
            buf[s]=0;
            printf("%s",buf);    
    
        }    
        if(feof(fp))    
        {                                           
            break;    
        }                                                                                                                                  
    }        

fclose(fp);                                                                                                                        
    return 0;                                         
}  

显示结果:

[hongxin@VM-8-2-centos 12-31-1]$ make
gcc -o test test.c
[hongxin@VM-8-2-centos 12-31-1]$ ./test 
hello world
hello world
hello world
hello world
hello world
hello world
 

这里fread将文件内数据拷贝到数组,s接受fread返回的个数,s大于0,大于文件。当打印完时文件为空退出循环。

追加

我只需要将w改成a,一直执行程序就行对文件中数据进行重复拷贝

FILE* fp=fopen("myfile","a"); 

对于文件的读和写我们不仅仅可以用fread,fwrite,还可以用fgetc,fputc,fgets,fputs,fscanf,fprintf等。

总结打开文件的方式

r       Open text file for reading.

        The stream is positioned at the beginning of the file.

r+    Open for reading and writing.

        The stream is positioned at the beginning of the file.

w     Truncate(缩短) file to zero length or create text file for writing.

        The stream is positioned at the beginning of the file.

w+   Open for reading and writing.

        The file is created if it does not exist, otherwise it is truncated.

        The stream is positioned at the beginning of the file.

a      Open for appending (writing at end of file).

        The file is created if it does not exist.

        The stream is positioned at the end of the file.

a+    Open for reading and appending (writing at end of file).

        The file is created if it does not exist.

        The initial file position for reading is at the beginning of the file,

        but output is always appended to the end of the file.

二、系统文件I/O

在C语言中打开文件接口是fopen,fopen也是基于系统文件接口open之上的,紧接着就会学习open参数的含义和使用方法。这里需要明白不管是打开还是读写操作,都是在对文件进行操作,文件操作的本质是被打开文件和进程的关系。他们的关系,就好比进程是被打开文件的使用者,为文件的使用者,进程理所当然地将要使用的文件记录于自己的控制块。另外,由于进程所对应的程序也是一个文件,因此进程控制块还必须记录这个文件的相关信息。由于操作系统要对系统所以进程提供服务,因此操作系统还要维护一个记录所有进程打开文件的总表。


2.1用比特位传递选项

在开始学习文件接口open之前,我们先学习如何用比特位传递选项,这个是为了更加容易理解open参数flags的使用。

一个int有32个比特位,通过比特位传递选项。

#include<stdio.h>    
    
#define ONE (1<<0)    
#define TWO (1<<1)    
#define THREE (1<<2)    
#define FOUR (1<<3)    
    
void show(int flags)    
{    
    if(flags & ONE) printf("one\n");    
    if(flags & TWO) printf("two\n");    
    if(flags & THREE) printf("three\n");    
    if(flags & FOUR) printf("four\n");    
}    
    
    
int main()    
{    
    show(ONE);    
    printf("------------------------\n");    
    
    show(TWO);    
    printf("------------------------\n");    
    show(ONE|TWO);    
    printf("------------------------\n");    
    show(ONE|TWO|THREE);    
    printf("------------------------\n");    
    show(ONE|TWO|THREE|FOUR);    
    printf("------------------------\n");    
    
    
    
    return 0;                                                                                                                                                                              
}  

结果:

[hongxin@VM-8-2-centos 12-31-1]$ ./myfalfs 
one
------------------------
two
------------------------
one
two
------------------------
one
two
three
------------------------
one
two
three
four
------------------------

比特位通过与预算以0000 0001->0000 0011的形式进行到达传递选项的目的

2.2接口介绍

通过man 2 open打开文件手册学习open参数的含义和使用方法

//头文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags, mode_t mode);

pathname: 要打开或创建的目标文件

flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。

参数:

        O_RDONLY: 只读打开

        O_WRONLY: 只写打开

        O_RDWR : 读,写打开

                        这三个常量,必须指定一个且只能指定一个

        O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限

        O_APPEND: 追加写

返回值:

成功:新打开的文件描述符

失败:-1

mode:权限

例 0666 

通过man 2 write打开文件手册学习write参数的含义和使用方法

   #include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);


fd:文件描述符--open返回值

*buf:写入从缓冲区指向buf

count:数量

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
#include <string.h>    
#include <unistd.h>    
    
    
int main()    
{    
    int fd=open("log.txt",O_WRONLY|O_CREAT,0666);    
    if(fd<0)    
    {    
        perror("open fail");    
        return 1;    
    }    
    
    int cnt =5;    
    char outBuffer[64];    
    while(cnt)    
    {    
        sprintf(outBuffer,"%s:%d\n","hello world",cnt--);    
    
        write(fd,outBuffer,strlen(outBuffer));                                                                                                                                             
    }    
    
    
    close(fd);    
    
    return 0;    
}    
  

特别注意:

我们在线文件中写入string 的时候以、0作为字符串的结尾,它是由C语言规定的,但是这里是文件写入是,结束时是与\0无关的。所以在strlen()不需要+1;

运行结果:

[hongxin@VM-8-2-centos 12-31-1]$ gcc -o test1 test1.c
[hongxin@VM-8-2-centos 12-31-1]$ ./test1
[hongxin@VM-8-2-centos 12-31-1]$ ll
total 60
-rw-rw-r-- 1 hongxin hongxin   70 Jan  1 19:19 log.txt
[hongxin@VM-8-2-centos 12-31-1]$ cat log.txt
hello world:5
hello world:4
hello world:3
hello world:2
hello world:1

再次注意:

当我们再次对文件写入是,文件里的数据还是会有一部分数据未被清楚

在log.txt文件中: 

aaaa:5                                                                                                                                                                                 
  2 aaaa:4
  3 aaaa:3
  4 aaaa:2
  5 aaaa:1
  6 orld:3
  7 hello world:2
  8 hello world:1
~

在C语言我们知道w重新写它会自动清除,但是在系统接口中是不能自动清除的,所以在open中flags参数加上O_TUNC

open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);

追加

我们追加是在原来的内容上,进行追加。那么就不需要清除数据,所以open接口就不需要O_TRUNC,但是需要加上O_APPEND

open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);

通过上面测试我们不难发现C语言中的库函数接口是通过封装了系统调用接口实现的

cde1a9d24e6e476395aadca2cc344a87.png

系统调用接口和库函数的关系,一目了然。

所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

b4c11249d00441ddb0e32cc45645b52f.png

2.3文件描述符fd(重)

通过对open函数的学习,我们知道了文件描述符就是是open的返回值,具体是什么我们也没有看见,接下来我就来观察open函数返回值fd是多少。

printf("%d",fd);        //我们在之前的代码上打印open返回值fd即可

[hongxin@VM-8-2-centos 12-31-1]$ ./test1
3

通过打印我们发现是3,那么问题来了,为什么不是0,1,2开始为什么是3开始呢?在学习C语言的时候,有三个标准输入输出流。

stdin ----键盘          0

stdout----显示器     1

stderr----显示器     2

代码演示:

 1: test.c  ⮀                                                                                                                                                                 ⮂⮂ buffers 
#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
int main()    
{    
    printf("stdin->fd:%d\n",stdin->_fileno);    
    printf("stdout->fd:%d\n",stdout->_fileno);    
    printf("stderr->fd:%d\n",stderr->_fileno);    
    umask(0);    
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);    
    if(fd<0)    
    {    
        perror("open fail");    
        return 0;    
    }    
    
    printf("fd: %d\n",fd);    
    
    close(fd);                                                                                                                                                                             
    
    return 0;    
}    

运行结果:

[hongxin@VM-8-2-centos 1-2]$ make
gcc -o test test.c
[hongxin@VM-8-2-centos 1-2]$ ./test
stdin->fd:0
stdout->fd:1
stderr->fd:2
fd: 3

通过演示,我们知道我们自己打开的文件的文化描述符是3,在Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。0,1,2默认被占用,012对应的物理设备一般是:键盘,显示器,显示器

我们在学习C语言文件的时候,有FIEL *fp =fopen(); 那么FILE又是什么呢?是结构体!我们在文件调用接口的时候,我们发现我们并没使用FILE,但是在文件系统调用接口中我们使用的是文件描述符fd,那么在FILE结构体中必定有一个文件描述符的字段。

文件操作的本质:进程和被打开文件的关系。进制中可以打开多个文件,系统中就存在大量的被打开文件,这些文件都会被管理起来,我们通过先描述,后组织。操作系统为了管理对应打开文件,必须要为文件创建对应的内核数据结构标识文件struct file{},它就包含文件大部分属性。

不管是键盘,显示器,显示器还是log.txt,他们都是文件,而且他们都是struct file{}结构体,系统中存在这文件,都是struct file{},那么操作系统如何管理呢?我们从上面知道每个打开的文件都文件标识符fd,所以操作系统就通过fd来识别文件和寻找文件。这个fd我们观察到的是一串有效的数字,那么这些数字就操作系统中struct file* fd_array[]的下标。

过程:操作系统中进程pbc(task_struc)中有*files,通过*files找到files_struct文件,再通过files_struct的struct file* fd_array[]的下标标注struct file* ,通过struct file*找到struct file。

如图

a4a5c92b283b464e8916e9a39a782566.png

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件,找到文件后就可以对文件进行操作了。 

2.4文件描述符的分配规则

我们将Linux进程默认3个标准输入输入错误的文件描述符关闭后后,会是什么现象呢?

通过代码演示

#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
    
int main()    
{    
    close(0);   
    //close(2);
    // close(1) 
    umask(0);    
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);   
 if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
    
    printf("fd:%d\n",fd);    
    close(fd);
    return 0;
}

运行结果:

1.首先关闭的默认0

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
fd:0

2.关闭2
[hongxin@VM-8-2-centos 1-2]$ ./myfile 
fd:2
3.关闭1

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
[hongxin@VM-8-2-centos 1-2]$ 

4.同时关闭0,2

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
fd:0

关闭了0,2后,我们发现我们所写的文件的文件标识符就变成了0,2,将他们的位置占据,当我们关闭1时,因为1是标准输出--显示器,所以没有打印到显示器上。当我们同时关闭0-2时,我们发现fd是0.说明文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的 最小的一个下标,作为新的文件描述符。

2.5重定向

我们专门将colse(1)拿出来观察,代码演示:

#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
    
int main()    
{    
    close(1) 
    umask(0);    
    int fd = open("log.txt", O_WRONLY|O_CREAT, 00644);
 if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
    
    printf("fd:%d\n",fd);    
    close(fd);
    return 0;
}

运行结果:

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
[hongxin@VM-8-2-centos 1-2]$ 

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <

没有显示的原因就是:原本数组中1是指向的标准输出--显示器,但是我将它关闭之后,文件描述符的分配规则是将没有用的最小的坐标进行占用,那么数组中1就指向了myfile文件。由于关闭是显示器所以就不显示。

72c51fd0991a47b3a4c25d89905b7bf9.png

虽然close(1),但是它会打印到log.txt中

    printf("open fd: %d\n", fd); // printf -> stdout
    fprintf(stdout, "open fd: %d\n", fd); // printf -> stdout
    fflush(stdout);

hongxin@VM-8-2-centos 1-2]$ ./myfile 

 [hongxin@VM-8-2-centos 1-2]$ cat log.txt 
open fd: 1
open fd: 1

本来是写入标准输出文件中,但是通过关闭1,然后又通过文件描述符的分配规则,最后打印到了log.txt文件中,这个就是重定向,重定向的本质:上层用的fd不变,在内核中更改fd对应的struct file* 的地址。

这种重定向的方式是比较麻烦的,需要先关闭,然后在打开。dup2就很好的解决这个问题,不需要关闭打开繁琐的操作。

2.6使用 dup2 系统调用

#include <unistd.h>
int dup2(int oldfd, int newfd);

使用方法:

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:

注意:dup2它是将文件标识符中的内容进行交换,而不是交换文件标识符。

输出重定向

#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
    
int main()    
{    
    umask(0);    
    int fd = open("log.txt", O_WRONLY|O_CREAT, 00644);
 if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    } 
   
    dup2(fd, 1);
    
    printf("fd:%d\n",fd);    
    close(fd);
    return 0;
}

运行结果:

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
[hongxin@VM-8-2-centos 1-2]$ cat log.txt 
open fd: 1

追加重定向

#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
    
int main()    
{    
    umask(0);    
    int fd = open("log.txt", O_WRONLY|O_APPEND, 00644);
 if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    } 
   
    dup2(fd, 1);
    
    printf("fd:%d\n",fd);    
    close(fd);
    return 0;
}

运行结果:

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
[hongxin@VM-8-2-centos 1-2]$ cat log.txt 
open fd: 1
open fd: 1
[hongxin@VM-8-2-centos 1-2]$ ./myfile 
[hongxin@VM-8-2-centos 1-2]$ cat log.txt 
open fd: 1
open fd: 1
open fd: 1

 2.7再谈myshell

这次主要是给myshell增加重定向指令,目的是展现重定向的应用场景,使myshell更加完善。

添加重定向,首先就是需要扫描重定向标识符">","<",">>"。然后将命令和文件分开,识别到相应重定向标识符之后,进行完成重定向的功能。

标识符清"0",与文件分开:
            "ls  -a -l -i>myfile.txt"  -> "ls -a -l -i" "myfile.txt" ->  实现重定向 

头文件与初始化

 #include <stdio.h>
  #include <string.h>
  #include <stdlib.h>
  #include <unistd.h>
  #include <sys/types.h>
  #include <sys/wait.h>
  #include <assert.h>
  #include <ctype.h>
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <errno.h>
  
  #define NUM 1024
  #define OPT_NUM 64
  
  #define NONE_REDIR 0
  #define INPUT_REDIR 1
  #define OUTPUT_REDIR 2
  #define APPEND_REDIR 3
  
  
  #define  trimSpace(start)do{\
      while(isspace(*start)) start++;\
  }while(0)
  
  char lineCommand[NUM];
  char *myargv[OPT_NUM]; //指针数组
  int  lastCode = 0;
  int  lastSig = 0;
  
  int redirType =NONE_REDIR;//重定向文件类型
  char* redirFile=NULL;//重定向文件的名称

扫描函数

void commandCheck(char* commands)
  {
      //正扫描
      assert(commands);
  
      //用于循环扫描
      char* start = commands;
      char* end =commands+strlen(commands);
  
      while(start<end)
      {
          if(*start == '>')
          {
              *start = '\0';
              ++start;
              if(*start=='>')
              {    
                  //"ls -a" >>file.log    
                  redirType = APPEND_REDIR;
                  ++start;    
              }    
              else{                                                                                                                                                                        
                  //"ls -a" >file.log    
                  redirType = OUTPUT_REDIR;    
              }    
              trimSpace(start);    
              redirFile = start ;    
              break;    
          }    
          else if (*start =='<')    
          {
              //"cat < file.txt"
              *start ='\0';
              start++;
  
              trimSpace(start);//过滤空格
              //填写重定向信息
              redirType =INPUT_REDIR;
              redirFile =start;

          }
          else{
              start++;
          }
      }
  }

主函数实现

在主函数中,因为父进程主要负责将信息提供给子进程,对于执行是子进程的子进程执行的,真正重定向的工作一定是要自进程完成。所以主要增加代码只子进程中。

 
  int main()
  {
      while(1)
      {
          // 输出提示符
          printf("用户名@主机名 当前路径# ");
          fflush(stdout);
  
          // 获取用户输入, 输入的时候,输入\n
          char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
          assert(s != NULL);
          (void)s;
          // 清除最后一个\n , abcd\n
          lineCommand[strlen(lineCommand)-1] = 0; // ?                                                                                                                                     
          //printf("test : %s\n", lineCommand);
          
          // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
          //"ls -> -a -l -i>myfile.txt"  -> "ls -a -l -i" "myfile.txt" ->  实现重定向
          commandCheck(lineCommand);
  
          // 字符串切割
          myargv[0] = strtok(lineCommand, " ");
          int i = 1;
          if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
          {
              myargv[i++] = (char*)"--color=auto";
          }
  
          // 如果没有子串了,strtok->NULL, myargv[end] = NULL
W>        while(myargv[i++] = strtok(NULL, " "));
  
          // 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
          // 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
           if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
             if(myargv[1] != NULL) chdir(myargv[1]);
              continue;
          }
          if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
          {
              if(strcmp(myargv[1], "$?") == 0)
              {
                  printf("%d, %d\n", lastCode, lastSig);
              }
              else
              {
                  printf("%s\n", myargv[1]);
              }
              continue;
          }
          // 测试是否成功, 条件编译
  #ifdef DEBUG
          for(int i = 0 ; myargv[i]; i++)
          {
              printf("myargv[%d]: %s\n", i, myargv[i]);
          }
  #endif
          // 内建命令 --> echo
  
          // 执行命令
          pid_t id = fork();
          assert(id != -1);
  
          if(id == 0)
          {
              //因为命令是子进程执行的,真正重定向的工作一定是要自进程完成
              //如何重定向,是父进程给子进程提供的信息
             //这里重定向不会影响父进程
              switch(redirType)
              {
                  case NONE_REDIR:
                      //什么都不做
                      break;
                  case INPUT_REDIR:
                      {
                          int fd = open(redirFile,O_RDONLY);
                          if(fd<0)                                                                                                                                                         
                          {
                              perror("open fail");
                              exit(errno);
                          }
                          //重定向的文件已经成功打开
                          dup2(fd,0);
                      }
                      break;
                  case OUTPUT_REDIR:
                  case APPEND_REDIR:
                      {
                          int flags = O_WRONLY|O_CREAT;
                          if(redirType==APPEND_REDIR) flags |= O_APPEND;
                          else flags |= O_TRUNC;
                          int fd =open(redirFile,flags);
                          if(fd<0)
                          {
                              perror("open fail");
                              exit(errno);
                          }
                           dup2(fd,1);
                      }
                      break;
                  default:
                      printf("bug!!\n");
                      break;
              }
              execvp(myargv[0], myargv);
              exit(1);
          }
          int status = 0;
          pid_t ret = waitpid(id, &status, 0);
          assert(ret > 0);
          (void)ret;
          lastCode = ((status>>8) & 0xFF);
          lastSig = (status & 0x7F);
      }
  }

问题:执行程序替换时,会不会影响曾经进程打开的重定向文件呢?

答:是不会的,程序替换是在磁盘与内存阶段,程序替换是将代码进行覆盖。重定向是在pcb与*files阶段,它是在内核数据结构中。程序替换是不影响内核数据结构

b04cb4fb47bd4ea9851d4da1f5525c7d.png

三、FILE-缓冲区

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。 所以C库当中的FILE结构体内部,必定封装了fd。 我们进行再一步研究系统调用与函数调用有何不同。


3.1抛出问题

通过代码研究:

#include <stdio.h>
#include <string.h>


int main()
{
 
 printf("hello printf\n");
 fprintf(stdout,"hello fprintf\n");
 fputs("hello fputs\n",stdout);
 
const char *msg="hello write\n";
write(1,msg,strlen(msg));
 
 fork();
 
 return 0;
}

运行出结果: 

[hongxin@VM-8-2-centos 2023-1-13]$ ./test 
hello printf
hello fprintf
hello fputs
hello write

但如果对进程实现输出重定向呢? ./test > log.txt , 我们发现结果变成了:

[hongxin@VM-8-2-centos 2023-1-13]$ cat log.txt 
hello write    //系统接口
hello printf   //以下都是库接口
hello fprintf
hello fputs
hello printf
hello fprintf
hello fputs

通过对比发现,C接口的函数打印了两次,系统接口打印了一次。

那么我们再把fork注释掉,再打印出结果:

6e966175168d47ac922084cde4ee1646.png

[hongxin@VM-8-2-centos 2023-1-13]$ ./test > file
[hongxin@VM-8-2-centos 2023-1-13]$ cat file
hello write
hello printf
hello fprintf
hello fputs

为什么会这样呢?但是我们大概能够明白,导致这种情况可能与fork有关!

3.2认识缓冲区

首先我们想知道缓冲区是什么呢?缓冲区的本质就是一段内存 ,那么是谁申请的呢?又是属于谁的呢?为什么要有缓存区呢?

为了更好的理解,我们先通过例子演示,再进一步探究!

例子:

在日常生活你和朋友关系很好,有一天你朋友喊你带点你们地方特产给他。有两种选择:1.自己给他送过去 2.你去快递公司给他送过去。自己送过去是非常不划算的,因为又浪费自己的时间,而且费用也高,那么我们选择快递送过去。

这段路程就好比是进程将数据送到磁盘,自己送时间会很长,而且代价也会很大。这个时候有个叫缓存的公司说,我专门送数据的,你给我,我送的又快,代价又小。

14d5204b3076422d838230cf3a6520f7.png

通过上述例子,不难得出缓存区的意义:节省进程进行数据的IO时间

进程将数据传到缓冲区中,通过的是fwrite,fwrite是写入到文件的函数,实质fwrite就是拷贝函数!将数据从进程拷贝到"缓冲区"或者外设中。

3.3缓冲区刷新策略

缓冲区刷新策略就跟快递公司送货的策略相似,一般情况下我们寄快递并不是给快递公司,他就立刻寄货。如果货少就需要积累到达一定量才送走。

那么缓冲区也是一样的,进程将数据拷贝到缓冲区中,缓冲区一定会结合具体的设备,定制自己的刷新策略:

a.立即刷新    --  无缓冲

b.行刷新        --  行缓存  --显示器

c.缓冲区满    --  全缓冲  --磁盘文件

对于行刷新,一般情况下是在显示器下采用的,因为文字是给人看的,比如我们给朋友发消息,也不可能采用将缓冲区占满了再发生给他,每次他看我的消息又久又长,这样是不符合现实的。所以我们需要通过行刷新。文件读写的时候就会采用全缓冲,这样的效率是最高的,一般情况下磁盘的需求也是比较大的。

特殊情况:

a.用户强制刷新

b.进程退出   --  一般都要进行行缓冲区刷新

3.4解决缓冲区问题(重)

又回到最开始的问题,最开始打印四条信息,然后通过冲定向后变成了七条。

8815670963f24b9f9bc26d488edd5893.png

首先这种现象一定和缓冲区有关,根据上述理解缓冲区一定不在内核中。我们在调用C语言接口和系统接口时,发现C语言接口调用了打印两次,系统接口打印了一次。如果缓冲区在内核中,那么系统接口write也应该打印两次。

那么所有缓冲区,都是指的户用级语言层面给我们提供的缓冲区!我们在写和读时,打开文件stdin,stdout,他们都是FLIE*指向的file结构体,在他这个结构体中有fd,缓冲区等。

那么我们可在xshell中,打开用vim该文件

vim /usr/include/stdio.h

该FILE结构体文件--typedef struct _IO_FILE FILE;

struct _IO_FILE {
  int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

//缓冲区相关

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;   /* Current read pointer */
  char* _IO_read_end;   /* End of get area. */
  char* _IO_read_base;  /* Start of putback+get area. */
  char* _IO_write_base; /* Start of put area. */
  char* _IO_write_ptr;  /* Current put pointer. */
  char* _IO_write_end;  /* End of put area. */
  char* _IO_buf_base;   /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;//fd

#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

缓冲区在哪里,缓冲区是什么,也迎刃而解了。通过上述知识,关于fork问题,那么也能解决了。

缓冲区问题:

代码在结束之前,进行创建子进程

1.如果我们没有进行重定向(>),就看见了4条消息的原因:

写入文件是对显示器写,stdout默认使用的是行刷新,在进程fork之前,三条C函数已经将数据进行打印到输出到显示器上(外设),你的FILE内部, 进程内部不存在对应的数据。

2.如果我们进行了重定向(>),看见了7条消息,少1条消息的原因:

写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲,在进程fork之前,三条C函数已经将数据进行打印到输出到显示器上(外设),write虽然带了\n,但是不足以stdout缓冲区写满!数据并没有被刷新!

3.打开fork显示7条消息,关闭fork显示4条的原因:

执行fork的时候,stdout属于父进程,创建进程时,紧接着就是进程退出。不管谁先退出,一定要进行缓冲区刷新(就是修改),这个时候就会发生写时拷贝,所以最终数据会显示两份就打印了7条条消息。我们关闭fork,没有发生写时拷贝,那么输入到显示器上的还是那4条消息。

代码在结束之前,没有进行创建子进程

4.write为什么没有写时拷贝的原因:

已上过程都与write无关,write是没有FILE,而是用的fd,就没有C提供的缓冲区。

使用write等 IO接口,函数直接输出到输出设备上,是不带缓冲但是标准IO库是带有缓冲的,printf遇到\n的时候才会冲刷缓冲区,输出到输出设备上。

3.4myStdio-dome

通过调用系统接口实现的C语言库接口,主要是用于理解系统调用接口和缓冲区。通过代码我们就能理解到,缓冲区是实则是结构体文件(FILE)中的一段内存,是通过文件标识符链接的---缓冲区通过文件标识符链接打开文件,然后再将缓冲区数据拷贝到文件中。

下面就将代码展示,不理解的代码中有非常详细的注释。

myStd.h--接口的定义

#pragma once

#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>

//buffer-缓冲区大小
#define SIZE 2014

//缓冲模式
#define SYNC_NOW    1
#define SYNC_LINE   2
#define SYNC_FULL   4

typedef struct _FILE
{
    int flags;   //刷新方式
    int fileno;  //文件标识符

    char buffer[SIZE]; //缓冲区

    int cap;    //buffer的总容量
    int size;   //buffer当前的使用量

}FILE_;

//打开文件
FILE_ *fopen_(const char *path_name,const char *mode);

//写入
void fwrite_(const void* ptr,int num,FILE_ *fd);

//关闭文件
void fclose_(FILE_* fp);

//强制缓冲区
void fflush_(FILE_* fp);

myStd.c--接口的实现

#include "myStdio.h"


FILE_ *fopen_(const char *path_name,const char *mode)
{
    //打开文件时,可以传入多个参数选项用下面的一个或者多个常量进行“或”运算,构成flags。
    int flags =0;
    int defaultMode =0666;//默认打开文件权限

    if(strcmp(mode,"r")==0)//选择以读的方式打开文件
    {
        flags |=O_RDONLY;//只读模式:O_RDONLY
    }
    else if(strcmp(mode,"w")==0)//选择以写的方式打开文件
    {
        flags |=(O_TRUNC|O_WRONLY|O_CREAT);//只写模式:O_WRONLY-只写,O_CREAT-创建新文件,O_TRUNC-覆盖原有
    }
    else if(strcmp(mode,"a")==0)//选择以追加的方式打开文件
    {  
        flags |=(O_WRONLY|O_APPEND|O_CREAT);//追加模式:O_WRONLY-只写,O_CREAT-创建新文件,O_APPEND--追加
    }
    else{
    }

    //调用系统接口实现

    int fd = 0;
    if(flags & O_RDONLY) fd=open(path_name,flags);//只读不需要其他权限
    else fd = open(path_name,flags,defaultMode);

    //调用失败,说明原因,返回null
    if(fd < 0)
    {
        const char* err = strerror(errno);//获取错误码
        write(2,err,strlen(err));//向fd=2-stderr中打印,显示错误原因
        return NULL;
    }

    FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));//开辟结构体大小空间
    assert(fp);

    fp->flags =SYNC_LINE;       //默认设置成行刷新
    fp->fileno =fd;             //open返回的文件标识符--文件描述符:通过映射 路径+文件名
    fp->cap = SIZE;             //缓冲区总容量
    fp->size = 0;               //实际总容量
    memset(fp->buffer,0,SIZE);  //初始化buffer
    
    return fp;//为什么打开一个文件,就返回一个FIEL* 指针--因为是用FIEL的方式组织,便于后续操作--写,刷新缓存,关闭文件等
}

void fwrite_(const void* ptr,int num,FILE_ *fp)//冲文件
{
    //1.写入到缓冲区中
    memcpy(fp->buffer+fp->size,ptr,num);//这里不考虑缓冲区溢出的问题
    fp->size+=num;

    //2.判断已什么方式刷新
    if(fp->flags & SYNC_NOW)//立即刷新
    {
        write(fp->fileno,fp->buffer,fp->size);//刷新就是将缓冲区数据拷贝到打开文件中
        fp->size = 0; //清空缓冲区
    }
    else if(fp->flags & SYNC_LINE)//\n--行刷新
    {
        if(fp->buffer[fp->size-1]=='\n')
        {
            write(fp->fileno,fp->buffer,fp->size);
            fp->size=0;
        }
    }
    else if(fp->flags & SYNC_FULL)//实际与当前容量相等--全刷新
    {
        if(fp->size==fp->cap)
        {
            write(fp->fileno,fp->buffer,fp->size);
            fp->size = 0;
        }
    }
    else{

    }

}

void fflush_(FILE_* fp)
{
    if(fp->size>0)
    write(fp->fileno,fp->buffer,fp->size);
    fsync(fp->fileno);//将数据强制要求刷入磁盘
    fp->size=0;

}

void fclose_(FILE_* fp)
{
    fflush_(fp);
    close(fp->fileno);
}

mian.c--测试

#include "myStdio.h"

int main ()
{
    FILE_*  fp=fopen_("./log.txt","w");//传入路径名和刷新模式
    if(fp==NULL)
    {
        return 1;
    }
    const char *msg = "hello fwirte_!\n";

    fwrite_(msg,strlen(msg),fp);

    fclose_(fp);

    return 0;
}

运行结果:

[hongxin@VM-8-2-centos 2023-1-14]$ cat log.txt 
hello fwirte_!

根据上面代码再次进行调试,感受刷新缓冲区。

#include "myStdio.h"

int main ()
{
    FILE_*  fp=fopen_("./log.txt","w");//传入路径名和刷新模式
    if(fp==NULL)
    {
        return 1;
    }

    int cnt = 10;
    const char *msg = "hello!\n";

    while(1)
    {
        fwrite_(msg, strlen(msg), fp);
        //fflush_(fp);
        sleep(1);
        printf("count: %d\n", cnt);
        //if(cnt == 5) fflush_(fp);
        cnt--;
        if(cnt == 0) break;
    }

    fclose_(fp);

    return 0;
}

监控脚本 

 while :; do cat log.txt ; sleep 1 ;echo "##########################" ; done

没有刷新缓冲区时,最后关闭的时候才刷新缓冲区,直到最后才一次性打印出来。 

c4ccad848f4745d9932b767e706d4cf6.png

每次都强制刷新缓冲区时,会一个一个打印出来。

5c962bb76e9f42f6b7fe6723436fe0ac.png

那么在OS(操作系统)中,数据是怎么写入磁盘中的呢?

学习了缓冲区,我们就明白了数据是不能直接就拷贝到磁盘的,数据是先struct file-->*files->文件描述发->内核缓冲区-->刷新缓冲区-->磁盘。这个过程是由操作系统自主决定,这个就跟我们上述代码中的行缓存,全缓冲是不一样的。上述是C语言应用层方面自己封装的FILE,这里是操作系统层从缓冲区刷新到磁盘中是非常复杂的。

特别需要理解的库级别的缓冲区和系统级别的缓冲区不是一个概念库级别是FILE中的一段内存,系统级别则是更加复杂的处理方式。

cb6b6ac57c194039b31b524dc8332281.png在这个过程中,数据都是通过拷贝进行传输,输入一段数据到磁盘会发生三次拷贝。 

如果操作系统(OS)突然宕机(down机,死机)了,数据还在缓冲区中。这个时候就可用fsync。

#include <unistd.h>

int fsync(int fd);

int fdatasync(int fd);

fsync, fdatasync - synchronize a file's in-core state with storage device

--  将文件的内核状态与存储设备同步/换句话说就强制将数据刷新到磁盘中

当我们在代码中使用fsync之后,发现没有调用fflush_,它也会强制刷新。

四、文件系统

上面讲的FILE也是文件,是打开文件。那么未被打开文件,也是放在磁盘上,磁盘上有大量的文件也是必须被静管理的,方便我们随时打开,这也是文件系统。文件系统既要管理动态打开文件,又要管理静态未被打开文件。文件是放在磁盘的,我们对于磁盘是陌生的,不知为何物,不知是什么样子,不知道它到底是这样存数据,也不知道它是如何找数据的。下面我们就会先了解硬件--磁盘,然后再来学习文件系统。


4.1磁盘的物理结构

磁盘分为硬盘与软盘,在历史的长流中,软盘渐渐地被淘汰,因为软盘是容量比较小,寿命比较短,现在内存动辄就是TB,那么软盘就渐渐的退出历史舞台。硬盘是一个机械结构加外设,硬盘的访问速度相比于内存,CPU是非常慢的。

就导致现在我们就很少看见磁盘,磁盘是我们计算机中唯一一个机械结构。现在笔记本电脑中更多是选择SSD(固态硬盘),相比传统的机械硬盘的性能优势主要表现在:读写速度快、防震抗摔性、低功耗、无噪音、工作温度范围大、轻便。固态硬盘由于其做工精细,因此价格也更贵。

但是相对于企业,磁盘依旧是主流。SSD也并不是完美,首先来说价格相对于磁盘是更高,最重要的是SSD有读写的限制,如果写多了就会出现SSD被击穿的问题,就会造成数据的丢失。但是这都不是绝对的,磁盘和SSD都各有所长,很多时候也会选择混盘,磁盘和固态硬盘混用。

硬盘是什么样子:

c868beccd4b54b30af42a8e770fa7604.png3be454f07fcc47d4926cddbbfcb79d1f.png

磁盘是如何动的:

14164a6503984aaab3098abcf10ccc38.png770ca18336f644b08cbc0d0bf7b1cd76.gif

磁头摆动和盘片旋转是通过马达控制的,我们可以通过硬件电路组成伺服系统给磁盘发二进制指令,然后让磁盘定位去寻址某个特定的区域,然后从磁盘上读取数据。磁盘和磁头都有两面,两面都是可以进行读写数据的。

磁头和盘片是没有接触的!他们的距离就像一架波音757隔着1米的距离在飞行。而且磁盘是不能有一点灰尘的,就好像飞机飞行前没有障碍物一样。所以磁盘必须得防止抖动,一旦磁头和盘面接触就会使得盘面刮花,丢失数据。

硬件上保存二进制,是跟对应的设备有关,寄存器和内存是通过触发器电脉冲,对硬件设备进行带点或者失电,通过电路的有无来代表二进制。每一个寄存器触发器存储单元,每一个单元都是硬件电路,他们都可以进行存放电的。

在不同的设备,表示二进制的方式也是不同的,在网络里面通过信号的有无,通过信号的疏密来表示01。

这里可以理解磁头通过带放电,对磁盘某一个位置进行N/S极互换,就完成了0/1的写入,如果需要写入512比特位,就相当于触电523次。--磁化技术

4.2磁盘的存储结构 

我们看到盘面是非常光滑的,但是在放大下,磁道和磁道之间是有间隙的。在微观下,一个扇区可存大量的电子。

 ff164d76412942a4bd919f3a797c346e.png2918a8846ef048fb813492e0d8e56d33.png

磁盘寻址的基本单位是扇区(512byte),虽然越靠近圆心外侧面积越大,但是可以通过工艺进行不同扇区的密度大小进行调解,所以每个扇区存储都是512byte。不考虑其他情况。

在单面中定位一个扇区:   

先确认磁道,再对应的磁道确定扇区。磁头来回摆动确认磁道,盘片旋转确定扇区。 

在立体视图中,有的时候会磁道就等于柱面,磁头和扇面不变。磁头是共同进退的-同时进行寻找磁道。

在磁盘中定位一个扇区:

先定位在哪一个磁道,再定位磁头(盘面),最后定位在那个扇区。

4.3磁盘的逻辑结构

在我小时候就特别喜欢把手指母放磁带的一个孔让转,当时感觉特别的爽。因为后面随着人们生活质量的提高,技术的更新,慢慢就开始普及了CD了。家里的磁带也没有用了,我们就喜欢可以将磁带中的塑料带子扯出来,不为别的就是好奇!

40c41b5d34414d0ca209cfc1a4b95c84.pngff164d76412942a4bd919f3a797c346e.png

当把磁带扯直,我们就可以把它看成一条瘦长的矩形,这条矩形中存放这数据。磁盘其实也是一样,我们将磁盘中磁道拉直,也就变成矩形,里面存在着大量的数据。我们就可以把它想象成线性结构。

1023f7f4163146dbbf5df7f17e827935.png

上述是对于一条磁道,那么多块磁盘呢?一般电脑选择的500GB,500GB是可以分盘的CDEF。那么我们就可以抽象化将不同盘面想象成一块数据,整个磁盘看做一个数组sector arr[n],这样就方便对磁盘进行管理。

602f9db9184c4fa291e83368326ffc81.png

对磁盘进行管理就变成了对数组进行管理。磁道-track, 盘面-platter,扇区-sector。在磁盘中的定位方法是TPS:1磁道2盘面3扇区。那在操作系统中我们就可以通PTS进行查找(方法)。查找扇区地址,在操作系统中被称为LBA地址。

计算一个盘的容量(自定义数据):

盘面:4  

每一面的磁道:10

每一个磁道的扇区:100

扇区大小:512byte

盘的容量:4*10**100*512=1.95G

下标范围:4*10*100=4000

寻找LAB-123地址(自定义地址):

123/1000=0        --第0盘面

123/100  =1        --1号磁道

123%100=23      --23号扇区

 我们将磁盘抽象化就是下面这样子

94d056e4101d4c7694ad9c7274c66332.png

访问一个扇区是512字节,如果将磁盘的访问的基本单位设置512字节,对于IO访问来说效率太低,一般是对8个扇区进行同时访问,OS内的文件系统定制的进行多个扇区的读取4KB为基本单位即使是指向读取/修改1bit,那么必须将4KB load内存,进行读取或者修改,如果必要,在写回磁盘

内存是被划分成为了4KB大小的空间–页框;磁盘中的文件尤其是可执行文件是按照4KB大小划分好的块–页帧,磁盘向内存拷贝数据,实质也是页帧将数据拷贝到页框中。

22c85387a120464f9a11c8ad469d6a86.png

不管是拷贝数据还是修改数据,那么最开始都是需要将数据管理起来,这里我们通过分治的思想,一级一级的到向下一层减少管理空间,我们将最底层管理,对于上层而言也就是重复的工作了。所以理解文件管理我们就开始从这5GB开始(自定义5GB)。

b69c3b4fde754490b7856b255aa8231b.png

4.4理解文件系统(重)

我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。

[hongxin@VM-8-2-centos ~]$ ls -l
total 12  //总用量
drwxrwxr-x  2 hongxin hongxin 4096 Jan 15 16:30 2023-1-15
drwxrwxr-x 40 hongxin hongxin 4096 Jan 15 16:25 linux_code
-rw-rw-r--  1 hongxin hongxin  148 Oct 25 16:37 tmp.tgz

每行包含7列:

drwxrwxr-x 40              hongxin         hongxin   4096         Jan 15 16:25      linux_code

模式           硬链接数   文件所有者    组             大小           最后修改时间   文件名

ls -l读取存储在磁盘上的文件信息,然后显示出来,为什么它能分开显示呢?我们知道:

文件=内容+属性

linux的文件属性和文件内容是分批储存的,如下所示:

974b8f531f1a45448be4510e0c50848d.png

一个文件属性的,文件的大小,权限,等等信息在都在Inode中。

其实这个信息除了通过ls -l方式来读取,还有一个stat命令能够看到更多信息  

[hongxin@VM-8-2-centos ~]$ stat test.c 
  File: ‘test.c’
  Size: 0             Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d    Inode: 656174      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1005/ hongxin)   Gid: ( 1005/ hongxin)
Access: 2023-01-16 15:44:36.036230458 +0800
Modify: 2023-01-16 15:44:36.036230458 +0800
Change: 2023-01-16 15:44:36.036230458 +0800
Birth: -

在这里也显示I/O块为4kb,也就再次证实磁盘的访问的基本单位4kb(大多数操作系统)。

上面的执行结果有信息需要解释清楚 inode,为了能解释清楚inode我们先简单了解一下文件系统。我们也知道了解文件系统就应该了解最底层这空间大小:

93c7cc2531634e50afbfa9538ff142fb.png

Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被 划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。 

启动块-Boot Block

大小就是1kB,由PC标准规定,用来存储磁盘分区的信息和启动信息,任何文件系统都不能使用该块。(如果这个块损坏,整个文件系统也就启动不起来了)。操作系统的开机,加电,启动相关的信息都是在这个块中的。

块组-block group():

ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。block group()它所包含的各种块根据分组得来。

超级块(Super Block):

存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了

                                                --保存整个文件系统的信息

正常情况下,保存的整个文件系统的信息不应该放在分区的最开始吗,为什么会在多个分组中存在呢,这里是为了备份,当文件受损时,就可以将其他分组的Super Block拷贝到当前,文件系统就得到恢复。 

在学习下面内容之前,我们还是得对文件内容和属性深一步挖掘。我们知道文件是有文件内容和属性构成的,文件属性和文件内容是分批存储的。

文件内容:

data block就是存放文件内容,它的特性就是随着应用类型的变化,大小在变化。

文件属性:

ionde就是用来存储文件属性的,inodo属性信息不仅仅包括文件大小,属主,用户组,文件权限,修改时间,类型。还包含指向文件实体的指针功能,但是inode里面不包含文件名。inode的大小也是固定的。ionde为了区分彼此,每个inode都有自己的ID。

通过输入ls -li,我们发现他们文件的inode是不一样的。

[hongxin@VM-8-2-centos 2023-1-15]$ ls -li
total 0
926813 -rw-rw-r-- 1 hongxin hongxin 0 Jan 16 19:49 log.txt
926812 -rw-rw-r-- 1 hongxin hongxin 0 Jan 16 19:47 test.c

inode表(inode Table) :

保存了分组内部所有可用的(已经使用+未被使用)inode。当我们创建一个文件时,第一时间找的就是inode Table,将文件属性如文件大小,所有者,最近修改时间等存放在这个表中

数据区(Date blocks):

保存分组内部所有文件的数据库

inode位图(inode Bitmap):

每个bit表示一个inode是否空闲可用。

例如:0000001

位图中比特位的位置和当前文件对应的inode位置是一一对应的。比特位的位置代表的是它是第几个inode,比特位为0 ,代表inode未被占用,比特位为1,代表inode被占用。通过比特位的偏移位置去找inode Table去找到该inode(inode自己的编号),增加属性等操作 。

块位图(Block Bitmap):

Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用

块组描述符GDT(Group Descriptor Table):对应分组的宏观的属性信息,

查找文件属性和内容原理

如何查找文件属性和内容,首先我们需要知道inode编号是如何形成的:

30675df7958e4a178de19dc456e0d167.png

每个区都有自己编号,例如:Block group 0-Block group n: 1000-10000 ,每个区都有自己的组,组会通过比特位图进行增加值,例如:起始1000,当新建一个inode,那么比特位图就加1:1001,你从01也可以得知在inode table中,它是第一个建立的ionde。

查找文件属性:实质就是从文件中找到inode编号,然后通过inode编号找到inode Table。

我们知道ionde Table后,又是如何通inode查找dateblocks呢?如图所示:

27928f171c394aadb3d1c90318c878dc.png

 在struct_ionde中,通过data数组找到相应的数据块,这里特别需要注意的是,0-11是特指,而12-14是泛指。例如12指向一个数据块,这块数据块可以指向其他块数据块,这个块数据块保存的是其他数据块的地址。如果12不够用,例如13就可以指向其他数据块,其他数据块又保存其他数据块的地址。12是一级索引,13就是二级索引。

最后在这里需要了解:删除一个文件,直接将inode和block比特位清零即可-惰性删除。

建新文件操作

新文件操作将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们在通过touch一个新文件来看看如何工作。         

[hongxin@VM-8-2-centos 2023-1-15]$ touch abc

[hongxin@VM-8-2-centos 2023-1-15]$ ls -i abc 
263466 abc

通过上面ls -i abc 我们可以发现文件名是不在ionde中的,不属于文件属性管的。我们查找文件通过的文化名,这个是因为目录的数据块放的是当前目录下的文件名和inode的映射关系。所以我们就可以通过文件名来查找文件信息。

为了说明问题,将上图简化:

eecb3543cc7e4370a1d5002dc738121c.png

建一个新文件主要有一下4个操作:

1. 存储属性

内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。

2. 存储数据

该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。

3. 记录分配情况

文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。

4. 添加文件名到目录

新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起

最后为了避免知识混淆,我们汇聚一幅图需要理解的是向磁盘进行I/O前,操作系统会读取该文件内容和属性,再进行加载(读取也要结合自身的操作)。

d4b49ad943794e27ba372661073f1f74.png

五、软硬链接(重)

这块知识内容本来是应该在文件一块介绍的,但软硬链接的细节点也不少,所以把它单独出来进行探究。 


5.1软硬链接的创建

我们先创建一个软链接文件,输入 ln -s myfile.c  soft_file.link

[hongxin@VM-8-2-centos 2023-1-17]$ ln -s myfile.c  soft_file.link

[hongxin@VM-8-2-centos 2023-1-17]$ ll
total 0
d1e8f2348ca2462a9a1e5ad1e7d8c2d5.png

lrwxrwxrwx :l--是文件类型,链接文件

 再查看inode,他们都有自己独立的inode,说明他们都是一个独立的文件。6e8eb9f41bc9471c829577bcf2e63932.png

我们再建立一个硬链接文件,输入ln myfile.c hard_file.link ,发现hard_file.link和 myfile.c的数字变成了2。

afad4f1a2a2641db9269fb444afd2a34.png

再查看inode,它的inode数是一样的。a494c2c0c2c94b36a4385b693b1f4520.png

为什么硬链接没有独立inode呢?我们通过测试来理解。

最开始我们没有给文件写入数据时,文件的大小为0

[hongxin@VM-8-2-centos 2023-1-17]$ ll -li
total 0
1054545 -rw-rw-r-- 2 hongxin hongxin 0 Jan 17 18:59 hard_file.link
1054545 -rw-rw-r-- 2 hongxin hongxin 0 Jan 17 18:59 myfile.c
1054546 lrwxrwxrwx 1 hongxin hongxin 8 Jan 17 19:01 soft_file.link -> myfile.c

当我们给myfile.c写入时,文件的大小一起变成了12,再给hard_file.link 写入时,他们文件大小一起变成了24。不管是myfile.c还是hard_file.link写入时,他们大小都会变成一样的。

[hongxin@VM-8-2-centos 2023-1-17]$ echo "hello  link" >> myfile.c 
[hongxin@VM-8-2-centos 2023-1-17]$ ll -li
total 8
1054545 -rw-rw-r-- 2 hongxin hongxin 12 Jan 17 19:28 hard_file.link
1054545 -rw-rw-r-- 2 hongxin hongxin 12 Jan 17 19:28 myfile.c
1054546 lrwxrwxrwx 1 hongxin hongxin  8 Jan 17 19:01 soft_file.link -> myfile.c

[hongxin@VM-8-2-centos 2023-1-17]$ echo "hello  link" >> hard_file.link 

[hongxin@VM-8-2-centos 2023-1-17]$ ll -li
total 8
1054545 -rw-rw-r-- 2 hongxin hongxin 24 Jan 17 19:29 hard_file.link
1054545 -rw-rw-r-- 2 hongxin hongxin 24 Jan 17 19:29 myfile.c
1054546 lrwxrwxrwx 1 hongxin hongxin  8 Jan 17 19:01 soft_file.link -> myfile.c

相信大家心里已经有答案了,为了再次确认我们再看看他们文件的内容。所以实则hard_file.link和cat myfile.c 都是cat myfile.c 文件。

[hongxin@VM-8-2-centos 2023-1-17]$ cat hard_file.link 
hello  link
hello  link
hello  link
[hongxin@VM-8-2-centos 2023-1-17]$ cat myfile.c 
hello  link
hello  link
hello  link

5.2软硬链接的区别

软链接有独立的inode,可被当做独立文件看待。硬链接没有独立的inode,那么建立一个硬链接就是在指定路径下,新增文件名和inode编号的映射关系!

在shell中的做法 ,硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。

1054545 -rw-rw-r-- 2 hongxin hongxin 36 Jan 17 19:29 hard_file.link
1054545 -rw-rw-r-- 2 hongxin hongxin 36 Jan 17 19:29 myfile.c
1054546 lrwxrwxrwx 1 hongxin hongxin  8 Jan 17 19:01 soft_file.link -> myfile.c

1df3134682724be2abe3827f3fe33600.png

在硬链接中,那个链接数使用计数器,有一个文件指向myfile.c,count++。所以链接数为2,再有文件硬链接myfile.c,count++,所以链接数为2。

98ae392c112a44d6853434e881ce7594.png

链接失效

df87fb39b9344ec88be533fc8f15a19f.png

硬链接的硬链接变成1了,然后 cat hard_file.link ​​​​文件还有数据。​​​

[hongxin@VM-8-2-centos 2023-1-17]$ cat hard_file.link 
hello  link
hello  link
hello  link 

真正删除一个文件,文件的硬链接数0的时候这个文件才算真正被删除。所以这个硬链接就好比重命名

我们发现软件连接文件变红了,那个这个文件还存在吗?答案是存在的,虽然myfile.c文件删除了但是soft_file.link -> myfile.c的ionde还是在的,因为我们访问一个文件是通过文件名路径进行访问的,删除了myfile.c破坏了soft_file.link -> myfile.c的文件名,这个文件名是在上级目录中存放的。

1356af8a3b52450699df65c123a965b8.png

我们再重新新建myfile.c,让文件路径完整,但是我们发现这个文件是新的文件了,myfile.c和 hard_file.link没有联系了。 myfile.c中也没有数据了。

a82f6a5f227748e09b2c18fd4befe312.png

我们删除软链接,是不影响链接的文件的。删除软链接: unlink soft_file.link 

[hongxin@VM-8-2-centos 2023-1-17]$ unlink soft_file.link 
[hongxin@VM-8-2-centos 2023-1-17]$ ll
total 4
-rw-rw-r-- 1 hongxin hongxin 36 Jan 17 19:29 hard_file.link
-rw-rw-r-- 1 hongxin hongxin  0 Jan 17 20:48 myfile.c

这里的软链接好比是Windows下的快捷方式。

2a9484d20c4546eebfe6cf9ceb00d596.png

我们打开浏览器,可以通过快捷方式,也可以去磁盘中找到可执行程序。

5.3软硬链接的应用 

软链接

我们在目录下建立test文件,然后随便写上端代码,生成可执行文件。

[hongxin@VM-8-2-centos Test]$ touch test.c
[hongxin@VM-8-2-centos Test]$ vim test.c 

#include <stdio.h>

int main()
{
    printf("hello 软链接!");

    return 0 ;
}

[hongxin@VM-8-2-centos Test]$ gcc test.c -o test 
[hongxin@VM-8-2-centos Test]$ ./test 
hello 软链接!

当我们cd ..退出,在其他文件执行时,需要记住文件路径,发现是非常麻烦的!

[hongxin@VM-8-2-centos ~]$ ./2023-1-17/Test/test
hello 软链接!

我们就可以建立软链接,在当前文件下直接执行。

[hongxin@VM-8-2-centos ~]$ ln -s ./2023-1-17/Test/test test

0ed35b8f5b0f40c1bd1433880930fe86.png

硬链接 

我们先在创建一个文件file.txt,然后在创建一个目录empty。

[hongxin@VM-8-2-centos Link]$ touch file.txt
[hongxin@VM-8-2-centos Link]$ mkdir empty 

4398ec2b21a6418289f66c05faef24c1.png 我们发现目录的链接数是2,普通文件连接数是1,这是为什么呢?

因为普通文件本身有一个文件名和自己的inode具有一个映射关系。

关于目录empty,我们发现empty中他不关有自己当前路径,还有他的上级路径。

[hongxin@VM-8-2-centos Link]$ ll -li 
1054553 drwxrwxr-x 2 hongxin hongxin 4096 Jan 17 21:37 empty

[hongxin@VM-8-2-centos Link]$ cd empty/
[hongxin@VM-8-2-centos empty]$ ls -lina
1054553 drwxrwxr-x 2 1005 1005 4096 Jan 17 21:37 .
1054551 drwxrwxr-x 3 1005 1005 4096 Jan 17 21:37 ..

[hongxin@VM-8-2-centos Link]$ pwd
/home/hongxin/2023-1-17/Link

[hongxin@VM-8-2-centos Link]$ ll -lina
1054551 drwxrwxr-x 3 1005 1005 4096 Jan 17 21:37 .

./代表当前路径,cd..代表返回上级路径。 这个.就相当于是empty的硬链接。这个..就是上级目录中的硬链接。

这里Link是链接数是4,因为在empty中的..是Link的硬链接数,file.txt中也有..,再加.,还有自己本身的ionde。所以Link的链接数为4。

eedaa4a6feb74c22a484ffa2e19c7269.png

 如图所示:5eba7863913d4aacaabb02ab91de8758.png

最后一点注意:系统不让用户给普通文件建立硬链接的原因是,害怕文件出现死循环。这个./..系统默认创建的。

六、动态库和静态库 

动静态库是不陌生的,平时写的代码经常会用到各种标准库,自己模拟实现过一些库函数。但只是看见过"猪跑",没有吃过"猪肉"。


6.1动态库和静态库的理解

静态库(.a):

程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静 态库

动态库(.so):

程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

代码测试:

/my_add.h/

#pragma once

#include <stdio.h>

extern int Add(int x,int y);

/my_add.c/

#include "my_add.h"

int Add(int x,int y)
{
    printf("entrt Add func,%d+%d=?\n",x,y);
    return x+y;
}

/my_sub.h/

#pragma once

#include <stdio.h>

extern int Sub(int x, int y);

/my_sub.c/

#include "my_sub.h"

int Sub(int x, int y)
{
    printf("entrt Add func,%d-%d=?\n",x,y);
    return x-y;
}

/main.c/

#include "my_add.h"
#include "my_sub.h"

int main()
{
    int a=10;
    int b=20;

    int ret=Sub(a,b);
    printf("Sub result: %d\n",ret);
    ret=Add(a,b);
    printf("Add result: %d\n",ret);
}

生成可执行程序

(1)直接生成可执行文件

//生成可执行文件

[hongxin@VM-8-2-centos 2023-1-18]$ gcc -o mymath main.c my_add.c my_sub.c

//执行结果

[hongxin@VM-8-2-centos 2023-1-18]$ ./mymath 
entrt Add func,10-20=?
Sub result: -10
entrt Add func,10+20=?
Add result: 30

(2)先直接生成目标文件,再链接生成可执行文件

//生成目标文件

[hongxin@VM-8-2-centos 2023-1-18]$ gcc -c my_add.c

[hongxin@VM-8-2-centos 2023-1-18]$ gcc -c my_sub.c
[hongxin@VM-8-2-centos 2023-1-18]$ gcc -c main.c

[hongxin@VM-8-2-centos 2023-1-18]$ ll *o
-rw-rw-r-- 1 hongxin hongxin 1752 Jan 18 11:08 main.o
-rw-rw-r-- 1 hongxin hongxin 1536 Jan 18 11:08 my_add.o
-rw-rw-r-- 1 hongxin hongxin 1536 Jan 18 11:08 my_sub.o

//链接目标和库,生成可执行

[hongxin@VM-8-2-centos 2023-1-18]$ gcc -o my_math main.o my_sub.o my_add.o 

[hongxin@VM-8-2-centos 2023-1-18]$ ll *my_math
-rwxrwxr-x 1 hongxin hongxin 8480 Jan 18 11:11 my_math

//执行结果

[hongxin@VM-8-2-centos 2023-1-18]$ ./my_math 
entrt Add func,10-20=?
Sub result: -10
entrt Add func,10+20=?
Add result: 30

方法一和方法二实质上是没有区别的,方法一也是有方法二的步骤,只是操作系统就帮我们做了。

在其他文件调用方法

当把代码写好了,我们想在其他文件下使用该方法,那应该怎么来操作呢?首先我们将相应代码划分在不同的目录中。

98d1ade99d364291b2de77c7e03abaa0.png

在Test目录中,我们认为有了main.c,我们将main.c生成目标文件,然后再将其他两个目标文件链接起来不就可以生成可执行了吗。

d81dd9b5e9fc4a02ab50e085181c7037.png

很遗憾,报错了。虽然有了方法的目标文件,但是main.c调用方法时,需要头文件。不就是头文件嘛,把它拷贝到Test目录下就是了,反正最重要的是我们写的方法。

a36d342d3f58449f815b918da5ff515d.png

我们想要的都有了,我们再次生成main.c的目标文件。

[hongxin@VM-8-2-centos Test]$ gcc -c main.c 

[hongxin@VM-8-2-centos Test]$ ll main.o
-rw-rw-r-- 1 hongxin hongxin 1752 Jan 18 12:20 main.o

再将整体生成可执行

[hongxin@VM-8-2-centos Test]$ gcc -o mymath main.o my_add.o my_sub.o
[hongxin@VM-8-2-centos Test]$ ll mymath

-rwxrwxr-x 1 hongxin hongxin 8480 Jan 18 12:23 mymath

//执行结果

[hongxin@VM-8-2-centos Test]$ ./mymath 
entrt Add func,10-20=?
Sub result: -10
entrt Add func,10+20=?
Add result: 30

这里虽然是两个文件之间的拷贝,其实已经有了库的思想。当其他人想使用该方法,我们可以通过数据打包压缩,但我们却不给对方提供我们的源文件,提供.o可重定向目标二进制文件,让他自己进行链接。未来可以我们可以提供给对方.o(方法的实现)和.h(声明方法),他也可以用他自己的代码进行链接。

但是我们这样一个一个的拷贝".o文件"和".h文件"是非常不方便的,所以我们就尝试将所有".o文件"打一个包,提供给对方一个库文件。

这个库文件是通过打包方式不同,而形成动态库和静态库。

所以,库的本质:就是.o文件的集合

6.2静态库和静态链接的生成

生成静态库指令-ar

[hongxin@VM-8-2-centos Test]$ ar -rc libmymath.a my_add.o my_sub.o

//ar(archive)是gnu归档工具,rc表示(replace and create)

发布 -output

output "文件夹",把一个文件移动到另一个文件夹

为了方便使用,我们写一个makefile。

/makefile/

libmymath.a:my_add.o my_sub.o
	ar -rc $@ $^
my_add.o:my_add.c
	gcc -c my_add.c -o my_add.o
my_sub.o:my_sub.c
	gcc -c my_sub.c -o my_sub.o
	
.PHONY:output
output:
	mkdir -p mylib/include
	mkdir -p mylib/lib
	cp -f *.a mylib/lib
	cp -f *.h mylib/include

.PHONY:clean
clean:
	rm -rf *.o libmymath

通make形成库文件

[hongxin@VM-8-2-centos Func]$ make
ar -rc libmymath.a my_add.o my_sub.o

[hongxin@VM-8-2-centos Func]$ ll *.a
-rw-rw-r-- 1 hongxin hongxin 3280 Jan 18 14:23 libmymath.a

//发布--新建文件,拷贝内容

[hongxin@VM-8-2-centos Func]$ make output
mkdir -p mylib/include
mkdir -p mylib/lib
cp -f *.a mylib/lib
cp -f *.h mylib/include

//查看是否建议

[hongxin@VM-8-2-centos Func]$ tree mylib
mylib
|-- include
|   |-- my_add.h
|   `-- my_sub.h
`-- lib
    `-- libmymath.a

我们交付库文件实质就是将文件.a/.so和匹配的头文件一起交付给别人使用

再将这个库文件进行压缩打包,然后拷贝给其他人使用

//Func

[hongxin@VM-8-2-centos Func]$ tar czf mylib.tgz mylib/

bcfcb71516014b459a2947b8c888ac8d.png

[hongxin@VM-8-2-centos Func]$ cp mylib.tgz ../Test/ 

使用者再得到文件后,进行解压使用 

Test

[hongxin@VM-8-2-centos Test]$ ls
main.c  main.o  my_add.h  my_add.o  mylib.tgz  mymath  my_sub.h  my_sub.o

/再将不要的删的删除/

[hongxin@VM-8-2-centos Test]$ ls
main.c  mylib.tgz

解压

[hongxin@VM-8-2-centos Test]$ tar xzf mylib.tgz 

[hongxin@VM-8-2-centos Test]$ ll
total 12
-rw-rw-r-- 1 hongxin hongxin  196 Jan 18 11:06 main.c
drwxrwxr-x 4 hongxin hongxin 4096 Jan 18 15:26 mylib
-rw-rw-r-- 1 hongxin hongxin  948 Jan 18 15:36 mylib.tgz

虽然我们有mylib文件,但是main.c是在当前路径中找头文件和目标文件,而头文件和目标文件是在mylib路径下 。

[hongxin@VM-8-2-centos Test]$ tree mylib
mylib
|-- include
|   |-- my_add.h
|   `-- my_sub.h
`-- lib
    `-- libmymath.a

在生成可执行文件时,有几个细节需要特别注意:

1. 通过路径找到头文件            -I ./mylib/include/

2.通过路径找到目标文件         -L ./mylib/lib

3.如果链接第三方库,必须指明库名称  libmymath.a需要去掉前缀和后缀--  mymath

[hongxin@VM-8-2-centos Test]$ gcc -o mymath  main.c -I ./mylib/include/ -L ./mylib/lib/ -lmymath

[hongxin@VM-8-2-centos Test]$ ll
total 24
-rw-rw-r-- 1 hongxin hongxin  196 Jan 18 11:06 main.c
drwxrwxr-x 4 hongxin hongxin 4096 Jan 18 15:26 mylib
-rw-rw-r-- 1 hongxin hongxin  948 Jan 18 15:36 mylib.tgz
-rwxrwxr-x 1 hongxin hongxin 8480 Jan 18 16:01 mymath

//执行结果

[hongxin@VM-8-2-centos Test]$ ./mymath 
entrt Add func,10-20=?
Sub result: -10
entrt Add func,10+20=?
Add result: 30

形成一个可执行程序,肯定不仅仅依赖的一个库,需要依赖多个库。

//mymath --可执行程序

[hongxin@VM-8-2-centos Test]$ file mymath 
mymath: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=3947f194fd9c7903b6f4f06271aa5fe2352be969, not stripped

gcc默认是动态链接,对于特定的库,它是取决于你提供的是动态库还静态库。如果是多个库,gcc也是对库一个一个进行链接,例如:2个动态库,1个静态库,gcc处理动态库用动态链接-处理两次,用静态链接处理一次。

我们写的代码是静态库,但是会用到动态库,那么gcc会把代码拷贝到可执行程序里。所以我们查看mymath可执行程序它是动态链接--gcc默认。

安装

库搜索路径

从左到右搜索-L指定的目录。

由环境变量指定的目录 (LIBRARY_PATH)

由系统指定的目录

        /usr/lib

        /usr/local/lib

这样链接一个库,实在是太麻烦了,细节也颇多,我们可以直接将库安装到系统头文件路径下。

[hongxin@VM-8-2-centos Test]$ ls
main.c  mylib  mylib.tgz  mymath
[hongxin@VM-8-2-centos Test]$ rm mymath 


[hongxin@VM-8-2-centos Test]$ sudo cp mylib/include/* /usr/include/
[sudo] password for hongxin: *******


[hongxin@VM-8-2-centos Test]$ ls /usr/include/my_*

/usr/include/my_add.h  /usr/include/my_sub.h


[hongxin@VM-8-2-centos Test]$ sudo cp mylib/lib/*.a /lib64/

[hongxin@VM-8-2-centos Test]$ ls /lib64/libmymath.a 
/lib64/libmymath.a

这个行为就是安装,所谓安装的本质就是拷贝 

安装好后就可以直接使用,生成可执行文件了,需要注意的是需要指明是链接哪个库。

[hongxin@VM-8-2-centos Test]$ gcc main.c -lmymath
[hongxin@VM-8-2-centos Test]$ ./a.out 
entrt Add func,10-20=?
Sub result: -10
entrt Add func,10+20=?
Add result: 30

卸载 

当我们把库和头文件删除,这个过程就叫做卸载

[hongxin@VM-8-2-centos Test]$ sudo rm /usr/include/my_*
[sudo] password for hongxin: 

[hongxin@VM-8-2-centos Test]$ sudo rm /lib64/libmymath.a  

6.3生成动态库

shared: 表示生成共享库格式

fPIC:产生位置无关码(position independent code)

库名规则:libxxx.so

生成动态库

//查看普通文件

hongxin@VM-8-2-centos Func_d]$ ll
total 20
-rw-rw-r-- 1 hongxin hongxin 293 Jan 18 18:36 makefile
-rw-rw-r-- 1 hongxin hongxin 106 Jan 18 18:35 my_add.c
-rw-rw-r-- 1 hongxin hongxin  62 Jan 18 18:36 my_add.h
-rw-rw-r-- 1 hongxin hongxin 106 Jan 18 18:35 my_sub.c
-rw-rw-r-- 1 hongxin hongxin  63 Jan 18 18:36 my_sub.h

//生成目标文件
[hongxin@VM-8-2-centos Func_d]$ gcc -c -fPIC my_add.c
[hongxin@VM-8-2-centos Func_d]$ gcc -c -fPIC my_sub.c
[hongxin@VM-8-2-centos Func_d]$ ll
total 28
-rw-rw-r-- 1 hongxin hongxin  293 Jan 18 18:36 makefile
-rw-rw-r-- 1 hongxin hongxin  106 Jan 18 18:35 my_add.c
-rw-rw-r-- 1 hongxin hongxin   62 Jan 18 18:36 my_add.h
-rw-rw-r-- 1 hongxin hongxin 1584 Jan 18 18:37 my_add.o
-rw-rw-r-- 1 hongxin hongxin  106 Jan 18 18:35 my_sub.c
-rw-rw-r-- 1 hongxin hongxin   63 Jan 18 18:36 my_sub.h
-rw-rw-r-- 1 hongxin hongxin 1592 Jan 18 18:37 my_sub.o

//归档
[hongxin@VM-8-2-centos Func_d]$ gcc -shared -o libmymath.so my_add.o my_sub.o
[hongxin@VM-8-2-centos Func_d]$ ll
total 36
-rwxrwxr-x 1 hongxin hongxin 8096 Jan 18 18:38 libmymath.so
-rw-rw-r-- 1 hongxin hongxin  293 Jan 18 18:36 makefile
-rw-rw-r-- 1 hongxin hongxin  106 Jan 18 18:35 my_add.c
-rw-rw-r-- 1 hongxin hongxin   62 Jan 18 18:36 my_add.h
-rw-rw-r-- 1 hongxin hongxin 1584 Jan 18 18:37 my_add.o
-rw-rw-r-- 1 hongxin hongxin  106 Jan 18 18:35 my_sub.c
-rw-rw-r-- 1 hongxin hongxin   63 Jan 18 18:36 my_sub.h
-rw-rw-r-- 1 hongxin hongxin 1592 Jan 18 18:37 my_sub.o

//查看可执行是否为动态链接
[hongxin@VM-8-2-centos Func_d]$ file libmymath.so 
libmymath.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=1412c70d6ec360cf4b0910847b4c2bf1613a45ac, not stripped

当我们建立好动态库文件后,再把他们整理打包,压缩,然后再拷贝给他人。

//手动建立两个目录

hongxin@VM-8-2-centos Func_d]$ mkdir mylib/include

[hongxin@VM-8-2-centos Func_d]$ mkdir mylib/lib

//整理打包

[hongxin@VM-8-2-centos Func_d]$ mv libmymath.so  mylib/lib/ 

[hongxin@VM-8-2-centos Func_d]$ cp *.h mylib/include/

//查看mylib

[hongxin@VM-8-2-centos Func_d]$ tree mylib/
mylib/
|-- include
|   |-- my_add.h
|   `-- my_sub.h
`-- lib
    `-- libmymath.so

//将mylib拷贝到Test中

[hongxin@VM-8-2-centos Test]$ cp -r Func_d/mylib/ Test/

[hongxin@VM-8-2-centos Test]$ tree .
.
|-- main.c
`-- mylib
    |-- include
    |   |-- my_add.h
    |   `-- my_sub.h
    `-- lib
        `-- libmymath.so

形成可执行文件,运行

[hongxin@VM-8-2-centos Test]$ gcc -o mymath main.c  -Imylib/include -Lmylib/lib -lmymath

[hongxin@VM-8-2-centos Test]$ ls
main.c  mylib  mymath

这里也是需要特别注意:当运行可执行文件时,我们会发现找不到动态库 。这也就是动态库的特别之处,上述步骤跟静态库操作几乎一样,这里开始就发生转折。

[hongxin@VM-8-2-centos Test]$ ./mymath 
./mymath: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory
[hongxin@VM-8-2-centos Test]$ ldd mymath 
        linux-vdso.so.1 =>  (0x00007ffc669a8000)
        libmymath.so => not found
        libc.so.6 => /lib64/libc.so.6 (0x00007f59a53eb000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f59a57b9000)

造成错误原因,在gcc下已形成可执行文件,程序编译已完成,执行文件的过程是和gcc无关,程序运行是操作系统管理,所以操作系统也需要找到动态库,总结就是:库文件没有在系统路径下,操作系统无法找到。

如何让系统找到呢?这里有几种方法

(1)将库路径添加在环境变量中--短暂性

//查看我的环境变量

[hongxin@VM-8-2-centos Test]$ echo $LD_LIBRARY_PATH 
:/home/hongxin/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/hongxin/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/hongxin/.VimForCpp/vim/bundle/YCM.so/el7.x86_64

添加至环境变量

//通路径查看libmymath.so

[hongxin@VM-8-2-centos Test]$ ls /home/hongxin/2023-1-19/Test/mylib/lib/
libmymath.so

//将路径添加到环境变量中

[hongxin@VM-8-2-centos Test]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/hongxin/2023-1-19/Test/mylib/lib/

//再次查看环境变量

[hongxin@VM-8-2-centos Test]$ echo $LD_LIBRARY_PATH 
:/home/hongxin/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/hongxin/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/hongxin/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/hongxin/2023-1-19/Test/mylib/lib/

 运行可执行

//查看

[hongxin@VM-8-2-centos Test]$ ldd mymath 
        linux-vdso.so.1 =>  (0x00007ffd7c9d1000)
        libmymath.so => /home/hongxin/2023-1-19/Test/mylib/lib/libmymath.so (0x00007fec8654e000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fec86180000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fec86750000)

//运行

[hongxin@VM-8-2-centos Test]$ ./mymath 
entrt Add func,10-20=?
Sub result: -10
entrt Add func,10+20=?
Add result: 30

这里有需要注意:添加环境变量后,默认只在本次登录有效,下次登录时无效(默认清理登录前一次添加环境变量)。

//关闭后再次查看环境变量-没有上次添加的环境变量

[hongxin@VM-8-2-centos Test]$ echo $LD_LIBRARY_PATH 
:/home/hongxin/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/hongxin/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/hongxin/.VimForCpp/vim/bundle/YCM.so/el7.x86_64

//关闭后再次执行-执行错误

[hongxin@VM-8-2-centos Test]$ ./mymath 
./mymath: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory

(2)将动态库路径添加到配置文件路径中--永久性

//进入配置文件

[hongxin@VM-8-2-centos Test]$ cd /etc/ld.so.conf.d/

//查看配置文件
[hongxin@VM-8-2-centos ld.so.conf.d]$ ll
total 16
-rw-r--r-- 1 root root 26 Feb 24  2022 bind-export-x86_64.conf
-rw-r--r-- 1 root root 19 Aug  9  2019 dyninst-x86_64.conf
-r--r--r-- 1 root root 63 Jun 28  2022 kernel-3.10.0-1160.71.1.el7.x86_64.conf
-rw-r--r-- 1 root root 17 Oct  2  2020 mariadb-x86_64.conf

//再当前配置文件下新建文件

hongxin@VM-8-2-centos ld.so.conf.d]$ sudo touch Test.conf
[sudo] password for hongxin: 

//查看

[hongxin@VM-8-2-centos ld.so.conf.d]$ ll
total 16
-rw-r--r-- 1 root root 26 Feb 24  2022 bind-export-x86_64.conf
-rw-r--r-- 1 root root 19 Aug  9  2019 dyninst-x86_64.conf
-r--r--r-- 1 root root 63 Jun 28  2022 kernel-3.10.0-1160.71.1.el7.x86_64.conf
-rw-r--r-- 1 root root 17 Oct  2  2020 mariadb-x86_64.conf
-rw-r--r-- 1 root root  0 Jan 19 15:40 Test.conf

//进入Test.conf添加libmymath.so路径

[hongxin@VM-8-2-centos ld.so.conf.d]$ sudo vim Test.conf 

[hongxin@VM-8-2-centos ld.so.conf.d]$ cat Test.conf 
/home/hongxin/2023-1-19/Test/mylib/lib/

//更新动态路径缓存

[hongxin@VM-8-2-centos ld.so.conf.d]$ sudo ldconfig

//进入Test

[hongxin@VM-8-2-centos ld.so.conf.d]$ cd /home/hongxin/2023-1-19/Test/
[hongxin@VM-8-2-centos Test]$ ls
main.c  mylib  mymath

//运行
[hongxin@VM-8-2-centos Test]$ ./mymath 
entrt Add func,10-20=?
Sub result: -10
entrt Add func,10+20=?
Add result: 30

关闭后再次打开

[hongxin@VM-8-2-centos Test]$ ldd mymath 
        linux-vdso.so.1 =>  (0x00007ffee37c7000)
        libmymath.so => /home/hongxin/2023-1-19/Test/mylib/lib/libmymath.so (0x00007f7d30e4f000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f7d30a81000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f7d31051000)

(3)软链接--永久性

在当前文件路径下建立软链接 

//建立软链接 

[hongxin@VM-8-2-centos Test]$ ln -s /home/hongxin/2023-1-19/Test/mylib/lib/libmymath.so  libmymath.so 
[hongxin@VM-8-2-centos Test]$ ll
total 20
lrwxrwxrwx 1 hongxin hongxin   51 Jan 19 16:05 libmymath.so -> /home/hongxin/2023-1-19/Test/mylib/lib/libmymath.so
-rw-rw-r-- 1 hongxin hongxin  196 Jan 19 14:50 main.c
drwxrwxr-x 4 hongxin hongxin 4096 Jan 19 14:52 mylib
-rwxrwxr-x 1 hongxin hongxin 8432 Jan 19 14:58 mymath

//执行

[hongxin@VM-8-2-centos Test]$ ./mymath 
entrt Add func,10-20=?
Sub result: -10
entrt Add func,10+20=?
Add result: 30

 在系统路径下建立软链

//删除当前文件建立的软链接

[hongxin@VM-8-2-centos Test]$ unlink libmymath.so 

//在系统文件路径下建立软链接

[hongxin@VM-8-2-centos Test]$ sudo ln -s /home/hongxin/2023-1-19/Test/mylib/lib/libmymath.so  /lib64/libmymath.so
[sudo] password for hongxin

//查看

[hongxin@VM-8-2-centos Test]$ ls /lib64/libmymath.so -l
lrwxrwxrwx 1 root root 51 Jan 19 16:10 /lib64/libmymath.so -> /home/hongxin/2023-1-19/Test/mylib/lib/libmymath.so

//运行

[hongxin@VM-8-2-centos Test]$ ./mymath 
entrt Add func,10-20=?
Sub result: -10
entrt Add func,10+20=?
Add result: 30

(4)拷贝.so文件到系统共享库路径下, 一般指/usr/lib

略~

6.3使用外部库--ncurses库

安装

sudo yum install ncurses-devel

测试代码

#include <curses.h>
 
int main()
{
        char c;
        initscr();//ncurse界面的初始化函数
        printw("please input a char:\n");
        c = getch();
        printw("\nc = %c\n",c);
        getch();//等待用户的输入,如果没有这句话,程序就退出了,看不到运行的结果,也就是无法看到上面那句话
        endwin();//程序退出,恢复shell终端的显示,如果没有这句话,shell终端字乱码,坏掉
 
        return 0;
}

编译--编译时需要用-lcurses进行链库。

[hongxin@VM-8-2-centos Curses]$ gcc test.c -lncurses

变成成功后会头图形化界面

[hongxin@VM-8-2-centos Curses]$ ./a.out  

92020188cec44e7ca7788e0f374a46bf.png

6.4动态库的加载

静态库不需要加载

这个过程,在磁盘中main.c和lib.c-库,会先在磁盘中形成一段代码,然后对这段代码进行编译,编译的本质就是预处理,编译-查找错误,形成二进制代码,然后进行汇编形成二级制指令。在编译阶段的时候就已经形成了虚拟地址空间。在虚拟地址空间中,这段代码也就被存入代码区,这个是根据不同区的特性所决定的。当执行这段代码的时候,操作系统就会直接在代码区进行访问。

54414205025341aebe22c8ced5ca8be3.png

动态库加载

动态库加载的过程,在磁盘中有一个my.exe(可执行)和lib.so(动态库),在形成可执行之前,编译阶段时,我们用到了fPIC :产生位置无关码。

gcc -c -fPIC my_add.c

在这个阶段,动态库会将指定的函数地址,写入到可执行中。这个地址可以理解成my_add.c(地址)+偏移地址。

形成好可执行文件之后,磁盘将可执行文件拷贝到内存中,内存通过页表映射到虚拟地址空间的代码区中,当OS执行程序时,扫描到my_add.c是需要调用动态库的时候,程序会停下来,OS会再通过函数的地址,然后页表映射去内存到磁盘中找动态库中,找到后拷贝到内存,又通过页表映射到共享区中。OS再去共享区调用该方法,然后向下执行程序。

bdc51c0099df4af6864c8a858e4811f7.png

0f5406668bab4065b15e5aa5ba9dc432.png [ 作者 ]   includeevey

[ 日期 ]   2023.1.19
[ 声明 ]   到这里就该说再见了,若本文有错误和不准确之处,恳望读者批评指正!
                    有则改之无则加勉!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/includeevey/article/details/128508638

智能推荐

分布式光纤传感器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告_预计2026年中国分布式传感器市场规模有多大-程序员宅基地

文章浏览阅读3.2k次。本文研究全球与中国市场分布式光纤传感器的发展现状及未来发展趋势,分别从生产和消费的角度分析分布式光纤传感器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。主要生产商包括:FISO TechnologiesBrugg KabelSensor HighwayOmnisensAFL GlobalQinetiQ GroupLockheed MartinOSENSA Innovati_预计2026年中国分布式传感器市场规模有多大

07_08 常用组合逻辑电路结构——为IC设计的延时估计铺垫_基4布斯算法代码-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏12次。常用组合逻辑电路结构——为IC设计的延时估计铺垫学习目的:估计模块间的delay,确保写的代码的timing 综合能给到多少HZ,以满足需求!_基4布斯算法代码

OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏5次。OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版

关于美国计算机奥赛USACO,你想知道的都在这_usaco可以多次提交吗-程序员宅基地

文章浏览阅读2.2k次。USACO自1992年举办,到目前为止已经举办了27届,目的是为了帮助美国信息学国家队选拔IOI的队员,目前逐渐发展为全球热门的线上赛事,成为美国大学申请条件下,含金量相当高的官方竞赛。USACO的比赛成绩可以助力计算机专业留学,越来越多的学生进入了康奈尔,麻省理工,普林斯顿,哈佛和耶鲁等大学,这些同学的共同点是他们都参加了美国计算机科学竞赛(USACO),并且取得过非常好的成绩。适合参赛人群USACO适合国内在读学生有意向申请美国大学的或者想锻炼自己编程能力的同学,高三学生也可以参加12月的第_usaco可以多次提交吗

MySQL存储过程和自定义函数_mysql自定义函数和存储过程-程序员宅基地

文章浏览阅读394次。1.1 存储程序1.2 创建存储过程1.3 创建自定义函数1.3.1 示例1.4 自定义函数和存储过程的区别1.5 变量的使用1.6 定义条件和处理程序1.6.1 定义条件1.6.1.1 示例1.6.2 定义处理程序1.6.2.1 示例1.7 光标的使用1.7.1 声明光标1.7.2 打开光标1.7.3 使用光标1.7.4 关闭光标1.8 流程控制的使用1.8.1 IF语句1.8.2 CASE语句1.8.3 LOOP语句1.8.4 LEAVE语句1.8.5 ITERATE语句1.8.6 REPEAT语句。_mysql自定义函数和存储过程

半导体基础知识与PN结_本征半导体电流为0-程序员宅基地

文章浏览阅读188次。半导体二极管——集成电路最小组成单元。_本征半导体电流为0

随便推点

【Unity3d Shader】水面和岩浆效果_unity 岩浆shader-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏18次。游戏水面特效实现方式太多。咱们这边介绍的是一最简单的UV动画(无顶点位移),整个mesh由4个顶点构成。实现了水面效果(左图),不动代码稍微修改下参数和贴图可以实现岩浆效果(右图)。有要思路是1,uv按时间去做正弦波移动2,在1的基础上加个凹凸图混合uv3,在1、2的基础上加个水流方向4,加上对雾效的支持,如没必要请自行删除雾效代码(把包含fog的几行代码删除)S..._unity 岩浆shader

广义线性模型——Logistic回归模型(1)_广义线性回归模型-程序员宅基地

文章浏览阅读5k次。广义线性模型是线性模型的扩展,它通过连接函数建立响应变量的数学期望值与线性组合的预测变量之间的关系。广义线性模型拟合的形式为:其中g(μY)是条件均值的函数(称为连接函数)。另外,你可放松Y为正态分布的假设,改为Y 服从指数分布族中的一种分布即可。设定好连接函数和概率分布后,便可以通过最大似然估计的多次迭代推导出各参数值。在大部分情况下,线性模型就可以通过一系列连续型或类别型预测变量来预测正态分布的响应变量的工作。但是,有时候我们要进行非正态因变量的分析,例如:(1)类别型.._广义线性回归模型

HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计_垃圾分类网页设计目标怎么写-程序员宅基地

文章浏览阅读69次。环境保护、 保护地球、 校园环保、垃圾分类、绿色家园、等网站的设计与制作。 总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮播图、音频 视频 Flash的应用、ul li、下拉导航栏、鼠标划过效果等知识点,网页的风格主题也很全面:如爱好、风景、校园、美食、动漫、游戏、咖啡、音乐、家乡、电影、名人、商城以及个人主页等主题,学生、新手可参考下方页面的布局和设计和HTML源码(有用点赞△) 一套A+的网_垃圾分类网页设计目标怎么写

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁_.net dll 全局目录-程序员宅基地

文章浏览阅读614次,点赞7次,收藏11次。之前找到一个修改 exe 中 DLL地址 的方法, 不太好使,虽然能正确启动, 但无法改变 exe 的工作目录,这就影响了.Net 中很多获取 exe 执行目录来拼接的地址 ( 相对路径 ),比如 wwwroot 和 代码中相对目录还有一些复制到目录的普通文件 等等,它们的地址都会指向原来 exe 的目录, 而不是自定义的 “lib” 目录,根本原因就是没有修改 exe 的工作目录这次来搞一个启动程序,把 .net 的所有东西都放在一个文件夹,在文件夹同级的目录制作一个 exe._.net dll 全局目录

BRIEF特征点描述算法_breif description calculation 特征点-程序员宅基地

文章浏览阅读1.5k次。本文为转载,原博客地址:http://blog.csdn.net/hujingshuang/article/details/46910259简介 BRIEF是2010年的一篇名为《BRIEF:Binary Robust Independent Elementary Features》的文章中提出,BRIEF是对已检测到的特征点进行描述,它是一种二进制编码的描述子,摈弃了利用区域灰度..._breif description calculation 特征点

房屋租赁管理系统的设计和实现,SpringBoot计算机毕业设计论文_基于spring boot的房屋租赁系统论文-程序员宅基地

文章浏览阅读4.1k次,点赞21次,收藏79次。本文是《基于SpringBoot的房屋租赁管理系统》的配套原创说明文档,可以给应届毕业生提供格式撰写参考,也可以给开发类似系统的朋友们提供功能业务设计思路。_基于spring boot的房屋租赁系统论文