本文转载自:http://blog.csdn.net/zqixiao_09/article/details/50839042

一、字符设备基础知识

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结构体的定义如下:

  1. <include/linux/cdev.h>
  2. struct cdev {
  3. struct kobject kobj;                  //内嵌的内核对象.
  4. struct module *owner;                 //该字符设备所在的内核模块的对象指针.
  5. const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
  6. struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表.
  7. dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.
  8. unsigned int count;                   //隶属于同一主设备号的次设备号的个数.
  9. };

内核给出的操作struct cdev结构的接口主要有以下几个:

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

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

  1. void cdev_init(struct cdev *cdev, const struct file_operations *fops)
  2. {
  3. memset(cdev, 0, sizeof *cdev);
  4. INIT_LIST_HEAD(&cdev->list);
  5. kobject_init(&cdev->kobj, &ktype_cdev_default);
  6. cdev->ops = fops;
  7. }

 该函数主要对struct cdev结构体做初始化最重要的就是建立cdev 和 file_operations之间的连接:

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

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

(3) 初始化kobj成员;

(4) 初始化ops成员;

 b --struct cdev *cdev_alloc(void);

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

其源代码清单如下:

  1. struct cdev *cdev_alloc(void)
  2. {
  3. struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
  4. if (p) {
  5. INIT_LIST_HEAD(&p->list);
  6. kobject_init(&p->kobj, &ktype_cdev_dynamic);
  7. }
  8. return p;
  9. }

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

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

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

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

(1)第一个设备号 dev,

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

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

 

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

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

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

(1) struct file_operations结构指针;

(2) dev设备号;

(3) count次设备号个数。

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

 

三、设备号相应操作

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

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

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

  typedef u_long dev_t;

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

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

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

MAJOR(dev_t dev);                             

MINOR(dev_t dev);

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

MKDEV(int major,int minor);

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

  1. //宏定义:
  2. #define MINORBITS    20
  3. #define MINORMASK    ((1U << MINORBITS) - 1)
  4. #define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))
  5. #define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))
  6. #define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))</span>

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

a -- 静态申请

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

其源代码清单如下:

  1. int register_chrdev_region(dev_t from, unsigned count, const char *name)
  2. {
  3. struct char_device_struct *cd;
  4. dev_t to = from + count;
  5. dev_t n, next;
  6. for (n = from; n < to; n = next) {
  7. next = MKDEV(MAJOR(n)+1, 0);
  8. if (next > to)
  9. next = to;
  10. cd = __register_chrdev_region(MAJOR(n), MINOR(n),
  11. next - n, name);
  12. if (IS_ERR(cd))
  13. goto fail;
  14. }
  15. return 0;
  16. fail:
  17. to = n;
  18. for (n = from; n < to; n = next) {
  19. next = MKDEV(MAJOR(n)+1, 0);
  20. kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
  21. }
  22. return PTR_ERR(cd);
  23. }

b -- 动态分配:

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

其源代码清单如下:

  1. int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
  2. const char *name)
  3. {
  4. struct char_device_struct *cd;
  5. cd = __register_chrdev_region(0, baseminor, count, name);
  6. if (IS_ERR(cd))
  7. return PTR_ERR(cd);
  8. *dev = MKDEV(cd->major, cd->baseminor);
  9. return 0;
  10. }

可以看到二者都是调用了__register_chrdev_region 函数,其源代码如下:

  1. static struct char_device_struct *
  2. __register_chrdev_region(unsigned int major, unsigned int baseminor,
  3. int minorct, const char *name)
  4. {
  5. struct char_device_struct *cd, **cp;
  6. int ret = 0;
  7. int i;
  8. cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
  9. if (cd == NULL)
  10. return ERR_PTR(-ENOMEM);
  11. mutex_lock(&chrdevs_lock);
  12. /* temporary */
  13. if (major == 0) {
  14. for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
  15. if (chrdevs[i] == NULL)
  16. break;
  17. }
  18. if (i == 0) {
  19. ret = -EBUSY;
  20. goto out;
  21. }
  22. major = i;
  23. ret = major;
  24. }
  25. cd->major = major;
  26. cd->baseminor = baseminor;
  27. cd->minorct = minorct;
  28. strlcpy(cd->name, name, sizeof(cd->name));
  29. i = major_to_index(major);
  30. for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
  31. if ((*cp)->major > major ||
  32. ((*cp)->major == major &&
  33. (((*cp)->baseminor >= baseminor) ||
  34. ((*cp)->baseminor + (*cp)->minorct > baseminor))))
  35. break;
  36. /* Check for overlapping minor ranges.  */
  37. if (*cp && (*cp)->major == major) {
  38. int old_min = (*cp)->baseminor;
  39. int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
  40. int new_min = baseminor;
  41. int new_max = baseminor + minorct - 1;
  42. /* New driver overlaps from the left.  */
  43. if (new_max >= old_min && new_max <= old_max) {
  44. ret = -EBUSY;
  45. goto out;
  46. }
  47. /* New driver overlaps from the right.  */
  48. if (new_min <= old_max && new_min >= old_min) {
  49. ret = -EBUSY;
  50. goto out;
  51. }
  52. }
  53. cd->next = *cp;
  54. *cp = cd;
  55. mutex_unlock(&chrdevs_lock);
  56. return cd;
  57. out:
  58. mutex_unlock(&chrdevs_lock);
  59. kfree(cd);
  60. return ERR_PTR(ret);
  61. }

