Linux系统--基础IO


文章目录

  • 文件的概念
  • C语言文件IO相关操作
  • 系统调用接口
  • 文件描述符
  • 文件周边概念
  • 设计用户层缓冲区
  • minishell中支持重定向
  • 文件系统
  • 软硬链接
  • 动静态库

一、文件的概念

1.在系统角度上来说

文件=内容+属性

如果在磁盘上建立一个为0KB的文件,磁盘上也会存储文件的属性。(因为文件的属性也是数据).

文件的操作

  • 1.一种是对于文件内容做操作。
  • 2.另外一种是对于文件的属性,比如说更改文件的相关权限等。

注意:

  • 对于文件内容做操作也可能会影响文件属性改变,比如说文件的大小发生了变化。
  • 由于文件在磁盘(硬件)中存放,我们访问文件,先写代码->编译->链接->生成可执行文件->运行。

注意:

  • 访问文件的本质是进程在访问,进程访问文件是需要通过接口(语言层面的接口,C语言、C++)访问的。
  • 如果想要向硬件写入,那么只能通过操作系统。如果普通用户也想向硬件写入,那么就必须让操作系统提供相应的系统调用接口(文件类的系统调用接口)。由于文件类的系统调用接口比较复杂,所以一写语言对于这些系统调用接口进行了封装,这就导致了不同语言有不同的语言级别的文件访问接口(都不一样),但是都封装的是系统接口,底层原理相同。

为什么要学习OS层面的文件接口?

  • 1.由于选择的操作系统只有一个,这样的接口,在Linux上只有一套,其他OS也是一样的。
  • 2.这样的代码具有跨平台性。使用C++的平台都可以使用C++的文件接口,使用Python的平台都可以使用Python的文件接口。
  • 如果语言不提供给文件的系统接口的封装,所有访问文件的操作都必须直接使用操作系统的接口。(Windows的系统接口和Linux的系统接口种类,参数等都是不一样的)。

注意:

        使用语言的用户,也需要访问文件,但是一旦使用系统接口编写文件代码,这份代码只能在该平台中使用,无法在其他平台上运行,不具有跨平台性。而C语言和C++语言将所有平台中的代码都实现出来并封装好。然后采用条件编译的方式,在编译的时候实现动态编译。

2.stdin & stdout & stderr

C/C++默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

 由于显示器也是硬件,那么printf向显示器打印,也是一种写入,和磁盘写入到文件没有本质区别!

3.Linux下一切皆文件

在Linux下,硬件设备可以被看成文件,可以进行scanf、fgets、printf、cin、cout。在C/C++程序在编译的时候,将代码加载到你的程序中,这就是默认情况下会打开stdin、stdout、stderr。

  • stdin标准输入->默认为键盘
  • stdout标准输出->默认为显示器
  • stderr标准错误->默认为显示器

对于文件来说,读和写是文件操作的核心。

对于显示器来说,printf/cout本质上也是一种写入(write),对于键盘来说,scanf/cin本质上也是一种读入(read).将输入内容给显示器一份也给程序一份。站在内存角度,程序必须加载到内存,键盘将我们输入的数据交给内存,系统将内存中的数据刷新到显示器或者写入到硬盘中,也就是对应着input和output(也就是I/O的动作)。如果一个普通文件,我们使用fopen/fread去读取它,他就被读取到我们程序(进程)的内部(内存),再使用fwrite将数据写入到文件中,从普通程序读取到进程内部(内存)就是input,从内存中写入文件中就是output。

文件的定义:站在系统角度上来说,能够被input读取,能够被output写出的设备就是文件。

  • 狭义上的文件:普通的磁盘文件(.txt .doc文件等等)
  • 广义上的文件:显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有的外设都可以称之为文件。(这些设备都具有上面可以被读或者被写的特点)。

二、C语言文件IO相关操作

1.文件类型

  • 文本文件:把数据的终端形式的二进制数据输出到磁盘上存放,也就是说存放的是数据的终端形式。
  • 二进制文件:把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放,也就是说存放的是数据的原形式。

2.相对路径和绝对路径的理解

  • 绝对路径:是从目录树的树根"/"目录开始往下直至到达文件所经过的所有节点目录。下级目录接在上级目录后面用“/"隔开。
  • 相对路径:相对路径是指目标目录相对于当前目录的位置。
  • 注意:绝对路径都是从“/"开始的,所以第一个字符一定是“/"。

3.C语言中文件操作函数

文件的打开

fopen():打开文件

FILE *fopen( const char *filename, const char *mode );

 文件的关闭

fclose():关闭文件

int fclose( FILE *stream );

文件的读写

fgetc():读取一个字符

fputc():写入一个字符

fgets():读取一个字符串

fputs():写入一个字符串

fprintf():写入格式化数据

fscanf():格式化读取数据

fread():读取数据

fwrite():写入数据

int fgetc( FILE *stream );
int fputc( int ch, FILE *stream );
char *fgets( char *str, int count, FILE *stream );
int fputs( const char *str, FILE *stream );
int fprintf( FILE *stream, const char *format, ... );
int fscanf( FILE *stream, const char *format, ... );
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
int fwrite( const void *buffer, size_t size, size_t count, FILE *stream );

 文件状态检查

feof():文件是否结束

ferror():文件读/写是否出错

clearerr():清除文件错误标志

ftell():文件指针的当前位置

int feof ( FILE * stream );
int ferror ( FILE * stream );
void clearerr ( FILE * stream );
long int ftell ( FILE * stream );

 文件指针定位

rewind():把文件指针移到开始处

fseek():重定位文件指针                     

void rewind ( FILE * stream );
int fseek ( FILE * stream, long int offset, int origin );

 注意:r+具有读写属性,从文件头开始写,保留原文件中没有被覆盖的内容,w+也具有读写属性,如果原文件存在,会被清空从头开始写。

perror打印错误信息

#include <stdio.h>
#include <stdlib.h>

int main()
{
    //当前目录中不存在这个文件
    FILE* fp=fopen("log.txt","r");
    //如果打开失败,就以文本形式返回
    if(fp==NULL)
    {
        perror("fopen");
        exit(1);
    }
    return 0;
}

在Linux下,如果当前是以写的方式的话,如果写入的文件不存在,会在当前路径下直接创建。

4.通过进程来创建一个文件

#include <stdio.h>
#include <stdlib.h>

int main()
{
    FILE* fp=fopen("log.txt","w");
    if(fp==NULL)
    {
        perror("fopen");
        exit(1);
    }
    fclose(fp);
    while(1) sleep(1);
    return 0;
}

 从上图我们可以发现ext指向的路径就是我们当前进程的工作路径。

cwd:进程的内部属性,就是我们这个进程所指向的工作目录。将cwd所指向的路径,和我们上面传入的文件拼接起来,形成完整的路径名称。当一个进程运行起来,每个进程都会记录当前所处的工作路径,当你打开一个文件的时候,创建的文件就是我们当前路径下,也就是我们进程所处的路径下。由于我们进程内部会直接用这个进程的cwd也就是当前的工作路径,然后将文件名test拼接起来形成我们的exe找到可执行文件。

注意:进程具有确定性,一般我们程序将程序部署到系统中,路径一般不发生改变,比如说你安装了一个程序到D盘中,那么路径就在D盘下。

5.文件相关接口演示:

相关代码:

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

int main()
{
    FILE* fp=fopen("log.txt","w");
    //如果打开失败了,就将打开失败的原因以文本的形式返回
    if(fp==NULL)
    {
        perror("fopen");
        return 1;
    }

    //进行文件的相关操作
    const char* s1="hello fwrite\n";

    //二进制的方式写入
    fwrite(s1,strlen(s1),1,fp);
    
    const char* s2="hello fprintf\n";

    //往特定的文件流中,写入特定的字符串
    fprintf(fp,"%s",s2);

    const char* s3="hello fputs\n";
    fputs(s3,fp);

    //关闭文件
    fclose(fp);
    return 0;
}

 

7.文件不同模式

1.w模式

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

int main()
{
    FILE * fp=fopen("log.txt","w");

    if(fp==NULL)
    {
        perror("fopen");
        return 1;
    }

    const char* s1="hello world\n";

    fwrite(s1,strlen(s1),1,fp);

    fclose(fp);
    return 0;
}

 由于是在w模式下,先将文件打开的时候,将文件中的内容进行清空处理,然后才写入内容!

这里我们通过echo指令对log.txt进行写入内容,首先将文件中的全部内容被清空。这与先清空再写入的原理相同!

2.a模式追加

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

int main()
{
    FILE *fp=fopen("log.txt","a");

    if(fp==NULL)
    {
        perror("fopen");
        return 1;
    }

    const char*s1="hello world\n";
    fwrite(s1,strlen(s1),1,fp);

    fclose(fp);
    return 0;
}

上图产生的原因是因为打开了文件,而不是将文件的内容进行清空的操作而是将文件后面的内容进行追加处理。

3.fgets进行按行读取操作

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

int main()
{
    FILE* fp=fopen("log.txt","r");

    if(fp==NULL)
    {
        perror("fopen");
        return 1;
    }

    char line[64];

    while(fgets(line,sizeof(line),fp)!=NULL)
    {
        //将读到行的内容显示出来,同时也可以通过printf的方式打印出来
        printf("%s\n",line);

        //也可以通过标准输出的方式打印出来
        fprintf(stdout,"%s",line);
    }

    fclose(fp);
    return 0;
}


4.利用cat命令来读取文件

cat命令用来打印文件中的内容。

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

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        printf("argc error!\n");
        return 1;
    }

    //argv[1]就是我们传入的参数
    FILE* fp=fopen(argv[1],"r");

    //如果打开失败的话,则将失败的原因以文本的方式进行返回
    if(fp==NULL)
    {
        perror("fopen");
        return 2;
    }

    //按行读取
    char line[64];
    while(fgets(line,sizeof(line),fp)!=NULL)
    {
        //将读取的行显示出来
        printf("%s",line);

        //往显示器上写,stdout就是标准输出
        fprintf(stdout,"%s",line);
    }

    //关闭文件
    fclose(fp);
    return 0;
}

这里我们通过给argv[1]中传入文件名,我们可以发现我们可以打印其文件中的内容。

三、系统调用接口

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。

1.open

  • #include <sys/types.h>
  • #include <sys/stat.h>
  • #include <fcntl.h>
  • int open(const char *pathname, int flags);
  • 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
注意:open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限(mode),否则,使用两个参数的open。

如何给函数传递标志位?

在宏定义中利用大写字母和_来表示一个宏,上面的flags只能传递一种状态,但是如果在C语言中传入大量的选项呢,这时候我们可以利用标记位来表示该状态是否的意思,例如一个int是32字节,那么一个标记位就是一个比特位,那么就可以表示32个状态了。这种表示方式就是一种数据结构--位图

测试代码:

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

#define ONE 0x1
#define TWO 0x2
#define THREE 0x3

void show(int flags)
{
    if(flags&ONE) printf("hello one\n");
    if(flags&TWO) printf("hello two\n");
    if(flags&THREE) printf("hello three\n");
}

int main()
{
    show(ONE);
    printf("--------------------------------\n");
    show(TWO);
    printf("--------------------------------\n");
    show(ONE|TWO);
    printf("--------------------------------\n");
    show(ONE|TWO|THREE);
    return 0;
}

1.O_CREAT

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

