【Linux设备驱动】1.字符设备驱动程序框架及相关结构体

程序总体框架

/* 包含相关头文件 */
#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
#define GLOBALMEM_MAJOR 230

static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);

struct globalmem_dev {
	struct cdev cdev;
	unsigned char mem[GLOBALMEM_SIZE];
};

struct globalmem_dev *globalmem_devp;

/* 用于 填充file_operations的函数 */
static int globalmem_open(struct inode *inode, struct file *filp)
{
	filp->private_data = globalmem_devp;
	return 0;
}

static int globalmem_release(struct inode *inode, struct file *filp)
{
	return 0;
}

static long globalmem_ioctl(struct file *filp, unsigned int cmd,
			    unsigned long arg)
{
	return 0;
}

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;

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

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

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

	return ret;
}

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;
}

/* 定义一个file_operations结构体 */
static const struct file_operations globalmem_fops = {
	.owner = THIS_MODULE,
	.read = globalmem_read,
	.write = globalmem_write,
	.unlocked_ioctl = globalmem_ioctl,
	.open = globalmem_open,
	.release = globalmem_release,
};

/* 定义模块入口函数 */
static int __init globalmem_init(void)
{
    /* 分配设备号 */
	int ret;
    
	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 = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
	if (!globalmem_devp) {
		ret = -ENOMEM;
		goto fail_malloc;
	}
	
    /* 设置cdev结构体 */
    int err;
    
	cdev_init(&globalmem_devp->cdev, &globalmem_fops);
	globalmem_devp->cdev.owner = THIS_MODULE;
	err = cdev_add(&globalmem_devp->cdev, devno, 1);
	if (err)
		printk(KERN_NOTICE "Error %d adding globalmem%d", err, 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("xxx");
MODULE_LICENSE("GPL v2");

总体框架可以分为:

  • 驱动模块加载函数
  • 驱动模块卸载函数
  • 操作函数如open、read、write、release函数,填充file_operations结构体

模块加载函数

/* 定义模块入口函数 */
static int __init globalmem_init(void)
{
    /* 分配设备号 */
	int ret;
    
	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 = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
	if (!globalmem_devp) {
		ret = -ENOMEM;
		goto fail_malloc;
	}
	
    /* 设置cdev结构体 */
    int err;
    
	cdev_init(&globalmem_devp->cdev, &globalmem_fops);
	globalmem_devp->cdev.owner = THIS_MODULE;
	err = cdev_add(&globalmem_devp->cdev, devno, 1);
	if (err)
		printk(KERN_NOTICE "Error %d adding globalmem%d", err, 0);
	return 0;

 fail_malloc:
	unregister_chrdev_region(devno, 1);
	return ret;
}
module_init(globalmem_init);

主要完成几件事:申请\分配设备号、cdev结构体的初始化和注册(便于统一管理)。

模块卸载函数

/* 定义模块出口函数 */
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);

主要完成几件事:注销cdev结构体,释放设备号。

具体操作函数

/* 用于 填充file_operations的函数 */
static int globalmem_open(struct inode *inode, struct file *filp)
{
	filp->private_data = globalmem_devp;
	return 0;
}

static int globalmem_release(struct inode *inode, struct file *filp)
{
	return 0;
}

static long globalmem_ioctl(struct file *filp, unsigned int cmd,
			    unsigned long arg)
{
	return 0;
}

static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size,
			      loff_t * ppos)
{
	return 0;
}

static ssize_t globalmem_write(struct file *filp, const char __user * buf,
			       size_t size, loff_t * ppos)
{
	return 0;
}

/* 定义一个file_operations结构体 */
static const struct file_operations globalmem_fops = {
	.owner = THIS_MODULE,
	.read = globalmem_read,
	.write = globalmem_write,
	.unlocked_ioctl = globalmem_ioctl,
	.open = globalmem_open,
	.release = globalmem_release,
};

定义操作函数如open、read、write、release函数,填充file_operations结构体

相关结构体

cdev结构体

// <include/linux/cdev.h>
struct cdev { 
    struct kobject kobj;                  //内嵌的内核对象.
    struct module *owner;                 //该字符设备所在的内核模块的对象指针.
    const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法
    struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表.
    dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.
    unsigned int count;                   //隶属于同一主设备号的次设备号的个数.
};

// 操作cdev结构体的函数

// 对struct cdev结构体做初始化, 最重要的就是建立cdev 和 file_operations之间的连接:
void cdev_init(struct cdev *, const struct file_operations *); 

// 分配一个struct cdev结构,动态申请一个cdev内存,并对cdev进行初始化
struct cdev *cdev_alloc(void);

void cdev_put(struct cdev *p);

