总的说来,字符设备驱动程序的实现包含两个大的方面,所以分2篇来讨论。
这两个个方面分别为:设备注册与驱动加载、字符设备的内部实现(个人分类,仅作参考)
一、设备的创建
1)原程序中
dev_t devno = MKDEV(globalmem_major, 0); //加载函数中对MKDEV的调用
2)索引的函数
/include/linux/kedev_t.h #define MINORBITS 20 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
3)这是一个创建设备的函数,也算不上一个函数,可以看成一个运算:将ma(jor)左移20位后与mi(nor)相或。
为何要这样做,先来看看dev_t的定义。
/include/asm-generic/int-l64.h typedef unsigned int __u32; // 32位的整型 /include/linux/types.h typedef __u32 __kernel_dev_t; typedef __kernel_dev_t dev_t; // 由此可知dev_t是无符号的32位整型
dev_t为32位的整型,dev_t前12位是主设备号,后20位是次设备号,如此便充分利用的32位CPU的资源。
要明白其工作,就要进一步了解它是如何被使用的。
#define MINORMASK ((1U << MINORBITS) - 1) //已知MINORBITS是20,将1左移20位后减1,得到的就是20位的1。 //而一个32位的数,前12位为0,后20位为1,用16进制数表示就是0x000F_FFFF //其实,如果从英文的角度就很容易理解了 //MINORBITS是次设备号的位,MINORMASK是次设备号的掩码 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //由上面的讲解可知,将dev_t右移了次设备号的位数后便只剩主设备号了 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //dev与次设备号掩码相与,得到的就只剩次设备号了 //这样看来,主次设备号的生成就很好理解了
4)综上考虑,在原程序头文件与设备注册文件中的两个定义,就已经指明了文件的主次设备号
/*预设的mem的主设备号*/ #define GLOBALMEM_MAJOR 250 //the major dev_t devno = MKDEV(globalmem_major, 0); // 次设备号为0
二、设备号的分配函数
1)原程序中
if(globalmem_major) //静态分配设备号 result = register_chrdev_region(devno, 1, "globalmem"); else{ //动态分配设备号 result = alloc_chrdev_region(&devno, 0, 1, "globalmem"); globalmem_major = MAJOR(devno); }
2)在开始看设备号分配的函数前,先来看看设备号在文件中的表现
在linux中一共有三种设备:字符设备,块设备和网络接口
#ll //在/dev目录下输入ll即ls -l可以看到如下内容 //其中划红线地方的首字母表示设备号 //c表示字符设备、d表示块设备
这里的tty就是我们常用的终端了,硬盘自然是块设备了。而在12月之前的两个编号,前面的表示主设备号,后面的表示次设备号。
在linux中,一切设备皆文件的思想无处不在,要将设备文件化,把他们合理地进行编号是尤为重要的,设备号的重要程度也可想而知了。
接下来往下看,在第一组if/else中,如若预先设置了主设备号,则执行静态注册设备号函数,缺省则执行动态注册设备号函数。
3)静态注册设备号函数
/linux/fs/char_dev.c //动态注册设备号函数也在同一文件夹下 int register_chrdev_region(dev_t from, unsigned count, const char *name) //传入参数为 (devno, 1, "globalmem"),即为(250, 1, "globalmem") { struct char_device_struct *cd; //创建结构体 dev_t to = from + count; //即to = 251 dev_t n, next; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n)+1, 0); //因为n = from,所以无论初始化的主设备号是什么,这里的next都等于主设备号加1,这是一定会比to大的…… if (next > to) //所以这个判断在这里一定成立,而它真正的重点在于to,若to的值大于一个主设备号的范围,此时next中的值将是下一个设备 next = to; //而不是to了,所以to决定了设备号申请的范围 cd = __register_chrdev_region(MAJOR(n), MINOR(n), next - n, name); //这里就因为to的大小产生了两种情况了 //1、to > 一个主设备号,逐个选取主设备号,次设备号除from和to都为0 //2、to < 一个主设备号,申请from和to两个设备号 if (IS_ERR(cd)) goto fail; //申请失败跳转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);
//返回最后一次创建失败的设备空间指针
}
4)静态注册函数的内部实现
static struct char_device_struct * //这里传入的参数为(MAJOR(n), MINOR(n), next-n, name) __register_chrdev_region(unsigned int major, unsigned int baseminor, //这个值得注意的是这个next - n, 它表示了该设备可以申请的设备号范围 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); //申请内核空间 if (cd == NULL) //申请失败 return ERR_PTR(-ENOMEM); //ENOMEM内存不足标志 mutex_lock(&chrdevs_lock); //互斥锁 /* temporary */ if (major == 0) { //主设备号为0 for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) { //从254到1循环查询空闲的主设备号 if (chrdevs[i] == NULL) break; } if (i == 0) { //循环结束主设备号仍为0,则设备全在使用中,0号设备禁止分配 ret = -EBUSY; //返回 忙错误 goto out; } major = i; //将i中的第一个空闲设备号用为主设备号 ret = major; } cd->major = major; //将主设备号、起始次设备号、次设备号、设备名字 标记到 申请到的空间 cd->baseminor = baseminor; cd->minorct = minorct; strlcpy(cd->name, name, sizeof(cd->name)); i = major_to_index(major); //主设备号索引值,相当于 major % MAX_PROBE_HASH(=255),散列 for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) //这里的主要是在支持多设备的设备驱动中作用,比如说我们的tty0, tty1都属于同一驱动 //只是它们属于并行的同一设备驱动函数所创建,如此便要注意之前提醒的next - n了 if ((*cp)->major > major || ((*cp)->major == major && (((*cp)->baseminor >= baseminor) || ((*cp)->baseminor + (*cp)->minorct > baseminor)))) //这个if所判断返回的,是 主设备号大于当前设备号的设备 //或者是 同一主设备号下的 次设备号大于当前次设备号的设备(散列的后一个元素) // 或 次设备号范围大于当前次设备号的(散列的元素范围内) //其间的内存孔洞,可能是rmmod造成的 break; /* Check for overlapping minor ranges. */ 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; //同一主设备号下的下一个设备 //下一个设备的次设备号起始地址baseminor 赋值给 old_min //下一个设备的次设备号范围赋给 old_max //当前申请到的设备号赋值给 new_min //当前申请到的设备号 加上 设备号申请的范围 (next - n),赋值给 new_max /* New driver overlaps from the left. */ if (new_max >= old_min && new_max <= old_max) { //这里其实就是检查是否有设备号范围重叠 ret = -EBUSY; // new_max 在 old_min 与 old_max之间时,则次设备号范围重叠,返回 忙错误 goto out; } /* New driver overlaps from the right. */ if (new_min <= old_max && new_min >= old_min) { // new_min 在 old_min 与 old_max之间时,次设备号重叠,返回 忙错误 ret = -EBUSY; goto out; } } cd->next = *cp; //若无重叠情况产生,则将这下一个设备的指针,赋值给cd->next, 此处next指向也可为 NULL *cp = cd; mutex_unlock(&chrdevs_lock); //释放锁 return cd; out: mutex_unlock(&chrdevs_lock); kfree(cd); return ERR_PTR(ret); }
5)在分配设备的函数中,用到了三个重要的参数 major, baseminor, minorct,通过索找到其定义
/incldue/fs/char_dev.c 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];
6)顺着静态注册函数的主体往下看,很快便出现了一个简单而强大的函数kzalloc
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL); //GFP的意思是get free pages,后面的参数是所要调用GFP类型的flags //kzalloc函数原型 /linux/include/linux/slab.h static inline void *kzalloc(size_t size, gfp_t flags) { return kmalloc(size, flags | __GFP_ZERO); } //在这里可以看到,我们输入的flags类型要与GFP_ZERO(返回零页)类型相或,一般情况下是得到我们输入的类型,在不填的时候默认为零页 //kmalloc是一个内核使用的空间分配函数 //kzalloc一次实现了内核空间分配和分配类型现则,所以说简单而强大
7)接着往下就回看到mutex_lock函数,在此先不讨论,结束本文尽快揩文一篇记录锁机制的点点滴滴
8)动态分配设备号
/** * alloc_chrdev_region() - register a range of char device numbers * @dev: output parameter for first assigned number * @baseminor: first of the requested range of minor numbers * @count: the number of minor numbers required * @name: the name of the associated device or driver * * Allocates a range of char device numbers. The major number will be * chosen dynamically, and returned (along with the first minor number) * in @dev. Returns zero or a negative error code. */ int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) { struct char_device_struct *cd; cd = __register_chrdev_region(0, baseminor, count, name); if (IS_ERR(cd)) return PTR_ERR(cd); *dev = MKDEV(cd->major, cd->baseminor); return 0; }
相对而言,动态设备的分配方法,从本质上说都是调用__register_chrdev_region(0, baseminor, count, name)函数,所以看明白了静态分配设备号的函数,动态分配设备号函数也大同小异了。
9)内核空间的分配
……
//在前面的设备注册函数中,返回的都是result,如果注册失败,返回的是-EBUSY,或者是ERR_PTR(cd)等 if (result < 0)
//若为负值则直接返回
return result; globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
//当申请返回的result >= 0 时,为设备申请内核空间
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; }
10)驱动移除
void globalmem_exit(void) { cdev_del(&globalmem_devp->cdev); kfree(globalmem_devp); unregister_chrdev_region(MKDEV(globalmem_major, 0), 1); }
具体便不再讨论,细节与注册过程基本相同,只是将分配了的空间释放掉。
11)模块启动和退出程序
module_init(globalmem_init);
module_exit(globalmem_exit);
这个无须多讲……
最后,小总结一下,在设备注册与驱动加载这一部分,具体实现了哪些过程。
1、设备号的申请
2、设备的注册
3、内核空间的分配
要执行这些过程,必不可少的是globalmem_major(可以预先设定) 与 struct file operations。
接下来就进入字符设备的内部实现。