测试代码:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd=open("log.txt",O_CREAT);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    printf("open success fd:%d\n",fd);
    return 0;
}

2.O_WRONLY

只写打开
测试代码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd=open("log.txt",O_WRONLY);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    printf("open success fd:%d\n",fd);
    return 0;
}

这里我们发现,使用O_WRONLY不会自动创建文件,我们需要添加O_CREAT选项来解决。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd=open("log.txt",O_WRONLY|O_CREAT);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    printf("open success fd:%d\n",fd);
    return 0;
}

传入标记位

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    printf("open success fd:%d\n",fd);
    return 0;
}

这里我们通过给文件设置相关的权限为0666(即-rw-rw-rw-),而实际上不是,是由于系统中有创建时默认的权限umask权限(默认是0002)

如果想要得到我们想要设置的权限,那么我们只需要将此时umask设置为0即可解决问题。

2.close

关闭文件

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    //设置文件掩码为0
    umask(0);
    
    int fd=open("log.txt",O_WRONLY|O_CREAT);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    printf("open success fd:%d\n",fd);

    //关闭文件
    close(fd);
    return 0;
}

3.write

向文件中写入

测试代码:

#include <stdio.h>
#include <unistd.h>
#include <string.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,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    const char* s="hello write\n";
    write(fd,s,strlen(s));
    printf("open success fd:%d\n",fd);
    close(fd);
    return 0;
}

这里我们只将上述代码写入字符串s改变为“HeLLoWRITE”,那么会写入到文件中是什么内容呢?

这里我们发现此时在log.txt中是进行覆盖式的写入,而不是先清空再进行写入的方式!

1.O_TRUNC

先清空文件再向文件中写入

#include <stdio.h>
#include <unistd.h>
#include <string.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|O_TRUNC,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    const char* s="hello\n";
    write(fd,s,strlen(s));
    printf("open success fd:%d\n",fd);
    close(fd);
    return 0;
}

这里我们发现此时文件首先进行了清空处理然后重新写入了hello。

2.O_APPEND

在文件后面进行追加方式。

#include <stdio.h>
#include <unistd.h>
#include <string.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|O_APPEND,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    const char* s="hello write\n";
    write(fd,s,strlen(s));
    printf("open success fd:%d\n",fd);
    close(fd);
    return 0;
}

4.read

对文件进行读取处理

 1.O_RDONLY

以只读的方式打开

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    umask(0);
    int fd=open("log.txt",O_RDONLY,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    char buffer[64];
    memset(buffer,'\0',sizeof(buffer));
    read(fd,buffer,sizeof(buffer));
    printf("%s\n",buffer);
    printf("open success fd:%d\n",fd);
    close(fd);
    return 0;
}

四、文件描述符

为什么打开一个文件,默认的文件描述符为3呢?

#include <stdio.h>
#include <unistd.h>
#include <string.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|O_APPEND,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    printf("open success fd:%d\n",fd);
    close(fd);
    return 0;
}

这里我们发现此时打开的文件该文件的文件描述符为3。那么此时我们打开多个文件呢?

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    umask(0);
    int fd1=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    printf("open success fd1:%d\n",fd1);
    int fd2=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    printf("open success fd2:%d\n",fd2);
    int fd3 =open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    printf("open success fd3:%d\n",fd3);
    int fd4=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    printf("open success fd4:%d\n",fd4);

    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);
    return 0;
}

这里我们发现打开多个文件此时文件描述符是从3开始递增的,为什么不是从0开始呢?

这是因为在C语言中,系统会默认打开三个默认的文件,分别是stdin,stdout,stderr,它们都是FILE*类型的,每一个文件都对应了一个文件描述符。0-标准输入,1-标准输出,2-标准错误。

测试代码:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    //向显示屏中打印
    fprintf(stdout,"hello fprintf\n");

    const char*s="hello write\n";
    write(1,s,strlen(s));//向显示屏中写入

    //从标准输入中读取
    int a=10;
    fscanf(stdout,"%d",&a);
    printf("%d\n",a);

    //从0中读取
    char buff[16];
    size_t ss=read(0,buff,sizeof(buff));

    if(ss>0)
    {
        buff[ss]='\0';
        printf("%s\n",buff);
    }
    return 0;
}

1.FILE的认识

这里的FILE是由C语言的标准库提供的!,是一个结构体,所以其内部封装了很多属性,由于是C语言的库函数,所以内部一定要进行系统调用,在系统角度只认识fd,由于是FILE结构体,所以其内部一定封装了fd!

验证是否存在fd代码:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    printf("stdin:%d\n",stdin->_fileno);
    printf("stdout:%d\n",stdout->_fileno);
    printf("stderr:%d\n",stderr->_fileno);
    return 0;
}

上述代码说明了:C语言将我们系统中的0,1,2文件封装进了stdin,stdout,stderr中,而stdin,stdout,stderr都是结构体指针,其内部的_fileno属性就是我们对应的0,1,2!

2.fd的认识

首先进程需要访问文件,那么就必须先将文件打开。一个进程可以打开多个文件,文件打开的目的就是被访问,文件想要被访问,前提是加载到内存当中才能够被访问!如果多个进程打开自己的文件,那么系统可能会存在大量被打开的文件!那么OS将这些被打开的文件进行先描述再组织的管理方式,首先OS内部需要管理每一个被打开的文件,首先需要构建出内核的结构体对象struct file,这是一种内核数据结构,里面包含了文件的所有内容和属性(包括文件的打开的时间,修改时间,所属组,所有者等等)

struct file结构体定义在/linux/include/linux/fs.h(Linux 2.6.11内核)中,其原型是:
struct file {
        /*
         * fu_list becomes invalid after file_free is called and queued via
         * fu_rcuhead for RCU freeing
         */
        union {
                struct list_head        fu_list;
                struct rcu_head         fu_rcuhead;
        } f_u;
        struct path             f_path;
#define f_dentry        f_path.dentry
#define f_vfsmnt        f_path.mnt
        const struct file_operations    *f_op;
        atomic_t                f_count;
        unsigned int            f_flags;
       mode_t                  f_mode;
        loff_t                  f_pos;
        struct fown_struct      f_owner;
        unsigned int            f_uid, f_gid;
        struct file_ra_state    f_ra;

        unsigned long           f_version;
#ifdef CONFIG_SECURITY
        void                    *f_security;
#endif
        /* needed for tty driver, and maybe others */
        void                    *private_data;

#ifdef CONFIG_EPOLL
        /* Used by fs/eventpoll.c to link all the hooks to this file */
        struct list_head        f_ep_links;
        spinlock_t              f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
        struct address_space    *f_mapping;

};

创建出struct file对象来充当一个被打开的文件,如果被打开的文件很多,那么此时需要OS用双链表将这些对象组织起来!所以一个进程的PCB只要找到上面那个打开的文件的链表头部就可以找到全部的文件!

进程与文件的对应关系是多对一的,在内核当中为了维护进程和文件的关系,会有一个数组(struct_file *array[32])(指针数组,数组中的类型都是struct file*),第一个数组元素指向第一个文件,依次类推,系统就可以通过数组进行哈希索引来找到对应的文件。

综上所述:fd在内核中,本质上来说是数组下标!

在内存当中的文件,OS通过双链表来添加我们新打开的文件,并且将其地址放入struct_file*array数组中,然后OS可以通过fd可以查询到对应文件的存储地址,进而可以读取到打开的文件!

(文件对象)struct file里面包含了文件中的所有内容,(文件描述符表)struct_file*array也可以成为文件映射表。本质上来说文件描述符表就是数组下标!

3.文件的分类

内存文件:被进程打开的文件(open属于系统调用),这里的文件称为内存文件。

磁盘文件:没有被进程打开,那么此时该文件保留在磁盘中,磁盘上保存的文件(内容和属性).

一旦操作系统打开了大量文件的情况,操作系统此时就需要将这些文件管理起来,先构建出struct file对象,里面包含了文件的所有内容和属性(包括文件的打开时间,修改时间,所属组,所有者等信息)。文件存在于磁盘当中,本身就有属性,从磁盘中被加载到内存中,相关属性被填充到结构体中。

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

五、文件周边概念

1.文件描述符fd的理解

Linux进程默认的情况下会有3个缺省打开的文件描述符,分别是0,1,2,对应的标准输入,标准输出,标准错误,对应的物理设备是:键盘,显示器,显示器。

这里我们测试一下将0号文件描述符关闭:

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

int main()
{
    //这里我们将0号文件描述符关闭
    close(0);
    
    int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd<0)
    {
        perror("fd");
        return 1;
    }

    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    close(fd);
    return 0;
}

这里我们发现此时打开的log.txt此时分配的文件描述符是0,同理我们可以关闭2号文件符,我们也可以得到该分配的文件描述符为2。

这里我们需要阐述一下fd的分配规则:这里fd的分配是按照当前文件描述符中最小的,还没有被占用的文件描述符。由于OS会帮我们维护文件映射表,所以OS会默认分配最小的,没有被占用的文件描述符。由于上述代码中0号文件描述符被关闭,所以新分配的文件描述符就是0。

如果我们关闭1号文件描述符会发生什么?

测试代码:

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

int main()
{
    //这里我们将1号文件描述符关闭
    close(1);
    
    int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd<0)
    {
        perror("fd");
        return 1;
    }

    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    close(fd);
    return 0;
}

这里我们发现此时关闭1号文件描述符文件不打印,这时我们注释close(fd)观察到

由于printf默认是往stdout中打印,由于关闭了1号文件描述符(显示器),所以分配文件描述符是log.txt分配给1号文件描述符,所以打印到log.txt中。

这里我们也可以不注释close(fd),直接利用fflush将此时内容刷新到log.txt中。

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

int main()
{
    //这里我们将1号文件描述符关闭
    close(1);
    
    int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd<0)
    {
        perror("fd");
        return 1;
    }

    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);

    fflush(stdout);
    close(fd);
    return 0;
}

 测试代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    //这里我们将1号文件描述符关闭
    close(1);
    
    int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd<0)
    {
        perror("fd");
        return 1;
    }

    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    printf("fd:%d\n",fd);
    fprintf(stdout,"hello fprintf\n");

    const char*s="hello write\n";
    fwrite(s,strlen(s),1,stdout);

    fflush(stdout);
    close(fd);
    return 0;
}

 这里我们发现将往显示器中打印的都打印到log.txt中。这里我们注释close(1)可以观察到:

此时我们发现所有的内容都打印到显示器上,也就是说本来1是显示器,如果将1关闭,按照上述文件描述符分配规则,我们log.txt分配的文件描述符就是1,然后本应该打印到显示器上的内容都被打印到我们的文件中。这个我们称之为输出重定向!

2.输出重定向原理

当我们默认打开文件时,系统会默认打开三个文件,这三个文件就是标准输入,标准输出,标准错误,分别对应着键盘,显示器,显示器,这三个都是struct file对象(变量)。由于Linux下一切皆文件,所以这些硬件可以被OS描述成struct file对象。站在OS的角度,一个进程默认打开三个文件,本质上就是将三个文件的文件描述符填入相应的文件地址,比如0号文件描述符填入标准输入的文件地址。

这里我们打开了myfile文件,,但是我们将1号文件描述符给关闭了,也就是将1号的标准输出指向的地址设置为null,也就是底层没有了,但是我们上层stdout中保存的数字还是1。当我们再次open的时候,打开的就是1,也就是我们的myfile文件,由于我们按照文件描述符的分配规则,所以这个1中的地址就被我们的OS重心指向了myfile。

