带你了解磁盘驱动程序

发表于 3年以前  | 总阅读数:292 次

磁盘驱动程序

本文来聊聊磁盘驱动程序,驱动程序是硬件的接口,操作系统通过这个接口来控制硬件工作,所以驱动程序就好比是硬件和系统之间的桥梁。这是百科上给出的解释,可能看起来还是云里雾里,我来做做注解。

每个硬件都有自己的 "CPU"(控制器),寄存器,有着自己的一套执行逻辑。对外提供了一些列的物理接口,就是那一个个端口(寄存器),可以通过设置这些端口来控制硬件工作。要知道直接通过物理接口来控制硬件工作是很繁复的,所以将这些接口给封装起来便于使用,这就是驱动程序。

所以操作系统通过驱动程序提供的接口来间接控制硬件工作,驱动程序通过硬件实际的物理接口来直接控制硬件工作,驱动程序就是对硬件物理接口的封装。本文通过 xv6 来看一看简单的磁盘驱动程序是怎样的,以便对驱动程序有更深刻的认识

IDE接口

寄存器

首先来了解磁盘的一些寄存器:

xv6 中磁盘驱动程序需要用到的寄存器如上所示,没有翻译,有些也不太好翻译,就直接看英文吧,应该也能看懂,我们一个一个来看:

0x1F0/Data,唯一一个 16 bit 的寄存器,用来传输数据。

0x1F1/Error, 读的时候表示错误,8 bit,每一位表示一种错误,这里不展开了,有需要的看我后面给出的资料链接。

0x1F2/Sector Count,表扇区总数,读写的时候指定要操作的扇区总数

0x1F3,0x1F4,0x1F5 分别表示 LBA 地址的低中高 8 位,LBA 地址有 24 位,还有顶部的 4 位见下

0x1F6/Device/Head

  • 0~3 位为 LBA 地址的最高 4 位
  • DRV 位为 1 表示该盘是主盘,0 表示该盘是从盘
  • LBA 位为 1 表示采用 LBA 寻址,0 表示采用 CHS 寻址,现今一般都采用 LBA 寻址,所以 0x1F3~5 表示 LBA 地址的 24 位,否则应表示 CHS 三个指标
  • bit 5 和 bit 7 固定为 1

0x1F7/Status

  • ERR,有错误发生,错误码放在错误寄存器中(0x1F1)
  • WFT,检测到有写错误
  • RDY,表示硬盘就绪,这是在对硬盘诊断的时候用的,表示硬盘检测正常,可以继续执行一些命令
  • BSY,表示硬盘是否繁忙,1 表示繁忙,此时其他所有位无效

0x1F7/Command,向这个寄存器写入命令来操作硬盘,具体命令见后面

0x3F6/DeviceControl,这个寄存器只用到了低 2 位:

  • SRST(bit2),设置为 1 时表示发送复位 reset 信号,正常情况下此位应为 0
  • IEN(bit1),设置为 1 时键盘控制器将不会发送中断信号,正常情况应为 0,使得键盘在完成某些命令之后能够发送中断给 CPU,然后 CPU 进行后续处理。

命令

0x20,读扇区命令

0x30,写扇区命令

0xc4,读多个扇区

0xc5,写多个扇区

xv6 里面就只用了这几个命令,其他的命令见后链接资料,有了这些了解之后来看 xv6 的相关源码

xv6

缓存

具体看 xv6 有关磁盘操作的代码之前,先来了解磁盘缓存。磁盘读写操作是很慢的,所以一般都会将一部分内存作为磁盘的缓存,xv6 也是如此,专门在内存中分配一片区域作为磁盘缓存,这片缓存的最小单位是块。我在[捋一捋文件系统] 一文中讲过,操作系统或者说文件系统层面磁盘读写的单位是块,磁盘自己本身的读写单位是扇区,一般块大小等于一个或多个扇区的大小。

因此我们来看看 xv6 中缓存块的定义:

struct buf {
  int flags;      //表示该块是否脏是否有效
  uint dev;       //该缓存块表示的设备
  uint blockno;   //块号
  struct sleeplock lock;   //休眠锁
  uint refcnt;    //引用该块的个数
  struct buf *prev;  //该块的前一个块
  struct buf *next;  //该块的后一个块
  struct buf *qnext; //下一个磁盘队列块
  uchar data[BSIZE];  //缓存的数据
};
#define B_VALID 0x2  // buffer has been read from disk
#define B_DIRTY 0x4  // buffer needs to be written to disk

