实验24.创建并挂载文件系统

news2024/11/24 14:44:43

已完成实验

已完成实验链接

简介

实验 24. 创建并挂载文件系统

总结

在这里插入图片描述
在这里插入图片描述

  • 创建文件系统: 初始化每一个分区的结构,把扇区划分为超级块,扇区位图,inode 位图,inode 表,根目录,空闲扇区

  • 挂载分区: 创建一个分区结构体,在内存中存放要挂在的分区的超级块、扇区位图、inode 位图

主要代码

main.c

// 文件: main.c
// 时间: 2024-07-19
// 来自: ccj
// 描述: 内核从此处开始

#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall.h"
#include "syscall-init.h"
#include "stdio.h"
#include "memory.h"

int main(void) {
    put_str("I am kernel\n");

    init_all();
    while (1);

    return 0;
}

init.c

在这里插入图片描述

fs.c

// 文件: fs.c
// 时间: 2024-08-06
// 来自: ccj
// 描述: 文件系统相关
//          1.格式化分区:把分区的超级块、扇区位图、inode位图、inode_table位图初始化
//          2.挂载分区:创建一个分区结构体,在内存中存放要挂在的分区的超级块、扇区位图、inode位图

#include "fs.h"
#include "super_block.h"
#include "inode.h"
#include "dir.h"
#include "stdint.h"
#include "stdio-kernel.h"
#include "list.h"
#include "string.h"
#include "ide.h"
#include "global.h"
#include "debug.h"
#include "memory.h"

struct partition* cur_part;  // 默认情况下操作的是哪个分区

/// @brief 挂载分区链表中找到名为arg的分区
/// @param pelem 分区链表
/// @param arg 分区名
/// @return
static bool mount_partition(struct list_elem* pelem, int arg) {
    char* part_name = (char*)arg;

    // 获取分区指针
    struct partition* part = elem2entry(struct partition, part_tag, pelem);
    if (!strcmp(part->name, part_name)) {  // 找到
        cur_part = part;
        struct disk* hd = cur_part->my_disk;

        // 在内存中创建分区cur_part的超级块
        cur_part->sb = (struct super_block*)sys_malloc(sizeof(struct super_block));
        if (cur_part->sb == NULL) { PANIC("alloc memory failed!"); }

        // 读入超级块
        struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
        memset(sb_buf, 0, SECTOR_SIZE);
        ide_read(hd, cur_part->start_lba + 1, sb_buf, 1);

        // 把读入的超级块复制给内存中cur_part的超级块
        memcpy(cur_part->sb, sb_buf, sizeof(struct super_block));

        // 1. 复制扇区位图
        // 1.1 申请位图内存
        cur_part->block_bitmap.bits =
            (uint8_t*)sys_malloc(sb_buf->block_bitmap_sects * SECTOR_SIZE);
        if (cur_part->block_bitmap.bits == NULL) { PANIC("alloc memory failed!"); }

        // 1.2 复制位图占用字节
        cur_part->block_bitmap.btmp_bytes_len = sb_buf->block_bitmap_sects * SECTOR_SIZE;

        // 1.3 把块位图复制到申请的内存块位图中
        ide_read(hd, sb_buf->block_bitmap_lba, cur_part->block_bitmap.bits,
                 sb_buf->block_bitmap_sects);


        // 2,复制inode位图
        // 2.1 申请位图内存
        cur_part->inode_bitmap.bits =
            (uint8_t*)sys_malloc(sb_buf->inode_bitmap_sects * SECTOR_SIZE);
        if (cur_part->inode_bitmap.bits == NULL) { PANIC("alloc memory failed!"); }
        // 2.2 复制位图占用字节
        cur_part->inode_bitmap.btmp_bytes_len = sb_buf->inode_bitmap_sects * SECTOR_SIZE;

        // 1.3 把inode位图复制到申请的内存块位图中
        ide_read(hd, sb_buf->inode_bitmap_lba, cur_part->inode_bitmap.bits,
                 sb_buf->inode_bitmap_sects);

        list_init(&cur_part->open_inodes);
        printk("mount %s done!\n", part->name);

        // 返回true,list_traversal停止遍历
        return true;
    }
    return false;  // 使list_traversal继续遍历
}

