您的位置:首页 > 运维架构 > Linux

LINUX块设备驱动1

2010-07-01 10:20 190 查看
第1章

+---------------------------------------------------+

|
写一个块设备驱动 |

+---------------------------------------------------+

|
作者:赵磊 |

|
网名:OstrichFly、飞翔的鸵鸟 |

| email:
zhaoleidd@hotmail.com |

+---------------------------------------------------+

|
文章版权归原作者所有。 |

|
大家可以自由转载这篇文章,但原版权信息必须保留。 |

| 如需用于商业用途,请务必与原作者联系,若因未取得 |

|
授权而收起的版权争议,由侵权者自行负责。 |

+---------------------------------------------------+


样是读书,读小说可以行云流水,读完后心情舒畅,意犹未尽;读电脑书却举步艰难,读完后目光呆滞,也是意犹未尽,只不过未尽的是痛苦的回忆。

研究
证明,痛苦的记忆比快乐的更难忘记,因此电脑书中的内容比小说记得持久。

而这套教程的目的是要打破这种状况,以至于读者在忘记小说内容忘记本文。


这套教程中,我们通过写一个建立在内存中的块设备驱动,来学习linux内核和相关设备驱动知识。

选择写块设备驱动的原因是:

1:容易上


2:可以牵连出更多的内核知识

3:像本文这样的块设备驱动教程不多,所以需要一个

好吧,扯淡到此结束,我们开始写了。


章的目的用尽可能最简单的方法写出一个能用的块设备驱动。

所谓的能用,是指我们可以对这个驱动生成的块设备进行mkfs,mount和读写文件。


了尽可能简单,这个驱动的规模不是1000行,也不是500行,而是100行以内。

这里插一句,我们不打算在这里介绍如何写模块,理由是
介绍的文章已经满天飞舞了。

如果你能看得懂、并且成功地编译、运行了这段代码,我们认为你已经达到了本教程的入学资格,

当然,如果你不幸
的卡在这段代码中,那么请等到搞定它以后再往下看:

mod.c:

#include <linux/module.h>

static
int __init init_base(void)