缓存块缓存的是磁盘上的数据,缓存块的数据需要与磁盘数据同步,所以需要设定标志位来表示目前缓存块的数据是否有效,是否需要从磁盘中读取数据到该块;缓存块的数据是否已做改动变脏,是否需要写回磁盘上去

以前的主板上为硬盘提供了两个通道, 通道和 通道,每个通道可以挂两个磁盘,分为主盘和从盘。不同通道的端口也就是寄存器地址不同,上面所讲的都是 通道的寄存器,一般也就是使用 通道, 通道相应的端口可以参考文末给出的手册或链接。通道上的两块硬盘又分为主盘和从盘,这是根据 device 寄存器的第 4 位来区分的。主从,听名字就知道主盘要重要些,里面主要存放启动程序和操作系统,从盘主要就是拿来存储数据。就跟 windows 中常见的系统盘 C 和其他盘一样,如果没有从盘,那所有的信息就放在主盘上。

缓存块算作是公共资源,需要避免竞争条件,所以配了一把锁,而且是休眠锁,因为同步缓存块到磁盘涉及 I/O 操作是很慢的,所以一般在读写的时候进程就在上面休眠。

每个缓存块可能多个任务使用,所以有了 refcnt 来表示该块被引用了多少次,但是任何时刻应该最多只有一个任务在使用该缓存块

当一个缓存块需要从磁盘读取数据或者写回磁盘,xv6 维持了一个磁盘的请求队列,所以有了 这个属性,它指向下一个请求磁盘的缓存块。

这些缓存块也是需要组织管理的,方便必要的时候进行分配和回收,前面文章说过,基本上组织这些块的方式一般就是位图或者链表,xv6 使用了链表的形式而且是双向循环链表,所以每个块有了 和 属性,分别指向前一个块和后一个块。来看具体的实现:

struct {
  struct spinlock lock;
  struct buf buf[NBUF];
  struct buf head;
} bcache;  //申明一片缓存区

void binit(void)        //头插法将N个缓存块串起来
{
  struct buf *b;
  initlock(&bcache.lock, "bcache");

  bcache.head.prev = &bcache.head;
  bcache.head.next = &bcache.head;
  for(b = bcache.buf; b < bcache.buf+NBUF; b++){
    b->next = bcache.head.next;
    b->prev = &bcache.head;
    initsleeplock(&b->lock, "buffer");
    bcache.head.next->prev = b;
    bcache.head.next = b;
  }
}

上面函数应该很简单吧,就是双向链表的实现,数据结构课程上应该都练习过很多了,所以对代码也不解释了,只说明一点:每个缓存块有个休眠锁,因为可能要进行磁盘请求,速度很慢,所以使用了休眠锁,必要情况下直接让进程休眠让出 CPU,提高 CPU 利用率。而整个缓存区 也有个锁,是自旋锁, 就像缓存块的分配器一般,获取释放缓存块都需要 同意,而且 也是公共资源,任意时刻都应最多只有一个任务访问,另外获取释放缓存块的操作一般很短,所以配了一把自旋锁。

既然说到获取释放缓存块,那就来看看怎么实现的:

static struct buf* bget(uint dev, uint blockno)
{
  struct buf *b;

  acquire(&bcache.lock);   //取bcache的锁

  // Is the block already cached? 要获取的磁盘块已缓存
  for(b = bcache.head.next; b != &bcache.head; b = b->next){ //双向循环链表,从前往后扫描
    if(b->dev == dev && b->blockno == blockno){  //如果设备和块号都对上,那么是要找的块
      b->refcnt++;                   //该块的引用加1
      release(&bcache.lock);         //释放bcache的锁
      acquiresleep(&b->lock);        //给该块加锁
      return b;         //返回该块
    }
  }

  // Not cached; recycle an unused buffer.
  // Even if refcnt==0, B_DIRTY indicates a buffer is in use
  // because log.c has modified it but not yet committed it.
  for(b = bcache.head.prev; b != &bcache.head; b = b->prev){     //该磁盘块没有缓存,从后往前扫描
    if(b->refcnt == 0 && (b->flags & B_DIRTY) == 0) {            //找一个引用为0,且脏位也为0的空闲缓存块
      b->dev = dev;     //设备
      b->blockno = blockno;  //块号
      b->flags = 0;    //刚分配的缓存块,数据无效
      b->refcnt = 1;   //引用数为1
      release(&bcache.lock);     //释放bcache的锁
      acquiresleep(&b->lock);    //给该缓存块加锁
      return b;    //返回该缓存块
    }
  }
  panic("bget: no buffers");  //既没缓存该块,也没得空闲缓存块了,panic
}

