字符设备驱动的几种写法

随着linux内核的更新换代和计算机硬件的不断增多,字符设备驱动在不同内核版本下也呈现出了几种不同的写法,本文将具体随着linux发展的脚步详述字符设备驱动写法的更新。

在Linux2.4以前,内核中所有已分配的字符设备编号都记录在一个名为 chrdevs ,元素个数为255的散列表里。该散列表中的每一个元素是一个 char_device_struct 结构,代表主设备号相同的一组设备。它在内核中的定义如下:

static struct char_device_struct {
       struct char_device_struct *next;    // 指向散列表中的下一个指针
       unsigned int major;                 // 主设备号
       unsigned int baseminor;             // 起始次设备号
       int minorct;                        // 设备编号数
       char name[64];                      // 设备驱动名
       struct file_operations *fops;       // 指向该设备对应的文件操作函数结构体指针
       struct cdev *cdev;                  // 指向字符设备驱动程序描述符的指针
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev()register_chrdev_region()alloc_chrdev_region() 。这三个函数都会调用一个共用的 __register_chrdev_region()函数来注册一组设备编号范围(即一个 char_device_struct 结构)。

1. __register_chrdev_region()

首先来看一下__register_chrdev_region函数的定义:

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 ret = 0;
   int i;
   cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
   if (cd == NULL)
       return ERR_PTR(-ENOMEM);
   mutex_lock(&chrdevs_lock);
   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;
   strncpy(cd->name,name, 64);
   i = major_to_index(major);
   for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
       if ((*cp)->major > major ||
            ((*cp)->major == major && 
            ( ((*cp)->baseminor >= baseminor) || ((*cp)->baseminor + (*cp)->minorct > baseminor)) ))
           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;

       /* 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;
   mutex_unlock(&chrdevs_lock);
   return cd;
out:
   mutex_unlock(&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 结构的地址。

2. register_chrdev()

在Linux2.4内核以前使用的是这种分配设备编号范围的函数。它每次都是粗粒度的分配一个单独主设备号和 0 ~ 255 的次设备号范围(如果申请的主设备号为 0 则动态分配一个)。该函数内部自动分配了一个新的 cdev 结构,我们另外还需传入一个 file_operations 结构的指针,用来和新建的char_device_struct结构体绑定,以后凡是相同主设备号(即所有256个共享该主设备号的次设备号设备)的设备均使用同一个file_operations,不管你实际使用了几个次设备号,默认都会将相应主设备号下的256个次设备号连续注册,造成了极大的浪费。其定义如下:

int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
{
   struct char_device_struct *cd;
   struct cdev *cdev;
   char *s;
   int err = -ENOMEM;

   cd = __register_chrdev_region(major, 0, 256, name);
   if (IS_ERR(cd))
       return PTR_ERR(cd);

   cdev = cdev_alloc();
   if (!cdev)
       goto out2;

   cdev->owner = fops->owner;
   cdev->ops = fops;
   kobject_set_name(&cdev->kobj, "%s", name);
   for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/'))
       *s = '!';

   err = cdev_add(cdev, MKDEV(cd->major, 0), 256);
   if (err)
       goto out;

   cd->cdev = cdev;

   return major ? 0 : cd->major;
out:
   kobject_put(&cdev->kobj);
out2:
   kfree(__unregister_chrdev_region(cd->major, 0, 256));
   return err;
}
  • 其对应的设备注销函数为unregister_chrdev(),内部调用了 __unregister_chrdev_region()函数.
  • 为了避免资源的浪费,在linux2.4以后内核采用了register_chrdev_region或者alloc_chrdev_region函数来分配和注册字符设备。

3. register_chrdev_region()

看到region这个词相比大家应该知道,这是分配一个域,即指定的一个次设备号范围。当我们使用自定义的主设备号和次设备号范围时,使用该函数。

如果申请的设备编号范围跨越了主设备号,它会把分配范围内的编号按主设备号分割成较小的子范围,并在每个子范围上调用 __register_chrdev_region() 。如果其中有一次分配失败的话,那会把之前成功分配的都全部退回。具体请看其实现代码:

int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
   struct char_device_struct *cd;
   dev_t to = from + count;
   dev_t n, next;

   for (n = from; n < to; n = next) {
       next = MKDEV(MAJOR(n)+1, 0);
       if (next > to)
           next = to;
       cd = __register_chrdev_region(MAJOR(n), MINOR(n), next - n, name);
       if (IS_ERR(cd))
           goto 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);
}

/*
from: 注册的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0

count:需要连续注册的次设备编号个数,比如: 起始次设备号为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations操作方法结构体上

*name:字符设备名称
*/
  • 其对应的设备注销函数为unregister_chrdev_region(),内部也同样调用了 __unregister_chrdev_region()函数.

4. alloc_chrdev_region()

该函数用于动态申请设备编号范围,即我们事先不指定主设备号的情况下使用。它通过dev指针参数返回实际获得的起始设备编号。

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;
}

/*
*dev: 存放起始设备编号的指针,当注册成功, *dev就会等于分配到的起始设备编号,可以通过MAJOR()和MINNOR()函数来提取主次设备号

baseminor:次设备号基地址,也就是起始次设备号

count:需要连续注册的次设备编号个数,比如: 起始次设备号(baseminor)为0,baseminor=2,表示0~1的此设备号都要绑定在同一个file_operations操作方法结构体上

*name:字符设备名称
*/
  • 其对应的设备注销函数也为unregister_chrdev_region(),内部也同样调用了 __unregister_chrdev_region()函数.

5. 后续工作

static struct cdev hello1_cdev; 		//定义字符设备

