Gos —— 掌控硬盘

写在前面:自制操作系统Gos 第二章第四篇:主要内容是如何操纵外设,如何操纵硬盘

关于硬盘的原理我在Linux —— 文件系统及相关操作命令其实是已经简单讲了一下了,这里就不再赘述了。

在上一篇中我们讲到了CPU和显示器交互的接口其实就是显存。那CPU和硬盘打交道也是同样的道理:硬盘控制器


硬盘控制器是专门驱动外部设备的模块电路,CPU只同它们交互,由它们将信息传递给外部设备

硬盘控制器端口

让硬盘工作,我们需要通过读写硬盘控制器的端口。下面列出了部分的端口,也是我们开发Gos需要用到的端口:

Primary通道端口Secondary通道端口读操作时用途写操作时用途

首先是命令寄存器:Command register

0x1F00X170datadata
0X1F10X171errorfeatures
0X1F20X172sector countsector count
0X1F30X173LBA lowLBA low
0X1F40X174LBA midLBA mid
0X1F50X175LBA highLBA high
0X1F60X176devicedevice
0X1F70X177statuscommand

除此之外是控制寄存器:Control register

0x3F60x376alternate statusdevice control


电脑主板上有两个硬盘串行接口(STAT),每个接口可以插一块硬盘。其中第一个STAT被称之为Primary通道,其连接主盘master;第二个STAT称之为Secondary通道,其连接从盘slave
但是这里要注意一个问题:端口是按照通道给出的
也就是说:一个通道的主、从两块硬盘都用这些端口号。想要操作某通道所对应的某块硬盘,直接单独指定device寄存器的第四位(硬盘标志位)为1就可以了。

其中命令寄存器Command register 用于向硬盘驱动器写入命令或者从硬盘控制器获得硬盘状态
控制寄存器Control register 用于控制硬盘的工作状态

下面我们具体讲一下这些寄存器的作用:

  • data:负责管理数据。在读硬盘时,硬盘准备好的数据后,硬盘控制器就将其放在内存的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据;写硬盘时,我们要把数据源源不断地输送到此端口,之后便存入缓冲区,之后再写入相应的扇区。
  • error:当0x171或者0x1F1读硬盘失败的时候才有用,里面会记录着失败的信息。其中尚未读取的扇区在sector count寄存器中。在写硬盘的时候,其则被称之为feature寄存器。
  • sector count:用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值-1.如果失败,那么其就存储着尚未读取的扇区数
  • LBA:逻辑块地址(Logical block address),一共有两种:第一种时LBA28,用28为来描述一个扇区的地址。可以这次228*512=128GB大小的空间,Gos用LBA28就可以了;第二种是LBA48,顾名思义,其可以支持128PB的空间
  • LBA low:存储28位地址的0~7位
  • LBA middle:存储28位地址的8~15位
  • LBA high:存储28位地址和的16~23位
  • device:0~3位用来存储28位地址的剩下4位,第4位表示是主盘还是从盘,0表示主盘,1代表从盘。第六位表示是否开启LBA,1代表启用。第五位和第七位表示MBS位。
  • status:用来给出给出硬盘的状态信息。第0位表示error位;第三位表示data request,如果此位为1,表示数据准备好了;第6位表示drdy,表示硬盘就绪;第七位是BSY,表示硬盘是否繁忙。

:
在Gos中,我们主要使用三个命令:
identify:0xEC,即硬盘识别
read sector:0x20,即读扇区
write sector:0x30,即写扇区

下面是device status两个寄存器的示意图:
在这里插入图片描述

常用硬盘操作方法

硬盘的指令很多,用法也有很多。有的直接往command寄存器中写,有的则是在feature寄存器中写入参数,可谓是多种多样。但是不管是哪种写法,command寄存器都是最后写。因为其一旦一开始就写,那么整个硬盘就轰隆隆的启动了。

而我们的操作顺序如下:

  1. 先选择通道,往该通道的sector count寄存器 写入待操作的扇区数
  2. 往该通道上的三个LBA寄存器写入扇区起始地址的低24位
  3. 往device寄存器写入剩下四位,之后置第6位为1;设置第4位,选择要操作的硬盘
  4. 往该通道的command寄存器写入操作命令
  5. 读取该通道上的status寄存器,判断硬盘工作是否完成
  6. 如果是读硬盘,进入下一个步骤。否则,完成。
  7. 将硬盘数据读出


我们使用的读取方式是以下两种结合:
无条件传送方式:数据随时随地准备好,直接拿来吧你
查询传送方式:取之前先查一查数据准备好了没有,然后再拿来吧你