{

printk("----Hello.
World----/n");

return 0;

}

static void __exit
exit_base(void)

{

printk("----Bye----/n");

}

module_init(init_base);

module_exit(exit_base);

MODULE_LICENSE
("GPL");

MODULE_AUTHOR("Zhao Lei");

MODULE_DESCRIPTION("For
test");

Makefile:

obj-m := mod.o

KDIR :=
/lib/modules/$(shell uname -r)/build

PWD := $(shell pwd)

default:

$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

clean:

$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean

rm -rf Module.markers
modules.order Module.symvers

好了,这里我们假定你已经搞定上面的最简单的模块了,懂得什么是看
模块,以及简单模块的编写、编译、加载和卸载。

还有就是,什么是块设备,什么是块设备驱动,这个也请自行google吧,因为我们已经迫不及待要
写完程序下课。

为了建立一个可用的块设备,我们需要做......1件事情:

1:用add_disk()函数向系统中添加这个块
设备

添加一个全局的

static struct gendisk *simp_blkdev_disk;

然后申明模块的入口和出口:

module_init(simp_blkdev_init);

module_exit(simp_blkdev_exit);

然后在入口处添加这个设备、出口处私房这个设备:

static int __init simp_blkdev_init(void)

{

add_disk(simp_blkdev_disk);

return 0;

}

static
void __exit simp_blkdev_exit(void)

{

del_gendisk(simp_blkdev_disk);

}

当然,在添加设备之前我们需要申请这个设备的资源,这用
到了alloc_disk()函数,因此模块入口函数simp_blkdev_init(void)应该是:

static int
__init simp_blkdev_init(void)

{

simp_blkdev_disk =
alloc_disk(1);

if (!simp_blkdev_disk) {

ret = -ENOMEM;

goto err_alloc_disk;

}

add_disk(simp_blkdev_disk);

return 0;

err_alloc_disk:

return ret;

}

还有别忘了在卸载模块的代码中也加一个行清理
函数:

put_disk(simp_blkdev_disk);

还有就是,设备有关的属性也是需要设置的,因此在
alloc_disk()和add_disk()之间我们需要:

strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);

simp_blkdev_disk->major = ?1;

simp_blkdev_disk->first_minor = 0;

simp_blkdev_disk->fops = ?2;

simp_blkdev_disk->queue =
?3;

set_capacity(simp_blkdev_disk, ?4);

SIMP_BLKDEV_DISKNAME
其实是这个块设备的名称,为了绅士一些,我们把它定义成宏了:

#define SIMP_BLKDEV_DISKNAME
"simp_blkdev"

这里又引出了4个问号。(天哪,是不是有种受骗的感觉,像是陪老婆去做头发)

第1个问号:


个设备需要对应的主、从驱动号。

我们的设备当然也需要,但很明显我不是脑科医生,因此跟写linux的那帮疯子不熟,得不到预先为我保留的设
备号。

还有一种方法是使用动态分配的设备号,但在这一章中我们希望尽可能做得简单,因此也不采用这种方法。

那么我们采用的是:抢
别人的设备号。

我们手头没有AK47,因此不敢干的太轰轰烈烈,而偷偷摸摸的事情倒是可以考虑的。

柿子要捡软的捏,而我们试图找
出一个不怎么用得上的设备,然后抢他的ID。

打开linux/include/linux/major.h,把所有的设备一个个看下来,我们
觉得最胜任被抢设备号的家伙非COMPAQ_SMART2_XXX莫属。

第一因为它不强势,基本不会被用到,因此也不会造成冲突;第二因为它
有钱,从COMPAQ_SMART2_MAJOR到COMPAQ_SMART2_MAJOR7有那8个之多的设备号可以被抢,不过瘾的话还有它妹
妹:COMPAQ_CISS_MAJOR~COMPAQ_CISS_MAJOR7。

为了让抢劫显得绅士一些,我们在外面又定义一个宏:

#define
SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR

然后在?1的位置填上
SIMP_BLKDEV_DEVICEMAJOR。

第2个问号:

gendisk结构需要设置fops指针,虽然我们用不到,但该设还
是要设的。

好吧,就设个空得给它:

在全局部分添加:

struct block_device_operations
simp_blkdev_fops = {

.owner = THIS_MODULE,

};


后把?2的位置填上&simp_blkdev_fops。

第3个问号:

这个比较麻烦一些。

首先介绍请求队列的概
念。对大多数块设备来说,系统会把对块设备的访问需求用bio和bio_vec表示,然后提交给通用块层。

通用块层为了减少块设备在寻道时损
失的时间,使用I/O调度器对这些访问需求进行排序,以尽可能提高块设备效率。

关于I/O调度器在本章中不打算进行深入的讲解,但我们必须知
道的是:

1:I/O调度器把排序后的访问需求通过request_queue结构传递给块设备驱动程序处理

2:我们的驱动程序需
要设置一个request_queue结构

申请request_queue结构的函数是blk_init_queue(),而调用
blk_init_queue()函数时需要传入一个函数的地址,这个函数担负着处理对块设备数据的请求。

因此我们需要做的就是:

1:
实现一个static void simp_blkdev_do_request(struct request_queue *q)函数。

2:
加入一个全局变量,指向块设备需要的请求队列:

static struct request_queue
*simp_blkdev_queue;

3:在加载模块时用simp_blkdev_do_request()函数的地址作参数调用
blk_init_queue()初始化一个请求队列:

simp_blkdev_queue =
blk_init_queue(simp_blkdev_do_request, NULL);

if
(!simp_blkdev_queue) {

ret = -ENOMEM;

goto err_init_queue;

}

4:卸载模块时把simp_blkdev_queue还回去:

blk_cleanup_queue(simp_blkdev_queue);

5:在?3的位置填上
simp_blkdev_queue。

第4个问号:

这个还好,比前面的简单多了,这里需要设置块设备的大小。

块设备的大
小使用扇区作为单位设置,而扇区的大小默认是512字节。

当然,在把字节为单位的大小转换为以扇区为单位时,我们需要除以512,或者右移9
位可能更快一些。

同样,我们试图把这一步也做得绅士一些,因此使用宏定义了块设备的大小,目前我们定为16M:

#define
SIMP_BLKDEV_BYTES (16*1024*1024)

然后在?4的位置填上
SIMP_BLKDEV_BYTES>>9。

看到这里,是不是有种身陷茫茫大海的无助感?并且一波未平,一波又起,在搞定这
4个问号的同时,居然又引入了simp_blkdev_do_request函数!

当然,如果在身陷茫茫波涛中时你认为到处都是海,因此绝望,那
么恭喜你可以不必挨到65岁再退休;

反之,如果你认为到处都是没有三聚氰胺鲜鱼,并且随便哪个方向都是岸时,那么也恭喜你,你可以活着回来继续享
受身为纳税人的荣誉。

为了理清思路,我们把目前为止涉及到的代码整理出来:

#define
SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR

#define
SIMP_BLKDEV_DISKNAME "simp_blkdev"

#define SIMP_BLKDEV_BYTES
(16*1024*1024)

static struct request_queue
*simp_blkdev_queue;

static struct gendisk *simp_blkdev_disk;

static
void simp_blkdev_do_request(struct request_queue *q);

struct
block_device_operations simp_blkdev_fops = {

.owner
= THIS_MODULE,

};

static int __init
simp_blkdev_init(void)

{

int ret;

simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);

if (!simp_blkdev_queue) {

ret = -ENOMEM;

goto err_init_queue;

}

simp_blkdev_disk = alloc_disk(1);

if (!simp_blkdev_disk) {

ret = -ENOMEM;

goto err_alloc_disk;

}

strcpy(simp_blkdev_disk->disk_name,
SIMP_BLKDEV_DISKNAME);

simp_blkdev_disk->major =
SIMP_BLKDEV_DEVICEMAJOR;

simp_blkdev_disk->first_minor =
0;

simp_blkdev_disk->fops = &simp_blkdev_fops;

simp_blkdev_disk->queue = simp_blkdev_queue;

set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);

