<Linux> 基础IO

文件操作

基本概念

  1. 文件=文件内容+文件属性。文件属性也是数据-> 即便你创建一-个空文件,也要占据磁盘空间

  2. 文件操作=文件内容的操作+文件属性的操作,也有可能,在操作文件的过程中,即改变内容,又改变属性

  3. 所谓的“打开”文件,究竟在干什么?将文件的属性或内容加载到内存中! 这是由冯诺依曼体系决定的!

  4. 是不是所有的文件,都会处于被打开的状态?绝对不是!没有被打开的文件,在哪里?只在磁盘上静静的存储着!

  5. 文件可以分为:打开的文件(内存文件)和磁盘文件

  6. 通常我们打开文件,访问文件,关闭文件,是进程进行相关操作。fopen ,fclose, fread, fwrite… ->代码->
    程序->当我们的文件程序运行起来的时候,才会执行对应的代码,然后才是真正的对文件进行相关的操作进程!

  7. 学习文件操作,本质上是,学习进程和打开文件的关系!(内存级的概念)

当前路径

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

int main()
{
    //chdir("/home/tyyg");// 更改当前进程的工作路径
    // 默认这个文件会在哪里生成呢?当前路径
    // r, w, r+, w+, a, a+
    // 文件清空的问题
    FILE *fp = fopen("log.txt", "w");// 写入
    if(fp == NULL)
    {
        perror("fopen");// 
        return 1;
    }

    printf("mypid: %d\n", getpid());

    while(1)
    {
        sleep(1);
    }

    const char *msg = "hello world";
    int cnt = 1;
    while(cnt<20)
    {
        fprintf(fp, "%s: %d\n", msg, cnt++);
    }
    fclose(fp);
}

查到pid后通过ll /proc/pid来查看进程信息,部分信息如下

image-20221016133044256

添加更改路径的语句chdir("/home/tyyg");// 更改当前进程的工作路径

更改后执行:

image-20221016133957915

此时,log.txt也生成在/home/tyyg路径中

image-20221016134056474

由此可见,cwd即当前路径:当前进程所处的工作路径

文件打开方式

“a”

追加写入

不断往文件中新增内容->增加重定项

int main()
{
    FILE *fp = fopen("log.txt", "a");// "a"追加写入
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    const char *msg = "hello world";
    int cnt = 1;
    while(cnt<=5)
    {
        fprintf(fp, "%s: %d\n", msg, cnt++);
    }
    fclose(fp);
}

image-20221016135315670

“w”

但我们以w方式打开文件,准备写入时,其实文件已经被清空了

int main()
{
    FILE *fp = fopen("log.txt", "w");// "w"
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    fclose(fp);
}

log.txt被清空了

image-20221016135800389

“r”

先在log.txt里加点内容

image-20221016140253361

执行以下内容

int main()
{
    FILE *fp = fopen("log.txt", "r");
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    char buffer[64];
    while(fgets(buffer, sizeof(buffer), fp) != NULL)
    {
        printf("echo: %s", buffer);
    }
    fclose(fp);
}

读取内容:

image-20221016140330517

再来点好玩的:改进一下程序,自定义读取的文件

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]);
        return 1;
    }

    FILE* fp = fopen(argv[1], "r");
    if(fp == NULL)
    {
        perror("fopen");// 
        return 1;
    }

    char buffer[64];
    while(fgets(buffer, sizeof(buffer), fp) != NULL)
    {
        printf("%s", buffer);
    }

    fclose(fp);
}

image-20221016141030173

经过改进,我们的myfile能做到cat命令的显示文件内容,于是我们给它改个名字,再执行,相当于自己写了个cat

image-20221016141132296

当我们向文件写入时, 最终是向磁盘写入,磁盘是硬件,只有操作系统才有资格向硬件写入。所有的上层访问文件的操作,都必须贯穿操作系统

那么操作系统是如何被上层使用的?必须使用操作系统提供的相关系统调用

回忆一下printf,printf是向显示器这个硬件进行写入,也就是说printf里一定封装了系统的接口

我们为什么没有见过系统接口呢?因为所有语言都对系统接口做了封装

为什么要封装呢?

  1. 原生系统接口,使用成本比较高
  2. 直接使用os接口,语言不具备跨平台性;以c语言为例,封装中穷举了所有底层接口,运用条件编译,使c语言具有跨平台性

