块设备驱动程序

花门楼前见秋草,岂能贫贱相看老。这篇文章主要讲述块设备驱动程序相关的知识,希望能为你提供帮助。


文章目录

  • ??9.1 总体功能??
  • ??9.1.1 块设备管理的数据结构——块设备请求项和请求队列??
  • ??9.1.2 块设备访问调度处理流程??
  • ??9.2 硬盘初始化程序 setup.S??
  • ??9.3 接口程序 ll_rw_blk.c??
  • ??9.3.1 功能描述??
  • ??9.3.2??
  • ??9.3.3 块设备初始化函数??
  • ??9.3.4 块设备驱动程序与系统的接口函数??
  • ??9.3.5 低级页面读写函数??
  • ??9.3.6 关于缓冲块的两个操作??
  • ??9.3.7 创建请求项并插入请求队列??
  • ??9.4 块设备头文件 blk.h??
  • ??9.5 硬盘控制器驱动程序 hd.c??
  • ??9.5.1??
  • ??9.5.2 初始化硬盘和设置硬盘所用数据结构信息??
  • ??9.5.3 向硬盘控制器发送命令??
  • ??9.5.4 处理硬盘当前请求项??
  • ??9.5.5 硬盘中断处理过程中调用的 C 函数??
  • ??9.5.6 硬盘控制器操作辅助函数??
  • ??9.6 内存虚拟盘驱动程序 ramdisk.c??
  • ??9.5.1 处理内存虚拟盘当前请求项??
  • ??9.5.2 内存虚拟盘初始化??
  • ??9.5.3 加载根文件系统??


块设备驱动管理相关代码:
文件名称

位置

功能

blk.h

linux-0.12\\kernel\\blk_drv

块设备专用头文件

ll_rw_blk.c

linux-0.12\\kernel\\blk_drv

其它程序访问块设备的接口程序

hd.c

linux-0.12\\kernel\\blk_drv

硬盘驱动程序

ramdisk.c

linux-0.12\\kernel\\blk_drv

内存虚拟盘驱动程序

floppy.c

linux-0.12\\kernel\\blk_drv

软盘驱动程序

Linux 0.11 内核中的主设备号
主设备号

类型

说明

请求项操作函数

0





NULL

1

块/字符

【块设备驱动程序】ram,虚拟盘,内存设备

do_rd_request()

2



fd,软驱设备

do_fd_request()

3



hd,硬盘设备

do_hd_request()

4

字符

ttyx 设备

NULL

5

字符

tty 设备

NULL

6

字符

lp 打印机设备

NULL

Linux 0.11 内核主要支持硬盘、软盘和虚拟盘三种块设备。
9.1 总体功能对硬盘和软盘设备上数据的读写操作是通过中断处理程序进行的。内核每次读写的数据量以一个逻辑块(1KB)为单位,块设备控制器以扇区(512B)为单位。在处理过程中,使用读写请求项等待队列来顺序缓冲一次读写多个逻辑块的操作。
当进程需要从硬盘上读取逻辑块时,会向缓冲区管理程序提出申请,缓冲区管理程序会在缓冲区中寻找该块是否存在,如果存在则将缓冲区块头指针返回给申请进程,若缓冲区中不存在该块,则缓冲区管理程序调用低级块读写函数ll_rw_block(),向块设备驱动程序发送请求。

9.1.1 块设备管理的数据结构——块设备请求项和请求队列
对于各种块设备,内核使用一张块设备表来管理,每种块设备都在块设备表中占有一项。
struct blk_dev_struct
void (*request_fn)(void); // 请求项操作的函数指针
struct request * current_request; // 当前请求项指针
;
extern struct blk_dev_struct blk_dev[NR_BLK_DEV]; // 块设备表(数组)NR_BLK_DEV=7

  • 第一个字段——函数指针:用于操作相应块设备的请求项,对于硬盘驱动程序,对应的是do_hd_request()。
  • 第二个字段——当前请求项结构指针:用于指明被块设备目前正在处理的请求项。