根据设备和块号在缓存区中寻找该磁盘块是否已经缓存,如果已缓存,则返回该缓存块。如果没有缓存,那么在缓存区中找一个空闲缓存块来缓存相应磁盘块,这里要注意,只是将磁盘块所属设备和块号赋给了刚分配的缓存块,但是数据还没有传到缓存块,这需要磁盘请求,所以 为 0 表示缓存块里面的数据无效

void brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");

  releasesleep(&b->lock);          //释放缓存块的锁

  acquire(&bcache.lock);           //获取bcache的锁
  b->refcnt--;                     //该块的引用减1
  if (b->refcnt == 0) {            //没有地方再引用这个块,将块链接到缓存区链头
    // no one is waiting for it.
    b->next->prev = b->prev;
    b->prev->next = b->next;
    b->next = bcache.head.next;
    b->prev = &bcache.head;
    bcache.head.next->prev = b;
    bcache.head.next = b;
  }

  release(&bcache.lock);           //释放bache的锁
}

该函数释放缓存块,如果引用该块的进程较多,直接将引用数减 1,减 1 之后如果没有进程再引用该缓存块,则真正的释放该缓存块,直接将该块放在链头。缓存块的引用数为 0 一般就认为该块就是空闲的了,所以不用做什么额外工作,当然这只是一般情况,还有特殊情况超出本文范围,后面文件系统的文章再详述。

struct buf* bread(uint dev, uint blockno)  //返回一个存在有效数据的缓存块      
{
  struct buf *b;

  b = bget(dev, blockno);    //获取一个缓存块
  if((b->flags & B_VALID) == 0) {  //如果该块是临时分配的数据无效
    iderw(b);    //请求磁盘,读取数据
  }
  return b;
}

// Write b's contents to disk.  Must be locked.
void bwrite(struct buf *b)   //将缓存块写到相应磁盘块
{
  if(!holdingsleep(&b->lock))  //要写这个块,那说明已经拿到了这个块,所以肯定也拿到锁了
    panic("bwrite");
  b->flags |= B_DIRTY;      //设置脏位
  iderw(b);                 //请求磁盘写数据
}

读写磁盘块变成读写缓存块,然后再由操作系统给同步到磁盘。 负责将数据读到缓存块或者将数据写到磁盘中去,到底是读还是写根据缓存块 来判断,如果无效则需要从磁盘读取数据,如果缓存块的数据脏则将数据写到磁盘

磁盘驱动程序

下面来看看底层磁盘的操作,也就是常说的磁盘驱动程序,驱动程序听起来很复杂,但是简单来讲的话,驱动程序就是将硬件操作封装成过程,避免每次进行重复无味的操作,来具体看看:

#define SECTOR_SIZE   512     //扇区大小512字节
#define IDE_BSY       0x80    //状态寄存器的第7位表硬盘是否繁忙
#define IDE_DRDY      0x40    //状态寄存器的第6位表硬盘是否就绪,可继续执行命令
#define IDE_DF        0x20    //写错误
#define IDE_ERR       0x01    //出错

#define IDE_CMD_READ  0x20    //读扇区命令
#define IDE_CMD_WRITE 0x30    //写扇区命令
#define IDE_CMD_RDMUL 0xc4    //读多个扇区
#define IDE_CMD_WRMUL 0xc5    //写多个扇区

首先是一些宏定义,上述为 中定义的各个宏,对照着上述讲的寄存器应该很容易明白什么意思。下面来看一些关键函数:

static int idewait(int checkerr)   //等待硬盘就绪
{
  int r;

  while(((r = inb(0x1f7)) & (IDE_BSY|IDE_DRDY)) != IDE_DRDY)    //从端口0x1f7读出状态,若硬盘忙,空循环等待
    ;
  if(checkerr && (r & (IDE_DF|IDE_ERR)) != 0)    //检查是否有错误发生,若checkerr为0则不检查
    return -1;
  return 0;
}

