《操作系统真象还原》第十三章——磁盘驱动程序

文件系统磁盘创建

创建磁盘

进入bochs安装目录,输入以下命令

./bin/bximage

然后按照以下步骤创建硬盘 

 修改硬盘配置

vim boot.disk

添加以下代码行

ata0-slave: type=disk, path="hd80M.img", mode=flat,cylinders=162,heads=16,spt=63

 完整配置如下

# 设置 Bochs 在运行过程中能够使用的内存,本例为 32MB
megs: 32

# 设置对应真实机器的 BIOS 和 VGA BIOS
romimage: file=/home/minios/bochs/share/bochs/BIOS-bochs-latest
vgaromimage: file=/home/minios/bochs/share/bochs/VGABIOS-lgpl-latest

# 设置 Bochs 使用的磁盘
# floppya: 1_44=a.img, status=inserted

# 选择启动盘符
# boot: flopy # 默认从软盘启动
boot: disk # 从硬盘启动,我们的任何代码都将直接写在硬盘上,所以不会再有读写软盘的操作。

# 设置日志文件输出
log: bochs.out

# 关闭鼠标,打开键盘,按照书上写会报错
mouse: enabled=0
#keyboard: enabled=1,
keyboard: keymap=/home/minios/bochs/share/bochs/keymaps/x11-pc-us.map


# 硬盘设置
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14

#新加入的代码,虚拟硬盘配置,ata0-master表示0号通道主盘
ata0-master: type=disk, path="hd60M.img", mode=flat,cylinders=121,heads=16,spt=63
#从盘硬盘配置,ata0-slave表示0号通道从盘
ata0-slave: type=disk, path="hd80M.img", mode=flat,cylinders=162,heads=16,spt=63

# 增加 Bochs 对 GDB 的支持,GDB 远程连接到此机器的 1234 端口便可调试
#gdbstub: enabled=1, port=1234, test_base=0, data_base=0, bss_base=0

磁盘分区

磁盘的基本结构

CHS 是硬盘分区和寻址的一种方式,全称是 Cylinder-Head-Sector,即 柱面-磁头-扇区。它是硬盘寻址的早期方式,用于标识硬盘上的数据存储位置。通过 CHS,硬盘的每个数据位置都由三个参数指定:

  1. Cylinder(柱面)

    • 也称为 磁道,表示硬盘盘片上的一圈数据存储区域。
    • 硬盘的每个盘面上都有许多圆形的磁道,所有盘面上的同一位置的磁道构成了一个柱面。
    • 每个柱面上的数据存储区域都是相同的。
  2. Head(磁头)

    • 磁头是硬盘的读写头,用来读取或写入数据。
    • 每个硬盘都有多个磁头,通常对应硬盘上的每个盘面。一个硬盘可能有多个盘片,每个盘片上有一个磁头(通常是上下各一个磁头,表示两面都可以读取数据)。
  3. Sector(扇区)

    • 扇区是硬盘的最小数据存储单位,通常大小为 512 字节或 4KB。
    • 硬盘上的每个磁道被分为若干个扇区。每个扇区存储一定量的数据,通常是 512 字节(在现代硬盘中,有些硬盘采用更大的扇区,如 4KB 扇区)。

总结:

  • C:柱面数(Cylinder count)—— 硬盘上的磁道数量。
  • H:磁头数(Head count)—— 硬盘上的磁头数量。通常硬盘有 16 个盘面,每个盘面上有一个磁头。
  • S:每个磁道的扇区数(Sectors per track)—— 每个磁道上存储的扇区数。
MBR分区结构
  • MBR分区分为主分区、扩展分区、逻辑分区
  • 一块硬盘最多只能创建4个主分区
  • 一个扩展分区会占用一个主分区的位置
  • 逻辑分区是基于扩展分区创建出来的,先有扩展分区,然后在扩展分区的基础上再创建逻辑分区
  • 分区是由扇区组成的
创建分区
fdisk ./hd80M.img

帮助文档菜单 

创建主分区

 创建扩展分区

在扩展分区上创建逻辑分区

 查看创建的分区