当内核发出一个块设备读写或其他操作请求时,ll_rw_block()函数会根据参数指明的操作命令和数据缓冲块头中的设备号,利用对应的请求项操作函数 ??do_XX_request()?? 建立一个块设备请求项,利用电梯调度算法构造请求项队列。
struct request
int dev; // 使用的设备号(-1表示空闲)
int cmd; // 命令(read或write)
int errors; // 操作时产生的错误次数
unsigned long sector; // 起始扇区(1块=2扇区)
unsigned long nr_sectors; // 读/写扇区数
char * buffer; // 数据缓冲区
struct task_struct * waiting; // 任务等待操作执行完成的地方
struct buffer_head * bh; // 缓冲区头指针
struct request * next; // 指向下一请求项
;
extern struct request request[NR_REQUEST]; // 请求项数组(NR_REQUEST=32)

每个块设备的当前请求指针与请求项数组中该设备的请求项链表共同构成了该设备的请求队列,项与项之间形成链表,所有请求项只有32项,所有块设备共用32个请求项。

采用数组+链表的目的:
  1. 利用请求项的数组结构在搜索空闲请求块时提高效率。
  2. 满足电梯算法插入请求项操作。



建立写操作时的空闲项搜索范围限制在整个请求项数组的前2/3范围内,剩下的部分留给读操作建立请求项使用。

9.1.2 块设备访问调度处理流程
在向硬盘控制器发送操作命令前,先对读/写磁盘扇区数据的顺序进行排序(I/O调度程序处理),使得请求项访问的磁盘扇区数据块都尽量依次顺序进行操作,并非按照请求项收到的顺序发送给块设备进行处理。

电梯算法:磁头向一个方向移动,要么一直向盘片圆心方向移动,要么反方向向盘片边缘移动。

写盘操作

写数据过程中:cpu 发送的写命令是hd_out(),允许写信号DRQ是控制器状态寄存器的数据请求服务标志,当控制器把数据全部写入驱动器(或发送错误)后,产生中断信号,调用预置C函数(write_intr())来检查是否还有数据要写,如果有,则再把一个扇区的数据送入驱动器,再次等待引发的中断。
如果所有数据均已写入驱动器,则C函数就执行本次写盘结束后的处理工作:唤醒等待该请求项有关数据的相关进程、唤醒等待请求项的进程、释放当前请求项并从链表中删除该请求项以及释放锁定的相关缓冲区。再调用请求项操作函数去执行下一个读/写盘请求项。
读盘操作

读数据过程:cpu 向控制器发送参数,控制器按照要求从驱动器中读入数据到自己的缓冲区,向 cpu 产生中断,执行预设置 C 函数(read_intr())将控制器中的数据送入系统缓冲区,cpu 继续等待中断信号。
预置的C函数首先把控制器缓冲区中一个扇区的数据放到系统的缓冲区中,调整系统缓冲区中当前写入位置,然后递减需读的扇区数量。如果还有数据要读,则继续等待控制器发出下一个中断信号。
对于虚拟设备,由于不涉及与外部设备之间的同步操作,当前请求项对虚拟设备的读写操作完全在do_rd_request()中实现。
各系统间调度流程:

9.2 硬盘初始化程序 setup.S

linux/boot/setup.S

setup.S是一个操作系统加载程序,主要作用是利用中断从Bios中读取机器系统数据,将这些数据保存到0x90000开始的位置,内存地址0x90080和0x90090保存了两块硬盘的参数表,如果没有第二块硬盘则清空。
INITSEG= DEF_INITSEG! 0x9000
! 取第1个硬盘信息(复制硬盘参数表)
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41] ! 取中断向量0x41的值,即内存4*0x41=0x104处的值(第1个硬盘参数表的首地址) -> ds:si
mov ax,#INITSEG
mov es,ax
mov di,#0x0080! 硬盘参数表目的地址 0x9000:0x0080 -> es:di
mov cx,#0x10! 表的长度为16B
rep
movsb

