首页 > 学院 > 开发设计 > 正文

Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析

2019-11-10 18:34:16
字体:
来源:转载
供稿:网友

一、字符设备基础知识

1、设备驱动分类

      linux系统将设备分为3类:字符设备、块设备、网络设备。使用驱动程序:

字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。

块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。

每一个字符设备或块设备都在/dev目录下对应一个设备文件linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备

2、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系

     如图,在Linux内核中:

a -- 使用cdev结构体来描述字符设备;

b -- 通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性;

c -- 通过其成员file_Operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;

     在Linux字符设备驱动中:

a -- 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;

b -- 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;

c -- 模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;

     用户空间访问该设备的程序:

a -- 通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数;

3、字符设备驱动模型

二、cdev 结构体解析

      在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:

[cpp] view plain copy 在CODE上查看代码片<include/linux/cdev.h>    struct cdev {       struct kobject kobj;                  //内嵌的内核对象.      struct module *owner;                 //该字符设备所在的内核模块的对象指针.      const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.      struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表.      dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.      unsigned int count;                   //隶属于同一主设备号的次设备号的个数.  };  内核给出的操作struct%20cdev结构的接口主要有以下几个:

a%20--%20void%20cdev_init(struct%20cdev%20*,%20const%20struct%20file_operations%20*);

其源代码如代码清单如下:

[cpp]%20view%20plain%20copy%20void cdev_init(struct cdev *cdev, const struct file_operations *fops)  {      memset(cdev, 0, sizeof *cdev);      INIT_LIST_HEAD(&cdev->list);      kobject_init(&cdev->kobj, &ktype_cdev_default);      cdev->ops = fops;  }   %20 %20 %20该函数主要对struct%20cdev结构体做初始化最重要的就是建立cdev%20和%20file_operations之间的连接:

(1)%20将整个结构体清零;

(2)%20初始化list成员使其指向自身;

(3)%20初始化kobj成员;

(4)%20初始化ops成员;

 b%20--struct%20cdev%20*cdev_alloc(void);

 %20 %20 该函数主要分配一个struct%20cdev结构动态申请一个cdev内存,并做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在调用cdev_alloc后,显式的做初始化即:%20.ops=xxx_ops).

其源代码清单如下:

[cpp]%20view%20plain%20copy%20struct cdev *cdev_alloc(void)  {      struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);      if (p) {          INIT_LIST_HEAD(&p->list);          kobject_init(&p->kobj, &ktype_cdev_dynamic);      }      return p;  }  

 %20 %20 在上面的两个初始化的函数中,我们没有看到关于owner成员、dev成员、count成员的初始化;其实,owner成员的存在体现了驱动程序与内核模块间的亲密关系,struct%20module是内核对于一个模块的抽象,该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式的初始化%20.owner%20=%20THIS_MODULE,%20该成员可以防止设备的方法正在被使用时,设备所在模块被卸载。而dev成员和count成员则在cdev_add中才会赋上有效的值。

 c%20--%20int%20cdev_add(struct%20cdev%20*p,%20dev_t%20dev,%20unsigned%20count);

 %20 %20 %20 该函数向内核注册一个struct%20cdev结构,即正式通知内核由struct%20cdev%20*p代表的字符设备已经可以使用了。

当然这里还需提供两个参数:

(1)第一个设备号%20dev,

(2)和该设备关联的设备编号的数量。

这两个参数直接赋值给struct%20cdev%20的dev成员和count成员。

d%20--%20void%20cdev_del(struct%20cdev%20*p);

 %20 %20 该函数向内核注销一个struct%20cdev结构,即正式通知内核由struct%20cdev%20*p代表的字符设备已经不可以使用了。

 %20 %20 从上述的接口讨论中,我们发现对于struct%20cdev的初始化和注册的过程中,我们需要提供几个东西

(1)%20struct%20file_operations结构指针;

(2)%20dev设备号;

(3)%20count次设备号个数。

但是我们依旧不明白这几个值到底代表着什么,而我们又该如何去构造这些值!

三、设备号相应操作

1%20--%20主设备号和次设备号(二者一起为设备号):

 %20 %20 %20一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

  linux内核中,设备号用dev_t来描述,2.6.28中定义如下:

  typedef%20u_long%20dev_t;

  在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。

内核也为我们提供了几个方便操作的宏实现dev_t:

1)%20-- 从设备号中提取major和minor

MAJOR(dev_t%20dev);                              

MINOR(dev_t%20dev);

2)%20-- 通过major和minor构建设备号

MKDEV(int%20major,int%20minor);

注:这只是构建设备号。并未注册,需要调用 register_chrdev_region 静态申请;

[cpp]%20view%20plain%20copy%20//宏定义:  #define MINORBITS    20  #define MINORMASK    ((1U << MINORBITS) - 1)  #define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))  #define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))  #define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))</span>  

2、分配设备号(两种方法):

a%20--%20静态申请

int%20register_chrdev_region(dev_t%20from,%20unsigned%20count,%20const%20char%20*name);

