当前位置: 首页 > news >正文

《操作系统真象还原》第十四章(2)——文件描述符、文件操作基础函数

文章目录

    • 前言
    • 文件描述符简介
      • 文件描述符原理
      • 文件描述符实现
        • 修改thread.h
        • 修改thread.c
    • 文件操作相关的基础函数
      • inode操作相关函数
      • 文件相关函数
        • 编写file.h
        • 编写file.c
      • 目录相关函数
        • 完善fs/dir.h
        • 编写fs/dir.c
      • 路径解析相关函数
      • 实现文件检索功能
        • 修改fs.h
        • 继续完善fs.c
      • makefile
    • 结语

前言

本章是14章第二篇博客,计划完成14.3、14.4两个小节内容。14.3是文件描述符简介,比较短,我们摘一些要点即可,14.4是一个大章,我们要完成文件操作的很多基础函数,代码量很大。


文件描述符简介

文件描述符原理

首先引入文件结构这个概念。文件结构用于描述文件被打开后,文件读写偏移量等信息。每次打开一个文件,就会创建一个文件结构,多次打开会创建多个文件结构。

Linux 把所有的“文件结构”组织到一起形成数组统一管理, 该数组称为文件表,我们要多次引用此概念。

然后引入文件描述符。在Linux中,我们读写函数文件时都是通过操作文件描述符来完成的。例如open,返回一个数字,而该数字就是我们所说的文件描述符,文件描述符是个整数,准确地说,它是PCB中文件描述符数组元素的下标,只不过此数字并不用来表示“数量”,而是用 来表示“位置”,它是位于进程PCB中的文件描述符数组的元素的下标,而文件描述符数组元素中的信息又指向文件表中的某个文件结构。

为什么文件描述符是数字,而不是像其他描述符那样,是个具有多个成员属性的复合数据结构?(1)所有进程可打开的文件数是一致的,每个进程都要有一套独立完整的文件描述符数组。(2)文件结构中包含进程执行文件操作的偏移量,它属于与各个任务单独绑定的资源,因此最好放在 PCB 中管理。

综合以上两点,我们不会把完整庞大的文件表塞进pcb,只要在 PCB中建立个文件描述符数组就可以了,该数组成员不需要是真正的文件结构,出于简单处理,咱们用int整型就足够了,用它存储文件表中文件结构的下标。

如何通过一个数字,也就是文件描述符,来找到文件数据块的?见示意图

梳理寻址过程:某个进程调用类似于open这样的函数,把文件描述符(int类型)作为参数提交给文件系统,文件系统通过进程pcb中的文件描述符数组,索引到一个文件表的文件结构表项,从这个文件结构获取inode指针,通过inode指针找到数据块。

文件描述符数组在pcb中创建,inode队列在创建文件系统时就已经实现,所以创建文件描述符的过程就是在pcb数组、文件表和inode队列中寻找空位填充的过程。

文件描述符实现

这部分其实是在pcb中完善文件描述符数组属性。完整的通过文件描述符操作文件我们会在14.4中实现

修改thread.h

只给出修改部分吧,开头新增了一个宏,pcb结构体新增了一个文件描述符属性。

...
#define MAX_FILES_OPEN_PER_PROC 8 // 每个进程最大能同时打开的文件数 
...
/* 线程或进程的pcb程序控制块 */
struct task_struct
{uint32_t *self_kstack;     // 线程自己的栈的栈顶指针pid_t pid;                 // 线程的pid,系统调用部分对它进行操作enum thread_status status; // 线程的状态uint8_t priority;          // 线程的优先级uint8_t ticks;             // 线程的时间片,在处理器上运行的时间滴答数uint32_t elapsed_ticks;    // 线程的运行时间,也就是这个线程已经执行了多久char name[16];             // 线程的名字int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; // 文件描述符数组struct list_elem general_tag;  // 用于线程在一般队列中的节点struct list_elem all_list_tag; // 用于线程在thread_all_list队列中的节点uint32_t *pgdir;                              // 如果是进程,这是进程的页表结构中页目录表的虚拟地址,线程则置为NULLstruct virtual_addr userprog_vaddr;           // 用户进程的虚拟地址,后续转化为物理地址后存入cr3寄存器struct mem_block_desc u_block_desc[DESC_CNT]; // 进程内存块描述符数组,用于用户进程的堆内存管理uint32_t stack_magic;                         // 线程栈的魔数,边界标记,用来检测栈溢出
};
...
修改thread.c

初始化刚刚新增的文件描述符数组

...
/* 初始化线程基本信息 */
void init_thread(struct task_struct *pthread, char *name, int prio)
{memset(pthread, 0, sizeof(*pthread)); // 清空线程pcbpthread->pid = allocate_pid();        // 获取唯一的pidstrcpy(pthread->name, name);          // 线程名字if (pthread == main_thread)           // 线程状态pthread->status = TASK_RUNNING;elsepthread->status = TASK_READY;/* self_kstack 是线程自己在内核态下使用的栈顶地址 */pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE); // 线程内核栈/*初始化文件描述符数组*/// 预留标准输入输出pthread->fd_table[0] = 0;pthread->fd_table[1] = 1;pthread->fd_table[2] = 2;// 剩下的设置为-1uint8_t fd_idx = 3;while (fd_idx < MAX_FILES_OPEN_PER_PROC){pthread->fd_table[fd_idx] = -1;fd_idx++;}pthread->priority = prio;          // 线程优先级pthread->ticks = prio;             // 线程时间片pthread->elapsed_ticks = 0;        // 线程运行时间pthread->pgdir = NULL;             // 线程页表pthread->stack_magic = 0x20250325; // 线程栈的魔数,边界标记,用来检测栈溢出
}
...

有关线程的部分就修改这些,剩下的就涉及到其他文件模块了,我们下一小节实现。


文件操作相关的基础函数

摘一下书上的原文:在本节我们要想实现文件及目录的创建、打开、读、写操作,必须建设好基础设施,现在咱们要一步 一个脚印,慢慢走向目的地。 为了帮助大伙儿理清楚函数间的依赖关系,本文按照它们的调用关系来介绍相关的文件,同时为了减 少学习的复杂性,根据实际情况有可能只会列出文件中的部分代码,并不是一股脑地把不相关的内容也搬 出来,待需要的时候依然会在相同的文件中添加新功能,因此有可能同一件文件会在不同功能的讲解中反 复更新,请您知晓。

inode操作相关函数

回顾:inode在inode数组里,inode数组实际位置在根目录前,inode位图后,inode数组的起始地址和大小也被记录在超级块里。这部分可以参考14.1文件系统布局。

在我们的文件系统里,引导扇区占据第0扇区、超级块占用第1扇区,位图类结构必须对齐到扇区,数据类结构(包括数组,根目录,空闲块)大小不定,可以跨扇区存在。

来看代码,在fs路径下创建inode.c文件,总共是写160行左右的代码,实现5个函数


