CTDIY-2-字符设备驱动的注册

 
     

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

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

你可能感兴趣的:(DI)