Linux驱动 实现一个字符设备驱动

By | 2019-09-23

================================================================
Linux驱动 编译一个hello模块
Linux驱动 实现一个字符设备驱动
Liunx驱动 实现一个misc设备驱动
================================================================

字符设备基础知识

写一个简单的linux字符设备驱动程序,需要内核里的以下几个头文件,因为需要调用一些基本的宏和一些基本的函数来使用

#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>

进入Linux内核源码,进入include/linux/,打开cdev.h

struct cdev {
	struct kobject kobj;//设备模型相关的
	struct module *owner;//所属于哪个模块--->THIS MODULE
	//利用file_operations跟用户态进行操作--->有open , read , write 等方法
	const struct file_operations *ops;
	struct list_head list;//链表,将设备插入到一条链表里去
	dev_t dev;/通过设备号匹配对应的驱动
	unsigned int count;//要注册字符设备的个数
} __randomize_layout;

里面还有部分函数,我们暂时只需要cdev_init,cdev_add,cdev_del
打开kdev_t.h

#define MINORBITS	20
#define MINORMASK	((1U << MINORBITS) - 1)
//从设备号中取出主设备号
#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
//从设备号中取出次设备号
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
//创建一个设备号
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

创建设备号就需要kdev_t这个宏,创建设备号后还要对设备进行注册,这时候需要fs.h这个头文件里的函数,注册和释放

extern int register_chrdev_region(dev_t, unsigned, const char *);
//动态分配设备号,由内核给我们分配一个设备号,这个设备号是内核自动分配的,就不需要我们去使用MKDEV这个宏来进行手动
extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
extern void unregister_chrdev_region(dev_t, unsigned);

编写简单的字符设备需要以下步骤:

  1. 创建设备号
  2. 注册设备号
  3. 退出驱动时,注销设备

创建设备文件,利用cat /proc/devices可以查看申请到的设备名、设备号,这里贴出部分代码:

//创建一个字符设备
struct char_dev
{
    struct cdev c_dev;
    dev_t dev_no;
    char buf[1024];
};
struct char_dev *my_dev;
static int __init cdev_test_init(void)
{  
    int ret;
    //创建设备号->主设备号,次设备号
    //dev_no = MKDEV(222,2);
    //注册设备号
    //ret = register_chrdev_region(dev_no,1,"my_dev");
    //1.给字符设备分配内存空间
    my_dev = kmalloc(sizeof(*my_dev),GFP_KERNEL);
    //2.自动申请设备号并注册字符设备
    ret = alloc_chrdev_region(&my_dev->dev_no,1,1,"my_dev");
    //3.初始化字符设备
    cdev_init(&my_dev->c_dev, &my_ops);
    //4.添加一个字符设备
    ret = cdev_add(&my_dev->c_dev, my_dev->dev_no, 1);
    return 0;
}

自动创建设备节点

利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置
在驱动用加入对udev 的支持主要做的就是:在驱动初始化的代码里调用class_create(…)为该设备创建一个class,再为每个设备调用device_create(…)创建对应的设备
内核中定义的struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用 device_create(…)函数来在/dev目录下创建相应的设备节点
这样,加载模块的时候,用户空间中的udev会自动响应 device_create()函数,去/sysfs下寻找对应的类从而创建设备节点

这里贴出部分实现代码:

struct device *my_device;
    
    ......
    //5.为该设备创建一个class
    //这个类存放于sysfs下面,调用device_create函数时会在/dev目录创建相应的设备节点
    cls = class_create(THIS_MODULE, "myclass");//sys/devices/virtual/myclass/my_dev
    if(IS_ERR(cls))
    {
        unregister_chrdev_region(my_dev->dev_no,1);
        return -EBUSY;
    }
    //6.创建对应的设备节点
    //加载模块时,用户空间的udev会自动响应该函数,去/sysfs下寻找对应的类创建设备节点
    my_device = device_create(cls,NULL,my_dev->dev_no,NULL,"my_dev");//mknod /dev/my_dev
    if(IS_ERR(my_device))
    {
        class_destroy(cls);
        unregister_chrdev_region(my_dev->dev_no,1);
        return -EBUSY;
    }

实现效果如下图:

为驱动添加open()、read()、write()、ioctl()函数

当一个字符设备被注册后,我们随即就要来操作这个字符设备,open , read , write , close等操作,需要file_operations这个结构体:

static const struct file_operations __fops = {				\
	.owner	 = THIS_MODULE,						\
	.open	 = __fops ## _open,					\
	.release = simple_attr_release,					\
	.read	 = simple_attr_read,					\
	.write	 = simple_attr_write,					\
	.llseek	 = generic_file_llseek,					\
}

struct file_operations {
	struct module *owner;
	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 *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	......

那么内核是如何去识别相应的函数呢?
是通过系统调用 , 在上层应用程序,打个比方 .
通过open()打印相应的设备,那么syscall函数就会通过系统调用号识别到内核态里的函数,进而调用到我们这里实现的my_open,这就是内核态和用户态相互沟通的方式

部分代码:

//创建函数,这里就不写出具体实现了
int my_open(struct inode *inode, struct file *file){...}
int my_close(struct inode *inode, struct file *file){...}
ssize_t my_read(struct file *file,char __user *buf,size_t len,loff_t *pos){...}
ssize_t my_write(struct file *file,const char __user *buf,size_t len,loff_t *pos){...}
long my_ioctl(struct file *file,unsigned int cmd,unsigned long arg){...}
struct file_operations my_ops = {
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .unlocked_ioctl = my_ioctl,
    .release = my_close,
};
//初始化ops
cdev_init(&my_dev->c_dev, &my_ops);

最终效果图 :

Linux字符设备驱动完成!

项目源码地址:https://github.com/huchanghui123/my_cdev

参考链接:
https://blog.csdn.net/zqixiao_09/article/details/50839042
https://blog.csdn.net/morixinguan/article/details/55002774

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注