#include "inode.h"
#include "../device/ide.h"
#include "../kernel/debug.h"
#include "../kernel/interrupt.h"
#include "../thread/thread.h"
#include "../lib/string.h"
#include "../lib/kernel/stdint.h"/*用来存储inode位置的结构体*/
struct inode_position
{bool two_sec;      // 此inode是否跨区uint32_t sec_lba;  // 此inode起始扇区号uint32_t off_size; // 此inode在扇区内的字节偏移量
};/*获取inode所在的扇区和扇区内的偏移量*/
static void inode_locate(struct partition *part, uint32_t inode_no, struct inode_position *inode_pos)
{ASSERT(inode_no < 4096);uint32_t inode_table_lba = part->sb->inode_table_lba;uint32_t inode_size = sizeof(struct inode);uint32_t off_size = inode_no * inode_size; // 第no号inode据inode数组起始位置的字节偏移量uint32_t off_sec = off_size / 512;         // 字节偏移量对应的扇区偏移量uint32_t off_size_in_sec = off_size % 512; // 待查找的inode在此扇区的起始地址// 判断此inode是否跨区uint32_t left_in_sec = 512 - off_size_in_sec; // 本扇区剩余存储空间// 剩余空间不够一个inodeif (left_in_sec < inode_size){inode_pos->two_sec = true;}else{inode_pos->two_sec = false;}inode_pos->sec_lba = inode_table_lba + off_sec;inode_pos->off_size = off_size_in_sec;
}/*将内存中的inode写入到硬盘分区part*/
void inode_sync(struct partition *part, struct inode *inode, void *io_buf)
{uint8_t inode_no = inode->i_no;struct inode_position inode_pos;// 调用上面的函数获取inode所在的扇区和偏移量,保存到inode_posinode_locate(part, inode_no, &inode_pos);ASSERT(inode_pos.sec_lba <= (part->start_lba + part->sec_cnt));struct inode pure_inode;memcpy(&pure_inode, inode, sizeof(struct inode));// 以下三个成员只在内存中有意义,写入硬盘时清理掉即可pure_inode.i_open_cnts = 0;pure_inode.write_deny = false; // 可读可写pure_inode.inode_tag.prev = pure_inode.inode_tag.next = NULL;// 以下,先把原有inode读取出来,更新后再写入char *inode_buf = (char *)io_buf;if (inode_pos.two_sec){// 如果跨扇区了,需要读两个扇区ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);/*inode_buf + inode_pos.off_sizes是在缓冲区内的偏移地址*用于在512/1024字节的缓冲区内定位到实际indoe位置*/memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode));ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 2);}else{ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode));ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 1);}
}/*根据i节点号返回i节点指针*/
struct inode *inode_open(struct partition *part, uint32_t inode_no)
{// 先在每个分区中存在的,打开的i节点链表中寻找i节点,此链表是为了提速创建的缓冲区struct list_elem *elem = part->open_inodes.head.next;struct inode *inode_found;while (elem != &part->open_inodes.tail){inode_found = elem2entry(struct inode, inode_tag, elem);if (inode_found->i_no == inode_no) // 如果成功找到,inode打开次数+1,返回indoe地址{inode_found->i_open_cnts++;return inode_found;}elem = elem->next;}/*目前在链表中没有找到,于是从硬盘中读入inode并加入链表*/struct inode_position inode_pos;// 调用locate函数,获知no对应的inode的信息inode_locate(part, inode_no, &inode_pos);/*为了让进程新创建的inode被共享,需要将inode放在内核区*需要临时将pcb的pgdir设置为NULL*/struct task_struct *cur = running_thread();uint32_t *cur_pagedir_bak = cur->pgdir; // 临时记录cur->pgdir = NULL;inode_found = (struct inode *)sys_malloc(sizeof(struct inode));cur->pgdir = cur_pagedir_bak;char *inode_buf;if (inode_pos.two_sec == true){// 跨扇区读两个扇区inode_buf = (char *)sys_malloc(1024);ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);}else{inode_buf = (char *)sys_malloc(512);ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);}memcpy(inode_found, inode_buf + inode_pos.off_size, sizeof(struct inode));// 加入队列方便后续使用list_push(&part->open_inodes, &inode_found->inode_tag);// 队列里没有,说明这是第一次被打开,打开次数设置为1inode_found->i_open_cnts = 1;sys_free(inode_buf);return inode_found;
}/*关闭indoe或减少inode打开数*/
void inode_close(struct inode *inode)
{enum intr_status old_status = intr_disable(); // 关inode应为原子操作if (--inode->i_open_cnts == 0){list_remove(&inode->inode_tag);// 内存中新的inode开辟在内核空间,移除时也需要确保回收内核空间struct task_struct *cur = running_thread();uint32_t *cur_pagedir_bak = cur->pgdir; // 临时记录cur->pgdir = NULL;sys_free(inode);cur->pgdir = cur_pagedir_bak;}intr_set_status(old_status);
}/*初始化new_inode*/
void inode_init(uint32_t inode_no, struct inode *new_inode)
{new_inode->i_no = inode_no;new_inode->i_size = 0;new_inode->i_open_cnts = 0;new_inode->write_deny = false;// 初始化块索引数组i_sectoruint8_t sec_idx = 0;while (sec_idx < 13){new_inode->i_sectors[sec_idx] = 0;sec_idx++;}
}

这个文件已经编译过一次,排除了编译错误。

现在是2025年5月11日21点11分,今天就写到这里吧。

文件相关函数

这部分涉及两个新文件,file.c和file.h,都在fs目录下。

