Linux内核设备驱动之字符设备驱动笔记整理
/******************** *字符设备驱动 ********************/
(1)字符设备驱动介绍
字符设备是指那些按字节流访问的设备,针对字符设备的驱动称为字符设备驱动。
此类驱动适合于大多数简单的硬件设备。比如并口打印机,我们通过在/dev下建立一个设备文件(如/dev/printer)来访问它。
用户应用程序用标准的open函数打开dev/printer,然后用write向文件中写入数据,用read从里面读数据。
调用流程:
- write():用户空间-->
- sys_write():VFS-->
- f_op->write:特定设备的写方法
所谓驱动,就是提供最后的write函数,通过访问打印机硬件的寄存器直接和打印机对话
(2)主设备号和次设备号
a.设备编号介绍
对字符设备的访问是通过文件系统内的设备文件进行的。这些文件位于/dev。用"ls-l"查看。
设备通过设备号来标识。设备号分两部分,主设备号和次设备号。
通常,主设备号标示设备对应的驱动程序,linux允许多个驱动共用一个主设备号;
而次设备号用于确定设备文件所指的设备。
在内核中,用dev_t类型
2.4内核中采用16位设备号(8位主,8位从),而2.6采用32位,12位主,20位从。
在驱动中访问设备号应该用
获取设备号:
- MAJOR(dev_tdev)
- MINOR(dev_tdev)
- MKDEV(intmajor,intminor)
b.分配和释放设备编号
在建立一个字符设备前,驱动需要先获得设备编号。
分配:
#includeintregister_chrdev_region(dev_tfirst,unsignedintcount,char*name); //first:要分配的设备编号范围的起始值(次设备号常设为0) //count:所请求的连续编号范围 //name:和编号关联的设备名称(见/proc/devices)
也可以要求内核动态分配:
intalloc_chrdev_region(dev_t*dev,unsignedintfirstminor,unsignedintcount,char*name); //firstminor:通常为0 //*dev:存放内核返回的设备号
释放:
voidunregister_chrdev_region(dev_tfirst,unsignedintcount); //在模块的清除函数中调用
在Documentation/devices.txt中可以找到内核已经分配的设备号。
c.建立设备文件
当设备驱动模块向系统申请了主设备号和次设备号,并且已经通过insmod加载到内核中后,我们就可以通过在/dev下创建设备文件来访问这个设备了。
字符设备的创建:$>mknod/dev/mycharcmajorminor
我们在驱动中常常采用动态分配主次设备号的方法,这样不会和系统中已有的设备号冲突。
动态分配时,/dev下的设备文件也需要通过分析/proc/devices动态建立。
见char_load和char_unload脚本。
(3)字符设备的基本数据结构
和字符设备驱动关系最紧密的3个基本的数据结构是:file,file_oepeations和inode
a.file_operations数据结构
结构中包含了若干函数指针。这些函数就是实际和硬件打交道的函数。
用户空间调用的open,write等函数最终会调用这里面的指针所指向的函数。每个打开的文件和一组函数关联。
见
2.6内核结构的初始化:
structfile_operationsmy_fops={ .owner=THIS_MODULE, .llseek=my_llseek, .read=my_read, .write=my_write, .ioctl=my_ioctl, .open=my_open, .release=my_release, }
2.4内核结构的初始化:
structfile_operationsmy_fops={ owner:THIS_MODULE, llseek:my_llseek, ... }
b.file结构
file是一个内核结构体,实际上和用户open文件后返回的文件描述符fd对应。
file结构代表一个打开的文件,系统中每个打开的文件在内核空间都有一个对应的file结构。
它由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数,在文件的所有实例都被关闭后,内核会释放这个结构。
用户空间进程fork一个新进程后,新老进程会共享打开的文件描述符fd,这个操作不会在内核空间创建新的file结构,只会增加已创建file结构的计数。
见
mode_tf_mode; 通过FMODE_READ和FMODE_WRITE标示文件是否可读或可写。
loff_tf_pos; 当前的读写位置,loff_t为64位
unsignedintf_flags; 文件标志,如O_RDONLY,O_NONBLOCK,O_SYNC。标志都定义在
structfile_operations*f_op; 与文件相关的操作。内核在执行open时对这个指针赋值。可以在驱动的open方法中根据次设备号赋予不同的f_op
void*private; 通常将表示硬件设备的结构体赋给private.
structdentry*f_dentry; 文件对应的目录项(dentry)结构。可通过filp->f_dentry->d_inode访问索引节点。
file中其他的内容和驱动关系不大。
c.inode结构
内核用inode结构表示一个实际的文件,可以是一个普通的文件,也可以是一个设备文件。
每个文件只有一个inode结构,而和文件描述符对应的file结构可以有多个(多次进行open调用)。这些file都指向同一个inode。
inode定义在
dev_ti_rdev; 对于表示设备文件的inode结构,i_rdev里包含了真正的设备编号
structcdev*i_cdev cdev是表示字符设备的内核的内部结构。当inode表示一个字符设备时,i_cdev指向内核中的structcdev.
其他结构和设备驱动关系不大。
用如下宏从inode获取设备号:
- unsignedintiminor(structinode*inode)
- unsignedintimajor(structinode*inode)
(4)字符设备的注册
内核内部使用structcdev结构来表示一个字符设备。
我们的驱动要把自己的cdev注册到内核中去。见
a.通常在设备的结构中加入cdev
structscull_dev{ ... structcdevcdev;/*字符设备结构*/ }
b.初始化
voidcdev_init(structcdev*cdev,structfile_operations*fops)
c.设定cdev中的内容
- dev->cdev.owner=THIS_MODULE;
- dev->cdev.ops=&scull_fops;
d.向内核添加设定好的cdev
intcdev_add(structcdev*dev,dev_tnum,unsignedintcount); //num:设备对应的第一个编号 //count:和设备关联的设备编号的数量,常取1 //一旦cdev_add返回,内核就认为设备可以使用了,所以要在调用之前完成设备的硬件初始化。
(5)老式的注册函数
2.4中的老式注册函数仍然在驱动函数中大量存在,但新的代码不应该使用这些代码。
注册:
intregister_chrdev(unsignedintmajor, constchar*name, structfile_operations*fops); //为给定的主设备号注册0~255作为次设备号,并为每个设备建立一个对应的默认cdev结构
注销:
intunregister_chrdev(unsignedintmajor, constchar*name);
(6)open和release
a.open
在驱动的open方法中完成设备的初始化工作,open完成后,硬件就可以使用,用户程序可以通过write等访问设备,open的工作有:
- *检查设备的特定错误
- *如果设备首次打开,则对其进行初始化(有可能多次调用open)
- *如有必要,更新f_op指针
- *分配并填写置于filp->private_data中的数据
open原型;
int(*open)(structinode*inode,structfile*filp); //在open中通过inode获得dev指针,并将其赋给file->private_data //structscull_dev*dev; //dev=contain_of(inode->i_cdev,structscull_dev,cdev); //filp->private_data=dev; //(如果dev是静态分配的,则在open或write等方法中可以直接访问dev,但如果dev是在module_init时动态分配的,则只能通过上面的方法获得其指针)
b.release
并不是每个close调用都会引起对release方法的调用,只有当file的计数器归零时,才会调用release,从而释放dev结构)
(7)read和write
read和write的工作是从用户空间拷贝数据到内核,或是将内核数据拷贝到用户空间。其原型为:
ssize_tread(structfile*filp,char__user*buff,size_tcount,loff_t*offp); ssize_twrite(structfile*filp,constchar__user*buff,size_tcount,loff_t*offp); //buff:用户空间的缓冲区指针 //offp:用户在文件中进行存取操作的位置 //在read和write中,拷贝完数据后,应该更新offp,并将实际完成的拷贝字节数返回。
(8)和用户空间交换数据
read和write中的__user*buff是用户空间的指针,内核不能直接引用其中的内容(也就是不能直接对buff进行取值操作),需要通过内核提供的函数进行数据拷贝。其原因是:
- a.在不同架构下,在内核模式中运行时,用户空间的指针可能是无效的。
- b.用户空间的内存是分页的,系统调用执行时,buff指向的内存可能根本不在RAM中(被交换到磁盘中了)
- c.这可能是个无效或者恶意指针(比如指向内核空间)
内核和用户空间交换数据的函数见
如:
1.unsignedlongcopy_to_user(
void__user*to,
constvoid*from,
unsignedlongcount);
//向用户空间拷贝数据2.unsignedlongcopy_from_user(
void*to,
constvoid__user*from,
unsignedlongcount);
//从用户空间获得数据3.intput_user(datum,ptr)
//向用户空间拷贝数据。字节数由sizeof(*ptr)决定
//返回值为0成功,为负错误。4.intget_user(local,ptr);
//从用户空间获得数据。字节数由sizeof(*ptr)决定
//返回值和local都是从用户空间获得的数据
任何访问用户空间的函数都必须是可睡眠的,这些函数需要可重入。
copy_to_user等函数如果返回值不等于0,则read或write应向用户空间返回-EFAULT
主设备号用来表示设备驱动,次设备号表示使用该驱动的设备
在内核dev_t表示设备号,设备号由主设备号和次设备号组成
#include#defineMAJOR(dev)((unsignedint)((dev)>>MINORBITS))//根据设备号获取主设备号 #defineMINOR(dev)((unsignedint)((dev)&MINORMASK))//获取次设备号 #defineMKDEV(ma,mi)(((ma)< //静态:申请指定的设备号,from指设备号,count指使用该驱动有多少个设备(次设备号),设备名 intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name); //name的长度不能超过64字节 //动态申请设备号,由内核分配没有使用的主设备号,分配好的设备存在dev,baseminor指次设备号从多少开始,count指设备数,name设备名 intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount, constchar*name) //释放设备号,from指设备号,count指设备数 voidunregister_chrdev_region(dev_tfrom,unsignedcount) //cat/proc/devices可查看设备使用情况 在内核源码的documentations/devices.txt可查看设备号的静态分配情况 ///内核里使用structcdev来描述一个字符设备驱动 #include structcdev{ structkobjectkobj;//内核用于管理字符设备驱动 structmodule*owner;//通常设为THIS_MODULE,用于防止驱动在使用中时卸载驱动模块 conststructfile_operations*ops;//怎样操作(vfs) structlist_headlist;//因多个设备可以使用同一个驱动,用链表来记录 dev_tdev;//设备号 unsignedintcount;//设备数 };
////////字符设备驱动//////////
1.申请设备号
2.定义一个cdev的设备驱动对象
structcdevmycdev; //定义一个file_operations的文件操作对象 structfile_operationsfops={ .owner=THIS_MODULE, .read=读函数 .... };
3.把fops对象与mycdev关联起来
cdev_init(&mycdev,&fops);//mycdev.ops=&fops; mycdev.owner=THIS_MODULE;
4.把设备驱动加入内核里,并指定该驱动对应的设备号
cdev_add(&mycdev,设备号,次设备号的个数);
5.卸载模块时,要把设备驱动从内核里移除,并把设备号反注册
cdev_del(&mycdev); ///////////创建设备文件 mknod/dev/设备文件名c主设备号次设备号 ////////inode节点对象描述一个文件/设备文件,包括权限,设备号等信息 structinode{ ... dev_ti_rdev;//设备文件对应的设备号 structcdev*i_cdev;//指向对应的设备驱动对象的地址 ... }; ////file对象描述文件描述符,在文件打开时创建,关闭时销毁 structfile{ ... conststructfile_operations*f_op;//对应的文件操作对象的地址 unsignedintf_flags;//文件打开的标志 fmode_tf_mode;//权限 loff_tf_pos;//文件描述符的偏移 structfown_structf_owner;//属于哪个进程 unsignedintf_uid,f_gid; void*private_data;//给驱动程序员使用 ... };
通file里的成员f_path.dentry->d_inode->i_rdev可以获取到设备文件的设备号
///错误码在
/////////structfile_operations////
inode表示应用程序打开的文件的节点对象, file表示打开文件获取到的文件描述符
成功返回0,失败返回错误码
int(*open)(structinode*,structfile*);
buf指向用户进程里的缓冲区,len表示buf的大小(由用户调用read时传进来的)
off表示fl文件描述符的操作偏移,返回值为实际给用户的数据字节数.
ssize_t(*read)(structfile*fl,char__user*buf,size_tlen,loff_t*off);
用户进程把数据给驱动
ssize_t(*write)(structfile*,constchar__user*,size_t,loff_t*);
to指用户进程的缓冲区,from指驱动里装数据的缓冲区,n多少字节,返回值是0
externinlinelongcopy_to_user(void__user*to,constvoid*from,longn)
to指驱动的... from用户... n多少字节,....
staticinlineunsignedlong__must_checkcopy_to_user(void__user*to,const void*from,unsignedlongn) { if(access_ok(VERIFY_WRITE,to,n)) n=__copy_to_user(to,from,n); returnn;//返回值为剩下多少字节没拷贝 }
externinlinelongcopy_from_user(void*to,constvoid__user*from,longn)
- 如果与用户进程交互的数据是1,2,4,8字节的话,可用put_user(x,p)//x为值,p为地址
- 如果从用户进程获取1,2,4字节的话,可用get_user(x,p)
/////////// ///动态申请内存,并清零.size为申请多大(不要超过128K), //flags为标志(常为GFP_KERNEL).成功返回地址,失败返回NULL //GFP_ATOMIC,使用系统的内存紧急池 void*kmalloc(size_tsize,gfp_tflags);//申请后要内存要清零 void*kzalloc(size_tsize,gfp_tflags);//申请出来的内存已清零 voidkfree(constvoid*objp);//回收kmalloc/kzalloc的内存 void*vmalloc(unsignedlongsize);//申请大内存空间 voidvfree(constvoid*addr);//回收vmalloc的内存 //kmalloc申请出来的内存是物理地址连续的,vmalloc不一定是连续的 /////container_of(ptr,type,member)type包括member成员的结构体, //ptr是type类型结构体的member成员的地址. //此宏根据结构体成员的地址获取结构体变量的首地址 #definecontainer_of(ptr,type,member)({\ consttypeof(((type*)0)->member)*__mptr=(ptr);\ (type*)((char*)__mptr-offsetof(type,member));}) #defineoffsetof(TYPE,MEMBER)((size_t)&((TYPE*)0)->MEMBER) 15typedefstructled_dev_t{ 16dev_tmydevid; 17unsignedint*rLEDCON; 18unsignedint*rLEDDAT; 19structcdevmycdev; 20}LED_DEV; LED_DEVmyled; //ind->i_cdev是指向myled.mycdev成员的地址 //结构体变量myled首地址可由container_of(ind->i_cdev,LED_DEV,mycdev)获取;
///////自动创建设备文件////
#include
1.
structclass*cl; cl=class_create(owner,name);//owner指属于哪个模块,name类名 //创建出来后可以查看/sys/class/类名 voidclass_destroy(structclass*cls);//用于销毁创建出来的类
2.创建设备文件
structdevice*device_create(structclass*cls,structdevice*parent, dev_tdevt,void*drvdata, constchar*fmt,...) __attribute__((format(printf,5,6))); device_create(所属的类,NULL,设备号,NULL,"mydev%d",88);//在/dev/目录下产生名字为mydev88的设备文件 voiddevice_destroy(structclass*cls,dev_tdevt);//用于销毁创建出来的设备文件 //////// intregister_chrdev(unsignedintmajor,constchar*name, conststructfile_operations*fops);//注册设备号并创建驱动对象 voidunregister_chrdev(unsignedintmajor,constchar*name);//反注册设备号并删除驱动对象 staticinlineintregister_chrdev(unsignedintmajor,constchar*name, conststructfile_operations*fops) { return__register_chrdev(major,0,256,name,fops); } int__register_chrdev(unsignedintmajor,unsignedintbaseminor, unsignedintcount,constchar*name, conststructfile_operations*fops) { structchar_device_struct*cd; structcdev*cdev; interr=-ENOMEM; cd=__register_chrdev_region(major,baseminor,count,name); if(IS_ERR(cd)) returnPTR_ERR(cd); cdev=cdev_alloc(); if(!cdev) gotoout2; cdev->owner=fops->owner; cdev->ops=fops; kobject_set_name(&cdev->kobj,"%s",name); err=cdev_add(cdev,MKDEV(cd->major,baseminor),count); if(err) gotoout; cd->cdev=cdev; returnmajor?0:cd->major; out: kobject_put(&cdev->kobj); out2: kfree(__unregister_chrdev_region(cd->major,baseminor,count)); returnerr; }
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对毛票票的支持。如果你想了解更多相关内容请查看下面相关链接
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。