/// @brief 格式化分区,也就是初始化分区的元信息,创建文件系统
/// @param part 分区
static void partition_format(struct partition* part) {
    uint32_t boot_sector_sects = 1;  // 引导块 = 1个扇区
    uint32_t super_block_sects = 1;  // 超级块 = 1个扇区

    // I结点位图占用的扇区数 文件数 / 4096(1个扇区512字节=4096比特)
    uint32_t inode_bitmap_sects = DIV_ROUND_UP(MAX_FILES_PER_PART, BITS_PER_SECTOR);

    // inode表占用的扇区数 总字节 / 512
    uint32_t inode_table_sects =
        DIV_ROUND_UP(((sizeof(struct inode) * MAX_FILES_PER_PART)), SECTOR_SIZE);
    // 已经使用的扇区数
    uint32_t used_sects =
        boot_sector_sects + super_block_sects + inode_bitmap_sects + inode_table_sects;
    // 剩余的空闲扇区数
    uint32_t free_sects = part->sec_cnt - used_sects;

    /************** 简单处理块位图占据的扇区数 ***************/
    uint32_t block_bitmap_sects;
    // 剩余的空闲扇区数的位图所占用的扇区数
    block_bitmap_sects = DIV_ROUND_UP(free_sects, BITS_PER_SECTOR);
    // 空闲扇区数 - 剩余的空闲扇区数的位图所占用的扇区数 = 扇区位图的位数
    uint32_t block_bitmap_bit_len = free_sects - block_bitmap_sects;
    // 扇区位图占用的扇区数
    block_bitmap_sects = DIV_ROUND_UP(block_bitmap_bit_len, BITS_PER_SECTOR);
    /*********************************************************/

    /* 超级块初始化 */
    struct super_block sb;
    sb.magic = 0x19590318;
    sb.sec_cnt = part->sec_cnt;
    sb.inode_cnt = MAX_FILES_PER_PART;
    sb.part_lba_base = part->start_lba;

    // 第0块是引导块,第1块是超级块,第2块开始时扇区位图
    sb.block_bitmap_lba = sb.part_lba_base + 2;
    sb.block_bitmap_sects = block_bitmap_sects;

    // 扇区位图过后是inode位图
    sb.inode_bitmap_lba = sb.block_bitmap_lba + sb.block_bitmap_sects;
    sb.inode_bitmap_sects = inode_bitmap_sects;

    // inode位图过后是inode表
    sb.inode_table_lba = sb.inode_bitmap_lba + sb.inode_bitmap_sects;
    sb.inode_table_sects = inode_table_sects;

    // inode表过后是数据
    sb.data_start_lba = sb.inode_table_lba + sb.inode_table_sects;
    sb.root_inode_no = 0;                          // 根节点的inode
    sb.dir_entry_size = sizeof(struct dir_entry);   目录项大小

    printk("%s info:\n", part->name);
    printk(
        "   magic:0x%x\n   part_lba_base:0x%x\n   all_sectors:0x%x\n   inode_cnt:0x%x\n   "
        "block_bitmap_lba:0x%x\n   block_bitmap_sectors:0x%x\n   inode_bitmap_lba:0x%x\n   "
        "inode_bitmap_sectors:0x%x\n   inode_table_lba:0x%x\n   inode_table_sectors:0x%x\n   "
        "data_start_lba:0x%x\n",
        sb.magic, sb.part_lba_base, sb.sec_cnt, sb.inode_cnt, sb.block_bitmap_lba,
        sb.block_bitmap_sects, sb.inode_bitmap_lba, sb.inode_bitmap_sects, sb.inode_table_lba,
        sb.inode_table_sects, sb.data_start_lba);

    struct disk* hd = part->my_disk;
    // 1 将超级块写入本分区的1扇区
    ide_write(hd, part->start_lba + 1, &sb, 1);
    printk("   super_block_lba:0x%x\n", part->start_lba + 1);

    // 找到三个位图占用的扇区数的最大值
    uint32_t buf_size = (sb.block_bitmap_sects >= sb.inode_bitmap_sects ? sb.block_bitmap_sects
                                                                        : sb.inode_bitmap_sects);
    buf_size = (buf_size >= sb.inode_table_sects ? buf_size : sb.inode_table_sects) * SECTOR_SIZE;

    // 申请一段足够长的内存,来代表位图的情况,最后把buffer复制给位图
    uint8_t* buf = (uint8_t*)sys_malloc(buf_size);

    // 2.初始化块位图block_bitmap并写入sb.block_bitmap_lba
    buf[0] |= 0x01;  // 第0个位预留给根目录,写1表示占用
    uint32_t block_bitmap_last_byte = block_bitmap_bit_len / 8;  // 位图的字节数
    uint8_t block_bitmap_last_bit = block_bitmap_bit_len % 8;  // 位图最后小于1字节的比特数

    // 2.1 先将位图最后一字节到其所在的扇区的结束全置为1,即超出实际块数的部分直接置为已占用
    // block_bitmap_last_byte % 512 = 位图最后不满足1个扇区的字节数
    // last_size是位图最后一个字节到这个字节的扇区尾部的字节数
    uint32_t last_size = SECTOR_SIZE - (block_bitmap_last_byte % SECTOR_SIZE);
    memset(&buf[block_bitmap_last_byte], 0xff, last_size);

    // 2.2 再将上一步中覆盖的最后一字节内的有效位重新置0
    uint8_t bit_idx = 0;
    while (bit_idx <= block_bitmap_last_bit) { buf[block_bitmap_last_byte] &= ~(1 << bit_idx++); }

    // 2.3 将buf写入扇区位图的起始扇区
    ide_write(hd, sb.block_bitmap_lba, buf, sb.block_bitmap_sects);

    // 3 将inode位图初始化并写入sb.inode_bitmap_lba
    memset(buf, 0, buf_size);
    // 3.1 第0个inode分给了根目录
    buf[0] |= 0x1;
    // 3.2 inode位图刚好4096字节 = 512*8,直接写入
    ide_write(hd, sb.inode_bitmap_lba, buf, sb.inode_bitmap_sects);

    // 4 将inode数组初始化并写入sb.inode_table_lba
    memset(buf, 0, buf_size);
    // 4.1 第0个inode分给了根目录
    struct inode* i = (struct inode*)buf;
    i->i_size = sb.dir_entry_size * 2;  // .和.. 两个目录项的大小
    i->i_no = 0;                        // 根目录占inode数组中第0个inode
    i->i_sectors[0] = sb.data_start_lba;
    // 4.2 写入sb.inode_table_lba
    ide_write(hd, sb.inode_table_lba, buf, sb.inode_table_sects);

    // 5. 将根目录初始化并写入sb.data_start_lba
    memset(buf, 0, buf_size);
    // 5.1 写入根目录的两个目录项.和..
    struct dir_entry* p_de = (struct dir_entry*)buf;
    // 5.1.1 初始化当前目录"."
    memcpy(p_de->filename, ".", 1);
    p_de->i_no = 0;
    p_de->f_type = FT_DIRECTORY;
    p_de++;

    // 5.1.2 初始化当前目录父目录".."
    memcpy(p_de->filename, "..", 2);
    p_de->i_no = 0;  // 根目录的父目录依然是根目录自己
    p_de->f_type = FT_DIRECTORY;

    // 5.2 写入sb.data_start_lba
    ide_write(hd, sb.data_start_lba, buf, 1);

    printk("   root_dir_lba:0x%x\n", sb.data_start_lba);
    printk("%s format done\n", part->name);
    sys_free(buf);
}