文件描述符

open

image-20221022173017711

返回值

image-20221022173006369

mode:设置文件权限

image-20221022172001804

用位图结构来进行宏传递

它们拥有独特的标记位

它们的逻辑大致如下

#include <stdio.h>

#define PRINT_A 0x1
#define PRINT_B 0x2

void show(int flags)
{
    if(flags & PRINT_A) printf("A\n");
    if(flags & PRINT_B) printf("B\n");
}

int main()
{
    show(PRINT_A);
    show(PRINT_B);
    show(PRINT_A | PRINT_B);// 同时打印AB

    return 0;
}

清空log.txt文件:>log.txt

写入:

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

int main()
{
    umask(0);// umask掩码,让文件按我设置的umask来
    // fopen("log.txt", "w");先清空再写// 底层的open,O_WRONLY | O_CREAT | O_TRUNC
    //int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC/*清空之前内容*/, 0666);// 如果不设置文件权限(0666),权限会显示乱码
    //fopen("log.txt", "a");追加 //底层的open,O_WRONLY | O_CREAT | O_APPEND
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    // 写入
    int cnt = 0;
    //const char* str = "hello\n";
    const char* str = "aaaaaaaaa";

    while(cnt++<1)
    {
        write(fd, str, strlen(str));
    }

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


    close(fd);// 关
    return 0;
}

读取:

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

int main()
{
    umask(0);// umask掩码,让文件按我设置的umask来
    int fd = open("log.txt", O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    printf("fd:%d\n", fd);
    
	// 读取
    char buffer[128];
    ssize_t s = read(fd, buffer, sizeof(buffer)-1);
    if(s > 0)
    {
        buffer[s] = '\0';
        printf("%s\n", buffer);
    }

    close(fd);// 关
    return 0;
}

文件描述符fd是啥

image-20221022173006369

文件描述符fd是open()的返回值

fd<0:打开失败

fd>=0:打开成功

int main()
{
    umask(0);// umask掩码,让文件按我设置的umask来
   
    int fda = open("loga.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fde = open("loge.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    printf("fda: %d\n", fda);
    printf("fdb: %d\n", fdb);
    printf("fdc: %d\n", fdc);
    printf("fdd: %d\n", fdd);
    printf("fde: %d\n", fde);
  
    return 0;
}

image-20221024205656758

1. 为什么fd是从3开始,0,1,2呢?

系统自动打开了三个文件

0:标准输入(键盘)(对应c语言extern FILE* stdin)

1:标准输出(显示器)(对应c语言extern FILE* stdout)

2:标准错误(显示器)(对应c语言extern FILE* stderr)

c语言库封装系统调用接口,对文件操作而言,系统调用自认fd —> FILE结构体内部必定封装了fd

// 先验证0,1,2就是标准IO
// char buffer[1024];
// ssize_t s = read(0, buffer, sizeof(buffer)-1);
// if(s > 0)
// {
//    buffer[s] = '\0';

//    printf("echo: %s", buffer);
// }

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

// 验证012和stdin,stdout,stderr的对应关系
printf("stdin: %d\n", stdin->_fileno);// FILE结构体里的_fileno就是文件描述符
printf("stdout: %d\n", stdout->_fileno);
printf("stderr: %d\n", stderr->_fileno);

image-20221024211208436

2. fd为什么是0,1,2,3,4,5……呢?

0,1,2,3,4,5……是数组下标

一个进程能打开多个文件,系统中,可能会存在大量被打开的文件,OS要管理这些文件,它要先描述组织这些数据。

描述:struct file结构体里包含了我想打开的文件的大部分内容+文件属性。多个文件对应了多个struct file结构体

组织:进程里的struct task_struct中有一个struct files_struct *files,files指向struct files_structstruct files_struct中有一部分空间给了struct file*fd_array[],fd_array是一个指针数组,它里面存了struct file*类型的指针(指针指向struct file),fd_array通过数组的下标0,1,2,3……来管理struct file*

image-20221024221438770

3. 如何理解:linux下一切皆文件?

那么fd:0,1,2对应的键盘,显示器,显示器(硬件)也用上面的struct file来标识对应文件吗?

OS内的内存文件系统给外设的读写做了一层软件封装,封装成struct file,这样进程就能和文件一样正常读写。以统一的视角来看待所有设备!一切皆文件!

(所有软件底层的差异,都可以通过添加一个软件层来屏蔽其底层的差异。更多例子:进程地址空间、多态)

文件描述符的分配规则

从头遍历fd_array[],找到一个最小的,未被使用的下标,分配给新文件

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

int main()
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd<0)
    {
        perror("open");
        return 0;
    }
    //本来应该要往显示器打印,最终却变成了向指定文件打印 -> 重定向
    fprintf(stdout, "open sucessfully:%d\n", fd);
    fflush(stdout);

    close(fd);
    return 0;
}

