linux字符设备驱动模型简介及其实现方法(globalmem例程)

环境:主机-Ubuntu 16.04,开发板-友善之臂tiny4412开发板,内核版本linux-3.5

参考《Linux设备驱动开发详解基于最新的Linux 4.0内核》(宋宝华编著)

字符设备驱动,在Linux设备驱动中较为基础,本文将大致分析Linux字符设备驱动的整体结构,并编写简单的驱动模板。

 

字符设备:在I/O传输过程中以字符为单位串行顺序进行传输的设备,即以一个字节一个字节进行读写操作的设备,是面向字节流的,如鼠标、键盘、串口、GPIO等。

 

1、字符设备驱动结构

在Linux内核中,使用struct cdev结构体来描述一个字符设备,其定义如下:

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

cdev结构体的dev_t成员定义了设备号,为32位(高12位为主设备号,低20位为次设备号)。设备号相关宏如下:

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))    //获取主设备号
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))     //获取次设备号
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))         //指定主、次设备号生成设备号

关于设备号,有两个重要的函数,分别完成设备号的分配与释放:

/* 向系统申请设备号,已知起始设备号的情况 */
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);

/* 释放所申请的设备号 */
void unregister_chrdev_region(dev_t from, unsigned count);

Linux内核提供了一组函数以用于操作cdev结构体:

/* 初始化cdev结构的成员,并建立cdev和file_operation之间的连接 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops);

/* 动态申请一个cdev结构内存 */
struct cdev *cdev_alloc(void);

/* 向系统添加一个字符设备cdev */
int cdev_add(struct cdev *p, dev_t dev, unsigned count);

/* 向系统删除一个字符设备cdev */
void cdev_del(struct cdev *p);

 

2、struct file_operations文件操作结构体

cdev结构体中的成员file_operation非常重要,其定义了字符设备驱动提供给虚拟文件系统接口函数,大部分的对设备操作函数都要经过这个结构体,是连接应用程序(用户空间)与驱动程序(内核空间)的纽带

定义:

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 (*readdir) (struct file *, void *, filldir_t);
	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);
};

用户空间的open()/close()/read()/write()/ioctl()等函数分别与file_operations中的open()/close()/read()/write()/ioctl()相对应。

其流程一般如下:

(1)应用程序调用open()打开设备时,最终会调用到驱动的file_operations结构的open指向的函数;

(2)应用程序调用read/write/ioctl等函数时,最终分别调用到驱动的file_operations结构的read/write/ioctl等指向的函数;

(3)应用程序调用close()关闭设备时,最终会调用到驱动的file_operations结构的close指向的函数;

所以,用户空间要对设备驱动做操作,都要通过这个file_operations调用到相应的驱动函数。

大致层次调用示意图如下:

 

3、字符设备驱动编写步骤

(1)编写linux内核模块的加载与卸载函数;

(2)file_operation结构体赋值;

(3)注册cdev;

(4)实现file_operation中的read()/write()/ioctl()等函数;

(5)编写Makefile;

(6)编译;

(7)通过命令加载及测试;

 

本文先以一个虚拟设备globalmem(全局内存)为例,在globalmem字符设备驱动中会分配一块N字节内存空间,并在驱动中提供对这块内存的读写、控制及定位函数,以供用户空间的进程能通过Linux系统调用获取或设置。

(1~4)、globalmem_drv.c文件,代驱动代码如下:

#include <linux/module.h>		// 包含了一些模块相关函数,支持动态添加和卸载模块
#include <linux/fs.h>			// 包含了文件操作相关struct的定义
#include <linux/init.h>			// 包含了一些初始化函数接口,如module_init
#include <linux/cdev.h>			// 包含了cdev 结构及相关函数的定义
#include <linux/slab.h>			// 包含了kcalloc/kzalloc内存分配函数的定义
#include <linux/uaccess.h>		// 包含了copy_to_user/copy_from_user等内核访问用户进程内存地址的函数定义


#define GLOBALMEM_SIZE		32
#define GLOBALMEM_MAJOR		230

/* IOCTL CMD */
#define CMD_MEM_CLEAR			0x0A
#define CMD_MEM_SET				0x0B

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;

/* 打开设备 */
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)
{
	struct globalmem_dev *dev = filp->private_data;
	unsigned char ArgIn = (unsigned char)arg;

	switch(cmd)
	{
		case CMD_MEM_CLEAR:
			memset(dev->mem, 0, GLOBALMEM_SIZE);
			printk(KERN_INFO "globalmem is clear.\n");
			break;

		case CMD_MEM_SET:
			memset(dev->mem, ArgIn, GLOBALMEM_SIZE);
			printk(KERN_INFO "globalmem is set to 0x%02x.\n", ArgIn);
			break;
	
		default:
			printk(KERN_INFO "ioctl cmd is illegal!\n");
			return -EINVAL;
	}

	return 0;
}

