操作系统真象还原:编写硬盘驱动程序

第13章-编写硬盘驱动程序

这是一个网站有所有小节的代码实现,同时也包含了Bochs等文件

13.1 硬盘及分区表

13.1.1 创建从盘及获取安装的磁盘数

要实现文件系统,必须先有个磁盘介质,虽然咱们己经有个虚拟磁盘 hd60M.img,但它只充当了启动盘的作用,仅用来存储内核,是个没有文件系统的裸盘( raw disk)

如同我们之前第一章创建主盘时一样,在bochs目录下:

创建磁盘

bin/bximage

然后在输入框依次输入以下,输入一个,按一次回车

1

hd

flat

80

hd80M.img

接下来,我们在bochsrc.disk文件中,写入

ata0-slave: type=disk, path="hd80M.img", mode=flat,cylinders=162,heads=16,spt=63
这样,bochs虚拟机启动时,就会识别这个磁盘并且自动挂载。

xp:用来查看物理地址处的值,eg:xp/b 0x475查看0x475处一个字节的值。这个0x475处存储的是主机上安装的硬盘数量。下面是成功安装了两个硬盘:
在这里插入图片描述

13.1.2 创建磁盘分区表

**文件系统是运行在操作系统中的软件模块,是操作系统提供的一套管理磁盘文件读写的方法和数据组织、存储形式,**因此,文件系统=数据结构+算法,哈哈,所以它是程序。它的管理对象是文件,管辖范围是分区,因此它建立在分区的基础上,每个分区都可以有不同的文件系统。咱们刚创建了磁盘而己,磁盘还是裸盘,即传说中的 raw disk,本节的任务是把刚创建的磁盘 hd80M.img 分区。