如果close(0),运行结果如下。向显示器上打印提示语句,log.txt文件里啥都没有

如果close(1),关掉了标准输出,运行myfile不会往显示器上打印open successfully了,而在log.txt文件里发现了这句提示(open successfully打印到了文件里)

image-20221025204059502

image-20221025204112119

上层只认文件描述符0,1,2,3……,我们可以再OS内部,通过一定方式(这里是close了一个,再open一个)调整数组的特定下标的内容(指向),就能完成重定向的操作

重定向

谁能做替换?只有操作系统能做这样的事,它肯定提供了相关接口(dup2)

image-20221025205356795

image-20221025205518209

拷贝file*指针,newfd是oldfd的一份拷贝,最终只剩oldfd。

将fd的内容拷贝到1里面,最终,fd和1的内容都和fd一致。正确写法:dup(fd, 1)

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

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

    int ret = dup2(fd, 1);// 让1和3都指向log.txt,fd = 3,ret = 1
    if(ret > 0) close(fd);// 关掉3,只让1指向log.txt

    printf("ret = %d\n", ret);

    //本来应该要往显示器打印,最终却变成了向指定文件打印 -> 重定向吗?
    fprintf(stdout, "open sucessfully:%d\n", fd);
    fflush(stdout);// 等下再讲这个

    close(fd);
    return 0;
}

缓冲区

  1. 什么是缓冲区?

    本质就是一段内存

  2. 为什么要有缓冲区?

    解放使用缓冲区的进程的时间(进程不用一直等着它将信息写入外设)

    缓冲区的存在可以集中处理数据刷新,减少IO次数,从而提高整机的效率

  3. 缓冲区在哪?

    int main()
    {
        // printf没有立即刷新的原因,是因为有缓冲区的存在
        printf(" hello printf"); // stdout -> 1, -> 封装了write
        fprintf(stdout, " hello fprintf");
        fputs(" hello fputs",  stdout);
    
        //write可是立即刷新的!printf -> write
        //那么这个缓冲区在哪里?只能是C语言提供的,语言级别的缓冲区
        //那么这个缓冲区不在哪里?一定不在write内部!我们曾经谈论的缓冲区,不是内核级别的
        const char *msg = "hello write";
        write(1, msg, strlen(msg));
    
        sleep(5);
    
        return 0;
    }
    

    FILE结构体内部封装了很多属性,里面有fd,也有该FILE对应的语言级别的缓冲区。(既然缓冲区在FILE内部,在c语言中,我们每一次打开一个文件,都要有一个FILE*会返回,也就意味着,每一个文件都有一个fd和它自己的语言级别的缓冲区)

    数据先放到语言级别的缓冲区里,一定时间刷新一次缓冲区,缓冲区通过fd调用系统接口write将缓冲区里的内容打印出来

    1. 怎么刷新,刷新策略是怎样的

      行缓冲(显示器文件)

      全缓冲:写满了再刷新(磁盘文件)

      来看一个奇葩的现象

      int main()
      {
      
          const char *str1 = "hello printf\n";
          const char *str2 = "hello fprintf\n";
          const char *str3 = "hello fputs\n";
          const char *str4 = "hello write\n";
      
          //C库函数
          printf(str1); //?
          fprintf(stdout, str2);
          fputs(str3, stdout);
      
          //系统接口
          write(1, str4, strlen(str4));
      
          //是调用完了上面的代码,才执行的fork
          fork();
      
          return 0;
      }
      

      image-20221026170308547

      重定向之后,为什么每个c语言里的函数都能在磁盘文件中打印两次?

      答:

      重定向后刷新策略发生了改变,从行缓冲变成了全缓冲。

      刷新的本质,把缓冲区的数据write到OS内部,清空缓冲区

      缓冲区,是自己FILE维护的,本质上是属于父进程的数据区域。清空缓冲区之前,子进程要写实拷贝。

      这就导致:fork创建子进程,父子进程准备退出,要开始刷新缓冲区了。子进程发生写实拷贝,父进程将缓冲区的数据write到OS内部,父进程清空缓冲区。子进程将缓冲区的数据write到OS内部,子进程清空缓冲区。总共写入了两次数据!

    2. 如果在刷新之前关闭fd会有什么问题?

      int main()
      {
          // printf没有立即刷新的原因,是因为有缓冲区的存在
          printf(" hello printf"); // stdout -> 1, -> 封装了write
          fprintf(stdout, " hello fprintf");
          fputs(" hello fputs",  stdout);
      
          const char *msg = "hello write";
          write(1, msg, strlen(msg));
      
          close(1);// 关闭fd
      
          sleep(5);
          return 0;
      }
      

      缓冲区的数据在进程退出时不会刷新出来,fd被关了,后续的调用wirte函数都会失败,无法刷新缓冲区。这个现象再次证明printf等c语言接口,没有立即刷新,而是放到缓冲区里。

      所以,在之前的代码里,关闭fd之前要手动fflush清理缓冲区,这样才能将内容打印出来

