author: hjjdebug
date: 2024年 03月 28日 星期四 16:49:14 CST
description: 从fread 到 磁盘驱动
文章目录
-
- [1. linux 内核调用栈](#1. linux 内核调用栈)
- [2. 读中断服务程序.](#2. 读中断服务程序.)
- [3. 何时计算的柱面,磁头,扇区号? 现代磁盘还有柱面,磁头,扇区概念吗?](#3. 何时计算的柱面,磁头,扇区号? 现代磁盘还有柱面,磁头,扇区概念吗?)
- [4. 固定的dev,block, 是不是每次都能找到固定的buffer缓冲区 bh ?](#4. 固定的dev,block, 是不是每次都能找到固定的buffer缓冲区 bh ?)
fread 函数到底是怎样工作的? 看下面代码
char buf[1024];
FILE *fp=fopen("1.txt",1); // 从文件系统中找到了1.txt 所对应的磁盘位置.
int n=fread(buf,1,sizeof(buf),fp); //从磁盘中读取1024个字节到内存,返回实际读取的字节数.
fread 通过 libc 库进入内核调用, libc部分咱就不说了,过程而已,看内核调用.
1. linux 内核调用栈
// 这里向硬盘控制器芯片设置了磁道,磁头,扇区号,设置了硬盘中断服务程序回调函数,并发命令为读数据,让磁盘作出反应,
//然后一路凯歌返回
0 in hd_out of hd.c:192
1 in do_hd_request of hd.c:341 //返回
2 in add_request of ll_rw_blk.c:76 // 返回
3 in make_request of ll_rw_blk.c:143 // 返回
4 in ll_rw_block of ll_rw_blk.c:155 // 返回
// 要等待数据到达,数据更新后返回 , 当磁盘迟迟不响应时,系统就不得不在这里等待了.
// 一次只能读一块数据到磁盘缓冲块中,早期的磁盘缓冲块大小时1024bytes, 2个扇区大小.
// 这就是内存cache, 当下一次再读该块数据时,就不用读磁盘了,而是可以直接从缓存中拿数据.
//bread 把数据读取到了磁盘缓冲区,有可能需要从磁盘读,也可能不用读了(刚读过,数据还有效)
5 in bread of buffer.c:285
// 当读取数据很大时,就只能循环一次次的调用bread 函数了.
// 每次把数据读取到缓冲块,再从缓冲块中把数据copy到用户区(file_read函数)
6 in file_read of file_dev.c:27
7 in sys_read of read_write.c:77 //直接返回
8 in system_call of system_call.s:94 // 判别一下是否需要调度进程,然后返回
2. 读中断服务程序.
当磁盘把数据准备好,发磁盘中断请求, CPU 响应中断,进硬盘中断服务程序
执行设定的回调函数void read_intr(void)
从端口读取一个扇区数据
port_read(HD_DATA,CURRENT->buffer,256); //从端口读取256个word,一个扇区,当时的总线16bits!
CURRENT->buffer += 512; //缓冲指针加512,以便存下一次数据, 这个缓冲区就是bh, bread 等待的就是它.
CURRENT->sector++; // 扇区数加1
//如果请求的扇区数比较多, 会继续设定read_intr 为回调,函数返回,中断服务程序结束.
if (--CURRENT->nr_sectors) {
do_hd = &read_intr; //还有数据需要读,设置read_intr(自己) 为回调,继续读
return;
}
如果磁盘请求的扇区数较多,磁盘数据还没有读完,会再次触发硬盘中断,引起新一轮中断响应.
其实最多也就2个扇区,而且每次也必然是2个扇区,因为内核就是这么设定的. 一次一块.
众多的数据,那是靠file_read 循环来获取的.
3. 何时计算的柱面,磁头,扇区号? 现代磁盘还有柱面,磁头,扇区概念吗?
当你打开文件fopen 时, 文件系统就给你找到了inode号,它记录了文件位置(逻辑地址块号).
在bread 函数中有一个getblk 调用,根据dev(硬盘号例如0x301)和块号查找磁盘缓冲块函数
if (!(bh=getblk(dev,block))) //一定会得到一个bh,并且其bh->count>=1, >1表示不只一处使用 =1是新申请到的.
哦! buffer_head 结构中没有柱面,磁头,扇区信息.
在do_hd_request 函数中有以下代码:
__asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
"r" (hd_info[dev].sect)); //余数在edx,为sector,商在eax,继续做除法
__asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0),
"r" (hd_info[dev].head)); //block/sector/head = cylinder, 余数head
sec++;
__asm__汇编语法,有2个冒号分割,第一部分是输出寄存器,第二部分为输入寄存器,第三部分(若有的话,那就需要3个冒号了)为其它改动寄存器
第一行的汇编代码的意思是输出把eax赋值给block, 把edx赋值给sec, 输入时把block送eax, 把0送edx. 把hd_info[dev].sec送寄存器
用eax/寄存器, 商在eax, 余数在edx, 注意,做完除法后eax送给了block, edx送给了sec
第二行的汇编代码. 输出eax给cyl, 余数edx给head, 输入部分block送eax,0送edx,hd_info[dev].head送寄存器
此时的block是第一步操作得到的整数,再继续除以head, 则整数是cyl,余数是head.
如果汇编代码你看懂了, 还要理解为什么要这样算, 这涉及到硬盘的构造. 对于一片双面磁盘,上下有2个磁头可以读写上面和下面.
磁盘被划分为很多同心圆叫磁道, 每个磁道又被按512bytes 划分为扇区.
硬盘就是由多个双面磁盘摞在一起组成的,这时候这些同心圆不叫磁道了,叫柱面,对吧,很形象. 多个磁盘的相同的磁道构成柱面.
心存这个硬盘结构,就理解了为什么这样就得到了柱面号,磁头号,扇区号
我想查看一下我的磁盘参数, 发现fdisk, smartctl 都不再提供柱面,磁头参数,只提供存储容量,难道是这些参数不使用了吗?
查了一下互联网:
CHS编址方式在早期的小容量盘中非常流行.
目前的大容量硬盘的设计已经有所改变,转为LBA编址方式。不再划分柱面和磁头号, 这些数据由硬盘自身保留,
而磁盘对外提供全部为线性的地址,即LBA地址。(Logical Block Address)
是啊, 柱面,磁头,扇区是磁盘厂家的事,没必要暴露给外边, 所以改良后,直接传逻辑地址, 磁盘就可以工作了.
sudo fdisk -L /dev/sda
命令(输入 m 获取帮助): p
Disk /dev/sda:931.53 GiB,1000204886016 字节,1953525168 个扇区
Disk model: ST1000DM010-2EP1
单元:扇区 / 1 * 512 = 512 字节
扇区大小(逻辑/物理):512 字节 / 4096 字节
I/O 大小(最小/最佳):4096 字节 / 4096 字节
磁盘标签类型:dos
磁盘标识符:0xe264da3d
设备 启动 起点 末尾 扇区 大小 Id 类型
/dev/sda1 2048 838862847 838860800 400G 83 Linux
/dev/sda2 838862848 1953525167 1114662320 531.5G 83 Linux
命令(输入 m 获取帮助): q
可见,现在大容量磁盘都是一次读取4K字节, 512字节已成过去式.
为啥?原来的磁盘数据总线才16bits,现在都是64bits的天下, 原来接口读一次才能读2bytes, 现在接口读一次就能读8bytes.
4. 固定的dev,block, 是不是每次都能找到固定的buffer缓冲区 bh ?
答, 不是. 磁盘buffer缓冲区被串成一个链表来管理, 空闲的缓冲区也被串成一个链表, 当(dev,block)对申请一块缓存存放数据时,
从闲置的缓冲区中拿一个最闲的就可以了. 就是拉郎配! 不过要把配对的bh挂在由(dev,block)构建的hash 表中. 这样当再次读取
(dev,block)数据时,先到hash表中看一下,有bh, 就不用到磁盘读了,直接去用就可以了.
那bh 什么时候失效呢?
嗯, 磁盘缓存是循环着使用的,当bh变成最闲的那一个,被新的(dev,block)拉郎配的时候,那旧hash表中记录的信息就必需要删除了.
代码也是现实世界的反映, 搞清了外部道理,看代码也就看懂了.