其源代码清单如下:

[cpp]%20view%20plain%20copy%20int 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);  }  b%20--%20动态分配:

int%20alloc_chrdev_region(dev_t%20*dev,%20unsigned%20baseminor,%20unsigned%20count,%20const%20char%20*name);

其源代码清单如下:

[cpp]%20view%20plain%20copy%20int 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%20函数,其源代码如下:

[cpp]%20view%20plain%20copy%20static 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);        /* 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;      strlcpy(cd->name, name, sizeof(cd->name));        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 alloc_chrdev_region%20的区别,register_chrdev_region直接将Major%20注册进入,而 alloc_chrdev_region从Major%20=%200%20开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去;

二者应用可以简单总结如下:

 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 register_chrdev_region  %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 alloc_chrdev_region 

   %20devno = MKDEV(major,minor);    ret = register_chrdev_region(devno, 1, "hello");     cdev_init(&cdev,&hello_ops);    ret = cdev_add(&cdev,devno,1); %20  alloc_chrdev_region(&devno, minor, 1, "hello"); %20 %20major = MAJOR(devno); %20 %20cdev_init(&cdev,&hello_ops); %20 %20ret = cdev_add(&cdev,devno,1)register_chrdev(major,"hello",&hello

 %20 %20 可以看到,除了前面两个函数,还加了一个register_chrdev%20函数,可以发现这个函数的应用非常简单,只要一句就可以搞定前面函数所做之事;

下面分析一下register_chrdev%20函数,其源代码定义如下:

[cpp]%20view%20plain%20copy%20static inline int register_chrdev(unsigned int major, const char *name,                    const struct file_operations *fops)  {      return __register_chrdev(major, 0, 256, name, fops);  }  调用了 __register_chrdev(major,%200,%20256,%20name,%20fops)%20函数:[cpp]%20view%20plain%20copy%20int __register_chrdev(unsigned int major, unsigned int baseminor,                unsigned int count, const char *name,                const struct file_operations *fops)  {      struct char_device_struct *cd;      struct cdev *cdev;      int err = -ENOMEM;        cd = __register_chrdev_region(major, baseminor, count, 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);        err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);      if (err)          goto out;        cd->cdev = cdev;        return major ? 0 : cd->major;  out:      kobject_put(&cdev->kobj);  out2:      kfree(__unregister_chrdev_region(cd->major, baseminor, count));      return err;  }  可以看到这个函数不只帮我们注册了设备号,还帮我们做了cdev%20的初始化以及cdev%20的注册;

3、注销设备号:

void%20unregister_chrdev_region(dev_t%20from,%20unsigned%20count);

4、创建设备文件:

 %20 %20 利用cat%20/PRoc/devices查看申请到的设备名,设备号。

1)使用mknod手工创建:mknod%20filename%20type%20major%20minor

2)自动创建设备节点:

 %20 %20利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

 %20 %20详细解析见:Linux%20字符设备驱动开发%20(二)——%20自动创建设备节点

下面看一个实例,练习一下上面的操作:

hello.c

[cpp]%20view%20plain%20copy%20#include <linux/module.h>  #include <linux/fs.h>  #include <linux/cdev.h>  static int major = 250;  static int minor = 0;  static dev_t devno;  static struct cdev cdev;  static int hello_open (struct inode *inode, struct file *filep)  {      printk("hello_open /n");      return 0;  }  static struct file_operations hello_ops=  {      .open = hello_open,           };    static int hello_init(void)  {      int ret;          printk("hello_init");      devno = MKDEV(major,minor);      ret = register_chrdev_region(devno, 1, "hello");      if(ret < 0)      {          printk("register_chrdev_region fail /n");          return ret;      }      cdev_init(&cdev,&hello_ops);      ret = cdev_add(&cdev,devno,1);      if(ret < 0)      {          printk("cdev_add fail /n");          return ret;      }         return 0;  }  static void hello_exit(void)  {      cdev_del(&cdev);      unregister_chrdev_region(devno,1);      printk("hello_exit /n");  }  MODULE_LICENSE("GPL");  module_init(hello_init);  module_exit(hello_exit);  

测试程序%20test.c

[cpp]%20view%20plain%20copy%20#include <sys/types.h>  #include <sys/stat.h>  #include <fcntl.h>  #include <stdio.h>    main()  {      int fd;        fd = open("/dev/hello",O_RDWR);      if(fd<0)      {          perror("open fail /n");          return ;      }        close(fd);  }  makefile:[cpp]%20view%20plain%20copy%20派生到我的代码片ifneq  ($(KERNELRELEASE),)  obj-m:=hello.o  $(info "2nd")  else  KDIR := /lib/modules/$(shell uname -r)/build  PWD:=$(shell pwd)  all:      $(info "1st")      make -C $(KDIR) M=$(PWD) modules  clean:      rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order  endif  

编译成功后,使用 insmod 命令加载:

然后用cat /proc/devices 查看,会发现设备号已经申请成功;


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表