通过这个函数可以看出 register_chrdev_region alloc_chrdev_region 的区别,register_chrdev_region直接将Major 注册进入,而 alloc_chrdev_region从Major = 0 开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去;

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

                                     register_chrdev_region                                                alloc_chrdev_region 

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

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

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

  1. static inline int register_chrdev(unsigned int major, const char *name,
  2. const struct file_operations *fops)
  3. {
  4. return __register_chrdev(major, 0, 256, name, fops);
  5. }

调用了 __register_chrdev(major, 0, 256, name, fops) 函数:

  1. int __register_chrdev(unsigned int major, unsigned int baseminor,
  2. unsigned int count, const char *name,
  3. const struct file_operations *fops)
  4. {
  5. struct char_device_struct *cd;
  6. struct cdev *cdev;
  7. int err = -ENOMEM;
  8. cd = __register_chrdev_region(major, baseminor, count, name);
  9. if (IS_ERR(cd))
  10. return PTR_ERR(cd);
  11. cdev = cdev_alloc();
  12. if (!cdev)
  13. goto out2;
  14. cdev->owner = fops->owner;
  15. cdev->ops = fops;
  16. kobject_set_name(&cdev->kobj, "%s", name);
  17. err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
  18. if (err)
  19. goto out;
  20. cd->cdev = cdev;
  21. return major ? 0 : cd->major;
  22. out:
  23. kobject_put(&cdev->kobj);
  24. out2:
  25. kfree(__unregister_chrdev_region(cd->major, baseminor, count));
  26. return err;
  27. }

可以看到这个函数不只帮我们注册了设备号,还帮我们做了cdev 的初始化以及cdev 的注册;

3、注销设备号:

void unregister_chrdev_region(dev_t from, unsigned count);

4、创建设备文件:

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

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

2)自动创建设备节点:

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

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

 

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

hello.c

  1. #include <linux/module.h>
  2. #include <linux/fs.h>
  3. #include <linux/cdev.h>
  4. static int major = 250;
  5. static int minor = 0;
  6. static dev_t devno;
  7. static struct cdev cdev;
  8. static int hello_open (struct inode *inode, struct file *filep)
  9. {
  10. printk("hello_open \n");
  11. return 0;
  12. }
  13. static struct file_operations hello_ops=
  14. {
  15. .open = hello_open,
  16. };
  17. static int hello_init(void)
  18. {
  19. int ret;
  20. printk("hello_init");
  21. devno = MKDEV(major,minor);
  22. ret = register_chrdev_region(devno, 1, "hello");
  23. if(ret < 0)
  24. {
  25. printk("register_chrdev_region fail \n");
  26. return ret;
  27. }
  28. cdev_init(&cdev,&hello_ops);
  29. ret = cdev_add(&cdev,devno,1);
  30. if(ret < 0)
  31. {
  32. printk("cdev_add fail \n");
  33. return ret;
  34. }
  35. return 0;
  36. }
  37. static void hello_exit(void)
  38. {
  39. cdev_del(&cdev);
  40. unregister_chrdev_region(devno,1);
  41. printk("hello_exit \n");
  42. }
  43. MODULE_LICENSE("GPL");
  44. module_init(hello_init);
  45. module_exit(hello_exit);

测试程序 test.c

  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <stdio.h>
  5. main()
  6. {
  7. int fd;
  8. fd = open("/dev/hello",O_RDWR);
  9. if(fd<0)
  10. {
  11. perror("open fail \n");
  12. return ;
  13. }
  14. close(fd);
  15. }