/// @brief 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统
void filesys_init() {
    struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
    if (sb_buf == NULL) { PANIC("alloc memory failed!"); }

    printk("searching filesystem......\n");
    uint8_t channel_no = 0;
    while (channel_no < channel_cnt) {
        uint8_t dev_no = 0;
        while (dev_no < 2) {
            if (dev_no == 0) {  // 跨过主盘
                dev_no++;
                continue;
            }
            struct disk* hd = &channels[channel_no].devices[dev_no];

            // 遍历每一个分区,判断分区的下一个块就是超级块是否初始化
            struct partition* part = hd->prim_parts;
            uint8_t part_idx = 0;
            while (part_idx < 12) {   // 4个主分区+8个逻辑
                if (part_idx == 4) {  // 开始处理逻辑分区
                    part = hd->logic_parts;
                }

                if (part->sec_cnt != 0) {  // 如果分区存在
                    memset(sb_buf, 0, SECTOR_SIZE);

                    // 读出分区的超级块,根据魔数是否正确来判断是否存在文件系统
                    ide_read(hd, part->start_lba + 1, sb_buf, 1);

                    // 只支持自己的文件系统.若磁盘上已经有文件系统就不再格式化了
                    if (sb_buf->magic == 0x19590318) {
                        printk("%s has filesystem\n", part->name);
                    } else {  // 其它文件系统不支持,一律按无文件系统处理
                        printk("formatting %s`s partition %s......\n", hd->name, part->name);
                        partition_format(part);
                    }
                }
                part_idx++;
                part++;  // 下一分区
            }
            dev_no++;  // 下一磁盘
        }
        channel_no++;  // 下一通道
    }

    sys_free(sb_buf);


    // 确定默认操作的分区
    char default_part[8] = "sdb1";
    // 挂载分区
    list_traversal(&partition_list, mount_partition, (int)default_part);
}

