Open vislee opened 2 years ago
磁盘设备速度和cpu存在着巨大差异,为了提升性能在内存开辟了一块空间作为cpu和磁盘块设备的桥梁,称为磁盘缓冲区。
从代码的角度分析一次带缓冲的磁盘操作。涉及到的linux-0.11代码文件有:fs.h、 buffer.c、 blk.h、 hd.c、 ll_rw_blk.c 要操作硬盘需要先了解一点硬盘逻辑结构或者说是文件系统结构。 文件系统由6部分组成,引导块|超级块|i节点位图|逻辑块位图|i 节点|数据区逻辑块 引导块:计算机加电BIOS自动会读入的代码和数据。 超级块:文件系统信息,i 节点数、逻辑块数、i 节点位图所占块、逻辑块位图所在块等 i节点位图:标志i节点是否使用。 逻辑块位图:标志逻辑块位图是否使用。 i 节点:磁盘上文件或目录索引节点。 逻辑块:存放文件或目录数据。
用户上层看到的是目录和文件,实际上读取目录文件是需要经过一系列转化定位到逻辑块的。 转换和定位的过程后面再起一篇来说。先看读写逻辑块bread函数。
bread
buffer.c
struct buffer_head * bread(int dev,int block) { struct buffer_head * bh; // 根据设备号和块号获取一个缓冲块 // 如果没有空闲缓存块,该任务就会被挂在缓冲等待队列buffer_wait睡眠。 // 所以如果从该函数返回就肯定会分配一个缓冲块,否则就有bug系统异常。 if (!(bh=getblk(dev,block))) panic("bread: getblk returned NULL\n"); // 缓冲块内容有效。即:和磁盘块内容是一致的。直接返回就可以,不用再从磁盘读取了。 if (bh->b_uptodate) return bh; // 否则,从磁盘读取。读取也是异步的。 ll_rw_block(READ,bh); // 跟踪上述函数,返回后。已经把读写磁盘的请求添加到磁盘的电梯队列里了。 // 等待判断是否已经把数据从磁盘读到了缓冲区。如果没有就把本任务挂到该缓冲区等待队列,让出cpu。 // 等待电梯队列请求被执行,把磁盘内容读到该缓冲块后唤醒执行。 wait_on_buffer(bh); // 被唤醒调度执行后,从该处恢复执行。首先判断缓冲块内容是否和磁盘一致。 if (bh->b_uptodate) return bh; // 如果不一致释放缓冲块返回NULL brelse(bh); return NULL; }
bread函数主要是分配缓冲块,调用ll_rw_block函数从磁盘读取数据到缓冲块。 下面看下ll_rw_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_NR是3,回调函数是 DEVICE_REQUEST do_hd_request 在blk.h文件中定义 !(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) { struct request * req; int rw_ahead; ...... // 读 写 if (rw!=READ && rw!=WRITE) panic("Bad block dev command, must be R/W/RA/WA"); lock_buffer(bh); // ***锁定缓冲区***,排斥其他任务使用。因为此时缓冲区数据不可用。 // 同时也是本任务是否可以继续执行的标识。 // 写磁盘操作然而缓冲区数据未更新 或 读磁盘 缓冲区数据和磁盘一致 // 因此不用重新操作磁盘,直接解锁缓冲区唤醒等待任务。 if ((rw == WRITE && !bh->b_dirt) || (rw == READ && bh->b_uptodate)) { unlock_buffer(bh); return; } repeat: /* we don't allow the write-requests to fill up the queue completely: * we want some room for reads: they take precedence. The last third * of the requests are only for reads. */ // 读请求是优先的,请求队列后1/3只留给读请求 if (rw == READ) req = request+NR_REQUEST; // 请求数组最后一项 else req = request+((NR_REQUEST*2)/3); // 请求数组2/3处 /* find an empty request */ // 从请求数组中找一个没用的请求结构 while (--req >= request) if (req->dev<0) break; /* if none found, sleep on new requests: check for rw_ahead */ if (req < request) { if (rw_ahead) { unlock_buffer(bh); return; } // 没有可用的请求结构,把该任务添加到等待请求结构队列后睡眠,等待被其他任务释放后唤醒。 sleep_on(&wait_for_request); goto repeat; } /* 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; // 把请求添加到磁盘电梯队列 // blk_dev 是支持外设数组,该结构体包含2个元素,回调函数和请求等待队列头指针。在blk.h文件定义。 add_request(major+blk_dev,req); } // 添加请求到电梯队列 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; // 磁盘电梯队列为空,则直接调用初始化的回调函数do_hd_request if (!(tmp = dev->current_request)) { dev->current_request = req; sti(); (dev->request_fn)(); return; } // 磁盘电梯队列不为空,找到合适的位置把请求插入队列。 // 此处的排序逻辑也就是电梯队列这个名称的由来。 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(); }
到此,我们已经跟踪完带缓冲的读操作。 大概的流程就是:查找一个缓冲块,构造一个磁盘读请求添加到磁盘电梯队列,挂起本任务,等待磁盘数据读到缓冲块后被唤醒继续执行返回缓冲块内容。
接下来分析一下磁盘操作函数函数do_hd_request的大概流程。
do_hd_request
首先在操作系统main.c文件main函数中,调用磁盘初始化函数hd_init注册了磁盘操作回调函数do_hd_request 和 磁盘中断回调函数 hd_interrupt.
main
hd_init
hd_interrupt
hd.c文件
void hd_init(void) { blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; // 注册磁盘中断回调函数。 hd_interrupt 在sys_call.s中定义,是个汇编函数_hd_interrupt。 // 因为gcc编译器会对c函数添加前缀(_),因此 c语言的hd_interrupt 在汇编符号就是_ hd_interrupt. // hd_interrupt函数,我们只关注_do_hd这个函数(符号)就可以。大概流程就是磁盘中断最终会调用该函数。 set_intr_gate(0x2E,&hd_interrupt); outb_p(inb_p(0x21)&0xfb,0x21); outb(inb_p(0xA1)&0xbf,0xA1); } // 该函数被添加到电梯队列的首任务调度。 // 主要的作用就是遍历电梯队列向磁盘发指令。 void do_hd_request(void) { int i,r; unsigned int block,dev; unsigned int sec,head,cyl; unsigned int nsect; INIT_REQUEST; dev = MINOR(CURRENT->dev); block = CURRENT->sector; if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) { end_request(0); goto repeat; } ...... if (CURRENT->cmd == WRITE) { hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr); for(i=0 ; i<3000 && !(r=inb_p(HD_STATUS)&DRQ_STAT) ; i++) /* nothing */ ; if (!r) { bad_rw_intr(); goto repeat; } port_write(HD_DATA,CURRENT->buffer,256); } else if (CURRENT->cmd == READ) { // 读磁盘请求,向磁盘发指令,具体涉及到磁盘专业的知识,和本文关系不大。知道大概流程和意思即可。 // 关键是最后一个参数read_intr,就是读磁盘请求结束后的磁盘中断的回调函数。 // 在hd_out 中,会把最后一个参数的函数指针付给do_hd,而do_hd就是前面说的磁盘中断回调函数。 hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr); } else panic("unknown hd-command"); } // 磁盘读请求结束后的磁盘中断回调函数 static void read_intr(void) { // 判断是否出错 if (win_result()) { bad_rw_intr(); do_hd_request(); return; } //从寄存器端口把数据读到缓冲块内存中 port_read(HD_DATA,CURRENT->buffer,256); CURRENT->errors = 0; CURRENT->buffer += 512; CURRENT->sector++; // 请求还有扇区没读完,继续等待磁盘中断 if (--CURRENT->nr_sectors) { do_hd = &read_intr; return; } // 本次请求读取的扇区已经全部读完 // 设置缓冲块b_uptodate标志,表示缓冲块数据有效。 // ***并唤醒等待该请求的进程*** end_request(1); // 继续遍历磁盘电梯队列的请求,向磁盘发送操作指令 do_hd_request(); }
最后在总一个总结,调用bread 函数读取外设数据,先根据外设号和磁盘块获取一个缓冲块,然后调用ll_rw_block函数, ll_rw_block函数根据外设号,找到注册的外设驱动,然后调用 make_request函数创建读外设请求。 make_request函数先从请求数组中获取一个空闲请求结构,赋值后调用add_request函数添加到对应外设驱动的请求对列里。add_request函数中,首先判断对以外设驱动的请求队列是否为空,如果是空则添加进去就调用外设回调函数向外设发指令。如果不为空,则说明已经有向外设发指令,排队等待执行即可。该任务也被挂起,等待磁盘响应后再被调度执行。 硬盘的回调函数是do_hd_request函数,该函数先赋值磁盘中断回调函数,读请求为:read_intr或 写请求为:write_intr。 当磁盘处理完请求后,就会给cpu发送0x2E中断,中断回调函数最后会调用read_intr或write_intr。 在read_intr函数中,把磁盘的响应数据保存到缓冲块buffer中,唤醒对应任务的请求,等待被再次调度执行然后bread函数返回缓冲块。
make_request
add_request
read_intr
write_intr
0x2E
概述
磁盘设备速度和cpu存在着巨大差异,为了提升性能在内存开辟了一块空间作为cpu和磁盘块设备的桥梁,称为磁盘缓冲区。
代码分析
从代码的角度分析一次带缓冲的磁盘操作。涉及到的linux-0.11代码文件有:fs.h、 buffer.c、 blk.h、 hd.c、 ll_rw_blk.c 要操作硬盘需要先了解一点硬盘逻辑结构或者说是文件系统结构。 文件系统由6部分组成,引导块|超级块|i节点位图|逻辑块位图|i 节点|数据区逻辑块 引导块:计算机加电BIOS自动会读入的代码和数据。 超级块:文件系统信息,i 节点数、逻辑块数、i 节点位图所占块、逻辑块位图所在块等 i节点位图:标志i节点是否使用。 逻辑块位图:标志逻辑块位图是否使用。 i 节点:磁盘上文件或目录索引节点。 逻辑块:存放文件或目录数据。
用户上层看到的是目录和文件,实际上读取目录文件是需要经过一系列转化定位到逻辑块的。 转换和定位的过程后面再起一篇来说。先看读写逻辑块
bread
函数。buffer.c
bread
函数主要是分配缓冲块,调用ll_rw_block
函数从磁盘读取数据到缓冲块。 下面看下ll_rw_block
函数是如何从磁盘读数据到缓冲块的。到此,我们已经跟踪完带缓冲的读操作。 大概的流程就是:查找一个缓冲块,构造一个磁盘读请求添加到磁盘电梯队列,挂起本任务,等待磁盘数据读到缓冲块后被唤醒继续执行返回缓冲块内容。
接下来分析一下磁盘操作函数函数
do_hd_request
的大概流程。首先在操作系统main.c文件
main
函数中,调用磁盘初始化函数hd_init
注册了磁盘操作回调函数do_hd_request
和 磁盘中断回调函数hd_interrupt
.hd.c文件
总结
最后在总一个总结,调用
bread
函数读取外设数据,先根据外设号和磁盘块获取一个缓冲块,然后调用ll_rw_block
函数,ll_rw_block
函数根据外设号,找到注册的外设驱动,然后调用make_request
函数创建读外设请求。make_request
函数先从请求数组中获取一个空闲请求结构,赋值后调用add_request
函数添加到对应外设驱动的请求对列里。add_request
函数中,首先判断对以外设驱动的请求队列是否为空,如果是空则添加进去就调用外设回调函数向外设发指令。如果不为空,则说明已经有向外设发指令,排队等待执行即可。该任务也被挂起,等待磁盘响应后再被调度执行。 硬盘的回调函数是do_hd_request
函数,该函数先赋值磁盘中断回调函数,读请求为:read_intr
或 写请求为:write_intr
。 当磁盘处理完请求后,就会给cpu发送0x2E
中断,中断回调函数最后会调用read_intr
或write_intr
。 在read_intr
函数中,把磁盘的响应数据保存到缓冲块buffer中,唤醒对应任务的请求,等待被再次调度执行然后bread
函数返回缓冲块。