! 取第2个硬盘信息
mov ax,#0x0000
mov ds,ax
lds si,[4*0x46] ! 取中断向量0x46的值 第2个硬盘的首地址 ds:si
mov ax,#INITSEG
mov es,ax
mov di,#0x0090 ! 目的地址:0x9000:0x0090 -> es:di
mov cx,#0x10
rep
movsb

! 检查是否存在第2块硬盘
mov ax,#0x01500 ! 功能号ah=0x15
mov dl,#0x81! 驱动器号
int 0x13
jcno_disk1
cmp ah,#3
jeis_disk1
no_disk1:
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
mov ax,#0x00
rep
stosb
is_disk1:

代码流程:
  1. 复制第一块硬盘参数表内容到内存。中断0x41处向量值保存了硬盘参数表的首地址(4B),复制BIOS 中第一块硬盘参数表的内容到内存0x90080处。代码第5行,取出了0x41处4个字节的内容,即第一块硬盘参数表的地址保存到[ds:si]。代码第6~12行所示,设置目的地址[es:di]=0x9000:0x0080,传输字节数(16字节)。
  2. 复制第二块硬盘参数表内容到内存。代码14~22行所示,第二块硬盘参数表的地址存储在中断0x46处。
  3. 检查是否有第二块硬盘,如果没有则把第二个表清零。如代码2539行所示,代码2530行实现了判断是否是硬盘的功能,25和26行,传入了功能号ah=0x15和驱动器号dl=0x81,以选择第2块硬盘。然后调用0x13中断取类型功能。代码29行,根据保存在ah中的类型码,判断是否为3(硬盘类型)来确定是否存在第二块硬盘。代码31~39行,执行了第2个硬盘表清零操作,清空16字节的硬盘参数表,代码第28行,jc 命令的原因是:int13的出口参数CF=1——操作失败,AH=状态代码,否则,AH=00H — 未安装驱动器。
BIOS 使用int13调用取盘类型功能

  • 参数1:ah(功能号,表明读取盘类型)=0x15;
  • 参数2:dl(驱动器号):0x80-第1个硬盘,0x81-第2个硬盘。
返回结果 ah(类型码):00-没有这个盘CF置位;01-是软驱,没有change-line支持;02-是软驱(或其它可移动设备),有change-line支持;03-是硬盘。

9.3 接口程序 ll_rw_blk.c

blk-dev-initll-rw-blockll-rw-pagemake-requestadd-request

9.3.1 功能描述
为其它设备创建块设备读写请求项,并插入到指定设备请求队列中。
该程序主要用于执行底层块设备读/写操作,是本章所有块设备(硬盘、软盘和 Ram 虚拟盘)与系统其他部分之间的接口程序。通过调用该程序的低级块读写函数 ll_rw_block(),系统中的其他程序可以异步读写块设备中的数据。实际的读写操作是由设备的请求项处理函数 request_fn()完成(对于硬盘操作——do_hd_request()、对于软盘操作——do_fd_request()、对于虚拟盘操作——do_rd_request())。
根据下图执行流程,ll_rw_block()针对一个块设备建立起一个请求项,并通过测试块设备的当前请求项指针为空而确定设备空闲时,就会设置该新建的请求项,并直接调用request_fn()对该请求项进行操作。否则就会使用电梯调度算法将新建的请求项插入到该设备的请求项链表中等待处理。而当request_fn()结束对一个请求项的处理,就会把该请求项从链表中删除。在处理每个请求项时,通过中断方式进行。

9.3.2
9.3.3 块设备初始化函数

linux/kernel/blk_drv/ll_rw_blk.c/blk_dev_init()

该程序主要由main.c进行调用,完成对请求数组**request[NR_REQUEST]**进行初始化,将所有的请求项置为空闲(-1)。
struct request request[NR_REQUEST]; // 请求项数组队列 NR_REQUEST=32
void blk_dev_init(void) // 初始化请求项

int i;
for (i=0 ; i< NR_REQUEST ; i++)
request[i].dev = -1; // 表示该设备空闲
request[i].next = NULL;