super_block.h

// 文件: super_block.h
// 时间: 2024-08-06
// 来自: ccj
// 描述: 超级块数据结构,分区的第1个扇区,第0个为引导扇区


#ifndef __FS_SUPER_BLOCK_H
#define __FS_SUPER_BLOCK_H

#include "stdint.h"

/// @brief 超级块
struct super_block {
    uint32_t magic;          // 用来标识文件系统类型
    uint32_t sec_cnt;        // 本分区总共的扇区数
    uint32_t inode_cnt;      // 本分区中inode数量
    uint32_t part_lba_base;  // 本分区的起始lba地址

    uint32_t block_bitmap_lba;    // 扇区位图本身起始扇区地址
    uint32_t block_bitmap_sects;  // 扇区位图本身占用的扇区数量

    uint32_t inode_bitmap_lba;    // i结点位图起始扇区lba地址
    uint32_t inode_bitmap_sects;  // i结点位图占用的扇区数量

    uint32_t inode_table_lba;    // i结点表起始扇区lba地址
    uint32_t inode_table_sects;  // i结点表占用的扇区数量

    uint32_t data_start_lba;  // 数据区开始的第一个扇区号
    uint32_t root_inode_no;   // 根目录所在的I结点号
    uint32_t dir_entry_size;  // 目录项大小

    uint8_t pad[460];  // 加上460字节,凑够512字节1扇区大小
} __attribute__((packed));
#endif

inode.h

// 文件: inode.h
// 时间: 2024-08-06
// 来自: ccj
// 描述: inode数据结构

#ifndef __FS_INODE_H
#define __FS_INODE_H
#include "stdint.h"
#include "list.h"

/// @brief inode结构
struct inode {
    uint32_t i_no;  // inode编号

    // 当此inode是文件时,i_size是指文件大小,
    // 若此inode是目录,i_size是指该目录下所有目录项大小之和
    uint32_t i_size;

    uint32_t i_open_cnts;  // 记录此文件被打开的次数
    bool write_deny;       // 写文件不能并行,进程写文件前检查此标识

    // i_sectors[0-11]是直接块, i_sectors[12]用来存储一级间接块指针
    uint32_t i_sectors[13];
    struct list_elem inode_tag;
};
#endif

dir.h

#ifndef __FS_DIR_H
#define __FS_DIR_H
#include "stdint.h"
#include "inode.h"
#include "ide.h"
#include "global.h"

#define MAX_FILE_NAME_LEN 16  // 最大文件名长度

/// @brief 目录结构
struct dir {
    struct inode* inode;
    uint32_t dir_pos;      // 记录在目录内的偏移
    uint8_t dir_buf[512];  // 目录的数据缓存
};

/// @brief 目录项结构
struct dir_entry {
    char filename[MAX_FILE_NAME_LEN];  // 普通文件或目录名称
    uint32_t i_no;                     // 普通文件或目录对应的inode编号
    enum file_types f_type;            // 文件类型
};

#endif

第一次初始化

在这里插入图片描述