add_disk(simp_blkdev_disk);

return 0;

err_alloc_disk:

blk_cleanup_queue(simp_blkdev_queue);

err_init_queue:

return ret;

}

static void __exit simp_blkdev_exit(void)

{

del_gendisk(simp_blkdev_disk);

put_disk(simp_blkdev_disk);

blk_cleanup_queue(simp_blkdev_queue);

}

module_init(simp_blkdev_init);

module_exit(simp_blkdev_exit);


下部分的不多了,真的不多了。请相信我,因为我不在质监局上班。

我写的文章诚实可靠,并且不拿你纳税的钱。

我们还有一个最重要的
函数需要实现,就是负责处理块设备请求的simp_blkdev_do_request()。

首先我们看看究竟把块设备的数据以什么方式
放在内存中。

毕竟这是在第1章,因此我们将使用最simple的方式实现,也就是,数组。

我们在全局代码中定义:

unsigned
char simp_blkdev_data[SIMP_BLKDEV_BYTES];

对驱动程序来说,这个数组看起来大了一些,如果不幸被懂
行的人看到,将100%遭到最无情、最严重的鄙视。

而我们却从极少数公仆那里学到了最有效的应对之策,那就是:无视他,然后把他定为成“不明真相
的群众”。

然后我们着手实现simp_blkdev_do_request。

这里介绍elv_next_request()函
数,原型是:

struct request *elv_next_request(struct request_queue *q);


来从一个请求队列中拿出一条请求(其实严格来说,拿出的可能是请求中的一段)。

