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

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

第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模式, 正常EOIoutb (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,还是从1struct 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通道的起始端口号是0x1f0channel->irq_no = 0x20+14;  //从片8259A上倒数第二个中断引脚 硬盘,也就是ide0通道的中断向量号 , 0x20为起始中断号break;case 1:channel->port_base = 0x170; //ide1通道的起始端口号是0x170channel->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 位为 1reg_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会只用aloutb(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通道的起始端口号是0x1f0channel->irq_no = 0x20+14;  //从片8259A上倒数第二个中断引脚 硬盘,也就是ide0通道的中断向量号 , 0x20为起始中断号break;case 1:channel->port_base = 0x170; //ide1通道的起始端口号是0x170channel->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位的字中,相邻字符的位置是互换的,所以通过此函数来做转换。

相关文章:

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

第13章-编写硬盘驱动程序 这是一个网站有所有小节的代码实现&#xff0c;同时也包含了Bochs等文件 13.1 硬盘及分区表 13.1.1 创建从盘及获取安装的磁盘数 要实现文件系统&#xff0c;必须先有个磁盘介质&#xff0c;虽然咱们己经有个虚拟磁盘 hd60M.img&#xff0c;但它只…...

firewalld防火墙(二)

一&#xff1a;firewalld高级配置 1&#xff1a;关于iptables的知识 iptables 是Linux系统中传统的命令行防火墙管理工具&#xff0c;它基于内核的netfilter框架工作&#xff0c;用于配置和管理网络规则集&#xff0c;比如过滤&#xff08;允许/拒绝&#xff09;进出的数据包…...

Android-悬浮窗口

在Android系统中&#xff0c;如果应用需要弹出一个悬浮窗口&#xff0c;就需要申请一项特殊权限 <uses-permission android:name"android.permission.SYSTEM_ALERT_WINDOW"/>在Android O之前的系统中申请了该权限后&#xff0c;再给对应的window设置 WindowM…...

打破僵局:Foxit Reader无法打开的终极解决方案

打破僵局&#xff1a;Foxit Reader无法打开的终极解决方案 在数字化阅读时代&#xff0c;Foxit Reader作为一款广受欢迎的PDF阅读器&#xff0c;其打不开的问题无疑会给用户带来诸多不便。本文将为您提供全面的解决方案&#xff0c;从基础检查到高级技巧&#xff0c;确保您能够…...

[调试] JTAG下运行正常,从QSPI或者SD卡启动则无响应,如何查找问题

[调试] JTAG下运行正常&#xff0c;从QSPI或者SD卡启动则无响应&#xff0c;如何查找问题 一、问题现象二、用自定义fsbl替代系统默认的fsbl1. 新建fsbl_new2. 如果提示缺少xilffs库3. 使能调试信息输出 三. 启动成功和失败情况下的典型输出1. JTAG启动模式: 正常加载2. QSPI启…...

Linux内核 -- 多线程之wait_event用法

Linux Kernel 中 wait_event 的高级用法及注意事项 在Linux内核编程中&#xff0c;wait_event 系列函数是用于实现进程等待和事件通知机制的重要工具。本文将详细介绍 wait_event 的高级用法以及注意事项。 1. 基本用法 wait_event 系列宏主要包括以下几种形式&#xff1a; …...

双指针系列第 8 篇:盛水最多的容器。几句话讲明白!

Leetcode 题目链接 思路 取首尾双指针和水量如下所示&#xff0c;设高度函数为 h ( i ) h(i) h(i)&#xff0c;在下图中 h ( l ) < h ( r ) h(l) < h(r) h(l)<h(r)。 观察以 l l l 为左边界所能构成的其他水量&#xff0c;与矮的右边界搭配结果如下。 与高的…...

c++高阶-1-模板

文章目录 模板一、模板基本语法二、函数模板1.基本语法2.函数模板注意事项3.普通函数和函数模板区别4.普通函数和函数模板调用规则 三、类模板1.基本语法2.类模板和函数模板的区别3.类模板中成员函数调用时机4.类模板对象做函数参数5.类模板与继承6.成员函数的类外实现 模板 一…...

.net core 的 winform 的 浏览器控件 WebView2

在.NET Core WinForms应用程序中&#xff0c;没有直接的“浏览器控件”&#xff0c;因为WinForms不支持像WebBrowser控件那样的功能。但是&#xff0c;你可以使用WebView2控件&#xff0c;它是一个基于Chromium的浏览器内核&#xff0c;可以在WinForms应用程序中嵌入Web内容。 …...

Django QuerySet对象,all()方法

all()方法 在Django中&#xff0c;all()方法是QuerySet对象的一个方法&#xff0c;用于获取模型的所有实例。 当你调用ModelName.objects.all()时&#xff0c;Django会生成一个SQL查询&#xff0c;从数据库中获取该模型的所有记录&#xff0c;并返回一个QuerySet对象&#xf…...

自动生成网站sitemap

要在 Next.js 和 Contentlayer 项目中实现自动生成 Sitemap 的功能&#xff0c;你可以编写一个脚本&#xff0c;在每次生成文档后自动生成 Sitemap。以下是一个示例脚本&#xff0c;你可以根据自己的需求进行调整。 步骤 1&#xff1a;安装必要的依赖 首先&#xff0c;你需要…...

中国经济昆虫志(55卷)

中国经济昆虫志&#xff0c;共55卷&#xff0c;内容包括概述、形态特征、分类等。各级分类单元均编有检索表&#xff0c;每个种有特征描述、地理分布&#xff0c;有的还记载有生活习性和防治方法。为便于鉴定&#xff0c;绘制有特征图和彩色图。 包括鞘翅目天牛科、半翅目蝽科、…...

linux环境安装elasticsearch缓存数据库和Kibana客户端

linux环境安装elasticsearch缓存数据库&#xff0c;今天我们安装7.17.18版本&#xff0c;并分析遇到的问题。 一、elasticsearch安装运行 1、直接下载 wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.18-linux-x86_64.tar.gz2、解压 tar -…...

OpenSSL的一些使用案例

目录 一、介绍 二、基本使用 1、Shell &#xff08;1&#xff09;文件加解密 &#xff08;2&#xff09;生成密钥文件 2、API &#xff08;1&#xff09;md5sum &#xff08;2&#xff09;AES256加解密 一、介绍 本篇博客重点不是详细描述 OpenSSL 的用法&#xff0c;只…...

常用字符串方法<python>

导言 在python中内置了许多的字符串方法&#xff0c;使用字符串方法可以方便快捷解决很多问题&#xff0c;所以本文将要介绍一些常用的字符串方法。 目录 导言 string.center(width[,fillchar]) string.capitalize() string.count(sub[,start[,end]]) string.join(iterabl…...

线程池666666

1. 作用 线程池内部维护了多个工作线程&#xff0c;每个工作线程都会去任务队列中拿取任务并执行&#xff0c;当执行完一个任务后不是马上销毁&#xff0c;而是继续保留执行其它任务。显然&#xff0c;线程池提高了多线程的复用率&#xff0c;减少了创建和销毁线程的时间。 2…...

Python28-5 k-means算法

k-means 算法介绍 k-means 算法是一种经典的聚类算法&#xff0c;其目的是将数据集分成 ( k ) 个不同的簇&#xff0c;每个簇内的数据点尽可能接近。算法的基本思想是通过反复迭代优化簇中心的位置&#xff0c;使得每个簇内的点与簇中心的距离之和最小。k-means 算法的具体步骤…...

主流国产服务器操作系统技术分析

主流国产服务器操作系统 信创 "信创"&#xff0c;即信息技术应用创新&#xff0c;作为科技自立自强的核心词汇&#xff0c;在我国信息化建设的进程中扮演着至关重要的角色。自2016年起步&#xff0c;2020年开始蓬勃兴起&#xff0c;信创的浪潮正席卷整个信息与通信技…...

【Linux】线程封装与互斥(万字)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 文章目录 前言 C多线程的用法 对原生线程进行一次封装 理解pthread线程 Linux线程互斥 进程线程间的互斥相关背景概念 互斥量mutex 操作共享变量会有问题的售票…...

5分钟教你部署MySQL8.0环境

此方法基于Windows操作系统&#xff01; 一、在MySQL官网单击downloads&#xff08;下载&#xff09;MySQLhttps://www.mysql.com/cn/ 选择在Windows操作系统下载 二、选择合适的版本 推荐下载第二种&#xff0c;安装时离线安装即可 三、安装MySQL8.0 1、找到MySQL下载完成…...

LLM应用:传统NLP任务

LLM出来以后&#xff0c;知乎上就出现了“传统NLP已死”的言论&#xff0c;但是传统NLP真的就被扔进历史的垃圾桶了吗&#xff1f; 其实&#xff0c;尽管LLM具有出色的通用能力&#xff0c;但仍然无法有效应对低资源领域的自然语言处理任务&#xff0c;如小语种翻译。为了更好地…...

基于Hadoop平台的电信客服数据的处理与分析③项目开发:搭建Kafka大数据运算环境---任务11:基础环境准备

任务描述 任务主要是安装配置基础环境&#xff0c;主要内容包括&#xff1a; 1、安装java Kafka和ZooKeeper都需要安装Java环境&#xff0c;推荐至少Java8及以上版本 2、安装ZooKeeper ZooKeeper是Kafka集群的必要组件 3、安装kafka Kafka版本包括使用的scala语言版本和kafka版…...

Golang中swtich中如何强制执行下一个代码块

switch 语句中的 case 代码块会默认带上 break&#xff0c;但可以使用 fallthrough 来强制执行下一个 case 代码块。 package mainimport ("fmt" )func main() {isSpace : func(char byte) bool {switch char {case : // 空格符会直接 break&#xff0c;返回 false…...

读书笔记-Java并发编程的艺术-第4章(Java并发编程基础)-第2节(启动和终止线程)

文章目录 4.2 启动和终止线程4.2.1 构造线程4.2.2 启动线程4.2.3 理解中断4.2.4 过期的suspend()、resume()和stop()4.2.5 安全地终止线程 4.2 启动和终止线程 在前面章节的示例中通过调用线程的start()方法进行启动&#xff0c;随着run()方法的执行完毕&#xff0c;线程也随之…...

通俗大白话理解Docker

什么是Docker Docker本质上是一种容器化技术&#xff0c;用于将应用程序及其所有依赖打包到一个标准化的单元中。这些单元&#xff08;容器&#xff09;可以在任何运行Docker的机器上运行。每个容器是相互隔离的&#xff0c;具有自己的文件系统、网络和进程空间。 以下是大白话…...

题解:CF1981C(Turtle and an Incomplete Sequence)

题解&#xff1a;CF1981C&#xff08;Turtle and an Incomplete Sequence&#xff09; Part 1&#xff1a;题意理解 地址链接&#xff1a;CF、洛谷。题面翻译&#xff1a;给定一个长度为 n n n 的序列 a a a&#xff0c;其中有一些元素未知&#xff0c;用 − 1 -1 −1 表示…...

Swift 中强大的 Key Paths(键路径)机制趣谈(上)

概览 小伙伴们可能不知道&#xff1a;在 Swift 语言中隐藏着大量看似“其貌不扬”实则却让秃头码农们“高世骇俗”&#xff0c;堪称卧虎藏龙的各种秘技。 其中&#xff0c;有一枚“不起眼”的小家伙称之为键路径&#xff08;Key Paths&#xff09;。如若将其善加利用&#xff…...

(十二)纹理和采样

纹理 在绘制三角形的过程中&#xff0c;将图片贴到三角形上进行显示的过程&#xff0c;就是纹理贴图的过程 uv坐标 如果如果图片尺寸和实际贴图尺寸不一致&#xff0c;就会导致像素不够用了的问题 纹理与采样 纹理对象(Texture)&#xff1a;在GPU端&#xff0c;用来以一…...

QT创建地理信息shp文件编辑器shp_editor

空闲之余创建一个简单的矢量shp文件编辑器&#xff0c;加深对shp文件的理解。 一、启动程序 二、打开shp文件 三、显示shp文件的几何图形 四、双击右边表格中的feature&#xff0c;主窗体显示选中feature的各个节点。 五、鼠标在主窗体中选中feature的节点&#xff0c;按鼠标左…...

解析Kotlin中扩展函数与扩展属性【笔记摘要】

1.扩展函数 1.1 作用域&#xff1a;扩展函数写的位置不同&#xff0c;作用域就也不同 扩展函数可以写成顶层函数&#xff08;Top-level Function&#xff09;&#xff0c;此时它只属于它所在的 package。这样你就能在任何类里使用它&#xff1a; package com.rengwuxianfun …...

【Java学习笔记】java图形界面编程

在前面的章节中&#xff0c;我们开发运行的应用程序都没有图形界面&#xff0c;但是很多应用软件&#xff0c;如Windows下的Office办公软件、扑克牌接龙游戏软件、企业进销存ERP系统等&#xff0c;都有很漂亮的图形界面。素以需要我们开发具有图形界面的软件。 Java图形界面编程…...

STM32入门笔记(03): ADC(SPL库函数版)(2)

A/D转换的常用技术有逐次逼近式、双积分式、并行式和跟踪比较式等。目前用的较多的是前3种。 A/D转换器的主要技术指标 转换时间 分辨率 例如&#xff0c;8位A/D转换器的数字输出量的变化范围为0&#xff5e;255&#xff0c;当输入电压的满刻度为5V时&#xff0c;数字量每变化…...

2024年7月2日 (周二) 叶子游戏新闻

老板键工具来唤去: 它可以为常用程序自定义快捷键&#xff0c;实现一键唤起、一键隐藏的 Windows 工具&#xff0c;并且支持窗口动态绑定快捷键&#xff08;无需设置自动实现&#xff09;。 卸载工具 HiBitUninstaller: Windows上的软件卸载工具 经典名作30周年新篇《恐怖惊魂夜…...

如何使用Spring Boot Profiles进行环境配置管理

如何使用Spring Boot Profiles进行环境配置管理 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将深入探讨如何利用Spring Boot Profiles来管理不同环境…...

Java错题归纳(二)

1、若有如下接口A的定义&#xff0c;下列哪些类下确实现了该接口&#xff1a;C interface A { void method1(int i); void method2(int j); } A class B implements A{ void method1( ) { } void method2( ) { } } B class B implements A { void method1(int i ) { }…...

Grafana面试题精选和参考答案

目录 Grafana是什么以及它的主要应用场景 Grafana支持的数据源 Grafana的体系结构及主要组件 Grafana如何实现数据的可视化和监控 Grafana支持的图表类型 如何在Grafana中创建和编辑仪表盘 Grafana的查询编辑器功能 Grafana支持的认证方式 Grafana的性能调优建议 Gra…...

Node版本管理工具 fnm 安装使用

fnm 是一个基于 Rust 开发的 Node 版本管理工具&#xff0c;它的目标是提供一个快速、简单且可靠的方式来管理 Node.js 的不同版本。同时&#xff0c;它是跨平台的&#xff0c;支持 macOS、Linux、Windows。&#x1f680; Fast and simple Node.js version manager, built in R…...

vector模拟实现【C++】

文章目录 全部的实现代码放在了文章末尾准备工作包含头文件定义命名空间和类类的成员变量 迭代器迭代器获取函数 构造函数默认构造使用n个值构造迭代器区间构造解决迭代器区间构造和用n个值构造的冲突拷贝构造 析构函数swap【交换函数】赋值运算符重载emptysize和capacityopera…...

《每天5分钟用Flask搭建一个管理系统》第11章:测试与部署

第11章&#xff1a;测试与部署 11.1 测试的重要性 测试是确保应用质量和可靠性的关键步骤。它帮助开发者发现和修复错误&#xff0c;验证功能按预期工作。 11.2 Flask测试客户端的使用 Flask提供了一个测试客户端&#xff0c;可以在开发过程中模拟请求并测试应用的响应。 …...

Landsat数据从Collection1更改为Collection2

目录 问题解决 问题 需要注意!您使用的是废弃的陆地卫星数据集。为确保功能持续&#xff0c;请在2024年7月1日前更新。 在使用一些以前的代码时会遇到报错&#xff0c;因为代码里面用的是老的数据集 解决 对于地表反射率SR&#xff0c;需要在name中&#xff0c;将C01换为C02&…...

《每天5分钟用Flask搭建一个管理系统》第12章:安全性

第12章&#xff1a;安全性 12.1 Web应用的安全威胁 Web应用面临的安全威胁包括但不限于跨站脚本攻击&#xff08;XSS&#xff09;、SQL注入、跨站请求伪造&#xff08;CSRF&#xff09;、不安全的直接对象引用&#xff08;IDOR&#xff09;等。 12.2 Flask-Talisman扩展的使…...

Unity之创建与导出PDF

内容将会持续更新&#xff0c;有错误的地方欢迎指正&#xff0c;谢谢! Unity之创建与导出PDF TechX 坚持将创新的科技带给世界&#xff01; 拥有更好的学习体验 —— 不断努力&#xff0c;不断进步&#xff0c;不断探索 TechX —— 心探索、心进取&#xff01; 助力快速…...

【Android面试八股文】优化View层次过深问题,选择哪个布局比较好?

优化深层次View层次结构的问题&#xff0c;选择合适的布局方式是至关重要的。以下是几点建议&#xff1a; 使用ConstraintLayout&#xff1a;ConstraintLayout是Android开发中推荐的布局&#xff0c;能够有效减少嵌套&#xff0c;提高布局性能。相比RelativeLayout&#xff0c;…...

什么是带有 API 网关的代理?

带有 API 网关的代理服务显著提升了用户体验和性能。特别是对于那些使用需要频繁创建和轮换代理的工具的用户来说&#xff0c;使用 API 可以节省大量时间并提高效率。 了解 API API&#xff0c;即应用程序编程接口&#xff0c;是服务提供商和用户之间的连接网关。通过 API 连接…...

sql拉链表

1、定义&#xff1a;维护历史状态以及最新数据的一种表 2、使用场景 1、有一些表的数据量很大&#xff0c;比如一张用户表&#xff0c;大约1亿条记录&#xff0c;50个字段&#xff0c;这种表 2.表中的部分字段会被update更新操作&#xff0c;如用户联系方式&#xff0c;产品的…...

STM32CubeMX实现矩阵按键(HAL库实现)

功能描述&#xff1a; 实现矩阵按键验证&#xff0c;将矩阵按键的按键值&#xff0c;通过串口显示&#xff0c;便于后面使用。 实物图 原理图&#xff1a; 编程原理&#xff1a; 原理很简单&#xff0c;就是通过循环设置引脚为低电平&#xff0c;另外引脚扫描读取电平值&…...

mmdetection3D指定版本安装指南

1. 下载指定版本号 选择指定版本号下载mmdetection3d的源码&#xff0c;如这里选择的是0.17.2版本 git clone https://github.com/open-mmlab/mmdetection3d.git -b v0.17.22. 安装 cd mmdetection3d安装依赖库 pip install -r requirment.txt编译安装 pip install -v e .…...

SQLMap工具详解与SQL注入防范

SQLMap工具详解与SQL注入防范 大家好&#xff0c;我是微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将深入探讨SQLMap工具的详细使用方法以及如何防范SQL注入攻击。 SQL注入简介 SQL注入是一种常见的安全漏洞&am…...

如何在Java中实现自定义数据结构:从头开始

如何在Java中实现自定义数据结构&#xff1a;从头开始 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将探讨如何在Java中实现自定义数据结构&#xff…...

【机器学习】在【Pycharm】中的应用:【线性回归模型】进行【房价预测】

专栏&#xff1a;机器学习笔记 pycharm专业版免费激活教程见资源&#xff0c;私信我给你发 python相关库的安装&#xff1a;pandas,numpy,matplotlib&#xff0c;statsmodels 1. 引言 线性回归&#xff08;Linear Regression&#xff09;是一种常见的统计方法和机器学习算法&a…...

自研直播系统-直播系统实战

文章目录 1 流媒体基础本文教程下载地址1.1 流媒体1.2 流式传输方式1.2.1 顺序流式传输1.2.2 实时流式传输 1.3 流媒体传输协议1.3.1 rtmp协议1.3.2 HLS协议1.3.3 RTSP协议1.3.4 视频流的对比 1.4 视频编码(codec)1.5 分辨率的规范分辨率簡介&#xff1a;1.5.2 分辨率單位 1.6 …...

绝区零 Mac 下载安装详细教程(MacOS IPA 砸壳包 playCover 完美运行)

绝区零 7.4 号开始公测&#xff0c;但刚刚就可以开始下载了&#xff0c;我也是第一时间就迫不及待的安装到了我的 Mac 电脑上&#xff0c;感兴趣的朋友可以跟我一起安装试试 我这里是通过 playCover 的形式在 Mac 上安装运行的&#xff0c;根据之前原神的经验所以这次还是同样…...

20K Stars!一个轻量级的 JS 库

大家好,我是CodeQi! 一位热衷于技术分享的码仔。 Driver.js 是一个轻量级的 JavaScript 库,旨在帮助开发人员创建网站或应用程序的引导和教程。通过 Driver.js,您可以引导用户了解网站的各个功能和使用方式。 Driver.js 提供了高度可定制的功能,使其能够适应各种需求和…...

Mongodb地理信息数据查询

在MongoDB中&#xff0c;可以使用地理空间查询来查找特定的地理位置信息。以下是一个使用MongoDB的地理空间查询的例子&#xff0c;假设我们有一个名为places的集合&#xff0c;它包含有关地点的信息&#xff0c;并且每个文档都有一个location字段&#xff0c;该字段包含一个地…...

【MindSpore学习打卡】应用实践-计算机视觉-深入解析 Vision Transformer(ViT):从原理到实践

在近年来的深度学习领域&#xff0c;Transformer模型凭借其在自然语言处理&#xff08;NLP&#xff09;中的卓越表现&#xff0c;迅速成为研究热点。尤其是基于自注意力&#xff08;Self-Attention&#xff09;机制的模型&#xff0c;更是推动了NLP的飞速发展。然而&#xff0c…...

22_嵌入式微处理器

目录 嵌入式微处理器分类 嵌入式硬件结构 嵌入式微处理器的分类 典型8位微处理器 8位微处理器结构 8051单片机的硬件组成 8051单片机的引脚 时钟电路 MCS-51指令集 典型16位微处理器 16位微处理器结构 MSP430单片机硬件结构 典型32位微处理器 32位微处理器特点 A…...

竞争问界M7?东风奕派eπ008将于6月上市

作为东风奕派品牌旗下的第二款量产车型,东风奕派eπ008定位中大型SUV,已在2024北京车展期间开启预售,预售价格为20万-25万元。而日前笔者从相关渠道获悉,东风奕派eπ008将于今年6月完成上市。结合实车图来看,东风奕派eπ008拥有宽大饱满的车头造型,同时前包围两侧集成熏黑…...

web刷题记录(1)

[GXYCTF 2019]Ping Ping Ping 进入页面&#xff0c;发现有一个传入参数的框&#xff0c;目的就是为了让我们通过参数传入内容来执行代码。这里先传入本地ip&#xff0c;方便后面的ping命令运行 ls命令来查看&#xff0c;目录中的文件 传入后&#xff0c;发现目录下有flag.php,…...

HNU-计算机体系结构-实验3-缓存一致性

计算机体系结构 实验3 计科210X 甘晴void 202108010XXX 文章目录 计算机体系结构 实验31 实验目的2 实验过程2.0 预备知识2.0.1 多cache一致性算法——监听法2.0.1.1 MSI协议2.0.1.2 MESI协议2.0.1.3 本题讲解 2.0.2 多cache一致性算法——目录法2.0.2.1 有中心的目录法2.0.2…...

【车道线网络,给予他预训练模型权重,为什么继续训练得不到更好的权重参数,反而会出现检测效果的下降?】】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、检测效果的下降&#xff1f;解决方案总结 车道线网络&#xff0c;给予他预训练模型权重&#xff0c;为什么继续训练得不到更好的权重参数&#xff0c;反而会出现…...

java组合设计模式Composite Pattern

组合设计模式&#xff08;Composite Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许你将对象组合成树形结构来表示“部分-整体”的层次结构。组合模式使得客户端对单个对象和组合对象的使用具有一致性。 // Component - 图形接口 interface Graphic {void draw()…...

用大模型搭建一个自己的新闻小助手

背景 信息快速增长的时代&#xff0c;及时获取到有价值的资讯是一件很必要的事情。已经有各类新闻app和获取信息的渠道了&#xff0c;为什么还需要在构建一个小助手来获取新闻资讯呢&#xff1f;其实原因很简单各类新闻app服务的是具体一类人群&#xff0c;个人和人群还是有偏…...