// 请求队列中,请求项的结构 blk.c
struct request
int dev; /* -1 if no request */
int cmd; /* READ orc WRITE */ // READ(0) WRITE(1)
int errors; //操作时产生错误的次数
unsigned long sector; // 起始扇区
unsigned long nr_sectors; // 读/写扇区数
char * buffer; // 数据缓冲区
struct task_struct * waiting; // 任务等待请求完成操作的队列
struct buffer_head * bh; // 缓冲区头指针
struct request * next; // 指向下一请求项
;

9.3.4 块设备驱动程序与系统的接口函数

linux/kernel/blk_drv/ll_rw_blk.c/ll_rw_block()

低级块读写函数。该函数通常是在fs/buffer.c程序中被调用,其主要功能是创建块设备读写请求项并插入到指定块设备请求队列中。实际的读写操作则是由设备的request_fn()完成。
void ll_rw_block(int rw, struct buffer_head * bh) // 接口函数 检查参数 调用 make_request

unsigned int major; // 主设备号
if ((major=MAJOR(bh-> b_dev)) > = NR_BLK_DEV || // NR_BLK_DEV=7
!(blk_dev[major].request_fn))// 判断主设备号是否存在以及该设备的请求操作函数是否存在
printk("Trying to read nonexistent block-device\\n\\r");
return;

make_request(major,rw,bh);

函数输入:rw – READ、READA、WRITE、WRITEA,bh – 数据缓冲块头指针。
第4~5行,判断设备主设备号是否存在或者该设备的请求操作函数不存在,如果是则显示出错信息,否则创建请求项并插入请求队列。
// 数据缓冲块头指针定义 Fs.h 文件系统
struct buffer_head
char * b_data; /* pointer to data block (1024 bytes) */// 指针
unsigned long b_blocknr; /* block number */// 块号
unsigned short b_dev; /* device (0 = free) */ // 数据源的设备号
unsigned char b_uptodate; // 更新标志:表示数据是否已更新
unsigned char b_dirt; /* 0-clean,1-dirty */ // 修改标志:0:未修改,1:已修改
unsigned char b_count; /* users using this block */// 使用的用户数
unsigned char b_lock; /* 0 - ok, 1 -locked */ // 缓冲区是否被锁定
struct task_struct * b_wait; // 指向等待该缓冲区解锁的任务
struct buffer_head * b_prev; // hash 队列上前一块(这四个指针用于缓冲区管理)
struct buffer_head * b_next; // hash 队列上下一块
struct buffer_head * b_prev_free; // 空闲表上前一块
struct buffer_head * b_next_free; // 空闲表上下一块
;
// 块设备数组
struct blk_dev_struct blk_dev[NR_BLK_DEV] =
NULL, NULL ,/* no_dev */
NULL, NULL ,/* dev mem */
NULL, NULL ,/* dev fd */
NULL, NULL ,/* dev hd */
NULL, NULL ,/* dev ttyx */
NULL, NULL ,/* dev tty */
NULL, NULL/* dev lp */
;

struct task_struct// 调度程序头文件 sched.h 中
/* these are hardcoded - dont touch */
long state; /* -1 unrunnable, 0 runnable, > 0 stopped */
long counter;
long priority;
long signal;
struct sigaction sigaction[32];
long blocked; /* bitmap of masked signals */
/* various fields */
int exit_code;
unsigned long start_code,end_code,end_data,brk,start_stack;
long pid,pgrp,session,leader;
int groups[NGROUPS];
/*
* pointers to parent process, youngest child, younger sibling,
* older sibling, respectively.(p-> father can be replaced with
* p-> p_pptr-> pid)
*/
struct task_struct*p_pptr, *p_cptr, *p_ysptr, *p_osptr;
unsigned short uid,euid,suid;
unsigned short gid,egid,sgid;
unsigned long timeout,alarm;
long utime,stime,cutime,cstime,start_time;
struct rlimit rlim[RLIM_NLIMITS];
unsigned int flags; /* per process flags, defined below */
unsigned short used_math;
/* file system info */
int tty; /* -1 if no tty, so it must be signed */
unsigned short umask;
struct m_inode * pwd;
struct m_inode * root;
struct m_inode * executable;
struct m_inode * library;
unsigned long close_on_exec;
struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds& ss */
struct desc_struct ldt[3];
/* tss for this task */
struct tss_struct tss;
;
struct blk_dev_struct
void (*request_fn)(void);
struct request * current_request;
;