inb(0x1f7) 表示从状态寄存器读出一字节的状态数据,对其 BSY 位和 DRDY 位做与运算,检查磁盘是否就绪。inb 是用内敛汇编封装的函数,用来从 I/O 端口读取数据,详见:[内联汇编] 。其他的没什么说的,见注释,应该很好理解。

void ideinit(void)   //初始化磁盘
{
  int i;

  initlock(&idelock, "ide");
  ioapicenable(IRQ_IDE, ncpu - 1);     //让这个CPU来处理硬盘中断
  idewait(0);           //等磁盘就绪,不过以0来调用错误检查就不起作用了,猜测是为了更快返回

  // Check if disk 1 is present
  outb(0x1f6, 0xe0 | (1<<4));     //将硬盘device寄存器高4位设置为1111,表示从盘,寻址模式为LBA

  for(i=0; i<1000; i++){      //指定为从盘后,循环读取状态来判断是否有从盘
    if(inb(0x1f7) != 0){
      havedisk1 = 1;
      break;
    }
  }

  // Switch back to disk 0.
  outb(0x1f6, 0xe0 | (0<<4));     //将第4位置0表切换成主盘
}

这个函数用来初始化硬盘,被 中的 函数锁调用,作为启动时建立环境的一项。

磁盘有一把锁,磁盘是公共资源,每次进行磁盘操作的进程最多只有一个。关于中断 APIC 的配置后面讲中断的时候详述,可以先参考文章:[再谈中断(APIC)]

前面说过每个通道支持两块磁盘,将 device 寄存器第 4 位置 1 来表示从盘,从状态寄存器读数据,如果有数据那说明从盘存在,反之从盘不存在,至于循环 1000 次那是因为切换磁盘得需要一定时间吧。最后再切换成主盘,因为目前正在初始化环境阶段,这些较为重要的数据比如操作系统等都在主盘

static void idestart(struct buf *b)
{
  if(b == 0)   
    panic("idestart");
  if(b->blockno >= FSSIZE)   //块号超过了文件系统支持的块数
    panic("incorrect blockno");
  int sector_per_block =  BSIZE/SECTOR_SIZE;   //每块的扇区数
  int sector = b->blockno * sector_per_block;   //扇区数
  int read_cmd = (sector_per_block == 1) ? IDE_CMD_READ :  IDE_CMD_RDMUL;  //一个块包含多个扇区的话就用读多个块的命令
  int write_cmd = (sector_per_block == 1) ? IDE_CMD_WRITE : IDE_CMD_WRMUL; //一个块包含多个扇区的话就用写多个块的命令

  if (sector_per_block > 7) panic("idestart");   //每个块不能大于7个扇区

  idewait(0);     //等待磁盘就绪
  outb(0x3f6, 0);  //用来产生磁盘中断,详见前面0x3f6寄存器

  outb(0x1f2, sector_per_block);  // 读取几个扇区
  /*像0x1f3~6写入扇区地址*/
  outb(0x1f3, sector & 0xff);             //LBA地址 低8位
  outb(0x1f4, (sector >> 8) & 0xff);      //LAB地址 中8位
  outb(0x1f5, (sector >> 16) & 0xff);     //LBA地址 高8位
  outb(0x1f6, 0xe0 | ((b->dev&1)<<4) | ((sector>>24)&0x0f));  //LBA地址最高的4位,(b->dev&1)<<4来选择读写主盘还是从盘

  if(b->flags & B_DIRTY){       //表示数据脏了,需要写到磁盘去了
    outb(0x1f7, write_cmd);     //向0x1f7发送写命令
    outsl(0x1f0, b->data, BSIZE/4);   //向磁盘写数据
  } else {
    outb(0x1f7, read_cmd);     //否则发送读命令,但没有读
  }
}

先不解释此函数,接着看磁盘中断处理程序:

void ideintr(void)
{
  struct buf *b;

  // First queued buffer is the active request.
  acquire(&idelock);    //取锁

  if((b = idequeue) == 0){   //如果磁盘请求队列为空
    release(&idelock);
    return;
  }
  idequeue = b->qnext;   //磁盘请求队列链首向后移

  // Read data if needed.
  if(!(b->flags & B_DIRTY) && idewait(1) >= 0)  //如果此次请求磁盘的操作是读且磁盘已经就绪
    insl(0x1f0, b->data, BSIZE/4);   //从0x1f0端口读取数据到b->data

  // Wake process waiting for this buf.
  b->flags |= B_VALID;    //此时缓存块数据有效
  b->flags &= ~B_DIRTY;   //此时缓存块数据不脏
  wakeup(b);    //唤醒等待在缓存块b上的进程

  // Start disk on next buf in queue.
  if(idequeue != 0)   //此时队列还不空,则处理下一个
    idestart(idequeue);  

  release(&idelock);   //释放锁
}

