Linux学习笔记(实现一个带read,write,ioctl的字符设备驱动)

运行环境

000

字符设备基础知识

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

1
2
3
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>

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

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
#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这个头文件里的函数,注册和释放。

1
2
3
4
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可以查看申请到的设备名、设备号,这里贴出部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//创建一个字符设备
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下寻找对应的类从而创建设备节点。

这里贴出部分实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}

实现效果如下图:

001

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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,这就是内核态和用户态相互沟通的方式

这里贴出部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//创建函数,这里就不写出具体实现了
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);

最终效果图

003

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

参考博客:

  1. https://blog.csdn.net/zqixiao_09/article/details/50839042
  2. https://blog.csdn.net/morixinguan/article/details/55002774
------ 本文结束------