linux学习--字符设备驱动
目录
linux驱动有基本的接口进行注册和卸载,这里不再做详细说明,本文主要关注linux字符设备驱动框架实现的细节。
1.字符设备驱动抽象结构
字符设备驱动管理的核心对象是字符设备,从字符设备驱动程序的设计框架出发,内核为字符设备抽象出数据结构struct cdev,定义如下:
include/linux/cdev.h struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; };
设备驱动程序中可以有两种方式来产生struct cdev对象,一种是静态定义的方式,另一种是在程序的执行期通过动态分配。而一个struct cdev对象在被加入系统前,应该被初始化,可以通过cdev_init接口完成,具体实现如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops) { memset(cdev, 0, sizeof *cdev); init_list_head(&cdev->list); kobject_init(&cdev->kobj, &ktype_cdev_default); cdev->ops = fops; }
2.设备号及设备节点
由于struct cdev在添加到系统时需要设备号和设备节点的知识,所以此处先做个简介。
2.1 设备号分配与管理
linux系统中设备号由主设备号和次设备号组成,内核使用主设备号来定位设备驱动程序,用次设备号来管理同类设备。使用dev_t类型变量标记设备号,具体定义如下:
include/linux/types.h typedef __u32 __kernel_dev_t; typedef __kernel_dev_t dev_t;
在当前版本内核,dev_t的低20位表示次设备号,高12位表示主设备号,具体定义如下:
include/linux/kdev_t.h #define minorbits20 #define minormask ((1u << minorbits) - 1) #define major(dev) ((unsigned int) ((dev) >> minorbits)) #define minor(dev) ((unsigned int) ((dev) & minormask)) #define mkdev(ma,mi) (((ma) << minorbits) | (mi))
字符设备涉及到设备号分配的内核函数
fs/char_dev.c int register_chrdev_region(dev_t from, unsigned count, const char *name) { struct char_device_struct *cd; dev_t to = from + count; dev_t n, next; for (n = from; n < to; n = next) { next = mkdev(major(n)+1, 0); if (next > to) next = to; cd = __register_chrdev_region(major(n), minor(n), next - n, name); if (is_err(cd)) goto fail; } return 0; fail: to = n; for (n = from; n < to; n = next) { next = mkdev(major(n)+1, 0); kfree(__unregister_chrdev_region(major(n), minor(n), next - n)); } return ptr_err(cd); }
该函数第一个参数表示设备号,第二个参数表示连续设备编号个数,即驱动管理的同类设备个数,第三个参数表示设备的名称,该函数的核心是__register_chrdev_region,讨论该函数前需要先看下全局指针数组chrdevs,这个数组每一项都指向一个struct char_device_struct结构,具体定义如下:
static struct char_device_struct { struct char_device_struct *next; unsigned int major; unsigned int baseminor; int minorct; char name[64]; struct cdev *cdev;/* will die */ } *chrdevs[chrdev_major_hash_size];
__register_chrdev_region将当前设备驱动程序使用的设备号记录到这个数组中,首先分配一个struct char_device_struct的对象,然后对其初始化,完成后哈希遍历char devs并将这个对象添加到数组中。 在对字符设备初始化后,需要将该设备添加到内核当中,可以查看cdev_add函数定义。
int cdev_add(struct cdev *p, dev_t dev, unsigned count) { int error; p->dev = dev; p->count = count; error = kobj_map(cdev_map, dev, count, null,exact_match, exact_lock, p); if (error) return error; kobject_get(p->kobj.parent); return 0; }
cdev_add函数的核心是通过kobj_map实现,后者通过操作一个全局变量cdev_map把设备加入到哈希表中,cdev_map定义如下: static struct kobj_map *cdev_map;
struct kobj_map { struct probe { struct probe *next; dev_t dev; unsigned long range; struct module *owner; kobj_probe_t *get; int (*lock)(dev_t, void *); void *data; } *probes[255]; struct mutex *lock; };
简单的说,设备驱动程序通过调用cdev_add把它所管理的设备对象指针嵌入到一个struct probe节点对象中,然后再把该节点加入到cdev_map的哈希链表中。
2.2 设备节点的生成
设备节点的创建可以使用mknod指令在/dev目录下进行创建,例如mknod /dev/demodev c 2 0,如果该指令成功执行,将会在/dev目录下生成一个demodev的字符设备节点。
而mknod实际上是通过系统调用sys_mknod进入内核空间,函数的原型asmlinkage long sys_mknod(const char __user *filename, umode_t mode, unsigned dev);
mknod首先在根目录下寻找dev目录所对应的inode,通过inode编号得到该inode的内存地址,然后通过dev的inode结构中的i_op成员指针(ext3_dir_inode_operations),来调用该对象的mknod方法,这将导致ext3_mknod函数被调用。
ext3_mknod函数会创建一个新的inode节点,然后调用init_special_inode函数与设备相关联,具体实现如下:
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev) { inode->i_mode = mode; if (s_ischr(mode)) { inode->i_fop = &def_chr_fops; inode->i_rdev = rdev; } else if (s_isblk(mode)) { inode->i_fop = &def_blk_fops; inode->i_rdev = rdev; } else if (s_isfifo(mode)) inode->i_fop = &pipefifo_fops; else if (s_issock(mode)) inode->i_fop = &bad_sock_fops; else printk(kern_debug "init_special_inode: bogus i_mode (%o) for"" inode %s:%lu\n", mode, inode->i_sb->s_id,inode->i_ino); }
这个函数主要是初始化inode的成员i_fop和i_rdev,其中i_rdev成员表示该inode所对应设备的设备号。
3.打开设备文件
假设已经实现了/dev/demodev的创建,我们再来看下用户空间open函数是如何打开设备文件的。
首先看下linux用户空间open函数原型,详细说明可以使用man 2 open查看,其定义int open(const char pathname, int flags, mode_t mode);
而struct file_operations结构中对应open函数原型int (open) (struct inode , struct file *)两者差别很大,那么用户态的open接口是如何一步步的调用到内核态的open函数呢?首先用户空间的open会产生系统调用,通过sys_open进入内核空间,而sys_open函数声明如下:
asmlinkage long sys_open(const char __user *filename, int flags, umode_t mode);
但是源码中搜寻不到sys_open函数实现,可以通过”syscall_define3(open”搜索源代码,感兴趣的同学可以把这个宏展开,这里不再赘述,看下函数定义:
syscall_define3(open, const char __user *, filename, int, flags, umode_t, mode) { if (force_o_largefile()) flags |= o_largefile; return do_sys_open(at_fdcwd, filename, flags, mode); } //很明显do_sys_open才是代码核心,接着来看: long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode) { struct open_flags op; int lookup = build_open_flags(flags, mode, &op); struct filename *tmp = getname(filename); int fd = ptr_err(tmp); if (!is_err(tmp)) { fd = get_unused_fd_flags(flags); if (fd >= 0) { struct file *f = do_filp_open(dfd, tmp, &op, lookup); if (is_err(f)) { put_unused_fd(fd); fd = ptr_err(f); } else { fsnotify_open(f); fd_install(fd, f); } } putname(tmp); } return fd; }
其中do_sys_open首先会调用get_unused_fd_flags为这次open操作分配一个文件描述符fd,get_unused_fd_flags实际上是对alloc_fd的封装。随后do_sys_open调用do_filp_open函数,后者会查询/dev/demodev设备文件inode,将inode结构中i_fop赋值给filp->f_op,然后调用i_fop中的open函数。在设备节点的生成时,inode->i_fop = &def_chr_fops,所以open函数直接调用chrdev_open。
const struct file_operations def_chr_fops = { .open = chrdev_open, .llseek = noop_llseek, }; static int chrdev_open(struct inode *inode, struct file *filp) { struct cdev *p; struct cdev *new = null; int ret = 0; spin_lock(&cdev_lock); p = inode->i_cdev; if (!p) { struct kobject *kobj; int idx; spin_unlock(&cdev_lock); kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); if (!kobj) return -enxio; new = container_of(kobj, struct cdev, kobj); spin_lock(&cdev_lock); /* check i_cdev again in case somebody beat us to it while we dropped the lock. */ p = inode->i_cdev; if (!p) { inode->i_cdev = p = new; list_add(&inode->i_devices, &p->list); new = null; } else if (!cdev_get(p)) ret = -enxio; } else if (!cdev_get(p)) ret = -enxio; spin_unlock(&cdev_lock); cdev_put(new); if (ret) return ret; ret = -enxio; filp->f_op = fops_get(p->ops); if (!filp->f_op) goto out_cdev_put; if (filp->f_op->open) { ret = filp->f_op->open(inode, filp); if (ret) goto out_cdev_put; } return 0; out_cdev_put: cdev_put(p); return ret; }
chrdev_open通过调用kobj_lookup在cdev_map中用inode->i_rdev来查找设备号对应的字符设备,成功找到设备后,通过filp->f_op = fops_get(p->ops)将cdev对象的ops赋值给file对象的filp成员,同时会把cdev对象保存到inode->i_cdev成员中,这时已经将file和file_operations关联起来。