1 字符设备

1.1 Struct cdev

cdev结构体用于描述一个字符设备,定义如下:

  1. // include/linux/cdev.h
  2. struct cdev {
  3. struct kobject kobj; //内嵌kobject对象
  4. struct module *owner; //该设备所属的模块
  5. const struct file_operations *ops; //读写字符设备的操作结构体, 驱动要实现的主要部分
  6. struct list_head list;
  7. dev_t dev; //设备号
  8. unsigned int count;
  9. } __randomize_layout; //此宏表示结构体内成员不按顺序存储,防止被攻击

内核提供现成的函数用于操作struct cdev:

  • void cdev_init(struct cdev , struct file_operations ):初始化cdev,绑定file_operation
  • struct cdev *cdev_alloc(void):动态申请cdev控件
  • void cdev_put(struct cdev *p);
  • int cdev_add(struct cdev *, dev_t, unsigned):模块加载时,向系统添加一个cdev,注册字符设备
  • void cdev_del(struct cdev *):模块卸载时,从系统删除一个cdev,注销字符设备

    1.2 申请释放字符设备号

    当我们要注册一个字符设备时,从cdev_add的参数可以看出,我们需要一个设备号。
    申请设备号可以用如下两个函数:
    1. //需要输入起始设备号
    2. int register_chrdev_region(dev_t from, unsigned count, const char *name);
    3. //用于随便申请设备号,没有起始设备号
    4. int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
    卸载字符设备时,需要同时释放字符设备号,函数如下:
    1. void unregister_chrdev_region(dev_t from, unsigned count);

    1.3 用户空间内存的复制

    由于内核空间和用户空间的内存地址不能直接读写,所以在驱动中操作数据时,首先需要把数据在内核空间和用户空间进行复制。相关函数定义如下: ```c //include/linux/uaccess.h static always_inline unsigned long must_check \ copy_from_user(void to, const void __user from, unsigned long n);

static always_inline unsigned long must_check copy_to_user(void __user to, const void from, unsigned long n);

  1. - 完全复制返回0
  2. - 部分复制返回未被复制的字节数
  3. - 复制失败返回负数
  4. 另外对于简单的数据类型(charint等),可以使用更简单的`put_user()``get_user()`。这四个函数内部都会先检查用户空间缓冲区的合法性,在操作数据。很多Linux漏洞就是忘了检查,所以**推荐使用这四个函数,避免自创新函数**。
  5. <a name="bLp6k"></a>
  6. # 2 字符驱动的结构和模板
  7. <a name="GPF8v"></a>
  8. ## 2.1 字符设备驱动的结构
  9. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/690827/1600790857390-599a125a-2ba4-4477-a49c-8ecf5fe1f597.png#align=left&display=inline&height=262&margin=%5Bobject%20Object%5D&name=image.png&originHeight=349&originWidth=870&size=121345&status=done&style=none&width=653)
  10. <a name="aCxkI"></a>
  11. ## 2.2 驱动模块加载和卸载
  12. Linux的编码习惯:**定义一个和设备相关的结构体,包含cdev、私有数据、锁等信息,驱动代码操作这个自定义的结构体**。
  13. ```c
  14. //设备相关结构体
  15. struct xxx_dev_t {
  16. struct cdev cdev;
  17. ...
  18. } xxx_dev;
  19. //设备驱动模块加载函数
  20. static int __init xxx_init(void)
  21. {
  22. ...
  23. cdev_init(&xxx_dev.cdev, &xxx_fops); // 初始化 cdev,xxx_fops的定义看下面2.3
  24. xxx_dev.cdev.owner = THIS_MODULE;
  25. //获取字符设备号
  26. if (xxx_major) {
  27. register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
  28. } else {
  29. alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
  30. }
  31. ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); //注册设备
  32. ...
  33. }
  34. //设备驱动模块卸载函数
  35. static void __exit xxx_exit(void)
  36. {
  37. unregister_chrdev_region(xxx_dev_no, 1); //释放占用的设备号
  38. cdev_del(&xxx_dev.cdev); //注销设备
  39. ...
  40. }

2.3 file_operations成员函数

file_operations是驱动和虚拟文件系统的接口,实现了它内部的成员函数,用户才能正常的使用系统调用操作设备。

  1. //定义文件操作的结构体,用于cdev_init
  2. struct file_operations xxx_fops = {
  3. .owner = THIS_MODULE,
  4. .read = xxx_read,
  5. .write = xxx_write,
  6. .unlocked_ioctl= xxx_ioctl,
  7. ...
  8. };
  9. //读设备
  10. ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t*f_pos)
  11. {
  12. ...
  13. copy_to_user(buf, ..., ...);//把数据复制到用户空间
  14. ...
  15. }
  16. //写设备
  17. ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
  18. {
  19. ...
  20. copy_from_user(..., buf, ...);//先从用户空间复制数据,再处理
  21. ...
  22. }
  23. //ioctl 函数
  24. long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
  25. {
  26. ...
  27. switch (cmd) {
  28. case XXX_CMD1:
  29. ...
  30. break;
  31. case XXX_CMD2:
  32. ...
  33. break;
  34. default://不能支持的命令
  35. return - ENOTTY;
  36. }
  37. return 0;
  38. }

ioctl命令的定义

在file_opeartion的ioctl函数中,驱动需要支持IO控制命令,为了防止不同的设备驱动使用相同的命令号,建议采用同一的Ioctl命令生成方式
内核中也提供了一些预定义的IO控制命令(定义在include/uapi/asm-generic/ioctls.h中),这些命令会被内核处理,不会被设备驱动处理。

Linux建议的命令生成格式如下:

设备类型 序列号 方向 数据尺寸
8位 8位 2位 13/14位
  • 设备类型:内核中iocrl_bumber.txt文件给出了一些推荐和已经被使用的设备类型,新的类型需要避免重复。
  • 方向:从app角度看数据传送的方向,分为如下四种,可以用内核提供的宏函数(#include )生成命令号
    • _IOC_NONE:无数据传输,对应生成函数_IO(type,nr)
    • _IOC_READ:读,对应_IOR(type,nr,size)
    • _IOC_WRITE:写,对应_IOW(type,nr,size)
    • _IOC_READ_LOC_WRITE:双向,对应_IOWR(type,nr,size)
  • 数据尺寸:用户数据的大小

使用示例:

  1. #define GLOBALMEM_MAGIC 'g'
  2. #define MEM_CLEAR _IO(GLOBALMEM_MAGIC,0)

3 模拟开发示例

  1. //假设有个globalmem的字符设备,如何编写字符设备驱动
  2. #include <linux/module.h>
  3. #include <linux/fs.h>
  4. #include <linux/init.h>
  5. #include <linux/cdev.h>
  6. #include <linux/uaccess.h>
  7. #include <linux/slab.h>
  8. #include <linux/ioctl.h>
  9. //定义宏
  10. #define GLOBALMEM_SIZE 0X1000
  11. #define GLOBALMEM_MAGIC 'g'
  12. #define MEM_CLEAR _IO(GLOBALMEM_MAGIC,0)
  13. #define GLOBALMEM_MAJOR 230 //默认主设备号
  14. //添加模块参数,主设备号
  15. static int globalmem_major = GLOBALMEM_MAJOR;
  16. module_param(globalmem_major, int, S_IRUGO);
  17. //定义字符驱动结构体
  18. struct globalmem_dev
  19. {
  20. struct cdev cdev;//系统字符设备结构体
  21. unsigned char mem[GLOBALMEM_SIZE];//模拟设备占用的内存
  22. };
  23. struct globalmem_dev* globalmem_devp;//指针,指向申请的设备空间
  24. //file_operation成员函数
  25. static int globalmem_open(struct inode* inode, struct file* filp)
  26. {
  27. filp->private_data = globalmem_devp;
  28. return 0;
  29. }
  30. static int globalmem_release(struct inode* inode, struct file* filp)
  31. {
  32. return 0;//释放文件,没什么特殊操作
  33. }
  34. static ssize_t globalmem_read(struct file* filp, char __user * buf, size_t size, loff_t* ppos)
  35. {
  36. unsigned long p = *ppos;
  37. unsigned int count = size;
  38. int ret = 0;
  39. //文件的私有数据一般指向设备结构体,在open函数中设置
  40. struct globalmem_dev* dev = filp->private_data;
  41. if (p >= GLOBALMEM_SIZE)
  42. return 0;//偏移位置不能超过空间容量
  43. if (count > (GLOBALMEM_SIZE - p))
  44. count = GLOBALMEM_SIZE - p;//字节数不能超过容量
  45. if (copy_to_user(buf, dev->mem + p, count))//复制内容到用户空间
  46. {
  47. ret = -EFAULT;
  48. }
  49. else
  50. {
  51. *ppos += count;
  52. ret = count;
  53. printk(KERN_INFO"read %u bytes from %lu", count, p);
  54. }
  55. return ret;
  56. }
  57. static ssize_t globalmem_write(struct file* filp, const char __user * buf, size_t size, loff_t* ppos)
  58. {
  59. unsigned long p = *ppos;
  60. unsigned int count = size;
  61. int ret = 0;
  62. struct globalmem_dev* dev = filp->private_data;
  63. if (p >= GLOBALMEM_SIZE)
  64. return 0;//偏移位置不能超过空间容量
  65. if (count > (GLOBALMEM_SIZE - p))
  66. count = GLOBALMEM_SIZE - p;//字节数不能超过容量
  67. if (copy_from_user(dev->mem + p, buf, count))//从用户空间复制内容
  68. {
  69. ret = -EFAULT;
  70. }
  71. else
  72. {
  73. *ppos += count;
  74. ret = count;
  75. printk(KERN_INFO"write %u bytes from %lu", count, p);
  76. }
  77. return ret;
  78. }
  79. static loff_t globalmem_llseek(struct file* filp, loff_t offset, int orig)
  80. {
  81. loff_t ret = 0;
  82. switch(orig)
  83. {
  84. case 0://从文件开头seek
  85. if (offset < 0 || (unsigned int)offset > GLOBALMEM_SIZE)
  86. {
  87. ret = -EINVAL;
  88. break;
  89. }
  90. filp->f_pos = (unsigned int)offset;//设置文件对象新位置
  91. ret = filp->f_pos;
  92. break;
  93. case 1://从文件当前位置seek
  94. if ((filp->f_pos + offset) > GLOBALMEM_SIZE || (filp->f_pos + offset) < 0)
  95. {
  96. ret = -EINVAL;
  97. break;
  98. }
  99. filp->f_pos += (unsigned int)offset;//设置文件对象新位置
  100. ret = filp->f_pos;
  101. break;
  102. default:
  103. ret = -EINVAL;
  104. break;
  105. }
  106. return ret;
  107. }
  108. static long globalmem_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
  109. {
  110. struct globalmem_dev* dev = filp->private_data;
  111. switch(cmd)
  112. {
  113. case MEM_CLEAR: //本示例里我们只支持clear命令
  114. memset(dev->mem, 0, GLOBALMEM_SIZE);
  115. printk(KERN_INFO"globalmem is set to zero\n");
  116. break;
  117. default:
  118. return -EINVAL;
  119. }
  120. return 0;
  121. }
  122. //定义文件操作结构体
  123. static const struct file_operations globalmem_fops =
  124. {
  125. .owner = THIS_MODULE,
  126. .llseek = globalmem_llseek,
  127. .read = globalmem_read,
  128. .write = globalmem_write,
  129. .unlocked_ioctl = globalmem_ioctl,
  130. .open = globalmem_open,
  131. .release = globalmem_release
  132. };
  133. //驱动模块加载函数
  134. static void globalmem_setup_cdev(struct globalmem_dev* dev, int index)
  135. {
  136. int err, devno = MKDEV(globalmem_major, index); //获得dev_t对象
  137. cdev_init(&dev->cdev, &globalmem_fops);//初始化设备
  138. dev->cdev.owner = THIS_MODULE;
  139. //注册设备
  140. err = cdev_add(&dev->cdev, devno, 1);
  141. if (err)
  142. {
  143. printk(KERN_NOTICE"Error %d adding globalmem %d", err, index);
  144. }
  145. }
  146. static int __init globalmem_init(void)
  147. {
  148. int ret;
  149. dev_t devno = MKDEV(globalmem_major, 0);
  150. //申请设备号
  151. if (globalmem_major)
  152. {
  153. ret = register_chrdev_region(devno, 1, "globalmem");
  154. }
  155. else
  156. {
  157. ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
  158. globalmem_major = MAJOR(devno);
  159. }
  160. if (ret < 0)
  161. return ret;
  162. globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
  163. if (!globalmem_devp)
  164. {
  165. //空间申请失败
  166. ret = -ENOMEM;
  167. goto fail_malloc;
  168. }
  169. globalmem_setup_cdev(globalmem_devp, 0);
  170. return 0;
  171. fail_malloc:
  172. unregister_chrdev_region(devno, 1);
  173. return ret;
  174. }
  175. module_init(globalmem_init);
  176. //驱动模块卸载函数
  177. static void __exit globalmem_exit(void)
  178. {
  179. cdev_del(&globalmem_devp->cdev);//注销设备
  180. kfree(globalmem_devp);
  181. unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);//释放设备号
  182. }
  183. module_exit(globalmem_exit);
  184. //模块声明
  185. MODULE_AUTHOR("BARRET REN <barret.ren@outlook.com>");
  186. MODULE_LICENSE("GPL v2");
  187. MODULE_DESCRIPTION("A driver for virtual globalmem charactor device");
  188. MODULE_ALIAS("globalmem device driver");

运行结果如下:

  1. # 加载模块后,可以看到已经加载230的设备
  2. $ cat /proc/devices
  3. Character devices:
  4. 1 mem
  5. 4 /dev/vc/0
  6. ...
  7. 226 drm
  8. 230 globalmem
  9. 241 hidraw
  10. 242 aux
  11. # 创建设备号位230,0的设备
  12. $ sudo mknod /dev/globalmem c 230 0
  13. $ sudo chmod 777 /dev/globalmem
  14. $ echo "hello world" > /dev/globalmem # 写入数据
  15. $ cat /dev/globalmem
  16. hello world #刚才的输入可以打印出来,读写没有问题