随后的处理请求本质上是根据rq_data_dir(req)返回
的该请求的方向(读/写),把块设备中的数据装入req->buffer、或是把req->buffer中的数据写入块设备。

刚才已
经提及了与request结构相关的rq_data_dir()宏和.buffer成员,其他几个相关的结构成员和函数是:

request.sector:
请求的开始磁道

request.current_nr_sectors:请求磁道数

end_request():结束一个请求,第2个参
数表示请求处理结果,成功时设定为1,失败时设置为0或者错误号。

因此我们的simp_blkdev_do_request()函数为:

static
void simp_blkdev_do_request(struct request_queue *q)

{

struct request *req;

while ((req = elv_next_request(q)) !=
NULL) {

if ((req->sector +
req->current_nr_sectors) << 9

>
SIMP_BLKDEV_BYTES) {

printk(KERN_ERR
SIMP_BLKDEV_DISKNAME

": bad request:
block=%llu, count=%u/n",

(unsigned
long long)req->sector,

req->current_nr_sectors);

end_request(req,
0);

continue;

}

switch (rq_data_dir(req)) {

case READ:

memcpy(req->buffer,

simp_blkdev_data + (req->sector << 9),

req->current_nr_sectors << 9);

end_request(req, 1);

break;

case WRITE:

memcpy(simp_blkdev_data + (req->sector << 9),

req->buffer, req->current_nr_sectors << 9);

end_request(req, 1);

break;

default:

/* No
default because rq_data_dir(req) is 1 bit */

break;

}

}

}

函数使用elv_next_request()遍
历struct request_queue *q中使用struct request
*req表示的每一段,首先判断这个请求是否超过了我们的块设备的最大容量,

然后根据请求的方向rq_data_dir(req)进行相应的请求
处理。由于我们使用的是指简单的数组,因此请求处理仅仅是2条memcpy。

memcpy中也牵涉到了扇区号到线性地址的转换操作,我想对坚持到
这里的读者来说,这个操作应该不需要进一步解释了。

编码到此结束,然后我们试试这个程序:

首先编译:

# make

make
-C /lib/modules/2.6.18-53.el5/build
SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step1 modules

make[1]:
Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'

CC
[M] /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.o

Building
modules, stage 2.

MODPOST

CC
/root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.mod.o

LD
[M] /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.ko

make[1]:
Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'

#

加载模块

#
insmod simp_blkdev.ko

#

用lsmod看看。

这里我们注意到,该模块的Used
by为0,因为它既没有被其他模块使用,也没有被mount。

# lsmod

Module
Size Used by

simp_blkdev 16784008 0

...

#

如果当前系统支
持udev,在调用add_disk()函数时即插即用机制会自动为我们在/dev/目录下建立设备文件。

设备文件的名称为我们在
gendisk.disk_name中设置的simp_blkdev,主、从设备号也是我们在程序中设定的72和0。

如果当前系统不支持
udev,那么很不幸,你需要自己用mknod /dev/simp_blkdev b 72 0来创建设备文件了。

# ls -l
/dev/simp_blkdev

brw-r----- 1 root disk 72, 0 11-10 18:13
/dev/simp_blkdev

#

在块设备中创建文件系统,这里我们创建常用的ext3。

当然,作为通用的块设备,创建其他类
型的文件系统也没问题。

# mkfs.ext3 /dev/simp_blkdev

mke2fs 1.39 (29-May-2006)

Filesystem
label=

OS type: Linux

Block size=1024 (log=0)

Fragment
size=1024 (log=0)

4096 inodes, 16384 blocks

819 blocks (5.00%)
reserved for the super user

First data block=1

Maximum filesystem
blocks=16777216

2 block groups

8192 blocks per group, 8192
fragments per group

2048 inodes per group

Superblock backups
stored on blocks:

8193

Writing inode tables: done

Creating
journal (1024 blocks): done