这两个函数应该结合在一起看,才是完整的一次磁盘操作。CPU 不能直接和磁盘进行数据交换,要用内存来中转或者说是缓存,所以进程要读写磁盘的数据的话都不是直接对磁盘进行读写的,而是读写磁盘在内存中的缓存,随后再同步到磁盘。这里所说的同步到磁盘就要用到上述的两个函数了也就是磁盘驱动程序。磁盘是公共资源,每次最多应只有一个进程操作磁盘,而且每次操作的单位是块,对于所有需要同步到磁盘的块维护了一个请求队列 ,这是一个单链表, 是其头指针,同步完一个缓存块,头指针便向后移动一个结点。

磁盘的操作大致可以分为以下几个步骤:

  1. 等待磁盘就绪
  2. 向相应的寄存器写入 要读写的扇区数,首个扇区的 LBA 地址,指定主盘和从盘
  3. 然后向命令寄存器写入命令
  4. 等待磁盘完成任务触发中断
  5. 执行磁盘中断程序完成磁盘操作

等待磁盘就绪就是上述的 函数,它读取状态寄存器查看磁盘是否就绪是否发生错误

向扇区数目寄存器写要操作的扇区数,这个跟块大小有关,据代码的意思每次操作不能超过 7 个扇区,但实际在硬件方面应该没有这个规定,据实际测试修改数值也能正常运行。

向 LBA 寄存器填写扇区地址的低 24 位, 表示低 8 位写到端口 0x1F3, 表示中 8 位,写到端口 0x1F4, 表示高 8 位,写到端口 0x1F5

表示顶 4 位,写到 device寄存器低 4 位, 表示缓存块缓存的数据所属哪个设备:主盘还是从盘,另外 device 寄存器的第 5 位和第 7 位始终为 1,所表示的值为 。将上述三者做或运算就是 device 寄存器该填写的值。

磁盘操作需要的参数已经传给磁盘了,现在该发送命令了,向命令寄存器(0x1F7) 发送相应的命令,这里只使用了读写两种命令,当然两种命令又分为读写一个扇区还是读写多个扇区,这个就还是跟块大小相关了,xv6 里面就是读写一个扇区的命令。

不论是 函数还是 函数,两者既处理读操作,又处理写操作,如何区分呢?就是靠缓存块的 标识,如果数据脏说明应该是写操作,否则就是读操作。但仔细看 函数最后几行代码:

if(b->flags & B_DIRTY){       //表示数据脏了,需要写到磁盘去了
    outb(0x1f7, write_cmd);     //向0x1f7发送写命令
    outsl(0x1f0, b->data, BSIZE/4);   //向磁盘写数据
} else {
    outb(0x1f7, read_cmd);     //否则发送读命令,但没有读
}

写操作的话,将写命令发给命令寄存器,然后开始写,将数据传给数据寄存。但是读呢?只是将读命令发送给命令寄存,然后就没下文了。为什么会这样呢?来捋捋磁盘的工作方式:

要让磁盘工作,就要给他发送命令,想让磁盘执行这些命令的话,还要提前将需要的参数给它,就比如要操作的扇区数,扇区地址等等,然后再写入命令。等待磁盘完成任务后它自己会触发中断通知 CPU 去拿结果。

除了我们在内存中给磁盘专门留了一块缓存区之外,磁盘自己本身有个缓冲区,一般也是 512 字节。所以当 CPU 发送读命令给磁盘后,磁盘就马不停蹄的将数据准备到自己的缓冲区,准备好了之后,触发中断通知 CPU 来拿数据。而写操作呢,直接将写命令写入命令寄存器,再将数据通过数据寄存器写到磁盘的缓冲区就行了,剩下的事交给磁盘自己去做,完成任务之后触发中断,执行中断处理程序修改缓存块的属性数据不再脏

所以知道了为什么 对待读写操作不同的原因了吧,读操作要磁盘准备好数据触发中断后才能从数据寄存器读到缓存块,而写操作直接通过数据寄存器写到磁盘的缓冲区就行了,剩下实际物理上的写操作磁盘自己完成,完成之后触发中断 CPU 再去收尾。