// 向内核注册一个struct cdev结构体
int cdev_add(struct cdev *, dev_t, unsigned);

// 向内核注销一个struct cdev结构体
void cdev_del(struct cdev *);

void cd_forget(struct inode *);

字符设备驱动结构cdev介绍 - 知乎 (zhihu.com)

这篇文章对cdev结构体讲解的很清楚。

总的来说就是:

模块加载时会调用cdev_init初始化字符设备结构体,调用cdev_add把这个字符设备添加进内核

模块卸载时调用cdev_del把这个字符设备从内核中销毁

// linux/fs/char_dev.c
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
	int error;

	p->dev = dev;
	p->count = count;

	error = kobj_map(cdev_map, dev, count, NULL,
			 exact_match, exact_lock, p);
	if (error)
		return error;

	kobject_get(p->kobj.parent);

	return 0;
}

// linux/drivers/base/Map.c
struct kobj_map {
	struct probe {
		struct probe *next;
		dev_t dev;
		unsigned long range;
		struct module *owner;
		kobj_probe_t *get;
		int (*lock)(dev_t, void *);
		void *data;
	} *probes[255];
	struct mutex *lock;
};

int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
	     struct module *module, kobj_probe_t *probe,
	     int (*lock)(dev_t, void *), void *data)
{
	unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
	unsigned index = MAJOR(dev);
	unsigned i;
	struct probe *p;

	if (n > 255)
		n = 255;

	p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);

	if (p == NULL)
		return -ENOMEM;

	for (i = 0; i < n; i++, p++) {
		p->owner = module;
		p->get = probe;
		p->lock = lock;
		p->dev = dev;
		p->range = range;
		p->data = data;
	}
	mutex_lock(domain->lock);
	for (i = 0, p -= n; i < n; i++, p++, index++) {
		struct probe **s = &domain->probes[index % 255];
		while (*s && (*s)->range < range)
			s = &(*s)->next;
		p->next = *s;
		*s = p;
	}
	mutex_unlock(domain->lock);
	return 0;
}

简单地说,设备驱动程序通过调用cdev_add把它所管理的设备对象的指针嵌入到一个类型为struct probe的节点之中,然后再把该节点加入到cdev_map(kobj_map )所实现的哈希链表中。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。

kobj_map函数中哈希表的实现原理和后面注册分配设备号中的几乎完全一样,通过要加入系统的设备的主设备号major(major=MAJOR(dev))来获得probes数组的索引值i(i = major % 255),然后把一个类型为struct probe的节点对象加入到probes[i]所管理的链表中。

file_oparations结构体

file_operation就是把系统调用和驱动程序关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

在通读file_operations 方法的列表时, 不少参数包含字串 __user. 这种注解是一种文档形式, 注意, 一个指针是一个不能被直接解引用的用户空间地址. 对于正常的编译,__user没有效果, 但是它可被外部检查软件使用来找出对用户空间地址的错误使用。

struct file_operations被定义在include/linux/fs.h

// include/linux/fs.h
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);
	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 *);
	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 *);
	void (*mremap)(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 **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
};

**struct module *owner:**是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为**THIS_MODULE**, 一个在 <linux/module.h> 中定义的宏.

设备号

一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
linux内核中,设备号用dev_t来描述。

typedef u_long dev_t;

在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。
因此,主设备号范围为2^12 = 4096 = 4K,次设备号范围为2^20 = 1048576 = 1M
1)从dev_t设备号中提取major和minor
MAJOR(dev_t dev)
MINOR(dev_t dev)
2)使用下列宏则可以通过主设备号和次设备号生成dev_t,构建设备号:
MKDEV(int major, int minor)

#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))

分配设备号

静态分配:register_chrdev_region()
动态分配:alloc_chrdev_region()

register_chrdev_region()函数用于已知起始设备的设备号的情况,而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数dev中。alloc_chrdev_region()相比于register_chrdev_region()的优点在于它会自动避开设备号重复的冲突。

静态分配和动态分配的函数都调用了__register_chrdev_region函数

// linux\fs\char_dev.c
static struct kobj_map *cdev_map;

static DEFINE_MUTEX(chrdevs_lock);

static struct char_device_struct {
	struct char_device_struct *next;
	unsigned int major;
	unsigned int baseminor;
	int minorct;
	char name[64];
	struct cdev *cdev;		/* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; // 全局的哈希表

static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
			   int minorct, const char *name)
{
	struct char_device_struct *cd, **cp;
	int ret = 0;
	int i;
	
	cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
	if (cd == NULL)
		return ERR_PTR(-ENOMEM);
	// 上锁,防止并发访问chrdevs全局数组
	mutex_lock(&chrdevs_lock);

	/* 如果major为0,那么寻找第一个未被占用的设备号 */
	if (major == 0) {
		for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
			if (chrdevs[i] == NULL)
				break;
		}

