Linux下第一个驱动程序
因为在Ubuntu环境下写的文章和做的实验,没有安装linux下比较好用的截图工具,所以没有附带太多截屏,还望海涵,不过该描述的都到位了。
曾经还一直处于应用程序开发的我,以为驱动开发者是那么的厉害,以为只有牛人才能走到这一步,随着知识的积累,发现并非如此,驱动开发并不像想象中那么特别,俗话说术业有专攻,开发者只是使用的工具不同,且从事的领域不同,产品不同罢了。只要能作出好的产品,你就是一个”牛人”。
从这里开始进行系统化的驱动学习,主线是《Linux设备驱动开发详解》,之前大致看过这本书,起初感觉有些晦涩,但看了两本内核的书籍以后,重新回来读起来就比较顺流了,一口气读了好几章(主要是前几章是知识介绍性文章)。所以这里顺便推荐两本内核的书籍:
《Linux内核设计与实现》,我看的是第二版,写的非常棒,简单易懂,要在介绍内核原理与实现机制,广度到了,深度不够,所以最好还得配合下边这本书一块儿看。
《UnderstandingLinux kernel》——深入理解linux内核,这本书的第三版是基于2.6内核的,06年出版。我看的是英文原版的,所以看得速度比较满,不过正好和《Linux设备驱动开发详解》对接上了,刚看过《Understanding Linux kernel》中的同步异步,应该是第五章那里,然后《Linux设备驱动开发详解》就在第七章也讲到了,这样,可能那么多机制:锁,读写锁,顺序锁,信号量,读写信号量……一下出来这么多东西的话有些接受不了,但如果你之前看了《Linux内核设计与实现》后,最起码不会感觉恐慌,其实学习新的知识就是这样,一回生两回熟,再难理解的东西,功夫到了,也就理解了。
好的,开始第一个驱动程序的学习,实例来自 Linux设备驱动开发详解,这里是创建了一个虚拟的字符设备globalmem,也就是一片内核空间的内存区域,来实现内核空间和用户空间的信息传递。(确实,我举不出来比这更好的例子来作第一个例子了,不过请相信,哪怕就是这个例子也是我一个字母一个字母敲出来的,并未直接取材自隋书的源码,主要是看我的注释,和我遇到的问题以及解决它的全过程,成功者找方法,失败者找借口!呵呵)
不废话,上代码,别心急,看注释。。。
下边是驱动的源码,附上了详尽的注释:
globalmem.c
#include <linux/module.h> #include <linux/fs.h> #include <linux/types.h> #include <linux/errno.h> #include <linux/sched.h> #include <linux/init.h> #include <linux/cdev.h> #include <asm/io.h> #include <asm/system.h> #include <asm/uaccess.h> #define GLOBALMEM_SIZE 0x1000 /*4k的空间*/ #define MEM_CLEAR 0x1 /*清空全局内存*/ #define GLOBALMEM_MAJOR 250 /*预设的主设备号*/ static int globalmem_major = GLOBALMEM_MAJOR; /*用面向对象思想对cdev重新封装,以便于方便我们的操作*/ struct globalmem_dev{ struct cdev cdev; unsigned char mem[GLOBALMEM_SIZE]; }; struct globalmem_dev *globalmem_devp; /*声明一个全局的设备结构体*/ /*用来注册到file_operations结构中open */ int globalmem_open(struct inode *inode,struct file *filp) { filp->private_data = globalmem_devp; /*当有多个同类设备时,用私有变量访问很有必要也很方便*/ return 0; } /*用来注册到file_operations结构中release */ int globalmem_release(struct inode *inode,struct file *filp) { return 0; } /*用来注册到file_operations结构中ioctl */ static int globalmem_ioctl(struct inode *inodep,struct file *filp,unsigned int cmd,unsigned long arg) { struct globalmem_dev *dev = filp->private_data; /*从私有数据获取设备结构体指针*/ switch (cmd){ case MEM_CLEAR: memset(dev->mem,0,GLOBALMEM_SIZE); printk(KERN_INFO"globalmem is set to zero\n"); break; default: return -EINVAL; } return 0; } /*用来注册到file_operations结构中read */ static ssize_t globalmem_read(struct file *filp,char __user *buf,size_t size,loff_t *ppos) { unsigned long p = *ppos; /*获取到当前全局内存的*/ unsigned int count = size; /*获取到要读取数据的大小*/ int ret = 0; /*用来记录返回值*/ struct globalmem_dev *dev = filp->private_data; /*从私有变量获取到设备结构体指针*/ /*分析和获取有效的读长度,就是看给的要读取的长度是否合法*/ if(p>=GLOBALMEM_SIZE) return 0; if(count>GLOBALMEM_SIZE-p) /*如果要读取的量比剩余的还多,只给它可读到的量*/ count=GLOBALMEM_SIZE-p; /*一切就绪后就开始往用户空间读了,这里的读指的是用户空间的读,就是说从内核拷贝到用户空间, *因为内核是万能的有着所有的权限,主动权在于它,并不是说用户想读就可以读的,因为该空间是在内核状态下分配出来的 */ if(copy_to_user(buf,(void*)(dev->mem+p),count)) /*从mem的偏移量p处拷贝count个数据到buf所指区*/ ret = -EFAULT; /*拷贝失败,返回-EFAULT*/ else{ /*拷贝成功,返回重新拷贝的数据量并重新计算的偏移量*/ *ppos+=count; ret = count; printk(KERN_INFO "read %u bytes(s) from %lu\n",count,p); } return ret; } /*用来注册到file_operations结构中write */ static ssize_t globalmem_write(struct file *filp,char __user *buf,size_t size,loff_t *ppos) { unsigned long p = *ppos; /*获取到当前全局内存的*/ unsigned int count = size; /*获取到要读取数据的大小*/ int ret = 0; /*用来记录返回值*/ struct globalmem_dev *dev = filp->private_data; /*从私有变量获取到设备结构体指针*/ /*分析和获取有效的写长度,就是看给的要写的长度是否合法*/ if(p>=GLOBALMEM_SIZE) return 0; if(count>GLOBALMEM_SIZE-p) /*如果要读取的量比剩余的还多,只给它可读到的量*/ count=GLOBALMEM_SIZE-p; /*一切就绪后就开始往共享空间里写了,这里的写指的就是说从用户空间拷贝到内核, *因为内核是万能的有着所有的权限,主动权在于它,并不是说用户想写就可以写的,因为该空间是在内核状态下分配出来的 */ if(copy_from_user(dev->mem+p,buf,count)) /*往mem的偏移量p处拷贝count个数据(从buf所指区)*/ ret = -EFAULT; /*拷贝失败,返回-EFAULT*/ else{ /*拷贝成功,返回重新拷贝的数据量并重新计算的偏移量*/ *ppos+=count; ret = count; printk(KERN_INFO "read %u bytes(s) from %lu\n",count,p); } return ret; } /*seek文件定位函数*/ static loff_t globalmem_llseek(struct file *filp,loff_t offset,int orig) { loff_t ret = 0; switch (orig){ case 0: /*相对于文件开始位置*/ if(offset<0){ ret = -EINVAL; break; } if((unsigned int)offset>GLOBALMEM_SIZE){ ret = -EINVAL; break; } filp->f_pos=(unsigned int)offset; ret=filp->f_pos; break; case 1: /*相对于文件当前位置*/ if((filp->f_pos+offset)>GLOBALMEM_SIZE){ ret = -EINVAL; break; } if((filp->f_pos+offset)<0){ ret = -EINVAL; break; } filp->f_pos+=offset; ret=filp->f_pos; break; default: ret = -EINVAL; break; } return ret; } /*文件操作结构体*/ static const struct file_operations globalmem_fops={ .owner = THIS_MODULE, .llseek = globalmem_llseek, .read = globalmem_read, .write = globalmem_write, .ioctl = globalmem_ioctl, .open = globalmem_open, .release = globalmem_release, }; /*对设备进行初始化的函数*/ static void globalmem_setup_cdev(struct globalmem_dev *dev,int index) { int err,devno = MKDEV(globalmem_major,index); /*MKDEV宏来生成设备号主设备号占12位,从设备号占20位*/ cdev_init(&dev->cdev,&globalmem_fops); dev->cdev.owner = THIS_MODULE; err = cdev_add(&dev->cdev,devno,1); if(err) printk(KERN_NOTICE "ERROR %d adding globalmem %d",err,index); } /**************************模块相关函数******************************/ /*为了便于开发和派错,建议上来先定义这些模块相关的初始化和卸载函数,你完全可以 *只做一个空的函数实现,意在每写一个功能函数就编译(make)一次,这样会很有利于开发的 */ /*驱动加载函数*/ int globalmem_init(void) { int result; dev_t devno =MKDEV(globalmem_major,0); /*申请设备号*/ if(globalmem_major) result = register_chrdev_region(devno,1,"globalmem"); else{ /*动态申请*/ result = alloc_chrdev_region(&devno,0,1,"globalmem"); globalmem_major = MAJOR(devno); } if(result<0) return result; /*动态申请设备结构体用到的全局共享内存*/ globalmem_devp = kmalloc(sizeof(struct globalmem_dev),GFP_KERNEL); if(!globalmem_devp){ result = -ENOMEM; goto fail_malloc; } memset(globalmem_devp,0,sizeof(struct globalmem_dev)); globalmem_setup_cdev(globalmem_devp,0); return 0; fail_malloc: /*分配失败后要还原现场,把已经注册的设备给释放掉*/ unregister_chrdev_region(devno,1); /*释放设备号*/ return result; } /*驱动卸载函数*/ void globalmem_exit(void) { cdev_del(&globalmem_devp->cdev); /*注销掉设备*/ kfree(globalmem_devp); /*释放掉分配的内存,好借好还再借不难*/ unregister_chrdev_region(MKDEV(globalmem_major,0),1); /*释放设备号*/ } MODULE_AUTHOR("Jun < [email protected] >"); MODULE_LICENSE("DUAL BSD/GPL"); module_init(globalmem_init); module_exit(globalmem_exit);
Makefile的写法和前面一篇文章的helloworld的模块的Makefile写法一致,换个模块的名字而已,这里是:
obj-m += globalmem.o # XXX.o对应于你的XXX.c同时也是你的模块名称 all: make -C /usr/src/linux-headers-2.6.32-27-generic M=$(shell pwd) modules # 这里通过uname -r命令获取系统信息,同时拼装出内核源码树的路径; # pwd获取当前文件夹,这就要求着在你进行make的时候要在源码目录下。 clean: make -C /usr/src/linux-headers-2.6.32-27-generic M=$(shell pwd) clean # 原理同上
准备好后开始进行编译,就是make一下就OK ;
make时又遇到了这样的问题:
make:Nothing to be done for 'all'.
Makeclearn 时也会出现:
make:Nothing to be done for 'clearn'.
网上说的都是一编译好了,只是没有修改源文件,所以没有进行再编译(我知道这也是make存在的理由,它的一方面功能就是这样的,避免重复编译未修改的文件),但显示是它确实不存在这个问题,我小纠结了一下,并且也没有其他任何编译时的报错提示。为了做测试,我又试着编译之前的helloworld的模块,终于报错了,哈哈哈。问题在于,make中的make -C /usr/src/linux-headers-$(shell uname -r) M=$(shell pwd)modules ,该命令是动态的通过shell命令的uname来获取当前内核源码路径,并进行连接编译驱动的。而我下载并构建的内核是:2.6.32-30-generic,而此时系统装载的是(也就是uname -r命令得到的内核版本):2.6.30-27-generic,而该版本的内核源码树我已经手动删除,所以我的源码路径和通过uname -r组装出来的是不一致的,所以我把Makefile中的命令手动修改成了:
make -C /usr/src/linux-headers-2.6.32-30-generic M=$(shell pwd) modules ,直接指定路径(这样的可维护性下降了,不过我们的工程太小,可以忽略这个问题)。OK ,try again,make 通过了,目录下编译出来了那群你梦寐以求想看到的文件们,他们这时显得是如此的可爱。
这里还要说的是,模块只是一种把自己的代码动态加载到内核的手段,这也就说明了为什么我的驱动模块的Makefile文件为何和helloworld模块的Makefile文件是一模一样的(确实,模块的名字是不一样的,你知道我是什么意思),所以对于驱动来说,模块就是一条小船,它把代码运载到了kernel所在的海域,作为一个载体把驱动的代码带到了内核空间,从而使得你在用户空间调用某些系统操作时,OS可以找到对应的代码来完成你的请求。
好的,编译好了,就该装载并测试了。
$ insmod globalmem.ko #“Ubuntu下必要时记着加sudo,因为我已经开启了ubuntu的su”
呵呵,又来错误了。看来又要成长了,要善待你碰到的没一个失败,毕竟她是成功他妈。
装载时报错了:
insmod:error inserting 'globalmem.ko': -1 Invalid module format
还是因为我系统现装载的内核和对模块进行编译的内核版本不一致造成的,所以,我得把在构建的内核源码树中版本较新的内核安装到系统中去才行。这里安装新内核的方法,可以借鉴这篇博文(哈哈,我越来越爱西邮的学生了,如果你想考研或者招聘,西邮出来的做linux的都是非常棒的。额,作广告了,呵呵):http://edsionte.com/techblog/archives/3289/comment-page-1#comment-2350
一切就绪,有装载过对应版本的内核后,重启一下,进入对应的内核版本中去。重新insert our module ,try again。
$ insmod globalmem.ko
welldone,we've already made it.好的,接下来在用户空间测试一下。
驱动是针对设备而言的(虽然这里的设备并非是实实在在的,只是一片内存),而linux下的设备又都是抽象成文件来看待的(毕竟unix最早是从一个文件系统演变过来的,也就是这一壮举,使得驱动的开发容易多了,统一的抽象带来了非常大的方便)。所以我们要把这个设备文件创建出来。
$ mknod /dev/globalmem c 250 0 #创建一个主设备号为250次设备号是0的字符设备文件globalmem到/dev下。
然后就可以开始测试了:
$ echo“Hello jun” > /dev/globalmem #写“hello jun”到设备
提示:
bash:/dev/globalmem: Permission denied
权限不够,ls -l 发现:
crw-r--r--1 root root 250, 0 Oct 20 15:47 /dev/globalmem
你要么用sudo来echo,要么把文件权限该一下,这个自由留给你了。呵呵
$ cat /dev/globalmem #显示设备文件的内容
显示如下:
root@jun-desktop:/home/jun/driver/ch6#cat /dev/globalmem
hellojun
好的,大功告成!!!
后记说明:
1、做驱动的话,建议可以在原生的linux环境下,其实也挺方便的。
2、推荐一个c/c++的IDE吧——Code:Blocks ,挺好用的集成开发环境,只是第一次用它就喜欢上了,不过和SCIM输入法稍有冲突,注释的时候要输入中文,如果中文文字删减时,会出现打不上中文的情况,要调成英文状态再调回中文状态才能继续。
另外,它带有语句联想功能,当然是只支持用户空间c函数库德联想,不支持kernel中的函数或结构。个人感觉要比Vi用着来的有效率些,呵呵
3、 对于echo 是“!”的问题。截屏中可以看到,要输出!还要进行转义字符转义。
4、从文章可以看出,作者我确实够笨,每次记录都会碰见如此多的看似比较弱智的问题,好在找到了解决办法,详尽写实的记录比较符合本人博客的特色。
5、编写和编译该驱动的环境并非前边文章介绍的虚拟机环境,是本机硬盘上的Ubuntu10.04,在我机器上有些时日了,忘了内核版本才凸显了上边遇到的很多问题,unlikely(你的开发过程中不会遇到类似的装载内核和编译内核不协调的情况);不过你可能会在其他地方遇到,遇到是千万别说没见过而束手无策。
6、提前把第一个驱动实验的记录过程发blog了,驱动相关的知识还没有内容介绍,可能和前边知识不太衔接,得花谢时间尽快补上。不过希望读者和我同步一起不断积累内核体系的相关知识,非常方便理解的。
。
。