Linux——基础IO【3万字大章】

server/2025/3/10 22:54:54/

目录

  • Linux——基础IO
    • 1.文件基础知识
    • 2.文件操作(C语言)
    • 3.系统文件IO
      • 3.1open
      • 3.2write
      • 3.3read
    • 4.open返回值
    • 5.文件描述符的本质
    • 6.文件描述符的分配规则
    • 7.重定向
      • 7.1理解重定向原理
      • 7.2重定向接口dup2
      • 7.3各种重定向
    • 8.在shell添加重定向功能
    • 9.理解Linux中一切皆文件
    • 10.引用计数
    • 11.缓冲区的理解
      • 11.1缓冲区的本质
      • 11.2缓冲区在哪里?
        • 11.2.1FILE
      • 11.3总结
    • 12.通过实验加深缓冲区理解
      • 实验代码:
      • 实验现象
    • 13.内核缓冲区
      • 13.1fsync函数
    • 14.理解文件系统
      • 14.1磁盘的结构
        • 磁盘的物理结构
        • 磁盘的存储结构
        • 磁盘的逻辑结构
      • 14.2文件系统和内存的耦合
      • 14.3 文件系统与磁盘(理解Inode)
        • 理解文件系统如何将属性和数据分开存储又能联系起来
        • 总结:
    • 15. 软硬链接
    • 16.动态库静态库
      • 16.1库的本质
      • 16.2静态库和静态链接
      • 16.3动态库和动态链接
      • 16.4动静态库的加载
    • 17.总结
      • 16.4动静态库的加载
    • 17.总结

Linux——基础IO

1.文件基础知识

在了解基础IO之前,需要先对文件的基础有所了解才行,如下图所示:

image-20250226190853734

其实上图的知识大部分都是之前接触过的,应该是不陌生的,陌生的话就要复习一下。

经过上图,可以总结出文件操作的本质就是——进程和被打开文件的关系

2.文件操作(C语言)

在说C语言的文件操作之前,需要了解一下文件操作的一些知识,如下图所示:

image-20250226191936466

总结来说就是:

不同的语言都有其各自的文件操作的接口,但是这样就会造成学习成本过高的问题。但是不要忘了,文件是存于硬盘上的,只有OS有权限和资格访问硬盘,并且无论是什么语言的文件操作,最终还是要调用OS提供的系统调用接口来实现文件操作,因此为了降低学习成本,直接学习OS提供的系统调用的接口

接下来就可以来讲有关C语言的文件操作了

这里有个拓展——在vim中批量化注释normal模式下,按ctrl+v 再按j,此时显示光标,鼠标滚轮下滑选中范围,按I,按//,再按ESC

要取消上一步的操作就是u,之前讲过了

来看一段C的文件操作的代码:

#include<stdio.h>
#include<unistd.h>
#include<string.h>#define FILE_NAME "test.txt"int main()
{//以w打开,不存在文件会直接创建  // r,w,r+,w+,a,a+ 不记得要复习//FILE* fp = fopen(FILE_NAME, "w"); // 向文件写入,每次打开会清空文件FILE* fp = fopen(FILE_NAME, "r"); // 只读取文件//FILE* fp = fopen(FILE_NAME, "a"); // 向文件结尾追加写入的内容if(fp == NULL){perror("error fopen");return 1;}// 向文件中写入数据
//    for(int i = 0; i < 5; i++)
//    {
//        fprintf(fp, "%s:%d\n", "hello", i);
//    }
//// 将文件中的数据读取出来,并输出到屏幕上[stdout]char buffer[64];// fgets读取失败返回NULL, // -1的原因是在buffer始终给\0留一个位置,因为fgets函数会将读取出来的数据加上\0while(fgets(buffer, sizeof(buffer)-1, fp) != NULL){// 将buffer里的数据输出到屏幕上buffer[strlen(buffer)-1] = 0; // 将\n置为0. 防止多余的换行puts(buffer);}fclose(fp);return 0;
}

执行效果可以自己试一下,代码中涉及了读写,测试的时候注意。

3.系统文件IO

3.1open

除了可以使用语言级别(c\c++\java\python等)的文件操作,其实我们也可以直接使用系统调用来实现文件操作

在C语言中使用的文件操作的接口fopen,用于打开文件,其实底层调用的系统调用接口叫做open

使用Linux的man手册查看,【open是系统层面的接口,手册的时候记得输入2,而不是3】

image-20250227113128680

由于这个是系统调用,有很多东西和语言层面的是不一样的。需要先提前知道一下。

  • 标记位问题

在C语言中传一个整数作为标记位,还有布尔类型作为标记位,但是在系统层面是没有布尔类型的,而且也不会传一个整数作为标记位。

int类型有32个比特位,系统会通过比特位来传递选项,系统自定义每个不同的比特位代表着什么选项,这里举个例子来理解系统是怎么通过传递比特位来实现选项的

#include<stdio.h>// 每一个宏,只有一个比特位是1,彼此直接不会重叠
#define ONE (1<<0) // 等价于0x01
#define TWO (1<<1) // 等价于0x02
#define THREE (1<<2)
#define FOUR (1<<3) void show(int flag)
{if(flag & ONE)printf("one\n");if(flag & TWO)printf("two\n");if(flag & THREE)printf("three\n");if(flag & FOUR)printf("fout\n");
}int main()
{// 通过与的方式,利用比特位传递选项,这样32位会有32个选项show(ONE);printf("-----------------------\n");show(ONE | TWO);printf("-----------------------\n");show(ONE | TWO | THREE);printf("-----------------------\n");show(ONE | TWO | THREE | FOUR);return 0;
}

程序执行效果如下图所示:

image-20250227120753737

知道了利用标记位的比特位来传递选项的过程后,再来看open这个系统调用的参数就不会觉得奇怪了

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

int是open返回的参数,指针是语言级别的,系统层面没有指针概念。这个返回值后面会详细讲,这是一个文件描述符

pathname就是要打开的文件的路径,flags就是标记位,而传递的选项有这些

image-20250227121927102

mode是创建文件的权限【当文件不存在的使用,要使用open来创建文件时,这个文件的起始权限为mode】

下面来看看open的代码使用案例:

#define FILE_NAME "text.txt"int main()
{// 以O_WRONLY(只写)的方式打开文件int fd = open(FILE_NAME, O_WRONLY);if(fd < 0){perror("open error");return 1;}close(fd);return 0;
}

如果这样写的话,文件不存在时,是不会创建新文件的,以只写方式打开文件,在文件不存在的时候创建新文件,是c语言的接口对open进行了封装时候额外实现的,open并没有这样的作用

image-20250227124245033

如果想要open在不存在文件的时候创建一个新的出来就要在带一个选项:

int fd = open(FILE_NAME, O_WRONLY | O_CREAT); 

但是这样创建出来的文件是乱码,权限也是乱的

image-20250227124426306

为什么会这样呢?因为起始权限不对,之前在Linux权限那部分学过,在Linux中创建一个文件是由起始权限配合上umask,最终得到一个文件的权限的,而目录的起始权限是777, 其他的是666.那凭什么是777和666呢?其实就是靠open的第三个参数mode,我们需要给mode传起始权限

int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);

此时在执行程序得到的,text的权限是664,这就正常了

image-20250227124735478

3.2write

image-20250227130243967

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

fd,往那个文件里去写。buf,数据所在的缓冲区,count:写的字节个数

返回值,就是count,写了10个,返回10.

并且在write看来,不会有写入的分类,都是以二进制形式写入文件。

image-20250227143920558

直接看一段写入的代码:

    // 使用write对文件进行写入char buffer[64];for(int i = 1; i < 6;  i++){sprintf(buffer, "%s:%d\n", "hello", i);write(fd, buffer, strlen(buffer)); // 向文件写入string的时候,不要将\0写进去,会出现乱码}

image-20250227145559884

但是要注意了,在C语言中,我们说过使用w方式打开文件,每次打开都会自动清空文件内容。但是write是没有这样的功能的。要实现的话,要在open的选项中多带一个选项

// c语言中fopen选w,在open中要选 O_WRONLY, O_CREAT(文件不存在创建文件),O_TRUNC(每次打开清空文件内容) + 0666int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);

如果想要fopen的a模式的话

int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666); // fopen的a模式

3.3read

image-20250227151303146

fd就是读取那个文件,buf就是读取完当到那个缓冲区,count就是期望读取到多少字节

返回值是一个有符号整形,读取成功就返回读取到多少个字节

直接来使用的例子:

    // 以只读方式打开文件int fd2 = open(FILE_NAME, O_RDONLY); if(fd < 0){perror("open error");return 1;}//使用read对文件进行读取char buffer2[1024];ssize_t num = read(fd2, buffer2, sizeof(buffer2) - 1);//-1是为了留个\0的位置if(num > 0)buffer2[num] = 0; // 0 '\0' NULL都是0printf("%s", buffer2);close(fd2);

到这里我们已经通过系统调用接口实现了文件的读写操作了,这是所有语言的库函数文件操作接口底层都要调用的系统调用接口。

4.open返回值