操纵硬盘

上硬菜咯!

# 文件 mbr.S 中的内容
; MBR引导程序,从BIOS接过权力,交接给loader内核加载器
;*************************
%include "boot.inc"
SECTION MBR vstart=0x7c00
    ; 初始化显卡控制信息,本质是往显存写入数据
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov sp,0x7c00
    mov ax,0xb800
    mov gs,ax
;*************************
; 清屏利用0x06号功能,上卷全部行,进行清屏
; int 0x10  功能号:0x60    功能描述:上卷窗口
; 输入:
; AH 功能号: 0x06
; AL = 上卷的行数(0代表全部)
; BH = 上卷的行属性
; (CL,CH) = 窗口左上角(x,y) 的位置
; (DL,DH) = 窗口右下角(x,y)的位置
; 无返回值!
    mov ax,0600h
    mov bx,0700h
    mov cx,0        ;左上角(0,0)
    mov dx,0x184f   ;右下角(80,25)
                    ;VAG文本模式中,一行只能容纳80个字符,总共25行
                    ;下标从0开始,所以0x18=24,0x4f=79
    int 10h
;*************************
    ;输出字符串:hello,Gos!
    mov byte [gs:0x00],'h'
    mov byte [gs:0x01],0x0F     ;黑底亮白不闪烁

    mov byte [gs:0x02],'e'
    mov byte [gs:0x03],0x0F

    mov byte [gs:0x04],'l'
    mov byte [gs:0x05],0x0F

    mov byte [gs:0x06],'l'
    mov byte [gs:0x07],0x0F

    mov byte [gs:0x08],'o'
    mov byte [gs:0x09],0x0F

    mov byte [gs:0x0a],','
    mov byte [gs:0x0b],0x0F

    mov byte [gs:0x0c],'G'
    mov byte [gs:0x0d],0x0F

    mov byte [gs:0x0e],'o'
    mov byte [gs:0x0f],0x0F

    mov byte [gs:0x10],'s'
    mov byte [gs:0x11],0x0F

    mov byte [gs:0x12],'!'
    mov byte [gs:0x13],0x0F
;*************************
;初始化磁盘信息
    mov eax,LOADER_START_SECTOR     ;起始扇区LBA地址
    mov bx,LOADER_BASE_ADDR         ;写入的地址
    mov cx,1                        ;待读入的扇区数
    call read_disk_m_16             ;调用读取程序起始部分的函数

    jmp LOADER_BASE_ADDR
;*************************
;读取磁盘的n个扇区
read_disk_m_16:
                                    ;eax=LBA扇区号
                                    ;bx=将数据写入的内存地址
                                    ;cx=读入的扇区数
    mov esi,eax                     ;备份eax
    mov di,cx                       ;备份cx
;读写硬盘
                                    ;1.设置要读取的扇区数量
    mov dx,0x1f2
    mov al,cl
    out dx,al                       ;读取的扇区数
    mov eax,esi                     ;恢复eax

                                    ;2.将LBA地址存入0x1f3~0x1f6
                                    ;LBA地址7~0位写入端口0x1f3
    mov dx,0x1f3
    out dx,al

                                    ;LBA地址15~8位写入端口0x1f4
    mov cl,8
    shr eax,cl
    mov dx,0x1f4
    out dx,al

                                    ;LBA地址23~16位写入端口0x1f5
    shr eax,cl
    mov dx,0x1f5
    out dx,al

    shr eax,cl
    and al,0x0f                     ;LBA第24~27位
    or al,0xe0                      ;设置7~4位为1110,表示LBA模式
    mov dx,0x1f6
    out dx,al

                                    ;3.向0x1f7端口写入读命令,0x20
    mov dx,0x1f7
    mov al,0x20
    out dx,al

                                    ;4.检测硬盘状态
.not_ready:
    nop                             ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
    in al,dx                        ;
    and al,0x88                     ;第3位为1表示硬盘控制器已经准备号数据传输了
                                    ;第7位为1表示硬盘忙
    cmp al,0x08                     
    jnz .not_ready                  ;若未准备号,继续等

                                    ;5.从0x1f0端口读数据
    mov ax,di
    mov dx,256                      ;一个扇区512字节,1字=2字节,所以时256
    mul dx
    mov cx,ax
    mov dx,0x1f0

.go_on_read:
    in ax,dx
    mov [bx],ax
    add bx,2
    loop .go_on_read
    ret

times 510-($-$$) db 0
db 0x55,0xaa