模拟实现:自己封装C标准库

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

#define NUM 1024

#define NONE_FLUSH 0x0
#define LINE_FLUSH 0x1// 行缓冲
#define FULL_FLUSH 0x2// 全缓冲

typedef struct _MyFILE
{
    int _fileno;
    char _buffer[NUM];
    int _end;
    int _flags;// fflush method
}MyFILE;

MyFILE* my_fopen(const char* filename, const char* method)
{
    assert(filename);
    assert(method);

    int flags = O_RDONLY;

    if(strcmp(method, "r") == 0)
    {}
    else if(strcmp(method, "r+") == 0)
    {}
    else if(strcmp(method, "w") == 0)
    {
        flags = O_WRONLY | O_CREAT | O_TRUNC;
    }
    else if(strcmp(method, "w+") == 0)
    {}
    else if(strcmp(method, "a") == 0)
    {
        flags = O_WRONLY | O_CREAT | O_APPEND;
    }
    else if(strcmp(method, "a+") == 0)
    {}

    int fileno = open(filename, flags, 0666);
    if(fileno < 0)return NULL;

    MyFILE* fp = (MyFILE*)malloc(sizeof(MyFILE));
    if(fp == NULL)return fp;
    memset(fp, 0, sizeof(MyFILE));
    fp->_fileno = fileno;
    fp->_flags |= LINE_FLUSH;
    fp->_end = 0;

    return fp;
}

void my_fflush(MyFILE* fp)
{
    assert(fp);

    if(fp->_end > 0)
    {
        write(fp->_fileno, fp->_buffer, fp->_end);
        fp->_end = 0;
        syncfs(fp->_fileno);// 将数据刷新到磁盘
    }


}

void my_fwrite(MyFILE* fp, const char* start, int len)
{
    assert(fp);
    assert(start);
    assert(len>0);

    strncpy(fp->_buffer+fp->_end, start, len);// 指向有效字符的下一个位置 写入缓冲区
    fp->_end += len;

    if(fp->_flags & NONE_FLUSH)
    {}
    else if(fp->_flags & LINE_FLUSH )
    {
        if(fp->_end>0 && fp->_buffer[fp->_end-1] == '\n')
        {
            // 写入内核
            write(fp->_fileno, fp->_buffer, fp->_end);
            fp->_end = 0;
            syncfs(fp->_fileno);// 将数据刷新到磁盘
        }
    }
    else if(fp->_flags & FULL_FLUSH)
    {}
}

void my_fclose(MyFILE* fp)
{
    my_fflush(fp);
    close(fp->_fileno);
    free(fp);
}