在讲open的返回值之前,先来看开头讲的一句话:文件操作的本质:进程和被打开文件的关系

这个关系究竟是什么关系呢?来分析一下:

首先一个进程肯定可以打开很多个文件——>系统中一定会存在大量被打开的文件——>被打开的文件,肯定要被统一管理,不然数量太多,很麻烦——>而OS管理的方式一直都是先描述,再组织——>根据之前的经验,那就肯定会有一个内核数据结构来描述被打开的文件,它就是struct_file{} ——>这个内核数据结构,包含了一个文件的大部分属性——>而OS想要管理这些被打开的文件就很简单了,将描述好的struct_file以一个数据结构的形式组织起来就行了

上面说了这么多,好像没有解决问题,上面描述的是OS对被打开文件的管理缘由和方式,那进程和被打开文件的关系是什么呢?这就需要我们先学习一下open的返回值了

open的返回值其实是一个文件描述符

但是这里其实有个小问题,既然open的返回值既然是一个文件描述符,那为什么在C语言中fopen接口的返回值是一个FILE* 类型的数据,难道FILE是一个宏,其实就是int?

并不是,FILE其实是一个结构体,但是系统调用访问文件,必须使用文件描述符,因此在FILE这个结构体中,应该是存在一个字段,存放着文件描述符才对

关于这个FILE结构体,后面会再谈

先来一段代码,将open的返回值输出一下

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>//探究open系统调用接口的返回值#define FILE_NAME(n) "text.txt"#n // 宏的用法,忘记了记得复习int main()
{int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd2 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd3 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd4 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd5 = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);printf("fd: %d\n", fd1);printf("fd: %d\n", fd2);printf("fd: %d\n", fd3);printf("fd: %d\n", fd4);printf("fd: %d\n", fd5);return 0;
}

执行结果如下:

image-20250227212214899

这是因为0、1、2都默认被其他占用了,这个其他就是——标准输入输出流

Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.

0,1,2对应的物理设备一般是:stdin–键盘,stdout–显示器,stderr–显示器

其实这标准输入输出流,也是FILE * 类型

image-20250227213835622

疑问:为什么文件描述符为0、1、2的标准输入输出流的类型是FILE*类型,即stdin–键盘,stdout–显示器,stderr–显示器,这三个难道也是文件吗?

后面会谈

前面说FILE这个结构体应该有一个字段存放着文件描述符,事实上也确实这样

看如下代码:

image-20250227214757789

运行结果如下:

image-20250227214834767

那为什么文件描述符是从0开始然后顺序的012345呢?

这就要涉及到文件描述符的本质是什么东西了

5.文件描述符的本质

为什么上面代码中输出的文件描述符是一个顺序的结果呢?

下面我们来谈谈为什么

首先,文件操作的本质就是进程和被打开文件的关系,对于一个进程来说,OS会突然给它一堆被打开的文件,而对于进程来说,想要准确的找到某个文件,就必然需要将这些被被打开的文件管理起来,因此在进程必须存放着一个管理struct file的结构体——struct files struct,而进程的PCB中,就有一个指针——struct files struct *files,而这个结构体本身是一个指针数组——struct file* fd_array[]。因此这就解释了为什么文件描述符打印出来是连续的从0开始的int,因为文件标识符就是这个struct file* fd_array[]指针数组的下标!而每个文件从磁盘中被系统调用的接口打开后,都会加载到这个数组中,然后系统调用的接口open会返回这个文件在这个数组中的下标,即文件描述符!

image-20250227224319010

知道了这个过程之后,我们就知道进程每次要访问文件的时候,都需要传入一个文件描述符。这样就会直接根据下标到指针数组中找,然后访问文件

因此我们可以得出结论——文件描述符的本质就是数组的下标!!!

而这个指针数组也叫进程的文件描述符表

image-20250227221312862

下图了解一下:

image-20250302123349078

6.文件描述符的分配规则

前面我们说了,默认0,1,2,是打开的,因此只要打开一个0,1,2外的新文件,那么他的struct file就会被放在文件描述符表的第四个位置,也就是下标为3的位置,因此他的文件描述符就是3。但是它只能从3开始吗?不一定

我们可以将0文件关掉,这样下标为0的位置就为空,此时打开一个新文件就会放到这个位置,即它的文件描述符为0。看下面这段代码:

int main()
{close(0); // 关掉标准输入流0这个文件,空出文件描述符表下标为0的这个位置int fd = open("text.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if(fd < 0){perror("open error");return 1;}printf("fd : %d\n", fd);close(fd);return 0;
}

image-20250228104452455

那关掉1和2会发生什么?

关掉2的执行结果如下:

image-20250228104727530

因此我们可以得出结论——文件描述符的分配规则为:从小到大,按照顺序寻找最小的且未被占用的文件描述符

7.重定向

7.1理解重定向原理

上面还没说完,关掉1会发生什么呢?来看看

关掉1的执行结果如下:

image-20250228104738153

为什么这里没有结果呢?

因为1是标准输出流stdout——显示器,这个文件被关掉了,打印printf函数其实就是向stdout输出,因此它默认会向文件描述符为1的文件输出,但是此时1不再是stdout了,而是我们自己打开的新文件,因此printf都输出到文件描述符为1的新文件了,来抓取一下文件内容

image-20250228105724080

可以看到文件里也没有,这是为什么?难道上面的推论是错的?

并不是,上面的推论没问题,printf也确实向文件描述符为1的新文件输出内容了,但是**都在缓冲区了!**此时还没有写进文件中,要看到的话要刷新一下缓冲区

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

此时再来查看文件中就存在内容了、【为什么fflush(stdout);中参数是stdout呢,因为函数默认stdout的文件描述符为1,只是此时被替换成了新文件】

image-20250228110033664

这里有个现象——本来是要往显示器输出内容的,最后却输出到了文件里!

这个现象和特性叫什么呢?——重定向

是的,这个就叫做重定向,重定向的本质就是——上层调用的文件描述符不变,内核中更改文件描述符对应的struct file 的地址*

上述例子发生的重定向的原理如下图所示:

image-20250228111102975

重定向不止这一个例子,只是这个例子也体现出了重定向

7.2重定向接口dup2

实际上,上面实现重定向的代码有点搓,一旦0,1,2都被关掉了,此时还想重定向1的话就不好搞了。

因此Linux中为了更好的帮助我们实现重定向,提供了一些接口,其中有个接口叫dup2

image-20250228112118844

int dup2(int oldfd, int newfd);

下图是man手册对dup2函数的介绍:

image-20250228112358484

其实dup2的两个参数,就是对传进来的oldfd文件描述符进行拷贝,注意了不是拷贝文件描述符本身(本身就是个下标),而是拷贝文件描述符所指向的内容(指针)。

本质上其实就是在拷贝文件描述符这个下标所指向的内容。比如传了0,1、那么就会拷贝文件描述符表当中,下标为0,这个位置的内容,然后粘贴到下标为1的位置,对下标为1的原本的内容进行覆盖

这就意味着0,1这两个文件描述符找到的文件都是0,它不会将原本0的文件关掉。

下面我们就来使用一下dup2,将上面的例子用dup2实现一下:

// 使用dup2实现重定向int main()
{int fd = open("text.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if(fd < 0){perror("open error");return 1;}dup2(fd, 1); // 把fd的内容拷贝到1上printf("fd : %d\n", fd); // 当关掉1时,会发生重定向close(fd);return 0;
}

此时文件描述符1找到的文件也是文件描述符fd所找到的文件,两个文件描述符找到同一个文件text.txt上

执行结果如下:

image-20250228121705193

7.3各种重定向

上面的例子可以说是输出重定向,意思就是本来要输出到显示器上的,但是都重定向输出到了指定文件中

除了输出重定向,还有追加重定向,输入重定向。只要是文件操作,基本都可以重定向

  • 追加重定向只需要将打开文件的方式改成追加【int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666); // fopen的a模式】然后重定向,把原本要向某个文件追加的内容,追加到指定文件上

  • 输入重定向,举个例子:将原本从标准输入——键盘输入,重定向为从fd这个文件描述符指向的文件输入,代码如下:

image-20250228150729738

8.在shell添加重定向功能

理解了重定向的原理之后,可以尝试将让自己的shell,也能实现重定向功能

重点在思路上,使用shell的重定向无非就是输入 内容 > 文件内容 >> 文件内容 < 文件

这三个情况,对应输出重定向,追加重定向,输入重定向

我的思路是用一个函数来判断用户输入的是否为重定向,是什么重定向:

#define NUM 1024
#define OPTION_NUM 32// 定义重定向状态
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3 int redirtype = NONE_REDIR; // 设置重定向状态
char* redirFile = NULL;// 定义一个宏函数,用于过滤多个空格
#define trimSpace(start) do{\while(isspace(*start)) ++start;\}while(0) // 不需要加; 当放到条件时也不会报错void CommandCheck(char* command)
{// 先判断是不是重定向,是什么重定向assert(command != NULL);char* start = command;char* end = command + strlen(command); // 指向末尾while(start < end){if(*start == '>'){// 可能是追加,也可能是输出重定向*start = '\0'; start++;if(*start == '>'){// 追加重定向redirtype = APPEND_REDIR;start++; }else{// 输出重定向redirtype = OUTPUT_REDIR;}trimSpace(start); //找到文件redirFile = start;break;}else if(*start == '<'){// 输入重定向*start = '\0'; // 左边为指令,右边为文件start++;trimSpace(start); // 过滤所有空格,直到碰到文件redirtype = INPUT_REDIR;redirFile = start;break;}start++; // 迭代}// 走出来就是没有重定向,但是redirtype初始化就是NONE_REDIR
}

只要调用了这个函数,那么用户输入的指令就可以被确认是什么重定向,还是没有重定向

然后,就要交给子进程去完成对重定向的操作【不会影响父进程】

//此时已经处理好,用户所输入的指令了,这个时候就要去执行命令// 执行命令可以用进程的程序替换pid_t id = fork();assert(id != -1);if(id == 0){// child// 程序替换的函数选择execvp,环境变量用系统的,将处理好的命令直接传给execvp即可// 这样只用传要执行什么指令,以及其指令所带的选项即可// 判断是否有重定向,是什么重定向, 这里的重定向不会影响到父进程`:`switch(redirtype){case NONE_REDIR:break;case INPUT_REDIR:{int fd = open(redirFile, O_RDONLY, 0666);if(fd < 0){perror("open error");exit(1);}// 此时获得了要重定向的文件的文件描述符fddup2(fd, 0); // 输入重定向close(fd);}break;case OUTPUT_REDIR:case APPEND_REDIR:{// 输出和追加都在这里处理int flags = O_WRONLY | O_CREAT; // 不管是输出还是追加都要这两个选项if(redirtype == APPEND_REDIR)flags |= O_APPEND;elseflags |= O_TRUNC;int fd = open(redirFile, flags, 0666);if(fd < 0){perror("open error");exit(1);}// 成功打开重定向文件,获得fddup2(fd, 1); // 1文件即为标准输出流,显示器close(fd);}break;default:printf("redirtype error\n");break;}execvp(myargc[0], myargc); // 进程程序替换exit(0);}

总体代码如下:

# include<stdio.h>
# include<assert.h>
# include<stdlib.h>
# include<assert.h>
# include<sys/types.h>
# include<sys/stat.h>
# include<fcntl.h>
# include<sys/wait.h>
# include<unistd.h>
# include<string.h>
# include<ctype.h>// 实现一个自己的简单的shell
#define NUM 1024
#define OPTION_NUM 32// 定义重定向状态
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3 char lineCommand[NUM]; // 大小是NUM,这里是全局变量
char* myargc[OPTION_NUM]; // 指针数组,用于存在字符串切割后的选项
int redirtype = NONE_REDIR; // 设置重定向状态
char* redirFile = NULL;// 定义一个宏函数,用于过滤多个空格
#define trimSpace(start) do{\while(isspace(*start)) ++start;\}while(0) // 不需要加; 当放到条件时也不会报错void CommandCheck(char* command)
{// 先判断是不是重定向,是什么重定向assert(command != NULL);char* start = command;char* end = command + strlen(command); // 指向末尾while(start < end){if(*start == '>'){// 可能是追加,也可能是输出重定向*start = '\0'; start++;if(*start == '>'){// 追加重定向redirtype = APPEND_REDIR;start++; }else{// 输出重定向redirtype = OUTPUT_REDIR;}trimSpace(start); //找到文件redirFile = start;break;}else if(*start == '<'){// 输入重定向*start = '\0'; // 左边为指令,右边为文件start++;trimSpace(start); // 过滤所有空格,直到碰到文件redirtype = INPUT_REDIR;redirFile = start;break;}start++; // 迭代}// 走出来就是没有重定向,但是redirtype初始化就是NONE_REDIR
}int main()
{while(1){// 每次进来都要重置有关重定向的变量,redirtype, redirFile redirtype = NONE_REDIR;redirFile = NULL; // shell就需要有一个输出提示符printf("wzf@主机名 当前路径# "); // 这里不需要\n,输入指令是在#后面的fflush(stdout); //没有\n 用fflush刷新缓冲区//获取用户的命令char* s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);assert(s != NULL);// 用户每次输入完一个命令之后,都会输入一个回车键作为结尾lineCommand[strlen(lineCommand)-1] = 0; // 将输入abcd\n中的\n给取消掉了//printf("receive : %s\n", lineCommand); // 测试是否正常接受到用户的指令// 判断用户是否执行的是重定向指令,三种情况【输出、追加、输入重定向】CommandCheck(lineCommand);// 判断完之后,若有重定向,则做出处理,让子进程去处理// 将用户输入的指令,判断是否带选项,带选项的话要切割并放进myargc中// "ls -a -l" 要切割成 "ls" "-a" "-l"myargc[0] = strtok(lineCommand, " "); // myargc的第一个选项是要执行什么命令//切割该命令所带的选项//myargc[1]开始装选项,这里myargc[end]必须是NULLint i = 1;// 这里要注意,如果要继续在已经被切割的原来的字符串上面继续切割,就要传一个NULLwhile(myargc[i++] = strtok(NULL, " ")); // 切到不能切了就返回null,刚好循环停止,这个循环一直检测myargc[i++]// cd命令的处理【这种不需要子进程执行的命令,我们叫做内建命令】if(myargc[0] != NULL && strcmp(myargc[0], "cd") == 0){// 如果是cd命令,那么一定会跟一个路径,因此myargc[1]一定不为空if(myargc[1] != NULL)chdir(myargc[1]); // 更改父进程当前路径continue; //直接结束本次shell解析,进行下一次,cd目的已经完成}// 为了测试是否正确切割并放入myargc,这里可以用条件编译
#ifdef DEBUGfor(int i = 0; myargc[i]; i++){printf("myargc[%d]: %s\n", i, myargc[i]);}
#endif//此时已经处理好,用户所输入的指令了,这个时候就要去执行命令// 执行命令可以用进程的程序替换pid_t id = fork();assert(id != -1);if(id == 0){// child// 程序替换的函数选择execvp,环境变量用系统的,将处理好的命令直接传给execvp即可// 这样只用传要执行什么指令,以及其指令所带的选项即可// 判断是否有重定向,是什么重定向, 这里的重定向不会影响到父进程`:`switch(redirtype){case NONE_REDIR:break;case INPUT_REDIR:{int fd = open(redirFile, O_RDONLY, 0666);if(fd < 0){perror("open error");exit(1);}// 此时获得了要重定向的文件的文件描述符fddup2(fd, 0); // 输入重定向}break;case OUTPUT_REDIR:case APPEND_REDIR:{// 输出和追加都在这里处理int flags = O_WRONLY | O_CREAT; // 不管是输出还是追加都要这两个选项if(redirtype == APPEND_REDIR)flags |= O_APPEND;elseflags |= O_TRUNC;int fd = open(redirFile, flags, 0666);if(fd < 0){perror("open error");exit(1);}// 成功打开重定向文件,获得fddup2(fd, 1); // 1文件即为标准输出流,显示器}break;default:printf("redirtype error\n");break;}execvp(myargc[0], myargc);exit(0);}//parentint status = 0;pid_t ret = waitpid(id, &status, 0); // 这里传0.使用阻塞等待方式assert(ret != -1);if(ret > 0){//等待成功,子进程资源被回收。子进程的退出信息已获取// 如果想要打印出来,在该分支打印即可}}return 0;
}

测试重定向的结果如下:

image-20250301170821210

image-20250301171211162

这样就算完成一个踉踉跄跄的shell了,不嫌弃也是能跌跌撞撞的跑一下的,hhh

9.理解Linux中一切皆文件

首先我们知道,在计算机体系中是存在很多硬件/设备的,比如磁盘、网卡、键盘等等。

而Linux为了更好的管理每一个设备,仍然是先描述、后组织,将每一个设备都描述为一个结构体,并为其准备了各自的读写函数【读键盘和读磁盘的函数肯定不能一样,不同设备就要用不同的函数】

image-20250302095640333

当然,像有些设备,如键盘,天然不带写入属性,那也不妨碍Linux为其提供写函数,这个写函数为空就可以了,显示器的读函数同理

那为什么说Linux中一切皆文件呢?这仅仅只是将其描述起来,封装成一个结构体/类、

因为在Linux中,会做一个操作,用struct file 来表示所有要使用的设备。将设备看成一个文件来使用

通过往struct file里面描述文件的属性,比如类型,状态,还有指向对应读写函数的函数指针

image-20250302102127739

当然这个struct file里实际上肯定不止这些东西。但是会有如上图的这么一个操作

因此站在struct file的上层来看,它并不关心我使用的到底是什么文件,这个struct file里面描述的是什么设备还是其他文件,它只会调用这个struct file。具体调用了什么文件是通过struct file内的描述,和函数指针实现的

image-20250302102513543

这个过程就和c++的多态是类似的

10.引用计数

在Linux当中,关闭文件就用到了引用计数

基本的理解就是,当一个空间同时被多个指针所指向的时候,此时会有一个变量count用来统计该空间被多少指针指向,当一个指针取消指向的时候,count–,直至count为0,即没有指针指向该空间,才会释放该空间

在上面我们让子进程去完成文件操作的过程中,其实就用到了引用计数,子进程会以父进程为模版进行拷贝,因此子进程的struct files struct也是拷贝父进程的,那就意味着子进程的文件描述符表,也是和父进程一样的,指向的都是同一个文件。因此在子进程当中关闭文件,并不是真正的关闭了文件,而只是让该文件的引用计数–了一次。

因此打开文件自然也就是让该文件的引用计数++一次

Linux当中的文件系统是非常复杂的,自顶向下看,哪怕是IO过程,也是非常复杂的。有关底层的硬件如何与系统进行交互,这个过程也是非常复杂的。Linux发展至今,已经有了上千万行代码,都是由世界顶级工程师编写的,因此学完是不太可能的。

11.缓冲区的理解

11.1缓冲区的本质

缓冲区的本质就是一段内存!【这样听着其实并没发很好理解,还是要结合例子】

来看一段代码,理解一下缓冲区:

#include <stdio.h>
#include <string.h>
#include<unistd.h>// 理解缓冲区的位置和刷新策略  int main()
{const char *msg0="hello printf\n";const char *msg1="hello fwrite\n";const char *msg2="hello write\n";printf("%s", msg0);fwrite(msg1, strlen(msg0), 1, stdout);write(1, msg2, strlen(msg2));fork();return 0;
}

代码的执行结果和重定向后的输出结果如下:

image-20250302125243871

为什么会刷新两次呢?这就和缓冲区有关了

  • 首先我们要搞清楚为什么要有缓冲区,缓冲区存在的意义是什么?

我们知道,在冯诺依曼计算机体系结构中,任何数据要要传输和交互,都要加载到内存中,这是因为数据在内存的传输的速度是非常快的,当执行代码的时候,突然要与外设交互,比如磁盘,那么数据的传输速度就会突然掉下来,那难道要进程一直等待吗,并不会,此时内存会额外开辟一块空间,将要向外设传输的数据拷贝到这个空间中,然后继续执行进程后续的代码,让那块空间不断向外设传输。【这个空间就叫做缓冲区!它存在的意义就是节省进程进行数据IO的时间】

  • 上面对缓冲区的理解中,出现了一个拷贝关键字,但是实际上操作好像并没有拷贝函数的出现?

并不是,实际上拷贝函数就是fwrite,它会将要写入的数据,拷贝到"缓冲区"中,最终数据再从缓冲区,拷贝到外设中

  • 但是将数据拷贝到缓冲区中,什么时候缓冲区会将数据拷贝到外设中呢?

这个时候就要理解缓冲区的刷新策略

实际上,为什么数据传输到外设速度会变慢,原因就出在等待外设是很慢的。如果说将一份数据传入到外设要花的时间是一1s,那么有99%的时间都是花在和等待外设(和外设建立连接),而不是花在将数据输入到外设

我们站在缓冲区的角度来理解一下,下面是缓冲区会根据实际情况而考虑的三种策略,和两种特殊情况

  1. 立即刷新————无缓冲——数据不会停留在缓冲区,到了缓冲区立马就传输给外设【很少使用这个策略,效率太低了】
  2. 行刷新——行缓存——显示器就是这个策略,一行一行的刷新【碰到\n就会刷新】
  • 但是这里似乎有个悖论,明明将缓冲区写满,再一次传输给显示器的方式,IO的效率是最高的,为什么这里要采用行缓冲呢?

  • 这是因为显示器是给人看的,人的阅读习惯就是从左到右一行一行的阅读,如果一次写满缓冲区并且输出到屏幕上的话,对于人来说就是突然出现一大堆东西,阅读起来非常痛苦,因此对于显示器来说,要采用行缓冲的策略

  1. 全缓冲——写满一个缓冲区在向外设传输数据——对磁盘传输数据就是这个策略

两种特殊情况:

  1. 用户强制刷新

用户自己定义了一套刷新缓冲区的规则,这个时候OS只能遵守用户的缓冲区刷新策略

  1. 进程退出

在进程退出的时候,一般都会进行缓冲区的刷新

11.2缓冲区在哪里?

缓冲区在内存上这是物理层面的,但是在OS看来,缓冲区在哪里呢?

从上面那个例子的现象来看,这个缓冲区一定不在内核上,因为如果在内核上面的话,write的内容也会被输出两次才对。因此,这里所说的缓冲区是位于语言层面上给用户提供的的缓冲区

这个时候可以回过头来看,在之前进行文件操作的时候,出现的标准输入输出流<stdin, stdout,stderr>.

都是FILE* 的类型,这个FILE我们之前说过它是一个结构体,里面封装着fd文件描述符。现在我们就来了解一下FILE这个结构体类型【其实缓冲区就在FILE里面】

11.2.1FILE

其实缓冲区在FILE里面也是可以猜测到的。

之前我们再实现进度条的时候,为了刷新缓冲区,就是用的fflush函数,这个函数要传一个FILE*文件指针,因为这样才能找到缓冲区,然后将其刷新

也就是说,之前所有的C语言的操作,fflush,fwrite等函数,最终都会将要输出的数据,放入FILE中的缓冲区中

来看看FILE 结构体的内部代码:

/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 0int _blksize;
#elseint _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
};