Command (m for help): p
Disk ./hd80M.img: 79.8 MiB, 83607552 bytes, 163296 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xb865d4d6

Device       Boot  Start    End Sectors  Size Id Type
./hd80M.img1        2048  33263   31216 15.2M 83 Linux
./hd80M.img4       33264 163295  130032 63.5M  5 Extended
./hd80M.img5       35312  51407   16096  7.9M 83 Linux
./hd80M.img6       53456  76607   23152 11.3M 83 Linux
./hd80M.img7       78656  91727   13072  6.4M 83 Linux
./hd80M.img8       93776 121967   28192 13.8M 83 Linux
./hd80M.img9      124016 163295   39280 19.2M 83 Linux

修改逻辑分区的id号,以区别主分区

重新查看创建好的分区

Command (m for help): p
Disk ./hd80M.img: 79.8 MiB, 83607552 bytes, 163296 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xb865d4d6

Device       Boot  Start    End Sectors  Size Id Type
./hd80M.img1        2048  33263   31216 15.2M 83 Linux
./hd80M.img4       33264 163295  130032 63.5M  5 Extended
./hd80M.img5       35312  51407   16096  7.9M 66 unknown
./hd80M.img6       53456  76607   23152 11.3M 66 unknown
./hd80M.img7       78656  91727   13072  6.4M 66 unknown
./hd80M.img8       93776 121967   28192 13.8M 66 unknown
./hd80M.img9      124016 163295   39280 19.2M 66 unknown

 确认写入创建的分区,注意只有写入之后创建的分区才会创建成功

Command (m for help): w
The partition table has been altered.
Syncing disks.

再次确认分区是否创建成功

 fdisk -l ./hd80M.img
Disk ./hd80M.img: 79.8 MiB, 83607552 bytes, 163296 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xb865d4d6

Device       Boot  Start    End Sectors  Size Id Type
./hd80M.img1        2048  33263   31216 15.2M 83 Linux
./hd80M.img4       33264 163295  130032 63.5M  5 Extended
./hd80M.img5       35312  51407   16096  7.9M 66 unknown
./hd80M.img6       53456  76607   23152 11.3M 66 unknown
./hd80M.img7       78656  91727   13072  6.4M 66 unknown
./hd80M.img8       93776 121967   28192 13.8M 66 unknown
./hd80M.img9      124016 163295   39280 19.2M 66 unknown

磁盘初始化

通道、磁盘、分区的关系

  • 通道相当于数据线,需要和机器的接口连接之后机器才能访问磁盘设备
  • 一个接口有两个通道,每个通道有磁盘,分别是主盘和磁盘,磁盘由分区构成

打开硬盘控制器的中断

如图所示,也就是打开引脚IRQ14和IRQ15(第2个 ata 通道接在 8259A从片的IRQ15 上,该 ata 通道上可支持两个硬盘)

另外,来自8259A从片的中断是由8259A主片帮忙向处理器传达的,8259A从片是级联在8259A主片的IRO2接口的,因此为了让处理器也响应来自8259A从片的中断,屏蔽中断寄存器必须也把IRO2打开。

/kernel/interrupt.c

/* 初始化可编程中断控制器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

   /* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
   // outb (PIC_M_DATA, 0xfe);
   // outb (PIC_S_DATA, 0xff);

   /* 测试键盘,只打开键盘中断,其它全部关闭 */
   // outb (PIC_M_DATA, 0xfd);   //键盘中断在主片ir1引脚上,所以将这个引脚置0,就打开了
   // outb (PIC_S_DATA, 0xff);

   // 同时打开时钟中断与键盘中断
   // outb(PIC_M_DATA, 0xfc);
   // outb(PIC_S_DATA, 0xff);

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

初始化数据结构

为了在c语言下管理硬盘,需要初始化对应硬盘的数据结构