int main()
{
    MyFILE* fp = my_fopen("log.txt", "w");
    if(fp == NULL)
    {
        printf("open error\n");
        return 1;
    }

    const char* s = "hello my 111\n";
    my_fwrite(fp, s, strlen(s));
    printf("消息立即刷新");
    sleep(3);

    const char *ss = "hello my 222";
    my_fwrite(fp, ss, strlen(ss));
    printf("写入了一个不满足刷新条件的字符串\n");
    sleep(3);

    const char *sss = "hello my 333";
    my_fwrite(fp, sss, strlen(sss));
    printf("写入了一个不满足刷新条件的字符串\n");
    sleep(3);


    const char *ssss = "end\n";
    my_fwrite(fp, ssss, strlen(ssss));
    printf("写入了一个满足刷新条件的字符串\n");
    sleep(3);

    //模拟进程退出
    my_fclose(fp);

    return 0;
}

image-20221026204619869

标准输出,标准错误

#include <iostream>
#include <stdio.h>

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

    // stderr
    perror("hello perror 2");
    fprintf(stderr, "hello fprintf 2\n");
    fputs("hello fputs 2\n", stderr);

    std::cout<<"hello cout"<<std::endl;

    std::cerr<<"hello cerr"<<std::endl;

    return 0;
}

image-20221027201643853

通过不同文件描述符打印,大家互不干扰。上面只对1重定向了,跟2没关系

分开打印:重定向2次

image-20221027202328311

作用:可以区分哪些是程序日常输出、哪些是错误(日志等级)

一起打印:

./a.out >all.txt 2>&1// 把1里面内容拷贝到2里面

image-20221027202535836

c语言有一个全局变量,记录最近一次c库函数调用失败的原因

perror是啥

// 模拟实现perror
#include <iostream>
#include <cstdio>
#include <cstring>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

void my_perror(const char* info)
{
    fprintf(stderr, "%s: %s\n", info, strerror(errno));// strerror(errno)将错误码转换成错误信息
}

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

image-20221027212430186

理解文件系统

上面学到的所有东西都是在内存中的。接下来我们将目光转移到磁盘上

不是所有文件都被打开

大量的文件,就在磁盘里,待着,这批文件很多很乱,我们该如何管理它们(磁盘级别的文件管理)。文件系统就是用来管理磁盘文件的

磁盘的物理结构

img

磁盘是我们电脑里的唯一的机械结构。目前,我们的笔记本上,可能已经不用磁盘了,而是SSD

机械式+它是外设 = 磁盘是很慢的(相比于CPU,内存)

盘片转动(决定区域),磁头摆动(决定半径),将数据存储在磁盘面上。(每个盘面都有一个磁头)

磁性的东西是有NS极(南极、北极)

磁头进行写入时,是在改变盘面上磁性物质的南北极(也就是在改变二进制0101)

我们的数据就存储在盘面上

磁盘的存储结构

img

img

磁盘上的基本单位是扇区(每个扇区的存储容量是512字节)

读写磁盘的时候,磁头找的是某个面 的 某个磁道 的 某个扇区

只要我们能找到磁盘上的盘面,柱面(磁道),扇区,就能找到一个存储单元。我们把这种物理上寻找最小单元的方式叫:CHS寻址(磁柱-磁面-扇区寻址)

磁盘的逻辑抽象结构

我们把盘片想象成线性结构,我们能不能把它想象成一个数组呢?定位一个扇区,我们只要找到下标(LBA,逻辑块地址)就行了。对磁盘的管理->对数组空间的管理

操作系统想向磁盘中写入,内存里只知道LBA地址(逻辑块地址),把LBA地址映射转化成CHS地址(磁柱-磁面-扇区地址),然后写入磁盘。

如何将LBA地址映射转化成CHS地址?

假设3片盘面共有3000个地址(每个盘面有1000个地址),每个盘面有20个磁道,假设LBA地址为1234。

1234/1000 = 1(在第2面)

1234%1000 = 234

234/20 = 11(在11号磁道)

234%20 = 14(在14号扇区)

—>CHS地址:C(磁道):11 H(磁面):3 S(扇区):14

每个扇区只有512字节,比较小,而且机械的磁盘数据移动比较慢,所以操作系统将8个扇区视为一个基本单位(4KB)

所以IO的基本单位是4KB

优点:

  1. 提高IO效率(一次读取到的内容增加,读取次数减少,机械寻址次数减少)
  2. 不要让软件设计和硬件具有强相关性,也就是,解耦合!(如果硬件变了,基本单位从512字节变成了1024字节,软件也不受影响)