重定向的本质:是在OS内部,更改fd对应的内容指向!(这里需要注意与C语言没有关系,是由OS帮我们完成)。

3.输入重定向原理

测试代码1:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    char buffer[64];
    fgets(buffer,sizeof(buffer),stdin);
    printf("%s\n",buffer);
    return 0;
}

 测试代码2:

从标准输入中读入!

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{

    int fd=open("log.txt",O_RDONLY);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    printf("fd:%d\n",fd);
    char buffer[64];
    fgets(buffer,sizeof(buffer),stdin);
    printf("%s\n",buffer);
    return 0;
}

 这里我们通过向键盘中输入"hello world",可以发现可以读取到!

我们关闭0号文件描述符查看!

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    close(0);
    int fd=open("log.txt",O_RDONLY);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    printf("fd:%d\n",fd);
    char buffer[64];
    fgets(buffer,sizeof(buffer),stdin);
    printf("%s\n",buffer);
    return 0;
}

这里我们可以发现将0号文件描述符关闭,此时log.txt分配的文件描述符为0号,所以本来从键盘中读取的内容现在从log.txt文件中进行读取,这就是输入重定向!

4.追加重定向原理

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    close(1);
    int fd=open("log.txt",O_WRONLY|O_APPEND|O_CREAT);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    fprintf(stdout,"you can see me,success\n");
    return 0;
}

5.重定向系统调用

1.使用 dup2 系统调用

头文件和函数原型

1

2

#include <unistd.h>

int dup2(int oldfd, int newfd);

参数简介

  • oldfd:原来的文件描述符
  • newfd:复制成的新的文件描述符

函数返回值

  • 成功:将oldfd复制给newfd, 两个文件描述符指向同一个文件
  • 失败:返回-1, 设置errno值

当调用dup2(int oldfd,int newfd)之后,若newfd原来已经打开了一个文件,则先关闭这个文件,然后newfd和oldfd指向了相同的文件;若newfd原来没有打开一个文件,则newfd直接指向和oldfd指向相同的文件。

6.重新理解重定向

测试代码1:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main(int argc,char*argv[])
{
    if(argc!=2) return 2;

    int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    fprintf(stdout,"%s\n",argv[1]);//stdout->1->显示器文件
    return 0;
}

 这里我们可以发现我们将我们传入的第二个参数打印到显示器中!

7.输出重定向

测试代码2:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main(int argc,char*argv[])
{
    if(argc!=2) return 2;

    int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    //将本来显示到显示器上的内容显示到文件中
    dup2(fd,1);
    fprintf(stdout,"%s\n",argv[1]);//stdout->1->显示器文件
    return 0;
}

这里我们发现此时我们传入的参数打印到log.txt这个文件中!

8.追加重定向

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main(int argc,char*argv[])
{
    if(argc!=2) return 2;

    int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    //将本来显示到显示器上的内容显示到文件中
    dup2(fd,1);
    fprintf(stdout,"%s\n",argv[1]);//stdout->1->显示器文件
    return 0;
}

这里我们可以看到在原先的基础上进行了追加操作。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main(int argc,char*argv[])
{
    if(argc!=2) return 2;

    int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    //将本来显示到显示器上的内容显示到文件中
    dup2(fd,1);
    fprintf(stdout,"%s\n",argv[1]);//stdout->1->显示器文件
    close(fd);
    return 0;
}
        由于我们的数据已经写入到缓冲区中,如果先进行close,那么文件描述符已经关闭了, 此时进程退出再进行刷新,那么文件描述符都没有了怎么进行刷新操作呢,所以必须先刷新再关闭!

.如何理解一切皆文件?(理性)

     在Linux下,一切皆文件是Linux的设计哲学,体现在操作系统的软件设计层面的!

Linux是用C语言写的,如何用C语言实现面向对象,甚至是运行时多态?   

C++中的类其实就是C语言中的结构体,也就是类:其中有

  •     1.成员属性
  •     2.成员方法

    C语言中用struct将一堆属性和方法放在一起,也就是我们现在面向对象的起点。struct中可以包含成员属性,但是在纯C语言中不可以包含成员函数。在struct内部是不存在所谓的成员方法的!所以定义一个struct file结构体

    struct file{
            int size;
            mode_t mode;
            int user;
            inr group;
            ……
        //函数指针!!!
            int (*readp)(int fd,void*buffer,int len);
            int (*readp)(int fd,void*buffer,int len);
            ……
        }

    所以我们C语言虽然不支持成员方法,但是我们可以保存函数指针,然后调用对应的方法!

    然后我们的C语言就可以封装一个类!

    如何重新理解文件?

    磁盘,显示器,网卡,键盘,显卡……底层不同的硬件,一定对应的是不同的操作方法!(访问显卡和显示器的方法显然不一样!)但是上面的设备都是外设!(冯诺依曼操作系统)

    所以每一个设备的核心访问函数,都可以是read和write,为什么?

    因为read代表的是i,外设代表的是o。也就是说所有的设备都可以有自己的read和write,但是代码的实现一定是不一样的!(函数的接口的名称可以是一样的,但是函数体的实现逻辑是不一样的)磁盘,显示器等等都有各自的read和write的方法,每一个硬件都必须要提供各自的读写方法。

    所以操作系统设计了一个struct file,当我们打开一个所谓的磁盘文件的时候,我们的内核中创建一个结构体,让其读和写的指针指向磁盘的读和写的方法。如果打开显示器,就再创建一个struct file,让其读和写的指针指向显示器的读和写的方法!

     然后为了先描述再组织,我们就将所有的struct file链接起来,变成一个双向链表,要使用哪一个设备的读和写就调用哪一个struct file。这里没有任何硬件的差别了,看待所有的文件的方式,都统一成为了struct file,所以在Linux下,就变成了一切皆文件。这里的双向链表将struct file连接起来的就是由我们的操作系统维护的。而下面的不同设备的struct file中的读和写的具体的方法,就是我们的驱动开发。我们将Linux的这一套文件系统就称为VFS(virtual file system也就是虚拟文件系统) 。

9.缓冲区

概念:缓冲区就是一段内存空间。

1.缓冲区存在的意义

举例说明:

    假设你在现在在云南大学,你的好朋友在北京邮电上学。你们之间的关系很好,所以你想要将一些书给你的朋友。你可以乘火车,飞机,或者其他的交通工具,将这些书给你的朋友。你直接将这些书给你的朋友,这种方式就是写透模式(WT)!这种方式有一个问题,就是成本非常高,也就是非常慢。

    所以你们学校中有一个机构,就是快递发送点,也就是顺丰快递,你的朋友那边也有顺丰快递的发送点。你现在只需要将你的快递交给顺丰快递,你的发送任务就完成了!因为你相信顺丰,因为顺丰会非常快并且安全地交给目的地。用这样的方式,我们就称作写回模式(WB)

    这种方式最大的优势就是快速,成本低!然后你到了顺丰,发现顺丰是将你的快递先暂存一下,然后将七八个人的全部的快递一起配送过去。反正就是积累足够多的数据,然后再发送出去。这个顺丰就是一个典型的缓冲区,只要顺丰不跑路,你的快递是不会丢的!(计算机是不会抱着你的64字节跑了的!)缓冲区存在的意义就是加速,提高整机的效率。

上述例子中你的朋友就是磁盘,你的要寄的书就是数据,你把你的数据交给缓冲区,写了一大批数据,然后将全部的数据一次性刷新到磁盘上。效率会有所提高,但是主要是为了提高用户的响应速度。

    磁盘是一个物理设备,是一个机械设备。

    在内存级别的操作往往是微秒级别的,然而磁盘的操作级别往往是毫秒级别的,相比之下,就非常慢了(1微秒=1/1000毫秒)(之间的差别是10^3,差了快整整1000倍,也就是你的朋友一个月20000块,你的月薪20块的水平!相差非常大!)必须要有缓冲区的存在!

缓冲区存在的意义:就是为了提高整机的效率(数据并不会立即写入到目标设备中,数据先写入缓冲区中,然后按照一定的时间进行刷新)。

2.缓冲区的刷新策略

一般情况下:

  • 立即刷新
  • 行刷新(行缓冲)
    • 以'\n'为代表,将包含在缓冲区之前的数据全部都刷新出去\n之前的内容全部刷新出去,\n之后的数据并不会刷新出去。
  • 满刷新(全缓冲)
    • 这里需要将缓冲区写满,才能将数据刷新出去。

其他的特殊情况:

  • 用户强制刷新(fflush):不满足刷新条件,还是会进行强制刷新。
  • 进程退出:进程一般退出时,一般都是会将缓冲区的内容刷新到OS内部!

3.缓冲区的认识

什么时候用行刷新,什么时候用满刷新?

一般而言:行缓冲的设备文件--显示器,全缓冲的设备文件--磁盘文件

为什么这两个设备之间会有差别呢?

所有的设备,永远都倾向于全缓冲!

因为缓冲区满了,也就意味着才刷新,需要更少次的IO操作,更少次的外设的访问,也就是说:提高了效率!那刷10次也是1000个数据,刷1次也是1000个数据的话,为什么就提高了效率呢?

和外部设备IO的时候,数据量的大小不是主要矛盾,你和外设预备IO的过程是最耗费时间的!比方说你去问老师问题,你一次性将问题全部问完,老师一次性全部解答,肯定比你一次次问问题,然后老师看到你的消息,再一次次地回答你的问题更加快。

所以准备IO的时候是最耗费时间的!(多次沟通的效率是非常低的)其他刷新策略是,结合具体情况做的妥协!

显示器:因为显示器是直接给用户看的,一方面要照顾效率,一方面要照顾用户体验!(如果你的显示器,很少的数据不刷新,一刷新就刷新一大批,那么用户得体验是非常不好的!)显示器一行一行刷新才符合人的阅读习惯!

极端情况下:你是可以自定义规则的!

磁盘为什么是全刷新的?

磁盘上写东西的时候,用户并不会马上看到,这是我们更多考虑的是效率,而不是用户体验,所以我们往磁盘中写的时候,就是满刷新。

所以我们得到的结论是:显示器是行刷新,其他的设备比方说磁盘是满刷新的。

4.缓冲区的位置

验证代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    //以下是C语言提供相关接口
    fprintf(stdout,"hello fprintf\n");
    printf("hello printf\n");
    const char*s="hello fputs\n";
    fputs(s,stdout);

    //OS提供相关接口
    const char*ss="hello write\n";
    write(1,ss,strlen(ss));

    //注意我们是在最后调用fork函数,上面的函数已经被执行完成了
    fork();//创建子进程

    return 0;
}

由于我们是在最后进行fork,所以正常打印出四条语句。这时候我们利用重定向到log.txt中查看

清空文件内容:>log.txt

这里我们发现 打印变成了7次,这是为什么呢?

我们将fork注释再观察:

此时我们观察到正常打印出四条,那么此时打印出7次应该与fork有关!