/device/ide.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];
    // 表示主盘还是从盘
    uint8_t dev_no;
    // 本磁盘所在的通道
    struct ide_channel *my_channel;
    // 磁盘的主分区,最多四个
    struct partition prim_parts[4];
    // 磁盘的逻辑分区,理论上数量无限,我们这里设定8个
    struct partition logic_parts[8];
};
ata通道结构
/* ata通道结构 */
struct ide_channel
{
    // ata通道名称
    char name[8];
    // 本通道的起始端口号(书p126)
    uint16_t port_base;
    // 本通道所用的中断号
    uint8_t irq_no;
    // ata通道有两个,这两个通道共用一个接口,故而每次访问都需要互斥进行
    struct lock lock;
    // 等待硬盘的中断
    bool expecting_intr;
    // 用于阻塞、唤醒驱动程序
    struct semaphore disk_done;
    // 一个通道上连接两个硬盘,一主一从
    struct disk devices[2];
};
 初始化

必要的数据定义

/* 定义硬盘各寄存器的端口号,见书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()
{
    printk("ide_init start\n");
    uint8_t hd_cnt = *((uint8_t *)(0x475)); // 获取硬盘的数量
    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通道的的中断向量号
            break;
        case 1:
            channel->port_base = 0x170;  // ide1通道的起始端口号是0x170
            channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,我们用来响应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;   // 将硬盘驱动器号置0,为下一个channel的两个硬盘初始化。
        channel_no++; // 下一个channel
    }

    printk("\n   all partition info\n");
    /* 打印所有分区信息 */
    list_traversal(&partition_list, partition_info, (int)NULL);
    printk("ide_init done\n");
}

添加进初始化代码中

/kernel/init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
#include "keyboard.h"
#include "tss.h"
#include "syscall-init.h"
#include "ide.h"

/*负责初始化所有模块 */
void init_all()
{
   put_str("init_all\n");
   idt_init();      // 初始化中断
   mem_init();      // 初始化内存管理系统
   thread_init();   // 初始化线程相关结构
   timer_init();    // 初始化PIT
   console_init();  // 控制台初始化最好放在开中断之前
   keyboard_init(); // 键盘初始化
   tss_init();      // tss初始化
   syscall_init();  // 初始化系统调用
   ide_init();      // 初始化硬盘
}

 实现 thread yield 和 idle 线程

idle主要用于在系统没有其他可运行任务时执行,占用 CPU 资源,防止 CPU 处于空闲状态而浪费掉能量

/thread/thread.c

/*系统空闲时运行的线程*/
static void idle(void *arg UNUSED)
{
   while (1)
   {
      thread_block(TASK_BLOCKED);
      /*
      执行hlt时必须要保证目前处在开中断的情况下
      sti:开中断指令
      hlt:悬停cpu,降低机器cpu功耗消耗
      */
      asm volatile("sti;hlt" ::: "memory");
   }
}

初始化idle线程

/* 初始化线程环境 */
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");
}

 在任务调度函数中添加

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

完整调度函数如下


/* 实现任务调度 */
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;
   // 激活任务页表,同时更新TSS中的esp0的值,让它指向线程/进程的0级栈
   process_activate(next);
   switch_to(cur, next);
}

 可以看到idle的作用主要就是防止cpu空转,从而消耗cpu资源

 实现简单的休眠函数

硬盘和 CPU 是相互独立的个体,它们各自并行执行,但由于硬盘是低速设备,其在处理请求时往往消耗很长的时间,为避免浪费 CPU资源,在等待硬盘操作的过程中最好把 CPU主动让出来,让CPU去执行其他任务,为实现这种“明智”的行为,我们在 timer.c中定义休眠函数

首先是 thread_yield的实现

/thread/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);
}

然后在此基础上实现简单的休眠函数

/device/timer.c

#define IRQ0_FREQUENCY 100 // 中断发生频率,100HZ,即每秒发生中断的次数
#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);
}

 

实现硬盘驱动程序

接下来我们正式实现硬盘驱动程序

/device/ide.c

#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"

#include "io.h"
#include "timer.h"

#include "string.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通道

