前段时间遇到一个功能需求,需要将内核空间的一些数据结构和接口导出给用户空间,当然这可以通过自定义文件系统、netlink等机制实现,但为了实现更一般的接口,决定通过字符设备来实现,编写字符设备驱动程序,向用户空间导出设备文件,这样用户空间程序可以使用简单的读写文件操作设备,由于Linux内核当前更新到5.x版本了,字符设备的实现较之前版本(很早了,我以前研究的2.6.x)还是有些区别,所以赶紧研究了5.x版本内核的驱动管理实现,并整理了当前内核版本的字符设备驱动编写流程。
驱动程序是操作系统很重要组成部分,往往驱动程序的稳定性和支持设备数量,是衡量操作系统性能的重要指标,在Linux中主要有 字符设备和块设备 。
字符设备:
按照字符流的方式顺序访问的设备(串口、键盘等)
块设备:
按照指定的数据块,随机访问数据的设备(硬盘、软盘、CD-ROM等)
使用 ls -l /dev 可以看到当前Linux中的设备列表
字符设备
块设备
我们通过系统调用来使用这些设备提供的功能,如 open、write、read等,系统内核最终通过调用对应设备的驱动程序来完成我们的功能调用。
- Linux字符设备数据结构
字符设备结构体cdev
struct cdev {
struct kobject kobj; //内核对象
struct module *owner; //驱动程序由外部模块进入内核时,owner指驱动程序所属的内核模块实例
const struct file_operations *ops; //包含设备功能函数的结构体
struct list_head list; //设备链表元素,系统中的字符设备通过链表关联。
dev_t dev; //设备号 主设备:从设备号
unsigned int count; //设备被引用数量,当驱动程序卸载时,如果count!=0,内核会阻止卸载。
};
设备操作函数结构体
struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int); //设置设备文件的偏移量位置
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //读设备文件
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //写设备文件
...
设备号(内核使用32位无符号整数存储 主设备号和从设备号)
typedef __kernel_dev_t dev_t;
typedef u32 __kernel_dev_t;
字符设备驱动开发涉及到的内核函数
//内核为设备分配主设备号(可以手动分配,但不建议那样做,容易发生设备号冲突)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
//创建设备类,实际上就是导出到/sys/class
struct class *__class_create(struct module *owner, const char *name,struct lock_class_key *key)
//初始化字符设备结构
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
//添加字符设备到内核(实际就是将cdev添加到内核的设备链表)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
//创建设备节点(用户空间进程udev会根据设备节点,在/dev目录创建设备文件,也可以手工在用户空间通过mkmod命令创建设备文件)
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
//移除设备节点
void device_destroy(struct class *class, dev_t devt)
//从内核删除字符设备
void cdev_del(struct cdev *p)
//从内核删除设备类
void class_destroy(struct class *cls)
//释放注册的主从设备号
void unregister_chrdev_region(dev_t from, unsigned count)
- 向内核注册字符设备驱动流程
- 分配主从设备号
- 初始化字符设备
- 添加字符设备实例到内核
- 创建设备节点
- 删除已注册的字符设备流程
- 删除创建的设备节点
- 删除字符设备实例
- 删除注册的设备类
- 删除主从设备号
一个基于当前最新5.14内核的字符设备驱动程序demo
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/cdev.h>
//设备名称
#define MYCDEV_NAME "mycdev_simple"
//设备类名称
#define MYCDEV_CLASS "mycdev_class"
//设备数量
#define CDEV_NUM 1
//设备类
static struct class *mycdev_class;
//所有设备的数组
static struct cdev cdevs[CDEV_NUM];
static struct device devices[CDEV_NUM];
//设备号
static dev_t dev_num;
static int strLength(const char *s);
static int mycdev_open(struct inode *inode, struct file *file);
static ssize_t mycdev_read(struct file *file, char __user *buff, size_t len , loff_t *pos);
static ssize_t mycdev_write(struct file *, const char __user *buff, size_t len, loff_t *pos);
static int mycdev_release(struct inode *inode, struct file *file);
static struct file_operations op_f={
.read=mycdev_read,
.write=mycdev_write,
.open=mycdev_open,
.release=mycdev_release
};
//模块初始化函数
static int __init simple_chardev_init(void)
{
int i,retVal;
/*设备号*/
dev_t curr_devnum;
/*请求内核分配设备号*/
retVal=alloc_chrdev_region(&dev_num,0,CDEV_NUM,MYCDEV_CLASS);
if(retVal!=0)
{
printk("alloc_chrdev_region error %d\n",retVal);
return retVal;
}
//为字符设备创建设备类,在/sys/class中可见
mycdev_class=class_create(THIS_MODULE,MYCDEV_CLASS);
/*创建字符设备结构并注册到内核*/
for(i=0;i<CDEV_NUM;i++)
{
/*初始化字符设备*/
cdev_init(&cdevs[i],&op_f);
cdevs[i].owner=THIS_MODULE;
/*设置设备号*/
curr_devnum=MKDEV(MAJOR(dev_num),MINOR(dev_num)+i);
/*添加设备到内核*/
retVal=cdev_add(&cdevs[i],curr_devnum,1);
printk("cdev_add retVal %d\n",retVal);
/*创建设备节点*/
devices[i]=*(device_create(mycdev_class,NULL,curr_devnum,NULL,MYCDEV_NAME "%d",i));
printk("device_create %p\n",&devices[i]);
}
return retVal;
}
static void __exit simple_chardev_exit(void)
{
dev_t _devnum;
int i;
for(i=0;i<CDEV_NUM;i++)
{
_devnum=MKDEV(MAJOR(dev_num),MINOR(dev_num)+i);
device_destroy(mycdev_class, _devnum);
cdev_del(&(cdevs[i]));
}
class_destroy(mycdev_class);
unregister_chrdev_region(dev_num,CDEV_NUM);
printk("simple_chardev_exit\n");
return;
}
/*打开设备*/
static int mycdev_open(struct inode *inode, struct file *file)
{
/*打印一条消息到内核日志*/
printk("call mycdev_open\n");
return 0;
}
/*读操作*/
static ssize_t mycdev_read(struct file *file, char __user *buff, size_t len , loff_t *pos)
{
int count,retVal;
const char *s1="from mycdev_simple!";
printk("call mycdev_read\n");
count=strLength(s1);
retVal=copy_to_user(buff,s1,count+1);
if(retVal==-EFAULT)
{
printk("mycdev_read error\n");
return -EFAULT;
}
return count+1;
}
/*写操作*/
static ssize_t mycdev_write(struct file *file, const char __user *buff, size_t len, loff_t *pos)
{
char *buffer;
int retVal;
buffer=NULL;
printk("call mycdev_write\n");
buffer = (char*)kmalloc(len, GFP_KERNEL);
if(buffer==NULL)
{
printk("kmalloc error\n");
return -EFAULT;
}
retVal=copy_from_user(buffer, buff, len);
if(retVal==-EFAULT)
{
printk("mycdev_write error\n");
return -EFAULT;
}
printk("recv %s\n",buffer);
return len;
}
/*关闭文件*/
static int mycdev_release(struct inode *inode, struct file *file)
{
printk("call mycdev_release\n");
return 0;
}
/*计算字符串长度*/
static int strLength(const char *s)
{
int len;
len=0;
while((*s)!=0)
{
s++;
len++;
}
return len;
}
module_init(simple_chardev_init);
module_exit(simple_chardev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxxxxx");
MODULE_DESCRIPTION("内核描述");