磁盘的物理结构:

  1. 盘片:类似光盘中的一个圆盘,上面布满了磁性介质。
  2. 扇区:扇区是硬盘读写的基本单位,它在磁道上均匀分布,与磁头和磁道不同,扇区从 1 开始编号扇区的大小字节数=256 × N. N为自然数,通常取2,因此扇区的大小为512字节
  3. 磁道:盘片上的一个个同心圈就是磁道,它是扇区的载体,每一个磁道由外向里从 0 开始编号
  4. 磁头:就是磁头,哈哈,可以粗略理解为磁带中的磁头一个盘片分为上下两个面,各面都有一个磁头,因此一个盘片包括两个磁头,磁头号就表示盘面,平时所说的盘面号就是磁头号
  5. 柱面:这些由不同盘面上的编号相同的磁道(这些编号相同的同心圆大小一致)从上到下所组成的圆柱体的回转面就称为柱面,因此柱面的大小等于盘面数(磁头数〉乘以每磁道扇区数。
  6. 分区:是由多个编号连续的柱面组成的,因此分区在物理上的表现是由某段范围内的所有柱面组成的通心环,并不是像“饼图”那种逻辑表示,当然若整个硬盘只有 1 个分区,那这个分区就是个所有柱面组成的圆柱体 。 分区不能跨柱面,也就是同一个柱面不能包含两个分区,一个柱面只属于一个分区,分区的起始和终止都落在完整的柱面上,并不会出现多个分区共享同一柱面的情况,这就是所谓的“分区粒度”

在这里插入图片描述

  • 硬盘容量=单片容量 x 磁头数;
  • 单片容量=每磁道扇区数 x 磁道数 x 512;

磁道数又等于柱面数:硬盘容量=每磁道扇区数 x 柱面数 x 512 x 磁头数;

下面是hd80M.img的分区布局图:

在这里插入图片描述

13.2 编写硬盘驱动程序

13.2.1 硬盘初始化

硬件是实实在在的东西,要想在软件中管理它们,只能从逻辑上抓住这些硬件的特性,将它们抽象成一些数据结构,然后这些数据结构便代表了硬件,用这些数据结构来组织硬件的信息及状态,在逻辑上硬件就是这数据结构。硬盘也是“实在”的东东,为了管理它们还是得将它们抽象成某些数据结构,这就是本节的任务之一。

硬盘上有两个 ata 通道,也称为 IDE 通道。第 1 个 ata 通道上的两个硬盘(主和从)的中断信号挂在 8259A 从片的 IRQ14 上,第 2 个 ata 通道接在 8259A 从片的 IRQ15 上,该 ata 通道上可支持两个硬盘 。 来自 8259A从片的中断是由 8259A 主片帮忙向处理器传达的, 8259A 从片是级联在 8259A 主片的 IRQ2 接口的,因此为了让处理器也响应来自 8259A 从片的中断,屏蔽中断寄存器必须也把 IRQ2 打开.

由于我们的两个磁盘都是挂在了IDE通道0上,而IDE通道0又是挂在了IRQ14线上,所以我们只需要再打开这条线的中断信号就行

/* 初始化可编程中断控制器8259A */
static void pic_init(void) {

   /* 初始化主片 */
   outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
   outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片. 
   outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式, 正常EOI

   /* 初始化从片 */
   outb (PIC_S_CTRL, 0x11);	// ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_S_DATA, 0x28);	// ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
   outb (PIC_S_DATA, 0x02);	// ICW3: 设置从片连接到主片的IR2引脚
   outb (PIC_S_DATA, 0x01);	// ICW4: 8086模式, 正常EOI

 
   outb (PIC_M_DATA, 0xf8);    //IRQ2用于级联从片,必须打开,否则无法响应从片上的中断主片上打开的中断有IRQ0的时钟,IRQ1的键盘和级联从片的IRQ2,其它全部关闭
   outb (PIC_S_DATA, 0xbf);    //打开从片上的IRQ14,此引脚接收硬盘控制器的中断 

   put_str("   pic_init done\n");
}

在以前,我们内核态下进行打印一直用的console_put_xxx之类的函数,这很不方便,因为我们经常打印信息需要调用console_put_int, console_put_str, console_put_ch这三个函数配合使用。所以我们先来实现一个类似于用户态函数printf的内核态函数printk

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-28 17:33:08
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-05-06 12:59:59
 * @FilePath: /OS/chapter13/13.2/kernel/stdio-kernel.c
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#include "stdio-kernel.h"
#include "console.h"
#include "stdint.h"
#include "global.h"
#include "stdio.h"

#define va_start(args, first_fix) args = (va_list)&first_fix
#define va_end(args) args = NULL


/*供内核使用的格式化输出函数*/
void printk(const char* format, ...){
    va_list args;
    va_start(args, format);
    char buf[1024] = {0};
    vsprintf(buf,format,args);
    va_end(args);
    console_put_str(buf);
}

创建硬盘相关的数据结构:

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-28 17:37:39
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-05-06 13:18:20
 * @FilePath: /OS/chapter13/13.2/device/ide.h
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#ifndef __DEVICE_IDE_H
#define __DEVICE_IDE_H

#include "stdint.h"
#include "list.h"
#include "bitmap.h"
#include "global.h"
#include "thread.h"
#include "sync.h"

struct partition{
    uint32_t start_lba; //起始扇区
    uint32_t sec_cnt;   //扇区数
    struct disk* my_disk;   //分区所属的硬盘
    struct list_elem part_tag;  //用于队列中的标记 
    char name[8];       //分区名称
    struct super_block* sb; //本分区的超级块
    struct bitmap block_bitmap; //块位图
    struct bitmap inode_bitmap; //i结点位图
    struct list open_inodes;    //本分区打开的i结点队列
};

/*硬盘结构*/
struct disk{
    char name[8];   //本硬盘的名称
    struct ide_channel* my_channel;     //此块硬盘归属于那个ide通道
    uint8_t dev_no;     //本硬盘是主0,还是从1
    struct partition prim_parts[4];     //主分区顶多只有4个
    struct partition logic_parts[8];    //逻辑分区数量无限,但总得有个上限支持,这里设置为8
};

/*ata通道*/
//port_base咱们这里只处理两个通道的主板,每个通道的
//端口范围是不一样的,通道1(Primary通道)的命令块寄存器端口范围是 Ox1FO~Ox1F7,控制块寄存器
//端口是 0x3F6,通道 2 ( Secondary 通道〉命令块寄存器端口范围是 Ox170~Ox177 ,控制块寄存器端口是0x376 
//通道 l 的端口可以以 0x1F0 为基数,其命令块寄存器端口在此基数上分别加上 0~ 7 就可以了,
//控制块寄存器端口在此基数上加上 0x206,同理,通道 2 的基数就是 0xl70 
struct ide_channel{
    char name[8];       //本ata通道名称
    uint16_t port_base;     //本通道的起始端口号
    uint8_t irq_no;     //本通道所用的中断号
    struct lock lock;   //通道锁
    bool expecting_intr;    //表示等待硬盘的中断
    struct semaphore disk_done; //用于阻塞、唤醒驱动程序
    struct disk devices[2]; //一个通道上连接两个硬盘,一主一从
};

创建并初始化

#include "stdint.h"
#include "global.h"
#include "ide.h"
#include "debug.h"
#include "sync.h"
#include "stdio.h"
#include "stdio-kernel.h"
#include "interrupt.h"
#include "memory.h"
#include "debug.h"

/* 定义硬盘各寄存器的端口号,见书p126 */
#define reg_data(channel)	 (channel->port_base + 0)
#define reg_error(channel)	 (channel->port_base + 1)
#define reg_sect_cnt(channel)	 (channel->port_base + 2)
#define reg_lba_l(channel)	 (channel->port_base + 3)
#define reg_lba_m(channel)	 (channel->port_base + 4)
#define reg_lba_h(channel)	 (channel->port_base + 5)
#define reg_dev(channel)	 (channel->port_base + 6)
#define reg_status(channel)	 (channel->port_base + 7)
#define reg_cmd(channel)	 (reg_status(channel))
#define reg_alt_status(channel)  (channel->port_base + 0x206)
#define reg_ctl(channel)	 reg_alt_status(channel)

/* reg_alt_status寄存器的一些关键位,见书p128 */
#define BIT_STAT_BSY	 0x80	      // 硬盘忙
#define BIT_STAT_DRDY	 0x40	      // 设备准备好	 
#define BIT_STAT_DRQ	 0x8	      // 数据传输准备好了

/* device寄存器的一些关键位 */
#define BIT_DEV_MBS	0xa0	    // 第7位和第5位固定为1
#define BIT_DEV_LBA	0x40        //指定为LBA寻址方式
#define BIT_DEV_DEV	0x10        //指定主盘或从盘,DEV位为1表示从盘,为0表示主盘

/* 一些硬盘操作的指令 */
#define CMD_IDENTIFY	   0xec	    // identify指令
#define CMD_READ_SECTOR	   0x20     // 读扇区指令
#define CMD_WRITE_SECTOR   0x30	    // 写扇区指令

/* 定义可读写的最大扇区数,调试用的 */
#define max_lba ((80*1024*1024/512) - 1)	// 只支持80MB硬盘

uint8_t channel_cnt;	   // 记录通道数
struct ide_channel channels[2];	 // 有两个ide通道


/*硬盘数据结构初始化*/
void ide_init(void){
    printk("ide_init start\n");
    uint8_t hd_cnt = *((uint8_t*)(0x475));  //获取硬盘的数量
    printk("   ide_init hd_cnt:%d\n",hd_cnt);
    ASSERT(hd_cnt > 0);
    list_init(&partition_list);
    channel_cnt = DIV_ROUND_UP(hd_cnt,2);   //一个 ide 通道上有两个硬盘,根据硬盘数量反推有几个 ide 通道
    struct ide_channel* channel;
    uint8_t channel_no = 0, dev_no = 0;

    /*处理每个通道上的硬盘*/
    while(channel_no < channel_cnt){
        channel = &channels[channel_no];
        sprintf(channel->name,"ide%d",channel_no);

        /*为每个 ide 通道初始化端口基址及中断向量*/
        switch(channel_no){
            case 0:
                channel->port_base = 0x1f0; //ide0通道的起始端口号是0x1f0
                channel->irq_no = 0x20+14;  //从片8259A上倒数第二个中断引脚 硬盘,也就是ide0通道的中断向量号 , 0x20为起始中断号
                break;
            case 1:
                channel->port_base = 0x170; //ide1通道的起始端口号是0x170
                channel->irq_no = 0x20+15;  //从片上最后一个中断引脚,我们用来相应ide1通道上的硬盘中断
                break;
        }
        channel->expecting_intr = false;    //未向硬盘写入指令时不期待硬盘的中断
        lock_init(&channel->lock);

        /*初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程,直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程*/
        sema_init(&channel->disk_done,0);
        register_handler(channel->irq_no,intr_hd_handler);
        /*分别获取两个硬盘的参数及分区*/
        while(dev_no < 2){
            struct disk* hd = &channel->devices[dev_no];
            hd->my_channel = channel;
            hd->dev_no = dev_no;
            sprintf(hd->name,"sd%c",'a'+channel_no*2+dev_no);
            identify_disk(hd);//获取硬盘参数
            if(dev_no!=0){  //内核本身的裸硬盘(hd60M.img)不处理
                partition_scan(hd,0);   //扫描该硬盘上的分区
            }
            p_no=0,l_no=0;
            dev_no++;
        }
        dev_no = 0;
        channel_no++;   //下一个channel
    }
    printk("\n all partition info\n");
    /*打印所有分区信息*/
    list_traversal(&partition_list,partition_info,(int)NULL);
    printk("ide_init done\n");
}

在物理地址0x475存储着主机上安装的硬盘数量,它是由BIOS检测并写入的。

13.2.2 实现thread_yied和idle线程

thread_yield 定义在也read.c 中,它的功能是主动把 CPU 使用权让出来,它与thread_block 的区别是thread_yield 执行后任务的状态是 TASK_READY,即让出 CPU 后,它会被加入到就绪队列中,下次还能继续被调度器调度执行,而 thread_block 执行后任务的状态是 TASK_BLOCKD,需要被唤醒后才能加入到就绪队列 , 所以下次执行还不知道是什么时候 。

硬盘是一个相对于CPU来说及其低速的设备,所以,当硬盘在进行需要长时间才能完成的工作时(比如写入数据),我们最好能让驱动程序把CPU让给其他任务。所以,我们来实现一个thread_yield函数,就是用于把CPU让出来。实质就是将调用者重新放入就绪队列队尾。

修改thread.c

/* 主动让出cpu,换其它线程运行 */
void thread_yield(void) {
   struct task_struct* cur = running_thread();   
   enum intr_status old_status = intr_disable();
   ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
   list_append(&thread_ready_list, &cur->general_tag);
   cur->status = TASK_READY;
   schedule();
   intr_set_status(old_status);
}

thread_yield中有个关中断的操作,会不会导致切换后由于关闭中断,而不响应时钟中断导致一直运行在切换后的进程/线程中呢?其实并不会,我们讨论两种情况,一种是进程/线程第一次上机运行,一种是进程/线程之前已经运行过,但由于时间片到期而换下过处理器。对于前者,我们进程/线程第一次上机运行都会经过kernel_thread这个线程启动器,而这个里面是有开中断的代码的。对于后者,当切换回进程/线程时,它们执行kernel.S中的中断退出代码jmp intr_exit,这里面有一条指令iretd会打开中断,让处理器能够继续响应中断代理发送来的中断信号。

接下来我们实现一个idle线程,用于在就绪队列为空时运行。需要注意一点:我们之前没有idle线程,我们的系统没有出现书上说的由于就绪队列为空然后被ASSERT(!list_empty(&thread_ready_list);悬停的情况,是因为我们的主线程(简单理解,就是main函数里面的while(1))会一直被不断加入就绪队列,所以就绪队列并不存在为空的时候。

修改thread.c

struct task_struct* idle_thread;    // idle线程

/* 系统空闲时运行的线程 */
static void idle(void* arg UNUSED) {
   while(1) {
      thread_block(TASK_BLOCKED);     
      //执行hlt时必须要保证目前处在开中断的情况下,hlt是停止处理器将进入暂停状态,直到发生硬件中断
      asm volatile ("sti; hlt" : : : "memory");
   }
}


/* 实现任务调度 */
void schedule() {
   ASSERT(intr_get_status() == INTR_OFF);
   struct task_struct* cur = running_thread(); 
   if (cur->status == TASK_RUNNING) { // 若此线程只是cpu时间片到了,将其加入到就绪队列尾
      ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
      list_append(&thread_ready_list, &cur->general_tag);
      cur->ticks = cur->priority;     // 重新将当前线程的ticks再重置为其priority;
      cur->status = TASK_READY;
   } 
   else { 
      /* 若此线程需要某事件发生后才能继续上cpu运行,
      不需要将其加入队列,因为当前线程不在就绪队列中。*/
   }

      /* 如果就绪队列中没有可运行的任务,就唤醒idle */
   if (list_empty(&thread_ready_list)) {
      thread_unblock(idle_thread);
   }

   ASSERT(!list_empty(&thread_ready_list));
   thread_tag = NULL;	  // thread_tag清空
/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
   thread_tag = list_pop(&thread_ready_list);   
   struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
   next->status = TASK_RUNNING;
   process_activate(next); //激活任务页表
   switch_to(cur, next);   
}


/* 初始化线程环境 */
void thread_init(void) {
   put_str("thread_init start\n");
   list_init(&thread_ready_list);
   list_init(&thread_all_list);
   lock_init(&pid_lock);
/* 将当前main函数创建为线程 */
   make_main_thread();
      /* 创建idle线程 */
   idle_thread = thread_start("idle", 10, idle, NULL);
   put_str("thread_init done\n");
}

13.2.3 实现简单的休眠函数

硬盘和 CPU 是相互独立的个体,它们各自并行执行,但由于硬盘是低速设备,其在处理请求时往往消耗很长的时间(不过手册上说最慢的情况也能在 31 秒之内完成),为避免浪费 CPU 资源,在等待硬盘操作的过程中最好把 CPU 主动让出来,让 CPU 去执行其他任务,为实现这种“明智”的行为,我们在 timer.c中定义休眠函数,当然这只是简易版,精度不是很高,能达到目的就可以了

之前我们实现的thread_yield是将当前任务加入就绪队列队尾,仅仅是把CPU让出来一次。我们来实现一个定时让出CPU的函数,也就是让一个任务在固定时间内都不执行。

修改timer.c

#define mil_seconds_per_intr (1000 / IRQ0_FREQUENCY)

/* 以tick为单位的sleep,任何时间形式的sleep会转换此ticks形式 */
static void ticks_to_sleep(uint32_t sleep_ticks) {
   uint32_t start_tick = ticks;
   /* 若间隔的ticks数不够便让出cpu */
   while (ticks - start_tick < sleep_ticks) {
      thread_yield();
   }
}

/* 以毫秒为单位的sleep   1秒= 1000毫秒 */
void mtime_sleep(uint32_t m_seconds) {
   uint32_t sleep_ticks = DIV_ROUND_UP(m_seconds, mil_seconds_per_intr);
   ASSERT(sleep_ticks > 0);
   ticks_to_sleep(sleep_ticks); 
}
13.2.4 完善硬盘驱动程序

在这里插入图片描述

现在,我们来实现驱动程序的主体部分,也就是实际与硬盘打交道的函数,实质就是将一系列寄存器操作进行封装

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-29 09:54:07
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-05-06 13:17:42
 * @FilePath: /OS/chapter13/13.2/device/ide.c
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#include "ide.h"
#include "stdint.h"
#include "global.h"
#include "list.h"
#include "bitmap.h"
#include "debug.h"
#include "stdio.h"
#include "thread.h"
#include "sync.h"
#include "io.h"
#include "timer.h"
#include "string.h"
#include "stdio-kernel.h"


/*定义硬盘各寄存器的端口号*/
#define reg_data(charnnel)  (charnnel->port_base + 0)
#define reg_error(charnnel)  (charnnel->port_base + 1)
#define reg_sect_cnt(charnnel)  (charnnel->port_base + 2)
#define reg_lba_l(charnnel)  (charnnel->port_base + 3)
#define reg_lba_m(charnnel)  (charnnel->port_base + 4)
#define reg_lba_h(charnnel)  (charnnel->port_base + 5)
#define reg_dev(charnnel)  (charnnel->port_base + 6)
#define reg_status(charnnel)  (charnnel->port_base + 7)
#define reg_cmd(charnnel)  (reg_status(charnnel))
#define reg_alt_status(charnnel)  (charnnel->port_base + 0x206)
#define reg_ctl(charnnel)  (reg_alt_status(charnnel))

/*reg_alt_status寄存器的一些关键位*/
#define BIT_ALT_STAT_BSY    0x80    //硬盘忙
#define BIT_ALT_STAT_DRDY   0x40    //驱动器准备好了
#define BIT_ALT_STAT_DRQ    0x8    //数据传输准备好了
#define BIT_ALT_STAT_ERR    0x1    //有错误发生

/*device寄存器的一些关键位*/
#define BIT_DEV_MBS 0xa0        //第7位和第5位固定位1
#define BIT_DEV_LBA 0x40        //指定为LBA寻址方式
#define BIT_DEV_DEV 0x10        //主盘还是从盘 现在为1是从盘,为0表示主盘

/*一些硬盘操作的指令*/
#define CMD_IDENTIFY    0xec    //identify指令,即硬盘识别
#define CMD_READ_SECTOR   0x20  //读扇区指令
#define CMD_WRITE_SECTOR   0x30 //写扇区指令

/*定义可读写的最大扇区数,调试用的*/
#define max_lba ((80*1024*1024/512)-1)  //只支持80M硬盘

uint8_t channel_cnt;    //按硬盘数计算的通道数
struct ide_channel channels[2]; //有两个ide通道


/*选择读写的硬盘*/
static void select_disk(struct disk* hd){
    uint8_t reg_device = BIT_DEV_MBS | BIT_DEV_LBA;
    if(hd->dev_no == 1){    //若是从盘就置 DEV 位为 1
        reg_device |= BIT_DEV_DEV;
    }
    
    outb(reg_dev(hd->my_channel),reg_device);
}

/*向硬盘控制器写入起始扇区地址及腰读写的扇区数*/
static void select_sector(struct disk* hd, uint32_t lba, uint8_t sec_cnt){
    ASSERT(lba <= max_lba);
    struct ide_channel* channel = hd->my_channel;

    /*写入要读写的扇区数*/
    outb(reg_sect_cnt(channel),sec_cnt); //如果 sec_cnt 为 0 ,贝Jj表示写入 256 个扇区

    /*写入lba地址,即扇区号*/
    outb(reg_lba_l(channel),lba);   //lba 地址的低8位,不用单独取出低8位,outb 函数中的汇编指令 outb %b0,%w1会只用al
    outb(reg_lba_m(channel),lba>>8); //lba地址的8-15位
    outb(reg_lba_h(channel),lba>>16); //lba地址的16-23位

    /*因为 lba 地址的第 24 ~27 位要存储在 device 寄存器的0-3 位,无法单独写入这 4 位,所以在此处把 device 寄存器再重新写入一次*/
    outb(reg_dev(channel),BIT_DEV_MBS|BIT_DEV_LBA|(hd->dev_no==1 ? BIT_DEV_DEV : 0)|lba>>24);
}

/*向通道channel发命令cmd*/
static void cmd_out(struct ide_channel* channel,uint8_t cmd){
    /*要向硬盘发出了命令便将此标记置为true,硬盘中断处理程序需要根据它来判断*/
    channel->expecting_intr = true;
    outb(reg_cmd(channel),cmd);
}

/*硬盘读入sec_cnt个扇区的数据到buf*/
static void read_from_sector(struct disk* hd, void* buf, uint8_t sec_cnt){
    uint32_t size_in_byte;
    if(sec_cnt==0){
        /*因为 sec_cnt 是自位变量,由主调函数将其赋值时,若为 256 则将最高位的 1 丢掉变为 0*/
        size_in_byte = 256 * 512;
    }else{
        size_in_byte = sec_cnt * 512;
    }
    insw(reg_data(hd->my_channel),buf,size_in_byte / 2);
}

/*将buf中国的sec_cnt扇区的数据写入硬盘*/
static void write2sector(struct disk* hd, void* buf, uint8_t sec_cnt){
    uint32_t size_in_byte;
    if(sec_cnt==0){
        /*因为 sec_cnt 是自位变量,由主调函数将其赋值时,若为 256 则将最高位的 1 丢掉变为 0*/
        size_in_byte = 256 * 512;
    }else{
        size_in_byte = sec_cnt * 512;
    }
    outsw(reg_data(hd->my_channel),buf,size_in_byte/2);
}

/*等待30秒*/
static bool busy_wait(struct disk* hd){
    struct ide_channel* channel = hd->my_channel;
    uint16_t time_limit = 30 * 1000;    //等待30000毫秒
    while(time_limit -= 10 >= 0){
        if(!(inb(reg_status(channel))&BIT_ALT_STAT_BSY))
            return (inb(reg_status(channel))&BIT_ALT_STAT_DRQ);
        else    
            mtime_sleep(10);        //睡眠10毫秒
    }
    return false;
}

/*硬盘读取 sec_cnt 个扇区到buf*/
void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt){
    ASSERT(lba <= max_lba);
    ASSERT(sec_cnt > 0);
    lock_acquire(&hd->my_channel->lock);
    
    /*1先选择操作的硬盘*/
    select_disk(hd);
    uint32_t secs_op;   //每次操作的扇区数
    uint32_t secs_done = 0; //已完成的扇区数

    while(secs_done < sec_cnt){
        if((secs_done+256)<=sec_cnt)
            secs_op = 256;
        else 
            secs_op = sec_cnt - secs_done;

        /*2写入待读入的扇区数和起始扇区号*/
        select_sector(hd,lba+secs_done,secs_op);

        /*3执行的命令写入reg_cmd寄存器*/
        cmd_out(hd->my_channel,CMD_READ_SECTOR);    //准备开始读取数据

        /*******************  阻塞自己的时机  ****************************
         * 在硬盘已经开始工作(开始在内部读数据或写数据)后才能阻塞自己,
         * 现在硬盘已经开始忙了,将自己阻塞,等待硬盘完成读操作后通过中断处理程序唤醒自己
        */
       /*硬盘完成操作后会发中断信号,后面介绍的硬盘中断处理程序 intr_hd_handler 会在该通道上执行“ sema_up(&channel->disk_done)”,从而唤醒当前的驱动程序*/
        sema_down(&hd->my_channel->disk_done);

        /*4 检查硬盘转改是否可读*/
        /*醒来后开始执行下面的代码*/
        if(!busy_wait(hd)){ //若失败
            char error[64];
            sprintf(error,"%s read sector %d failed!!!!!!",hd->name,lba);
            PANIC(error);
        }

        /*5 把数据从硬盘的缓冲区读出*/
        read_from_sector(hd,(void*)((uint32_t)buf + secs_done * 512),secs_op);
        secs_done += secs_op;
    }
    lock_release(&hd->my_channel->lock);
}

/*对于读硬盘来说,驱动程序阻塞自己是在硬盘开始读扇区之后,对于写硬盘来说,
驱动程序阻塞自己是在硬盘开始写扇区之后。总之,阻塞的时机一定是在硬盘开始真正忙活之后的那段“漫
长”的时间里*/

/*将buf中sec_cnt扇区数据写入硬盘*/
void ide_write(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt){
    ASSERT(lba<=max_lba);
    ASSERT(sec_cnt>0);
    lock_acquire(&hd->my_channel->lock);

    /*1.线选择操作的硬盘*/
    select_disk(hd);

    uint32_t secs_op;   //每次操作的扇区数
    uint32_t secs_done = 0; //已经完成的扇区数
    while(secs_done < sec_cnt){
        if((secs_done+256)<=sec_cnt)
            secs_op = 256;
        else 
            secs_op = sec_cnt - secs_done;

        /*2 写入待写入的扇区数和起始扇区号*/
        select_sector(hd, lba+secs_done, secs_op);

        /*3 执行的命令写入reg_cmd寄存器*/
        cmd_out(hd->my_channel,CMD_WRITE_SECTOR);
        
        /*4 检查硬盘状态是否可写*/
        if(!busy_wait(hd)){ //若失败
            char error[64];
            sprintf(error,"%s write sector %d failed!!!!!!",hd->name,lba);
            PANIC(error);
        }

        /*5 把数据从硬盘的缓冲区写进去*/
        write2sector(hd, (void*)((uint32_t)buf + secs_done * 512),secs_op);
        
        /*在硬盘响应期间阻塞自己*/
        sema_down(&hd->my_channel->disk_done);
        secs_done += secs_op;
    }
    /*醒来后开始释放锁*/
    lock_release(&hd->my_channel->lock);
}

/*硬盘中断处理程序*/
void intr_hd_handler(uint8_t irq_no){
    ASSERT(irq_no == 0x2e || irq_no == 0x2f);
    uint8_t ch_no = irq_no - 0x2e;
    struct ide_channel* channel = &channels[ch_no];
    ASSERT(channel->irq_no == irq_no);
    /*不必担心此中断是否对应的是这一次的 expecting_intr,每次读写硬盘时会申请锁,从而保证了同步一致性*/
    if(channel->expecting_intr){
        channel->expecting_intr = false;
        sema_up(&channel->disk_done);

        /*读取状态寄存器使硬盘控制器认为此次的中断已被处理,从而硬盘可以继续执行新的读写*/
        inb(reg_status(channel));
    }
}


/*硬盘数据结构初始化*/
void ide_init(void){
    printk("ide_init start\n");
    uint8_t hd_cnt = *((uint8_t*)(0x475));  //获取硬盘的数量
    printk("   ide_init hd_cnt:%d\n",hd_cnt);
    ASSERT(hd_cnt > 0);
    list_init(&partition_list);
    channel_cnt = DIV_ROUND_UP(hd_cnt,2);   //一个 ide 通道上有两个硬盘,根据硬盘数量反推有几个 ide 通道
    struct ide_channel* channel;
    uint8_t channel_no = 0, dev_no = 0;

    /*处理每个通道上的硬盘*/
    while(channel_no < channel_cnt){
        channel = &channels[channel_no];
        sprintf(channel->name,"ide%d",channel_no);

        /*为每个 ide 通道初始化端口基址及中断向量*/
        switch(channel_no){
            case 0:
                channel->port_base = 0x1f0; //ide0通道的起始端口号是0x1f0
                channel->irq_no = 0x20+14;  //从片8259A上倒数第二个中断引脚 硬盘,也就是ide0通道的中断向量号 , 0x20为起始中断号
                break;
            case 1:
                channel->port_base = 0x170; //ide1通道的起始端口号是0x170
                channel->irq_no = 0x20+15;  //从片上最后一个中断引脚,我们用来相应ide1通道上的硬盘中断
                break;
        }
        channel->expecting_intr = false;    //未向硬盘写入指令时不期待硬盘的中断
        lock_init(&channel->lock);

        /*初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程,直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程*/
        sema_init(&channel->disk_done,0);
        register_handler(channel->irq_no,intr_hd_handler);
        /*分别获取两个硬盘的参数及分区*/
        while(dev_no < 2){
            struct disk* hd = &channel->devices[dev_no];
            hd->my_channel = channel;
            hd->dev_no = dev_no;
            sprintf(hd->name,"sd%c",'a'+channel_no*2+dev_no);
            identify_disk(hd);//获取硬盘参数
            if(dev_no!=0){  //内核本身的裸硬盘(hd60M.img)不处理
                partition_scan(hd,0);   //扫描该硬盘上的分区
            }
            p_no=0,l_no=0;
            dev_no++;
        }
        dev_no = 0;
        channel_no++;   //下一个channel
    }
    printk("\n all partition info\n");
    /*打印所有分区信息*/
    list_traversal(&partition_list,partition_info,(int)NULL);
    printk("ide_init done\n");
}

select_disk接受一个参数,硬盘指针hd,功能是选择待操作的硬盘是主盘或从盘 。原理是利用device寄存器中的 dev 位,该位为 0 表示是通道中的主盘,为 1 表示是通道的从盘。最后通过 outb 函数将变量 reg_device 写入硬盘所在通道的device 寄存器,这样就完成了主盘或从盘的选择。

select_sector接受3个参数,硬盘指针hd、扇区起始地址lba、扇区数sec_cnt,功能是向硬盘控制器写入起始扇区地址及要读写的扇区数 。

cmd_out:向通道channel发命令cmd,即写入操作命令(读或者写)

read_from_sector:硬盘读入sec_cnt个扇区的数据到buf

write2sector:将buf中的sec_cnt扇区的数据写入硬盘

busy_wait :通过BIT_ALT_STAT_BSY 判断 status 寄存器的 BSY 位是否为 1 ,如果为 1 ,则表示硬盘繁,这时候就调用 mtime_sleep(10)去休眠 10 毫秒。如果 BSY 位为 0 则表示硬盘不忙,接着在再次读取status寄存器,返回其 DRQ 位的值, DRQ 位为 1 表示硬盘己经准备好数据了。

对于读硬盘来说,驱动程序阻塞自己是在硬盘开始读扇区之后,对于写硬盘来说,驱动程序阻塞自己是在硬盘开始写扇区之后。总之,阻塞的时机一定是在硬盘开始真正忙活之后的那段“漫长”的时间里

13.2.5 获取硬盘信息,扫描分区表

本节该是检验它们的时候了,咱们用两件工作来验证,一是向硬盘发 identify 命令获取硬盘的信息,二是扫描分区表。

在这里插入图片描述

Linux中所有的设备都在/dev/目录下,硬盘命名规则是[x]d[y][n],其中只有字母 d 是固定的,其他带中括号的字符都是多选值,下面从左到右介绍各个字符。

x:表示硬盘分类,硬盘有两大类, IDE 磁盘和 SCSI 磁盘。h代表 IDE 磁盘, s代表 SCSI 磁盘,故 x取值为h和s.

d:表示disk,即磁盘

y:表示设备号,更多操作以区分第几个设备,取值范围是小写字符,其中a是第1个硬盘,b是第2个硬盘依此类推。

n:表示分区号。也就是一个硬盘上的第几个分区。分区以数字 1开始,依次类推。

咱们这里统一用 SCSI 硬盘的命名规则来命名虚拟硬盘hd60M.img 和 hd80M且恕。其中 hd60M.img 为 sda, hd80M.img 为 sdb o hd60M.img 是裸盘,没有文件系统和分区,因此咱们只处理 hd80M.img,将其上的主分区占据 sdb【1 ~4】,逻辑分区占据 sdb【5~】。

修改ide.c

/*用于记录总扩展分区的起始 lba ,初始为 O, partition_scan 时以此为标记*/
int32_t ext_lba_base = 0;
uint8_t p_no = 0, l_no = 0; //用来记录硬盘主分区和逻辑分区的下标
struct list partition_list; //分区队列

/*构建一个 16 字节大小的结构体,用来存分区表项*/
struct partition_table_entry{
    uint8_t bootable;   //是否可引导
    uint8_t start_head; //起始磁头号
    uint8_t start_sec;  //起始扇区号
    uint8_t start_chs;  //起始柱面号
    uint8_t fs_type;    //分区类型
    uint8_t end_head;   //结束磁头号
    uint8_t end_sec;    //结束扇区号
    uint8_t end_chs;    //结束柱面号

    /*更需要关注的时下面这两项*/
    uint32_t start_lba; //本分区起始扇区的 lba 地址
    uint32_t sec_cnt;  //本分区的扇区数目
}__attribute__ ((packed));  //保证此结构是16字节大小

/*引导扇区,mbr或ebr所在扇区*/
struct boot_sector{
    uint8_t other[446]; //引导代码
    struct partition_table_entry partition_table[4];    //分区表中有四项,供六十四字节
    uint16_t signature; //启动扇区的结束标志是 Ox55,0xaa,
}__attribute__ ((packed));

/*将dst中len个相邻字节交换位置后存入buf*/
static void swap_pairs_bytes(const char* dst, char* buf, uint32_t len){
    uint8_t idx;
    for(idx=0;idx<len;idx+=2){
        /*buf中存储dst中两相邻袁术交换位置后的字符串*/
        buf[idx+1] = *dst++;
        buf[idx] = *dst++;
    }
    buf[idx] = '\0';
}

/*获取硬盘参数信息*/
static void identify_disk(struct disk* hd){
    char id_info[512];
    select_disk(hd);
    cmd_out(hd->my_channel,CMD_IDENTIFY);
    /*向硬盘发送指令后便通过信号量阻塞自己,待硬盘处理完成后,通过中断处理程序将自己唤醒*/
    sema_down(&hd->my_channel->disk_done);

    /*醒来后开始执行下面的代码*/
    if(!busy_wait(hd)){ //若失败
        char error[64];
        sprintf(error,"%s identify failed!!!!!!",hd->name);
        PANIC(error);
    }
    read_from_sector(hd,id_info,1);

    char buf[64];
    uint8_t sn_start = 10*2, sn_len = 20, md_start = 27*2, md_len = 40;
    swap_pairs_bytes(&id_info[sn_start],buf,sn_len);
    printk(" disk %s info:\n SN: %s\n",hd->name,buf);
    memset(buf,0,sizeof(buf));
    swap_pairs_bytes(&id_info[md_start],buf,md_len);
    printk("    MODULE: %s\n",buf);
    uint32_t sectors = *(uint32_t*)&id_info[60 * 2];
    printk("    SECTORS: %d\n",sectors);
    printk("    CAPACITY: %dMB\n",sectors*512/1024/1024);
}

/*扫描硬盘 hd 中地址为 ext_lba 的扇区中的所有分区*/
static void partition_scan(struct disk* hd, uint32_t ext_lba){
    struct boot_sector* bs = sys_malloc(sizeof(struct boot_sector));
    ide_read(hd, ext_lba, bs, 1);
    uint8_t part_idx = 0;
    struct partition_table_entry* p = bs->partition_table;

    /*遍历分区表4个分区表项*/
    while(part_idx++<4){
        if(p->fs_type == 0x5){  //若为扩展分区
            if(ext_lba_base!=0){
                /*子扩展分区的 start_lba 是相对于主引导扇区中的总扩展分区地址*/
                partition_scan(hd,p->start_lba + ext_lba_base);
            }else{
                //ext_lba_base为0表示是第一次读取引导块,页就是主引导记录所在的扇区

                /*记录下扩展分区的起始lba地址,后面所有扩展分区地址都相对于此*/
                ext_lba_base = p->start_lba;
                partition_scan(hd,p->start_lba);
            }
        }else if(p->fs_type != 0){ //若是有效分区类型
            if(ext_lba == 0){   //此时全是主分区
                hd->prim_parts[p_no].start_lba = ext_lba + p->start_lba;
                hd->prim_parts[p_no].sec_cnt = p->sec_cnt;
                hd->prim_parts[p_no].my_disk = hd;
                list_append(&partition_list,&hd->prim_parts[p_no].part_tag);
                sprintf(hd->prim_parts[p_no].name,"%s%d",hd->name,p_no+1);
                p_no++;
                ASSERT(p_no < 4);   //0,1,2,3
            }else{  //逻辑分区
                hd->logic_parts[l_no].start_lba = ext_lba + p->start_lba;
                hd->logic_parts[l_no].sec_cnt = p->sec_cnt;
                hd->logic_parts[l_no].my_disk = hd;
                list_append(&partition_list, &hd->logic_parts[l_no].part_tag);
                sprintf(hd->logic_parts[l_no].name, "%s%d", hd->name, l_no + 5);	 // 逻辑分区数字是从5开始,主分区是1~4.
                l_no++;
                if (l_no >= 8)    // 只支持8个逻辑分区,避免数组越界
                    return;
            }   
        }
        p++;
    }
    sys_free(bs);
}

/*打印分区信息*/
static bool partition_info(struct list_elem* pelem, int arg UNUSED){
    struct partition* part = elem2entry(struct partition, part_tag, pelem);
    printk("    %s start_lba:0x%x,sec_cnt:0x%x\n",part->name,part->start_lba,part->sec_cnt);

    /*在此处 return false与函数本身功能无关,只是为了让主调函数 list_traversal 继续向下遍历元素*/
    return false;  
}

swap_pairs_bytes:用来处理identify命令的返回信息,硬盘参数信息是以字为单位的,包括偏移、长度的单位都是字,在这16位的字中,相邻字符的位置是互换的,所以通过此函数来做转换。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

党建科普3D数字化展馆支持实时更新迭代

3D虚拟策展逐渐成为新时代下的主流方式&#xff0c;深圳华锐视点作为专业的web3d开发公司&#xff0c;具有专业化的3D数字化空间还原能力&#xff0c;能根据企业/个人不同需求和预算&#xff0c;为您打造纯线上虚拟3D艺术展&#xff0c;让您彻底摆脱实体美术馆的限制&#xff0…

好看的风景视频素材在哪下载啊?下载风景视频素材网站分享

随着短视频和自媒体的兴起&#xff0c;美丽的风景视频不仅能让人眼前一亮&#xff0c;更能吸引大量观众。无论是旅游博主分享那些令人心旷神怡的旅行片段&#xff0c;还是视频编辑师寻找背景素材来增强作品的视觉效果&#xff0c;高质量的风景视频素材需求量巨大。以下是几个下…

2024年上半年典型网络攻击事件汇总

文章目录 前言一、Ivanti VPN 的0 Day攻击(2024年1月)二、微软公司高管账户泄露攻击(2024年1月)三、Change Healthcare网络攻击(2024年2月)四、ConnectWise ScreenConnect漏洞利用攻击(2024年2月)五、XZ Utils软件供应链攻击(2024年3月)六、AT&T数据泄露攻击(20…

Continual Test-Time Domain Adaptation--论文笔记

论文笔记 资料 1.代码地址 https://github.com/qinenergy/cotta 2.论文地址 https://arxiv.org/abs/2203.13591 3.数据集地址 论文摘要的翻译 TTA的目的是在不使用任何源数据的情况下&#xff0c;将源预先训练的模型适应到目标域。现有的工作主要考虑目标域是静态的情况…

【数据分享】《中国建筑业统计年鉴》2005-2022 PDF

而今天要免费分享的数据就是2005-2022年间出版的《中国建筑业统计年鉴》并以多格式提供免费下载。&#xff08;无需分享朋友圈即可获取&#xff09; 需要2023的数据的请添加小编咨询 数据介绍 在过去的十八个年头中&#xff0c;中国建筑业经历了翻天覆地的变化。从《中国建…

web自动化(三)鼠标操作键盘

selenuim 键盘操作 import timefrom selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait from selen…

windows@无密码的本地用户账户相关问题@仅用用户名免密登录远程桌面登录和控制@无密码用户访问共享文件夹以及挂载问题

文章目录 abstract此用户无法登录账户被禁用问题访问共享文件夹时带上凭据错误案例和解决 两类登录方式控制台登录与远程登录的区别为什么限制空密码账户只允许控制台登录相关安全策略如何修改该策略注意事项 启用允许被免密登录功能使用空密码进行远程桌面连接设置远程桌面链接…

day02-广播机制

广播机制 广播是numpy对不同形状的数组进行数值计算的方式&#xff0c;对数组的算术运算通常在相应的元素上进行 1.如果两个数组a和b形状相同&#xff0c;即满足a.shape b.shape&#xff0c;那么a*b的结果就是a与b数组对应位相乘。这要求维数相同且各维度的长度相同 a np.a…

干货:科技论文写作保姆级攻略

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。科技论文是报道自然科学研究或技术开发工作成果的论说文章。通常基于概念、判断、推理、证明或反驳等逻辑思维体系&#xff0c;使用实验调研或理论计算等研究手段&#xff0c;按照特定格式撰写完成。 科技论文可以粗略分为…

在Linux操作环境下搭建内网源

在修改配置文件之前都应该有备份。 比如在/目录下专门创建一个目录用来储存文件的备份。 1.安装vsftpd软件 首先使用命令yum search ftpd 来查看当前Linux操作系统下是否有ftpd软件。 随后使用yum install vsftpd&#xff0c;来安装vsftpd软件 2.修改vsftpd的配置文件&…

Linux系统之玩转SafeLine防火墙应用

Linux系统之玩转SafeLine防火墙应用 一、SafeLine介绍1.1SafeLine简介1.2 SafeLine功能1.3 SafeLine 的工作原理二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本四、部署SafeLine4.1 安…

Picocli 开发命令行工具

大家有没有想过用Java开发个命令行程序呢&#xff1f;给大家介绍个框架Picocli。 官方文档&#xff1a;https://picocli.info/ 推荐从官方提供的快速入门教程开始&#xff1a;https://picocli.info/quick-guide.html 什么的Picocli Picocli 是一个单文件框架&#xff0c;几乎…

基于Web技术的教育辅助系统设计与实现(SpringBoot MySQL)+文档

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

GNeRF代码复现

https://github.com/quan-meng/gnerf 之前一直去复现这个代码总是文件不存在&#xff0c;我就懒得搞了&#xff08;实际上是没能力哈哈哈&#xff09; 最近突然想到这篇论文重新试试复现 一、按步骤创建虚拟环境安装各种依赖等 二、安装好之后下载数据&#xff0c;可以用Blen…

汽车电子工程师入门系列——汽车操作系统架构学习研究-AUTOSAR

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

MySQL高级-MVCC-原理分析(RR级别)

文章目录 1、RR隔离级别下&#xff0c;仅在事务中第一次执行快照读时生成ReadView&#xff0c;后续复用该ReadView2、总结 1、RR隔离级别下&#xff0c;仅在事务中第一次执行快照读时生成ReadView&#xff0c;后续复用该ReadView 而RR 是可重复读&#xff0c;在一个事务中&…

Java后端每日面试题(day2)

目录 Session和Cookie的关系Cookie与Session的区别JWT 由哪些部分组成&#xff1f;如何防止 JWT 被篡改&#xff1f;JWT 的特点 Session和Cookie的关系 Session和Cookie都可以用来实现跟踪用户状态&#xff0c;而二者是关系的&#xff1a;Session的实现依赖于Cookie。 Session…

字符串知识点

API API和API帮助文档 API:目前是JDK中提供的各种功能的Java类。 这些类将底层的实现封装了起来&#xff0c;我们不需要关心这些类是如何实现的&#xff0c;只需要学习这些类如何使用即可。 API帮助文档&#xff1a;帮助开发人员更好的使用API和查询API的一个工具。 String概…

Eslint与Prettier搭配使用

目录 前置准备 Eslint配置 Prettier配置 解决冲突 前置准备 首先需要安装对应的插件 然后配置settings.json 点开之后就会进入settings.json文件里&#xff0c;加上这两个配置 // 保存的时候自动格式化 "editor.formatOnSave": true, // 保存的时候使用prettier进…

无法定位程序输入点Z9 qt assertPKcS0i于动态链接库F:\code\projects\06_algorithm\main.exe

解决方法&#xff1a; 这个报错&#xff0c;是因为程序在运行时没要找到所需的dll库&#xff0c;如果把这个程序方法中对应库的目录下执行&#xff0c;则可正常执行。即使将图中mingw_64\bin 环境变量上移到msvc2022_64\bin 之前也不可以。 最终的解决方法是在makefile中设置环…