为什么只有write打印了一次?其他的语句都打印了两次?(c语言的文件操作函数都打印了两次?)

 同样的一个程序,向显示器打印,输出4行文本向普通文件(磁盘上),打印的时候,变成了7行

 其中①我们的C语言的IO接口是打印了2次的

        ②系统接口,只打印了一次,和向显示器打印一样!

    (上面的函数已经执行完了,并不代表进程的数据已经刷新了!)

 为什么fork函数会产生这样的问题呢?

    由于fork的原因发生了写时拷贝!而fork并不会影响系统接口只会影响C语言的这批接口。如果是OS统一提供的缓冲区,那么我们上面的代码,表现应该是一样的!那就只能是由C标准库提供的!C标准库会为我们的进程开辟一段空间,这里我们拿fputs举例,你把你的数据写入了C标准库提供的缓冲区当中。写到这里之后呢,由C标准库按照一定的时间刷新到操作系统内核当中(调用write接口)。所以进程把数据写入缓冲区当中,就可以返回了,置于什么时候刷新,就不用这个进程操心了!fputs所做的工作与其说是写入,不如说是将数据拷贝到缓冲区当中。那如果你调用的是write的话,数据就是直接写入系统内核的,没有写入缓冲区中!

  • 1.这些函数都是向显示器打印的,所采用的的刷新策略都是行刷新的。那么我打印的时候,我们所打的字符串是带有\n的,那么fork的时候,一定是函数执行完了,并且是数据已经被刷新了的!所以你接下来执行的fork就没有意义了,因为上面的数据已经刷新到显示器上了!
  • 2.如果你对应的程序进行了重定向,也就是比方说本来要向显示器上打印,但是现在变成了要向磁盘文件打印,那么隐形的刷新策略变成了全缓冲!(上面我们的操作就是将原本的要打印到显示器上的操作重定向到文件当中)那么我们上面在字符串中写的\n就没有意义了!是不会刷新的!而是放入缓冲区中的。fork的时候,一定是函数执行完了,然后fork,但是数据还没有刷新!这些数据在当前进程对应的C语言标准库对应的缓冲区中!所以我们的父子进程就往我们的缓冲区中刷进了两份相同的内容,然后全部都重定向进入了我们的文件中

    那这部分数据是不是父进程的数据呢?

    这个缓冲区的空间是C语言标准库给你的,你将数据拷贝到这个缓冲区当中,这个缓冲区就是你这个进程的,专门给你用的,也就是专门给父进程用的。这段数据还在父进程的上下文当中。那么当fork函数执行之后,父子进程分别执行自己的代码,当然在我们上面的代码中,就是父子进程分别各自退出 。

    在我们上面所说的特殊的情况的刷新策略中,我们的进程在退出时会发生强制刷新。你将这段数据写入到显示器文件中了。那么就会发生写时拷贝,子进程中也会有一份相同的数据。所以,在强制刷新了之后父子进程都会刷新这份数据,C标准库提供给我们的都是用户级缓冲区!

注意:除了用户级缓冲区之外,系统中还存在一个内核级缓冲区!

如果父进程在fork前是一个char * buffer ="hello world\n",那么父进程要输出的时候,会将父进程的数据拷贝到子进程,子进程中也会有一份"hello world\n",下面我们在fork之前先fflush强制刷新一下。

验证代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    //以下是C语言提供相关接口
    fprintf(stdout,"hello fprintf\n");
    printf("hello printf\n");
    const char*s="hello fputs\n";
    fputs(s,stdout);

    //OS提供相关接口
    const char*buffer="hello world\n";
    write(1,buffer,strlen(buffer));

    fflush(stdout);
    //注意我们是在最后调用fork函数,上面的函数已经被执行完成了
    fork();//创建子进程

    return 0;
}

这里我们通过fflush强制刷新到缓冲区当中,所以在fork之后并不会发生写时拷贝,因为数据已经刷新到缓冲区当中!

C语言中打开文件FILE* fopen(const char* path,const char* mode),struct FILE是一个结构体,其内部封装了fd还有相应的语言层面缓冲区结构:

//在/usr/include/libio.h
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; //封装的文件描述符
#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
};

注意:一旦拷贝了数据,该数据属于内核,不属于用户进程了。

六、设计用户层缓冲区

测试代码1:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>

#define NUM 1024

struct MyFILE_
{
    //文件描述符
    int fd;
    char buffer[NUM];
    int end;//当前缓冲区的结尾
};

typedef struct MyFILE_ MyFILE;

MyFILE* fopen_(const char* pathname,const char*mode)
{
    assert(pathname);
    assert(mode);

    MyFILE* fp=NULL;
    if(strcmp(mode,"r")==0)
    {}
    else if(strcmp(mode,"r+")==0)
    {}
    else if(strcmp(mode,"w")==0)
    {
        int fd=open(pathname,O_WRONLY|O_TRUNC|O_CREAT,0666);
        if(fd>=0)
        {
            fp=(MyFILE*)malloc(sizeof(MyFILE));
            memset(fp,0,sizeof(fp));
            fp->fd=fd;
        }
    }
    else if(strcmp(mode,"w+")==0)
    {}
    else if(strcmp(mode,"r+")==0)
    {}
    else if(strcmp(mode,"a")==0)
    {}
    else if(strcmp(mode,"a+")==0)
    {}

    else {}

    return fp;
}

//将message中的数据写入到缓冲区中
void fputs_(const char*message,MyFILE* fp)
{
    assert(message);
    assert(fp);

    //向缓冲区当中写入
    strcpy(fp->buffer+(fp->end),message);
    fp->end+=strlen(message);

    printf("%s\n",fp->buffer);

    if(fp->fd==0)//标准输入
    {}
    else if(fp->fd==1)//标准输出
    {
        if(fp->buffer[fp->end-1]=='\n')
        {
            fprintf(stderr,"fflush:%s\n",fp->buffer);
            write(fp->fd,fp->buffer,fp->end);
            fp->end=0;
        }
    }
    else if(fp->fd==2)//标准错误
    {}
    else 
    {}
}

void fflush_(MyFILE* fp)
{
    //将fp中的内容刷新出来
    assert(fp);

    if(fp->end!=0)
    {
        //这里是将数据写入到内核中,暂且认为是进行了刷新操作
        write(fp->fd,fp->buffer,fp->end);
        //将数据写入到磁盘中
        syncfs(fp->fd);
        fp->end=0;
    }
}

void fclose_(MyFILE* fp)
{
    assert(fp);
    
    //刷新
    fflush_(fp);
    
    //进行系统调用
    close(fp->fd);
    free(fp);
}

int main()
{
    close(1);
    MyFILE * fp=fopen_("./log.txt","w");
    if(fp==NULL)
    {
        perror("open file error");
        return 1;
    }

    fputs_("one:hello world\n",fp);
    sleep(1);
    fputs_("two:hello world\n",fp);
    sleep(1);
    fputs_("three:hello world",fp);
    sleep(1);
    fputs_("four:hello world\n",fp);
    sleep(1);
    fputs_("five:hello world\n",fp);
    sleep(1);

    fclose_(fp);
    return 0;
}

    由于我们这里只在one,two,four,five的时候对我们的缓冲区进行了刷新,在three的时候没有刷新,所以我们的three依然留在缓冲区中,所以我们的four再刷新的时候,会将three和four一同刷新出来!这里打印出来是因为调用了printf,所以将结果打印到显示器上!然后我们观察到我们的log.txt当中同样也有我们刚刚打印在屏幕上的内容,因为我们将1号文件关闭了,所以我们新打开的文件就是1号文件,也就是stdout,所以我们的自己写的文件都会打印到这个新打开的log.txt当中。

测试代码2:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>

#define NUM 1024

struct MyFILE_
{
    //文件描述符
    int fd;
    char buffer[NUM];
    int end;//当前缓冲区的结尾
};

typedef struct MyFILE_ MyFILE;

MyFILE* fopen_(const char* pathname,const char*mode)
{
    assert(pathname);
    assert(mode);

    MyFILE* fp=NULL;
    if(strcmp(mode,"r")==0)
    {}
    else if(strcmp(mode,"r+")==0)
    {}
    else if(strcmp(mode,"w")==0)
    {
        int fd=open(pathname,O_WRONLY|O_TRUNC|O_CREAT,0666);
        if(fd>=0)
        {
            fp=(MyFILE*)malloc(sizeof(MyFILE));
            memset(fp,0,sizeof(fp));
            fp->fd=fd;
        }
    }
    else if(strcmp(mode,"w+")==0)
    {}
    else if(strcmp(mode,"r+")==0)
    {}
    else if(strcmp(mode,"a")==0)
    {}
    else if(strcmp(mode,"a+")==0)
    {}

    else {}

    return fp;
}

//将message中的数据写入到缓冲区中
void fputs_(const char*message,MyFILE* fp)
{
    assert(message);
    assert(fp);

    //向缓冲区当中写入
    strcpy(fp->buffer+(fp->end),message);
    fp->end+=strlen(message);

    printf("%s\n",fp->buffer);

    if(fp->fd==0)//标准输入
    {}
    else if(fp->fd==1)//标准输出
    {
        if(fp->buffer[fp->end-1]=='\n')
        {
            fprintf(stderr,"fflush:%s\n",fp->buffer);
            write(fp->fd,fp->buffer,fp->end);
            fp->end=0;
        }
    }
    else if(fp->fd==2)//标准错误
    {}
    else 
    {}
}

void fflush_(MyFILE* fp)
{
    //将fp中的内容刷新出来
    assert(fp);

    if(fp->end!=0)
    {
        //这里是将数据写入到内核中,暂且认为是进行了刷新操作
        write(fp->fd,fp->buffer,fp->end);
        //将数据写入到磁盘中
        syncfs(fp->fd);
        fp->end=0;
    }
}

void fclose_(MyFILE* fp)
{
    assert(fp);
    
    //刷新
    fflush_(fp);
    
    //进行系统调用
    close(fp->fd);
    free(fp);
}

int main()
{
    //close(1);
    MyFILE * fp=fopen_("./log.txt","w");
    if(fp==NULL)
    {
        perror("open file error");
        return 1;
    }

    fputs_("one:hello world\n",fp);
    fork();
    fclose_(fp);
    return 0;
}

   

    对应的程序进行了重定向,也就是本来要向显示器上打印,但是现在要向磁盘文件打印,那么隐形的刷新策略变成了全缓冲!(上面我们的操作就是将原本的要打印到显示器上的操作重定向到文件当中)那么我们上面在字符串中写的\n就没有意义了!是不会刷新的!而是放入缓冲区中的。fork的时候,一定是函数执行完了,然后fork,但是数据还没有刷新!这些数据在当前进程对应的C语言标准库对应的缓冲区中!所以我们的父子进程就往我们的缓冲区中刷进了两份相同的内容,然后全部都重定向进入了我们的文件中。

这里我们将main函数的代码改为:


int main()
{
    //close(1);
    MyFILE * fp=fopen_("./log.txt","w");
    if(fp==NULL)
    {
        perror("open file error");
        return 1;
    }

    fputs_("one:hello world\n",fp);
    fork();
    fclose_(fp);
    return 0;
}

        此时我们发现在log.txt文件中有两条相同的数据,由于对应的程序进行了重定向,本来是往显示器中进行打印,现在重定向到向磁盘文件打印,那么隐形的缓存策略变成了全缓冲!(上面我们的操作本来就是将原本要打印到显示器上面的操作重定向到文件中),那么我们上面在字符串中写的‘\n’就没有意义,是不会进行刷新的,而是放入了缓冲区中!fork的时候一定函数执行完成了,然后进行fork操作,但是数据还没有进行刷新,这些数据存在于当前进程对应的C语言标准库对应的缓区中,由于创建了子进程,发生了写时拷贝导致了父子进程就往我们的缓冲区中加载了两份相同的内容,然后全部重定向到我们的文件中。