到这儿磁盘的中断处理程序应该也很好理解了,如果缓存块的 标志位显示不脏,说明本次磁盘操作应该是读操作,所以现在发生中断了,说明磁盘数据已经准备好了,该读取数据到缓存块了。如果不是读操作那就是写了,这里就只需要收个尾将缓存块的脏位去除掉就行。最后将休眠在这个缓存块上的进程唤醒。

中断处理程序最后应该显示通知磁盘本次中断完成,可以通过再次读取状态寄存器来完成。xv6 并没有这样操作,而是在每次 磁盘操作之前先向 端口0x3F6 写 0 来表示每次命令完成之后要产生中断。我在 ATA 手册里面没有找到这种显式通知磁盘中断结束的方式,但事实证明的确能够运行,有知情的大佬还请告知,另外据我测试,注释掉 中 这行代码,在中断处理程序末尾加上 也是能够正常工作的。

执行完本次的磁盘操作后,如果磁盘的请求队列不为空,那么就开始执行下一个。

void iderw(struct buf *b)
{
  struct buf **pp;

  if(!holdingsleep(&b->lock))   //要同步该块到磁盘,那前面应该是已经拿到了这个块的锁
    panic("iderw: buf not locked");
  if((b->flags & (B_VALID|B_DIRTY)) == B_VALID)  //这个缓存块既不脏数据又有效的话,则无事可做
    panic("iderw: nothing to do");
  if(b->dev != 0 && !havedisk1)   //这个缓存块缓存的不是任一设备的数据
    panic("iderw: ide disk 1 not present");

  acquire(&idelock);  //DOC:acquire-lock

  // 将这个块放进请求队列
  b->qnext = 0;  
  for(pp=&idequeue; *pp; pp=&(*pp)->qnext)  //DOC:insert-queue
    ;
  *pp = b;

  // 如果请求队列为空,当前块是唯一请求磁盘的块,则可以马上进行磁盘操作
  if(idequeue == b)
    idestart(b);

  // Wait for request to finish.
  while((b->flags & (B_VALID|B_DIRTY)) != B_VALID){  //进程休眠
    sleep(b, &idelock);
  }

  release(&idelock);  //释放锁
}

这个函数就没多少说的了,主要是将要同步的缓存块加进磁盘的请求队列,如果请求队列为空则可以马上执行,否则将当前缓存块加进请求队列的末尾。另外只有缓存块数据有效且不脏的时候才不会休眠,否则进程在缓存块之上休眠,等到中断服务程序处理之后再唤醒。

再者这个链表的操作还是值得说道说道,使用的是二级指针,主要因为这个队列是个单链表,再者就是为了操作的一致性,但也增加了复杂性。指针就是个存储地址的变量,而二级指针就是这个指针的地址。牢牢把握主这个指向关系,实际的问题应该就能够解决了,这里我就不详细解释了。设想如果请求队列为空,只有一个结点,有多个结点的情况,可以自己去模拟模拟增删的过程。

那就使用普通的一级指针行不行呢?也是得行的,就是普通的链表操作,我简单的写了下代码:

/**************/
struct buf *pp;
/**************/
pp = idequeue;
if(idequeue){   //请求队列不空的情况
    while(pp->qnext)
        pp = pp->qnext;
    pp->qnext = b;
}else{         //请求队列为空的情况
    idequeue = b;
}
/**************/

经测试也是能够正常工作的,如果使用一级指针,又没有头结点,那就需要分情况讨论,具体操作见上。

本文差不多到这就结束了,本文主要讲了简单的磁盘驱动程序,听起来高大上,细细研究其实也不难发现其本质。从上面已经可以看出,如果没得磁盘驱动程序,操作磁盘的步骤是很繁复的,要操作各个寄存器,处理各种前后的逻辑关系。所以磁盘驱动程序就将这些繁复操作给封装起来,形成几个函数,要操作磁盘调用这几个函数就是了。这么一说是不是对于磁盘驱动程序很清楚了,好啦本文就到这里了,有什么错误还请批评指针,也欢迎大家来同我讨论交流一起学习进步。

参考

https://www.unige.ch/medecine/nouspikel/ti99/ide.htm

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/DaYNPvA2aAWCXAfZ3zlKAQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录