/* 读 */
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
	struct globalmem_dev *dev = filp->private_data;
	unsigned long offset = *ppos;	// 相对文件头的偏移
	unsigned int count = size;
	int ret = 0;

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

	/* 从内核空间复制到用户空间 */
	if(copy_to_user(buf, dev->mem +offset, count))
	{
		ret = -EFAULT;
		printk(KERN_INFO"copy_to_user failed!\n");
	}
	else
	{
		*ppos += count;
		ret = count;
		printk(KERN_INFO"read data[%lu]: %s\n", offset, buf);
	}

	return ret;
}

/* 写 */
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
	struct globalmem_dev *dev = filp->private_data;
	unsigned long offset = *ppos;	// 相对文件头的偏移
	unsigned int count = size;
	int ret = 0;

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

	/* 从用户空间复制到内核空间 */
	if(copy_from_user(dev->mem +offset, buf, count))
	{
		ret = -EFAULT;
		printk(KERN_INFO"copy_from_user failed!\n");
	}
	else
	{
		*ppos += count;
		ret = count;
		printk(KERN_INFO"write data[%d]: %s\n", count, buf);
	}

	return ret;
}

/* 定位/设置读写偏移 */
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
	loff_t ret = 0;
	
	switch(orig)
	{
		case SEEK_SET:		// 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;
			printk(KERN_INFO"llseek SEEK_SET to %llu\n", offset);
			break;

		case SEEK_CUR:	// 1 - 相对当前位置        略~
			printk(KERN_INFO"llseek SEEK_CUR to %llu\n", offset);
			break;

		case SEEK_END:		// 2 - 相对文件尾
			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;
			printk(KERN_INFO"llseek SEEK_END to %llu\n", offset);
			break;

		default:
			ret = -EINVAL;
			break;
	}

	return ret;
}

/* 文件操作结构体 */
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,
};

/* 加载函数 */
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;

	/* 向kernel申请空间 */
	globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
	if(!globalmem_devp)
	{
		ret = -ENOMEM;
		goto fail_malloc;
	}

	cdev_init(&globalmem_devp->cdev, &globalmem_fops);		// 初始化cdev
	globalmem_devp->cdev.owner = THIS_MODULE;
	
	ret = cdev_add(&globalmem_devp->cdev, devno, 1);		// 将cdev添加到kernel
	if(ret)
	{
		goto fail_addcdev;
	}

	printk(KERN_INFO"globalmem_init[major: %d] ok.\n", globalmem_major);
	return 0;

	fail_addcdev:
	kfree(globalmem_devp);

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

/* 卸载函数 */
static void __exit globalmem_exit(void)
{
	cdev_del(&globalmem_devp->cdev);	// 删除cdev设备
	kfree(globalmem_devp);
	unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
	printk(KERN_INFO"globalmem_exit~\n");

}

module_init(globalmem_init);
module_exit(globalmem_exit);

MODULE_AUTHOR("zengzr");
MODULE_LICENSE("GPL v2");

(5)、Makefile文件,如下:

# make to build modules

obj-m := globalmem_drv.o

KERNELDIR ?= /data/arm-linux/kernel/tiny4412/linux-3.5
PWD := $(shell pwd)

all: modules

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	rm -rf *.o *.ko *mod* *.sy* *ord* .*cmd .tmp*

编译:

将驱动C文件和Makefile放在linux主机中某一目录,进入目录进行编译:

#make ,即可编译出“globalmem_drv.ko”驱动模块文件及一些中间文件:

zengzr@zengzr-ubu:/data/project/driver/char_globalmem$ make
make -C /data/arm-linux/kernel/tiny4412/linux-3.5 M=/data/project/driver/char_globalmem modules
make[1]: Entering directory '/data/arm-linux/kernel/tiny4412/linux-3.5'
  CC [M]  /data/project/driver/char_globalmem/globalmem_drv.o
  Building modules, stage 2.
  MODPOST 1 modules
  LD [M]  /data/project/driver/char_globalmem/globalmem_drv.ko
make[1]: Leaving directory '/data/arm-linux/kernel/tiny4412/linux-3.5'

zengzr@zengzr-ubu:/data/project/driver/char_globalmem$ ls
a.out       globalmem_drv.c      globalmem_drv.mod.o  modules.order
app_test    globalmem_drv.ko     globalmem_drv.o      Module.symvers
app_test.c  globalmem_drv.mod.c  Makefile

驱动模块操作命令:加载-insmod、卸载rmmod、列出信息lsmod。

在ARM板上,加载驱动,其加载函数会被调用:(lsmod可查看)