七、minishell中支持重定向

#include <string.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
#include <sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
//定义一个大小为1024个字节的缓冲区
#define NUM 1024
//第一一个用于打散之后的字符串的个数的大小
#define SIZE 32
//宏定义分隔符
//由于我们下面的分隔符系统中在定义的时候是一个char*的,所以我们必须传入一个字符串,所以我们下面用的SEP必须要是双引号包裹起来的空格
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串,也就是将每一个打散之后的子串的起始地址保存在这个g_argv中
char *g_argv[SIZE];
 
//写一个环境变量的buffer,用来测试
//cmd_line每一次都会被清空,我们的环境变量为了防止被清空我们这里定义一个buffer用来测试
char g_myval[64];
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define NONE_REDIR 0
int redir_status=NONE_REDIR;
char *CheckRedir(char *start)
{
    assert(start);
    char *end=start+strlen(start)-1;//让其找到最后一个有效字符(不包含\0)
 
//从输入的完整的字符串中从后往前寻找有没有重定向标识符,如果有就进入下面的判断
    while(end>=start)
    {
        if(*end=='>')
        {
            //如果是连着的两个>符号(>>)
            //ls -a -l>>myfile.txt
            if(*(end-1)=='>')
            {
                //追加重定向
                redir_status=APPEND_REDIR;
                *(end-1)='\0';
                end++;//找到文件名的其实字符串
                break;
            }
            //ls -a -l>myfile.txt
            redir_status=OUTPUT_REDIR;
            *end='\0';
            end++;
            break;
        }
        else if(*end=='<')
        {
            //cat<myfile.txt输入重定向
            redir_status=INPUT_REDIR;
            *end='\0';
            end++;
            break;
        }
        else
        {
            //不是大于也不是小于
            end--;
        }
    }
    //如果循环走完的时候,这里条件还是满足的,就说明是提前跳出的
    if(end>=start)
    {
        return end;//要打开的文件的字符串地址
    }
    else
    {
        //没有遇到>或者<符号
        return NULL;
    }
}
 
int main()
{
    //8.创建一个全局变量指针
    extern char**environ;
    //0.命令行解释器:通过让子进程执行命令,父进程等待&&解析命令
    while(1)
    {
        //1.打印出提示信息
        //这里我们减少学习成本,直接粘贴和打印出来
        printf("[root@我的主机 myshell]#");
        //将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来
        fflush(stdout);
        //sizeof可以不带圆括号,直接求大小
        //初始化我们的缓冲区,将我们的缓冲区也就是cmd_line中的全部内容(也就是大小为sizeof(cmd_line)的空间)全部初始化为'\0'
        memset(cmd_line,'\0',sizeof cmd_line);
        //2.获取用户的键盘输入
        //第一个参数是将我们读取到的字符放入哦我们上面定义的缓冲区中,第二个参数是定义放入的字符的大小,第三个参数是指从输入流中读取
        //"ls -a -l >log.txt"
        //"ls -a -l >>log.txt"
        //"ls -a -l <log.txt"
        if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
        {
            //如果读取到的是空,也就是出错了,就直接进行下一次循环,重新初始化
            continue;
        }
        //将我们输入的内容打印出来
        //但是我们输入的内容实际上最后带有一个\n
        //ls -a -l -i\n
        //我们需要将这个最后的\n给去除掉
        //strlen求字符串长度的时候不包括后面的\0
        //由于我们的下标是从0开始的,所以我们需要将cmd_line的长度-1的位置,也就是\n的位置置为\0
        cmd_line[strlen(cmd_line)-1]='\0';
 
 
        //2.1:分析是否有重定向,"ls-a -l>logtxt"->"ls -a -l\0log.txt"
        //"ls -a -l -i\n\0"
        char *sep=CheckRedir(cmd_line);
 
 
//        printf("echo:%s\n",cmd_line);
        //3.打散字符串
        //我们将字符串中的空格部分置为\0并且将每个子字符串的起始地址用指针指向,就得到了我们的打散的子字符串
        //第一个参数你要解析的子串,第二个参数是分隔符
        //export myval=105 左侧的是命令,右侧的是环境变量
        g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
        int index=1;
        //6.给我们的shell设置颜色
        if(strcmp(g_argv[0],"ls")==0)
        {
            g_argv[index++]=(char*)"--color=auto";
        }
 
 
        //7.让我们的shell支持别名
        //这里我们仅仅是支持ll,也就是ls的别名
        if(strcmp(g_argv[0],"ll")==0)
        {
            g_argv[0]=(char*)"ls";
            g_argv[index++]=(char*)"-l";
            g_argv[index++]=(char*)"--color=auto";
        }
        //进行循环读取
//        while(g_argv[index-1])
//        {
//            g_argv[index]= strtok(NULL,SEP);//第二次如果还要解析原始字符串,传入NULL,也就是第一个参数,第二个参数分隔符还是SEP
//            index++;
//        }
 
 
        //我们测试一下打散的操作成不成功
//        for(index=0;g_argv[index];index++)
//        {
//            printf("g_argv[%d]:%s\n",index,g_argv[index]);
//        }
 
 
        //先解析,再赋值,最后while解析g_argv,解析到最后,全部都解析完成的时候g_argv的值为null,while循环条件不满足,循环退出
        while(g_argv[index++]= strtok(NULL,SEP));
 
        //上面的代码将我们全部的命令都解析完,然后我们下面再进行分析,不然我们下面的argv[1]就可能并没有被解析,然后就并不会进入下面添加环境变量的操作
        //8.让我们的shell支持修改环境变量
        //并且我们的环境变量不空我们才能将我们的环境变量导入
        if(strcmp(g_argv[0],"export")==0&&g_argv[1]!=NULL)
        {
            //防止下一次导入的时候,将环境变量覆盖了
            strcpy(g_myval,g_argv[1]);
            //将新的环境变量导入到我们的shell当中
            //然后它的环境变量在地址空间中是在它的栈的上面的命令行地址空间中,就被添加进去了
            int ret=putenv(g_myval);
            //看看环境变量有没有成功导入
            if(ret==0)
            {
                printf("%s export success\n",g_argv[1]);
            }
            //当前我们的环境变量已经添加到系统中了
            //将获取到的环境变量打印出来
//            for(int i=0;environ[i];i++)
//            {
//                printf("%d:%s\n",i,environ[i]);
//            }
            continue;
        }
 
        //4.TODO内置命令
        //内置命令:让父进程shell自己执行的命令,我们叫做内置命令,内建命令
        //内建命令本质上就是shell内部中的一个函数
        if(strcmp(g_argv[0],"cd")==0)//不让我们的子进程去执行cd命令,而是交给我们的父进程去完成
        {
            if(g_argv[1]!=NULL)
            {
                //将要切换的目标路径传进来
                chdir(g_argv[1]);//cd ..
 
            }
            //进入下一次循环
            continue;
        }
 
        //5.fork()
        pid_t id=fork();
        if(id==0)//child
        {
            if(sep!=NULL)
            {
                //重定向工作
                //说明曾经有重定向
                //程序替换只会影响代码和数据,不会对进程所打开的文件和数据有任何影响,也就是对这里的文件描述符也没有任何影响
                //在switc-case中不能定义变量,所以我们就直接将这个fd定义在swtich-case的外面
                int fd=-1;
                switch(redir_status) {
                    //sep指向文件名的起始地址
                    case INPUT_REDIR:
                        fd=open(sep,O_RDONLY);
                        dup2(fd,0);
                        break;
                    case OUTPUT_REDIR:
                        //写入,清空,创建
                        fd=open(sep,O_WRONLY|O_TRUNC|O_CREAT,0666);
                        //重定向到标准输出
                        dup2(fd,1);
                        break;
                    case APPEND_REDIR:
                        fd=open(sep,O_WRONLY|O_APPEND|O_CREAT,0666);
                        dup2(fd,1);
                        break;
                    default:
                        printf("bug?\n");
                        break;
 
                }
            }
            //环境变量的测试代码
//            printf("下面功能让子进程执行的\n");
//            printf("child,MYVAL:%s\n",getenv("MYVAL"));
//            printf("child,PATH:%s\n",getenv("PATH"));
            //ls -a -l -i,第一个参数g_argv[0]保存的就是我们的命令,后面的全部都是我们的参数
            execvp(g_argv[0],g_argv);
//            execvpe(g_argv[0],g_argv,environ);
            exit(1);
        }
        //father
        int status=0;
        //阻塞式等待
        pid_t ret= waitpid(id,& status,0);
        if(ret>0)
        {
            printf("exit code:%d\n", WEXITSTATUS(status));
        }
    }
}

    上述代码中的这块部分就是将我们的不同的重定向标识符下定义一,分别是输入重定向,输出重定向和追加重定向,然后NONE_REDIR就是没有重定向,标记为0,我们利用redir_status来记录我们当前的重定向的状态是属于哪一类的。

CheckRedir是用来检测我们的输入的重定向中是

    1.> 输出重定向

    2.>> 追加重定向

    3.< 输入重定向

    然后对其分别分类,然后将我们的redir_status分别置为不同的状态。

    当然,如果没有重定向的话,就会被最终划分到return  NULL的那里,返回NULL

    我们这里的的CheckRedir从我们传入的字符串的最后有效字符的位置从后向前寻找,直到寻找到重定向字符串为止。

char *CheckRedir(char *start)
{
    assert(start);
    char *end=start+strlen(start)-1;//让其找到最后一个有效字符(不包含\0)
 
//从输入的完整的字符串中从后往前寻找有没有重定向标识符,如果有就进入下面的判断
    while(end>=start)
    {
        if(*end=='>')
        {
            //如果是连着的两个>符号(>>)
            //ls -a -l>>myfile.txt
            if(*(end-1)=='>')
            {
                //追加重定向
                redir_status=APPEND_REDIR;
                *(end-1)='\0';
                end++;//找到文件名的其实字符串
                break;
            }
            //ls -a -l>myfile.txt
            redir_status=OUTPUT_REDIR;
            *end='\0';
            end++;
            break;
        }
        else if(*end=='<')
        {
            //cat<myfile.txt输入重定向
            redir_status=INPUT_REDIR;
            *end='\0';
            end++;
            break;
        }
        else
        {
            //不是大于也不是小于
            end--;
        }
    }
    //如果循环走完的时候,这里条件还是满足的,就说明是提前跳出的
    if(end>=start)
    {
        return end;//要打开的文件的字符串地址
    }
    else
    {
        //没有遇到>或者<符号
        return NULL;
    }
}
 

     然后我们在这里定义了检测是否有重定向的代码,其中cmd_line是我们一开始定义的完整的输入的字符串。

     然后我们后面的这段代码就是将重定向工作判断一下,然后执行对应不同的方法!也就是我们的redir_status如果是不同的数字,那么我们的打开就是不同的方式,然后我们下面的dup2也就分别替换掉不同的IO文件,0为标准输入,1位标准输出,2位标准错误。

    下面我们就重新将我们的minishell运行起来看看输入下面的测试代码的结果,来看看我们的重定向操作是不是已经可以运行了。

注意:输入重定向是先清空文件内容,然后再进行写入操作!