Writing superblocks and filesystem
accounting information: done

This filesystem will be
automatically checked every 38 mounts or

180 days, whichever comes
first. Use tune2fs -c or -i to override.

#

如果这是第一次使用,建议创建一个目录用来
mount这个设备中的文件系统。

当然,这不是必需的。如果你对mount之类的用法很熟,你完全能够自己决定在这里干什么,甚至把这个设备
mount成root。

# mkdir -p /mnt/temp1

#

把建立好文件系统的块设备mount到刚才建立的目录中

#
mount /dev/simp_blkdev /mnt/temp1

#

看看现在的mount表

# mount

...

/dev/simp_blkdev
on /mnt/temp1 type ext3 (rw)

#

看看现在的模块引用计数,从刚才的0变成1了,

原因是我们
mount了。

# lsmod

Module Size Used by

simp_blkdev
16784008 1

...

#

看看文件系统的内容,有个mkfs时自动建立的lost+found目录。

#
ls /mnt/temp1

lost+found

#

随便拷点东西进去

# cp /etc/init.d/*
/mnt/temp1

#

再看看

# ls /mnt/temp1

acpid conman
functions irqbalance mdmpd
NetworkManagerDispatcher rdisc sendmail winbind

anacron
cpuspeed gpm kdump messagebus
nfs
readahead_early setroubleshoot wpa_supplicant

apmd
crond haldaemon killall microcode_ctl nfslock
readahead_later single xfs

atd
cups halt krb524 multipathd nscd
restorecond smartd xinetd

auditd
cups-config-daemon hidd kudzu netconsole ntpd
rhnsd smb ypbind

autofs
dhcdbd ip6tables lost+found netfs
pand rpcgssd sshd
yum-updatesd

avahi-daemon dund ipmi
lvm2-monitor netplugd pcscd rpcidmapd
syslog

avahi-dnsconfd firstboot iptables mcstrans
network portmap rpcsvcgssd vmware

bluetooth
frecord irda mdmonitor
NetworkManager psacct saslauthd vncserver

#


在这个块设备的使用情况是

# df

文件系统 1K-块 已用 可用 已用% 挂载点

...

/dev/simp_blkdev
15863 1440 13604 10% /mnt/temp1

#

再全删了玩玩

# rm
-rf /mnt/temp1/*

#

看看删完了没有

# ls /mnt/temp1

#

好了,大概玩够了,我们
把文件系统umount掉

# umount /mnt/temp1

#

模块的引用计数应该还原成0了吧

# lsmod

Module
Size Used by

simp_blkdev 16784008 0

...

#


后一步,移除模块

# rmmod simp_blkdev

#

这是这部教程的第1章,不好意思的是,内容比预期还是难了一
些。

当初还有一种考虑是在本章中仅仅实现一个写了就丢的块设备驱动,也就是说,对这个块设备的操作只能到mkfs这一部,而不能继续mount,
因为刚才写的数据全被扔了。

或者更简单些,仅仅写一个hello world的模块。

但最后还是写成了现在这样没,因为我觉得拿出一个真
正可用的块设备驱动程序对读者来说更有成就感。

无论如何,本章是一个开始,而你,已经跨入了学习块设备驱动教室的大门,或者通俗来说,上
了贼船。

而在后续的章节中,我们将陆续完善对这个程序,通过追加或者强化这个程序,来学习与块设备有关、或与块设备无关但与linux有关的方方
面面。

总之,我希望通过这部教程,起码让读者学到有用的知识,或者更进一步,引导读者对linux的兴趣,甚至领悟学习一切科学所需要的钻研精
神。

作为第一章的结尾,引用我在另一篇文章中的序言:

谨以此文向读者示范什么叫做严谨的研究。

呼唤踏实的治学态度,反对
浮躁的论坛风气。

--OstrichFly

原文地址

http://linux.chinaunix.net/bbs/thread-1045283-1-1.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: