字符设备驱动——申请设备号、注册字符设备

缺乏、安全感 2021-10-29 19:26 660阅读 0赞

1. 设备号

  1. 主设备号:用来标识与设备文件相关的驱动程序, ——反应设备类型
  2. 次设备号:为内核所用,被驱动程序用来辨别操作那个设备文件 ——区分同类型的具体某个设备

1.1 设备号的内部表达

  1. 在内核中,保存设备号(包括主设备号和此设备好)使用类型
  2. dev\_t (<linux/types.h>)
  3. 这是一个unsigned int 是一个32位的无符号整型。。
  4. 主设备号——高12
  5. 此设备号——低12
  6. 我们可以使用宏来取一个设备号(dev)的主设备号和此设备号
  7. 定义在 <linux/kdev\_t.h>
  8. MAJOR(dev\_t dev) 取主设备号
  9. MINOR(dev\_t dev) 取次设备号
  10. 也可以将主次设备号合成一个完整的dev\_t类型的设备号
  11. MKDEV(int major, int minor) 将主次设备号转换成dev\_t

1.2 分配主次设备号

  1. linux可以采用静态申请和动态申请两种方法来分配主次设备号
  2. 静态申请
  3. 1. 根据Documentation/devices.txt, 确定一个没有使用的主设备号
  4. 2. 使用register\_chrdev\_region(dev\_t first, unsigned int count, char \*name)
  5. 定义在<linux/fs.h>
  6. count 为所请求的连续设备编号个数, 如果count过大,可以会各下一个主设备号重叠。
  7. name 为设备名, 注册后 出现在/proc/devicessysfs
  8. 动态分配
  9. 作为一个新的驱动程序,应该使用动态分配机制获取主设备号
  10. alloc\_chrdev\_region(dev\_t \*dev, unsigned int first, unsigned int count, char \*name)
  11. 不管用何种方法分配, 不用时都要释放掉
  12. void unregister\_chrdev\_region(dev\_t first, unsigned int count);
  13. 静态申请与动态申请的优缺点:
  14. 静态申请——简单(优); 一旦驱动程序被广泛命使用, 随机选定的主设备号可以造成冲突,使驱动程序无法注册。(劣)
  15. 动态申请——简单,易于驱动推广(优);无法在驱动安装前创建设备文件, 因为不能保证分配的主设备号始终一致。(劣)

2 创建设备文件

  1. 设备文件的创建有
  2. 1. 使用mknod命令手工创建
  3. 2. 自动创建
  4. 两种方法。

2.1 mknod手工创建

  1. mknod 用法:
  2. mknod filename type major minor
  3. filename : 设备文件名
  4. type : 设备文件类型
  5. major : 主设备号
  6. minor : 次设备号

2.2 自动创建

  1. 如果我们在驱动里面动态创建的话需要这样做
  2. struct class *cdev_class;
  3. cdev_class = class_create(owner,name)
  4. device_create(_cls,_parent,_devt,_device,_fmt)

2.3 模块退出时要销毁设备文件

  1. device_destroy(_cls,_device)
  2. class_destroy(struct class * cls)

3. 一些重要的结构体

  1. 大部分的基础性的驱动操作包括 3 个重要的内核数据结构, 称为 file\_operations, file, inode.
  • 文件结构 struct file

    1. 定义于<linux/fs.h>, 是一个内核结构, 不会出现在用户空间

代表一个打开的文件。系统中每个找开的文件在内核空间一个关联的

struct file, 它由内核在打开文件时创建, 在文件关闭后释放

重要成员

  1. loff_t f_ops /* 文件读写位置 */
  2. struct file_operations *f_op /* 文件关联的操作 */
  3. mode_t f_mode /* 模式确定文件可读或者可写 */
  4. unsigned int f_flags /* 文件标志,一般用来判断是否请求非阻塞操 作, 标志定义<linux/fcntl.h> */
  5. void *private_data;
  6. open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, release 方法中释放那个内存. private\_data是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.
  • 文件操作 struct file_operation

file_operation 结构是一个字符驱动如何建立这个连接. 这个结构, 定

义在 , 是一个函数指针的集合. 每个打开文件(内部用一个 file 结构来代

表, 稍后我们会查看)与它自身的函数集合相关连( 通过包含一个称为 f_op 的成员, 它指

向一个 file_operations 结构). 这些操作大部分负责实现系统调用, 因此, 命名为 open,

read, 等等. 我们可以认为文件是一个”对象”并且其上的函数操作称为它的”方法”, 使用

面向对象编程的术语来表示一个对象声明的用来操作对象的动作.

下面是一个file_operationd的声明:

  1. struct file_operations my_fops = {
  2. .owner = THIS_MODULE,
  3. .llseek = my_llseek,
  4. .read = my_read,
  5. .write = my_write,
  6. .ioctl = my_ioctl,
  7. .open = my_open,
  8. .release = my_release,
  9. };

该声明使用标准的 C 标记式结构初始化语法. 这个语法是首选的, 因为它使驱动在结构定义的改变之间更加可移植 。

下面列出file_operationd部分成员的含义:(其他成员自行百度)

struct module *owner

  1. 第一个 file\_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS\_MODULE, 一个在 <linux/module.h> 中定义的宏.

loff_t (*llseek) (struct file *, loff_t, int);

llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个”long offset”, 并且就算在 32 位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在”file 结构” 一节中描述).

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

  1. 用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -
  2. EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

  1. 发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
  1. ** int (\*open) (struct inode \*, struct file \*); **
  1. 尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知

int (*release) (struct inode *, struct file *);

  1. 在文件结构被释放时引用这个操作. 相当于close
  • struct inode

    1. 由内核在内部用来表示文件。因些,它和代表打开文件的file结构是不同的。一个文件可以对应多个file结构, 但只有一个inode结构