1.close关闭fd之后文件内部没有数据

测试代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    close(0);

    int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    const char*msg="hello world\n";
    write(fd,msg,strlen(msg));
    close(fd);
    return 0;
}

        上述代码我们可以发现msg中的内容被重定向到我们的log.tx中去了。如果此时我们在进程退出前将fd关闭呢?

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    close(1);

    int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }
    
    printf("hello world\n");
    close(fd);
    return 0;
}

        上述现象我们可以发现,数据没有被打印出来是因为数据会暂时存在stdout的缓冲区中,由于刷新策略由行刷新变成了满刷新,所以我们的数据在缓冲区中的时候,对应的fd先关闭了,数据便无法刷新!

此时将代码改为:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    close(1);

    int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }

    printf("hello world\n");
    fflush(stdout);
    close(fd);
    return 0;
}

        这时候我们可以发现此时就可以重定向到我们的log.txt中去。

2.stdout和stderr的区别

测试代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    printf("hello printf 1\n");//stdout
    fprintf(stdout,"hello fprintf 1\n");

    perror("hello perror 2");//sterr

    const char* s1="hello write 1\n";//stdin
    write(1,s1,strlen(s1));

    const char* s2="hello write 2\n";
    write(2,s2,strlen(s2));

    return 0;
}

        这里我们可以发现打印的内容都会打印到相应的文件描述符中!

       我们重定向到log.txt中去,发现重定向到2号文件描述符时,此时的内容依旧被打印到显示器中去,但是我们打印到1号文件的打印全部都被打印到log.txt中。

重定向改的是几号文件描述符的内容呢?为什么只有1号文件描述符的内容被写入到了log.txt当中?

1和2对应的都是显示器文件,但是他们两个是不同的显示器文件!同一个显示器文件,被打开了两次!

一般而言,如果程序运行有可能有问题的话,建议使用stderr,或者C++中的cerr来打印

如果是常规的文本内容的话,建议使用sdtout打印。

将标准输出和错误分开打印,显示到2号文件描述符中的内容打印到err.txt文件中,然后如果是正常的标注输出文件中的内容打印到ok.txt文件中。

./test >ok.txt 2 >err.txt

        我们想要将我们的程序运行的代码运行的结果并不保存在显示器上,而是保存在文件日志中,然后查看日志就知道有没有错误。将1和2号文件中所有的内容都打印到log.txt中,这里的&1就是将1中的内容给2拷贝一份,然后全部都重定向到log.txt当中。

./test >log.txt 2>&1

    先将log.txt中的内容重定向到cat当中,然后再将cat中的内容拷贝到back.txt当中,也就是完成了一个拷贝操作。

cat <log.txt >back.txt

1.perror

perror是向2号文件描述符指向的文件进行打印操作

        上面中的代码出现了,多打印出了Success,这是由于errno的关系,那么我们通过设置errno来观察

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

//myfile helloworld将helloworld打印到文件里
int main()
{
    //向stdout中打印-->1
    printf("hello printf 1\n");
    fprintf(stdout,"hello fprintf 1\n");
 
    //向stdout里面打印-->2
    perror("hello perror 2");//stderr
 
    //向stdin里面打印
    const char *s1="hello write 1\n";
    write(1,s1,strlen(s1));
 
    const char *s2="hello write 2\n";
    write(2,s2,strlen(s2));

    return 0;
}

     这里我们可以发现,通过设置errno为3后,此时打开文件失败!

2.strerror

        这里由于log.txt文件不存在,并且我们是通过只读的方式打开的,所以不会创建出文件!

 
#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
//myfile helloworld将helloworld打印到文件里
 
void myperror(const char*msg)
{
    //第一个%s所打印的是open,然后strerror(errno)是具体的错误信息
    fprintf(stderr,"%s:%s\n",msg,strerror(errno));
}
int main()
{
    //这个log.txt当前并不存在
    int fd=open("log.txt",O_RDONLY);
    if(fd<0)
    {
        myperror("open");
        return 1;
    }
    return 0;
}

八、文件系统

没有被打开的文件存储在磁盘中,称为磁盘级文件!

1.磁盘文件

        内存是掉电易失存储介质,而磁盘是永久性存储介质。除了磁盘之外永久性存储介质还有SSD(固态硬盘)、U盘、flash卡、光盘、磁带等,而SSD这种一般没有什么机械式设备,所以比较贵,但是这个比磁盘快,一般在企业中由于磁盘容量大、价格低、性价比高,所以一般企业都是用磁盘存储的。磁盘作为一个外设(在冯诺依曼体系结构中承担着输入输出设备的角色),由于是计算机中唯一的一个机械设备,导致磁盘速度很慢,那么操作系统通过某些手段来提高磁盘的速度(如预加载等)。

        磁盘文件是一种计算机里的文件。存储信息不受断电的影响,存取速度相对于内存慢得多了。一般硬盘的容量比较大,也就是说它能记录的信息比较多,而且一般都装在机箱里面。软盘的容量就相对比较小了,一般放在机箱外面。计算机上有个特殊的地方叫做软盘驱动器,要用软盘的时候就把它放进这个地方,不用的时候可以很方便地拿出来带走。打个比方说,计算机像一个工厂,硬盘就是仓库,可以放很多东西,但是仓库是不能随便搬走的。硬盘在机箱里面负责储存数据,而软盘用来搬运数据,硬盘的容量大,软盘的容量小,这就是它们的区别,另外硬盘的存取速度比软盘快得多。硬盘是外部存储器,速度再快也比不上内存,否则计算机就用不着内存了。记住硬盘存取速度快于软盘,而内存存取信息的速度又远远的快于硬盘。

2.磁盘结构

磁盘中是存在磁盘盘片、磁头、伺服系统、音圈马达等

这里的伺服系统指的是那些硬件电路,当磁盘运行起来的时候 ,我们的马达就会带动磁盘盘片转动,然后我们的磁头就会在磁盘的不同位置取读取数据。盘面中存储数据。由于计算机只能认识二进制序列,所以盘面中存储的就是二进制,在内存中01是用电信号来区分,而磁盘由于具有磁性,那么可以利用磁铁来区分,由无数的磁铁小颗粒组成的盘面就是我们的磁盘。向磁盘中写入,本质上就是改变磁盘上的磁铁小颗粒的N和S(或者是正负性)。

1.盘片

        一个磁盘(如一个 1T 的机械硬盘)由多个盘片(如下图中的 0 号盘片)叠加而成。盘片的表面涂有磁性物质,这些磁性物质用来记录二进制数据。因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面。

2.磁道、扇区

每个盘片被划分为一个个磁道,每个磁道又划分为一个个扇区。如下图:

其中,最内侧磁道上的扇区面积最小,因此数据密度最大。

3.柱面

每个盘面对应一个磁头。所有的磁头都是连在同一个磁臂上的,因此所有磁头只能“共进退”。

所有盘面中相对位置相同的磁道组成柱面。如下图,

        磁头在来回读写磁盘的时候,就是向磁盘中特定位置发生放电行为,从而改变磁盘小颗粒的正负形,从而改变我们的磁盘中存储的数据。

注意:磁头和盘面是不接触的d,如果磁盘和盘面接触可能会造成数据的丢失。

4.磁盘的物理地址

由上,可用(柱面号,盘面号,扇区号)来定位任意一个“磁盘块”。

在“文件的物理结构”小节中,我们经常提到文件数据存放在外存中的几号块(逻辑地址),这个块号就可以转换成(柱面号,盘面号,扇区号)的地址形式。

可根据该地址读取一个“块”,操作如下:

① 根据“柱面号”移动磁臂,让磁头指向指定柱面;

② 激活指定盘面对应的磁头;

③ 磁盘旋转的过程中,指定的扇区会从磁头下面划过,这样就完成了对指定扇区的读/写。

3.磁盘的存储结构

    写入了文件,但我们的磁盘怎么知道我的文件在这个位置呢?

        由于磁盘是由一系列同心圆构成的,数据只能写在这些同心圆上面,这些同心圆就被称为磁道。

        但是磁道一圈的容量太大了,所以我们以圆心为中点,向外等角度地将我们磁盘的盘片划分成多个扇区。

        所有盘面半径相同的磁道的位置组成的,我们将其称为柱面。

         磁盘存储的基本单位是扇区。

    在物理上,如何将输入写入到磁盘中呢(本质上就是如何找到一个扇区)?

  •     1.我们首先知道我们的数据在哪一个盘面上。对应的问题就是哪一个磁头H
  •     2.在哪一个磁道(柱面)上,在哪一个同心圆上C
  •     3.在哪一个磁道上S

    这种寻址方式就是(CHS)寻址方式,也就是只需要CHS三个要素,就能够找到任意一个扇区了,那么所有的扇区我们都能找到。

     注意:

  •      一般扇区的大小是512字节,当然也有更好地4kb一个扇区的。
  •     这里的512kb是硬件上的要求。

    1.磁盘的抽象结构(虚拟,逻辑结构)

    通过磁带的例子来抽象出磁盘的结构,我们将磁带中的两个轴上的磁带带子从一个轴转到另外一个轴上的时候,我们就称为倒带。磁带的带面上就存在着小磁铁,也就是用于存储二进制数据的。那如果我们将磁带全部拉出来,让磁带的带子从一个圆形的结构变成一个线性结构。我们可以将这个卷在一起的磁带想象成我们的磁盘。在物理上是环形结构,但是我们可以在将其想象成是一个线性结构。(想象将每一条环形的磁道拉直)然后我将你拉开来之后的线性结构,然后按照扇区的大小,按512字节划分成一个小块,我们逻辑上可以将我们的磁盘抽象成一个大数组!

sector disk[1024*1024*n];

    所以我们将来要访问一个扇区,只要知道数组的下标是多少,就能够找到我们的扇区。我们将这种逻辑上线性抽象的数组,叫做(LBA)logical block address,逻辑块编址

    只要我们将LBA地址转换成CHS地址(我们上面的磁头,扇区,磁道的地址),我们就能够访问对应的物理地址了。(有点像我们的虚拟地址转换成物理地址的过程)

    所以操作系统访问我们磁盘当中的某一个扇区的时候,只要使用LBA地址就能找到我们对应的文件了。将数据存储到磁盘,在逻辑上就转化成了将数据存储到该数组!找到磁盘特定扇区的位置, 转化成了找到数组的特定位置,所以操作系统对磁盘的管理就变成了对该数组的管理。

    由于磁盘空间很大,操作系统非常难以管理,直接对整个磁盘进行管理,成本非常高,所以我们可以将整个磁盘进行拆分,就变成了一个个更小的区域。这就是我们分区的过程!操作系统对磁盘的管理转换成了对小分区的管理!

   假如说这里有100GB的小分区空间,这个空间依旧很大,我们继续对这100GB的空间进行拆分,我们下面图中也就是将一个个的block group也就是块组,又分成了一个个小小的块。现在也就是只要将一个个块组管理好了,全部的组都能管理好了。

            所有的块组之前有一个Boot Block,它可能会存在多份,以进行备份,以防其中一份损坏。(这个存储的是我们的启动装载的相关数据)