第二次直接用

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1986433.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习STM32(4)--STM32单片机定时器的应用

1 引 言 在STM32单片机的开发中&#xff0c;定时器是一个非常重要的模块&#xff0c;可以用于实现精准的时间控制和周期性的任务。在STM32F103系列单片机中&#xff0c;常见的定时器包括基本定时器、通用定时器和高级定时器。 2 实验目的 1.掌握STM32F103的基本定时器的使…

HAL库源码移植与使用之ADC

ADC类型&#xff1a; F1 F4 H4 H7用的都是逐次递进式 ADC电气特性&#xff1a; 该ADC只能承受3.6v以下电压 F1时钟不能超过14Mhz 流程顺序&#xff1a;先配置好1参考电压和芯片电源&#xff0c;再配置2电压输入通道并通过模拟多路开关选择通路到注入通道或规则通道&#xff0…

Spring AI -快速开发ChatGPT应用

Spring AI介绍 Spring AI是AI工程师的一个应用框架&#xff0c;它提供了一个友好的API和开发AI应用的抽象&#xff0c;旨在简化AI应用的开发工序&#xff0c;例如开发一款基于ChatGPT的对话、图片、音频等应用程序。 Spring AI已经集成了OpenAI的API&#xff0c;因此我们不需…

【虚拟化】KVM使用virt-manager部署及管理虚拟机

目录 一、KVM 概述 二、KVM工作原理 三、部署KVM 四、新建虚拟机步骤 4.1 创建存储池并创建存储卷 4.1.1 创建存储池 4.1.2 创建存储卷 4.3 创建ISO存储池 4.4 生成新的虚拟机 一、KVM 概述 KVM 是 Kernel-based Virtual Machine 的缩写&#xff0c;是一种用于虚拟化的…

LeetCode LCR147.最小栈

LeetCode LCR147.最小栈 思路&#x1f914;&#xff1a; 建立两个栈&#xff0c;一个栈正常入栈出栈&#xff0c;一个栈只用于出入最小数&#xff0c;当push值小于minst栈顶才入栈&#xff0c;当pop值等于minst栈顶才出栈。 代码&#x1f50e;&#xff1a; class MinStack { pu…

如何通过JavaScript提升逻辑判断的可读性?

在前端开发过程中&#xff0c;我们经常会遇到需要根据不同条件执行不同逻辑的场景。对于初学者来说&#xff0c;这样的逻辑判断可能会导致代码冗长且难以维护。那么&#xff0c;如何才能写出既简洁又易读的代码呢&#xff1f;本文将带你逐步优化 JavaScript 中的条件判断&#…

重塑电商新风尚:优选免单策略的深度解析

在当今电商领域&#xff0c;一种创新的销售策略——优选免单模式正悄然兴起。这一模式巧妙融合了价格策略、激励机制与社交互动&#xff0c;旨在激发消费者的购买潜能&#xff0c;引领销售业绩的飞跃式增长。 一、合规创新&#xff0c;重塑激励机制 我们秉承合法合规的原则&am…

API-EXPLORER项目开发笔记(一)

文章目录 前言一、我为什么要做这个项目&#xff1f;二、项目简单介绍三、项目技术栈总结 前言 最近接触到了接口平台这个东西&#xff0c;非常感兴趣&#xff0c;于是就想自己也动手做一个具备核心功能的接口平台&#xff0c;本篇文章主要介绍了做这个项目的初衷以及简单介绍…

DispatcherServlet 源码分析

一.DispatcherServlet 源码分析 本文仅了解源码内容即可。 1.观察我们的服务启动⽇志: 当Tomcat启动之后, 有⼀个核⼼的类DispatcherServlet, 它来控制程序的执⾏顺序.所有请求都会先进到DispatcherServlet&#xff0c;执⾏doDispatch 调度⽅法. 如果有拦截器, 会先执⾏拦截器…

自动获取ip地址什么意思?电脑ip地址怎么设置自动获取

在当今数字化时代&#xff0c;网络连接已成为我们日常生活和工作中不可或缺的一部分。然而&#xff0c;对于非技术用户而言&#xff0c;复杂的网络配置常常令人望而生畏。幸运的是&#xff0c;自动获取IP地址&#xff08;Dynamic Host Configuration Protocol, DHCP&#xff09…

