CTDIY2字符设备驱动的注册


  总的说来,字符设备驱动程序的实现包含两个大的方面,所以分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。

  接下来就进入字符设备的内部实现。

原文地址:https://www.cnblogs.com/plinx/p/2813947.html