11.3总结

将例子中的代码再次拿出来看:

#include <stdio.h>
#include <string.h>
#include<unistd.h>// 理解缓冲区的位置和刷新策略  
int main()
{const char *msg0="hello printf\n";const char *msg1="hello fwrite\n";const char *msg2="hello write\n";//C接口printf("%s", msg0);fwrite(msg1, strlen(msg0), 1, stdout);//系统接口write(1, msg2, strlen(msg2));fork();return 0;
}

综上对缓冲区和FILE的理解,我们就可以对上述例子中出现的,printf和fwrite重定向时输出两次,write不受影响的现象做出一个解释了

  1. 为什么不重定向的时候,输出到屏幕就是正常的?

因为stdout默认是使用行刷新,一行一行的刷新,因此在执行到fork函数之前,就已经将缓冲区的数据刷新到屏幕上了,因此子进程进行对父进程的拷贝的时候,没有数据被拷贝。也就是正常输出

  1. 为什么重定向的时候,C接口的数据输出了两遍?

因为当进行重定向的时候,位于缓冲区的数据不再向显示器输出,改成了像磁盘中的文件输出,这就意味着刷新策略由行刷新改成了全缓冲。因此,数据不写满缓冲区是不会刷新的。所以执行到fork函数的时候,数据仍然位于缓冲区。而当这个进程结束,准备退出的时候,就会将缓冲区里的所有内容全部刷新。而刷新缓冲区就是修改数据,因此子进程会发生写时拷贝,父子进程不再共用一个缓冲区,而是各自有一个,因此子进程和父进程的FILE中的缓冲区的数据全部都刷新出去了。

  1. 为什么重定向的时候,系统接口的数据,只输出一遍

因为系统接口的write,不会将数据放到FILE中的缓冲区中,跟FILE根本就无关。write的返回值是文件描述符fd

综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。

那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供

12.通过实验加深缓冲区理解

为了更好的理解缓冲区和不同的缓冲区的刷新策略下,这里我会自己封装fopen,fwrite,和fclose接口,来更好的理解缓冲区的不同策略下的现象

这里我会用2个源文件和一个头文件来编译出一个可执行文件

实验代码:

Makefile内容如下:

main:main.c mystdio.cgcc -o $@ $^ -std=c99
.PHONY:clean
clean:rm -f main

mystdio.h的内容如下:

#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<errno.h>#define SIZE 1024
#define SYNC_NOW 1 // 无缓冲
#define SYNC_LINE 2 //行缓冲
#define SYNC_FULL 3 // 全缓冲typedef struct MY_FILE{int flag; //刷新方式int fileno;int cap; //buffer's capacityint size; char buffer[SIZE]; // 缓冲区
}MY_FILE;MY_FILE* _fopen(const char* path_name, const char* mode);
void _fwrite(const void* ptr, int num, MY_FILE* fp);
void _fflush(MY_FILE* fp);
void _fclose(MY_FILE* fp);

当然,这里对接口的模拟不完全,但这个不是重点,重点是加深对缓冲区的理解

mystdio.c文件:

