LINUX—字符设备驱动之-globalmem

    技术2022-05-20  62

    下面将以linux设备驱动开发详解上的globalmem设备驱动为例来详细分析字符设备驱动的过程。

     

    #include <linux/module.h>//模块所需的大量符号和函数定义

    #include <linux/types.h>

    #include <linux/fs.h>//文件系统相关的函数和头文件

    #include <linux/errno.h>

    #include <linux/mm.h>

    #include <linux/sched.h>//包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明

    #include <linux/init.h>//指定初始化和清除函数

    #include <linux/cdev.h>//cdev结构的头文件 包含<linux/kdev_t.h>

    #include <asm/io.h>

    #include <asm/system.h>

    #include <asm/uaccess.h>//在内核和用户空间中移动数据的函数

     

    #define GLOBALMEM_SIZE  0x1000  /*全局内存最大4K字节*/

    #define MEM_CLEAR 0x1  /*0全局内存*/

    #define GLOBALMEM_MAJOR 254    /*预设的globalmem的主设备号*/

     

    static int globalmem_major = GLOBALMEM_MAJOR;

    /*globalmem设备结构体*/

    struct globalmem_dev                                    

    {                                                       

      struct cdev cdev; /*cdev结构体*/                      

      unsigned char mem[GLOBALMEM_SIZE]; /*全局内存*/       

    };

     

    struct globalmem_dev *globalmem_devp; /*设备结构体指针*/

    /*文件打开函数*/

    int globalmem_open(struct inode *inode, struct file *filp)

    {

      /*将设备结构体指针赋值给文件私有数据指针.

     

      系统在调用驱动程序的open方法前将这个指针置为NULL

      驱动程序可以将这个字段用于任意目的,也可以忽略这个字段。

      驱动程序可以用这个字段指向已分配的数据,

      但是一定要在内核释放file结构前的release方法中清除它*/

      filp->private_data = globalmem_devp;

      //将文件私有数据private_data指向设备结构体,readwriteioctlllseek等函数

      //通过private_data访问设备结构体

      return 0;

    }

    /*文件释放函数*/

    int globalmem_release(struct inode *inode, struct file *filp)

    {

      return 0;

    }

    /*

    备方法应当进行下面的任务:

    1、释放 open 分配在 filp->private_data 中的任何东西

    2、在最后的 close 关闭设备globalmem_release的基本形式没有硬件去关闭, 因此需要的代码是最少的.不是每个 close 系统调用引起调用 release 方法. 只有真正释放设备数据结构的调用会调用这个方法

    */

    /* ioctl设备控制函数 */

    static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned

      int cmd, unsigned long arg)

    {

      struct globalmem_dev *dev = filp->private_data;/*获得设备结构体指针*/

     

      switch (cmd)

      {

        case MEM_CLEAR://清除全局内存

          memset(dev->mem, 0, GLOBALMEM_SIZE);     

          printk(KERN_INFO "globalmem is set to zero/n");

          break;

     

        default://其他不支持的命令

          return  - EINVAL;

      }

      return 0;

    }

     

    /*读函数*/

    static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size,

      loff_t *ppos)

    {

      unsigned long p =  *ppos;

      unsigned int count = size;

      int ret = 0;

      struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/

     

      /*分析和获取有效的写长度*/

      if (p >= GLOBALMEM_SIZE)//0x1000 要读的偏移位置越界

        return count ?  - ENXIO: 0;

      if (count > GLOBALMEM_SIZE - p)//要读的字节数太大

        count = GLOBALMEM_SIZE - p;

     

      /*内核空间->用户空间*/

      //将内核空间的内容复制到用户空间,所复制的内容是从from来,到to去,复制n个字节。

      if (copy_to_user(buf, (void*)(dev->mem + p), count))

      {

        ret =  - EFAULT;

      }

      else

      {

        *ppos += count;

        ret = count;

       

        printk(KERN_INFO "read %d bytes(s) from %d/n", count, p);

      }

     

      return ret;

    }

     

    /*写函数*/

    static ssize_t globalmem_write(struct file *filp, const char __user *buf,

      size_t size, loff_t *ppos)

     /*

     filp 是文件指针,count 是请求的传输数据长度,buff 参数是指向用户空间的缓冲区,这个缓冲区或者保存要写入的数据,或者是一个存放新读入数据的空缓冲区,该地址在内核空间不能直接读写,offp 是一个指针指向一个"long offset type"对象, 它指出用户正在存取的文件位置. 返回值是一个"signed size type。写的位置相对于文件开头的偏移。

     */

    {

      unsigned long p =  *ppos;

      unsigned int count = size;

      int ret = 0;

      struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/

     

      /*分析和获取有效的写长度*/

      if (p >= GLOBALMEM_SIZE)//0x1000 要写的偏移位置越界

        return count ?  - ENXIO: 0; //???

      if (count > GLOBALMEM_SIZE - p)//将数据写入该缓存区,直到结尾。这个是判断要写的字节数太多

        count = GLOBALMEM_SIZE - p;

       

      /*用户空间->内核空间*/

      if (copy_from_user(dev->mem + p, buf, count))

     //目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0

        ret =  - EFAULT;

      else

      {

        *ppos += count;

        ret = count;

       

        printk(KERN_INFO "written %d bytes(s) from %d/n", count, p);

      }

     

      return ret;//如果ret=count,则完成了所请求数目的字节传送。

    }

     

    /* seek文件定位函数 */

    /*

    seek()函数对文件定位的起始地址可以是文件开头(SEEK_SET0)、当前位置(SEEK_CUR1)和文件尾(SEEK_END2),globalmem支持从文件开头和当前位置相对偏移。在定位的时候,应该检查用户请求的合法性,若不合法,函数返回- EINVAL,合法时返回文件的当前位置*/

    static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)

    {

      loff_t ret = 0;

      switch (orig)

      {

        case 0:   /*相对文件开始位置偏移*/

          if (offset < 0)

          {

            ret =  - EINVAL;

            break;

          }

          if ((unsigned int)offset > GLOBALMEM_SIZE)//偏移越界

          {

            ret =  - EINVAL;

            break;

          }

          filp->f_pos = (unsigned int)offset;//struct file

          ret = filp->f_pos;

          break;

        case 1:   /*相对文件当前位置偏移*/

          if ((filp->f_pos + offset) > GLOBALMEM_SIZE)//偏移越界

          {

            ret =  - EINVAL;

            break;

          }

          if ((filp->f_pos + offset) < 0)

          {

            ret =  - EINVAL;

            break;

          }

          filp->f_pos += offset;

          ret = filp->f_pos;

          break;

        default:

          ret =  - EINVAL;

          break;

      }

      return ret;//合法时返回文件的当前位置

    }

     

    /*文件操作结构体*/

    static const struct file_operations globalmem_fops =

    {

      .owner = THIS_MODULE,

      .llseek = globalmem_llseek,

      .read = globalmem_read,

      .write = globalmem_write,

      .ioctl = globalmem_ioctl,

      .open = globalmem_open,

      .release = globalmem_release,

    };

     

    /*初始化并注册cdev

    globalmem_setup_cdev(globalmem_devp, 0);*/

    static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)

    {

      int err, devno = MKDEV(globalmem_major, index);

     

      cdev_init(&dev->cdev, &globalmem_fops);

      /* cdev 结构嵌入一个你自己的设备特定的结构,你应当初始化你已经分配的结构使用以上函数,有一个其他的 struct cdev 成员你需要初始化. file_operations 结构,struct cdev 有一个拥有者成员,应当设置为 THIS_MODULE,一旦 cdev 结构建立, 最后的步骤是把它告诉内核, 调用:

       cdev_add(&dev->cdev, devno, 1);*/

      /*void cdev_init(struct cdev *cdev, const struct file_operations *fops)

    {

           memset(cdev, 0, sizeof *cdev);

           INIT_LIST_HEAD(&cdev->list);

           cdev->kobj.ktype = &ktype_cdev_default;

           kobject_init(&cdev->kobj);

           cdev->ops = fops;

    }  */

    /*

    struct cdev {

           struct kobject kobj;

           struct module *owner;

           const struct file_operations *ops;

           struct list_head list;

           dev_t dev;

           unsigned int count;

    };

    /*globalmem设备结构体*/

    struct globalmem_dev                                    

    {                                                       

      struct cdev cdev; /*cdev结构体*/                      

      unsigned char mem[GLOBALMEM_SIZE]; /*全局内存*/       

    };

    */

      dev->cdev.owner = THIS_MODULE;

      dev->cdev.ops = &globalmem_fops;

      err = cdev_add(&dev->cdev, devno, 1);

     /*

     cdev cdev 结构, devno 是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 常常 count 1, 但是有多个设备号对应于一个特定的设备的情形. 例如, 设想 SCSI 磁带驱动, 它允许用户空间来选择操作模式(例如密度), 通过安排多个次编号给每一个物理设备.在使用 cdev_add 是有几个重要事情要记住. 第一个是这个调用可能失败. 如果它返回一个负的错误码,

    你的设备没有增加到系统中. 它几乎会一直成功, 但是, 并且带起了其他的点: cdev_add 一返回, 你的设备就是"活的"并且内核可以调用它的操作. 除非你的驱动完全准备好处理设备上的操作, 你不应当调用 cdev_add */

     

      if (err)

        printk(KERN_NOTICE "Error %d adding LED%d", err, index);

    }

     

    /*设备驱动模块加载函数*/

    /*_init()__init是一个宏,编译时会用到。对于静态编译在内核的  模块有意义.

    内核使用了大量不同的宏来标记具有不同作用的函数和数据结构。如宏__init__devinit等。

    这些宏在include/linux/init.h头文件中定义。编译器通过这些宏可以把代码优化放到合适的内存位置,

    以减少内存占用和提高内核效率。

    init():标记内核启动时使用的初始化代码,内核启动完成后不再需要。以此标记的代码位于.init.text内存区域。

    #define _ _init    _ _attribute_ _ ((_ _section_ _ (".text.init")))

    初始化代码的特点是:在系统启动运行,且一旦运行后马上退出内存,不再占用内存。

    */

    int globalmem_init(void)

    {

      int result;

      dev_t devno = MKDEV(globalmem_major, 0);

      /*通过主次设备号生成dev_t.

    主次设备号的数据类型是dev_t,在/linux/types.h中定义。

    2.6内核中,dev_t是一个32位的数,其中12位用来表示主设备号,

    其余20位用来表示次设备号。要获得设备的主次设备号可以使用内核提供的宏:

    MAJOR(dev_t dev);        #获得主设备号

    MINOR(dev_t dev);        #获得次设备号

    这些宏定义位于linux/kdev_t.h中。如果要把主次设备号转换成dev_t类型,

    则可使用:

    MKDEV(int major, int minor);*/

     

      /* 申请设备号*/

      if (globalmem_major)/*静态申请设备号*/

        result = register_chrdev_region(devno, 1, "globalmem");

        /*linux/fs.h中,

        devno:是你要分配的起始设备编号. devno的次编号部分常常是 0, 但是没有要求是那个效果;

        1:是所请求的连续设备号的个数;

    globalmem:是和该设备号范围关联的设备名称,它将出现在/proc/devices/sysfs中。

    如果分配成功则返回0,分配失败则返回一个负的错误码,所请求的设备号无效。

        */

      else  /* 动态申请设备号*/

      {

        result = alloc_chrdev_region(&devno, 0, 1, "globalmem");

        /*dev_t *dev

        dev:是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数;

        firstminor:应当是请求的第一个要用的次编号; 它常常是 0

        1     是所请求的连续设备号的个数;

        Globalmem:是和该设备号范围关联的设备名称,它将出现在/proc/devices/sysfs中。*/   

        globalmem_major = MAJOR(devno);/*获得主设备号,dev_t结构中分解出主设备号*/

      } 

     /*

     如果globalmem_major=0,利用udev工具自动向系统动态申请未被占用的设备号相关函数定义在include/linux/kdev_t.h

     #define MINORBITS       20

     #define MINORMASK   ((1U << MINORBITS) - 1)

     //(1<<20 -1) 此操作后,MINORMASK宏的低20位为1,高12位为0

     

     #define MAJOR(dev)       ((unsigned int) ((dev) >> MINORBITS))

     #define MINOR(dev)       ((unsigned int) ((dev) & MINORMASK))

     #define MKDEV(ma,mi)  (((ma) << MINORBITS) | (mi))

    */

      if (result < 0)

        return result;

       

      /* 动态申请设备结构体的内存*/

      globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL);

      /*

      在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,或free_pages. 请注意,kmalloc等函数返回的是物理地址!而malloc等返回的是线性地址!要注意kmalloc最大只能开辟128k-1616个字节是被页描述符结构占用了。内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址*/

      if (!globalmem_devp)    /*申请失败*/

      {

        result =  - ENOMEM;

        goto fail_malloc;

      }

      memset(globalmem_devp, 0, sizeof(struct globalmem_dev));

      /*memset会将参数globalmem_devp所指的内存区域中前sizeof(struct globalmem_dev)个字节以参数0填入,然后返回指向globalmem_devp的指针*/

     

      globalmem_setup_cdev(globalmem_devp, 0);

      /*初始化并注册cdev*/

      return 0;

     

      fail_malloc: unregister_chrdev_region(devno, 1);

      /*释放原先申请的设备号

      如果我们不再使用设备号,则要使用unregister_chrdev_region()函数释放它。

        devno:是要分配的主设备号范围的起始值,次设备号一般设置为0

        1:是所请求的连续设备号的个数;*/

      return result;

    }

     

    /*模块卸载函数*/

    /*

    __exit,标记退出代码,对于非模块无效。

    */

    void globalmem_exit(void)

    {

      cdev_del(&globalmem_devp->cdev);   /*注销cdev,为从系统去除一个字符设备*/

      kfree(globalmem_devp);     /*释放设备结构体内存*/

      unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);

      /*释放设备号

      我们一般在模块的清除函数中调用设备号释放函数。*/

    }

     

    MODULE_AUTHOR("Song Baohua");

    MODULE_LICENSE("Dual BSD/GPL");//指定代码使用的许可证

     

    module_param(globalmem_major, int, S_IRUGO);

    /*module_param宏的第一个参数是选项名,可在/sys虚拟文件系统中该模块的parameter目录中中查看到。第二个参数是选项类型,第三个参数是选项的值*/

     

    module_init(globalmem_init);

    module_exit(globalmem_exit);

    /*向操作系统注册自己定义的这两个函数,该项目在Kconfig中配置项目为布尔型的话为YN两种选项,Y为编译进内核,N不编译,如果为三态型(tristate),为YNM三种选项M为编译为模块,模块也可以在内核编译后根据需要进行手动加载,也可以写个脚本自动加载*/

     

     

    总结:

    字符设备是3大类设备(字符设备、块设备和网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_opersation结构体中的操作函数,实现file_opersation结构体中的read()write()ioctl()等函数是驱动设计的主体工作。

     

    http://blogold.chinaunix.net/u3/104334/showart_2061894.html


    最新回复(0)