Linux字符设备驱动

一、字符设备驱动结构

1. cdev结构体

在Linux内核中,使用cdev结构体来描述一个字符设备

c 复制代码
struct cdev {
	struct kobject kobj;	//内嵌kobject对象
	struct module *owner;	//所属的模块
	const struct file_operations *ops;	//该设备的文件操作结构体
	struct list_head list;
	dev_t dev;	//设备号
	unsigned int count;
};

cdev相关的操作

cdev_init: 初始化cdev的函数,实际上就是将cdevfile_operation进行关联

c 复制代码
void cdev_init(struct cdev *cdev, struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops; /*  将传入的文件操作结构体指针赋值给 cdev 的 ops*/
}

cdev_alloc: 动态申请一个cdev

c 复制代码
struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&p->list);
		kobject_init(&p->kobj, &ktype_cdev_dynamic);
	}
	return p;
}

cdev_add/cdev_del: 向内核中添加/删除cdev,即对设备的注册和注销。通常发生在加载/卸载模块时

设备号

cdev结构体的dev_t定义了设备号,前12bit代表主设备号,后20bit代表次设备号。同一驱动可支持多个同类设备,因此同一类设备一般使用相同的主设备号,次设备号从0开始用来描述驱动的不同设备序号

c 复制代码
MKDEV(int major, int minor)		//组成一个设备号
MAJOR(dev_t dev)	//获取主设备号
MINOR(dev_t dev)	//获取次设备号

在调用cdev_add函数注册设备之前,需要先申请设备号

c 复制代码
//已知设备号时使用,直接申请
int register_chrdev_region(dev_t from, unsigned count, const char *name);
//未知设备号时使用,向内核申请一个未被占用的设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name);

在调用cdev_del函数注销设备前,需要释放设备号

c 复制代码
void unregister_chrdev_region(dev_t from, unsigned count);

2. file_operation结构体

应用程序调用的open/read/write等函数,最终时调用的对应设备的file_operation结构体中的对应函数,所以驱动程序设计的主题内容就是实现file_operation结构体中的成员函数

c 复制代码
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*iterate) (struct file *, struct dir_context *);
	unsigned int (*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 *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,
	unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
	loff_t len);
	int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

llseek(): 修改一个文件的当前读写位置
read()/write(): 向设备中读/写数据
unlocked_ioctl(): 提供设备相关控制命令的实现
mmap(): 将设备内存映射到进程的虚拟内存
open()/release(): 打开/关闭设备。

3. 组成

加载/卸载函数

字符设备驱动模块加载函数中,需要实现设备号的申请和cdev的注册

c 复制代码
static int __init xxx_init(void)
{
	...
	/*  初始化 cdev */
	cdev_init(&xxx_dev.cdev, &xxx_fops); 
	xxx_dev.cdev.owner = THIS_MODULE;
	
	/*  获取字符设备号 */
	if (xxx_major) {
	register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
	} else {
	alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
	}
	
	/*  注册设备 */
	ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); 
	...
}

卸载时需要释放设备号和注销设备

c 复制代码
static void _ _exit xxx_exit(void)
{
	unregister_chrdev_region(xxx_dev_no, 1); /*  释放占用的设备号 */
	cdev_del(&xxx_dev.cdev); /*  注销设备 */
	...
}

file_operations结构体成员函数

大多数字符设备会实现read()、write()、ioctl()函数。由于用户空间不能直接访问内核空间的内存,所以使用copy_from_user()copy_to_user()进行交互

c 复制代码
/*  读设备 */
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t*f_pos)
{
	...
	copy_to_user(buf, ..., ...);
	...
}
/*  写设备 */
ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
	...
	copy_from_user(..., buf, ...);
	...
}
/* ioctl 函数 */
long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	...
	switch (cmd) {
		case XXX_CMD1:
		...
		break;
		case XXX_CMD2:
		...
		break;
		default:
		/*  不能支持的命令 */
		return - ENOTTY;
	}
	return 0;
}

驱动程序结构

二、实例

实例是宋宝华的Linux设备驱动开发详解里的例子。实现了一个虚拟的globalmem设备的驱动程序,这个设备会分配一块内存空间,主要是实现对这块内存的相关驱动函数
代码实现

c 复制代码
/*
 * a simple char device driver: globalmem without mutex
 *
 * Copyright (C) 2014 Barry Song  (baohua@kernel.org)
 *
 * Licensed under GPLv2 or later.
 */

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define GLOBALMEM_SIZE	0x1000		//要分配的内存空间大小
#define MEM_CLEAR 0x1		//清内存cmd
#define GLOBALMEM_MAJOR 230		//主设备号

static int globalmem_major = GLOBALMEM_MAJOR;	//定义主设备号
module_param(globalmem_major, int, S_IRUGO);	//可以接受传参来定义主设备号

//定义globalmem设备的类型
struct globalmem_dev {
	struct cdev cdev;	//所有字符设备都必须包含的结构体
	unsigned char mem[GLOBALMEM_SIZE];	//不同设备可以自定义不同的变量
};

//定义一个globalmem设备
struct globalmem_dev *globalmem_devp;

//globalmem设备的open函数,filep代表打开动作的状态,inode代表文件本身的属性
static int globalmem_open(struct inode *inode, struct file *filp)
{
	//将globalmem设备结构体传给对应的file,以便后续其他函数使用globalmem_devp结构体
	filp->private_data = globalmem_devp;	
	return 0;
}

