1. 理解驱动程序
由于unix的万物皆文件的思想,底层的设备也用特殊文件类型,设备文件来操作,通过读写设备文件就可以读写设备内存或者设备端口了,因此需要一个在虚拟文件系统和底层设备之间的一个适配器,这就是设备驱动程序,用于粘合虚拟文件系统和底层的设备,提供上层的虚拟文件系统统一的接口,并且管理和底层设备的交互。
字符设备可以通过一种比较简单的方式管理,因为不需要I/O调度、底层同步、重排请求等块设备的复杂操作。下面看看linux内核管理字符设备驱动程序需要的工作,linux通过主从设备号来标识设备和驱动程序,因此为了保证每个设备驱动程序的主从设备号唯一,需要管理驱动程序主从设备号的数据结构。文件系统打开一个设备文件时,不能使用打开普通的接口,需要提供特殊的打开文件的接口将驱动程序的提供给文件系统的接口提供给通用的struct
file对象,这样就可以通过file对象调用设备驱动的读写操作来操作设备。
file对象,这样就可以通过file对象调用设备驱动的读写操作来操作设备。
2. 数据结构
如何管理驱动程序的主从设备号,主设备号用来标识驱动程序,因为同一个驱动程序可以管理多个设备,用从设备号来标识特定的设备,因此一个设备就可以通过主从设备号来标识。下面是linux管理字符设备主从设备号的数据结构:
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];
可以看到内核通过hash表来管理主从设备号,使用溢出链表来管理哈希冲突。在向内核注册字符设备驱动程序的时候先从这个数据结构中注册一个主设备号和若干从设备号,由于同一个主设备号的从设备号不能和已经注册的从设备号重叠,因此内核需要重叠检查,若重叠则出错。
/* * Register a single major with a specified minor range. * * If major == 0 this functions will dynamically allocate a major and return * its number. * * If major > 0 this function will attempt to reserve the passed range of * minors and will return zero on success. * * Returns a -ve errno on failure. */ static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name) { struct char_device_struct *cd, **cp; int ret = 0; int i; cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL); //分配一个char_device_struct if (cd == NULL) return ERR_PTR(-ENOMEM); mutex_lock(&chrdevs_lock); /* temporary */ if (major == 0) { //如果申请的主设备号为0,则从内核中申请一个没有被使用的主设备号 for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) { if (chrdevs[i] == NULL) break; } if (i == 0) { ret = -EBUSY; goto out; } major = i; ret = major; } cd->major = major; //填充char_device_struct cd->baseminor = baseminor; cd->minorct = minorct; strlcpy(cd->name, name, sizeof(cd->name)); i = major_to_index(major);//求当前设备号在哈希表中的位置,哈希函数使用很简单的 major%255 for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)/*在哈希表对应的位置上溢出链表上排序,排序键是主设备,然后是从设备号*/ if ((*cp)->major > major || ((*cp)->major == major && (((*cp)->baseminor >= baseminor) || ((*cp)->baseminor + (*cp)->minorct > baseminor)))) break; /* 检测是否和已经注册的设备号冲突,若冲突则直接返回出错 */ if (*cp && (*cp)->major == major) { int old_min = (*cp)->baseminor; int old_max = (*cp)->baseminor + (*cp)->minorct - 1; int new_min = baseminor; int new_max = baseminor + minorct - 1; /* New driver overlaps from the left. */ if (new_max >= old_min && new_max <= old_max) { ret = -EBUSY; goto out; } /* New driver overlaps from the right. */ if (new_min <= old_max && new_min >= old_min) { ret = -EBUSY; goto out; } } cd->next = *cp;//设备号分配成功,返回 *cp = cd; mutex_unlock(&chrdevs_lock); return cd; out: mutex_unlock(&chrdevs_lock); kfree(cd); return ERR_PTR(ret); }
注册完主从设备号,就需要将驱动程序关联设备号添加到内核。字符设备的驱动程序使用struct cdev表示:
struct cdev { struct kobject kobj;//用于该结构的一般性管理,添加到sysfs struct module *owner; //这个指向提供驱动程序的模块 const struct file_operations *ops; //向上提供给文件系统的接口 struct list_head list; /*由于一个从设备号标识特定的设备,并在虚拟文件系统中有对应的设备文件,这个list将所有和这个驱动程序相关的inode串起来,在inode中的链表节点为i_devices成员*/ dev_t dev; /*设备号,使用和此设备驱动程序关联的主设备和第一个从设备何从一个dev_t类型的对象,低20位表示从设备号,高12位表示主设备号*/ unsigned int count; //从设备号的个数 };
我们仍然需要向内核注册驱动程序,用于内核查找特定的驱动程序,内核继续通过一个哈希表来管理所有的字符设备驱动程序:
static struct kobj_map *cdev_map;
struct kobj_map { struct probe { struct probe *next; //溢出链表节点 dev_t dev; //设备号,同cdev的dev成员 unsigned long range; //从设备号的范围 struct module *owner; //驱动程序的模块 kobj_probe_t *get; /*这个函数可以通过设备查找驱动程序的cdev->kobj成员,一般来讲就是(struct cdev*)data->kobj */ int (*lock)(dev_t, void *); void *data; //对于字符设备,是一个cdev的指针 } *probes[255]; //哈希表的slot为255,继续通过主设备major%255作为哈希函数 struct mutex *lock; };
下面看一下想内核注册驱动程序的函数:
/** * cdev_add() - add a char device to the system * @p: the cdev structure for the device * @dev: the first device number for which this device is responsible * @count: the number of consecutive minor numbers corresponding to this * device * * cdev_add() adds the device represented by @p to the system, making it * live immediately. A negative error code is returned on failure. */ int cdev_add(struct cdev *p, dev_t dev, unsigned count) { p->dev = dev; p->count = count; return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p); //添加到cdev_map }
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range, struct module *module, kobj_probe_t *probe, int (*lock)(dev_t, void *), void *data) { unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1; /*dev + range - 1有可能超过了当前主设备号的范围,因此需要在哈希表中添加多个项来表示同一个设备驱动程序*/ unsigned index = MAJOR(dev); unsigned i; struct probe *p; if (n > 255) n = 255; p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL); //为probe分配内存 if (p == NULL) return -ENOMEM; for (i = 0; i < n; i++, p++) {//初始化所有的probe p->owner = module; p->get = probe; p->lock = lock; p->dev = dev; p->range = range; p->data = data; } mutex_lock(domain->lock); for (i = 0, p -= n; i < n; i++, p++, index++) { struct probe **s = &domain->probes[index % 255];//将所有的probe有序加入到哈希表对应的slot中 while (*s && (*s)->range < range) s = &(*s)->next; p->next = *s; *s = p; } mutex_unlock(domain->lock); return 0; }
这样就可以通过cdev_map查找特定的驱动程序。具体的查找工作就是在哈希表中找对应的项就可以了。
3. 打开设备文件
打开设备文件就需要将驱动程序和文件系统关联起来。在inode中设了几个成员用于处理这种关联。
- i_rdev表示驱动程序设备号
- i_mode表示文件类型,字符设备或者块设备
- i_fop 文件系统接口
- i_cdev/i_bdev 字符设备驱动或者块设备驱动
将这种关联建立起来就建立底层设备和上层文件系统的关联,驱动程序的职责就在此。
下面看看如何建立这种关联的,在文件系统中,打开特殊文件都通过fs/inode.c中的init_special_inode函数来处理,对于字符最后调用chrdev_open处理字符设备。
/* * Called every time a character special file is opened */ 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; //取得inode的字符驱动指针,第一次打开,这个指针为空 if (!p) {//如果是第一次打开 struct kobject *kobj; int idx; spin_unlock(&cdev_lock); kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);//在cdev_map中查找对应的驱动程序的kobj if (!kobj) return -ENXIO; new = container_of(kobj, struct cdev, kobj);//container_of机制获得cdev 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) { //如果i_cdev仍为空 inode->i_cdev = p = new; //设置inode中的驱动指针 list_add(&inode->i_devices, &p->list); /*将这个inode添加到cdev的list链表中,表示inode和cdev关联*/ new = NULL; } else if (!cdev_get(p)) ret = -ENXIO; } else if (!cdev_get(p)) //不是第一次打开,就直接增加p的kobj引用计数即可用了 ret = -ENXIO; spin_unlock(&cdev_lock); cdev_put(new); if (ret) return ret; ret = -ENXIO; filp->f_op = fops_get(p->ops); //将驱动的文件系统操作复制给file结构,这样对file的读写就是对设备的读写 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; }
struct file得到了驱动程序上虚拟文件系统的文件操作接口,所有对file的的读写操作都重定向到具体的驱动程序,由驱动程序完成读写操作,这就建立虚拟文件系统和底层设备的联系。
直接写入内核的字符设备驱动程序在drivers/char/mem.c中,包括主设备号为1的不同从设备号的/dev/null, /dev/zero, /dev/random等字符设备