环境:主机-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