可以看到,我们在这里包含了一个头文件boot.inc,这个文件中的内容很简单,只是定义了两个常量:

# boot.inc 文件中的内容
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2     ;第二扇区

之后,我们开始编译这个文件:

# -I 指定头文件搜索路径
# boot.inc 文件在同级目录include下
nasm -I ../include/ mbr.bin mbr.S

编译成功之后,会在目录中生成mbr.bin文件:
在这里插入图片描述
之后,我们使用dd工具将其写入我们之前创建好的磁盘hd60.img中:

# 记得换成自己的路径
sudo dd if=./mbr.bin of=/bochs/bo_tmp/bin/hd60M.img bs=512 count=1 conv=notrunc

然后,我们转到bochs的bin目录下,使用以下命令开启bochs:

sudo ./bochs -f boch.conf

在这里插入图片描述
进入到如下的界面了,默认选项是6,我们直接回车就好了:
在这里插入图片描述
之后,在bochs模拟的机器上便会打印出:hello Gos!(虽然这个和操纵硬盘没有关系,硬盘操纵是为了我们之后进入保护模式做铺垫用的)
在这里插入图片描述

fdisk命令 创建磁盘分区

分区时逻辑上划分磁盘空间的方式,本质上是人为的将柱面扇区划分成不同的分组,每个分组都是单独的分区。各个分区都有自己的元数据,用来记录分区本身在硬盘上的起止界限等信息。在硬盘的MBR中有个数据机构,64字节大小,就是分区表。其中的每个表现就是一个分区,每个表项大小是16字节,所以最多可用容纳4个分区。

但是,现在的操作系统通过在每个元数据中增加一个ID信息来支持更多的分区。原理是,这个ID新增了一种分区叫做逻辑分区,逻辑分区可用无限再分。所以这4个分区,其中有一个做扩展分区,扩展分区可用无限再分,而另外三个则是主分区

而分区的过程如下:

  1. 我们先用以下命令查看一下当前的硬盘信息
[ik@localhost bin]$ sudo fdisk -l ./hd10M.img

磁盘 ./hd10M.img:10 MB, 10321920 字节,20160 个扇区
Units = 扇区 of 1 * 512 = 512 bytes
扇区大小(逻辑/物理):512 字节 / 512 字节
I/O 大小(最小/最佳):512 字节 / 512 字节
  1. 使用fdisk命令开始分区:
[ik@localhost bin]$ sudo fdisk ./hd10M.img
欢迎使用 fdisk (util-linux 2.23.2)。

更改将停留在内存中,直到您决定将更改写入磁盘。
使用写入命令前请三思。

Device does not contain a recognized partition table
使用磁盘标识符 0xb2a716ed 创建新的 DOS 磁盘标签。

命令(输入 m 获取帮助)
  1. 进入专家模式,开始创建主分区
命令(输入 m 获取帮助):x		#进入专家模式

专家命令(输入 m 显示帮助):c	# 设置柱面数
柱面数 (1-1048576,默认为 1):162

专家命令(输入 m 显示帮助):h	# 设置磁头数
磁头数 (1-256,默认为 255):16

专家命令(输入 m 显示帮助):r	# 返回上一级

命令(输入 m 获取帮助):n	# 开始创建分区
Partition type:
   p   primary (0 primary, 0 extended, 4 free)
   e   extended
Select (default p): p	# 创建主分区
分区号 (1-4,默认 1):1	# 指定分区号为1
起始 扇区 (2048-20159,默认为 2048):2048	# 指定起始扇区和终止扇区
Last 扇区, +扇区 or +size{K,M,G} (2048-20159,默认为 20159):4096
分区 1 已设置为 Linux 类型,大小设为 1 MiB
  1. 创建扩展分区
命令(输入 m 获取帮助):n	# 开始创建分区
Partition type:
   p   primary (1 primary, 0 extended, 3 free)
   e   extended
Select (default p): e	# 创建扩展分区
分区号 (2-4,默认 2):2	# 指定分区号
起始 扇区 (4097-20159,默认为 6144):5000	# 指定起始扇区和终止扇区
Last 扇区, +扇区 or +size{K,M,G} (5000-20159,默认为 20159):10000
分区 2 已设置为 Extended 类型,大小设为 2.5 MiB
  1. 创建逻辑扇区
命令(输入 m 获取帮助):n	# 开始分区
Partition type:
   p   primary (1 primary, 1 extended, 2 free)
   l   logical (numbered from 5)