重要成员

dev_t i_rdev: / * 对于代表设备文件的节点, 这个成员包含实际的设备编号 */

struct cdev *i_cdev; /* struct cdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这个结构, 当节点指的是一个字符设备文件时.*/

4. 字符设备的注册

  1. Linux2.6内核中,字符设备使用struct cdev来描述字符设备驱动的注册。
  2. 字符设备驱动的注册主要有三个步骤
  3. 1 分配cdev
  4. 2)初始化cdev
  5. 3)添加cdev

分配

  1. struct cdev \*my\_cdev = cdev\_alloc();

初始化

  1. void cdev\_init(struct cdev \*cdev, const struct file\_operations \*fops);
  2. cdev: 待初始化的cdev结构
  3. fops: 设备对应的操作函数集

注册, 告诉内核

  1. int cdev\_add(struct cdev \*dev, dev\_t num, unsigned int count);
  2. dev: 添加到内核的字符设备结构
  3. num: 设备响应的第一个设备号
  4. count: 关联到设备的设备号数目,通常为1

去除字符设备

  1. void cdev\_del(struct cdev \*dev);

使用 cdev_add 是有几个重要事情要记住

  1. 1.第一个是这个调用可能失败. 如果它返回一个负的错误码, 你的设备没有增加到系统中.
  2. 2. cdev\_add 一返回成功, 你的设备就是"活的"并且内核可以调用它的操作.
  3. 所以除非你的驱动完全准备好处理设备上的操作, 你不应当调用 cdev\_add.

5 注册字符设备的一个例子

  1. 还是线上源代码:

    //memdev.h

    ifndef MEMDEV_H

    define MEMDEV_H

    ifndef MEMDEV_MAJOR

    define MEMDEV_MAJOR 200

    endif

    ifndef MEMDEV_NR_DEVS

    define MEMDEV_NR_DEVS 2

    endif

    ifndef MEMDEV_SIZE

    define MEMDEV_SIZE 4096

    endif

    struct mem_dev{

    1. char* data;
    2. unsigned long size;

    };

    endif

  1. //memdev.c
  2. # include < linux / module.h >
  3. # include < linux / types.h >
  4. # include < linux / fs.h >
  5. # include < linux / errno.h >
  6. # include < linux / mm.h >
  7. # include < linux / sched.h >
  8. # include < linux / init.h >
  9. # include < linux / cdev.h >
  10. # include < asm / io.h >
  11. # include < asm / system.h >
  12. # include < asm / uaccess.h >
  13. # include < linux / wait.h >
  14. # include < linux / completion.h >
  15. # include "memdev.h"
  16. MODULE_LICENSE( "Dual BSD/GPL" );
  17. static int mem_major = MEMDEV_MAJOR;
  18. struct mem_dev * mem_devp; /*设备结构体指针*/
  19. struct cdev cdev;
  20. /*文件打开函数*/
  21. int mem_open( struct inode * inode, struct file * filp)
  22. {
  23. printk( "open own file\n" );
  24. return 0 ;
  25. }
  26. /*文件操作结构体*/
  27. static const struct file_operations mem_fops =
  28. {
  29. .owner = THIS_MODULE,
  30. .open = mem_open,
  31. };
  32. /*设备驱动模块加载函数*/
  33. static int memdev_init( void )
  34. {
  35. int result;
  36. int i;
  37. dev_t devno = MKDEV(mem_major, 0 );
  38. /* 静态申请设备号*/
  39. result = register_chrdev_region(devno, 2 , "memdev" );
  40. if (result < 0 )
  41. return result;
  42. /*初始化cdev结构*/
  43. cdev_init( & cdev, & mem_fops);
  44. /* 注册字符设备 */
  45. cdev_add( & cdev, MKDEV(mem_major, 0 ), MEMDEV_NR_DEVS);
  46. return result;
  47. }
  48. /*模块卸载函数*/
  49. static void memdev_exit( void )
  50. {
  51. cdev_del( & cdev); /*注销设备*/
  52. unregister_chrdev_region(MKDEV(mem_major, 0 ), 2 ); /*释放设备号*/
  53. }
  54. module_init(memdev_init);
  55. module_exit(memdev_exit);
  56. #Makefile
  57. ifneq ($(KERNELRELEASE),)
  58. obj-m := memdev.o
  59. else
  60. KERNELDIR ?= /lib/modules/$(shell uname -r)/build
  61. PWD = $(shell pwd)
  62. default:
  63. $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
  64. clean:
  65. rm memdev.mod* module* memdev.o memdev.ko Module.*
  66. endif
  67. 2. 测试
  68. 首先先make下,生成memdev.ko
  69. 然后insmod memdev.ko生成memdev模块
  70. 创建设备节点:sudo mknod /dev/memdev_t c 200 0
  71. 接下开使用设备文件
  72. 下面是一个测试程序
  73. // memusr.c
  74. #include <stdio.h>
  75. #include <string.h>
  76. int main()
  77. {
  78. FILE *fp0;
  79. /*打开设备文件*/
  80. fp0 = fopen("/dev/memdev_t","r+");
  81. if (fp0 == NULL) {
  82. printf("Open Memdev0 Error!\n");
  83. return -1;
  84. }
  85. }

编译运行,然后使用dmesg可以看到日志文件里输出

  1. [38439.741816] Hello World!
  2. [38657.654345] Goodbye
  3. [40393.039520] open own file

记得要使用sudo 运行memusr 否则会显示设备打开失败。

发表评论

表情:
评论列表 (有 0 条评论,660人围观)

还没有评论,来说两句吧...

相关阅读