字符设备驱动编程②(cdev结构)

简介: 字符设备驱动编程②(cdev结构)

前言

我们接着讲,我们使用正点原子提供的字符设备驱动的模板来进行讲解。相信大家应该都已经知道代码的意思,我们来分析函数之间的调用关系。模板如下:

/* 注册字符设备驱动 */
  /* 1、创建设备号 */
  if (newchrled.major) {    /*  定义了设备号 */
    newchrled.devid = MKDEV(newchrled.major, 0);
    register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
  } else {            /* 没有定义设备号 */
    alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);  /* 申请设备号 */
    newchrled.major = MAJOR(newchrled.devid); /* 获取分配号的主设备号 */
    newchrled.minor = MINOR(newchrled.devid); /* 获取分配号的次设备号 */
  }
  printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor); 
  /* 2、初始化cdev */
  newchrled.cdev.owner = THIS_MODULE;
  cdev_init(&newchrled.cdev, &newchrled_fops);
  /* 3、添加一个cdev */
  cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
  /* 4、创建类 */
  newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
  if (IS_ERR(newchrled.class)) {
    return PTR_ERR(newchrled.class);
  }
  /* 5、创建设备 */
  newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
  if (IS_ERR(newchrled.device)) {
    return PTR_ERR(newchrled.device);
  }
  return 0;
}
/*
 * @description : 驱动出口函数
 * @param     : 无
 * @return    : 无
 */
static void __exit led_exit(void)
{
  /* 注销字符设备驱动 */
  cdev_del(&newchrled.cdev);/*  删除cdev */
  unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */
  device_destroy(newchrled.class, newchrled.devid);
  class_destroy(newchrled.class);
}

在这里,关于函数的参数什么的就不讲了,如果不知道,可以找我前面写的博文。


注册字符设备编号

内核提供了三个函数来注册一组字符设备编号,这三个函数分别是

int register_chrdev(unsigned int, const char *,struct file_operations *)//int为0时候动态注册
int alloc_chrdev_region(dev_t, unsigned, const char *);     //动态的申请注册一个设备号
int register_chrdev_region(dev_t, unsigned, const char *); //静态的申请和注册设备号

这三个函数都会调用一个共用的 __register_chrdev_region() 函数来注册一组设备编号范围(即一个 char_device_struct 结构)。

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 i;
    int ret = 0;
    cd = kmalloc(sizeof(struct char_device_struct), GFP_KERNEL);/*slab分配一个char_device_struct变量*/
    if (cd == NULL)
        return ERR_PTR(-ENOMEM);
    memset(cd, 0, sizeof(struct char_device_struct));/*将刚刚分配的变量的内存区清零*/
    write_lock_irq(&chrdevs_lock);/*关中断,禁止内核抢占+读写锁*/
    /* temporary */
    if (major == 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;
    cd->baseminor = baseminor;
    cd->minorct = minorct;/*申请设备号的个数*/
    cd->name = name;
    /****************处理char_device_struct变量的分配和初始化************/
    /****************将char_device_struct变量注册到内核*****************/
    i = major_to_index(major);/*将major对256取余数,得到可以存放char_device_struct在chrdevs中的索引*/
    /*
     *退出循环:
         (1)chrdevs[i]为空
         (2)chrdevs[i]的主设备号大于major
         (3)chrdevs[i]的主设备号等于major,但是次设备号大于等于baseminor
     */
  for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
    if ((*cp)->major > major ||
        ((*cp)->major == major &&
         (((*cp)->baseminor >= baseminor) ||
          ((*cp)->baseminor + (*cp)->minorct > baseminor))))
      break;
    /*
     *如果*cp不空,并且*cp的major与要申请的major相同,
     *此时,如果(*cp)->baseminor < baseminor + minorct,就会发生冲突
     *因为和已经分配了的设备号冲突了。
     *出错!
     */
  /* 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;
    /* 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;
    write_unlock_irq(&chrdevs_lock);
    return cd;
out:
    write_unlock_irq(&chrdevs_lock);
    kfree(cd);
    return ERR_PTR(ret);
}

函数 __register_chrdev_region() 主要执行以下步骤:

  1. 分配一个新的 char_device_struct 结构,并用 0 填充。
  2. 如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。动态分配主设备号的原则是从散列表的最后一个桶向前寻找,那个桶是空的,主设备号就是相应散列桶的序号。所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。
  3. 根据参数设置 char_device_struct 结构中的初始设备号,范围大小及设备驱动名称。
  4. 计算出主设备号所对应的散列桶,为新的 char_device_struct 结构寻找正确的位置。同时,如果设备编号范围有重复的话,则出错返回。
  5. 将新的 char_device_struct 结构插入散列表中,并返回 char_device_struct 结构的地址。

初始化cdev

函数cdev_init()用于初始化一个静态分配的cdev结构体变量,函数cdev_init会自动初始化cdev->ops对象,将函数的第二个输入参数赋值给cdev->ops对象,不会初始化cdev->owner对象,因此在经过函数cdev_alloc()和函数cdev_init()处理之后的cdev结构体变量,在应用程序中只需要给cdev->owner对象赋值,此结构变量就可以被插入Linux内核系统了,作为一个可用的字符设备使用。

struct cdev {
  struct kobject kobj;
  /*字段 kobj用来描述设备的引用计数,在终端中,可以通过lsmod命令显示模块相关的信息,其中包括引用计数。*/
  struct module *owner;
  /*字段owner描述了模块的从属关系,指向拥有这个结构的模块的指针,这个描述符只有对编译为模块方式的驱动才是有意义的,一般赋值为“THIS_MODULE”。*/
  const struct file_operations *ops;
  /*字段ops描述了字符设备的操作函数指针,*/
  struct list_head list;
  /*字段list描述了与cdev对应的字符设备文件的inode->i_devices的链表的表头。*/
  dev_t dev;
  /*字段dev描述了字符设备的设备号,包括主设备号和次设备号。*/
  unsigned int count;
  /*字段count指定设备编号范围的大小。*/
};