/* 选择读写的硬盘 */
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,则表示写入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是8位变量,由主调函数将其赋值时,若为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是8位变量,由主调函数将其赋值时,若为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_STAT_BSY))
        {
            return (inb(reg_status(channel)) & BIT_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); // 准备开始读数据

        /*********************   阻塞自己的时机  ***********************
             在硬盘已经开始工作(开始在内部读数据或写数据)后才能阻塞自己,现在硬盘已经开始忙了,
            将自己阻塞,等待硬盘完成读操作后通过中断处理程序唤醒自己*/
        sema_down(&hd->my_channel->disk_done);
        /*************************************************************/

        /* 4 检测硬盘状态是否可读 */
        /* 醒来后开始执行下面代码*/
        if (!busy_wait(hd))
        { // 若失败
            char error[64];
            sprintf(error, "%s read sector %d failed!!!!!!\n", 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!!!!!!\n", 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));
    }
}

/* 用于记录总扩展分区的起始lba,初始为0,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]; // 分区表中有4项,共64字节
    uint16_t signature;                              // 启动扇区的结束标志是0x55,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!!!!!!\n", 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;
}

/* 硬盘数据结构初始化 */
void ide_init()
{
    printk("ide_init start\n");
    uint8_t hd_cnt = *((uint8_t *)(0x475)); // 获取硬盘的数量
    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通道的的中断向量号
            break;
        case 1:
            channel->port_base = 0x170;  // ide1通道的起始端口号是0x170
            channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,我们用来响应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;   // 将硬盘驱动器号置0,为下一个channel的两个硬盘初始化。
        channel_no++; // 下一个channel
    }

    printk("\n   all partition info\n");
    /* 打印所有分区信息 */
    list_traversal(&partition_list, partition_info, (int)NULL);
    printk("ide_init done\n");
}

测试

/kernel/main.c

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

void k_thread_a(void *);
void k_thread_b(void *);
void u_prog_a(void);
void u_prog_b(void);

int main(void)
{
   put_str("I am kernel\n");
   init_all();
   while (1)
      ;
   process_execute(u_prog_a, "u_prog_a");
   process_execute(u_prog_b, "u_prog_b");
   thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
   thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
   while (1)
      ;
   return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void *arg)
{
   void *addr1 = sys_malloc(256);
   void *addr2 = sys_malloc(255);
   void *addr3 = sys_malloc(254);
   console_put_str(" thread_a malloc addr:0x");
   console_put_int((int)addr1);
   console_put_char(',');
   console_put_int((int)addr2);
   console_put_char(',');
   console_put_int((int)addr3);
   console_put_char('\n');

   int cpu_delay = 100000;
   while (cpu_delay-- > 0)
      ;
   sys_free(addr1);
   sys_free(addr2);
   sys_free(addr3);
   while (1)
      ;
}

/* 在线程中运行的函数 */
void k_thread_b(void *arg)
{
   void *addr1 = sys_malloc(256);
   void *addr2 = sys_malloc(255);
   void *addr3 = sys_malloc(254);
   console_put_str(" thread_b malloc addr:0x");
   console_put_int((int)addr1);
   console_put_char(',');
   console_put_int((int)addr2);
   console_put_char(',');
   console_put_int((int)addr3);
   console_put_char('\n');

   int cpu_delay = 100000;
   while (cpu_delay-- > 0)
      ;
   sys_free(addr1);
   sys_free(addr2);
   sys_free(addr3);
   while (1)
      ;
}