//当所有调用globalmem的进程都关闭时,才会调用release
static int globalmem_release(struct inode *inode, struct file *filp)
{
	return 0;
}

//可以接收MEM_CLEAR 命令,里面实现相关对应命令的操作
static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct globalmem_dev *dev = filp->private_data;	//通过传入的file来获取globalmem_dev 

	switch (cmd) {
	case MEM_CLEAR:
		memset(dev->mem, 0, GLOBALMEM_SIZE);	//对应命令的操作
		printk(KERN_INFO "globalmem is set to zero\n");
		break;

	default:
		return -EINVAL;
	}

	return 0;
}

//当应用程序调用read函数时,最终会调用此函数
static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size, loff_t * ppos)
{
	unsigned long p = *ppos;
	unsigned int count = size;
	int ret = 0;
	struct globalmem_dev *dev = filp->private_data; //通过传入的file来获取globalmem_dev 

	if (p >= GLOBALMEM_SIZE)
		return 0;
	if (count > GLOBALMEM_SIZE - p)
		count = GLOBALMEM_SIZE - p;

	if (copy_to_user(buf, dev->mem + p, count)) {	//将数据从内核空间copy到用户空间
		ret = -EFAULT;
	} else {
		*ppos += count;
		ret = count;

		printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
	}

	return ret;
}

//当应用程序调用write函数时,最终会调用此函数
static ssize_t globalmem_write(struct file *filp, const char __user * buf,
			       size_t size, loff_t * ppos)
{
	unsigned long p = *ppos;
	unsigned int count = size;
	int ret = 0;
	struct globalmem_dev *dev = filp->private_data;

	if (p >= GLOBALMEM_SIZE)
		return 0;
	if (count > GLOBALMEM_SIZE - p)
		count = GLOBALMEM_SIZE - p;

	if (copy_from_user(dev->mem + p, buf, count))
		ret = -EFAULT;
	else {
		*ppos += count;
		ret = count;

		printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
	}

	return ret;
}

static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
	loff_t ret = 0;
	switch (orig) {
	case 0:
		if (offset < 0) {
			ret = -EINVAL;
			break;
		}
		if ((unsigned int)offset > GLOBALMEM_SIZE) {
			ret = -EINVAL;
			break;
		}
		filp->f_pos = (unsigned int)offset;
		ret = filp->f_pos;
		break;
	case 1:
		if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
			ret = -EINVAL;
			break;
		}
		if ((filp->f_pos + offset) < 0) {
			ret = -EINVAL;
			break;
		}
		filp->f_pos += offset;
		ret = filp->f_pos;
		break;
	default:
		ret = -EINVAL;
		break;
	}
	return ret;
}

//定义file_operation结构体并填充成员函数
static const struct file_operations globalmem_fops = {
	.owner = THIS_MODULE,
	.llseek = globalmem_llseek,
	.read = globalmem_read,
	.write = globalmem_write,
	.unlocked_ioctl = globalmem_ioctl,
	.open = globalmem_open,
	.release = globalmem_release,
};

//初始化并向内核注册globalmem_dev设备
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
	//创建一个设备号
	int err, devno = MKDEV(globalmem_major, index);

	//关联cdev和file_operations
	cdev_init(&dev->cdev, &globalmem_fops);
	dev->cdev.owner = THIS_MODULE;
	//注册cdev到内核
	err = cdev_add(&dev->cdev, devno, 1);
	if (err)
		printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
}

//模块初始化函数
static int __init globalmem_init(void)
{
	int ret;
	//创建一个次设备号为0的设备号
	dev_t devno = MKDEV(globalmem_major, 0);

	//向内核注册设备号
	if (globalmem_major)
		ret = register_chrdev_region(devno, 1, "globalmem");
	else {
		ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
		globalmem_major = MAJOR(devno);
	}
	if (ret < 0)
		return ret;

	//申请一块globalmem_devp内存并清0
	globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
	if (!globalmem_devp) {
		ret = -ENOMEM;
		goto fail_malloc;
	}
	
	//初始化并向内核注册globalmem_dev设备
	globalmem_setup_cdev(globalmem_devp, 0);
	return 0;

 fail_malloc:
	unregister_chrdev_region(devno, 1);
	return ret;
}
//模块加载时会调用此函数
module_init(globalmem_init);

//模块卸载函数
static void __exit globalmem_exit(void)
{
	cdev_del(&globalmem_devp->cdev);	//从内核中注销设备
	kfree(globalmem_devp);	//释放申请的设备内存
	unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);	//释放设备号
}
module_exit(globalmem_exit);

MODULE_AUTHOR("Barry Song <baohua@kernel.org>");
MODULE_LICENSE("GPL v2");

验证

shell 复制代码
### 加载驱动 ###
sudo insmod globalmem.ko	#加载驱动
lnsmod	#查看是否已经被加载
cat /proc/devices	#可以看到多出的主设备号为230名字为globalmem的字符设备驱动
mknod /dev/globalmem c 230 0	#创建/dev/globalmem设备节点

### 读写验证 ###
echo "hello world" > /dev/globalmem	#将会调用write函数将hello wrold写进去
cat /dev/globalmem	#将会调用read函数将字符串读出来
相关推荐
HLC++7 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室8 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy11 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕12 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫13 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生13 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1114 小时前
linux基础IO
linux·运维·服务器
BelongPanda16 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯16 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983417 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器