Linux设备驱动之字符设备驱动

简述

Linux设备驱动有三种:字符设备驱动、块设备驱动、网络设备驱动。其中字符设备驱动最为基础。

Linux设备驱动目前有三种开发架构:原始架构、平台总线架构、设备树架构。三种架构理解难度依次上升,但开发难度依次下降。本篇文章介绍原始架构。

字符设备驱动开发最关键的两个数据结构是cdevfile_operations,如果想要自动生成设备节点,还要用到class结构。

cdev结构体用来描述一个字符设备,内容如下

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

file_operations中存放着与操作设备相关的函数。模块加载与卸载函数分别负责将cdev添加到内核与从内核中删除。加载完字符设备驱动模块后,/proc/devices文件中会生成对应的设备条目。

添加设备后,还需要生成设备节点文件,应用程序是通过设备节点文件来操控设备的,设备节点文件在/dev/目录下。

整体框架如下所示:

其中的的红字是我们进行原始驱动开发必需要做的几件预备工作。做完之后才能进行file_operations成员的开发。

本文将按如下顺序介绍字符设备驱动开发方法:

  • 主次设备号介绍
  • cdev相关函数介绍
  • 案例演示
  • 生成设备节点
  • file_operations成员列举

主次设备号

设备号由cdev结构体中的dev定义,一共32位,其中主设备号12位,次设备号20位,主设备号标识设备类型,次设备号标识同一设备类型中的不同设备。分解与合成设备号的方法如下:

MAJOR(dev_t dev)
MINOR(dev_t dev)
MKDEV(int major, int minor)

设备号需要申请才能获得,有静态和动态两种申请方法,分别入下:

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

cdev相关函数

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

  • void cdev_init(struct cdev*, struct file_operations *);

cdev_init用于初始化cdev成员,并建立与file_operations之间的联系

  • struct cdev *cdev_alloc(void);

cdev_alloc用于动态申请cdev内存

  • void cdev_put(struct cdev *p);

  • int cdev_add(struct cdev *, dev_t, unsigned);

cdev_add用于向系统添加一个cdev

  • void cdev_del(struct cdev *);

cdev_del用于从系统删除cdev

案例演示

该案例只是为了说明字符设备的添加方法,没有实现file_operations的任何函数

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/stat.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/moduleparam.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Shijia Yin");

#define DEV_NAME   "test"
#define DEV_MIN_NUM  2
#define DEV_MAJOR 0
#define DEV_MINOR 0

int dev_major = DEV_MAJOR;
int dev_minor = DEV_MINOR; 

struct test_dev_t {
    struct cdev cdev;
} test_dev;

struct file_operations test_fops = {
    .owner = THIS_MODULE,
};

static int test_init(void)
{
    int err;
    int ret = 0;
    dev_t dev_num;
    
    if(dev_major) {
		dev_num = MKDEV(dev_major, dev_minor);
		ret = register_chrdev_region(dev_num, DEV_MIN_NUM, DEV_NAME);
    } else {
		ret = alloc_chrdev_region(&dev_num, dev_minor, DEV_MIN_NUM, DEV_NAME);
		dev_major = MAJOR(dev_num);
		printk(KERN_EMERG "major number is %d !\n", dev_major);
    }
    
    if(ret < 0) {
		printk(KERN_EMERG "register_chrdev_region req %d is failed!\n", dev_major);
    }

    cdev_init(&test_dev.cdev, &test_fops);
    test_dev.cdev.ops = &test_fops;
    test_dev.cdev.owner = THIS_MODULE;
    err = cdev_add(&test_dev.cdev, dev_num, 1);

    if(err) {
        printk(KERN_EMERG "cdev_add %d is fail! %d\n", dev_minor, err);
    } else {
		printk(KERN_EMERG "cdev_add %d is success! \n", dev_minor);
    }
    printk(KERN_ALERT "Hello, world\n");
    return 0;
}

static void test_exit(void)
{
    printk(KERN_EMERG "test exit!\n");
    cdev_del(&test_dev.cdev);
    unregister_chrdev_region(MKDEV(dev_major, dev_minor), DEV_MIN_NUM);
    printk(KERN_ALERT "Goodbye, cruel world\n");
}