添加一个cdev

函数cdev_add()用于向Linux内核系统中添加一个新的cdev结构体变量所描述的字符设备,并且使这个设备立即可用。

创建类和创建设备就不多说了。

总结

  1. struct file_operations是一个字符设备把驱动的操作和设备号联系在一起的纽带,是一系列指针的集合,每个被打开的文件都对应于一系列的操作,这就是file_operations,用来执行一系列的系统调用。
    struct file代表一个打开的文件,在执行file_operation中的open操作时被创建,这里需要注意的是与用户空间inode指针的区别,一个在内核,而file指针在用户空间,由c库来定义。
    struct inode被内核用来代表一个文件,注意和struct file的区别,struct inode一个是代表文件,struct file一个是代表打开的文件。
  2. 在Linux文件系统中,每个文件都用一个struct inode结构体来描述,这个结构体里面记录了这个文件的所有信息,例如:文件类型,访问权限等。
struct inode {
  umode_t     i_mode; 
  ...
  unsigned int    i_flags;
    .....
  dev_t     i_rdev;
  loff_t      i_size;
  ...
  struct list_head  i_devices;
  union {
    struct pipe_inode_info  *i_pipe;
    struct block_device *i_bdev;
    struct cdev   *i_cdev;
  };
  ...
};

struct inode描述的是文件的静态信息,即这些信息很少会改变。而struct file描述的是动态信息,即在对文件的操作的时候,struct file里面的信息经常会发生变化。典型的是struct file结构体里面的f_pos(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变

2. 在Linux操作系统中,每个驱动程序在应用层的/dev目录下都会有一个设备文件和它对应,并且该文件会有对应的主设备号和次设备号。

3. 在Linux操作系统中,每个驱动程序都要分配一个主设备号,字符设备的设备号保存在struct cdev结构体中。

  1. 在Linux操作系统中,每打开一次文件,Linux操作系统在VFS层都会分配一个struct file结构体来描述打开的这个文件。该结构体用于维护文件打开权限、文件指针偏移值、私有内存地址等信息。

    结构体之间的关系:

    如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。
  2. 当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备)。还会分配一个struct file结构体。
  3. 根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备有一个struct cdev结构体。此结构体描述了字符设备所有的信息,其中最重要一项的就是字符设备的操作函数接口。
  4. 找到struct cdev结构体后,Linux内核就会将struct cdev结构体所在的内存空间首地记录在struct inode结构体的i_cdev成员中。将struct cdev结构体的中记录的函数操作接口地址记录在struct file结构体的f_op成员中。
  5. 任务完成,VFS层会给应用层返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层的应用程序就可以通过fd来找到strut file,然后在由struct file找到操作字符设备的函数接口了。



目录
相关文章
|
11月前
|
Ubuntu Linux 编译器
字符驱动设备原理及其相关函数(一)
字符驱动设备原理及其相关函数(一)
79 0
|
11月前
|
Linux
字符设备驱动编程①
字符设备驱动编程①
118 0
|
11月前
|
Linux
字符驱动设备原理及其相关函数(二)
字符驱动设备原理及其相关函数(二)
40 0
|
11月前
字符设备的编写二
字符设备的编写二
46 0
|
11月前
|
Linux 编译器
Linux设备驱动---字符设备驱动接口函数
Linux设备驱动---字符设备驱动接口函数
102 1
|
索引
UVC 基础学习(3):基础描述符介绍
UVC 基础学习(3):基础描述符介绍
446 0
|
Linux C语言 SoC
设备树基本原理与操作方法
设备树基本原理与操作方法
140 0
设备树基本原理与操作方法
驱动开发:内核遍历进程VAD结构体
在上一篇文章`《驱动开发:内核中实现Dump进程转储》`中我们实现了ARK工具的转存功能,本篇文章继续以内存为出发点介绍`VAD`结构,该结构的全程是`Virtual Address Descriptor`即`虚拟地址描述符`,VAD是一个`AVL`自`平衡二叉树`,树的每一个节点代表一段虚拟地址空间。程序中的代码段,数据段,堆段都会各种占用一个或多个`VAD`节点,由一个`MMVAD`结构完整描述。
429 0
驱动开发:内核遍历进程VAD结构体
|
存储 Windows 容器
驱动开发:内核中的链表与结构体
Windows内核中是无法使用`vector`容器等数据结构的,当我们需要保存一个结构体数组时,就需要使用内核中提供的专用链表结构`LIST_ENTRY`通过一些列链表操作函数对结构体进行装入弹出等操作,如下代码是本人总结的内核中使用链表存储多个结构体的通用案例。
205 0
驱动开发:内核中的链表与结构体
|
Linux 编译器 芯片
嵌入式Linux开发: 编写EEPROM驱动_采用杂项字符设备框架
嵌入式Linux开发: 编写EEPROM驱动_采用杂项字符设备框架
284 0
嵌入式Linux开发: 编写EEPROM驱动_采用杂项字符设备框架