		if (i == 0) {
			ret = -EBUSY;
			goto out;
		}
		major = i;
	}
	
	cd->major = major;
	cd->baseminor = baseminor; // 起始次设备号
	cd->minorct = minorct; // 次设备号的数量
	strlcpy(cd->name, name, sizeof(cd->name));
	
    // 插入设备到链表
	i = major_to_index(major);

    // 寻找合适的插入位置
	for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
		if ((*cp)->major > major ||
		    ((*cp)->major == major &&
		     (((*cp)->baseminor >= baseminor) ||
		      ((*cp)->baseminor + (*cp)->minorct > baseminor))))
			break;

	/* 检查次设备号范围重叠  */
	if (*cp && (*cp)->major == major) {
		int old_min = (*cp)->baseminor;
		int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
		int new_min = baseminor;
		int new_max = baseminor + minorct - 1;
		
        // 分别判断左半边和右半边是否重叠
		/* New driver overlaps from the left.  */
		if (new_max >= old_min && new_max <= old_max) {
			ret = -EBUSY;
			goto out;
		}

		/* New driver overlaps from the right.  */
		if (new_min <= old_max && new_min >= old_min) {
			ret = -EBUSY;
			goto out;
		}
	}
	// 插入设备并解锁
	cd->next = *cp;
	*cp = cd;
	mutex_unlock(&chrdevs_lock);
	return cd;
out: // 错误处理
	mutex_unlock(&chrdevs_lock);
	kfree(cd);
	return ERR_PTR(ret);
}

在这里插入图片描述

linux采用哈希表(chrdevs全局的指针数组)维护设备号。对于主设备号major,通过major_to_index哈希函数(major % 255)计算哈希表的键值作为数组下标i,在以数组下标i的指针元素构成的单链表中寻找合适的插入位置进行插入。

插入点计算如下代码:

// 寻找合适的插入位置
	for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
		if ((*cp)->major > major ||
		    ((*cp)->major == major &&
		     (((*cp)->baseminor >= baseminor) ||
		      ((*cp)->baseminor + (*cp)->minorct > baseminor))))
			break;

首先根据主设备号进行判断,保证主设备号是在链表中是递增的;如果主设备号相等,那么就判断次设备号,保证次设备号也是递增的。

另外,全局的chrdevs数组作为哈希表属于共享资源,需要保证互斥访问,因此使用一个互斥锁进行保护。

注销设备号

void unregister_chrdev_region(dev_t from, unsigned count)
void unregister_chrdev_region(dev_t from, unsigned count)
{
	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	}
}

static struct char_device_struct *
__unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct)
{
	struct char_device_struct *cd = NULL, **cp;
	int i = major_to_index(major);

    // 上锁
	mutex_lock(&chrdevs_lock);
    // 在chrdevs链表中寻找匹配的字符设备结构体
	for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
		if ((*cp)->major == major &&
		    (*cp)->baseminor == baseminor &&
		    (*cp)->minorct == minorct)
			break;
    // 如果找到了,就从链表中移除
	if (*cp) {
		cd = *cp;
		*cp = cd->next;
	}
    // 解锁
	mutex_unlock(&chrdevs_lock);
	return cd;
}

注销设备号的过程其实就是分配设备号的反过程,先对哈希表chrdevs上锁,然后寻找要注销的设备号对应的链表节点,然后将其从单链表中移除。

注意:设备号通过哈希表进行管理,cdev字符设备结构体也是通过哈希表进行管理。

创建设备文件

相关推荐

  1. linux驱动字符设备驱动框架

    2024-05-25 18:01:21       59 阅读
  2. linux 字符设备驱动程序介绍

    2024-05-25 18:01:21       54 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-25 18:01:21       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-25 18:01:21       101 阅读
  3. 在Django里面运行非项目文件

    2024-05-25 18:01:21       82 阅读
  4. Python语言-面向对象

    2024-05-25 18:01:21       91 阅读

热门阅读

  1. el-tree 后端返回的树形结构重命名数据循环

    2024-05-25 18:01:21       32 阅读
  2. Spring: OncePerRequestFilter

    2024-05-25 18:01:21       30 阅读
  3. 缪尔赛思又来到了你的面前(哈希)

    2024-05-25 18:01:21       26 阅读
  4. #php把pdf文件转成图片#

    2024-05-25 18:01:21       32 阅读
  5. 在已创建的git工程中添加.gitignore

    2024-05-25 18:01:21       36 阅读
  6. git忽略文件不生效解决方案

    2024-05-25 18:01:21       36 阅读