但一个完整磁盘假设有500GB,太大了,不好管。我们把它分小一点,分成每个100GB,再分成10GB……只要磁盘成功管好10GB的内容,就能管好总共500GB的内容,这就是分区的过程(分而治之)

文件 = 内容 + 属性 ----都是数据 —都要存储-----linux采用的是将内容和属性分开存储的方案----内容存在block(4KB)里,属性存在inode(128字节)里

image-20221030122224118

Data blocks:以块为单位,每块4KB,进行文件内容的保存

inode table:以128字节为单位,进行inode属性的保存。inode属性里面有个inode编号,一般而言,一个文件一个inode,一个inode号

image-20221030123237211

Block Bitmap:用位图来判断数据块的使用情况

inode Bitmap:用位图来显示inode块是否被占用

Group Descriptor Table(GDT):有多少inode,起始inode编号,有多少inode被使用,还剩多少,有多少block被使用,还剩多少……(管理分区里的某个组)

Super Block:文件系统的顶层数据结构(分区有几个块,分区的情况……管理整个分区)既然它是整个分区的属性,不应该只写在块外面吗?为什么写在块里,这主要是用来做备份的

一个inode(文件,属性)怎么和属于自己的内容关联起来呢?

inode编号能找到它在哪个group里面,并且找到那个inode

inode里面存有blocks编号,靠blocks编号来找文件所在的块

假设blocks[15]数组里存储blocks编号,blocks编号指向data block块,块里存储数据,这样的话,文件最大就只有4KB*15这么大,那我们需要更大的文件空间怎么办?

inode里面可以有一个blocks编号指向data block,而这个data block里存储的是其他块的blocks编号。也就是说data block也可以保存其他块的inode编号(二级索引),不仅如此,如果空间还不够用还能有三级索引……肯定会让你够用的

要找到文件,必须要找到文件的inode编号

**谁来帮我找inode编号呢?**目录

目录是文件,文件=内容(blocks)+属性(inode)。内容里放着:文件名和inode编号的映射关系

linux同一个目录下,可以创建多个同名文件吗?不能。文件名本身就是一个具有唯一Key值的东西

文件名算文件的属性吗?算。但是inode里面不保存文件名,在目录里才用到文件名

当我们创建一个文件,操作系统做了什么?

查找inode bitmap找到有个inode没有被使用,我们假设第141231位没被使用,inode bitmap第141231位置1,将文件属性值写入第141231号的inode里。如果要写入文件内容,就在block bitmap里面找没有被使用的block,修改block bitmap,然后将内容写入那些block里面,并且将那些block和141231号inode之间联系起来

创建一个文件时,肯定是在一个目录下

我们创建好文件后,拿到新文件的文件名和inode编号---->找到所处的目录---->根据目录的inode找到目录的data block---->将文件名和inode编号的映射关系写入到目录的数据块中

image-20221030140535196

删除一个文件,OS做了什么?

找到所处的目录,拿到文件名和inode编号的映射关系---->通过要删除文件的文件名----->查找inode编号---->根据inode找到inode属性,根据属性找到inode的blocks数组---->将inode bitmap和block bitmap的那些位,置0(等待未来其他内容覆盖这块空间)------>在所处目录中将该文件名和inode编号的映射关系删掉

我知道自己的目录,就能知道自己的inode吗?不能,要找到父目录,父目录的父目录……要从根目录开始找

image-20221030134346074

软硬链接

image-20221030141054634

软硬连接的区别:软连接是一个独立文件,有自己独立的inode和inode编号。硬链接不是一个独立文件(是什么? )他和目标文件使用的是同一个inode !

软链接:可以当Linux下的快捷方式!既然是一个独立文件,inode是独立的。软连接的文件内容是什么呢? 保存的是指向的文件的所在路径

硬链接:就是单纯的在Linux指定的目录下,给指定的文件新增文件名和inode编号的映射关系!
什么是硬链接数?
inode编号,不就是一个”指针“的概念吗?
本质就是改文件inode属性中的一个计数器,count,标识有几个文件名和我的inode建立了映射关系。
简言之,就是有几个文件名指向我的inode(文件本身! )
这有什么用? 路径间切换

为什么创建普通文件,硬链接数默认是1?
因为,普通文件的文件名本身就和自己的inode具有映射关系,而且只有一个