cdev_init(&hello1_cdev, &hello1_fops);	//为字符设备绑定文件操作符

cdev_add(&hello1_cdev, MKDEV(major,0), 2); //为字符设备绑定设备号

cls=class_create(THIS_MODULE, "hello");		//创建一种类,即创建/proc/class/hello
     
class_device_create(cls,0, MKDEV(major,0), 0, "hello0");	//创建字符设备节点/dev/hello0

一般字符设备的驱动需要以下几个步骤:

为了更好的说明字符设备的编写步骤,下面举一个具体的程序,即在同一主设备号下创建两个设备域,并分别与不同的文件操作函数绑定。

/*
 *创建两个字符设备,他们公用同一个主设备号;
 *但次设备号0~1对应第一个字符设备,使用hello1_fops文件操作符;
 * 次设备号2~3对应第二个字符设备,使用hello2_fops文件操作符;
 * 次设备号4不对应字符设备,不使用文件操作符;
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/irq.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/list.h>
#include <linux/cdev.h>

static int hello_fops1_open(struct inode *inode, struct file *file)
{
    printk("open_hello1\n");
    return 0;
}

 

static int hello_fops2_open (struct inode *inode, struct file *file)
{
    printk("open_hello2\n");
    return 0;
}

 
  /*  操作结构体1   */
static struct file_operations hello1_fops={
        .owner=THIS_MODULE,
        .open =hello_fops1_open,
};

  /*  操作结构体2   */
static struct file_operations hello2_fops={
        .owner=THIS_MODULE,
        .open =hello_fops2_open,
};

 
static int major;                                 //主设备
static struct cdev hello1_cdev;        //保存 hello1_fops操作结构体的字符设备 
static struct cdev hello2_cdev;         //保存 hello2_fops操作结构体的字符设备 
static struct class *cls;

static int chrdev_region_init(void)
{
     dev_t  devid;  
#if 0
    major = register_chrdev(0,"hello",&hello_fops);	//以前采用这种形式
#else
	 if(major)
     {
         devid = MKDEV(major,0);
         register_chrdev_region(devid, 2, "hello");	   
     }else
     {
     	 alloc_chrdev_region(&devid, 0, 2,"hello");    //动态分配字符设备
     	 major=MAJOR(devid);
     }    
     cdev_init(&hello1_cdev, &hello1_fops);		//初始化cdev,绑定fops结构体
     cdev_add(&hello1_cdev, MKDEV(major,0), 2); //注册cdev,即绑定(major,0~1)
	 
     devid = MKDEV(major,2);
	 register_chrdev_region(devid, 2, "hello2");	
     cdev_init(&hello2_cdev, &hello2_fops);
     cdev_add(&hello2_cdev,devid, 2);  //注册cdev,即绑定(major,2~3)
#endif

     cls=class_create(THIS_MODULE, "hello");
     /*创建字符设备节点*/
     class_device_create(cls,0, MKDEV(major,0), 0, "hello0");   //对应hello_fops1操作结构体
     class_device_create(cls,0, MKDEV(major,1), 0, "hello1");   //对应hello_fops1操作结构体
     class_device_create(cls,0, MKDEV(major,2), 0, "hello2");   //对应hello_fops2操作结构体
     class_device_create(cls,0, MKDEV(major,3), 0, "hello3");   //对应hello_fops2操作结构体
     class_device_create(cls,0, MKDEV(major,4), 0, "hello4");   //对应空
     
     return 0;
}

void chrdev_region_exit(void)
{
   class_device_destroy(cls, MKDEV(major,4));
   class_device_destroy(cls, MKDEV(major,3));
   class_device_destroy(cls, MKDEV(major,2));
   class_device_destroy(cls, MKDEV(major,1));
   class_device_destroy(cls, MKDEV(major,0));

   class_destroy(cls);


   cdev_del(&hello1_cdev);   
   unregister_chrdev_region(MKDEV(major,0), 2);     //注销(major,0)~(major,1)
    
   cdev_del(&hello2_cdev); 
   unregister_chrdev_region(MKDEV(major,2), 2);     //注销(major,2)~(major,3)
} 

module_init(chrdev_region_init);
module_exit(chrdev_region_exit);
MODULE_LICENSE("GPL");

针对以上驱动,编写如下测试程序:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void print_useg(char arg[])    //打印使用帮助信息
{
         printf("useg:  \n");
         printf("%s   [dev]\n",arg);
}

int main(int argc,char **argv)
{

  int fd;
  if(argc!=2)
    {
        print_useg(argv[0]);
        return -1;
    }

  fd=open(argv[1],O_RDWR);
  if(fd<0)
      printf("can't open %s \n",argv[1]);
  else
      printf("can open %s \n",argv[1]);
  return 0;
}

装载驱动后,进行测试,得到如下结果:

# ls /dev/hello* -l
crw-rw----	1 0		0		252,	0 Jan	1 00:12  /dev/hello0
crw-rw----	1 0		0		252,	1 Jan	1 00:12  /dev/hello1
crw-rw----	1 0		0		252,	2 Jan	1 00:12  /dev/hello2
crw-rw----	1 0		0		252,	3 Jan	1 00:12  /dev/hello3
crw-rw----	1 0		0		252,	4 Jan	1 00:12  /dev/hello4

#./a.out /dev/hello0	//打开/dev/hello0时,调用的是hello1_fops里的.open()
open hello0
#
#
#./a.out /dev/hello2  //打开/dev/hello2时,调用的是hello1_fops里的.open()
open hello2
#
#
#./a.out /dev/hello4  //打开无效,因为在驱动代码里没有分配次设备号4的操作结构体
can't open /dev/hello4
#
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leon_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值