9.3.5 低级页面读写函数

linux/kernel/blk_drv/ll_rw_blk.c/ ll_rw_page()

以页面(4K)为单位访问块设备数据,每次读写8个扇区。
将请求项建立完毕后,调用add_request()把它添加到请求队列中,然后直接调用调度函数让当前进程睡眠等待页面从交换设备中读入。
struct task_struct * wait_for_request = NULL; // 用于请求数组不空闲时,进程的临时等待处 ll_rw_page
void ll_rw_page(int rw, int dev, int page, char * buffer)

struct request * req;
unsigned int major = MAJOR(dev);
// 检查参数
// 检查主设备号以及设备的请求操作函数是否存在
if (major > = NR_BLK_DEV || !(blk_dev[major].request_fn))
printk("Trying to read nonexistent block-device\\n\\r");
return;

// 检查参数命令是否是 READ 或者 WRITE
if (rw!=READ & & rw!=WRITE)
panic("Bad block dev command, must be R/W");
// 建立请求项
repeat:
req = request+NR_REQUEST; // 将指针指向队列尾部
while (--req > = request)
if (req-> dev< 0) // 表示该项空闲
break;
if (req < request)
sleep_on(& wait_for_request); // 睡眠,过会再查看请求队列
goto repeat;

// 向空闲请求项中填写请求信息, 并将其加入队列中
/* fill up the request-info, and add it to the queue */
req-> dev = dev; // 设备号
req-> cmd = rw; // 命令(READ/WRITE)
req-> errors = 0; // 读写操作错误计数
req-> sector = page< < 3; // 起始读扇区 swap_nr
req-> nr_sectors = 8; // 读写扇区数 8 块
req-> buffer = buffer; // 数据缓冲区
req-> waiting = current; // 当前进程进入该请求等待队列
req-> bh = NULL; // 无缓冲块头指针(不用高速缓冲)
req-> next = NULL; // 下一个请求项指针
current-> state = TASK_UNINTERRUPTIBLE; // 置为不可中断状态
add_request(major+blk_dev,req); // 将请求项加入队列中
schedule(); // 因为需要对交换设备读/写8个扇区,需要花较长时间,所以将当前进程进行睡眠等待

函数执行流程:
  1. 检查参数。代码8~14行,如果设备主设备号不存在或者该设备的请求操作函数不存在,显示出错信息,退出;如果参数给出的命令既不是READ或WRITE,表示内核程序出错。
  2. 建立请求项。代码16~24行,从请求数组中寻址空闲项(从后往前寻址),如果没有则睡眠等等。
  3. 向空闲请求项中填写请求信息。向请求项中填写相关信息(代码27~35行),代码第36行,把当前进程置为不可中断睡眠状态,代码第37行,将请求项加入队列中,因为需要对交换设备读/写8个扇区,需要花较长时间,所以将当前进程进行睡眠等待。
struct task_struct
/* these are hardcoded - dont touch */
long state; /* -1 unrunnable, 0 runnable, > 0 stopped */
long counter;
long priority;
long signal;
struct sigaction sigaction[32];
long blocked; /* bitmap of masked signals */
/* various fields */
int exit_code;
unsigned long start_code,end_code,end_data,brk,start_stack;
long pid,pgrp,session,leader;
int groups[NGROUPS];
/*
* pointers to parent process, youngest child, younger sibling,
* older sibling, respectively.(p-> father can be replaced with
* p-> p_pptr-> pid)
*/
struct task_struct*p_pptr, *p_cptr, *p_ysptr, *p_osptr;
unsigned short uid,euid,suid;
unsigned short gid,egid,sgid;
unsigned long timeout,alarm;
long utime,stime,cutime,cstime,start_time;
struct rlimit rlim[RLIM_NLIMITS];
unsigned int flags; /* per process flags, defined below */
unsigned short used_math;
/* file system info */
int tty; /* -1 if no tty, so it must be signed */
unsigned short umask;
struct m_inode * pwd;
struct m_inode * root;
struct m_inode * executable;
struct m_inode * library;
unsigned long close_on_exec;
struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds& ss */
struct desc_struct ldt[3];
/* tss for this task */
struct tss_struct tss;
;
void sleep_on(struct task_struct **p)