为什么目录文件,硬链接数默认是2?本目录+.(共2个)

image-20221030150752867

动静态库

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

库里要不要main函数?不要,否则用户使用库会出错

链接:把所有的.o 文件链接成一个可执行程序。如果把所有的.o给别人,别人能使用吗?能,但.o文件太多了,我们打包一下给别人使用更方便

如何形成动静态库

#动态库
libmymath.so:mymath.o myprint.o
	gcc -shared -o libmymath.so mymath.o myprint.o
mymath.o:mymath.c
	gcc -fPIC -c mymath.c -o mymath.o -std=c99
myprint.o:myprint.c
	gcc -fPIC -c myprint.c -o myprint.o

.PHONY:dyl
dyl:
	mkdir -p lib-dy/lib
	mkdir -p lib-dy/include
	cp *.so lib-dy/lib
	cp *.o lib-dy/include
	
.PHONY:clean
clean:
	rm -rf *.o *.so lib-dyl

#静态库
# libmymath.a:mymath.o myprint.o
# 	ar -rc libmymath.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

# .PHONY:static
# static:
# 	mkdir -p lib-static/lib
# 	mkdir -p lib-static/include 
# 	cp *.a lib-static/lib
# 	cp *.o lib-static/include

# .PHONY:clean
# clean:
# 	rm -rf *.o *.a lib-static

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

总结为:

.PHONY:all
all: libmymath.so libmymath.a

libmymath.so:mymath.o myprint.o
	gcc -shared -o libmymath.so mymath.o myprint.o
mymath.o:mymath.c
	gcc -fPIC -c mymath.c -o mymath.o
myprint.o:myprint.c
	gcc -fPIC -c myprint.c -o myprint.o

libmymath.a:mymath_s.o myprint_s.o
	ar -rc libmymath.a mymath_s.o myprint_s.o
mymath_s.o:mymath.c
	gcc -c mymath.c -o mymath_s.o
myprint_s.o:myprint.c
	gcc -c myprint.c -o myprint_s.o

.PHONY:lib
lib:
	mkdir -p lib-static/lib
	mkdir -p lib-static/include
	cp *.a lib-static/lib
	cp *.h lib-static/include
	mkdir -p lib-dyl/lib
	mkdir -p lib-dyl/include
	cp *.so lib-dyl/lib
	cp *.h lib-dyl/include

.PHONY:clean
clean:
	rm -rf *.o *.a *.so lib-static lib-dyl

库的使用

头文件的搜索:

  • 在当前路径找
  • 在系统头文件路径下查找头文件,本质上是进程在找

使用:

  1. 将自己的头文件和库文件拷贝到系统路径下(但要用gcc -l 指明我们要链接的第三方库的名称)(非常不推荐!!这样会污染官方库)

  2. 指定头文件搜索路径

    静态库:

    gcc mytest.c -o mytest -I ./lib-static/include/ -L ./lib-static/lib/ -lmymath

    -I(头文件查找路径)-L(库文件搜索路径)-l(在-L指定的路径下,你要链接哪个库)

    如果安装到系统就不用这么麻烦了

    动态库:

    gcc mytest.c -o mytest -I ./lib-dyl/include/ -L ./lib-dyl/lib/ -lmymath

    但在运行时会出现问题

    image-20221101203515810

    那么静态库的时候,怎么没有这个问题呢? 静态库形成可执行程序之后,已经把需要的代码拷贝进我的代码中!运行时,不依赖你的库! – 不需要运行时查找

    为什么动态库会有这个问题?是因为程序和动态库,是分开加载的!而动态加载库的时候要先找到库在哪

    如何解决:

    想办法让进程找到动态库即可

    1. 动态库拷贝到系统路径下/lib64 – 安装

    2. 通过导入环境变量的方式–程序运行的时候,会在环境变量中查找自己需要的动态库路径-- LD_LIBRARY_PATH

      image-20221101204418694

      (缺点:退出重进后环境变量就没了)

    3. 系统配置文件来做。在/etc/ld.so.conf.d/下新建一个文件夹,将自己的路径加入新文件夹中,然后ldconfig一下,就能运行了

    4. 其他方式:软连接方案(在系统下建立软链接,链接自己写的动态库,作为一个快捷方式)运行时-l说明要链接哪个库

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天影云光

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值