[root@FriendlyARM /mnt]# insmod globalmem_drv.ko 
[ 2699.555000] globalmem_init[major: 230] ok.

还要在/dev上创建节点才能被正常访问,创建设备节点命令:#mknod 名称 类型 主设备号 次设备号

[root@FriendlyARM /mnt]# mknod /dev/globalmem c 230 0

再通过echo和cat命令分别进行写和读操作:

[root@FriendlyARM /mnt]# echo "hello world" > /dev/globalmem 
[ 5864.805000] write data[12]: hello world
[root@FriendlyARM /mnt]# cat /dev/globalmem 
[ 5875.900000] read data[0]: hello world

最后,卸载驱动,卸载函数被调用:

[root@FriendlyARM /mnt]# rmmod globalmem_drv
[ 6131.080000] globalmem_exit~

 

4、编写应用程序测试;

如何测试,大致步骤:

(1)打开设备open();

(2)读写等操作read()/write();

(3)命令控制操作ioctl();

(4)关闭设备close();

 

应用测试程序,主要考虑把设备驱动中实现的各个函数都测一遍,即file_operations结构中实现的函数。

app_test.c文件,代码:

#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

/* 设备名称 */
#define DEV_NAME_GLOBALMEM	"/dev/globalmem"

/* IOCTL CMD */
#define CMD_MEM_CLEAR			0x0A
#define CMD_MEM_SET				0x0B

char data_wr[32] = "hello world";
char data_rd[32] = {0};

int main(int argc, char *argv[])
{
	int fd;
	int ret;
	ssize_t len;

	/* open device driver */
	fd = open(DEV_NAME_GLOBALMEM, O_RDWR);
	if(fd == -1)
	{
		printf("[APP]open dev failed!\n");
		return -1;
	}
	printf("[APP]open dev[%s] ok\n", DEV_NAME_GLOBALMEM);

	/*** test read/write/lseek ***/
	lseek(fd, SEEK_SET, 0);
	len = write(fd, data_wr, strlen(data_wr));
	if(len <= 0)
		goto PRO_END;
	printf("[APP]write data[%d]: %s\n", len, data_wr);

	lseek(fd, SEEK_SET, 0);
	len = read(fd, data_rd, sizeof(data_rd));
	if(len <= 0)
		goto PRO_END;
	printf("[APP]read data[%d]: %s\n", len, data_rd);

	lseek(fd, SEEK_CUR, 1);
	lseek(fd, SEEK_END, 2);

	/*** test ioctl ***/
	ret = ioctl(fd, CMD_MEM_CLEAR, 0);
	if(ret == -1)
		goto PRO_END;
	ret = ioctl(fd, CMD_MEM_SET, 'a');
	if(ret == -1)
		goto PRO_END;

PRO_END:
	close(fd);
	printf("[APP]close device.\n");

	return 0;
}


将其交叉编译:

arm-linux-gcc app_test.c -o app_test

加载设备驱动,并创建设备节点:


[root@FriendlyARM /mnt]# insmod globalmem_drv.ko 
[   97.215000] globalmem_init[major: 230] ok.

[root@FriendlyARM /mnt]# mknod /dev/globalmem c 230 0

运行测试程序:


[root@FriendlyARM /mnt]# ./app_test 
[APP]open dev[/dev/globalmem] ok
[  122.140000] llseek SEEK_SET to 0
[  122.140000] write data[11]: hello world
[  122.145000] llseek SEEK_SET to 0
[  122.145000] read data[0]: hello world
[  122.145000] llseek SEEK_CUR to 1
[  122.145000] globalmem is clear.
[  122.145000] globalmem is set to 0x61.
[APP]write data[11]: hello world
[APP]read data[32]: hello world
[APP]close device.

printk/printf打印的顺序好像不对喔:大概是因为两个函数是机制不同,我也解释不清,通常printk输出较快的。

卸载驱动及删除节点:


[root@FriendlyARM /mnt]# rmmod globalmem_drv
[  972.600000] globalmem_exit~

[root@FriendlyARM /mnt]# rm /dev/globalmem 

 

关于自动创建设备节点的函数:class_create()、device_create(),在下篇博文将采用自动创建方式。

 

笔记:

关于加载/卸载函数的__init、__exit

在include/init.h中有定义:(在include/linux/complier.h有 # define __section(S) __attribute__ ((__section__(#S))) )

#define __init        __section(.init.text) __cold notrace
#define __exit          __section(.exit.text) __exitused __cold notrace

“section”关键字会将被修饰的变量或函数编译到指定的区域(可执行文件中的段),如.init.text、.exit.text,详解请研究__attribute__ ((__section__())) 属性设置

 

后记:

这篇博文终于要完工了,从2019写到2020,从GXX写到ACTIONS,感觉有点像是跨时代的里程碑式的意义。2020.2.22

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值