文章目录
模块是linux内核进行组件管理的一种方式,
驱动是基于模块进行注册和注销的,
不单单是字符设备,块设备驱动和网络设备驱动都是基于模块进行加载和卸载的。
字符设备(字符为单位,读写同步)
I/O传输过程中以字符为单位进行传输,
用户对字符设备发出读/写请求时,实际的使件读/写操作一般紧接着发生块设备。
eg:LCD 鼠标 键盘 触摸屏 串口
块设备(以块为单位,异步)
与字符相反,它的数据传输以块(内存缓冲)为单位传输用户对块设备读,硬件上的读/写操作不会紧接着发生,即用户请求和硬件操作是异步的
eg:磁盘类、闪存类等设备都封装成块设备
设备文件:字符设备 和块设备有设备文件,网络设备没有设备文件。
内核统一管理设备,所以编写内核驱动程序需要遵循内核的一些规则。内核将字符设备/块设备/网络设备抽象成对应的结构体。
一、 描述字符设备的结构体(cdev)
在include/linux/cdev.h中定义,有两个成员很重要dev,ops,
struct cdev {
struct kobject kobj; // 设备模型相关
struct module *owner; // 设备模型相关,赋值THIS_MODULE,设备驱动属于哪个模块
const struct file_operations *ops; // 函数指针,操作方法集
struct list_head list; // 内核通过这个list管理设备
dev_t dev; // 设备号
unsigned int count; // 设备个数,一个驱动 驱动了多个设备
};
1. 设备号 (dev_t dev)
用来标识设备,唯一。
32位无符号整型,12位主设备 +
20位次设备(根内核版本有关吧)
在/usr/include/linux/kdev_t.h中,
#define MAJOR(dev) ((dev)>>8) // 提取主设备号
#define MINOR(dev) ((dev) & 0xff) // 提取次设备号
#define MKDEV(ma,mi) ((ma)<<8 | (mi)) // 根据 主次设备号 合成 设备号
2. 操作方法集(file_operations *ops)
提供给应用层的一些操作方法集,open write read
/*
* NOTE:
* read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
* can be called without the big kernel lock held in all filesystems.
*/
struct file_operations {
...
struct module *owner;
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
...
};
二、 字符设备驱动框架用到的一些API
这些东西都是在内核模块框架下的。
0. 分配设备号
0.1 分配设备号,注册设备号
- 自动分配设备号
/********************************************* 功能:分配设备号 参数:dev 设备号指针,分配到的设备号就从指针传出 baseminor 次设备号起始,一般从0开始 count 次设备个数 name 设备号对应的名字 返回值:成功返回0,失败返回负数错误码 **********************************************/ int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
- 指定设备号
/********************************************* 功能:分配设备号 参数:from 需要程序员指定的设备号,MKDEV(major, minor),起始在次设备号中 count 次设备个数 name 设备号对应的名字 返回值:成功返回0,失败返回负数错误码 **********************************************/ int register_chrdev_region(dev_t from, unsigned count, const char *name)
0.2 注销设备号
/*********************************************
功能:从内核中注销设备号
参数:from 需要注销的设备号
count 次设备个数
name 设备号对应的名字
返回值:void
**********************************************/
void unregister_chrdev_region(dev_t from, unsigned count);
1. 为cdev结构体分配内存空间
在fs/char_dev.c中,cdev_alloc为cdev结构体分配内存空间,
/*********************************************
功能:为cdev结构体分配空间
参数:void
返回值:分配成功返回分配得到的结构体指针
失败返回NULL
**********************************************/
struct cdev *cdev_alloc(void)
可能由于内存不够 导致分配失败,一定要处理分配失败的情况,并且如果要返回一定要把前面分配到的资源释放掉,包含多层调用一定要查清楚分配了哪些资源,然后依次释放。
2. 初始化cdev结构体
在fs/char_dev.c中,
/*********************************************
功能:初始化结构体
参数:
cdev cdev结构体指针
fops 操作方法集指针
返回值:void
**********************************************/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
3. 添加(注册)字符设备到内核
注册字符设备中,由内核统一管理,在fs/char_dev.c中,
/*********************************************
功能:添加(注册)字符设备到内核中
参数:
p cdev结构体指针
dev 设备号
count 设备个数
返回值:
int 成功返回0,失败返回错误码-ENOMEM
**********************************************/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
- cdev_add返回值的情况
在drivers/base/map.c中定义kobj_map函数,成功返回0,失败返回错误码kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p) { ... p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL); if (p == NULL) return -ENOMEM; // #define ENOMEM 12 /* Out of memory */ ... return 0; }
- 第二参数dev设备号
设备号内核统一管理,属于内核的资源,设备号资源,需要申请设备号资源
4. 删除(注销)字符设备
在不用设备时,一定要注销该设备,应该在内核模块的卸载函数中,
/*********************************************
功能:从内核中删除(注销)字符设备
参数:p cdev结构体指针
返回值:void
**********************************************/
void cdev_del(struct cdev *p)
三、 编写字符设备驱动框架
所有驱动都是基于内核模块去完成的,内核模块三要素:
入口函数
出口函数
GPL协议声明
驱动注册时,就要申请cdev内存,执行初始化,申请设备号等;
当卸载驱动时就要去释放内存资源,注销设备号。
1. module_demo.c文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h> // 操作方法集
#include <linux/cdev.h>
#define BASEMINOR 0
#define COUNT 3
#define NAME "chrdev_demo"
// 保存申请到的设备号
dev_t devno;
// 抽象的字符设备结构体
struct cdev *cdevp;
// 操作方法集内的函数指针(函数)定义
int demo_open(struct inode *inode, struct file *filep)
{
printk("---%s---%s---%d", __FILE__, __func__, __LINE__);
return 0;
}
int demo_release(struct inode *inode, struct file *filep)
{
printk("---%s---%s---%d", __FILE__, __func__, __LINE__);
return 0;
}
// 定义操作方法集结构体,并部分初始化
struct file_operations fops = {
.owner = THIS_MODULE,
.open = demo_open,
.release = demo_release,
};
static int __init module_demo_init(void)
{
int ret = 0; // 用于保存分配到的设备号
// 0. 获取设备号 alloc_chrdev_region 自动获取设备号
ret = alloc_chrdev_region(&devno, BASEMINOR, COUNT, NAME);
if(ret < 0)
{
printk(KERN_ERR "alloc_chrdev_region failed.\n");
goto err1_alloc_chrdev_region;
}
printk(KERN_INFO "major = %d\n", MAJOR(devno)); // 主设备号是内核分配的,次设备是自己指定,提取主设备号打印
// 1. 分配内存 cdev_alloc
cdevp = cdev_alloc();
if(cdevp == NULL)
{
printk(KERN_ERR "cdev_alloc failed.\n");
ret = -ENOMEM; // #define ENOMEM 12 /* Out of memory */
goto err2_cdev_alloc;
}
// 2. 初始化cdev结构体 cdev_init
cdev_init(cdevp, &fops);
// 3. 注册设备到内核 cdev_add
ret = cdev_add(cdevp, devno, COUNT);
if(ret < 0)
{
printk(KERN_ERR "cdev_add failed.\n");
goto err2_cdev_alloc;
}
printk("---%s---%s---%d", __FILE__, __func__, __LINE__); // 打印此行说明前面成功
return 0; // 成功之后就不能执行下面的err释放资源步骤
// 释放资源,申请资源的倒序
err2_cdev_alloc:
unregister_chrdev_region(devno, COUNT);
err1_alloc_chrdev_region:
return ret;
}
static void __exit module_demo_exit(void)
{
// 注销设备号
unregister_chrdev_region(devno, COUNT);
// 注销字符设备
cdev_del(cdevp);
printk("---%s---%s---%d", __FILE__, __func__, __LINE__); // 打印此行说明注销成功
}
module_init(module_demo_init);
module_exit(module_demo_exit);
MODULE_LICENSE("GPL");
2. Makefile文件
KERNDIR := /lib/modules/$(shell uname -r)/build # 系统内核的路径
PWD := $(shell pwd) # 执行pwd命令并把结果赋给PWD
obj-m := module_demo.o # .ko的生成依赖于.o,.o默认依赖.c
all:
make -C $(KERNDIR) M=$(PWD) modules
clean:
make -C $(KERNDIR) M=$(PWD) clean
插入内核模块module_demo.ko到内核,查看内核日志,卸载内核模块,查看内核日志,
重新分配得到的设备号根之前的一样,说明上次释放资源成功,
三个重要的结构体
1. inode
struct inode {
...
dev_t i_rdev;
struct cdev *i_cdev;
...
}
2.fops
struct file_operations {
...
struct module *owner;
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
...
};
3. filep
struct file {
...
mode_t f_mode;
loff_t f_pos;
unsigned int f_flags;
const struct file_operations *f_op;
void *private_data;
...
};
inode结构体将设备号dev_t和字符设备抽象结构cdev联系起来,字符设备抽象结构cdev将设备号dev_t和操作方法集fops联系起来,
cdev_init函数用操作方法集fops去初始化字符设备抽象结构cdev(绑定驱动程序到cdev)
参考牛人的博客