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函数将字符串读出来
相关推荐
布鲁格若门1 分钟前
CentOS 7 桌面版安装 cuda 12.4
linux·运维·centos·cuda
C-cat.8 分钟前
Linux|进程程序替换
linux·服务器·microsoft
怀澈12210 分钟前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
DC_BLOG12 分钟前
Linux-Apache静态资源
linux·运维·apache
学Linux的语莫14 分钟前
Ansible Playbook剧本用法
linux·服务器·云计算·ansible
skywalk81631 小时前
树莓派2 安装raspberry os 并修改成固定ip
linux·服务器·网络·debian·树莓派·raspberry
C++忠实粉丝1 小时前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
量子网络1 小时前
debian 如何进入root
linux·服务器·debian
我们的五年2 小时前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++
我言秋日胜春朝★2 小时前
【Linux】进程地址空间
linux·运维·服务器