L28 生磁盘的使用
-
磁盘定义
磁盘利用了电流的磁效应,对一些电信号进行磁化,保存在磁盘中,用来表示一些信息。
每个盘面上都有磁道、扇区这2个概念。先从磁道说起,每个盘面上有多条圆环,也就是磁道,这些磁道中存储着磁盘的信息。每个磁道上又被分为若干个扇区,磁盘每次的读写数据的操作单位就是这个扇区。每个扇区的大小一般为512字节大小。磁盘是由多个盘片叠加在一起的,由于磁盘每次进行读写的开始都要移动机械的磁臂到对应的盘面、磁道上,这个过程很浪费时间。所以为了提高效率,把盘面与磁道抽象成另外2个概念,就是柱面、磁头。
其中,GAP 是间隔字段,用于起隔离作用。通常 GAP是 12 字节的 0。每个扇区地址场的地址字段存放着相关扇区的柱面号、磁头号(面号)和扇区号,因此通过读取地址场中的地址信息就可以唯一地确定一个扇区。
上图中的cyl
就是柱面的意思,就是图中蓝色区域的圆柱体侧面,也称为柱面。上图中的head
的范围就是柱面的底部到顶部范围之间。这样一旦确定cyl
和head
,就确定了哪个盘面和哪个磁道了,这样就可以找到对应的扇区了。为什么把盘面、磁道换成柱面、磁头查找会提高效率呢?根本原因是往往磁盘中盘面很多,每个盘面的承载信息有限,需要移动很多次磁臂才能记录信息。而用柱面的概念的话,就利用了每个盘面磁道的有限性,每次记载数据只需移动很少次的磁臂就可以搞定,提高了读写效率。sec
表示扇区。 -
磁盘使用
要往磁盘的某个扇区写一个字节,那么我们需要知道这个扇区对应的哪个柱面中的哪个磁头。知道了这些参数后,然后把这些参数传到磁盘控制器,磁盘控制器是 CPU 与驱动器之间的逻辑接口电路,它从 CPU 接收请求命令,向驱动器发送寻道、读/写和控制信号,并且控制和转换数据流形式。磁盘控制器再根据这些参数进行驱动磁盘写数据。下面看一段代码:
// linux-0.11/kernel/blk_drv/hd.c void do_hd_request(void) { ... // 前面blabla一大堆,就是要找出dev,nsect, // sec,head,cyl,WIN_WRITE,&write_intr这些数据 // 然后传送这些参数到磁盘控制器 if (CURRENT->cmd == WRITE) { hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr); } ... }
上面这段代码就是磁盘读写的请求函数,里面最后调用了
hd_out
函数,在调用之前传入了柱面号、磁头号、扇区号等核心参数。下面看下do_out
函数:static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect, unsigned int head,unsigned int cyl,unsigned int cmd, void (*intr_addr)(void)) { register int port asm("dx"); if (drive>1 || head>15) panic("Trying to write bad sector"); if (!controller_ready()) panic("HD controller not ready"); do_hd = intr_addr; // 这些outb_p接口就是往外设传送数据的 // 也是cpu中磁盘驱动的核心代码 outb_p(hd_info[drive].ctl,HD_CMD); port=HD_DATA; outb_p(hd_info[drive].wpcom>>2,++port); outb_p(nsect,++port); outb_p(sect,++port); outb_p(cyl,++port); outb_p(cyl>>8,++port); outb_p(0xA0|(drive<<4)|head,++port); outb(cmd,++port); }
上面这段代码就是磁盘驱动的核心代码。从这里不难看出,如果想读写磁盘,就传入磁盘的柱面、磁头、扇区、缓冲区等参数后,就可以读写磁盘啦。但是这样呢比较麻烦,在每次读写磁盘的时候。有一种简单的方法,就是把柱面、磁头、扇区包装成一个磁盘块的概念,接下来看下磁盘块。
-
磁盘块封装
CHS
通过磁盘块(block)参数怎样计算出柱面号(c)、磁头号(h)、扇区号(s)呢?
我们先来考虑一种情况,假如我们使用block参数读写磁盘,那么每次读写磁盘的block数可能有很多个,为了磁盘读写效率的提升,我们要保证block相邻的磁盘块可以快速读写。如何保证这样的效果呢?如果把相邻的block块读写近似映射成相邻的扇区号读写,那么达到上面的效果了。
所以可以通过这样一个公式来推算chs
:
block = c * (heads * sectors) + heads * sectors + s
公式中的heads是一个磁盘的物理磁头总数量;公式中的sectors是一个磁盘盘面的物理扇区总数量。给出block,我们可以逆推出c、h、s这些参数。(s=block%sectors
c=block/( heads * sectors)
)这样的话我们就可以通过一个block参数进行磁盘读写啦。还有一个block可以是若干个扇区的,这取决与程序怎样编写。如linux0.11的盘块就是2个扇区即1K
。 -
磁盘的访问时间
磁盘的访问时间 = 写入控制器时间 + 寻道时间 + 旋转时间 + 传输时间
其中寻道时间(移动磁臂)大约占12ms到8ms。 -
多进程通过队列使用磁盘
通过传递block这个参数就可以在磁盘中指定的一些扇区进行读写操作,操作系统会有一个请求队列缓存各种进程的磁盘请求,进而涉及到磁盘调度算法。 -
磁盘调度算法
- FCFS先来先服务
- SSFT短寻道优先,存在饥饿问题
- SCAN电梯算法,每个请求都有处理机会
-
电梯算法详解
相对保证了多个进程使用磁盘既准确又快速
// linux-0.11/kernel/blk_drv/ll_rw_blk.c static void add_request(struct blk_dev_struct * dev, struct request * req) { struct request * tmp; req->next = NULL; cli(); if (req->bh) req->bh->b_dirt = 0; if (!(tmp = dev->current_request)) { dev->current_request = req; sti(); (dev->request_fn)(); return; } // 调度算法的核心代码,分析下面代码,我们先要知道 // IN_ORDER是什么意思?可以看下文IN_ORDER的介绍 // 就可以知道是比较tmp与req中柱面号大小的 //下面从这段代码可以看出,当符合这两种情况时就跳出循环 // 并将req插入tmp和next之间。一种情况是当tmp的柱面号 // 小于req的柱面号,且req小于next的柱面号,这种其实 // 就相当于电梯上升时候的情况;还有一种情况是当tmp的柱面号 // 小于next的柱面号,且req小于next的柱面号,这种其实 // 就相当于电梯下降时候的情况。不管这两种任何一种情况, // 下一步磁盘读写都会进入req这个对象上,否则就按照原有的 // 队列进行磁盘读写。这样的话就能更高效的使用磁盘 for ( ; tmp->next ; tmp=tmp->next) if ((IN_ORDER(tmp,req) || !IN_ORDER(tmp,tmp->next)) && IN_ORDER(req,tmp->next)) break; req->next=tmp->next; tmp->next=req; sti(); }
IN_ORDER的代码如下:
// 核心思想是比较s1与s2中的sector大小,其实就是比较s1与s2中的柱面号的大小 // 因为柱面的寻找是耗时最长的,所以要保证寻找柱面也即寻道的时间不能太长, // 就要在寻道上面做优化处理 #define IN_ORDER(s1,s2) \ ((s1)->cmd<(s2)->cmd || ((s1)->cmd==(s2)->cmd && \ ((s1)->dev < (s2)->dev || ((s1)->dev == (s2)->dev && \ (s1)->sector < (s2)->sector))))
L29 从生磁盘到文件
-
从文件得到盘块号
文件:建立字符流到盘块集合的映射关系,完成字符流位置算出盘块号
连续结构实现文件映射,比较适合读写,只需要起始块号和块数
链式结构实现文件映射,读写存储慢但是适合动态变化
索引结构实现文件映射,inode存着文件信息,既适合读写又适合增长
实际系统使用多级索引
L30 文件使用磁盘的实现
-
代码分析:
-
得到磁盘号代码:
int do_execve(unsigned long * eip,long tmp,char * filename, char ** argv, char ** envp) { ... // 这里的关键代码是bread这个读磁盘的接口,我们发现函数 // bread的第二个参数是某个文件的设备号 if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) { retval = -EACCES; goto exec_error2; } ... } struct buffer_head * bread(int dev,int block) { struct buffer_head * bh; // 这句代码就是获取携带磁盘块的指针,也就是获取bh if (!(bh=getblk(dev,block))) panic("bread: getblk returned NULL\n"); if (bh->b_uptodate) return bh; // 读写磁盘 ll_rw_block(READ,bh); // 唤醒下一个磁盘读写请求队列,如果有的话。 wait_on_buffer(bh); if (bh->b_uptodate) return bh; brelse(bh); return NULL; }
-
从上面看出进程获取
block
是通过文件系统的inode
来获取的。下面当获取到block
后,我们会进入ll_rw_block
这个函数:void ll_rw_block(int rw, struct buffer_head * bh) { unsigned int major; if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV || !(blk_dev[major].request_fn)) { printk("Trying to read nonexistent block-device\n\r"); return; } // 这里会进入这句函数,来请求磁盘的读写 make_request(major,rw,bh); }
static void make_request(int major,int rw, struct buffer_head * bh) { /* fill up the request-info, and add it to the queue */ req->dev = bh->b_dev; req->cmd = rw; req->errors=0; req->sector = bh->b_blocknr<<1; req->nr_sectors = 2; req->buffer = bh->b_data; req->waiting = NULL; req->bh = bh; req->next = NULL; // 这个时候将解析好的req加入到磁盘读写请求队列中 add_request(major+blk_dev,req); }
-
从上面可以看出我们加完
req
后,就唤醒另一个磁盘请求队列进程,然后睡眠自己。那么磁盘控制器是如何访问这些队列来读写磁盘呢?当然是通过磁盘中断。hd_interrupt: pushl %eax pushl %ecx pushl %edx push %ds push %es push %fs movl $0x10,%eax // 进入内核空间 mov %ax,%ds mov %ax,%es movl $0x17,%eax // 重置fs mov %ax,%fs movb $0x20,%al outb %al,$0xA0 # EOI to interrupt controller #1 jmp 1f # give port chance to breathe 1: jmp 1f 1: xorl %edx,%edx xchgl do_hd,%edx testl %edx,%edx jne 1f movl $unexpected_hd_interrupt,%edx 1: outb %al,$0x20 // 中断处理函数do_hd入口 call *%edx # "interesting" way of handling intr. pop %fs pop %es pop %ds popl %edx popl %ecx popl %eax iret
-
从上面可以看出执行
do_hd
这个函数指针,这个函数指针又会指向谁呢?从下面代码可以看出可以执行到read_intr
。其实吧,这个do_hd
可以指向磁盘读写的中断处理函数,接下来分析一下读的处理函数。static void read_intr(void) { // 获取上次读磁盘的状态,看看有没有错误之类的 if (win_result()) { bad_rw_intr(); do_hd_request(); return; } // 这个就是读的核心代码,向外设的HD_DATA数据端口读取数据 // 每次读取一个扇区512个字节的信息 port_read(HD_DATA,CURRENT->buffer,256); CURRENT->errors = 0; CURRENT->buffer += 512; CURRENT->sector++; // 如果没读完,那就继续读,知道读完为止 if (--CURRENT->nr_sectors) { do_hd = &read_intr; return; } // 读完后,唤醒另一个等待进程啦 end_request(1); // 另一个等待进程继续做磁盘读写请求处理 do_hd_request(); }
-
至此一个进程的磁盘请求读写已经完成,后面的进程请求以此类推。
-
总结:
L31 目录与文件系统
- 从文件路径名
/.../.../...
到fd
找到FCB
中的inode
- 再由
inode
找到盘块号 - 将盘块号加入电梯队列
- 根据电梯队列的盘块号算出
c、h、s
- 使用
outp
发送到磁盘控制器 - 磁盘控制器驱动马达磁生电生成数据,使用磁盘
-
通过路径名找到
FCB
中的inode
首先找到根目录,根目录是一个文件,在FCB数组中有固定的位置,根据根目录中的索引项找到根目录对应的数据块,也就找到了根目录下的目录项、文件名、以及对应的FCB的地址,然后以此类推一层层找到最终的FCB的inode。
-
系统自举要格式化磁盘,具体的格式样子如下所示:
挂载使用磁盘mount就是去读取超级块,以便于找到根目录,从而实现磁盘文件系统的使用。 -
完全映射下的磁盘使用
L32 目录解析代码实现
-
open函数的核心就是找到 inode
-
get_dir完成真正的目录解析
-
从根目录开始进行目录解析。
-
iget读取inode,读磁盘一定要知道盘块号。
-
开始目录解析—find_entry (&inode,name,…,&de)
操作系统全图
操作系统是管理硬件的软件,管理CPU利用多进程结构交替执行,CPU充分忙碌,执行过程中会有计算指令,也会有类似*p=7
的指令,即将7放入内存,就要进行重定位和地址翻译,填写段表页表,内存就使用起来了;也会执行到open,read等函数操作文件,就会进行目录解析,算出磁盘块,发到电梯队列,进行磁盘的使用,进而IO以及文件系统就使用起来了;整个操作系统就在多进程带动下,就使得文件系统,IO操作等开始同时使用,操作系统也就运行起来了。
完结撒花!!!