/* 测试用户进程 */
void u_prog_a(void)
{
   void *addr1 = malloc(256);
   void *addr2 = malloc(255);
   void *addr3 = malloc(254);
   printf(" prog_a malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);

   int cpu_delay = 100000;
   while (cpu_delay-- > 0)
      ;
   free(addr1);
   free(addr2);
   free(addr3);
   while (1)
      ;
}

/* 测试用户进程 */
void u_prog_b(void)
{
   void *addr1 = malloc(256);
   void *addr2 = malloc(255);
   void *addr3 = malloc(254);
   printf(" prog_b malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);

   int cpu_delay = 100000;
   while (cpu_delay-- > 0)
      ;
   free(addr1);
   free(addr2);
   free(addr3);
   while (1)
      ;
}

编译运行

打印硬盘分区结构

 

参考

linux下使用fdisk进行磁盘分区详解 - 人生的哲理 - 博客园

用《操作系统真象还原》写一个操作系统 第十三章 编写硬盘驱动程序 第四节 获取硬盘信息,扫描分区表_哔哩哔哩_bilibili

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

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

相关文章

快速、可靠且高性价比的定制IP模式提升芯片设计公司竞争力

作者&#xff1a;Karthik Gopal&#xff0c;SmartDV Technologies亚洲区总经理 智权半导体科技&#xff08;厦门&#xff09;有限公司总经理 无论是在出货量巨大的消费电子市场&#xff0c;还是针对特定应用的细分芯片市场&#xff0c;差异化芯片设计带来的定制化需求也在芯片…

v-bind操作class

v-bind操作class 参考文献&#xff1a; Vue的快速上手 Vue指令上 Vue指令下 Vue指令的综合案例 指令的修饰符 文章目录 v-bind操作classv-bind对于样式控制的增强操作class案例(tab导航高亮)操作style操作style案例 结语 博客主页: He guolin-CSDN博客 关注我一起学习&#…

Kubernetes1.28 编译 kubeadm修改证书有效期到 100年.并更新k8s集群证书

文章目录 前言一、资源准备1. 下载对应源码2.安装编译工具3.安装并设置golang 二、修改证书有效期1.修改证书有效期2.修改 CA 证书有效期 三、编译kubeadm四、使用新kubeadm方式1.当部署新集群时,使用该kubeadm进行初始化2.替换现有集群kubeadm操作 前言 kubeadm 默认证书为一…

HarmonyOS NEXT应用开发边学边玩系列:从零实现一影视APP (三、影视搜索页功能实现)

在HarmonyOS NEXT开发环境中&#xff0c;我们可以使用nutpi/axios库来简化网络请求的操作。本文将展示如何使用HarmonyOS NEXT框架和nutpi/axios库&#xff0c;从零开始实现一个简单的影视APP&#xff0c;主要关注影视搜索页的功能实现。 为什么选择nutpi/axios&#xff1f; n…

高级运维:shell练习2

1、需求&#xff1a;判断192.168.1.0/24网络中&#xff0c;当前在线的ip有哪些&#xff0c;并编写脚本打印出来。 vim check.sh #!/bin/bash# 定义网络前缀 network_prefix"192.168.1"# 循环遍历1-254的IP for i in {1..254}; do# 构造完整的IP地址ip"$network_…

好用的php商城源码有哪些?

选择一个优秀的商城工具&#xff0c;能更好地帮助大家建立一个好用的商城系统。目前比较流行的都是开源PHP商城系统&#xff0c;那么现实中都有哪些好用的PHP商城源码值得推荐呢&#xff1f;下面就带大家一起来了解一下。 1.TigShop 【推荐指数】&#xff1a;★★★★★☆ 【推…

Docker Desktop 构建java8基础镜像jdk安装配置失效解决

Docker Desktop 构建java8基础镜像jdk安装配置失效解决 文章目录 1.问题2.解决方法3.总结 1.问题 之前的好几篇文章中分享了在Linux(centOs上)和windows10上使用docker和docker Desktop环境构建java8的最小jre基础镜像&#xff0c;前几天我使用Docker Desktop环境重新构建了一个…

Open FPV VTX开源之嵌入式OSD配置

Open FPV VTX开源之嵌入式OSD配置 1. 源由2. 安装3. 配置步骤一&#xff1a;备份/etc/telemetry.conf步骤二&#xff1a;修改/etc/telemetry.conf步骤三&#xff1a;配置时区步骤四&#xff1a;重启摄像头 4. 实测5. 参考资料 1. 源由 穿越机模拟图传延迟通常在10ms左右。 最…

数据平台浅理解

定义 数据平台架构是指用于收集、存储、处理和分析数据的一系列组件、技术和流程的整体架构设计。它就像是一个复杂的数据生态系统的蓝图&#xff0c;旨在高效地管理数据从产生源头到产生价值的整个生命周期。 主要层次 数据源层 这是数据的起点&#xff0c;包含各种类型的数据…

CSS3的aria-hidden学习

前言 aria-hidden 属性可用于隐藏非交互内容&#xff0c;使其在无障碍 API 中不可见。即当aria-hidden"true" 添加到一个元素会将该元素及其所有子元素从无障碍树中移除&#xff0c;这可以通过隐藏来改善辅助技术用户的体验&#xff1a; 纯装饰性内容&#xff0c;如…

【ArcGIS初学】产生随机点计算混淆矩阵

混淆矩阵&#xff1a;用于比较分类结果和地表真实信息 总体精度(overall accuracy) :指对角线上所有样本的像元数(正确分类的像元数)除以所有像元数。 生产者精度(producers accuracy) &#xff1a;某类中正确分类的像元数除以参考数据中该类的像元数(列方向)&#xff0c;又称…

认识机器学习中的结构风险最小化准则

上一篇文章我们学习了关于经验风险最小化准则&#xff0c;其核心思想是通过最小化训练数据上的损失函数来优化模型参数&#xff0c;从而提高模型在训练集上的表现。但是这也会导致一个问题&#xff0c;经验风险最小化原则很容易导致模型在训练集上错误率很低&#xff0c;但在未…

设计模式-工厂模式/抽象工厂模式

工厂模式 定义 定义一个创建对象的接口&#xff0c;让子类决定实列化哪一个类&#xff0c;工厂模式使一个类的实例化延迟到其子类&#xff1b; 工厂方法模式是简单工厂模式的延伸。在工厂方法模式中&#xff0c;核心工厂类不在负责产品的创建&#xff0c;而是将具体的创建工作…

Chatper 4: Implementing a GPT model from Scratch To Generate Text

文章目录 4 Implementing a GPT model from Scratch To Generate Text4.1 Coding an LLM architecture4.2 Normalizing activations with layer normalization4.3 Implementing a feed forward network with GELU activations4.4 Adding shortcut connections4.5 Connecting at…

Unity ShaderGraph中Lit转换成URP的LitShader

ShaderGraph中的LitShader如下&#xff1a; 在顶点和片元着色器暴露出了上图中的几个参数&#xff0c;要转换成URPLitShaderLab,首先要找到这几个参数&#xff0c;打开LitShader&#xff0c;找到第一个Pass&#xff0c;可以看到下图中的顶点和片元的定义函数&#xff0c;还有引…

uni-app的学习

uni-app 有着跨平台支持、丰富的插件和生态系统、高性能、集成开发工具HBuilderX的配合使用。允许使用者仅通过一套代码发布到多平台使用。 uni-app官网 uni-app 是一个适合开发跨平台移动应用和小程序的框架&#xff0c;能够大幅提高开发效率。 一、了解 1.1 工具准备 从Git…

USRP X310 Windows 烧录镜像

说明 USRP-X 系列设备包含两个用于两个以太网通道的 SFP 端口。由于 SFP 端口支持 1 千兆 (SFP) 和 10 千兆 (SFP) 收发器&#xff0c;因此 UHD 附带了多个 FPGA 图像&#xff0c;以确定上述接口的行为。 注意&#xff1a;Aurora 图像需要从 FPGA 源代码手动构建。 FPGA 图像…

Sprint Boot教程之五十八:动态启动/停止 Kafka 监听器

Spring Boot – 动态启动/停止 Kafka 监听器 当 Spring Boot 应用程序启动时&#xff0c;Kafka Listener 的默认行为是开始监听某个主题。但是&#xff0c;有些情况下我们不想在应用程序启动后立即启动它。 要动态启动或停止 Kafka Listener&#xff0c;我们需要三种主要方法…

Docker save load 镜像 tag 为 <none>

一、场景分析 我从 docker hub 上拉了这么一个镜像。 docker pull tomcat:8.5-jre8-alpine 我用 docker save 命令想把它导出成 tar 文件以便拷贝到内网机器上使用。 docker save -o tomcat-8.5-jre8-alpine.tar.gz 镜像ID 当我把这个镜像传到别的机器&#xff0c;并用 dock…

计算机网络(三)——局域网和广域网

一、局域网 特点&#xff1a;覆盖较小的地理范围&#xff1b;具有较低的时延和误码率&#xff1b;使用双绞线、同轴电缆、光纤传输&#xff0c;传输效率高&#xff1b;局域网内各节点之间采用以帧为单位的数据传输&#xff1b;支持单播、广播和多播&#xff08;单播指点对点通信…