2.Block group

   Super Block:是我们Linux系统的核心数据块,代表的就是文件系统本身,含有文件系统的属性信息,比方说整个分区有多少个块组,有多少个块组被使用了,又有多少个没有被使用。(文件系统的属性信息)(如果我们的磁盘的磁头将盘面刮花了,然后我们的Super Block坏掉了怎么办?其实在后面的块组中其实会有很多分拷贝的Super Block。也就是我们的电脑蓝屏之后,重启一下,然后就能够从这些拷贝中恢复我们原本的数据)

    Data blocks:虽然磁盘的基本单位是扇区(512字节),但是操作系统(文件系统)和磁盘进行IO的基本单数为:4KB(8*512字节byte)(即使我只想读取1kb的内容,也必须先读取这个4kb的内容才能读取到这1kb的数据)

    那么为什么不以512字节为单位呢?

  •     ①因为512字节太小了,我们进行数据拷贝的时候可能会超过这个大小,所以我们需要进行多次IO操作,进而导致我们效率的降低。
  •     ②如果操作系统使用和磁盘一样的大小,那万一磁盘基本大小变了的话,操作系统的源代码需要改变(如果我今天操作系统和磁盘的基本大小强耦合),这就是将我们的硬件和软件(OS)进行解耦合。(不管你硬盘的基本大小是多少,我操作系统都是4KB为基本单位,也就是一个块的大小,称作块大小,所以我们将磁盘称为块设备)

    所以我们可以将Data blocks理解成多个4KB(扇区*8)大小的集合

    文件=内容+属性,而Linux在磁盘上存储文件的时候,将内容和属性是分开存储的!

    Data blocks中保存的都是文件的特定的文件内容。

    inode Table每一个文件都有一个inode编号,不同的文件都有不同的编号。一般而言,inode是一个大小为128字节的空间,保存的是对应文件的属性,该块组内,所有文件的inode空间的集合。有多个文件的话,需要表示唯一性。所以每一个inode块,都需要有一个inode编号。

    一般而言,一个文件,一个inode,一个inode编号!

    所以文件的内容存储在Data blocks中,然后其属性存储在inode块中

    无论是Data blocks还是inode Table都是有多个的,我怎么知道哪些block已经被占用了,哪些没有被占用呢?

    Block Bitmap:如果我们有10000个block,那么我们的BlockBitmap就有10000+个比特位,比特位和特定的block是一一对应的,其中比特位(标记位)为1表示这个block被占用,否则表示可用!

    inode Bitmap:如果有10000个inode节点,那我们的inode Bitmap中就有10000+个比特位,比特位和特定的inode是一一对应的。其中Bitmap中比特位(标记位)为1,代表该inode被占用了,否则表示可用。

    Block Bitmap和inode Bitmap中存储的是数据的管理信息

    GDT(group descriptor table): 块组描述符,主要描述的是这个块组多大,已经使用多少了,有多少个inode,已经占用了多少个,还剩多少个,一共有多少个block,使用了多少。表述的是这个块组的属性信息。

    我们将每一个块组块组分割,并且写入相关的管理数据,那么整个分区就被写入了文件系统信息。那么这一个过程就被我们称为格式化!

注意:

  一个文件“只”对应一个inode属性节点,inode编号,

 如果一个文件是4KB以内,那么一个block就够了,那么如果文件的大小大于4kb呢?那就需要多个block了。

找文件的时候先找到inode就可以了,通过inode编号找到inode,进而就可以找到该文件。只要找到该文件对应的inode编号就能找到该文件的inode属性集合,可是文件的内容呢?

 这个inode中的blocks属性就是保存的和当前文件同一个块组的块的编号

        struct inode
        {
            //文件的大小
            //文件的inode编号
            //其他属性
         
            //int blocks[15];
        }

    假设我们的一个文件中保存在6,7,8号块中,那么我们不妨这样设置

        blocks[0]=6;
        blocks[1]=7;
        blocks[2]=8;

    我们就能够找到相关的文件内容

    这个文件特别大,怎么办?

    需要使用更多的块,Data blocks中不是所有的Data blocks只能存文件数据,也可以存其他块的块号!比方说block[12]中可以存其它块的编号。

    一个块大小为4KB,里面可以存储大量的块号。这样就可以形成一棵多叉树,形成二级、三级等等的文件,用内容块去索引其他的文件块,这样我们的二叉树的叶子结点就可以存储大量的文件。

4.inode和文件名

    如果需要找到某个文件,就必须先找到inode编号,才能找到分区特定的Block group,只有找到了Block group才能找到文件的inode,然后才能知道文件的属性和内容。

    查看磁盘文件的存储

        df -h

    文件名和inode之间有什么关系呢?

    在Linux中,inode属性里面,没有文件名这样的说法 !也就是inode中不保存文件名。

    如果你想要找到inode编号,一定要依托于对应的目录文件的映射关系表的!

  •     1. 一个目录下,是不是可以保存很多文件?但是这些文件没有重复的文件名。
  •     (在同一个目录下,我们的文件名不可以重复)
  •     2.那么根据我们的Linux下一切皆文件,那么目录是文件,所以目录需要有自己的inode和自己的Data blocks。在目录里面创建文件,文件名不放在文件自身的inode里面,是放在所属的目录的Data blocks里面!目录中存储文件的文件名和文件的inode编号的映射关系。所以他们两个互为key值的,文件系统既可以用文件名去找inode,也可以用inode去找文件名。

    进入目录 必须需要的权限(x权限,执行权限)

 

    创建文件 必须需要的权限(w权限,写入权限)

       因为你要创建文件,目录也是文件,有自己的数据块,里面存储文件的文件名和文件的inode的映射关系,我们需要将这个映射关系写入目录文件中的Data blocks,所以是写入权限w。

    显示文件名与属性 (r权限,读取权限)

        因为我们想要显示文件名,文件的inode里面又没有文件名,我们需要从目录文件中拿去文件的文件名。通过我们的映射表,要么从inode找到文件名,要么从文件名找到inode

    所以,你想要找到inode编号,一定要依托于对应的目录文件的映射关系表的!

创建文件,系统做了什么?

    当前创建的文件对应的目录的分区是知道的,然后我们找到我们目录所在的分区和块组,根据文件系统,确定我们要保存我们文件的块,然后再我们的inode Bitmap里找到第一个为0的比特位,将其置1,表示其已经被占用。然后在inode Table里面将我们的文件的属性写进去,创建的时间,文件的大小,等等。将其内容清空,建立inode和文件名的映射关系,写入目录当中(找到目录的编号,找到其数据块,写入)。在Data blocks中将文件的数据写入(这里的文件名是从用户提供的,inode是文件系统提供的(内部创建好以后再给我们))

    我们如何知道目录的inode编号呢?

    Linux内核有一棵文件树, 标记了不同目录的inode编号

删除文件,系统做了什么?

    找到目录对应的Data blocks,在目录文件中以文件名为索引找到inode,将inode Bitmap对应的比特位的1置为0,然后将数据块Block Bitmap对应的位图从1置为0,然后从目录中将我们的文件名和inode的映射关系删掉.

    为什么删除比插入快得多?

    由于删的时候不需要把Data blocks的对应的文件部分清空,只需要在inode Bitmap表中标记文件无效即可。所以我们其实可以恢复删除的文件。只要你还能找到曾经删掉的文件的inode,然后将对应的分区当中的inode编号的inode Bitmap恢复,就知道了对应的属性,知道了哪些数据块属于这个文件,然后恢复Block Bitmap。由于inode Table和Data blocks是不会删掉的!Linux的删除日志里面会保存你删除的文件的inode编号的。

    当然,这里的能够恢复的前提是你曾经的inode编号,没有被使用,inode和Data blocks没有被重复占用。因为你删除了这个文件,你把这个Data blocks和inode置为无效了,如果系统有用这块空间来写入文件,那么就没办法恢复了,会覆盖掉的! 所以你误删了文件,最好的办法就是什么都不要做,怕文件被覆盖掉。

查看文件,系统做了什么 ?

    比方所我们在某个目录下使用ll指令,或者是ls,或者是cat一个文件。

    ls的时候只要找到一个文件的目录,找到这个目录对应的Data blocks,然后这个Data blocks中存储了文件的文件名和inode的映射关系,然后通过每一个文件的inode找到每一个文件的属性,然后将所有的信息都打印出来。

        这个位图一开始一定会被操作系统全部都清零,表示没有被使用,然后哪些代表inode Bitmap,inode Table, Data blocks,是什么时候划分,属性什么时候填入Supper Block中,是谁进行管理的呢?

    对磁盘进行分区,对磁盘的500个G分了4个区,然后我们对磁盘进行格式化,这里的格式化就叫做写入文件系统!写入文件系统就写入了我们上面的这些内容。写入了文件系统之后,我们的磁盘才能够被使用。

inode的个数是固定的,datablock的个数是固定的 ,但是你一旦固定好了,但是我看到磁盘中还有空间,但是创建文件失败了?

    可能是因为inode还有,Data blocks没有了,或者inode没有,Data blocks没了。这样的话,我们的文件就没有办法创建了。(这种情况特别少)

九、软硬链接

在当前目录下,用teslink.txt创建出一个软链接(加上-s选项)

ln -s testlink.txt soft.link

在当前目录下,用testlink1创建出一个硬链接

ln testlink1.txt hard.link

软链接有自己独立的inode

硬链接没有独立的inode

软链接有自己独立的inode,说明软链接是一个独立的文件,而硬链接没有独立的inode,说明硬链接不是一个独立的文件。

1.软链接

首先在当前目录下建立一个可执行文件bin.exe

然后将当前目录下的test文件拷贝到/bin/exe下

在当前我的用户名的工作路径下建立一个软链接

        建立软链接后,此时就可以认为是我们可执行程序的快捷方式,软链接的文件内容是指向的文件对应的路径。

acm
下面解释一下文件的三个时间:
  • Access 最后访问时间
  • Modify 文件内容最后修改时间
  • Change 属性最后修改时间

2.硬链接

由于硬链接没有独立的文件,创建硬链接不是真正的创建新文件。硬链接有自己的inode,但是是别人的inode,别人文件的属性。那么创建硬链接就是在指定目录下建立了文件名和对应指定的inode的映射关系,可以理解为就起了个别名。

当我们删除我们的文件,来查看此时硬链接会发生什么?

由上图发现,没有发生硬链接之前,硬链接数是1,而发生硬链接之后,硬链接数变为2,而删除之后,硬链接数减少一个。

    假设有一个file1和hard_link,它们的inode相同,站在文件系统的角度,怎么知道有多少个文件名是关联的呢??

    创建一个引用计数count,引用这个文件,就将这个计数count++, 删除这个文件,就将这个计数--。

    当我们删除一个文件的时候,并不是把这个文件的inode删除,而是将这个文件的inode引用计数--。当引用计数为0的时候,这个文件才被真正删除!引用计数为0的时候,也就是没有文件名和我关联了,没有用户关心这个文件了。

    我们下面再将硬链接进行硬链接第二个链接第一个,第三个链接第二个,我们的三个硬链接的链接数全部都变成了4。

利用unlink+文件名,取消链接,就相当于是删除了这个文件

    为什么我们创建了一个文件,这里链接数就是1?

    因为我们的自己的文件名和inode就是一组映射关系,所以这里的硬链接数默认就是1。

    为什么默认创建目录,引用计数(硬链接)为什么是2呢?

    这个目录本身的名字和inode就有一层链接,然后这个目录下默认就有两个文件,一个是.,一个是..。下面的图中我们的Test中inode和Test中的.的inode是一样的,这就是第二层硬链接