MY_FILE* _fopen(const char* path_name, const char* mode)
{int flags = 0;if(strcmp(mode, "r") == 0){flags |= O_RDONLY;}else if(strcmp(mode, "w") == 0){flags |= O_WRONLY | O_CREAT | O_TRUNC;}else if(strcmp(mode, "a") == 0){flags |= O_WRONLY | O_CREAT | O_APPEND;}else{// 其他情况不处理}int fd = 0;// 要注意open在创建文件的时候要传入起始权限if(flags & O_RDONLY)fd = open(path_name, flags);elsefd = open(path_name, flags, 0666);if(fd < 0){const char* err = strerror(errno);write(2, err, strlen(err));return NULL; //fopen打开文件失败会返回NULL}MY_FILE* fp = (MY_FILE*)malloc(sizeof(MY_FILE));assert(fp != NULL);//fp->flag = SYNC_NOW; //默认设置成无缓冲fp->flag = SYNC_LINE; //默认设置成行缓冲fp->fileno = fd;fp->cap = SIZE;fp->size = 0;memset(fp->buffer, 0, SIZE);return fp;
}void _fwrite(const void* ptr, int num, MY_FILE* fp)
{// 先将数据写入缓冲区memcpy(fp->buffer+fp->size, ptr, num); // 这里不考虑缓冲区溢出的情况fp->size += num;// 这里就要判断当前的刷新策略是什么if(fp->flag == SYNC_NOW){// 无缓冲write(fp->fileno, fp->buffer, fp->size); // 将缓冲区的内容输出到fp当中找到的指定文件fp->size = 0;}else if(fp->flag == SYNC_FULL){// 全缓冲if(fp->cap == fp->size){// 当缓冲区满了之后,就进行刷新write(fp->fileno, fp->buffer, fp->size);fp->size = 0;}}else if(fp->flag == SYNC_LINE){//行缓冲if(fp->buffer[fp->size - 1] == '\n') // 不考虑abcd\naaa 的这种情况{write(fp->fileno, fp->buffer, fp->size);fp->size = 0;}}else{// 有问题}}void _fflush(MY_FILE* fp)
{assert(fp);write(fp->fileno, fp->buffer, fp->size);fp->size = 0; fsync(fp->fileno); //强制OS将数据写入到文件中
}void _fclose(MY_FILE* fp)
{assert(fp);// 关闭文件之前要把缓冲区给刷新_fflush(fp);close(fp->fileno); 
}

main.c文件:

# include"mystdio.h"int main()
{MY_FILE* fp = _fopen("text.txt", "a"); assert(fp);const char* s = "hello, hello";//_fwrite(s, strlen(s), fp); //直接写入//测试行缓冲 [s最后没有\n]for(int i = 0; i < 5; i++){_fwrite(s, strlen(s), fp);sleep(1);}// 这个测试的现象就是,由于不带\n,因此位于缓冲区的s,只会在进程退出的时候,刷新缓冲区// 所以五秒之后,数据才会从缓冲区写到文件中_fclose(fp);return 0;
}

实验现象

测试无缓冲:

image-20250303124854516

测试行缓冲:

image-20250303124843108

结果如下:
image-20250303124831692

13.内核缓冲区

前面一直在说,我们要向一个文件写入一些数据的时候,是先写到用户级缓冲区,然后用户层缓冲区通过不同的刷新策略,将数据从缓冲区写到文件中。

但是实际上,并不是直接从用户级缓冲区写道外设当中的,而是先写到内核缓冲区中,然后由OS的刷新策略,OS自主决定什么时候向外设写入数据【这个内核缓冲区的刷新策略非常复杂,比用户级的缓冲区的刷新策略复杂的多】

image-20250303124808920

13.1fsync函数

尽管OS很靠谱,交给它的事情基本上都能给我们完成,但是如果出现了意外呢?比如当数据从用户级缓冲区拷贝到内核缓冲区的时候,此时突然断电,数据还没有写入到外设中,此时就可能会发生数据丢失

因此为了避免这种情况的发生,Linux提供了一个接口给我们,fsync,强制将内核缓冲区的数据写入外设中

因此上面实验中的_fflush代码要稍微改一下:

void _fflush(MY_FILE* fp)
{assert(fp);write(fp->fileno, fp->buffer, fp->size);fp->size = 0; fsync(fp->fileno); //强制OS将数据写入到文件中}

14.理解文件系统

前面我们所讨论的文件,都是被打开的文件。也就是说,都在谈论一个文件是如何被打开的,是如何写入数据的,是如何重定向的…

言外之意,都是一个被打开的文件是如何被OS管理起来的

但是一个OS的文件是非常多的,除了被打开的文件,肯定也有未打开的文件。那这些未打开的文件,OS是如何管理的呢?

未打开的文件其实就是在磁盘中待着,但是是随便放的吗?OS随便在磁盘找个位置,把文件往里一丢,就不管了?并不是的

未打开的文件也是要被静态管理的!

就像在学校,当学生在床上睡觉的时候就是未打开的文件,学生去上课就是打开的文件,但是学校仍然会将学生的床位安排好,管理起来。这样才方便找到这个学生。文件也是一样,未打开的文件,也要被OS静态管理好,按照安排好的路径去存放,这样下次OS要找到这个文件想打开它,才能找得到、不然文件那么多,随便放的话,想找也找不到

上述说的未打开文件的静态管理,就叫做文件系统。当然被打开的文件是如何管理的,也叫做文件系统

14.1磁盘的结构

但是这样说文件系统好像还是有点不太清楚,要想更好的理解文件系统,就要知道底层——磁盘的结构

分为三个角度讲:

磁盘的物理结构

先看下图:

image-20250303144002318

磁盘的两个盘面(光滑),都可以进行读写,并且是以二进制的形式存储数据的

磁盘的存储结构

如果将一个盘放大,那么会发现一个一个的同心圆,看起来是一个光滑的平面,实际上就是由一圈一圈的同心圆组成的,这个同心圆就叫做磁道,一个扇形区域对应的就是一个扇区

看下面这张图片

image-20250303183819269

磁盘在寻址的时候,并不是一个字节一个字节的寻找,基础单位不是字节,也不是bit,而是512个字节。

这是因为磁盘读取数据相对于内存来说很慢,不能像内存一样1byte,1byte的去编址,查询。

而是要划分为一个个的块状结构来读取,一次读取一个块状结构,这也是为什么磁盘被叫做块设备,当然不仅仅是因为这个原因他被叫做块设备

每一个扇区存储的都是512字节,虽然大小不一样,但是电子的密度不一样,所以每个扇区存储的数据大小都是512字节【注意:确实存在每个扇区存储字节不一样的磁盘】

那作为磁盘,要如何定位一个扇区在哪里呢?

答案是通过磁道,来确定一个扇区的位置

先确定该扇区位于那个磁道,而磁道一开始就会有编号,来记录磁道的位置,因此就可以通过磁道来确定扇区的位置

那磁盘如何定位到磁道呢

通过磁头的来回摆动来确定磁道,而盘片在旋转的时候,就是让磁头定位到扇区

一个磁盘可能会有多个盘面,就会有多个磁头,这些磁头并不会像手指一样各自干各自的,而是共进退的

这就意味着,当磁盘在找一个扇区的时候,是所有磁头在所有的盘面上,找位于一个柱面上的磁道,并配合盘面的高速旋转,让所有磁头能快速的扫过一个柱面的所有扇区。从而提高寻找扇区(数据)的效率

这个方法叫做CHS定位法

磁盘的逻辑结构

image-20250303205316663

磁盘的物理结构看起来其实是和磁带有点类似的,磁带将一个长条卷成了一圈圈的圆,就类似磁盘的同心圆,即磁道。因此我们可以将磁盘的逻辑结构,看成是一个线性的结构

image-20250303212217038

因此,我们可以将磁盘的逻辑结构看做为一个数组!

image-20250303214324708

经过这样的处理之后,我们对磁盘的管理,转变为了对数组进行管理!

这个思维就是——先组织后管理

既然如此**,定位扇区,就转变为了扇区的下标的寻找!而这个扇区的下标,在OS的内部,称之为LBA地址(Logical Block Address)——逻辑块地址,又叫线性寻址**

并且这个下标范围也是非常好确定的,如下图所示:

image-20250303220554613

但是尽管将定位扇区,转化成了对数组下标的寻找,此时我们拿着LAB地址为——123,也就是说该扇区的下标为123,那么磁盘要怎么去找呢?物理上磁盘仍旧是圆形,仍旧需要按照CHS定位法,去定位扇区。

那么这个LAB地址是怎么转化成CHS的定位方案呢?

其实很简单,要从LAB地址找到扇区对应的物理磁盘上的位置

顺序就是先定位在几号盘面,在定位在几号磁道,在定位在几号扇区,就拿123号LAB地址来定位,如下图所示:

image-20250303223026797

这样就从LAB地址转化成物理上磁盘所能定位的CHS地址了,就可以直接定位到具体的扇区了

那为什么OS进行逻辑抽象呢?为什么不直接用CHS呢?

  • 更好的管理【这是先描述后组织的目的】
  • 不想让OS的代码和硬件强耦合

什么意思呢?就是今天要交互的数据可能不在磁盘中,可能在其他的外设中,因此OS统一将数据的存储位置看成LBA地址,然后再由LBA地址根据不同的硬件去寻找数据的位置

而将LBA地址转化为CHS定位,然后交给磁盘去定位该扇区,磁盘定位的过程,此时进程就需要将自己由运行状态,设置为阻塞状态,在Linux中就是睡眠状态。等到磁盘定位到该扇区之后,才能进行数据的交互

14.2文件系统和内存的耦合

尽管磁盘一次访问的基本单位是512字节(扇区),但是仍然很小,本身磁盘的等待对于OS来说就很耗费时间,因此OS的文件系统会定制的进行多个扇区的访问,一次性以1KB、2KB、4KB为基本单位!

哪怕我只想修改1bit的内容,但是OS仍然会读取一个基本单位——4KB(8个扇区)的内容Load到内存中,并进行修改等操作,然后再将剩下的内容写回磁盘【这个做法是以空间换时间的做法】

之前我们谈过内存其实被管理成一个个4kb组成的,叫做页框,如果一个内存4GB的话,就有4GB/4KB个页框。而磁盘中的文件,尤其是可执行文件,也是按照4kb分好一个个的数据块,叫页帧,方便load到内存中

14.3 文件系统与磁盘(理解Inode)

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

image-20250304104311507

而前面我们说过,文件 = 文件属性 + 文件内容,这里会有个重要的概念出现,那就是Inode

文件的属性就是存放在Inode中的!如下图所示:

image-20250304104227368

要注意这个Inode 大小是固定的,至于多大则和文件系统的版本相关,Linux ext2版本通常为128字节

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

[root@localhost linux]# stat test.cFile: "test.c"Size: 654         Blocks: 8          IO Block: 4096   普通文件
Device: 802h/2050d  Inode: 263715      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800

而为了更好的理解Inode,需要先理解一下文件系统与磁盘的关系

一个磁盘是很大的,哪怕挑一个小的磁盘,那也有512GB,前面说了,OS会将其逻辑进行抽象,将磁盘看作一个线性结构的数组,并且将数组的每个元素对应一个个扇区。

在OS看来,它会先分区然后在分组。如果管理好一个组,那么一个区的的所有组都可以管理好,如果一个区能管理好,那么所有区都能管理好,也就是说该磁盘就能管理好了。

而这个组是什么结构?如何管理的呢?如下图所示:

image-20250304100405749

Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。【其实就是分区之后再分组】,一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,

【自己在电脑上对硬盘进行分区之后的格式化的本质其实就是写入文件系统!】

下面来看看这些分组都装了些什么

  • Boot Block:这个是跟开机有关系的【了解即可】

  • lock group 0 【这个是第0组,组内有很多块部分】

重点要学习的部分,已经加粗了

  • 超级块(Super Block):里面装着整个文件系统的信息

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

这个Super Block 每个分组都有一个,并且装的内容都是一样的,这样是为了防止某一组的Super Block坏了,而导致文件系统损坏,导致整个分区的数据都损坏。

  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用

一个文件被创建时需要找到一个未使用的inode将文件属性保存起来,自然一个文件如果想要保存数据的时候,自然也要找到在数据区找到未使用的数据块来使用,而判断一个数据块是否被使用,就通过块位图来实现

块位图也是通过比特位来判断某个数据块是否被使用的,1表示已使用,0表示未使用

  • inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用

之所以会存在这个inode位图,就是因为当你创建一个文件的时候,首先要到inode table当中找到一个当前分组可用的inode,而通过inode位图就可以找到一个inode是否未使用。

inode位图判断一个inode是否可用也很简单,假设该组所有的inode一共有1000个,那么这个inode位图就需要有1000个比特位,从右往左,每一个比特位表示一个inode,如果某位的比特位是1,那么表示该inode被使用了,比特位为0,表示未使用

  • 节点表(inode Table):这个表里面存放着这个分组内部所有的可用的inode【这个可用包括已经使用的和没有使用的】

【inode Table的大小是inode的个数 * inode的大小【通常128kb】】

  • 数据区:保存该分组内部所有的数据块,单位:4KB/块【该区域的大小是可变化的,随着装的数据量越多就越大】
理解文件系统如何将属性和数据分开存储又能联系起来

经过上面对文件系统的了解,我们已经知道并理解文件有且仅有一个inode,且inode带编号,inode放属性,data区放数据了。

  • 那现在就可以来解决一个问题:在OS中是如何查找一个文件的呢?

首先要先拿着该文件的inode编号,在inode Bitmap中找到该inode编号是否为1,是为1的话记住这个偏移量,然后在inode Table中找到该inode,这样就可以找到该文件的所有属性了

  • 但是此时仍旧有一个问题,将文件的属性和数据分开存放,属性是能找到,但是数据如何找到呢?虽然知道数据存放在数据区,但是数据区有很多的数据块,哪个数据块保存该文件的数据又如何知道呢?

通过inode内部存放的数据块列表实现的,可以认为他是一个数组,类似于文件描述符表,每个元素都指向一个自己所使用的数据块。

image-20250304124041508

但是又有一个问题,当前数组的长度只有16个元素,下标是0~15,难道只能放16个数据块,也就是60kb的数据吗,一个文件当然不止放60kb,那是如何实现的呢?

其实就是一个数据块不止能用来存储文件数据,还可以用来存其他数据块的编号!。如下图所示:

image-20250304124948474

也可以算一下:一个数据块4kb,假设一个数据块id大小为4byte,那么一个数据块可以放1024个数据块id,那么这样就可以存下4096kb = 4Mb的数据了!

如果要存放更多数据,就继续套娃就行了。而这个套娃不是在下标12这个位置套的。是交给下标为13的数据块去解决的,下标为13指向的数据块存放的是二级索引,它指向的数据块所存放的数据块id所指向的数据块,依然存放其他数据块的id!

继续算一下:二级索引可以存下4Gb的数据

以此类推,下标14存放的就是3级索引,15存放的就是4级索引!所以根本存不完!

  • 那删除文件怎么办呢?

**可以说,在任何文件系统中,删除文件采用的都是惰性删除!**即直接将该文件的inode编号和Block data编号置0!

这也就是为什么删除一个文件能够恢复的原因!因为数据其实没有被真正的删除,只要能够找到删除文件的inode编号,和数据块编号,将其重新置为1,该文件就恢复了!

所以当一个文件被误删的时候,最好的操作就是除了恢复操作之外什么也别做!一旦创建了新文件,让新文件复用了误删文件的数据区编号或者inode编号,那就真的被删除了!

  • 但是还有一个问题,我们使用Linux至今,一直使用的都是文件名而不是inode?

要弄清楚这个问题,得先知道所有文件都在某个目录中,而目录也是文件,他也有自己的inode和数据块,那目录的数据块放的是什么呢?

放的就是当前目录下文件名和inode的映射关系!

这也就是为什么一个目录下不能存在两个相同的文件名,因为这样做的话,inode和文件名的映射关系就不唯一了!

总结:

image-20250304130627471

创建一个新文件大概会有四个步骤:

  1. 首先根据所在目录确定所在分区,然后再依次从各个分组中的inode table中是否存在可用inode编号,如果存在就去inode位图判断该inode是否被使用,不存在就去下一个组的inode table找
  2. 拥有了inode编号后,就往inode中存放该文件的属性,然后开始存储数据
  3. 在数据块位图中找到未被使用的数据块,然后存放该文件的data,并让被使用的数据块编号存放到inode中的数据块列表
  4. 最后添加文件的名字和inode到当前目录的inode/名字映射关系表

15. 软硬链接

15.1软硬链接的区别

软硬链接的区别如下:

输入ln创建硬链接,输入ln -s创建软链接

image-20250305001927832

这里最重要的区别就是,是否具有自己的inode。软链接就有自己的独立inode,是一个独立文件。而硬链接没有,换言之就是创建硬链接的时候根本没有创建新文件

  • 那如何理解硬链接?

由于建立硬链接并没有新建文件,所以硬链接用的就是被链接文件的文件属性(inode)和数据(data)。

所以创建硬链接的本质——在当前目录下,将该硬链接的文件名,与被链接文件的inode编号做了一个映射关系。直接通过别人的inode编号,找到别人的inode,使用的也是别人的inode和数据

image-20250305005226063

也就是说,一个inode很有可能对应多个文件名,因此inode内部会有一个计数器count,专门用来统计被多少个文件名。这个做法叫做引用计数,前面在讲文件的时候有讲到,一个文件会通过引用计数来统计自己被多少个指针指向

image-20250305010518437

并且在删除掉其中一个硬链接的时候,对其他硬链接不会有影响

image-20250305010807740

这是因为其他硬链接仍然与inode编号建立映射关系

**因此,如果想要真正的删除一个文件,就必须让一个文件没有硬链接!**在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。

上述的例子,我们给myfile.txt建立一个硬链接hard_file.txt,又将myfile.txt删除,剩下了一个hard_file.txt,最终效果好像是实现了一个文件的重命名?

其实不是的,硬链接的目的是:

  • 那如何理解软链接呢?

软链接有自己的独立inode,是一个独立文件,有自己的属性和数据

image-20250305084154145

软链接在删掉myfile.txt的时候显示文件被删除,无法找到该文件。但是实际上这个文件并没有被真正删除,inode有了其他硬链接

这说明软链接找到文件的方式是通过记载myfile.txt文件的路径! 因此删掉myfile.txt的时候,软链接无法找到该文件

这个软链接其实就相当于window系统下的快捷方式

15.2软硬链接的应用

  • 软链接的应用:

软链接的应用很简单,就是快捷方式

如果一个可执行文件位于一个很深的目录下,那么每次执行它都要带上一长串的路径,还是比较麻烦的。如果有了软链接,就可以在其他路径下执行这个可执行文件了!

  • 硬链接的应用:

要弄清楚这个,我们需要先来了解一下为什么目录刚被创建出来的inode1451743的硬链接数是2?

image-20250305090329720

这是因为在隐藏文件中有一个当前路径的存在,这个.文件的inode和empty目录的inode是一样的,说明.文件就是empty的硬链接!

而…的inode1451642的硬链接数是3也很好理解,因为…是上一层目录的硬链接

因此,硬链接的其中一个应用场景我们就已经理解了

16.动态库静态库

对于动态库和静态库,其实在之前讲Linux的gcc\g++编译器的时候提到过一会Linux工具的使用——【gcc/g++的使用】【make/Makefile的使用】

当时还没有学进程地址空间,因此没有学的很透彻,只是了解了一下静态链接和动态连接的区别,现在来深度的学习一下动态库和静态库

先来了解一些概念:

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

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

  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码

  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)

  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间

16.1库的本质

我们站在两个角度来讨论一下动静态库的本质:使用库的人的角度和编写库的人的角度

首先来一段代码:

测试程序
/add.h/
#ifndef __ADD_H__
#define __ADD_H__ 
int add(int a, int b);
#endif // __ADD_H__
/add.c/
#include "add.h"
int add(int a, int b)
{return a + b;
}
/sub.h/
#ifndef __SUB_H__
#define __SUB_H__ 
int sub(int a, int b);
#endif // __SUB_H__
/add.c/
#include "add.h"
int sub(int a, int b)
{return a - b;
}
///main.c
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main(void)
{int a = 10;int b = 20;printf("add(10, 20)=%d\n", a, b, add(a, b));a = 100;b = 20;printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
}

image-20250305230720040

如果一个使用者它想使用一个库的函数,那么我只需要有一个头文件,如上述例子的.h头文件,我就可以使用你的函数。【库里面是不需要有main函数的入口的】

一个库的编写者,我不想将自己的源代码暴露出去,那么采取的策略就是将头文件和编译好的.o文件,即可重定位目标二进制文件,交给你,让使用者去链接从而生成可执行文件。

但是如果库内的函数特别多,比如几千个,那库会给使用者几千个.o文件交给库使用者去使用吗?当然不是的,这样对库的使用着也太不友好了

因此,库就会将自己的所有.o文件进行一个打包,交给库的使用者去使用,而这个打包出来的东西就叫做库文件!而库文件的打包方式不同和工具的不同就区分出了动态库和静态库

因此库的本质就是一堆.o文件的集合

16.2静态库和静态链接

知道了库的本质是什么之后,先来尝试建立一个静态库

建立静态库需要用到一个工具 ar , ar是gnu归档工具,rc表示(replace and create)

为了方便,来编写一个Makefile文件先。

libmymath.a:add.o sub.oar -rc $@ $^
add.o:add.cgcc -c add.c -o add.osub.o:sub.cgcc -c sub.c -o sub.o
.PHONY:clean
clean:rm -f *.o libmymath.a

image-20250305230739787

可以看到我们成功创建了自己的静态库,但是此时这个静态库,别人是没有办法使用的,**因为让别人使用自己的库文件,除了要将库文件交付给别人,还要交付相对应的头文件给别人。**别人怎么知道你的头文件是什么叫什么呢

因此还要在往Makefile中添加一些东西:

libmymath.a:add.o sub.oar -rc $@ $^
add.o:add.cgcc -c add.c -o add.osub.o:sub.cgcc -c sub.c -o sub.o.PHONY:output
output:mkdir -p mylib/includemkdir -p mylib/libcp -f *.a mylib/libcp -f *.h mylib/include
.PHONY:clean
clean:rm -f *.o libmymath.a

image-20250305230835893

让静态库和头文件一起打包到一个文件,最好压缩一下,变成一个压缩包给被人下载。这样别人下载下来,使用的时候会自动的将头文件和静态库都带过去

要压缩,得使用tar指令

但是在使用第三方库的时候,要注意以下几个点:

  1. 由于gcc和g++都是官方的编译器,它只认识默认路径下官方的头文件,因此在使用gcc/g++,要加入-I选项输入头文件的路径,让编译器能够找到该库所带的头文件在哪里
  2. gcc/g++找不到第三方头文件,自然也找不到第三方的库,因**此要加入-L选项,告诉编译器第三方库函数路径 + 名字!【注意:只告诉库的路径是不够的,还要告诉编译器库的名字!**不然路径下很多个库怎么办,编译器怎么知道要链接那个库?】

不然就会报错:

image-20250305230753805

  1. 为了让gcc找到库文件,除了要告诉库文件路径,还要加上-l选项,告诉gcc链接那个库文件【要注意:库名字要去掉前面的lib和后缀,中间的才是库名字】

image-20250305230805366

这样编译才能成功,生成可执行文件

但是当我们查看库路径,和可执行文件的链接方式却发现有点奇怪

image-20250305230851494

这是因为gcc默认就是动态链接,它的标准库就是c语言的动态库。

注意:生成一个可执行文件,可能不仅仅只链接了一个库,大致分为3种情况

  1. 如果只提供了一个静态库给他,它实际上采用静态链接的方式链接静态库,
  2. 如果只提供了一个动态库,那么就会采用动态链接链接动态库。
  3. 那如果是7个动态库,三个静态库,那么gcc虽然默认动态链接,但是它会一个个链接库,动态库就动态链接,静态库就采用静态链接

上面图片的情况就是第三种情况,链接了多个库,连接了标准库(动态库),和自己的库。因此实际上链接自己的库的时候,还是采用的静态链接

16.3动态库和动态链接

如何生成动态库

  • shared: 表示生成共享库格式【动态库要用gcc来打包】

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

  • 库名规则:libxxx.so

示例:

[root@localhost linux]# gcc -fPIC -c sub.c add.c
[root@localhost linux]# gcc -shared -o libmymath.so *.o【*.o表示所带的-o文件】
[root@localhost linux]# ls add.c add.h add.o libmymath.so main.c sub.c sub.h sub.o

而动态库的使用和静态库也是一样的,需要将头文件和动态库一起打包起来给使用者使用,并且使用者需要带上-I选项输入头文件的路径, -L选项要加入-L选项,告诉编译器第三方库函数路径,加上-l选项,告诉gcc链接那个库文件

这样就可以成功编译了,生成可执行文件。

但是此时我们会发现一个问题,此时生成的可执行文件是会报错的!

image-20250305233847500

报错的原因是找不到动态库文件

输入ldd就可以看到,此时动态库是被识别到了的,但是无法找到

image-20250305234006850

这是因为,我虽然将动态库的路径和名字告诉了,但是只是告诉了gcc这个编译器。所以程序是可以编译完成的。但是想要将程序执行起来,shell和OS也是需要知道动态库的路径和名字的。

为什么静态链接不需要呢?因为静态链接,直接将静态库拷贝到可执行程序了。直接就能找到,所以不需要

因此解决的方案就是让shell和OS找得到动态库,方法有很多:

  1. echo,将动态库的路径和名字加入系统环境变量中
  2. ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
  3. 在当前路径下新建软链接,链接到动态库文件
  4. 在系统路径下/lib64,建立软链接

16.4动静态库的加载

  • 静态库的加载

静态库一般来说没有所谓的加载,静态库直接将需要的函数拷贝到程序中,这样如果多次调用同一个函数,就可能会造成空间的浪费。这个之前也有提到过

现在有一个新的问题:静态库将程序所需要的函数拷贝到程序中,拷贝到哪里了呢?

其实是拷贝到代码段了,在之前学习进程地址空间的时候,就谈到过程序在编译的时候也会以虚拟地址的方式编址,也会有代码段,数据段,静态区等等。除了堆区和栈区。这个地址叫做逻辑地址!因此静态库拷贝的时候,其实是将程序需要的代码给展开,然后拷贝到程序的代码段

而未来这个程序被load到内存的时候,有了自己的物理地址之后,由于存在页表,因此在内存上的地址没什么要求。但是这个静态库的函数一定是拷贝程序的代码段的

简单理解就是,在生成可执行文件之后,这个静态库的函数在程序中,对于程序来说,就是自己的函数

  • 动态库的加载(重点)

在讲述动态库的加载之前,需要先弄清楚前面生成动态库的一个指令

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

产生位置无关码是什么意思呢?

这里讲一个例子:一个跑道上,一共是100米长。张三和李四在跑步,并且张三永远在李四前面20米。如果了李四刚好跑到50米,那么张三就是70米。这个70米就是张三在这个跑道上的绝对地址!但是无论他们怎么跑,虽然绝对地址会变,但是张三永远在李四前面20米,这个就叫位置无关码!

动态库的加载就是将库函数的地址加载到程序中,这个库函数的地址就是一个相对地址,是与位置无关的。

image-20250306114301677

但是实际上并不是将库函数的start + 偏移量后得到的地址加载到程序中,而是将库函数在库中的偏移量加载到程序中

那这个过程是怎么样的呢?看下图:

image-20250306114343298

还要一个细节,在上图的第六步中,找到printf函数之后是直接调用,而不是拷贝到代码段。因为共享区也是在进程地址空间的,可以跳转,就跟跳转到堆区栈区一样。

执行完printf之后,就继续执行代码段的代码。

至此完成了动态库的加载和执行

注意:

哪怕有100个进程同时调用一个库,内存中也只会加载一个库,OS会给所有进程的共享区映射动态库,然后让每个进程自己去执行。

而静态库如果有100个进程调用,那么就会拷贝100个静态库到程序代码段中,就会占据内存空间。

这样对比下来,就能体现出动态库和动态链接的好处了,节省内存空间

从这里也可以体会到进程地址空间的重要性,进程地址空间可以说是学习OS中相当重要的一个知识。

在以后的学习中,仍然会很频繁的出现的进程地址空间

17.总结

本次一共讲述了四个大话题:

  1. 文件描述符
  2. 文件系统
  3. 软硬链接
  4. 动态库静态库
  • 在文件描述符大话题中,要知道:
  1. 理解文件描述符
  2. 文件是如何被打开的
  3. 理解重定向的原理
  4. 理解语言的文件操作就是封装了OS的文件操作的系统调用
  5. 文件操作的本质?
  1. LAB地址,CHS定位法
  2. 分区分组的意义?
  3. 分组后各个模块的作用和意义
  4. 全面理解inode
  5. inode和data block之间的联系
  1. 软硬链接的区别
  2. 应用场景
  • 静态库动态库要知道:
  1. 库的本质
  2. 动态库静态库的建立
  3. 动态库静态库如何加载,程序如何链接动态库静态库

也是需要知道动态库的路径和名字的。

为什么静态链接不需要呢?因为静态链接,直接将静态库拷贝到可执行程序了。直接就能找到,所以不需要

因此解决的方案就是让shell和OS找得到动态库,方法有很多:

  1. echo,将动态库的路径和名字加入系统环境变量中
  2. ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
  3. 在当前路径下新建软链接,链接到动态库文件
  4. 在系统路径下/lib64,建立软链接

16.4动静态库的加载

  • 静态库的加载

静态库一般来说没有所谓的加载,静态库直接将需要的函数拷贝到程序中,这样如果多次调用同一个函数,就可能会造成空间的浪费。这个之前也有提到过

现在有一个新的问题:静态库将程序所需要的函数拷贝到程序中,拷贝到哪里了呢?

其实是拷贝到代码段了,在之前学习进程地址空间的时候,就谈到过程序在编译的时候也会以虚拟地址的方式编址,也会有代码段,数据段,静态区等等。除了堆区和栈区。这个地址叫做逻辑地址!因此静态库拷贝的时候,其实是将程序需要的代码给展开,然后拷贝到程序的代码段

而未来这个程序被load到内存的时候,有了自己的物理地址之后,由于存在页表,因此在内存上的地址没什么要求。但是这个静态库的函数一定是拷贝程序的代码段的

简单理解就是,在生成可执行文件之后,这个静态库的函数在程序中,对于程序来说,就是自己的函数

  • 动态库的加载(重点)

在讲述动态库的加载之前,需要先弄清楚前面生成动态库的一个指令

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

产生位置无关码是什么意思呢?

这里讲一个例子:一个跑道上,一共是100米长。张三和李四在跑步,并且张三永远在李四前面20米。如果了李四刚好跑到50米,那么张三就是70米。这个70米就是张三在这个跑道上的绝对地址!但是无论他们怎么跑,虽然绝对地址会变,但是张三永远在李四前面20米,这个就叫位置无关码!

动态库的加载就是将库函数的地址加载到程序中,这个库函数的地址就是一个相对地址,是与位置无关的。

[外链图片转存中…(img-XoMGTSQg-1741489796347)]

但是实际上并不是将库函数的start + 偏移量后得到的地址加载到程序中,而是将库函数在库中的偏移量加载到程序中

那这个过程是怎么样的呢?看下图:

[外链图片转存中…(img-XMSsKamS-1741489796347)]

还要一个细节,在上图的第六步中,找到printf函数之后是直接调用,而不是拷贝到代码段。因为共享区也是在进程地址空间的,可以跳转,就跟跳转到堆区栈区一样。

执行完printf之后,就继续执行代码段的代码。

至此完成了动态库的加载和执行

注意:

哪怕有100个进程同时调用一个库,内存中也只会加载一个库,OS会给所有进程的共享区映射动态库,然后让每个进程自己去执行。

而静态库如果有100个进程调用,那么就会拷贝100个静态库到程序代码段中,就会占据内存空间。

这样对比下来,就能体现出动态库和动态链接的好处了,节省内存空间

从这里也可以体会到进程地址空间的重要性,进程地址空间可以说是学习OS中相当重要的一个知识。

在以后的学习中,仍然会很频繁的出现的进程地址空间

17.总结

本次一共讲述了四个大话题:

  1. 文件描述符
  2. 文件系统
  3. 软硬链接
  4. 动态库静态库
  • 在文件描述符大话题中,要知道:
  1. 理解文件描述符
  2. 文件是如何被打开的
  3. 理解重定向的原理
  4. 理解语言的文件操作就是封装了OS的文件操作的系统调用
  5. 文件操作的本质?
  1. LAB地址,CHS定位法
  2. 分区分组的意义?
  3. 分组后各个模块的作用和意义
  4. 全面理解inode
  5. inode和data block之间的联系
  1. 软硬链接的区别
  2. 应用场景
  • 静态库动态库要知道:
  1. 库的本质
  2. 动态库静态库的建立
  3. 动态库静态库如何加载,程序如何链接动态库静态库

http://www.ppmy.cn/server/174037.html

相关文章

2025开源SCA工具推荐 | 组件依赖包安全风险检测利器

软件成分分析&#xff08;Software Composition Analysis, SCA&#xff09;是Gartner定义的一种应用程序安全检测技术&#xff0c;该技术用于分析开源软件以及第三方商业软件涉及的各种源码、模块、框架和库等&#xff0c;以识别和清点开源软件的组件及其构成和依赖关系&#x…

ECC升级到S/4 HANA的功能差异 物料、采购、库存管理对比指南

ECC升级到S/4 HANA后&#xff0c;S4 将数据库更换为HANA后性能有一定提升&#xff0c;对于自开发程序&#xff0c;可以同时将计算和部分业务逻辑下推到HANA数据库层&#xff0c;减少应用层和数据库层的交互次数和数据传输&#xff0c;只返回需要的结果到应用层和显示层。提升自…

【jstack查询线程信息】1.对比下arthas的thread 和jvm指令

1)jps拿到进程号 2)jstack <pid> > <xxx.txt> // jstack作用:分析线程信息,死循环,死锁 jstack 23647 > 23647.txt Found 1 deadlock 3)对比:arthas查看线程信息 [arthas68751]$ thread -n 10 "MainWorker" Id69 cpuUsage72.29% deltaTime156ms …