__sleep_on(p,TASK_UNINTERRUPTIBLE);

9.3.6 关于缓冲块的两个操作

linux/kernel/blk_drv/ll_rw_blk.c

关于缓冲块的两个操作分别为锁定指定缓冲块以及释放锁定的缓冲区。
// 锁定指定缓冲块
static inline void lock_buffer(struct buffer_head * bh)

cli(); // 禁止中断
while (bh-> b_lock)// 如果缓冲区已被锁定则睡眠,直到缓冲区解锁
sleep_on(& bh-> b_wait);
bh-> b_lock=1; // 立刻锁定该缓冲区
sti(); // 开启中断

// 释放(解锁)锁定的缓冲区
static inline void unlock_buffer(struct buffer_head * bh)

if (!bh-> b_lock)// 该缓冲区没有被锁定
printk("ll_rw_block.c: buffer not locked\\n\\r");
bh-> b_lock = 0; // 解锁
wake_up(& bh-> b_wait); // 唤醒等待该缓冲区的任务

9.3.7 创建请求项并插入请求队列

linux/kernel/blk_drv/ll_rw_blk.c

add_request():把已经设置好的请求项 req 添加到指定设备的请求项链表中。如果该设备的当前请求项指针为空,则可以设置 req 为当前请求项并立刻调用设备请求项处理函数,否则就把 req 请求项插入到该请求项链表中。
make_request():创建请求项。
// 向链表中加入请求项
static void add_request(struct blk_dev_struct * dev, struct request * req) // dev:指定块设备结构指针;req:已经设置好的请求项

struct request * tmp;
req-> next = NULL; // 置空请求项中的下一请求项指针
cli(); // 关中断
if (req-> bh)
req-> bh-> b_dirt = 0; // 清缓冲区"脏"标志
if (!(tmp = dev-> current_request)) // 指定设备dev当前无请求项 =0:表示该设备没有请求项 本次是第一个请求项
dev-> current_request = req; // 将块设备当前请求指针直接指向该请求项
sti(); // 开中断
(dev-> request_fn)(); // 立即执行请求函数 硬盘是 do_hd_request()
return;

for ( ; tmp-> next ; tmp=tmp-> next) // 如果当前指定设备dev忙,有空闲请求项在处理,则将当前请求项插入请求链表中 for 循环中的判断语句用于把req所指请求项与请求队列中已有的请求项作比较,找出req插入该队列的正确位置(电梯调度算法)
if (!req-> bh)
if (tmp-> next-> bh)
break; // 读页优先
else
continue;
if ((IN_ORDER(tmp,req) ||
!IN_ORDER(tmp,tmp-> next)) & &
IN_ORDER(req,tmp-> next))
break;
/*
if (tmp > req & & req > tmp-> next) break;
elif (tmp < = tmp-> next& & req > tmp-> next ) break;
else continue;
*/

req-> next=tmp-> next;
tmp-> next=req;
sti();

// 创建请求项并插入请求队列
static void make_request(int major,int rw, struct buffer_head * bh) //主设备号;指定命令;存放数据的缓冲区头指针

struct request * req;
int rw_ahead;

/* WRITEA/READA is special case - it is not really needed, so if the */
/* buffer is locked, we just forget about it, else its a normal read */
if (rw_ahead = (rw == READA || rw == WRITEA))// rw_ahead 预读写标志 68行,放弃提前命令
if (bh-> b_lock) // 指定的缓冲区正在使用,已被上锁,放弃预读写请求
return;
if (rw == READA)
rw = READ;
else
rw = WRITE;

if (rw!=READ &

    推荐阅读