linux字符设备驱动基础 _
1.字符设备驱动相关系统函数简介1.1 container_of/** * container_of 通过一个结构体成员的指针获取包含该成员的结构体的起始地址 * ptr: 变量的指针 * type: 指针指向的结构体类型 * member: 结构体中的变量类型 */ #define container_of(ptr, type, member) ({ \ void *__mptr (void *)(ptr); \ BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)-member) \ !__same_type(*(ptr), void), \ pointer type mismatch in container_of); \ ((type *)(__mptr - offsetof(type, member))); })为了简化理解可以将 container_of 函数理解为如下表示#define container_of(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member)))该函数根据 结构体成员变量的指针通过该变量相对于结构体的偏移得到了该变量对应的结构体的指针。这是一个非常灵活的用法通过保存某个结构体变量的指针可以通过该指针反向推出该结构体的指针。在后续案例中有详细解释。1.2 register_chrdev_regionregister_chrdev_region 是 Linux 内核中用于注册字符设备编号范围的函数。该函数为驱动程序预留一段连续的设备号主设备号 起始次设备号后续将字符设备通过 cdev_add 绑定到这些设备号上函数原型如下/** * firstdev_t 类型指定要注册的起始设备号。使用 MKDEV(major, minor) 宏生成 * count需要注册的连续设备号数量次设备号的范围 * name设备的名称该名称会出现在 /proc/devices 文件中用于标识该组设备号属于哪个驱动。 * 返回值: 成功返回0 * 参数无效返回 -EINVAL * 设备号被占用返回 -EBUSY */ int register_chrdev_region(dev_t first, unsigned int count, const char *name);1.3 cdev_initstruct cdev 是内核表示字符设备的对象每个字符设备驱动都需要创建 cdev 实例并将其注册到内核cdev_init 负责设置 cdev 的基本字段为后续的 cdev_add做准备/* * dev指向要初始化的 struct cdev 结构体的指针。 * 该结构体可以由驱动静态分配 (kmalloc) 也可以动态分配 (cdev_alloc) * fops指向 struct file_operations 结构体的指针 * 该结构体包含了设备支持的各种操作函数如 open、read、write、ioctl 等 */ void 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; }1.4 cdev_add该函数建立了设备号与字符设备驱动的关联使 VFS虚拟文件系统能够通过设备号找到对应的驱动程序从而响应用户空间的打开、读写等操作将 struct cdev 对象与设备号绑定在内核的字符设备映射表中建立关联将 cdev 对象加入内核的全局字符设备链表或哈希表使 VFS 能够根据设备号找到对应的cdev激活设备调用 cdev_add 后该设备便可以被用户空间访问需要使用mknod关联到/dev/mydevname/* * p指向 cdev_init() 已经初始化的 struct cdev 对象的指针。 * dev分配给该设备的起始设备号应该与 register_chrdev_region() 向内核申请的设备号位置一致 * count与该设备关联的次设备号数量 * 返回值成功 0 参数无效 -EINVAL 设备号被占用 -EBUSY 理论上在分配设备号时已经检查过但这里仍可能发生如动态添加时竞争 内存不足 -ENOMEM */ int cdev_add(struct cdev *p, dev_t dev, unsigned count);2. 使用字符设备驱动实现共享内存下面是使用字符设备驱动实现共享内存的案例用来快速熟悉驱动函数的使用方法代码参考书籍为Linux设备驱动详解#include linux/init.h #include linux/errno.h #include linux/mm.h #include linux/sched.h #include linux/module.h #include linux/ioctl.h #include linux/io.h #include linux/fs.h #include linux/cdev.h #include linux/uaccess.h #include linux/slab.h #define GLOBALMEM_SIZE 1024 #define GLOBALMEM_MAGIC M #define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0) struct globalmem_dev { // 字符设备 struct cdev m_cdev; // 共享内存 unsigned char mem[GLOBALMEM_SIZE]; }; static int globalmem_major 266; // 存放两个字符设备私有数据 struct globalmem_dev* globalmem_devp; /* user open fd */ static int globalmem_open(struct inode* inode, struct file* filp) { struct globalmem_dev* dev; /* 下面是一种常用的区分次设备号的方法 * 通过 globalmem_init 初始化时传入不同的 cdev 指针实现区分 */ // inode 中保存的 i_cdev 指针是 globalmem_init 函数中传入的globalmem_dev 结构体变量 m_cdev 的指针 // 所以通过 inode-i_cdev 指针即 m_cdev 成员变量的指针可以找到其对应的结构体指针 dev container_of(inode-i_cdev, struct globalmem_dev, m_cdev); // 将设备结构体指针传给文件私有数据指针提供给其他函数调用时使用 filp-private_data dev; return 0; } /* user release fd*/ static int globalmem_release(struct inode* inode, struct file* filp) { return 0; } /* user read fd */ static ssize_t globalmem_read(struct file* filp, char __user* buf, size_t count, loff_t* ppos) { unsigned long p *ppos; // 获得设备结构体的指针这是通过open函数传入的指针实现不同次设备使用不同的共享内存 struct globalmem_dev* dev filp-private_data; if(p GLOBALMEM_SIZE) return 0; if(count GLOBALMEM_SIZE - p) count GLOBALMEM_SIZE - p; copy_to_user(buf, (void*)(dev-mem p), count); *ppos p count; return count; } /* user write fd */ static ssize_t globalmem_write(struct file* filp, const char __user* buf, size_t count, loff_t* ppos) { unsigned long p *ppos; // 获得设备结构体的指针 struct globalmem_dev* dev filp-private_data; if(p GLOBALMEM_SIZE) return 0; if(count GLOBALMEM_SIZE - p) count GLOBALMEM_SIZE - p; copy_from_user(dev-mem p, buf, count); *ppos p count; return count; } /* user lseek fd */ static loff_t globalmem_llseek(struct file* filp, loff_t offset, int orig) { loff_t ret; switch(orig) { // 从起始位置开始移动指针 case 0: if(offset 0) { ret -EINVAL; break; } if((unsigned int)offset GLOBALMEM_SIZE) { ret -EINVAL; break; } filp-f_pos (unsigned int)offset; ret filp-f_pos; break; // 从当前位置开始移动指针 case 1: if((filp-f_pos offset) GLOBALMEM_SIZE) { ret -EINVAL; break; } if((filp-f_pos offset) 0) { ret -EINVAL; break; } filp-f_pos offset; ret filp-f_pos; break; default: ret -EINVAL; } return ret; } /* user ioctl fd */ static int globalmem_ioctl(struct inode* inodep, struct file* filp, unsigned int cmd, unsigned long arg) { // 获取设备结构体指针 struct globalmem_dev* dev filp-private_data; switch(cmd) { case MEM_CLEAR: memset(dev-mem, 0, GLOBALMEM_SIZE); break; default: return -EINVAL; } return 0; } static const struct file_operations globalmem_fops { .owner THIS_MODULE, .open globalmem_open, .release globalmem_release, .llseek globalmem_llseek, .read globalmem_read, .write globalmem_write, .unlocked_ioctl globalmem_ioctl }; /* 设备驱动模块insmod加载函数 */ static int globalmem_init(void) { // 向 Linux 内核中注册字符设备编号范围 register_chrdev_region(MKDEV(globalmem_major, 0), 2, globalmem); // 为2个次设备以及共享内存分配内存 globalmem_devp kmalloc(2 * sizeof(struct globalmem_dev), GFP_KERNEL); memset(globalmem_devp, 0, 2 * sizeof(struct globalmem_dev)); // 初始化字符设备0的基本字段 cdev_init((globalmem_devp[0].m_cdev), globalmem_fops); globalmem_devp[0].m_cdev.owner THIS_MODULE; // 将主设备号globalmem_major次设备号0与字符设备驱动的关联 cdev_add((globalmem_devp[0].m_cdev), MKDEV(globalmem_major, 0), 1); // 初始化字符设备1的基本字段 cdev_init((globalmem_devp[1].m_cdev), globalmem_fops); globalmem_devp[1].m_cdev.owner THIS_MODULE; // 将主设备号globalmem_major次设备号1与字符设备驱动的关联 cdev_add((globalmem_devp[1].m_cdev), MKDEV(globalmem_major, 1), 1); return 0; } static int globalmem_exit(void) { // 注销cdev cdev_del((globalmem_devp[0].m_cdev)); cdev_del((globalmem_devp[1].m_cdev)); // 释放设备结构体内存 kfree(globalmem_devp); // 释放设备号 dev_t devno MKDEV(globalmem_major, 0); unregister_chrdev_region(devno, 2); } MODULE_AUTHOR(cear); MODULE_LICENSE(GPL); module_param(globalmem_major, int, S_IRUGO); module_init(globalmem_init); module_exit(globalmem_exit);