module_init(test_init);
module_exit(test_exit);

出于linux内核编码习惯,通常会为设备定义一个相关的结构体,该结构体包含设备所涉及的cdev、私有数据以及锁等信息。该案例中的设备结构体为test_dev_t。设备条目可以在/proc/devices中查看。

生成设备节点

在Linux中,万物皆文件,设备也要被抽象成文件,设备文件叫做设备节点,生成设备节点有两种方法:手动添加、自动生成。设备节点文件可以在/dev/目录下看到。

手动添加

手动添加指的是,模块加载完成后,在命令行下输入下面的命令来生成设备节点:

mknod /dev/test c 230 0

其中c用来标识字符设备,230是主设备号,0是次设备号,执行完该指令后会在/dev下生成test设备节点文件,应用程序可以像操作普通文件一样操作该文件。

自动添加

自动添加指的是在模块加载的时候生成设备节点,源码如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/stat.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/moduleparam.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Shijia Yin");

#define DEV_NAME   "test"
#define DEV_MIN_NUM  2
#define DEV_MAJOR 0
#define DEV_MINOR 0

int dev_major = DEV_MAJOR;
int dev_minor = DEV_MINOR; 

struct test_dev_t {
    struct cdev cdev;
} test_dev;

struct file_operations test_fops = {
    .owner = THIS_MODULE,
};
//定义test_class
static struct class *test_class;

static int test_init(void)
{
    int err;
    int ret = 0;
    dev_t dev_num;
    
    if(dev_major) {
	    dev_num = MKDEV(dev_major, dev_minor);
	    ret = register_chrdev_region(dev_num, DEV_MIN_NUM, DEV_NAME);
    } else {
	    ret = alloc_chrdev_region(&dev_num, dev_minor, DEV_MIN_NUM, DEV_NAME);
	    dev_major = MAJOR(dev_num);
	    printk(KERN_EMERG "major number is %d !\n", dev_major);
    }
    
    if(ret < 0) {
		printk(KERN_EMERG "register_chrdev_region req %d is failed!\n", dev_major);
    }
     
    cdev_init(&test_dev.cdev, &test_fops);
    test_dev.cdev.owner = THIS_MODULE;
    test_dev.cdev.ops = &test_fops;
    err = cdev_add(&test_dev.cdev, dev_num, 1);
    
    //创建设备节点
    test_class = class_create(THIS_MODULE, DEV_NAME);
    device_create(test_class, NULL, dev_num, NULL, DEV_NAME);
    
    if(err) {
        printk(KERN_EMERG "cdev_add %d is fail! %d\n", dev_minor, err);
    } else {
		printk(KERN_EMERG "cdev_add %d is success! \n", dev_minor);
    }
    printk(KERN_ALERT "Hello, world\n");
    return 0;
}

static void test_exit(void)
{
    printk(KERN_EMERG "test exit!\n");
    cdev_del(&test_dev.cdev);
    //删除设备节点
    device_destroy(test_class, MKDEV(dev_major, dev_minor));
    //删除test_class
    class_destroy(test_class);
    unregister_chrdev_region(MKDEV(dev_major, dev_minor), DEV_MIN_NUM);
    printk(KERN_ALERT "Goodbye, cruel world\n");
}

module_init(test_init);
module_exit(test_exit);

与生成设备节点相关的代码已用注释标出。

file_operations成员列举

file_operations结构体内容如下,实际上,驱动开发的主要时间用于下面函数的实现:

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 *);

/* end remove */
	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 *, 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);
/* end add */
};

总结

整个原始设备驱动编写流程大致可分为如下几步:

  • 定义设备结构体xxx_dev_t与file_operations
  • 实现file_operations中的各个成员
  • 申请设备号
  • 初始化cdev,即将cdev与file_operations绑定
  • 向系统添加cdev,即将cdev与前面申请的设备号相绑定
  • 创建设备节点
  • 编写模块退出函数:释放设备号、删除cdev、删除类、销毁设备节点等

引用

[1] 讯为驱动开发资料

[2] Linux内核源码

[3] 《Linux设备驱动程序开发详解》宋宝华 第一版

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值