centos基础知识

系统监控 proc文件系统 proc文件系统是一种无存储的文件系统,当读其中的文件时,其内容动态 生成,当写文件时,文件所关联的写函数被调用。内核部件可以通过该文件系统 向用户空间提供接口来提供查询信息、修改软件行为,因而它是一种比较重要的 特殊文件系统。 大致…

C++ Qt创建计时器

在Qt中&#xff0c;可以使用QTimer来创建一个简单的计时器。QTimer是一个用于定时触发事件的类&#xff0c;通常与QObject的子类&#xff08;如QWidget&#xff09;一起使用。以下是一个完整的示例&#xff0c;展示如何使用Qt创建一个带有计时器的窗口应用程序。 示例&#xff…

大整数加法(信息学奥赛一本通-1168)

【题目描述】 求两个不超过200位的非负整数的和。 【输入】 有两行&#xff0c;每行是一个不超过200位的非负整数&#xff0c;可能有多余的前导0。 【输出】 一行&#xff0c;即相加后的结果。结果里不能有多余的前导0&#xff0c;即如果结果是342&#xff0c;那么就不能输出为…

GitCode 助力 vue3-element-admin:开启中后台管理前端开发新征程

源码仓库&#xff1a; https://gitcode.com/youlai/vue3-element-admin 后端仓库&#xff1a; https://gitcode.com/youlai/youlai-boot 开源助力&#xff0c;开启中后台快速开发之旅 vue3-element-admin 是一款精心打造的免费开源中后台管理前端模板&#xff0c;它紧密贴合…

C++20 模块:告别头文件,迎接现代化的模块系统

文章目录 引言一、C20模块简介1.1 传统头文件的局限性1.2 模块的出现 二、模块的基本概念2.1 模块声明2.2 模块接口单元2.3 模块实现单元 三、模块的优势3.1 编译时间大幅减少3.2 更好的依赖管理3.3 命名空间隔离 四、如何使用C20模块4.1 编译器支持4.2 示例项目4.3 编译和运行…