编写字符设备驱动程序

一、编写及调试

(一)实验一

linux的设备驱动程序可以由两种形式来定义,一种是全局静态变量,另一种是使用内核提供的API,这里采用第二种方法来实现一个简单的虚拟设备的驱动,并且实现它的读写功能。

内核态代码 device_drive.c:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/cdev.h>
 
#define DEMO_NAME "my_demo_dev"
 	
static dev_t dev;   //设备号,dev_t封装的是u32类型,该类型前12位表示主设备号,后20位表示次设备号
static struct cdev *demo_cdev;	//字符设备
static signed count = 1;
 
 
//在demodrv_open操作中打印主次设备号
static int demodrv_open(struct inode *inode, struct file *file)
{   
    //Linux内核提供的读取主设备号和次设备号的方法
	int major = MAJOR(inode->i_rdev);   
	int minor = MINOR(inode->i_rdev);
 
	printk("%s: major=%d, minor=%d\n",__func__,major,minor);  //__func__宏获取当前的函数名
 
	return 0;
}


//在demodrv_read和demodrv_write中仅打印函数名
static ssize_t demodrv_read(struct file *file, char __user *buf,size_t lbuf,loff_t *ppos)
{
	printk("%s enter\n",__func__);   //打印函数名
	
	return 0;
}
 
 
static ssize_t demodrv_write(struct file *file, const char __user *buf,size_t count,loff_t *f_pos)
{
	printk("%s enter\n",__func__);
	
	return 0;
}
 
 //给设备的操作,和在文件系统中使用的是相同的结构体
static const struct file_operations demodrv_fops = {
	.owner = THIS_MODULE,
	.open = demodrv_open,
	.read = demodrv_read,
	.write = demodrv_write
};
 
 
static int __init simple_char_init(void)
{
	int ret;
 
	ret = alloc_chrdev_region(&dev,0,count,DEMO_NAME);		//动态申请设备号
	if(ret)
	{
		printk("failed to allocate char device region\n");
		return ret;
	}
	demo_cdev = cdev_alloc();   //分配空间
	if(!demo_cdev) 
	{
		printk("cdev_alloc failed\n");
		goto unregister_chrdev;
	}
 
	cdev_init(demo_cdev,&demodrv_fops);		//cdev_init初始化cdev,如果定义的是struct cdev结构体而不是指针类型,只需要执行cdev_init()就可以了
 
	ret = cdev_add(demo_cdev,dev,count);	//把这个设备添加到系统中
	if(ret)
	{
		printk("cdev_add failed\n");
		goto cdev_fail;
	}
 
	printk("successed register char device: %s\n",DEMO_NAME);
	printk("Major number = %d,minor number = %d\n",MAJOR(dev),MINOR(dev));
 
	return 0;
 
cdev_fail:
	cdev_del(demo_cdev);
 
unregister_chrdev:
	unregister_chrdev_region(dev,count);
 
	return ret;
} 
 
 
static void __exit simple_char_exit(void)
{
	printk("removing device\n");
 
	if(demo_cdev)
		cdev_del(demo_cdev);
 
	unregister_chrdev_region(dev,count);
}
 
module_init(simple_char_init);
module_exit(simple_char_exit);
 
MODULE_LICENSE("GPL");

运行结果:

在这里插入图片描述

另外,生成的设备需要在/dev目录下生成对应的节点,这里需要手动生成,使用mknod命令,执行指令 sudo mknod /dev/demo_drv c 240 0,c代表字符设备,240是主设备号,0是此设备号,然后执行指令 ll /dev 查看dev目录的情况:

在这里插入图片描述

接下来使用用户空间的测试程序来测试这个字符设备驱动:

test.c 代码如下:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
 
#define DEMO_DEV_NAME "/dev/demo_drv"		//定义设备的路径
 
int main() 
{
	char buffer[64];
	int fd;
 
	fd = open(DEMO_DEV_NAME,O_RDONLY);		//O_RDONLY: 以只读的方式打开
	if(fd<0) 
	{
		printf("open device %s failed\n",DEMO_DEV_NAME);
		return -1;
	}
 
	read(fd,buffer,64);
	close(fd);
 
	return 0;
}

执行该测试程序,打印内核消息:

在这里插入图片描述

打印出了open和read的方法

(二)实验二

字符设备驱动也可以采用misc机制来进行注册,也就是Linux将一些不符合预先确定的字符设备划分为杂项设备,这类设备的主设备号是10,内核中使用miscdevice结构体来描述,如果使用misc机制来创建设备,就需要定义miscdevice结构体

内核模块 drive2.c:

# include <linux/module.h>
# include <linux/fs.h>
# include <linux/uaccess.h>
# include <linux/init.h>
# include <linux/cdev.h>
//加入misc机制
# include <linux/miscdevice.h>
# include <linux/kfifo.h>
 
DEFINE_KFIFO(mydemo_fifo,char,64);	//定义和初始化一个fifo
 
//设备名
# define DEMO_NAME "my_demo_dev"
 
static struct device *mydemodrv_device;
 
static int demodrv_open(struct inode *inode, struct file *file)
{
	int major = MAJOR(inode->i_rdev);
	int minor = MINOR(inode->i_rdev);
 
	printk("%s: major=%d, minor=%d\n",__func__,major,minor);
 
	return 0;
}
 
 
static ssize_t demodrv_read(struct file *file, char __user *buf,size_t count,loff_t *ppos)
{
	int actual_readed;
	int ret;
 
	ret = kfifo_to_user(&mydemo_fifo,buf, count, &actual_readed);
	if(ret)
		return -EIO;
 
	printk("%s,actual_readed=%d,pos=%lld\n",__func__,actual_readed,*ppos);
 
	return actual_readed;
}
 
 
static ssize_t demodrv_write(struct file *file, const char __user *buf,size_t count,loff_t *ppos)
{
	unsigned int actual_write;
	int ret;
 
	ret = kfifo_from_user(&mydemo_fifo,buf, count, &actual_write);
	if(ret)
		return -EIO;
 
	printk("%s: actual_write=%d,ppos=%lld\n",__func__,actual_write,*ppos);
 
	return actual_write;
}
 
 
static const struct file_operations demodrv_fops = {
	.owner = THIS_MODULE,
	.open = demodrv_open,
	.read = demodrv_read,
	.write = demodrv_write,
};
 
static struct miscdevice mydemodrv_misc_device = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = DEMO_NAME,
	.fops = &demodrv_fops,  //设备相应的操作
};
 
static int __init simple_char_init(void)
{
	int ret;
 
	ret = misc_register(&mydemodrv_misc_device);	//misc_register()函数来注册,可以自动创建设备结点,不需要mknod来手动创建设备节点,传入的参数是定义的miscdevice结构体的地址
	if(ret)
	{
		printk("failed register misc device\n");
		return ret;
	}
 
	mydemodrv_device = mydemodrv_misc_device.this_device;
 
	printk("successed register char device: %s\n",DEMO_NAME);
 
	return 0;
} 
 
 
static void __exit simple_char_exit(void)
{
	printk("removing device\n");
 
	misc_deregister(&mydemodrv_misc_device);
}
 
module_init(simple_char_init);
module_exit(simple_char_exit);
 
MODULE_LICENSE("GPL");

运行结果:

在这里插入图片描述

查看dev目录:

在这里插入图片描述

用户态测试程序test.c:

# include <stdio.h>
# include <fcntl.h>
# include <unistd.h>
# include <malloc.h>
# include <string.h>
 
# define DEMO_DEV_NAME "/dev/my_demo_dev"	//设备路径
 
int main() 
{
	char buffer[64];
	int fd;
	int ret;
	size_t len;
 
	char message[] = "hello";
	char *read_buffer;
 
	len = sizeof(message);
 
	fd = open(DEMO_DEV_NAME,O_RDWR);
	if(fd<0) 
	{
		printf("open device %s failed\n",DEMO_DEV_NAME);
		return -1;
	}
 
	ret = write(fd,message,len);	//向设备写数据
	if(ret != len)
	{
		printf("cannot write on device %d,ret=%d\n",fd,ret);
		return -1;
	}
 
	read_buffer = malloc(2*len);
	memset(read_buffer,0,2*len);	//memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值。
	ret = read(fd,read_buffer,2*len);	//把数据从设备读到read_buffer
	printf("read %d bytes\n",ret);
	printf("read buffer=%s\n",read_buffer);
 
	close(fd);
 
	return 0;
}

运行该测试程序:

在这里插入图片描述

打印内核消息:

在这里插入图片描述

二、源码学习