makefile:

  1. ifneq  ($(KERNELRELEASE),)
  2. obj-m:=hello.o
  3. $(info "2nd")
  4. else
  5. KDIR := /lib/modules/$(shell uname -r)/build
  6. PWD:=$(shell pwd)
  7. all:
  8. $(info "1st")
  9. make -C $(KDIR) M=$(PWD) modules
  10. clean:
  11. rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order
  12. endif

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

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

Linux字符设备驱动结构(一)--cdev结构体、设备号相关知识机械【转】的更多相关文章

  1. linux设备驱动归纳总结(一)内核的相关基础概念【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-59413.html linux设备驱动归纳总结(一):内核的相关基础概念 xxxxxxxxxxxxxx ...

  2. linux设备驱动那点事儿之平台设备理论篇

    一:Platform总线 1.1概述 一个现实的linux设备驱动通常需要挂接在一种总线上,对于本身依附于PCI,USB,IIC,SPI等的设备而言,这自然不是问题,但是在嵌入式系统里面,SOC系统中 ...

  3. Linux驱动设计——字符设备驱动(一)

    Linux字符设别驱动结构 cdev结构体 struct cdev { struct kobject kobj; struct module *owner; const struct file_ope ...

  4. 深入浅出:Linux设备驱动之字符设备驱

    一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流 ...

  5. Linux字符设备驱动

    一.字符设备基础 字符设备 二.字符设备驱动与用户空间访问该设备的程序三者之间的关系 三.字符设备模型 1.Linux内核中,使用 struct cdev 来描述一个字符设备 动态申请(构造)cdev ...

  6. 【转】深入浅出:Linux设备驱动之字符设备驱动

    深入浅出:Linux设备驱动之字符设备驱动 一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据 ...

  7. 蜕变成蝶~Linux设备驱动之字符设备驱动

    一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流 ...

  8. 深入浅出:Linux设备驱动之字符设备驱动

    一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流 ...

  9. linux设备驱动第三篇:如何实现一个简单的字符设备驱动

    在linux设备驱动第一篇:设备驱动程序简介中简单介绍了字符驱动,本篇简单介绍如何写一个简单的字符设备驱动.本篇借鉴LDD中的源码,实现一个与硬件设备无关的字符设备驱动,仅仅操作从内核中分配的一些内存 ...

随机推荐

  1. 利用utl_file来读取文件.

    以前写过用external table来加载trace文件,详情参考下面链接. http://www.cnblogs.com/princessd8251/p/3779145.html 今天要做到是用U ...

  2. [BS-18] 对OC中不可变类的理解

    对OC中不可变类的理解 OC中存在很多不可变的类(如NSString,NSAttributedString,NSArray,NSDictionary,NSSet等),用它们创建的对象存在于堆内存中,但 ...

  3. 第四篇 SQL Server安全权限

    本篇文章是SQL Server安全系列的第四篇,详细内容请参考原文. 权限授予主体访问对象,以执行某些操作.SQL Server有大量你可以授予给主体的权限,你甚至可以拒绝或回收权限.这听起来有点复杂 ...

  4. .NET Framework Execution Was Aborted By Escalation Policy

    错误#1 09:31 2015/1/26上班查看ERRORLOG发现下面错误信息字面上理解是有内存压力,中午的时候ERRORLOG频繁报下面错误问题核实,一台服务器上安装两个实例,其中一个实例设置了最 ...

  5. mysql及redis环境部署时遇到的问题解决

    redis开启远程访问redis默认只允许本地访问,要使redis可以远程访问可以修改redis.conf打开redis.conf文件在NETWORK部分有说明 解决办法:注释掉bind 127.0. ...

  6. MVC 登录认证与授权及读取登录错误码

    最近悟出来一个道理,在这儿分享给大家:学历代表你的过去,能力代表你的现在,学习代表你的将来. 十年河东十年河西,莫欺少年穷 学无止境,精益求精    最近在自学MVC,遇到的问题很多,索性一点点总结下 ...

  7. update kernel

    1,version 2,command First, verify the current kernel version: $ uname -r 2.6.32-358.el6.x86_64 Befor ...

  8. MyCat:在.NET Core中使用MyCat

    http://www.cnblogs.com/yuangang/p/5830716.html

  9. C++Primer 第十章

    //1.标准库算法不仅可以应用于容器,还可以应用于内置数组,指针. //2.大多数算法都定义在头文件algorithm中.标准库还在头文件numeric中定义了一组数值泛型算法. //3.算法本身不会 ...

  10. SQL循环添加表中的字段

    USE BookDiscuss                   DECLARE @num INT  DECLARE @numdiff INT  DECLARE @table  VARCHAR(50 ...