Select (default p): l	# 开始创建逻辑分区
添加逻辑分区 5
起始 扇区 (7048-10000,默认为 8192):8000
Last 扇区, +扇区 or +size{K,M,G} (8000-10000,默认为 10000):8000
分区 5 已设置为 Linux 类型,大小设为 512 B

磁盘分区表

磁盘分区表(Disk Partition Table,DPT)是多个分区的元数据组成的表,表中的每一项都对应一个分区。所以其本质其实是个数组,此数组的长度跟分区个数一致,为4 。这四个元素位于MBR的0x1BE~0x1FD空间,总共64个字节,但值得提一下的是在这64字节之后是两字节的魔数0x55AA。至于支持扩展分区则是采用链式结构,将多张分区表串联起来。

对于分区表中的每一项则是一个16位的元数据:
在这里插入图片描述

硬件驱动程序

硬盘上有两个ata通道,也叫做IDE通道。第一个IDE通道上的两个硬盘挂在IRQ14上。当我们对硬盘操作的时候,我们需要提前指定是对主盘还是从盘执行,这个是device寄存器的第四位dev位指定的。

对于硬盘来说其属性也就是我们刚刚介绍的:

// * @brief 磁盘结构
struct disk
{
    char name[8];                    //磁盘名称
    struct ide_channel *my_channel;  //此硬盘归属哪个ide通道
    uint8_t dev_no;                  //主硬盘0,从硬盘1
    struct partition prim_parts[4];  //主分区
    struct partition logic_parts[8]; //逻辑分区
};

这里我们设定每个硬盘都有四个主分区和8个逻辑分区。每个分区主要包含这个分区的一些表述性信息:起始扇区、扇区数、磁盘位图等等。

// * @brief 分区结构体
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 *su_block; //本分区的超级块
    struct bitmap block_bitmap;   //块位图
    struct bitmap inode_bitmap;   //i节点位图
    struct list open_inodes;      //本分区打开的i节点队列
};

那么我们首先要做的就是接收来自可编程中断控制器PIC的信号,这样就需要设置一下:

   // 打开从片上的IRQ14,此引脚接收硬盘控制器的中断
   outb(PIC_S_DATA, 0xbf);

然后开始对硬盘进行初始化操作:

  • 设置硬盘对应的中断引脚
  • 设置硬盘中断处理函数
  • 获取每个的参数信息
/*
 * @brief 硬盘数据结构初始化
 * @note    1.获取硬盘数量
 * @note    2.获取通道数量 = 硬盘数量/2
 * @note    3.处理每个通道上的信息,包括起始端口和通道号
 * @note    4.获得每个通道上硬盘的信息、扫描硬盘分区信息
 */
void ide_init()
{
    printk("ide init start...\n");
    uint8_t hd_cnt = *((uint8_t *)(0x475)); //0x475BIOS规定的读取硬盘的位置

    ASSERT(hd_cnt > 0);
    list_init(&partition_list);
    channel_cnt = DIV_ROUND_UP(hd_cnt, 2); //一个通道两个硬盘,根据硬盘数量反推ide通道数
    struct ide_channel *channel;
    uint8_t channelogic_hd_no = 0;
    uint8_t dev_no = 0; //硬盘标号

    printk("harddisk info:\n");
    printk("    harddisk number: %d\n", hd_cnt);
    printk("    channle number: %d\n", channel_cnt);

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

        switch (channelogic_hd_no)
        {
        case 0:                          //主硬盘
            channel->port_base = 0x1f0;  //ide0的通道起始端口号
            channel->irq_no = 0x20 + 14; //从片上倒数第二个中断引脚
            break;
        case 1:
            channel->port_base = 0x170;
            channel->irq_no = 0x20 + 15; //从片的最后一个中断引脚
            break;
        default:
            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' + channelogic_hd_no * 2 + dev_no);
            identify_disk(hd); // 获取硬盘参数
            if (dev_no != 0)
            {                          // 内核本身的裸硬盘(hd60M.img)不处理
                partition_scan(hd, 0); // 扫描该硬盘上的分区
            }
            primary_hd_no = 0;
            logic_hd_no = 0;
            dev_no++;
        }
        dev_no = 0; // 将硬盘驱动器号置0,为下一个channel的两个硬盘初始化。

        channelogic_hd_no++;
    }

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

这样初始化完成之后,当对应硬盘中如果有中断信号产生,便会调用intr_hd_handler函数进行处理。而产生中断的则是硬盘完成读写操作之后,会主动来提示驱动程序。而我们的驱动程序在收到中断之后,便会唤醒阻塞于硬盘读写的程序:

/*
 * @brief 硬盘中断处理程序
 * @param irq_no 中断号
 */
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);

    if (channel->expecting_intr)
    {
        //正在等待中断
        channel->expecting_intr = false;
        //唤醒线程
        sema_up(&channel->disk_done);
        inb(reg_status(channel));
    }
}

硬盘读写

对于硬盘的读写在内核层面需要我们主动的传入往哪个硬盘去写、写什么以及写多少这些信息。这样,我们要做的第一件事情起始就是使用函数select_disk选择使用哪块硬盘,这步操作的本质还是对device寄存器的操作,如果写入主盘就将其位设置为0,如果是写入从盘那么置为0,之后把这个写入通道的起始端口号就可以了。

/*
 * @brief 选择操纵硬盘
 * @param hd 硬盘的指针
 */
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);
}

之后的下一步便是告诉硬盘我要从哪里开始写了。这个需要写入sector count寄存器中需要待读取的扇区数,之后再LBAlow LBAmid以及LBAhigh中写入LBA地址,这个过程也是对device寄存器操作。

/*
 * @brief 向硬盘控制器写入起始扇区地址以及要读写的扇区数
 * @param hd 硬盘指针
 * @param lba 扇区起始地址
 * @param sec_cnt 扇区数
 */
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;

    //# 1.写入要读取的扇区数
    //若sec_cnt为0,表示写入256个扇区
    outb(reg_sect_cnt(channel), sec_cnt);

    //# 2.写入扇区号
    outb(reg_lba_l(channel), lba);                                                                       //lba的低8位
    outb(reg_lba_m(channel), lba >> 8);                                                                  //lba的8~15位
    outb(reg_lba_h(channel), lba >> 16);                                                                 //lba的16~23位
    outb(reg_dev(channel), BIT_DEV_MBS | BIT_DEV_LBA | (hd->dev_no == 1 ? BIT_DEV_DEV : 0) | lba >> 24); //写入lba的24~27位,这四位在device寄存器中
}

之后,我们将读磁盘命令0x20写入status寄存器,这个时候硬盘就开始根据我们传入的位置信息开始读扇区了,之后会把这部分数据放到硬盘的缓冲区。这里还有一步就是设置当前进程状态是阻塞的情况。

/*
 * @brief 向通道channel发出命令cmd
 * @param channel 通道指针
 * @param cmd 硬盘操作命令
 */
static void cmd_out(struct ide_channel *channel, uint8_t cmd)
{
    //只要向硬盘发出命令便将此标记置为true
    channel->expecting_intr = true;
    outb(reg_cmd(channel), cmd);
}

硬盘读完之后便会触发中断处理程序唤醒等待中的线程。线程这个时候把数据从硬盘的缓冲区搬移到自己的缓存区。

/*
 * @brief 硬盘读入sec_cnt个扇区的数据到buf
 * @param hd 硬盘指针
 * @param buff 存放读取数据的缓冲区
 * @param sec_cnt 读取的扇区数
 */
static void read_from_sector(struct disk *hd, void *buff, uint8_t sec_cnt)
{
    uint32_t size_in_byte; //要读取的字节数
    if (sec_cnt == 0)      //当sec_cnt为0时,表示256
    {
        size_in_byte = 256 * 512;
    }
    else
    {
        size_in_byte = sec_cnt * 512;
    }
    //因为写入的单位是字,所以这里要除二啦
    insw(reg_data(hd->my_channel), buff, size_in_byte / 2);
}

硬盘写则是相同的过程,只不过最后需要操作写端口。

/*
 * @brief 将buf中的sec_cnt个扇区数据写入硬盘
 * @param hd 硬盘指针
 * @param lba 扇区地址
 * @param buff 缓冲区
 * @param 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);
    get_lock(&hd->my_channel->lock);

    //1.选择操作的硬盘
    select_disk(hd);
    uint32_t per_op_sectors;
    uint32_t done_sectors = 0;
    while (done_sectors < sec_cnt)
    {
        if ((done_sectors + 256) <= sec_cnt)
        {
            per_op_sectors = 256;
        }
        else
        {
            per_op_sectors = sec_cnt - done_sectors;
        }

        //2.写入待写入的扇区数和起始扇区号
        select_sector(hd, lba + done_sectors, per_op_sectors);

        //3.执行命令写入red_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 + done_sectors * 512), per_op_sectors);

        sema_down(&hd->my_channel->disk_done);
        done_sectors += per_op_sectors;
    }
    abandon_lock(&hd->my_channel->lock);
}

参考文献

[1] 操作系统真相还原		3.6 让MBR使用硬盘
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shenmingik

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值