//在linux内核中,字符设备是由cdev结构体来描述的
struct cdev {
	struct kobject kobj;
	struct module *owner;	//所属模块
	const struct file_operations *ops;		//文件操作结构体
	struct list_head list;		//list字段将所有的字符设备组织成一个链表
	dev_t dev;		//dev是字符设备的设备号,包括主设备号和次设备号
	unsigned int count;		//count字段是同一个主设备号中次设备号的个数
} __randomize_layout;
#define MINORBITS	20		//次设备号的位数是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))		//生成设备号
static struct char_device_struct {
	struct char_device_struct *next;	//next成员实现这些结构体之间的关联,在系统中主要是完成该类型变量的链接
	unsigned int major;		//major表示字符设备的主设备号
	unsigned int baseminor;		//baseminor表示起始次设备号
	int minorct;		//minorct表示该字符设备支持的最大次设备个数
	char name[64];
	struct cdev *cdev;		/* will die */	//cdev指针指向该字符设备对应的设备抽象结构体变量
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

char_device_struct实现了对已注册的字符设备区间的管理,并完成与cdev的关联

cdev主要用于实现该cdev的引用计数、与sysfs的关联。

三、问答

1.分别解释一下设备驱动和文件系统

​ 设备驱动为第一层抽象,也就是把柱面、磁道、扇区这样的三维数据转换为一维线性地址空间中的块

​ 文件系统则是在第一层抽象的基础上进行第二层抽象,即将块抽象和组织为文件系统

2.从应用程序的open(),read(),write()系统调用出发,说明如何陷入内核?

在这里插入图片描述

3.file_operations()这个结构体起什么作用?

file_operations结构体完成设备读取、写入、保存等等这些操作,都是由存储在file_operations结构体中的这些函数指针来处理的,这些函数指针所指向的函数都需要我们在驱动模块将其实现。

struct file_operations {
	struct module *owner;    //拥有该结构的模块的指针,一般为THIS_MODULES
	......
	int (*mmap) (struct file *, struct vm_area_struct *);    //用于请求将设备内存映射到进程地址空间
	......
	int (*open) (struct inode *, struct file *);    //打开
	......
}

4.从应用程序的open(),read(),write()到调用file_operations()中对应的函数,说出你对如下个图的理解:

在这里插入图片描述

在这里插入图片描述

5.设备号在驱动程序中起什么作用?为什么要有主设备号和次设备号?

每一个文件是一个设备,设备号是由主设备号和次设备号组成

主设备号:

  1. 一般认为一个主设备号对应一个驱动程序
  2. 一个驱动程序可以管理多个设备,即一个主设备号对应多个设备
  3. 一个主设备号也可以对应多个驱动程序

次设备号:

  1. 一个次设备号对应一个设备

一个驱动程序可以管理多个此类型的设备,设备数可以有2的20次方个,原因是次设备号有20位,不过实际不可能有这么多设备

6.驱动程序的注册和注销函数是做什么?为什么要进注册和注销?

字符设备驱动程序的注册函数:

static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}
/*__register_chrdev() - 创建并注册一个占有较小大小的字符设备
  major:主设备号,当用户设置为0时,内核会动态分配一个设备号。
  baseminor: 次设备号,要在一定范围内从0开始
  count: 次设备号的范围
  name: 设备名称
  fops: 文件系统的接口指针
  
  如果major == 0,此函数将动态分配一个major并返回它的编号。
  如果major > 0,此函数将尝试使用给定的主设备号来保留设备,成功时将返回0。
  
  失败时返回-ve errno。
*/
int __register_chrdev(unsigned int major, unsigned int baseminor,
		      unsigned int count, const char *name,
		      const struct file_operations *fops)
{
	struct char_device_struct *cd;
	struct cdev *cdev;
	int err = -ENOMEM;

	cd = __register_chrdev_region(major, baseminor, count, name);	//注册一个主设备号和一个一定范围内具体的次设备号
	if (IS_ERR(cd))
		return PTR_ERR(cd);

	cdev = cdev_alloc();	//分配空间
	if (!cdev)
		goto out2;
	//对cdev进行赋值操作
	cdev->owner = fops->owner;
	cdev->ops = fops;
	kobject_set_name(&cdev->kobj, "%s", name);

	err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);	//把这个设备添加到系统中
	if (err)
		goto out;

	cd->cdev = cdev;	//完成cd与cdev的关联

	return major ? 0 : cd->major;
out:
	kobject_put(&cdev->kobj);
out2:
	kfree(__unregister_chrdev_region(cd->major, baseminor, count));
	return err;
}

字符设备驱动程序的注销函数:

static inline void unregister_chrdev(unsigned int major, const char *name)
{
	__unregister_chrdev(major, 0, 256, name);
}
void __unregister_chrdev(unsigned int major, unsigned int baseminor,
			 unsigned int count, const char *name)
{
	struct char_device_struct *cd;

	cd = __unregister_chrdev_region(major, baseminor, count);
	if (cd && cd->cdev)
		cdev_del(cd->cdev);
	kfree(cd);
}

注册是为了生成相应的设备驱动程序,以便使用

注销是为了释放内存,以便其他进程的运行

猜你喜欢

转载自blog.csdn.net/qq_58538265/article/details/133915966