为什么我们在文件夹里面再创建一个目录文件,就会有三个硬链接?

由于路径中的.文件硬链接的是自身目录,而路径中的..文件硬链接的是上一层目录。所以除了我们目录自身的inode和文件的硬链接,第一层,还有我们文件中的.文件链接自身目录,也就是第二层

还有我们目录中的子目录的..,也就是第三层。所以目录的硬链接的个数2,就是目录中的合法目录数目。

通过硬链接[,然后也可以运行!

注意:

  • 目录一经创建,它的硬链接数就是2。
  • 普通文件一经创建,它的硬链接数就是1。

十、动静态库

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

1.创建静态库

首先创建以下所需要的文件

myprint.h

#pragma once
 
#include <stdio.h>
#include <time.h>
extern void Print(const char *str);

myprint.c

#include "myprint.h"
void Print(const char *str)
{
    printf("%s[%d]\n",str,(int)time(NULL));
}   

mymath.h

#pragma once
 
#include <stdio.h>
extern int addToTarget(int from,int to);
 

mymath.c

#include "mymath.h"
int addToTarget(int from,int to)
{
    int sum=0;
    for(int i=from;i<=to;i++)
    {
        sum+=i;
    }
    return sum;
}

创建一个userlib目录,在该目录下创建出main.c文件

main.c

#include "myprint.h"
#include "mymath.h"
#include <stdio.h>
int main()
{
    Print("hello world");
    int sum= addToTarget(2,10);
    printf("%d\n",sum);
    return 0;
}

编译程序,将main.c文件进行编译,然后将三个文件进行链接生成可执行文件即可。

gcc -c mymath.c -o mymath.o
gcc -c myprint.c -o myprint.o

将此时lib目录下所有的.h和.o文件拷贝给userlib目录下

cp *.h ../userlib
cp *.o ../userlib

此时我们进行编译链接就可以运行该文件了!

我们将所有的.o文件打包,此时就成为了一个静态库。

归档 archive files

-r replace

-c创建

库的前缀必须是lib

库的后缀名字必须是.a

ar -r libhello.a mymath.o myprint.o

也可以利用Makefile来完成

libhello.a:mymath.o myprint.o
	ar -rc libhello.a mymath.o myprint.o
mymath.o:mymath.c
	gcc -c mymath.c -o mymath.o -std=c99
myprint.o:myprint.c	
	gcc -c myprint.c -o myprint.o -std=c99
.PHONY:clean
clean:
	rm -f *.o libhello.a

库怎么给别人使用呢?

库有一个文件夹libhello -include(库的所有的头文件)-lib(保存对应的库文件)

libhello.a:mymath.o myprint.o
	ar -rc libhello.a mymath.o myprint.o
mymath.o:mymath.c
	gcc -c mymath.c -o mymath.o -std=c99
myprint.o:myprint.c	
	gcc -c myprint.c -o myprint.o -std=c99
.PHONY:hello
hello:
	mkdir -p hello/lib
	mkdir -p hello/include
	cp -rf *.h hello/include
	cp -rf *.a hello/lib
	
.PHONY:clean
clean:
	rm -f *.o libhello.a

将此时生成的静态库拷贝到我们的userlib文件夹中,将此时目录下只留下hello和main.c。

2.如何使用静态库?

1.将库添加到系统路径中

将这个库拷贝到系统库文件路径下:头文件gcc的默认搜索路径是:/usr/include,库文件的默认搜索路径是 /usr/lib64 或者/lib64。

sudo cp hello/include/* /usr/include/ -rf

 查看系统中是否存在该头文件

ls /usr/include/myprint.h
ls /usr/include/mymath.h

将我们的库文件拷贝到我们的系统的库中

sudo cp hello/lib/* /usr/lib64/ -rf

由于自己写的库属于第三方库,所以我们需要指定引用库的名字(库的名字需要去掉前缀lib和后缀.a)

gcc main.c -lhello

 我们刚刚的拷贝到系统的默认路径下,就叫做库的安装。安装软件就是拷贝,就是将软件拷贝到指定目录。 

注意:不建议将自己的库添加到系统的目录下,因为自己写的没有经过测试,容易出bug,会污染别人的头文件和库文件的内容 。

 头文件和库删掉的过程就是卸载

sudo rm /usr/include/myprint.h

sudo rm /usr/include/mymath.h

2.硬使用这个库

-I就是除了在系统路径和当前路径下,在我给你指定的路径下搜索头文件(头文件搜索路径)

-L在对应的路径下搜索我们的库(库文件搜索路径)

-l需要说明是这里路径下的那一个库(你要在特定路径下,使用哪一个库呢?)

gcc main.c -I ./hello/include/ -L ./hello/lib/ -lhello

3.创建动态库

形成一个与对应位置无关的二进制文件。也就是这个库形成了在任意位置都可以加载。

(静态库的话,你的代码必须要在特定的位置,绝对编址,但是动态库,你的代码可以放在任意地方,也就是采用相对编址的方式)

静态库就是直接拷贝到我们的程序中,然后动态库是链接到我们的程序中。

 相比于要形成静态库,要形成动态库需要加上-fPIC选项

gcc -fPIC-c mymath.c -o mymath.o -std=c99

gcc -fPIC-c myprint.c -o myprint.o -std=c99

readelf -S mymath.o

这里我们发现此时address里面全部都是0,我们进行打包处理,前缀为lib,后缀为.so,加上-shared形成动态库。

gcc -shared myprint.o mymath.o -o libhello.so

利用Makefile来进行打包形成动态库和静态库

.PHONY:all
all:libhello.a libhello.so

libhello.a:mymath.o myprint.o
	ar -rc libhello.a mymath.o myprint.o
mymath.o:mymath.c
	gcc -c mymath.c -o mymath.o -std=c99
myprint.o:myprint.c
	gcc -c myprint.c -o myprint.o -std=c99

libhello.so:mymath_d.o myprint_d.o
	gcc -shared mymath_d.o myprint_d.o -o libhello.so
mymath_d.o:mymath.c
	gcc -c -fPIC mymath.c -o mymath_d.o -std=c99
myprint_d.o:myprint.c
	gcc -c -fPIC myprint.c -o myprint_d.o -std=c99

.PHONY:output
output:
	mkdir -p output/lib
	mkdir -p output/include
	cp -rf *.h output/include
	cp -rf *.a output/lib
	cp -rf *.so output/lib

.PHONY:clean
clean:
	rm -rf *.o *.a *.so output

那么此时库的打包工作已经完成,此时我们将生成的动静态库移动到userlib中,模拟这个库给别人使用。

mv output ../userlib

此时进行压缩发布到网上就可以方便别人进行下载解压

tar czf mylib.tgz output

这里需要注意和硬链接一样,可以将库文件拷贝到系统路径中。

1.硬编译

gcc main.c -I output/include -L output/lib -lhello

查看a.out依赖的哪个库

ldd a.out

gcc默认使用的是动态库!

我们将这个动态库移除出这个文件夹 

mv output/lib/libhello.so ./

此时进行编译

gcc main.c -I output/include -L output/lib -lhello

现在调用的是静态库并没有调用动态库!

  •     a.如果只有静态库的话,gcc就只能针对该库进行静态链接(拷贝)
  •     b.如果同时存在动态库的静态库的话,就优先调用动态库
  •     c.如果动静态库同时存在,我非要使用静态库呢?

   如果非要使用静态库,就需要加上-static选项。

    -static的意义:摒弃默认优先使用动态库的原则,而是直接使用静态库的方案!

    gcc main.c -I output/include -L output/lib -lhello -static

2.动态库的加载

    动态库是一个独立的库文件

    动态库可以和可执行程序,分批加载!!

    进程地址空间有一块代码区,静态库的代码和我的代码都会被加载到代码区,他们全部都会被编址,所以是与地址有关,但是动态库呢?

    内存中有一块堆区(向地址减小的方向增长),栈区(向地址增大的方向增长)

    堆区和栈区相向而生,但其之间存在镂空,这片空间也就是我们的共享区。

    我们的a.out要访问动态库的时候,我们就将我们的动态库加载到了我们的共享区中。

    然后我们想使用库中某一个方法,我们就将其和页表建立映射,然后将去放入共享区中。

    所以我们的a.out只需要根据页表中找到这个共享区中的代码,然后调用这个方法就可以了。找到这个方法并且执行完成之后,然后返回就可以了。也就是可执行程序先加载,然后再见给我们的库文件加载进去。

    那如果我的系统中有大量的进程需要使用这一个动态库的代码呢?

    我们只需要将这个库都建立与这每一个进程的映射关系。只要这个库被加载到我们的共享区中,我们的其他进程都只要去这个共享区的地方调用这个库的代码就可以了。

   也就是说,如果是静态库的话,我们的内存中就会出现大量重复的库的代码,但是我们的动态库的话,只需要加载一次,也就是只需要在内存中存在一份就可以了!

    所以我们这里找不到动态库是因为我们的可执行程序被加载到内存中了,但是我们的库当前没有被加载到内存中。为什么已经知道动态库的路径却没有加载到内存?

     刚才的代码告诉我们的gcc我们的动态库的位置的那些参数全部都是跟gcc说的。

    但是我们的代码的加载与gcc没有关系!我们需要跟我们的加载器,或者是我们的操作系统说!

which ld

  

  我们需要跟我们的系统也说一下我们的动态库在哪里。

    静态库为什么不用告诉系统?

    因为我们编译好了,我们的库的代码已经在我们的可执行程序中了,不用跟系统说。而我们的动态库需要跟系统说。

    库加载的搜索路径

LD_LIBRARY_PATH

ls output/lib/

    系统在搜索库文件的时候就在下面的路径中搜索

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:你的库的路径

    但是我们的这个只能作为一个临时方案,也就是重新登录我们的xshell,我们的这个路径就没有了。

新增配置文件​​​​​​​

我们可以写一个配置文件放到我们的这个路径下面

ls /etc/ld.so.conf.d/ -ld

sudo touch /etc/ld.so.conf.d/test.conf

    我们将我们刚刚自定义的库的路径直接粘贴到这个文件中就可以了

sudo vim /etc/ld.so.conf.d/test.conf

    更新一下我们的配置路径,让其生效。

sudo ldconfig

sudo rm /etc/ld.so.conf.d/test.conf

    我们发现删掉了我们的配置文件,我们的程序还能跑,这是因为我们的缓存还在

更新一下发现sudo ldconfig,此时运行不了了

 3.建立软链接

    用软链接,链接到我们的动态库

sudo ln -s /home/zhuyuan/test_2022_11_05/output/lib/libhello.so /lib64/libhello.so

ls /lib64/libhello.so

此时程序能够继续运行!

或者是修改配置文件

vim .bashrc

vim .bash_prfile

    为什么要有库?

    站在使用库的角度,库的存在,可以大大减少我们开发的总周期,提高我们软件本身的质站在写库人的角度,他为啥不给我们源代码,只是给我们一个库呢?

    1.使用库更加简单,我们的代码之间的关系完全是解耦的。

    2.使代码更加安全。因为他并不想将代码公开。将代码变成二进制,很难还原。但是现在很多逆向工程,其实是能将这种.o文件还原成源文件的。  (将库放到网络上,提供对应的网络接口,我们就能够通过网络的接口来使用对应的服务)

猜你喜欢

转载自blog.csdn.net/qq_67458830/article/details/132271615