试验环境: Fedora8+ 2.6.23 内核+vim7.1+lxr Server
联系方式:4bsfreedom$gmail.com(替换 $ 为 @ )
(不知如何上传附件,相关资料包括源码可发邮件向我索取)
2008年01月28日
我的《Linux Device Driver》(简称LDD)读书笔记,这篇文章因为我贪玩,明显的迟到了,blog很久没有进行更新,表明我学习的进度不够快,看来象罗汉僧规定多少时间内出多少文章那样的方法促进自己的学习也8错。
因为现在看的是《Linux Device Driver》的英文版第三版,Sample Character Utility for Loading Localities简称就是scull,所以进度相对中文版的书是要慢很多,不过因为英语的重要性,我还是首选英文版来读。建议学弟学妹们学的话这本书 结合《Linux内核设计与实现》来看看(电子书我都有,可以像我发邮件要),这本针对的是2.6的内核,相对于第二版的针对2.4版本的内核有了很大的 变化,虽然是向下兼容的,但是想到以后2.8的内核,还是尽量的使用the new way吧.
OK,le's go.
整篇文章,最好能参照源码阅读。
书上有的我想少涉及一些,主要是补充书上看不明白的地方,或者我总结觉得不够的地方,关于很多的细节,可以去参考原书。我这里的源码并不是LDD书上原配的 源码,书上原配的源码使用者有些复杂,对于我这样的初学者很不方便理解,我把书上原配的源码进行了修理和裁减,变成了一个比较简单的框架,用于入门。还另 外从另外本书上(见Reference)弄了个比较旧的简单的源码。
驱 动程序以内核模块的方式插入内核运行,运行于内核空间,所以,他应该也算是内核编程的。前面的文章我提到过,Kernel分为五大部分,Process management,Memory management,Filesystems,Device control,Networking.很明显,我们现在处于第四部分的编程的入门:P。一般设备驱动程序有三种:char module( 字符设备驱动),block module(块设备驱动),network module(网络驱动)。字符设备常见的像键盘这类,块设备则是硬盘这类,网络常见的就是网卡了。在这里列出一个字符设备的框架,和执行的大概流程,深 的和详细的自己现在也说不清楚,不过看这本书时感觉很艰难,所以总结一下,希望对和我也一样也是初学的朋友有点儿帮助。若是helloworld那样的模 块还都没有编译通过的朋友要先搭建内核源码目录树,网上的方法很多,我没有完全的去试验,我一般就是重编一次内核,就把源码目录搭建起来了。文中的源码均 在 2.6.23 的内核下编译通过,若你从我这里拿到的源码不能通过编译,请确定你的内核版本是否相同以及是否已经配置内核目录树。
我看完书,想使用源码的时候突然有种很迷茫的感觉,代码编译成模块,insmod插入模块,然后呢,然后该怎么办?如何像文章说的那样去使用设备,观察设备?ls?cat?ls哪儿呢?所以这里我补充一下。
再次强调,请注意你的编译环境,这里是 2.6.23 的内核,LDD书上的原配的代码是2.6.10内核。而且编译模块前你必须搭建好源码目录树(直接下载源码编译一遍内核最好)。解压开scull.tar.bz2后,里面有Makefile,make编译好以后,使用mknod命令创建设备节点:
#mknod /dev/sc_dev c major minor
这里major和minor分别是你的主设备号和次设备号,sc_dev是你要创建设备节点的设备名,次设备号在代码中可以找到,主设备号要是代码中sc_dev_major=0;那么,就是动态分配,我们可以在/proc/devices这个文件中找到主设备号。
例如:
[hongmy525@lhc cdev]$ vim /proc/devices
1 Character devices:
2 1 mem
……
22 229 hvc
23 250 sc_dev
24 251 usb_endpoint
25 252 usbm on
29 Block devices:
30 1 ramdisk
31 2 fd
……
第23行动态分配的设备号250,每台机器上的可能不一样,所以在我这里上面那句创建节点的命令应该是
#mknod /dev/sc_dev c 250 0
创建好了以后,为了保持通用性,还需要修改他的权限,不然只有root用户能访问这个设备。
#chmod o+w /dev/sc_dev
这样,节点就创建好了,就可以用命令:
#ls > /dev/sc_dev
上面的命令的意思是讲ls输出的内容从定向到sc_dev这个设备中。
#cat /dev/sc_dev
从sc_dev这个设备中读取内容。
可以用这两个命令来访问节点。
在 这个脚本中,我们随机分配了主设备号,保持程序在不同的机器上的通用性。而次设备号指定位0。主设备号表示哪个设备,次设备号表示这个设备的第几个。例 如:有三个驱动程序调用skull设备,此时他们都先找到skull的主设备号,主设备号都是一样的,而一个次设备号分别对应一个驱动程序。
下面这行就是用awk获取/proc/devices文件下面的主设备号。
先匹配空格之后的设备的名字是否等于$module,然后把名字匹配上的设备的主设备号赋值给major,这样就得到了主设备号。
major=$(awk "//$2==/"$module/" {print //$1}" /proc/devices)
注意:这里脚本里面的额次设备号要和module里面——sc_dev.c里面的次设备号对应一样,不然注册的设备号和操作的设备号不一致导致操作的设备出错。
我在代码里配有两个脚本,一个是cdev_load,o用户装载,你用make编译好以后,切换成root用户执行cdev_load脚本就可以完成上面比较繁琐的工作。
另 一个脚本cdev_unload用于卸载.还配有两个程序,read和write(这两个程序的源码也在压缩包中),这两个程序分别是对设备sc_dev 进行读和写,而不是用shell来对设备进行读和写。当然,他们都是最终调用execve等其他系统调用。如果还有什么问题,欢迎发邮件或者留言在 blog上和我讨论。
我的这个字符设备的执行流程为:
获取设备号 -> 注册设备 -> 关联File operations结构 -> open(打开设备) -> write ->read -> release资源 -> close(关闭设备)
每个驱动程序都有自己的住设备号和次设备号,内核可以根据设备号或者设备名来定位设备。在linux的世界里,所有的设备都被看作文件来处理, 在今后的日子里我们将很快能感觉到这样做的好处。
要查看我们机器上的设备的设备号可以使用shell的命令:
$ls -l /dev/*
会显示:
……
crw-rw----+ 1 root root 116 , 2 01-29 13:10 timer
……
上面的116是主设备号,2就是次设备号,在/dev目录下的设备都可以查看。
内核用dev_t这样的一个类型来表示设备号,dev_t是一个32位的数,其中的12位表示主设备号,其余的表示次设备号。下面我们跟进内核代码中看看dev_t是怎么定义的:
typedef u_long dev_t;
typedef unsigned long u_long; //这里我们看到实际上dev_t就是无符号长整形
要把主设备号和次设备号转换成dev_t的函数,使用这个函数:
devnum = MKDEV(int major, int minor);
内核用这两个函数把dev_t类型的设备号分别转化为主设备号和次设备号
主设备号 = MAJOR(dev_t dev);
次设备号 = MINOR(dev_t dev);
上面这俩个宏分别是是获取主设备号和次设备好的,其实就是屏蔽高12位获取次设备好或者右移20位获取主设备号。
2.6的内核能支持255(1-255)个主设备号和255(1-255)个次设备号。
所以代示例码中我们先把主设备号赋值为零,然后若主设备号大于零,即是安静态分配的方法分配主设备号,不然,用动态分配的方法分配主设备号。
这是新特性的分配设备号的代码:
int skull_major = 0;
int skull_minor = 1;
if (skull_major){
// allocate static device number
devno = MKDEV(skull_major, skull_minor);
res = register_chrdev_region(devno, skull_count, "hello525");
} else {
// dynamic allocate device number
res = alloc_chrdev_region(&devno, skull_minor, skull_count,"hello525");
skull_major = MAJOR(devno);
}
if (res < 0){
printk(KERN_WARNING "skull: can't get makor %d/n",
skull_major);
return res;
}
文章里我始终也介绍新特性的代码为主线,在最后我再对比一下新的特性的代码和一个相对比较旧的代码,然后加以说明一下。这样应该比较便于理解。
有了设备号,我们就能用设备号通过注册函数向内核注册我们的设备。驱动向kernel注册了以后,内核就会把驱动程序和设备关联起来,以设备号为索引,这 样,操作设备文件就相当于操作实物设备。(在驱动程序中有个file operations的struct,这个结构关联对设备的各个操作,是一个很重要的结构体)
在书上说,应尽量使用新的特性,我同意这个观点,但是我个人认为,2.4版本的接口看着更直观一些,至少我看了一眼就明白了这个接口的大概意思,而2.6 的接口相对要麻烦很多。和罗汉僧探讨(luohandsome :p)时,他觉得接口应该是越来越直观,不明白为什么这次的改变变得复杂了,但是罗焱认为kernel hacker 这样做有他的道理,所以我们应该接受他。
我们这里先看看旧的接口:
res = register_chrdev(cdev_minor, cdev_name, &chr_fops);
……
cdev_major = res;
……
直接明了,拿到次设备号,设备名,函数返回主设备号。
&chr_fops是file_operations 结构地址。
static struct file_operations chr_fops = {
read: test_read,
write: test_write,
open: test_open,
release: test_release,
};
看 着函数很容易明白是函数吧这几个数据关联起来了。把设备向内核注册,内核就可以找到设备,想要操作设备,就要通过file_operations结构。这 里把结构的地址告诉内核,内核就可以轻易的找到这个结构,然后对设备实行操作。 file_operations这种定义方式是说,对这个设备执行read操作就转去执行test_read函数,对这个设备执行write操作就转去执 行test_write函数,其他同理,没有赋值的操作默认为空。
也可以赋值如以下形式:
// key data struct
static struct file_operations file_fops = {
.read = d ev_read,
.write = dev_write,
.open = dev_open,
.release = dev_release,
};
很基础的一句话,对设备的操作,首先就是要找到file_operations结构,然后通过这个结构去执行其他的操作,包括打开(open)和关闭(close)操作。上面给出了旧的接口的方式,相对直观很多了,要不咱再瞅瞅新的方式~:P
在新的接口中注册字符设备用的是cdev_*这一组函数。他们被定义再linux/cdev.h中,一般是初始化cdev_init(),挂载上内核cdev_add(),还有从内核删除cdev_del().代码中一般写一个函数来处理这个事情。这个函数如下:
static void skull_setup_cdev(struct skull_dev *dev, int index)
{
int err, devno = MKDEV(skull_major, skull_minor + index);
cdev_init(&dev->cdev, &file_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &file_fops;
err = cdev_add(&dev->cdev, devno,1);
if (err)
printk(KERN_NOTICE"Err %d adding skull", err);
}
&file_fops 就是上面给出的新的方式的 file_operations结构。这个结构我在程序里定义为全局变量。(详见示例源码)到头来,这样做的目的还是把 file_operations结构的地址告诉内核,以便我们通过内核来找到这个结构,进而能操作设备,达到控制的目的。
一 般open和close还有不少东西要做的,但是我们这里的两个是最简单的字符设备驱动框架,别不坐其他实际的操作,仅仅是一些分配内存,初始化,读和 写,释放内存,关闭,除此之外没有做什么东西,所以找到file_operations结构执行open操作时,返回0就够了。LDD上强调因为现在是 smp多处理器,随时有被打断的可能, 所以要特别注意对数据的保护。
LDD上说open操作一般要完成以下任务(摘自原书):
• Check for device-specific errors (such as device-not-ready or similar hardware problems)
• Initialize the device if it is being opened for the first time
• Update the f_op pointer, if necessary
• Allocate and fill any data structure to be put in filp->private_data
接着是Close(摘自原书):
• Deallocate anything that open allocated in filp->private_data
• Shut down the device on last close
这里我们看看open和close(release)的代码:
static ssize_t dev_open(struct inode *inode, struct file *filp)
{
// device infomation
struct skull_dev *dev;
dev = container_of(inode->i_cdev, struct skull_dev, cdev);
filp->private_data = dev;
// now trim to 0 if f_flag write only
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY){
skull_free(dev);
}
/* success */
return 0;
}
static ssize_t dev_release(struct inode *inode, struct file *filp)
{
return 0;
}
对 设备的读和写都用依赖着两个很关键的函数:copy_from_user和copy_to_user,因为我们写数据的时候是吧数据从用户空间向内核空间 写入,而读取正好相反 ,读取内核空间的数据出到用户空间。这中间实现起来要经过地址的转换,还有很多事情,所以还是直接使用内核给出的函数 copy_from_user和copy_to_user来得爽快。从这两个函数的用法,我们可以想像到read和write操作的实现,不过read和 write操作还有很多要注意的细节。
这 里我准备的两份代码中,一份(skull)是比较直观的,使用旧的办法,没有太多的错误处理,对内存也是使用直接读取,没有进行优化,但是可读性很好。第 二份代码cdev中是改写于LDD原配的源码,代码写得很健壮(罗琰说,内核代码必须写得很健壮,不然造成系统不稳定问题就大了),有很多的异常处理。 进 行读写时down_interruptible(&dev->sem)和up(&dev->sem)来加锁解锁保护,保证了 读写不会被切断。LDD的书上也反复强调,任何时刻我们必须牢记随时有被抢占的可能。把要处理的数据放在filp->private_data中处 理,这里好像是更安全的意思,这个地方我没有深究,我想以后学习的过程中会慢慢明白的。对内存的管理使用量子的管理方式,这样能拥有更高的存取效率(详细 请参见代码)。设备的每次传输以量子为单位,一个量子使用1024 * 4 byte(4k正好是1页),一个量子集有1024个量子,但是这里相应的也稍稍让代码变得复杂一点点,不过效率也是内核代码要重点注意的一部分,尤其是 在这样频繁使用的地方。
给出write的代码
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if (*f_pos > dev->size)
goto out;
if (*f_pos + count > dev->size)
count = dev->size - *f_pos;
// find listitem, qset index, and offset in the qauntum
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;
// follow the list upto the right position
dptr = skull_follow(dev, item);
if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
goto out;
// read only up to the end of this quantum
if (count > quantum - q_pos)
count = quantum - q_pos;
// copy_to_user(void __user *to, const void *from, ulong count)
if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)){
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
out:
up(&dev->sem);
return retval;
read的存取方式和write是类似的,具体的可以参见代码。
这 本经典的书我看着第三章入门感觉很吃力,就记录下来,看到网上很多配有也在看这本书,都也觉得很吃力,希望能对他人有帮助。我配的源码有skull和 cdev两个文件夹,cdev是国外的人书上配的代码我改的,skull是国内的人写的,同样是出书,但是代码质量差别很大,国外的代码的健壮,思维的严 谨,思路之流畅。我现在是第一次看这本书,看国内的好些高手都要把这本书反复的看,看透为止。这章讲述了一个最简单的驱动框架,虽然仅仅进行最简单的内存 存取操作,但是对于入门的朋友来说还是有很多不好理解的地方,里面包含了很多的操作系统的理论知识,我的OS理论水平明显地很浅,还需要不断的学习。
《Linux Device Driver》 3 rd Edition
《嵌入式Linux应用程序开发详解》华清远见 人民有电出版社
罗焱的大脑:P(可以在浙大或者水木清华bbs的kerneltech版找到luohandsame)
IBM developerWorks《Linux 同步方法剖析》
horizon-desktop sc_dev# ls
load Makefile readdev.c sc_dev.c unload writedev.c
horizon-desktop sc_dev# make
make -C /lib/modules/2.6.24-24-generic/build M=/home/horizon/laboratory/modules/sc_dev
make[1]: Entering directory `/usr/src/linux-headers-2.6.24-24-generic'
LD /home/horizon/laboratory/modules/sc_dev/built-in.o
CC [M] /home/horizon/laboratory/modules/sc_dev/sc_dev.o
/home/horizon/laboratory/modules/sc_dev/sc_dev.c: In function ‘dev_open’:
/home/horizon/laboratory/modules/sc_dev/sc_dev.c:218: warning: ISO C90 forbids mixed declarations and code
Building modules, stage 2.
MODPOST 1 modules
CC /home/horizon/laboratory/modules/sc_dev/sc_dev.mod.o
LD [M] /home/horizon/laboratory/modules/sc_dev/sc_dev.ko
make[1]: Leaving directory `/usr/src/linux-headers-2.6.24-24-generic'
make readdev
make[1]: Entering directory `/home/horizon/laboratory/modules/sc_dev'
cc readdev.c -o readdev
make[1]: Leaving directory `/home/horizon/laboratory/modules/sc_dev'
make writedev
make[1]: Entering directory `/home/horizon/laboratory/modules/sc_dev'
cc writedev.c -o writedev
make[1]: Leaving directory `/home/horizon/laboratory/modules/sc_dev'
horizon-desktop sc_dev# ./load
horizon-desktop sc_dev# ls -l /dev/sc_dev
crw-rw-r-- 1 root root 253, 0 2009-08-28 10:36 /dev/sc_dev
horizon-desktop sc_dev# ls
built-in.o Makefile readdev sc_dev.c sc_dev.mod.c sc_dev.o writedev
load Module.symvers readdev.c sc_dev.ko sc_dev.mod.o unload writedev.c
horizon-desktop sc_dev# ./readdev
OK,open success!
readbuf is NULL! Please write at first before read
horizon-desktop sc_dev# ./writedev
open is OK
good, write success. data -> hello,525
horizon-desktop sc_dev# ./readdev
OK,open success!
read success! data -> hello,525
horizon-desktop sc_dev# ./unload