1. 设备号
主设备号:用来标识与设备文件相关的驱动程序, ——反应设备类型
次设备号:为内核所用,被驱动程序用来辨别操作那个设备文件 ——区分同类型的具体某个设备
1.1 设备号的内部表达
在内核中,保存设备号(包括主设备号和此设备好)使用类型
dev_t (<linux/types.h>)
这是一个unsigned int 是一个32位的无符号整型。。
主设备号——高12位
此设备号——低12位
我们可以使用宏来取一个设备号(dev)的主设备号和此设备号
定义在 <linux/kdev_t.h>
MAJOR(dev_t dev) 取主设备号
MINOR(dev_t dev) 取次设备号
也可以将主次设备号合成一个完整的dev_t类型的设备号
MKDEV(int major, int minor) 将主次设备号转换成dev_t
1.2 分配主次设备号
linux可以采用静态申请和动态申请两种方法来分配主次设备号
静态申请
1. 根据Documentation/devices.txt, 确定一个没有使用的主设备号
2. 使用register_chrdev_region(dev_t first, unsigned int count, char *name)
定义在<linux/fs.h>
count 为所请求的连续设备编号个数, 如果count过大,可以会各下一个主设备号重叠。
name 为设备名, 注册后 出现在/proc/devices和sysfs中
动态分配
作为一个新的驱动程序,应该使用动态分配机制获取主设备号
alloc_chrdev_region(dev_t *dev, unsigned int first, unsigned int count, char *name)
不管用何种方法分配, 不用时都要释放掉
void unregister_chrdev_region(dev_t first, unsigned int count);
静态申请与动态申请的优缺点:
静态申请——简单(优); 一旦驱动程序被广泛命使用, 随机选定的主设备号可以造成冲突,使驱动程序无法注册。(劣)
动态申请——简单,易于驱动推广(优);无法在驱动安装前创建设备文件, 因为不能保证分配的主设备号始终一致。(劣)
2 创建设备文件
设备文件的创建有
1. 使用mknod命令手工创建
2. 自动创建
两种方法。
2.1 mknod手工创建
mknod 用法:
mknod filename type major minor
filename : 设备文件名
type : 设备文件类型
major : 主设备号
minor : 次设备号
2.2 自动创建
如果我们在驱动里面动态创建的话需要这样做
struct class *cdev_class; cdev_class = class_create(owner,name) device_create(_cls,_parent,_devt,_device,_fmt)
2.3 模块退出时要销毁设备文件
device_destroy(_cls,_device) class_destroy(struct class * cls)
3. 一些重要的结构体
大部分的基础性的驱动操作包括 3 个重要的内核数据结构, 称为 file_operations, file, 和 inode.
- 文件结构 struct file
定义于<linux/fs.h>, 是一个内核结构, 不会出现在用户空间
代表一个打开的文件。系统中每个找开的文件在内核空间一个关联的struct file, 它由内核在打开文件时创建, 在文件关闭后释放重要成员loff_t f_ops /* 文件读写位置 */ struct file_operations *f_op /* 文件关联的操作 */ mode_t f_mode /* 模式确定文件可读或者可写 */ unsigned int f_flags /* 文件标志,一般用来判断是否请求非阻塞操 作, 标志定义<linux/fcntl.h> */ void *private_data;
open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.
- 文件操作 struct file_operation
file_operation 结构是一个字符驱动如何建立这个连接. 这个结构, 定义在 <linux/fs.h>, 是一个函数指针的集合. 每个打开文件(内部用一个 file 结构来代表, 稍后我们会查看)与它自身的函数集合相关连( 通过包含一个称为 f_op 的成员, 它指向一个 file_operations 结构). 这些操作大部分负责实现系统调用, 因此, 命名为 open,read, 等等. 我们可以认为文件是一个"对象"并且其上的函数操作称为它的"方法", 使用面向对象编程的术语来表示一个对象声明的用来操作对象的动作.下面是一个file_operationd的声明:struct file_operations my_fops = { .owner = THIS_MODULE, .llseek = my_llseek, .read = my_read, .write = my_write, .ioctl = my_ioctl, .open = my_open, .release = my_release, };
该声明使用标准的 C 标记式结构初始化语法. 这个语法是首选的, 因为它使驱动在结构定义的改变之间更加可移植 。下面列出file_operationd部分成员的含义:(其他成员自行百度)struct module *owner第一个 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 *);用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
int (*open) (struct inode *, struct file *);
尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知int (*release) (struct inode *, struct file *);在文件结构被释放时引用这个操作. 相当于close
- struct inode
由内核在内部用来表示文件。因些,它和代表打开文件的file结构是不同的。一个文件可以对应多个file结构, 但只有一个inode结构
重要成员dev_t i_rdev: / * 对于代表设备文件的节点, 这个成员包含实际的设备编号 */
struct cdev *i_cdev; /* struct cdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这个结构, 当节点指的是一个字符设备文件时.*/
4. 字符设备的注册
Linux2.6内核中,字符设备使用struct cdev来描述字符设备驱动的注册。
字符设备驱动的注册主要有三个步骤
(1) 分配cdev
(2)初始化cdev
(3)添加cdev
分配
struct cdev *my_cdev = cdev_alloc();
初始化
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
cdev: 待初始化的cdev结构
fops: 设备对应的操作函数集
注册, 告诉内核
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
dev: 添加到内核的字符设备结构
num: 设备响应的第一个设备号
count: 关联到设备的设备号数目,通常为1
去除字符设备
void cdev_del(struct cdev *dev);
使用 cdev_add 是有几个重要事情要记住
1.第一个是这个调用可能失败. 如果它返回一个负的错误码, 你的设备没有增加到系统中.
2. cdev_add 一返回成功, 你的设备就是"活的"并且内核可以调用它的操作.
所以除非你的驱动完全准备好处理设备上的操作, 你不应当调用 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{ char* data; unsigned long size; }; #endif
//memdev.c # include < linux / module.h > # include < linux / types.h > # include < linux / fs.h > # include < linux / errno.h > # include < linux / mm.h > # include < linux / sched.h > # include < linux / init.h > # include < linux / cdev.h > # include < asm / io.h > # include < asm / system.h > # include < asm / uaccess.h > # include < linux / wait.h > # include < linux / completion.h > # include "memdev.h" MODULE_LICENSE( "Dual BSD/GPL" ); static int mem_major = MEMDEV_MAJOR; struct mem_dev * mem_devp; /*设备结构体指针*/ struct cdev cdev; /*文件打开函数*/ int mem_open( struct inode * inode, struct file * filp) { printk( "open own file " ); return 0 ; } /*文件操作结构体*/ static const struct file_operations mem_fops = { .owner = THIS_MODULE, .open = mem_open, }; /*设备驱动模块加载函数*/ static int memdev_init( void ) { int result; int i; dev_t devno = MKDEV(mem_major, 0 ); /* 静态申请设备号*/ result = register_chrdev_region(devno, 2 , "memdev" ); if (result < 0 ) return result; /*初始化cdev结构*/ cdev_init( & cdev, & mem_fops); /* 注册字符设备 */ cdev_add( & cdev, MKDEV(mem_major, 0 ), MEMDEV_NR_DEVS); return result; } /*模块卸载函数*/ static void memdev_exit( void ) { cdev_del( & cdev); /*注销设备*/ unregister_chrdev_region(MKDEV(mem_major, 0 ), 2 ); /*释放设备号*/ } module_init(memdev_init); module_exit(memdev_exit); #Makefile ifneq ($(KERNELRELEASE),) obj-m := memdev.o else KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD = $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: rm memdev.mod* module* memdev.o memdev.ko Module.* endif 2. 测试 首先先make下,生成memdev.ko 然后insmod memdev.ko生成memdev模块 创建设备节点:sudo mknod /dev/memdev_t c 200 0 接下开使用设备文件 下面是一个测试程序 // memusr.c #include <stdio.h> #include <string.h> int main() { FILE *fp0; /*打开设备文件*/ fp0 = fopen("/dev/memdev_t","r+"); if (fp0 == NULL) { printf("Open Memdev0 Error! "); return -1; } }
编译运行,然后使用dmesg可以看到日志文件里输出
[38439.741816] Hello World! [38657.654345] Goodbye [40393.039520] open own file
记得要使用sudo 运行memusr 否则会显示设备打开失败。