编写file.h
#ifndef __FS_FILE_H
#define __FS_FILE_H#include "../lib/kernel/stdint.h"
struct inode;     // 前向声明,代替inode
struct partition; // 代替ide.h/*文件结构,每次打开文件就会创建一个此结构*记录偏移量,打开情况,inode指针等信息*/
struct file
{uint32_t fd_pos;        // 记录文件操作的偏移地址,最小是0,最大是size-1uint32_t fd_flag;       // 记录是否打开struct inode *fd_inode; // 记录此文件的inode指针
};/*标准输入输出描述符*/
enum std_fd
{stdin_no,  // 0,标准输入stdout_no, // 1,标准输出stderr_no  // 2,标准错误
};/*位图类型*/
enum bitmap_type
{INODE_BITMAP, // inode位图BLOCK_BITMAP  // 块位图
};#define MAX_FILE_OPEN 32 // 系统可打开的最大文件数/*从文件表file_table中获取一个空闲位,成功返回下标,失败返回-1*/
int32_t get_free_slot_in_global(void);
/*将全局描述符下标安装到进程或线程自己的文件描述符数组fd_table中*成功返回下标,失败返回-1*/
int32_t pcb_fd_install(int32_t global_fd_idx);
/*分配一个i节点,返回i结点号*/
int32_t inode_bitmap_alloc(struct partition *part);
/*分配1个扇区,返回扇区地址*/
int32_t block_bitmap_alloc(struct partition *part);
/*将内存中bitmap第bit_idx位所在的512个字节同步到硬盘*/
void bitmap_sync(struct partition *part, uint32_t bit_idx, uint8_t btmp);
#endif
编写file.c
#include "file.h"
#include "inode.h"            //对应前向声明
#include "../device/ide.h"    //对应前向声明
#include "../lib/stdio.h"     //printk
#include "../thread/thread.h" //pcb/*文件表,前三个成员预留给标准输入、标准输出、标准错误*/
struct file file_table[MAX_FILE_OPEN];/*从文件表file_table中获取一个空闲位,成功返回下标,失败返回-1*/
int32_t get_free_slot_in_global(void)
{uint32_t fd_idx = 3; // 跳过0、1、2while (fd_idx < MAX_FILE_OPEN){if (file_table[fd_idx].fd_inode == NULL){break; // i结点==NULL说明这个文件结构没被使用,存在空闲}fd_idx++;}if (fd_idx == MAX_FILE_OPEN){// 超出最大打开文件数限制printk("exceed max open files\n");return -1;}return fd_idx;
}/*将全局描述符下标安装到进程或线程自己的文件描述符数组fd_table中*成功返回下标,失败返回-1*/
int32_t pcb_fd_install(int32_t global_fd_idx)
{struct task_struct *cur = running_thread();uint8_t local_fd_idx = 3;while (local_fd_idx < MAX_FILES_OPEN_PER_PROC){// 我们在线程初始化的时候,用-1表示线程文件表空位,所以-1对应可以使用if (cur->fd_table[local_fd_idx] == -1){cur->fd_table[local_fd_idx] = global_fd_idx;break;}local_fd_idx++;}if (local_fd_idx == MAX_FILES_OPEN_PER_PROC){// 超出单个进程最大打开文件数限制printk("exceed max open files_per_proc\n");return -1;}return local_fd_idx;
}/*分配一个i节点,返回i结点号*/
int32_t inode_bitmap_alloc(struct partition *part)
{int32_t bit_idx = bitmap_scan(&part->inode_bitmap, 1);if (bit_idx == -1){return -1; // 申请失败}// 申请成功,设置位图并返回编号bitmap_set(&part->inode_bitmap, bit_idx, 1);return bit_idx;
}/*分配1个扇区,返回扇区lba地址*/
int32_t block_bitmap_alloc(struct partition *part)
{int32_t bit_idx = bitmap_scan(&part->block_bitmap, 1);if (bit_idx == -1){return -1; // 申请失败}bitmap_set(&part->block_bitmap, bit_idx, 1);// 返回扇区lba号return (part->sb->block_bitmap_lba + bit_idx);
}/*内存中某个bitmap第bit_idx位修改后,将对应的修改同步到硬盘*/
void bitmap_sync(struct partition *part, uint32_t bit_idx, uint8_t btmp)
{// 一个位图的一个位进行了修改,说明512字节的一个扇区被分配或者回收,需要更新硬盘位图// 位图结构总是整扇区大小的,一次至少更新512字节uint32_t off_sec = bit_idx / 4096; // 找到位图对应的硬盘上的扇区偏移量uint32_t off_size = off_sec * 512; // 对应的字节偏移量uint32_t sec_lba;uint8_t *bitmap_off;if (btmp == INODE_BITMAP){sec_lba = part->sb->inode_bitmap_lba + off_sec;bitmap_off = part->inode_bitmap.btmp_bits + off_size;}else if (btmp == BLOCK_BITMAP){sec_lba = part->sb->block_bitmap_lba + off_sec;bitmap_off = part->block_bitmap.btmp_bits + off_size;}ide_write(part->my_disk, sec_lba, bitmap_off, 1);
}

目前file.c是100行左右代码,后续还会新增。

目录相关函数

先回顾目录和目录项:目录本身是一种特殊文件,内部的数据块保存的是目录项的列表。目录项是目录文件的一条记录,用于记录该目录下的一个文件或目录信息。这两种结构被抽象在dir.h中,大小都不需要是整扇区。

完善fs/dir.h

新增了函数声明

#ifndef __FS_DIR_H
#define __FS_DIR_H#include "../lib/kernel/stdint.h"
#include "fs.h" //提供enum file_types
struct inode;   // inode前向声明#define MAX_FILE_NAME_LEN 16 // 最大文件名长度/*目录结构体*/
struct dir
{struct inode *inode;uint32_t dir_pos;     // 记录在此目录下的偏移uint8_t dir_buf[512]; // 目录的数据缓冲区
};/*目录项结构体*/
struct dir_entry
{char filename[MAX_FILE_NAME_LEN]; // 普通文件或目录名称uint32_t i_no;                    // 对应的i结点编号enum file_types f_type;           // 文件类型
};/*打开根目录*/
void open_root_dir(struct partition *part);
/*在part分区打开i结点编号inode_no的目录,并返回目录指针*/
struct dir *dir_open(struct partition *part, uint32_t inode_no);
/*在part分区内的pdir目录内寻找名为name的文件或目录*找到后返回true,并把目录项存在dir_e,否则返回false*/
bool search_dir_entry(struct partition *part, struct dir *pdir, const char *name, struct dir_entry *dir_e);
/*关闭目录*/
void dir_close(struct dir *dir);
/*在内存中初始化目录项p_de*/
void create_dir_entry(char *filename, uint32_t inode_no, uint8_t file_type, struct dir_entry *p_de);
/*将目录项p_de写入父目录parent_dir中,io_buf由主调函数提供*/
bool sync_dir_entry(struct dir *parent_dir, struct dir_entry *p_de, void *io_buf);
#endif
编写fs/dir.c

回顾一下inode结构。一块一扇区512字节。对于我们的inode,只有13个指针,前12个是直接块指针,最后一个指针指向一级间接块索引表。索引表页也是一个块,512字节大小,内部包含128个4字节指针,再次指向128个块。一个inode结构可以控制140个块。

这是本节新增文件,写完是230+行。


#include "dir.h"
#include "inode.h"            // 前向声明
#include "file.h"             //block_bitmap_alloc函数
#include "../device/ide.h"    // partition结构体
#include "../kernel/memory.h" // sys_malloc函数
#include "../kernel/debug.h"  //ASSERT哨兵
#include "../lib/stdio.h"     //printk函数
#include "../lib/string.h"    //strcmp函数struct dir root_dir; // 根目录/*打开根目录,即初始化*/
void open_root_dir(struct partition *part)
{root_dir.inode = inode_open(part, part->sb->root_inode_no);root_dir.dir_pos = 0; // 偏移地址为0
}/*在part分区打开i结点编号inode_no的目录,并返回目录指针*/
struct dir *dir_open(struct partition *part, uint32_t inode_no)
{struct dir *pdir = (struct dir *)sys_malloc(sizeof(struct dir));pdir->inode = inode_open(part, inode_no);pdir->dir_pos = 0;return pdir;
}/*在part分区内的pdir目录内寻找名为name的文件或目录*找到后返回true,并把目录项存在dir_e,否则返回false*/
bool search_dir_entry(struct partition *part, struct dir *pdir, const char *name, struct dir_entry *dir_e)
{/*对于我们的inode,只有13个指针,前12个是直接块指针,最后一个指针指向一级间接块索引表*索引表页也是一个块,512字节大小,内部包含128个4字节指针,再次指向128个块*当硬盘被虚拟抽象到内存后,LBA号也就抽象成了指针*/uint32_t block_cnt = 12 + 128; // 12个直接块+128个一级间接块指针// all_block保存此inode所有块指针,32位系统中,一个指针大小为4字节uint32_t *all_block = (uint32_t *)sys_malloc((12 + 128) * 4);if (all_block == NULL){printk("search_dir_entry: sys_malloc for all_block failed");return false;}uint32_t block_idx = 0;// 先处理12个直接块while (block_idx < 12){all_block[block_idx] = pdir->inode->i_sectors[block_idx];block_idx++;}block_idx = 0;// 如果使用了一级间接块表,处理一级间接块if (pdir->inode->i_sectors[12] != 0){// 因为all_block的类型是uint32_t *,所以后面的+12其实是+12*4,目的是跳过12个直接块ide_read(part->my_disk, pdir->inode->i_sectors[12], all_block + 12, 1);}uint8_t *buf = (uint8_t *)sys_malloc(SECTOR_SIZE);struct dir_entry *p_de = (struct dir_entry *)buf; // 指向目录项的指针uint32_t dir_entry_size = part->sb->dir_entry_size;uint32_t dir_entry_cnt = SECTOR_SIZE / dir_entry_size; // 计算一个扇区包含多少目录项while (block_idx < block_cnt) // 遍历这个目录拥有的所有的块{// 地址为0说明块内无数据,寻找下一个块if (all_block[block_idx] == 0){block_idx++;continue;}ide_read(part->my_disk, all_block[block_idx], buf, 1);// 现在我们读取了一个块的数据到buf中(也就是pd_e中),接下来遍历这个扇区内所有的目录项uint32_t dir_entry_idx = 0;while (dir_entry_idx < dir_entry_cnt){// 取反的原因涉及到strcmp函数的返回值if (!strcmp(p_de->filename, name)){// 找到了相关文件或目录,复制到指定内存dir_ememcpy(dir_e, p_de, dir_entry_size);sys_free(buf);sys_free(all_block);return true;}dir_entry_idx++; // 进入下一个目录项p_de++;}block_idx++;                    // 此扇区已遍历完,进入下一个扇区p_de = (struct dir_entry *)buf; // 指向新扇区的bufmemset(buf, 0, SECTOR_SIZE);    // 将buf清零}sys_free(buf);sys_free(all_block);return false;
}/*关闭目录*/
void dir_close(struct dir *dir)
{/******************** 根目录不能被关闭 *********************1 根目录自打开后就不应该关闭,否则还需要再次open_root_dir();*2 root_dir 所在的内存是低端1MB之内,并非在堆中,free会出问题 */if (dir == &root_dir){return;}// 对于一般目录,关闭目录就是关闭目录文件inode,然后释放dir所占内存inode_close(dir->inode);sys_free(dir);
}/*在内存中初始化目录项p_de*/
void create_dir_entry(char *filename, uint32_t inode_no, uint8_t file_type, struct dir_entry *p_de)
{ASSERT(strlen(filename) <= MAX_FILE_NAME_LEN);memcpy(p_de->filename, filename, strlen(filename));p_de->i_no = inode_no;p_de->f_type = file_type;
}/*将目录项p_de写入父目录parent_dir中,io_buf由主调函数提供*/
bool sync_dir_entry(struct dir *parent_dir, struct dir_entry *p_de, void *io_buf)
{struct inode *dir_inode = parent_dir->inode;uint32_t dir_size = dir_inode->i_size;uint32_t dir_entry_size = cur_part->sb->dir_entry_size;ASSERT(dir_size % dir_entry_size == 0); // 目录大小是目录项大小的整数倍uint32_t dir_entry_per_sec = (512 / dir_size); // 每扇区目录项数int32_t block_lba = -1;                        // 数据块lbauint8_t block_idx = 0;uint32_t all_block[140] = {0};// 处理直接块while (block_idx < 12){all_block[block_idx] = dir_inode->i_sectors[block_idx];block_idx++;}struct dir_entry *dir_e = (struct dir_entry *)io_buf; // dir_e用来在io_buf中遍历目录项int32_t block_bitmap_idx = -1;                        // 数据块位图索引/*开始遍历扇区寻找空目录项,如果已有扇区没有空目录项,在文件大小范围内,申请新扇区*/while (block_idx < 140){block_bitmap_idx = -1;if (all_block[block_idx] == 0) // 此块未使用{// 申请此块,获得lbablock_lba = block_bitmap_alloc(cur_part);if (block_lba == -1) // 失败{printk("alloc block bitmap for sync_dir_entry failed\n");return false;}// 数据块位图索引=当前lba-数据块起始lbablock_bitmap_idx = block_lba - cur_part->sb->data_start_lba;ASSERT(block_bitmap_idx != -1);bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);block_bitmap_idx = -1;// 先定位到空目录项所在扇区if (block_idx < 12) // 直接块{dir_inode->i_sectors[block_idx] = all_block[block_idx] = block_lba;}else if (block_idx == 12) // 还未分配一级间接块表地址{dir_inode->i_sectors[12] = block_lba; // 将上面获取的lba作为一级间接块表地址block_lba = -1;block_lba = block_bitmap_alloc(cur_part); // 分配第0个间接块地址if (block_lba == -1)                      // 分配失败{block_bitmap_idx = dir_inode->i_sectors[12] - cur_part->sb->data_start_lba;bitmap_set(&cur_part->block_bitmap, block_bitmap_idx, 0); // 将相应块位图设置为未使用dir_inode->i_sectors[12] = 0;printk("alloc block bitmap for sync_dir_entry failed\n");return false;}// 同步block_bitmapblock_bitmap_idx = block_lba - cur_part->sb->data_start_lba;ASSERT(block_bitmap_idx != -1);bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);all_block[12] = block_lba;// 写入硬盘ide_write(cur_part->my_disk, dir_inode->i_sectors[12], all_block + 12, 1);}else // 还有未分配的间接块{all_block[block_idx] = block_lba;// 我们更新硬盘中的一级间接表,让间接表多一个指向新扇区的指针ide_write(cur_part->my_disk, dir_inode->i_sectors[12], all_block + 12, 1);}// 将新目录项p_de写入新分配的间接块memset(io_buf, 0, 512);memcpy(io_buf, p_de, dir_entry_size);// 区别dir_inode->i_sectors[12]和all_block// 前者是inode内一级块表,通过它索引到all_block后128项ide_write(cur_part->my_disk, all_block[block_idx], io_buf, 1);dir_inode->i_size += dir_entry_size;return true;}/*对应此块未使用的情况,如果此块已被使用,将块读入内存*然后寻找块内有没有空目录项*/ide_read(cur_part->my_disk, all_block[block_idx], io_buf, 1);uint8_t dir_entry_idx = 0; // 用于按目录项遍历块while (dir_entry_idx < dir_entry_per_sec){if ((dir_e + dir_entry_idx)->f_type == FT_UNKNOWN){// FT_UNKNOWN代表未使用或已删除,总之就是空白memcpy(dir_e + dir_entry_idx, p_de, dir_entry_size);ide_write(cur_part->my_disk, all_block[block_idx], io_buf, 1);dir_inode->i_size += dir_entry_size;return true;}dir_entry_idx++;}block_idx++;}printk("directory is full!\n");return false;
}

从今天下午4点开始写,到现在是2025年5月12日20点45分,进度推进到这里。

路径解析相关函数

路径解析,就是根据路径把文件名分层,逐层在磁盘查找,确认文件名是否存在。这部分继续编写fs.c,总共是实现两个函数。

/*将最上层路径名解析出来,在name_store保存当前路径名,然后返回解析后的子路径*/
static char *path_paser(char *pathname, char *name_store)
{if (pathname[0] == '/') // 跳过前面所有的/{while (*pathname == '/'){pathname++;}}// 开始一般的路径解析,提取最上层路径名,即从字符开始到第一个/停止while (*pathname != '/' && *pathname != 0) // 0是空字符ascii码{// 我不喜欢写自增,因为自增容易带来阅读障碍*name_store = *pathname;name_store++;pathname++;}if (pathname[0] == 0){return NULL;}return pathname;
}/*返回路径深度*/
int32_t path_depth_cnt(char *pathname)
{ASSERT(pathname != NULL);char *p = pathname;           // 用于保存每次path_paser返回的子路径char name[MAX_FILE_NAME_LEN]; // 用于保存每次path_paser返回的原文件名uint32_t depth = 0;p = path_paser(p, name);while (name[0] != 0) // 只要存在当前文件{depth++;memset(name, 0, MAX_FILE_NAME_LEN);if (p != 0) // 只要存在子路径{p = path_paser(p, name);}}return depth;
}

实现文件检索功能

文件检索就是确定文件是否存在于此路径

修改fs.h

新增了宏,枚举,结构体,还有上一节的函数的声明。

#ifndef __FS_FS_H
#define __FS_FS_Hstruct partition; // 前向声明,代替ide.h
struct dir;       // 代替dir.h#define MAX_FILES_PER_PART 4096 // 每个扇区最大支持文件数
#define BITS_PER_SECTOR 4096    // 每扇区的位数
#define SECTOR_SIZE 512         // 每扇区的字节数
#define BLOCK_SIZE SECTOR_SIZE  // 块字节大小 我们设置为1个块==1个扇区
#define MAX_PATH_LEN 512        // 路径最大长度/*文件类型枚举*/
enum file_types
{FT_UNKNOWN,  // 0,未知文件类型FT_REGULAR,  // 1,普通文件类型FT_DIRECTORY // 2,目录文件类型
};/*打开文件的选项枚举*/
enum oflags
{O_RDONLY,   // 只读O_WRONLY,   // 只写O_RDWR,     // 读写O_CREAT = 4 // 创建
};/*记录查找过程中的上级路径*/
struct path_search_record
{char searched_path[MAX_PATH_LEN]; // 父路径struct dir *parent_dir;           // 直接父目录enum file_types file_type;        // 找到的文件的类型
};void filesys_init(void);                /*在磁盘上搜索文件系统,若没有则格式化分区创建文件系统*/
int32_t path_depth_cnt(char *pathname); /*返回路径深度*/extern struct partition *cur_part;
#endif
继续完善fs.c
#include "fs.h"
#include "inode.h"
#include "dir.h"
#include "super_block.h"
#include "../lib/kernel/stdint.h"
#include "../lib/kernel/list.h"
#include "../lib/string.h"
#include "../lib/stdio.h"
#include "../device/ide.h" //partition
#include "../kernel/debug.h"struct partition *cur_part; // 记录默认情况下操作的分区/* 在分区链表中找到名为part_name的分区,并将其指针赋值给cur_part */
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;// 创建用来保存超级块的缓冲区struct super_block *sb_buf = (struct super_block *)sys_malloc(SECTOR_SIZE);// 在内存创建cur_part的超级块cur_part->sb = (struct super_block *)sys_malloc(sizeof(struct super_block));if (cur_part->sb == NULL){PANIC("alloc memory failed!");}/*读入超级块到缓冲区*/memset(sb_buf, 0, SECTOR_SIZE);ide_read(hd, cur_part->start_lba + 1, sb_buf, 1);/*把缓冲区超级块数据复制到cur_part的sb中*/memcpy(cur_part->sb, sb_buf, sizeof(struct super_block));/*为什么要先读入缓冲区,再把缓冲区数据复制到相应的变量中?*缓冲区大小就是1扇区512字节,和硬盘读取标准对齐,而实际超级块结构体小于512字节*如果直接读入实际结构体,会导致硬盘读写很慢。*//*将分区的块位图写入内存*/// 开辟内存空间给位图指针cur_part->block_bitmap.btmp_bits = (uint8_t *)sys_malloc(sb_buf->block_bitmap_sects * SECTOR_SIZE);if (cur_part->block_bitmap.btmp_bits == NULL){PANIC("alloc memory failed!");}// 设置位图长度cur_part->block_bitmap.btmp_bytes_len = sb_buf->block_bitmap_sects * SECTOR_SIZE;// sb_buf->block_bitmap_sects等价于cur_part->sb->block_bitmap_sects// 给位图指针赋值ide_read(hd, sb_buf->block_bitmap_lba, cur_part->block_bitmap.btmp_bits, sb_buf->block_bitmap_sects);/*将分区的inode位图写入内存*/cur_part->inode_bitmap.btmp_bits = (uint8_t *)sys_malloc(sb_buf->inode_bitmap_sects * SECTOR_SIZE);if (cur_part->inode_bitmap.btmp_bits == NULL){PANIC("alloc memory failed!");}cur_part->inode_bitmap.btmp_bytes_len = sb_buf->block_bitmap_sects * SECTOR_SIZE;ide_read(hd, sb_buf->inode_bitmap_lba, cur_part->inode_bitmap.btmp_bits, sb_buf->inode_bitmap_sects);list_init(&cur_part->open_inodes);printk("mount %s done!\n", part->name);/*返回true是为了配合定义在list.c的list_traversal函数,和本函数功能无关*返回true时list_traversal停止对链表的遍历*/return true;}return false;
}static void partition_format(struct disk *hd, struct partition *part)
{uint32_t boot_sector_sects = 1; // 根目录扇区uint32_t super_block_sects = 1; // 超级块扇区// inode位图所占扇区uint32_t inode_bitmap_sects = DIV_ROUND_UP(MAX_FILES_PER_PART, BITS_PER_SECTOR);// inode表所占扇区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 = 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 = 0x20250325;sb.sec_cnt = part->sec_cnt;sb.inode_cnt = MAX_FILES_PER_PART;sb.part_lba_base = part->start_lba;sb.block_bitmap_lba = sb.part_lba_base + 2;sb.block_bitmap_sects = block_bitmap_sects;sb.inode_bitmap_lba = sb.block_bitmap_lba + sb.block_bitmap_sects;sb.inode_bitmap_sects = inode_bitmap_sects;sb.inode_table_lba = sb.inode_bitmap_lba + sb.inode_bitmap_sects;sb.inode_table_sects = inode_table_sects;sb.data_start_lba = sb.inode_table_lba + sb.inode_table_sects;sb.root_inode_no = 0;sb.dir_entry_size = sizeof(struct dir_entry);printk("%s info:\n""  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_sects: 0x%x\n""  inode_bitmap_lba:   0x%x\n""  inode_bitmap_sects: 0x%x\n""  inode_table_lba:    0x%x\n""  inode_table_sects:  0x%x\n""  data_start_lba:     0x%x\n",part->name,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);// 1.将超级块写入本分区1扇区ide_write(hd, part->start_lba + 1, &sb, 1);printk("  super_blcok_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;buf_size *= SECTOR_SIZE;uint8_t *buf = (uint8_t *)sys_malloc(buf_size);// 2.将块位图初始化并写入sb.block_bitmap_lbabuf[0] |= 0x01; // 0号块留给根目录uint32_t block_bitmap_last_byte = block_bitmap_bit_len / 8;uint8_t block_bitmap_last_bit = block_bitmap_bit_len % 8;// last_size是保存位图的最后一个扇区中,多余出的位uint32_t last_size = (SECTOR_SIZE - block_bitmap_last_byte % SECTOR_SIZE);// 先将超出实际块数的部分设置为已占用1memset(&buf[block_bitmap_last_byte], 0xff, last_size);// 在将有效位重新设置为未占用0uint8_t bit_idx = 0;while (bit_idx <= block_bitmap_last_bit){// 通过取反+左移,实现逐位清零buf[block_bitmap_last_byte] &= ~(1 << bit_idx++);}ide_write(hd, sb.block_bitmap_lba, buf, sb.block_bitmap_sects);// 3.将inode位图初始化并写入sb.inode_bitmap_lba// 清空缓冲区memset(buf, 0, buf_size);buf[0] |= 0x1;/*inode_table中有4096个inode,正好一个扇区,*inode_bitmap扇区没有多余无效位,不需要进一步处理*/ide_write(hd, sb.inode_bitmap_lba, buf, sb.inode_bitmap_sects);// 4 将inode数组初始化并写入sb.inode_table_lba// 初始化了第一个indoememset(buf, 0, buf_size);struct inode *i = (struct inode *)buf;i->i_size = sb.dir_entry_size * 2; // 留出..和.目录i->i_no = 0;i->i_sectors[0] = sb.data_start_lba;ide_write(hd, sb.inode_table_lba, buf, sb.inode_table_sects);// 5.将根目录写入sb.data_start_lbamemset(buf, 0, buf_size);struct dir_entry *p_de = (struct dir_entry *)buf;// 初始化当前目录.memcpy(p_de->filename, ".", 1);p_de->i_no = 0;p_de->f_type = FT_DIRECTORY;p_de++;// 初始化父目录..memcpy(p_de->filename, "..", 2);p_de->i_no = 0;p_de->f_type = FT_DIRECTORY;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);
}/*在磁盘上搜索文件系统,若没有则格式化分区创建文件系统*/
void filesys_init()
{uint8_t channel_no = 0, dev_no, part_idx = 0;// 开辟超级块缓冲区struct super_block *sb_buf = (struct super_block *)sys_malloc(SECTOR_SIZE);if (sb_buf == NULL){PANIC("alloc memory failed!");}printk("searching filesystem......\n");while (channel_no < channel_cnt) // channel_cnt声明在ide.h,实现在ide.c{dev_no = 0;while (dev_no < 2) // 一个通道可以挂载2个设备{if (dev_no == 0){dev_no++;continue;}struct disk *hd = &channels[channel_no].devices[dev_no];struct partition *part = hd->prim_parts; // 初始指向4个主分区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 == 0x20250325){printk("    %s has file system\n", part->name);}// 不匹配,认为不存在文件系统,于是创建我的操作系统else{// 提示正在进行初始化printk("formatting %s's partition %s......\n", hd->name, part->name);// 调用函数创建每个分区的文件系统partition_format(hd, 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);
}/*将最上层路径名解析出来,在name_store保存当前路径名,然后返回解析后的子路径*/
static char *path_paser(char *pathname, char *name_store)
{if (pathname[0] == '/') // 跳过前面所有的/{while (*pathname == '/'){pathname++;}}// 开始一般的路径解析,提取最上层路径名,即从字符开始到第一个/停止while (*pathname != '/' && *pathname != 0) // 0是空字符ascii码{// 我不喜欢写自增,因为自增容易带来阅读障碍*name_store = *pathname;name_store++;pathname++;}if (pathname[0] == 0){return NULL;}return pathname;
}/*返回路径深度*/
int32_t path_depth_cnt(char *pathname)
{ASSERT(pathname != NULL);char *p = pathname;           // 用于保存每次path_paser返回的子路径char name[MAX_FILE_NAME_LEN]; // 用于保存每次path_paser返回的原文件名uint32_t depth = 0;p = path_paser(p, name);while (name[0] != 0) // 只要存在当前文件{depth++;memset(name, 0, MAX_FILE_NAME_LEN);if (p != 0) // 只要存在子路径{p = path_paser(p, name);}}return depth;
}/*搜索文件pathname,若找到返回inode号,否则返回-1*/
static int search_file(const char *pathname, struct path_search_record *search_record)
{/*如果查找的是根目录,直接返回根目录信息*/if (!strcmp(pathname, "/") || !strcmp(pathname, "/.") || !strcmp(pathname, "/..")){search_record->file_type = FT_DIRECTORY;search_record->parent_dir = &root_dir;search_record->searched_path[0] = 0;return 0;}uint32_t path_len = strlen(pathname);ASSERT(path_len < MAX_PATH_LEN);char *sub_path = (char *)pathname;struct dir *parent_dir = &root_dir;struct dir_entry dir_e; // 保存查到的目录项/*逐级解析路径*/char name[MAX_FILE_NAME_LEN] = {0};search_record->parent_dir = parent_dir;search_record->file_type = FT_UNKNOWN;uint32_t parent_inode_no = 0; // 父目录的inode号sub_path = path_paser(sub_path, name);while (name[0]){ASSERT(strlen(search_record->searched_path) < MAX_PATH_LEN);/*记录已经存在的路径*/strcat(search_record->searched_path, "/");strcat(search_record->searched_path, name);// 如果成功找到了当前本级文件名的目录项if (search_dir_entry(cur_part, parent_dir, name, &dir_e)){memset(name, 0, MAX_FILE_NAME_LEN);if (sub_path) // 如果还有子目录项{sub_path = path_paser(sub_path, name);}if (dir_e.f_type == FT_DIRECTORY) // 目录文件,需要进一步查找{parent_inode_no = parent_dir->inode->i_no;dir_close(parent_dir);parent_dir = dir_open(cur_part, dir_e.i_no);search_record->parent_dir = parent_dir;continue;}else if (dir_e.f_type == FT_REGULAR) // 普通文件,查找到了终点{search_record->file_type = FT_REGULAR;return dir_e.i_no;}}else // 没有找到当前文件名的目录项{return -1;}}/*至此,已经遍历完完整目录,并且最后一个文件是目录文件*/dir_close(search_record->parent_dir);// 更新查找记录search_record->parent_dir = dir_open(cur_part, parent_inode_no);search_record->file_type = FT_DIRECTORY;return dir_e.i_no;
}

这是目前能通过编译的fs.c,365行代码。

makefile

最后放一下这章结束时的makefile吧,这部分没有开bochs检验,因为还都是基础函数,所以没有什么运行截图。目前的makefile也是为了确保代码能编译,不保证代码逻辑的完全正确。

BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS =  -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS =  -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \$(BUILD_DIR)/switch.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \$(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o $(BUILD_DIR)/tss.o \$(BUILD_DIR)/process.o $(BUILD_DIR)/syscall-init.o $(BUILD_DIR)/syscall.o \$(BUILD_DIR)/stdio.o $(BUILD_DIR)/ide.o $(BUILD_DIR)/fs.o \$(BUILD_DIR)/inode.o $(BUILD_DIR)/file.o $(BUILD_DIR)/dir.o ################	c代码编译   ##################
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \lib/kernel/stdint.h kernel/init.h kernel/debug.h \kernel/memory.h thread/thread.h kernel/interrupt.h \device/console.h userprog/process.h lib/user/syscall.h \userprog/syscall-init.h lib/stdio.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \lib/kernel/stdint.h kernel/interrupt.h device/timer.h \kernel/memory.h thread/thread.h device/console.h \device/keyboard.h userprog/tss.h userprog/syscall-init.h \device/ide.h fs/fs.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \lib/kernel/stdint.h kernel/global.h kernel/io.h \lib/kernel/print.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/stdint.h \kernel/io.h lib/kernel/print.h kernel/interrupt.h \thread/thread.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \lib/kernel/print.h kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/string.o: lib/string.c lib/string.h \kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h \lib/string.h thread/sync.h thread/thread.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \lib/string.h kernel/interrupt.h lib/kernel/print.h \kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \lib/kernel/stdint.h lib/kernel/list.h lib/string.h \kernel/memory.h kernel/interrupt.h kernel/debug.h \lib/kernel/print.h userprog/process.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \lib/kernel/stdint.h kernel/interrupt.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \lib/kernel/stdint.h thread/thread.h kernel/debug.h \kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/console.o: device/console.c device/console.h \lib/kernel/print.h thread/sync.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \lib/kernel/print.h kernel/interrupt.h kernel/io.h \lib/kernel/stdint.h device/ioqueue.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h \lib/kernel/stdint.h thread/thread.h thread/sync.h \kernel/interrupt.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/tss.o: userprog/tss.c userprog/tss.h \lib/kernel/stdint.h thread/thread.h kernel/global.h \lib/kernel/print.h lib/string.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/process.o: userprog/process.c userprog/process.h \kernel/global.h lib/kernel/stdint.h thread/thread.h \kernel/debug.h userprog/tss.h device/console.h \lib/string.h kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/syscall.o: lib/user/syscall.c lib/user/syscall.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/syscall-init.o: userprog/syscall-init.c userprog/syscall-init.h \lib/kernel/stdint.h lib/user/syscall.h thread/thread.h \lib/kernel/print.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/stdio.o: lib/stdio.c lib/stdio.h \lib/kernel/stdint.h lib/string.h kernel/debug.h \lib/user/syscall.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/ide.o: device/ide.c device/ide.h \lib/stdio.h kernel/debug.h kernel/global.h \thread/sync.h kernel/io.h device/timer.h \kernel/interrupt.h lib/string.h fs/super_block.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/fs.o: fs/fs.c fs/fs.h \fs/inode.h fs/super_block.h fs/dir.h \lib/stdio.h lib/string.h kernel/debug.h \device/ide.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/inode.o: fs/inode.c fs/inode.h \device/ide.h kernel/debug.h kernel/interrupt.h \thread/thread.h lib/string.h lib/kernel/stdint.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/file.o: fs/file.c fs/file.h \fs/inode.h device/ide.h thread/thread.h \lib/stdio.h lib/kernel/stdint.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/dir.o: fs/dir.c fs/dir.h \fs/inode.h fs/file.h device/ide.h \kernel/memory.h kernel/debug.h lib/stdio.h \lib/string.h$(CC) $(CFLAGS) $< -o $@##############    汇编代码编译    ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S$(AS) $(ASFLAGS) $< -o $@$(BUILD_DIR)/print.o: lib/kernel/print.S$(AS) $(ASFLAGS) $< -o $@$(BUILD_DIR)/switch.o: thread/switch.S$(AS) $(ASFLAGS) $< -o $@##############    连接所有目标文件    #############
$(BUILD_DIR)/kernel.bin: $(OBJS)$(LD) $(LDFLAGS) $^ -o $@.PHONY : mk_dir hd clean allmk_dir:if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fihd:dd if=$(BUILD_DIR)/kernel.bin \of=/home/hongbai/bochs/bin/os_hd_60M.img \bs=512 count=200 seek=10 conv=notruncclean:cd $(BUILD_DIR) && rm -f ./*build: $(BUILD_DIR)/kernel.binall: mk_dir build hd

结语

现在是5月13日晚上8点,从前天晚上开始到现在,算是写完了14.3和14.4这部分。后面就是一个个功能的实现了,还是非常期待的。

这周能自由支配的时间比较短,所以一点点写吧,能写多少算多少。

感觉文件这部分想要学好,必须理解虚拟在内存的硬盘和内存之间的通信关系,只能说目前我只能是看懂代码copy一下,还需要进一步学习理解啊。

后续文件操作6章两篇博客,目录操作5章两篇博客,再有4篇博客我们就能完成文件系统这一部分了。

相关文章:

《操作系统真象还原》第十四章(2)——文件描述符、文件操作基础函数

文章目录 前言文件描述符简介文件描述符原理文件描述符实现修改thread.h修改thread.c 文件操作相关的基础函数inode操作相关函数文件相关函数编写file.h编写file.c 目录相关函数完善fs/dir.h编写fs/dir.c 路径解析相关函数实现文件检索功能修改fs.h继续完善fs.c makefile 结语 …...

k8s v1.26 实战csi-nfs 部署

一 前言 使用自开发的一键k8s基础环境部署后&#xff0c;存储需要解决&#xff0c;就是测试环境故选择nfs比较简单&#xff0c;翻阅很多网上资料感觉都不是很全面&#xff0c;结合网上资料折腾了一天&#xff0c;总算是完成了csi-nfs部署。其实之前也部署过&#xff0c;经过一…...

测试集群的功能-执行wordcount程序

具体的操作步骤如下&#xff1a; 确保hadoop是正确运行的。hdfs和yarn都正常启动了。在集群根目录下创建wcinput目录&#xff0c;并在它的下面上传两个文本文件word1.txt, word2.txt&#xff0c;其中保存了要测试的单词信息。在任意一台设备中&#xff0c;进入到hadoop的主目录…...

聊一聊Electron中Chromium多进程架构

Chromium 多进程架构概述 Chromium 的多进程架构是其核心设计之一&#xff0c;旨在提高浏览器的稳定性、安全性和性能。Chromium 将不同的功能模块分配到独立的进程中&#xff0c;每个进程相互隔离&#xff0c;避免了单进程架构中一个模块的崩溃导致整个浏览器崩溃的问题。 在…...

虹科技术 | 简化汽车零部件测试:LIN/CAN总线设备的按键触发功能实现

汽车零部件测试领域对操作的便捷性要求越来越高&#xff0c;虹科Baby-LIN-RC系列产品为这一需求提供了完美的解决方案。从基础的按键设置到高级的Shift键应用&#xff0c;本文将一步步引导您了解虹科Baby-LIN-RC系列产品的智能控制之道。 虹科Baby-LIN-3-RC 想象一下&#xff0…...

前端开发避坑指南:React 代理配置常见问题与解决方案

前端开发避坑指南:React 代理配置常见问题与解决方案 一、为什么需要配置代理?二、使用 create-react-app 默认配置代理三、使用 http-proxy-middleware 配置复杂代理四、高级代理配置五、生产环境中的代理配置一、为什么需要配置代理? React 应用在开发过程中经常需要与后端…...

43、Server.UrlEncode、HttpUtility.UrlDecode的区别?

Server.UrlEncode 和 HttpUtility.UrlDecode 是 .NET 中用于处理 URL 编码/解码的两个不同方法&#xff0c;主要区别在于所属命名空间、使用场景和具体行为。以下是详细对比&#xff1a; 1. 所属类库与命名空间 Server.UrlEncode 属于 System.Web.HttpServerUtility 类。通常…...

Azure 应用的托管身份与服务主体

Microsoft Entra ID -- 前称 Azure Active Directory -- 提供强大的身份验证和授权功能。托管身份和服务主体通过限制凭据暴露的风险来帮助确保对 Azure 资源的访问安全。 托管身份为Azure原生应用程序自动管理身份&#xff0c;而服务主体则非常适合需要访问Azure资源的外部应…...

音频特征工具Librosa包的使用

深入探索Mamba模型架构与应用 - 商品搜索 - 京东 DeepSeek大模型高性能核心技术与多模态融合开发 - 商品搜索 - 京东 要使用深度学习与语音特征进行抽取&#xff0c;首先需要准备能够对语音特征进行解析的工具。 Librosa是一个用于音频、音乐分析与处理的Python工具包&#x…...

Mapreduce初使用

&#xff08;一&#xff09;MapReduce的定义 MapReduce是一个分布式运算程序的编程框架&#xff0c;是用户开发“基于Hadoop的数据分析应用”的核心框架。 MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序&#xff0c;并发运行在一个…...

Samtec助力电视广播行业

【摘要前言】 现代广播电视技术最有趣的方面之一就是界限的模糊。过去&#xff0c;音频和视频是通过射频电缆传输的模拟技术采集的&#xff0c;而现在&#xff0c;数字世界已经取代了模拟技术。物理胶片和磁带已让位于数字存储设备和流媒体。 在这个过程中&#xff0c;连接器…...

根据输入的数据渲染柱形图

背景&#xff1a;根据不同季度的销售额&#xff0c;生成对应的柱形图&#xff0c;直观的看出差异 效果&#xff1a; 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatibl…...

Three.js模型材质调整与性能优化实战

一、材质基础调整 1.1 颜色与透明度控制 通过Material.color属性可直接修改材质颜色&#xff1a; material.color new THREE.Color(0xff0000); // 红色结合opacity属性实现透明效果&#xff1a; material.opacity 0.5; // 50%透明度如需动态调整&#xff0c;可通过Color.…...

QEMU模拟32位ARM实现自定义系统调用

实现自定义系统调用 如何使用 QEMU 模拟32位 ARM 环境参考&#xff1a;使用Qemu模拟32位ARM系统 修改linux内核源码 使用 linux-4.4.240 源码&#xff0c;下载链接&#xff1a;下载链接 在 arch\arm\include\uapi\asm\unistd.h 文件下新增系统调用 sys_test&#xff1a; /…...

AWS IoT Core自定义域名配置实战指南

在使用AWS IoT Core时,配置自定义域名可以为您的IoT解决方案带来多方面的好处。本文将详细介绍如何在AWS IoT Core中设置和使用自定义域名,包括证书配置、域名设置以及DNS记录创建等关键步骤。 为什么要使用自定义域名? 使用自定义域名(也称为客户管理的域名)有以下几个主要…...

【C盘空间不足怎么办】

C 盘空间不足是一个常见的问题&#xff0c;即使感觉没怎么用&#xff0c;也可能被各种文件悄悄占满。这里分析一下常见的原因和排查方向&#xff1a; 一、 常见的空间占用大户&#xff1a; Windows 系统文件和更新&#xff1a; Windows Update 缓存&#xff1a; 系统更新后&am…...

workman进阶应用 GatewayWorker 仿微信 做聊天室

聊天室主要用到GatewayWorker &#xff0c;它是对workerman的进一步封装 GatewayWorker基于Workerman开发的一个项目框架&#xff0c;用于快速开发TCP长连接应用&#xff0c;例如app推送服务端、即时IM服务端、物联网、智能家居等等。 1. 安装环境 1.1 首先下载框架 compos…...

WebRTC技术EasyRTC嵌入式音视频通信SDK打造远程实时视频通话监控巡检解决方案

一、方案概述​ 在现代工业生产、基础设施维护等领域&#xff0c;远程监控与巡检工作至关重要。传统的监控与巡检方式存在效率低、成本高、实时性差等问题。EasyRTC作为一种先进的实时音视频通信技术&#xff0c;具备低延迟、高稳定性、跨平台等特性&#xff0c;能够有效解决这…...

window 显示驱动开发-创建分配时指定段

显示微型端口驱动程序指定并返回有关其内存段的信息&#xff0c;当视频内存管理器调用驱动程序的 DxgkDdiCreateAllocation 函数时&#xff0c;它更喜欢视频内存管理器使用这些信息。 在调用 DxgkDdiCreateAllocation 时&#xff0c;驱动程序为视频资源创建分配。 驱动程序在描…...

如何创建企业微信应用,如何给企业微信发送消息

首先打开你的企业微信 然后在下面创建应用 我创建的是 pes 设置域名和白名单 等你要开发的时候 就需要配置了 然后就能直接在本地发送企业微信消息了 切记 要配置白名单ip 如果要获取所有部门信息&#xff0c;旧得接口已经不能用了&#xff0c;只能获取所有部门id 前提是…...

WM_TIMER定时器消息优先级低,可能会被系统丢弃,导致定时任务无法正常执行

之前在优化电子白板绘制曲线功能时就遇到WM_TIMER定时器消息被丢弃的问题。原先在绘制曲线图元时&#xff0c;左键按下后一直不放&#xff0c;拖动鼠标绘制曲线&#xff08;不断绘制一些小线段形成曲线&#xff09;&#xff0c;等到左键弹起后完成一个完整曲线的绘制&#xff0…...

在Babylon.js中实现完美截图的艺术:包含Canvas和HTML覆盖层

在现代Web 3D应用开发中&#xff0c;Babylon.js作为强大的3D引擎被广泛应用。一个常见的需求是实现场景截图功能&#xff0c;特别是当场景中包含HTML覆盖层(如UI控件、菜单等)时。本文将深入探讨如何在Babylon.js中实现完整的截图方案。 问题背景 这里我是希望实现一个渐隐的…...

mac 10.15.7 svn安装

macOS 版本推荐 SVN 安装方式≤10.14Homebrew 安装独立 SVN≥10.15优先使用 CLT 自带 SVN 一、使用 brew 安装 &#xff08;没成功&#xff09; brew install subversion 这个方法安装一直不成功&#xff0c;一直在提示说版本旧或都是一些引用工具安装失败&#xff0c; 二、使…...

文件同步2

请大家思考如何使用scp命令去解决这个问题。 有两种思路&#xff1a; 第一种&#xff1a;三个文件一个一个去拷贝。缺点是操作麻烦&#xff0c;要逐一操作。 第二种&#xff1a;重新把A上的conf拷贝到B上。缺点是会重复拷贝文件1&#xff0c;2&#xff0c;3&#xff0c;4。 …...

el-select 结合 el-tree:树形下拉数据

一、单选 <template><div class"selectTree-wapper"><el-selectv-model"selectValue"placeholder"请选择"popper-class"custom-el-select-class"ref"selectRef"clearableclear"clearHandle">&…...

GOOSE 协议中MAC配置

在 GOOSE&#xff08;Generic Object Oriented Substation Event&#xff09;协议中&#xff0c;主站&#xff08;Publisher&#xff09;发送的 MAC 地址不需要与从站&#xff08;Listener&#xff09;的 MAC 地址一致&#xff0c;其通信机制与 MAC 地址的匹配逻辑取决于 GOOSE…...

11. CSS从基础样式到盒模型与形状绘制

在前端开发中&#xff0c;CSS&#xff08;层叠样式表&#xff09;是控制网页样式和布局的核心技术。整理了关于 CSS 基础样式、文本样式、盒模型以及形状绘制的一些心得。以下是详细的学习笔记。 一、基础样式设置 1. 字体样式 字体样式是网页视觉呈现的重要组成部分&#xf…...

【springcloud学习(dalston.sr1)】项目整体介绍(含源代码)(一)

当前项目是用来记录下以前学习过的springcloud的dalston.sr1版本&#xff0c;该版本目前来看已经过时了&#xff0c;这里仅做下学习记录分享&#xff08;当前推荐学习spring cloud alibaba&#xff09;。 springcloud主要用于大型项目&#xff0c;比如有一个电商项目&#xff…...

集成DHTMLX 预订排期调度组件实践指南:如何实现后端数据格式转换

在企业级应用中&#xff0c;预订系统&#xff08;Booking System&#xff09;作为典型的调度类应用&#xff0c;广泛用于酒店、会议室、设备预约、医疗排班等业务场景。而DHTMLX Scheduler作为一款功能强大且高度可定制的 JavaScript 日程安排控件&#xff0c;已成为众多开发者…...

ROS多机集群组网通信(四)——Ubuntu 20.04图形化配置 Ad-Hoc组网通信指南

引言 在我之前的文章中已经讲解过Ad-Hoc网络的相关概念&#xff0c;以及如何使用网卡配置Ad-Hoc模式&#xff0c;实现局域网无中心路由通信。这篇文章主要讲解如何在ubuntu20.04上使用图形化配置工具来更方便的配置Ad-Hoc网络&#xff0c;实现组网通信。下面先复习一下之前的相…...

S7-1200 PLC与梅特勒-托利多IND360称重仪表通信

以下是使用西门子进行通信的方法及接线说明&#xff0c;基于常见的工业通信方案&#xff08;如Modbus RTU或Modbus TCP&#xff09;。由于IND360通常支持Modbus协议&#xff0c;而S7-1200需通过附加模块或库实现通信&#xff0c;以下分两种场景说明。 一、通信方案选择 Modbus …...

网络安全侦察与漏洞扫描One-Liners

在网络安全领域&#xff0c;侦察&#xff08;Reconnaissance&#xff09;和漏洞扫描是发现潜在安全风险的重要步骤。本文整合了一系列高效的命令行工具和脚本&#xff0c;涵盖子域名枚举、漏洞扫描、资产发现和信息提取等技术&#xff0c;旨在为安全研究人员和渗透测试人员提供…...

React Native告别图标体积大手动更换慢的噩梦:让图标更新像修改文字一样简单

写在前面:凌晨三点的图标战争 “所有图标都要换成圆角风格,明天上线!”——产品经理这条消息弹出时,我的保温杯差点从手中滑落。扫了一眼项目中的347个图标文件,我知道今晚又是个不眠夜。但就在绝望之际,同事发来一个GIF:他只是在终端输入了iconfont-rn --update,所有…...

【机器学习赋能的智能光子学器件系统研究与应用】

在人工智能与光子学设计融合的背景下&#xff0c;科研的边界持续扩展&#xff0c;创新成果不断涌现。从理论模型的整合到光学现象的复杂模拟&#xff0c;从数据驱动的探索到光场的智能分析&#xff0c;机器学习正以前所未有的动力推动光子学领域的革新。据调查&#xff0c;目前…...

信奥赛-刷题笔记-队列篇-T2-P1540机器翻译和P2952Cow Line S

总题单 本部分总题单如下 【腾讯文档】副本-CSP-JSNOI 题单 (未完待续) https://docs.qq.com/sheet/DSmJuVXR4RUNVWWhW?tabBB08J2 队列篇题单 P1540 [NOIP 2010 提高组] 机器翻译 https://www.luogu.com.cn/problem/P1540 题目背景 NOIP2010 提高组 T1 题目描述 小晨…...

ESP32C3连接wifi

文章目录 &#x1f527; 一、ESP32-C3 连接 Wi-Fi 的基本原理&#xff08;STA 模式&#xff09;✅ 二、完整代码 注释讲解&#xff08;适配 ESP32-C3&#xff09;&#x1f4cc; 三、几个关键点解释&#x1f51a; 四、小结 &#x1f527; 一、ESP32-C3 连接 Wi-Fi 的基本原理&a…...

nvidia驱动更新-先卸载再安装-ubuntu

显卡驱动升级前&#xff0c;卸载旧版本&#xff0c;可采用两种方式。 1.命令行 &#xff08;1&#xff09;查找已安装的 NVIDIA 驱动和相关包&#xff1a;dpkg -l | grep nvidia &#xff08;2&#xff09;完全卸载 NVIDIA 驱动&#xff1a;sudo apt remove purge nvidia-*…...

SparkSQL 连接 MySQL 并添加新数据:实战指南

SparkSQL 连接 MySQL 并添加新数据&#xff1a;实战指南 在大数据处理中&#xff0c;SparkSQL 作为 Apache Spark 的重要组件&#xff0c;能够方便地与外部数据源进行交互。MySQL 作为广泛使用的关系型数据库&#xff0c;与 SparkSQL 的结合可以充分发挥两者的优势。本文将详细…...

Tomcat与纯 Java Socket 实现远程通信的区别

Servlet 容器​​&#xff08;如 Tomcat&#xff09; 是一个管理 Servlet 生命周期的运行环境&#xff0c;主要功能包括&#xff1a; ​​协议解析​​&#xff1a;自动处理 HTTP 请求/响应的底层协议&#xff08;如报文头解析、状态码生成&#xff09;&#xff1b; ​​线程…...

Ubuntu 18.04.6下OpenSSL与OpenSSH版本升级

文章目录 升级背景下载必要软件包安装 zlib创建目录解压文件安装前置依赖离线安装依赖编译安装 zlib 安装 OpenSSL检查当前版本创建安装目录下载并解压 OpenSSL配置与安装验证安装解决动态库依赖问题永久更新环境变量安装OpenSSL常见错误 离线安装 Telnet 服务端指南1. 在联网机…...

BFS算法篇——从晨曦到星辰,BFS算法在多源最短路径问题中的诗意航行(下)

文章目录 引言一、01矩阵1.1 题目链接&#xff1a;https://leetcode.cn/problems/01-matrix/description/1.2 题目分析&#xff1a;1.3 思路讲解&#xff1a;1.4 代码实现&#xff1a; 二、飞地的数量2.1 题目链接&#xff1a;https://leetcode.cn/problems/number-of-enclaves…...

Cold Diffusion: Inverting Arbitrary Image Transforms Without Noise论文阅读

冷扩散&#xff1a;无需噪声的任意图像变换反转 摘要 标准扩散模型通常涉及两个核心步骤&#xff1a;图像降质 &#xff08;添加高斯噪声&#xff09;和图像恢复 &#xff08;去噪操作&#xff09;。本文发现&#xff0c;扩散模型的生成能力并不强烈依赖于噪声的选择&#xf…...

c++进阶——哈希表的实现

文章目录 哈希表的实现unordered_map和unordered_set哈希的引入散列的一些基本概念将Key转成整形和哈希函数哈希冲突负载因子 开放定址法和链地址法哈希函数的选取除法散列法/除留余数法乘法散列法全域散列法(了解)其他方法&#xff08;了解&#xff09; 针对于开放定址法的哈希…...

visual studio生成动态库DLL

visual studio生成动态库DLL 创建动态库工程 注意 #include “pch.h” 要放在上面 完成后点击生成 创建一个控制台项目 设置项目附加目录为刚才创建的动态库工程Dll1&#xff1a; 配置附加库目录&#xff1a; 配置动态库的导入库&#xff08;.lib&#xff09;&#xff1a;链…...

逆强化学习IRL在医疗行为模式研究中的应用

逆强化学习(Inverse Reinforcement Learning, IRL)通过从专家行为中推断潜在奖励函数,近年来在医疗领域的患者行为模式分析中展现出重要价值。 以下是相关研究的具体分析: 1. 脓毒症治疗策略优化 研究背景:脓毒症治疗依赖复杂的临床决策,但传统强化学习需预先定义奖励…...

niushop单商户V5多门店版V5.5.0全插件+商品称重、商家手机端+搭建环境教程

一.系统介绍 【全开源】niushop单商户V5多门店版V5.5.0版本&#xff0c;我看很多人都想要 商品称重、商家手机端等插件这套是全插件版本&#xff0c;整合起来本博主也花了不少啦~ Niushop系统是应用thinkphp6开发的完善的电商系统&#xff0c;拥有完善的商品机制&#xff0c;…...

Kafka Go客户端--Sarama

Kafka Go客户端 在Go中里面有三个比较有名气的Go客户端。 Sarama:用户数量最多&#xff0c;早期这个项目是在Shopify下面&#xff0c;现在挪到了IBM下。segmentio/kafka-go:没啥大的缺点。confluent-kafka-go&#xff1a;需要启用cgo,跨平台问题比较多&#xff0c;交叉编译也…...

Python打卡 DAY 24

知识点回顾&#xff1a; 1. 元组 2. 可迭代对象 3. os模块 作业&#xff1a;对自己电脑的不同文件夹利用今天学到的知识操作下&#xff0c;理解下os路径。 OS 模块 import os # os是系统内置模块&#xff0c;无需安装 获取当前工作目录 os.getcwd() # get current working…...

为什么hadoop不用Java的序列化?

Java的序列化是一个重量级序列化框架&#xff08;Serializable&#xff09;&#xff0c;一个对象被序列化后&#xff0c;会附带很多额外的信息&#xff08;各种校验信息&#xff0c;Header&#xff0c;继承体系等&#xff09;&#xff0c;不便于在网络中高效传输。所以&#xf…...

《类和对象(下)》

引言&#xff1a; 书接上回&#xff0c;如果说类和对象&#xff08;上&#xff09;是入门阶段&#xff0c;类和对象&#xff08;中&#xff09;是中间阶段&#xff0c;那么这次的类和对象&#xff08;下&#xff09;就可以当做类和对象的补充及收尾。 一&#xff1a;再探构造…...