Sample Code可以在https://github.com/ykdsea/linux-ldd-samples.git 下载。
本章的目标是编写一个完整得字符设备驱动程序,不设计模块化的问题。
对字符设备的访问都是通过文件系统内的设备名称进行的,叫做特别文件或设备文件,通常放在/dev/目录中。
ls命令list出来的文件属性的第一个字符会表明设备文件的类型,如”c”就是字符设备,而”b”就是块设备。
在文件修改时间之前的最后两个数,则是相应设备的主设备号和次设备号。
如下面的设备random,是字符设备,其主设备号为1,次设备号为8。
主设备号标识了设备对应的驱动程序。这不是绝对的,现代linux内核是允许多个驱动程序共享一个主设备号的,大多数情况仍然是一个主设备号对应一个驱动程序。
次设备号由内核使用,用来确定设备文件所指的设备(这依赖于驱动程序的编写方式)。我们可以通过次设备号获得一个指向内核设备的直接指针,也可以将次设备号作为设备本地数组的索引。
内核中使用dev_t(在
MAJOR(dev_t dev)
MINOR(dev_t dev)
从主设备号,次设备号构造dev_t的方式如下:
MKDEV(int major, int minor)
在明确知道主设备号情况下,获得设备编号,使用register_chrdev_region,需包含
int register_chrdev_region(dev_t first, unsigned int count, char* name)
first主要记录主设备号,次设备号一般是0。
count是要求的连续的设备编号的个数。
name是要注册设备名称,这个名字会出现/proc/devices和sysfs中。
在不知道使用主设备号的情况下,使用动态分配设备号的函数,
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char* name)
dev用来输出动态获取的设备号。
firstminor是要申请的第一个次设备号,一般是0.
无论哪种方式申请的设备号,都使用同一个函数进行释放:
void unregister_chrdev_region(dev_t first, unsinged int count)
一部分主设备号已经固定分配给了一些常见设备。那对于编写新的驱动来说,应该使用新的未使用的设备号,或者动态分配主设备号。为了保持灵活性,分配主设备号的最佳方式是:默认采用动态分配,同时保持在加载或者编译的时候指定主设备号的方式。
在动态分配设备号后,需要mknod来创建设备文件,创建设备文件需要知道该设备的设备号,可以在/proc/devices中获得。
模块的装载卸载的脚本,可以放在linux的发行版的init脚本目录中,系统启动的时候会自动加载。
基本的驱动程序,会设计到三个重要的数据结构,分别是file_operations,file和inode,这是需要在编写程序之前先了解清楚。
前面建立的设备文件,已经和特定的设备号联系起来了,但是驱动程序的操作和设备号联系起来,file_operations结构就是建立这种联系。
每个打开的文件都会和一组文件操作函数相关联,这些函数主要是先open,read,write,close等系统调用,这组函数就是file_operations。
file_operations结构或者指向它的指针叫做fops,fops中的每个字段都是指向实现特定操作的函数,如果某些操作不支持,那么可以将该字段设置为NULL。
在file_operations声明的操作中有__user字符串,这个字符串没有实际的作用,只是用来表明对应的指针是一个用户空间地址。对于编译来说没有效果,但是外部检查软件可以用来检查对于用户空间地址的错误使用。
下面是几个特别要说明一下操作的的说明:
+ struct module *owner: 拥有该fops的模块的指针,一般情况下,初始化为THIS_MODULE。
+ int (*mmap) (struct file , struct vm_area_struct ): 用于将设备内存映射到进程地址空间。如果该操作为NULL,则调用返回-ENODEV
+ int (*flush) (struct file *, fl_owner_t id): flush操作发生在进程关闭设备文件描述符副本的时候,会调用flush操作来执行没有完成的操作。如果操作为null,该操作会忽略。每次close操作后,会调用flush。
+ int (*relase) (struct inode , struce file ): 当file结构释放的时候,该操作被调用。file结构是在所有的handle都被关闭的情况下才会调用。
struct file是设备驱动程序使用的第二个最重要的数据结构,注意file结构和libc里面的FILE结构是没有关联的。
file结构代表的是一个被打开的文件,在内核open的时候创建,被传递给所有fops操作函数。在文件的所有实例被关闭了,内核才会释放该结构。指向struct file的指针常称为file或者filp。
下面是几个file结构中重要的字段:
+ mode_t f_mode: 文件模式。通过FMODE_READ和FMODE_WRITE来标示文件是否可读或可写。
+ unsinged int f_flags: 文件标志。如O_RDONLY,O_NONBLOCK和O_SYNC。注意,检查读写是使用f_mode,而不是f_flags。
+ const struct file_operations *f_op: 在Open的时候,对该对象进行赋值。对于同样主设备号的设备,open函数中可以针对不同的次设备号来对f_op赋值,这样允许对主设备号下设备实现多种操作行为。
+ void *private_data: open操作为将该指针设为NULL,驱动程序可以将该指针用作任意目的,常用来保存驱动的私有数据,注意我们在release的时候讲该指针指向的资源释放掉。
kernel在内部使用inode来表示文件,和file的区别是:file表示的是打开的文件,而inode表明的是文件本身,所以对于一个文件来说,可以有很多的file结构,他们都指向一个唯一的文件的inode结构。
inode中包含了很多的文件信息,一般常用到下面的两个重要字段:
dev_t在kernel版本中会发生一些变化,所以内核中增加了两个新的函数用来获取主/次设备号,避免直接操作dev_t带来兼容性的问题。
static inline unsigned iminor(const struct inode *inode)
static inline unsigned imajor(const struct inode *inode)
字符设备是用struct cdev来表示,头文件是
申请cdev结构有两种方式:
方式一
struct cdev* my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
my_cdev->owner = THIS_MODULE;
方式二(cdev嵌入到其他struct中去的)
struct mydev{
/*private data*/
int pri_data_len;
void* priv_data;
struct cdev dev;
}my_cdev;
cdev_init(&my_cdev.dev, &my_fops);
注册通过下面的函数将cdev的信息告诉内核:
int cdev_add(struct cdev *, dev_t, unsigned);
dev_t的参数是该设备的第一个设备编号。
unsigned表明是该设备的设备编号的数量,一般都是1。
注意,注册的操作是可能会失败的。
移除字符设备使用下面的函数
void cdev_del(struct cdev *);
注意:当cdev_del传给cdev_del之后,cdev就不能再使用了。
//@include
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops);
major是主设备号,如果为0的时候,会申请一个未使用的主设备号。name是驱动程序的名字,也就是/proc/devices下显示的名字。
register_chrdev会为指定的主设备注册0~255作为次设备号,并为每个设备建立一个默认的cdev结构。所以使用这个接口,要能处理这256个设备的open操作,并且不能使用大于255的主设备号和次设备号。
static inline void unregister_chrdev(unsigned int major, const char *name);
major和name必须和注册的时候保持一致,否则调用会失败。
open主要要完成下面的操作:
+ 初始化设备
+ 更新f_op指针
+ 分配并填写filp->private_data指向的数据结构
+ 确认打开的设备是否是所期望的,可以从inode结构中获取次设备号进行确认。
release需要完成的操作:
+ 释放由open分配,保存在filp->private_data中的内容。
+ 在最后一次关闭操作时,关闭设备。
并不是每个close操作都会引起release操作,只有那些真正会释放设备数据结构的close操作才会。内核中对每个file结构都维护了一个计数器,当计数为0的时候,close操作才会执行release操作。
flush方法会在每次close时候都被执行。
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
read和write中的char*是要读取或者写入的buffer的指针,他们都是用户空间的指针,kernel不能直接访问,有以下几种原因:
+ 因为架构的不同,用户空间的指针可能是无效的。
+ 因为用户空间是分页的,有可能该地址的内容不在ram中,所以访问会导致页错误。
所以,为了保证安全,必须使用专用的函数来操作用户空间的指针。
static inline long copy_from_user(void *to,
const void __user * from, unsigned long n);
static inline long copy_to_user(void __user *to,
const void *from, unsigned long n);
函数看起来和memcpy类似,但是有些不同的地方:
+ 当用户空间地址未准备好,比如不在ram中,需要当前进程挂起,等待内存准备好再继续执行。
+ 函数会检查用户空间的指针的合法性;而且如果copy过程中遇到无效地址,只会复制部分数据。
对于不需要检查用户空间指针的情况,建议使用__copy_to_user/__copy_from_user。
readv和writev是向量型的read/write函数。
在2.6时,设备的fops中有readv和writev的操作实现,但是目前readv/writev已经被拿掉了,添加了aio_read/aio_write取代了,aio_read/aio_write是异步操作的接口。
read/writev会优先调用aio_read/aio_write来进行读写操作,如果没有这些接口则使用read/write接口。
在驱动准备好之后,还需要建立设备文件,有了设备文件,用户才能访问到设备。前面说到设备文件的建立是使用mknod来建立,但是设备号和设备文件名的不确定导致这种方式不方便,所以内核提供了一些额外的支持来完成设备文件的建立。
mknod [OPTION]... NAME TYPE [MAJOR MINOR]
例子:
mknod /dev/scrull0 c 229 0
建立名字为scrull0的设备文件,其对应的主设备号是229,子设备号为0,用户可以建立多个设备文件,对应一个子设备号,但是一个设备文件不能对应多个子设备。
注意,mknod不会检测你的设备号的是否存在,比如说scrull设备子设备号范围是0到20,mknod /dev/scrull99 c 229 99仍然会建立成功,只是这个设备文件不对应到设备。
udev是在用户空间工作的一个守护进程。
在早期的linux版本中,/dev下放满了很多设备文件,即使对应的设备实际不存在,而uevent解决了这个问题,udev监听uevent来识别设备的插入/移除消息来完成设备文件的添加/删除。
在android中,ueventd就是udev。
udev要配合sysfs才能正确的工作(比如uevent),所以需要创建设备class。
struct class * myClass= class_create(THIS_MODULE, "myDevClass");
struct device *myDev = device_create(&myClass, NULL, MKDEV(major,minor), NULL, "myDevice");
device_destroy(&myClass, MKDEV(major,minor), NULL);
class_destory(&myClass);