小白入门机器学习被劝退的4大原因,你中了哪一个?

hi&#xff0c;喵老师&#x1f431;来啦。 很多小白朋友&#xff0c;尤其是准研究生、文科生&#xff0c;刚开始接触机器学习之后常常在短时间内就「入门即放弃」了。 其实背后主要的原因无非那么几个&#xff0c;今天喵老师就给大家盘一盘&#xff0c;看看你是哪一种&#x1…

SemanticKernel/C#:使用Ollama中的对话模型与嵌入模型用于本地离线场景

前言 上一篇文章介绍了使用SemanticKernel/C#的RAG简易实践&#xff0c;在上篇文章中我使用的是兼容OpenAI格式的在线API&#xff0c;但实际上会有很多本地离线的场景。今天跟大家介绍一下在SemanticKernel/C#中如何使用Ollama中的对话模型与嵌入模型用于本地离线场景。 开始…

redis面试(七)初识lua加锁脚本

redisson redisson如何来进行redis分布式锁实现的源码&#xff0c;基于redis实现各种各样的分布式锁的原理 https://redisson.org/ 这是官网 https://github.com/redisson/redisson/wiki/Table-of-Content 这是官方文档 开始 demo 建一个普通的工程在pom.xml里引入依赖 <…

CFA CAIA最新道德手册第14版+道德案例手册(2024年最新原创写的内容,上一版还是10年前14年写的)

纯原创CFA CAIA最新道德手册第14版道德案例手册&#xff08;2024年最新原创写的内容&#xff0c;上一版还是10年前14年写的&#xff09; standards 是CFA三个级别和CAIA两个级别重中之重&#xff0c;2014年的版本太过老旧&#xff0c;现在协会发布了新考纲&#xff0c;自己原创…

LVS(Linux virual server)

目录 一.集群和分布式简介 1.系统性能扩展方式 2.集群Cluster 3.分布式 4.集群和分布式 二.lvs(Linux virtual server) 运行原理 1.lvs介绍 2.lvs集群体系结构 3.LVS概念 4.lvs集群的类型 nat模式 nat模式数据逻辑 lvs-nat模式原理及部署方法 实验环境部署 实验流程…

Proxy302:你的一站式代理IP解决方案

一、Proxy302介绍 Proxy302&#xff0c;一款优秀的全球代理IP平台&#xff0c;以按需充值的灵活方式、覆盖广泛的代理类型及直观高效的用户上手体验与界面设计&#xff0c;赢得了市场广泛认可。Proxy302亮点不仅在于其功能的强大&#xff0c;更在于其对用户体验的深刻理解和不…

代发考生战报:7月26号北京考试通过 HCIP-Cloud云计算 H13-527

代发考生战报&#xff1a;7月26号北京考试通过 HCIP-Cloud云计算 H13-527 &#xff0c;考试遇到4个新题&#xff0c;剩下都是题库里的&#xff0c;但是没打高分&#xff0c;可能题库里的答案有问题&#xff0c;但是能考过就行&#xff0c;挺满足的&#xff0c;就是把题库都背会…

IT知识库文档查找与学习:rfc文档

RFC文档查找 RFC&#xff08;Request for Comments&#xff09;文档是互联网工程任务组&#xff08;Internet Engineering Task Force, IETF&#xff09;发布的一系列备忘录&#xff0c;旨在提供互联网技术和应用的标准、规范、指南和最佳实践。RFC文档是互联网发展的基石&…

小怡分享之String类的小练习

前言&#xff1a; &#x1f308;✨之前小怡给大家分享了String类&#xff0c;今天小怡给大家分享String类的一些小习题。 1.第一个只出现一次的字符 思路&#xff1a; 遍历字符串&#xff0c;把对应字符位置的下标开始计数&#xff0c;count[字符-‘a’]&#xff1b;再次遍历…

数模——灰色关联分析算法

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 文章目录 前言 一、基本概念了解 1.什么是灰色系统&#xff1f; 2.什么是关联分析&#xff1f; 二、模型原理 三、建模过程 1.找母序列&#xff08;参考序列&am…