@zwh8800
2017-08-23T10:01:16.000000Z
字数 9665
阅读 191642
blog
归档
linux
驱动开发
这次我们学习最简单的一种设备, 字符设备驱动的开发. 最终写出一个字符设备, 用户可以进行打开和关闭, 并向他写入数据, 它会始终保存着最后一次写入的数据, 对它进行读取会读出最后一次写入的数据.
在linux中执行ls -l命令, 在日期之前可以看到两个用逗号隔开的数, 这个便是设备号, 逗号之前的是主设备号(major)后面的是次设备号(minor).
在内核中, 设备号使用dev_t来表示(<linux/types.h>
). dev_t可以同时保存主设备号和次设备号, 当需要从dev_t中获取主设备号或次设备号时, 可以使用以下:
MAJOR(dev_t dev);
MINOR(dev_t dev);
相反, 如果需要用设备号构造出dev_t则使用:
MKDEV(int major, int minor);
建立字符设备前, 驱动程序首先应该分配设备号, 有三个函数用来分配(注册)设备号和释放设备号(在<linux/fs.h>
中):
int register_chrdev_region(dev_t first, unsigned int count,
char *name);
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
unsigned int count, char *name);
void unregister_chrdev_region(dev_t first, unsigned int count);
register_chrdev_region函数用在已知设备号的情况下向内核进行注册, alloc用在设备号不确定的情况下, 向内核动态分配设备号. first是申请的设备号的第一个, count是要连续申请的个数(次设备号的个数, 比如first是[10, 102], count是4, 则会申请[10,102][10,103][10,104][10,105]这四个). name是设备的名称, 将出现在/proc/devices和sysfs中. 如果出错返回负的错误号.
alloc函数成功后通过dev返回第一个设备号.
例子:
DEBUG_LOG("", "allocating device number\n");
/* 申请设备号 */
if (chr_major != 0) /* 如果用户提供了设备号 */
{
dev = MKDEV(chr_major, chr_minor);
ret = register_chrdev_region(dev, 1, DEV_NAME);
}
else
{
ret = alloc_chrdev_region(&dev, chr_minor, 1, DEV_NAME);
chr_major = MAJOR(dev);
}
if (ret < 0)
{
DEBUG_LOG(KERN_WARNING, "cannot get major%d\n", chr_major);
goto err;
}
DEBUG_LOG("", "success\n");
DEBUG_LOG("", "chr_major=%d, chr_minor=%d\n", chr_major, chr_minor);
关于字符设备驱动, 有三个重要的数据结构, 他们都在<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);
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 *);
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);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags, f_pos vs i_size in lseek SEEK_CUR.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
#ifdef CONFIG_SMP
int f_sb_list_cpu;
#endif
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};
struct inode {
...
dev_t i_rdev;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
...
};
对于一个字符设备, 应当满足linux对于字符设备的定义(既它可以进行字符设备的操作, 如打开,读取,写入,ioctl,关闭), 所以他应当自己定义这些操作的函数, 然后把函数指针保存在struct file_operations结构中传递给内核. 这样内核就可以在用户对设备调用这些函数时调用适当的驱动程序. 所以在struct file_operations中可以看到, 大部分的都是函数指针, 而有一个成员例外, struct module *owner成员应给它赋值THIS_MODULE.
例如:
struct file_operations chr_fops =
{
.owner = THIS_MODULE,
.open = open,
.release = release,
.read = read,
.write = write,
};
对于file和inode, 我的理解是, 用户每打开一个文件, 将产生一个file结构, 但是一个文件只有一个inode(硬盘上也保存着inode, 用户打开时将会读入内存). 所以看open和release函数的签名都有一个file和inode, 而其他函数只有file. 因为当打开文件时, 应当让file和inode建立联系, 关闭时应当解除联系. 所以内核的接口是这样设计的.
刚刚只是分配了设备号了, 内核其实连你的设备是什么类型都不知道. 所以第二步应该注册设备. 字符设备的注册用到的结构体为struct cdev(<linux/cdev.h>
, 刚刚在inode结构体中也看见这个结构了i_cdev)
cdev结构体的分配有两个函数:
struct cdev *cdev_alloc(void);
void cdev_init(struct cdev *cdev, struct file_operations *fops);
第一个函数会分配内存空间, 并进行初始化cdev, 第二个只是会初始化cdev.
cdev有两个重要的字段, owner和ops, owner应该设置成THIS_MODULE, ops应当设置成一个指向struct file_operations的指针.
cdev结构构造好之后通过这两个函数注册和移除字符设备:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
void cdev_del(struct cdev *dev);
看看通过cdev_add我们传递给内核什么信息:
这样, 当有用户请求对num指定的设备号的设备进行操作时, 就会通过ops中的指针调用相应的函数了. 这就完成了字符设备的注册.cdev_add会返回错误代码
例:
DEBUG_LOG("", "register char device\n");
/* 注册字符设备 */
chr_dev = kmalloc(sizeof(chr_dev), GFP_KERNEL);
if (chr_dev == NULL)
{
DEBUG_LOG(KERN_WARNING, "no memory\n");
ret = -ENOMEM;
goto err1;
}
memset(chr_dev, 0, sizeof(*chr_dev));
cdev_init(&chr_dev->cdev, &chr_fops);
chr_dev->cdev.owner = THIS_MODULE;
chr_dev->cdev.ops = &chr_fops;
if ((ret = cdev_add(&chr_dev->cdev, dev, 1)) != 0)
{
DEBUG_LOG(KERN_WARNING, "Error %d adding chr\n", ret);
goto err2;
}
DEBUG_LOG("", "success\n");
我们使用一个自己的结构来保存cdev和相关的信息chr_dev.
前面的所有操作讲的都是驱动程序的初始化操作, 都是应当写到module_init函数中的. 下面将驱动真正的事件处理函数应当怎么写.
我们的功能是这个设备可以打开,关闭,读取,写入. 所以我们只需实现这几个函数, 如果用户对设备调用其他的函数, 内核会有相应的默认操作.
前面说了, 打开操作就是将inode和file结构体建立联系, 这两个结构体内核都会在用户打开/关闭文件时自动创建/销毁, 驱动程序不用照看他们的生命周期.
open函数会用参数传来inode和file, inode是内核根据用户打开的文件创建的, 因为文件系统保存着设备文件的设备号, 而刚刚我们通过cdev_add函数将设备号和cdev建立了联系所以这个inode中会有一个指向对应cdev的指针. 但是file中没有, 我们要做的就是让每个打开的file也保存起这个指针(通过保存在filp->private_data中).
但是我们的cdev保存在chr_dev结构中, 而且这个结构中还有另外一些我们感兴趣的东西, 所以不如直接保存chr_dev结构.
int open(struct inode *inode, struct file *filp)
{
struct chr_dev *dev;
DEBUG_LOG("", "open from user\n");
dev = container_of(inode->i_cdev, struct chr_dev, cdev);
filp->private_data = dev;
if ((filp->f_flags & O_ACCMODE) == O_WRONLY)
{
dev->last_char = 0;
}
DEBUG_LOG("", "open success\n");
return 0;
}
当用户关闭文件时, 我们应当:
我们这两个工作都不用做.
这个很简单了没什么要说的. 注意一点, 传来的buf指针是用户空间的指针(用__user修饰), 我们不能对这个指针进行解引用, 因为它根本不指向我们这个空间(内核空间)的数据. 而对它进行读写只能通过函数copy_from_user/copy_to_user进行(<linux/uaccess>
).
另外, 如果在内核空间需要分配内存, 应使用kmalloc和kfree(<linux/stab.h>
)
直接看最终代码吧
#include <linux/module.h>
#include <linux/init.h> /* module_* */
#include <linux/kernel.h> /* printk */
#include <linux/moduleparam.h> /* module_param */
#include <linux/types.h> /* dev_t */
#include <linux/fs.h> /* reg*_chrdev */
#include <linux/cdev.h> /* cdev_add */
#include <asm/uaccess.h> /* copy_*_user */
#include <linux/string.h> /* memset */
#include <linux/slab.h> /* kmalloc */
#define DEV_NAME "chr"
#define _DEBUG
#ifdef _DEBUG
#define DEBUG_LOG(lvl, fmt, ...) \
printk(lvl "%s: %s:%d <%s>: " fmt, \
DEV_NAME, __FILE__, __LINE__, __func__, ##__VA_ARGS__)
#else
#define DEBUG_LOG(lvl, fmt, ...)
#endif
MODULE_LICENSE("Dual BSD/GPL");
struct chr_dev
{
char last_char;
struct cdev cdev;
};
int open(struct inode *inode, struct file *filp)
{
struct chr_dev *dev;
DEBUG_LOG("", "open from user\n");
dev = container_of(inode->i_cdev, struct chr_dev, cdev);
filp->private_data = dev;
if ((filp->f_flags & O_ACCMODE) == O_WRONLY)
{
dev->last_char = 0;
}
DEBUG_LOG("", "open success\n");
return 0;
}
int release(struct inode *inode, struct file *filp)
{
DEBUG_LOG("", "release from user\n");
DEBUG_LOG("", "release success\n");
return 0;
}
ssize_t read(struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
struct chr_dev *dev = filp->private_data;
char* kbuf = kmalloc(count, GFP_KERNEL);
DEBUG_LOG("", "read from user\n");
if (kbuf == NULL)
{
DEBUG_LOG(KERN_WARNING, "no memory\n");
goto err1;
}
memset(kbuf, dev->last_char, count);
if (copy_to_user(buf, kbuf, count) != 0)
{
DEBUG_LOG(KERN_WARNING, "copy_to_user error\n");
goto err;
}
kfree(kbuf);
DEBUG_LOG("", "read success count=%d\n", count);
return count;
err1:
kfree(kbuf);
err:
return -EFAULT;
}
ssize_t write(struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
struct chr_dev *dev = filp->private_data;
DEBUG_LOG("", "write from user\n");
if (copy_from_user(&dev->last_char, buf + count - 1, 1) != 0)
{
DEBUG_LOG(KERN_WARNING, "copy_from_user error\n");
return -EFAULT;
}
DEBUG_LOG("", "write success count=%d\n", count);
return count;
}
struct file_operations chr_fops =
{
.owner = THIS_MODULE,
.open = open,
.release = release,
.read = read,
.write = write,
};
int chr_major = 0;
int chr_minor = 0;
//module_param(chr_major, int, S_IRUGO);
struct chr_dev* chr_dev;
int chr_init(void)
{
int ret;
dev_t dev;
DEBUG_LOG("", "allocating device number\n");
/* 申请设备号 */
if (chr_major != 0) /* 如果用户提供了设备号 */
{
dev = MKDEV(chr_major, chr_minor);
ret = register_chrdev_region(dev, 1, DEV_NAME);
}
else
{
ret = alloc_chrdev_region(&dev, chr_minor, 1, DEV_NAME);
chr_major = MAJOR(dev);
}
if (ret < 0)
{
DEBUG_LOG(KERN_WARNING, "cannot get major%d\n", chr_major);
goto err;
}
DEBUG_LOG("", "success\n");
DEBUG_LOG("", "chr_major=%d, chr_minor=%d\n", chr_major, chr_minor);
DEBUG_LOG("", "register char device\n");
/* 注册字符设备 */
chr_dev = kmalloc(sizeof(chr_dev), GFP_KERNEL);
if (chr_dev == NULL)
{
DEBUG_LOG(KERN_WARNING, "no memory\n");
ret = -ENOMEM;
goto err1;
}
memset(chr_dev, 0, sizeof(*chr_dev));
cdev_init(&chr_dev->cdev, &chr_fops);
chr_dev->cdev.owner = THIS_MODULE;
chr_dev->cdev.ops = &chr_fops;
if ((ret = cdev_add(&chr_dev->cdev, dev, 1)) != 0)
{
DEBUG_LOG(KERN_WARNING, "Error %d adding chr\n", ret);
goto err2;
}
DEBUG_LOG("", "success\n");
return 0;
err2:
kfree(chr_dev);
err1:
unregister_chrdev_region(dev, 1);
err:
return ret;
}
void chr_exit(void)
{
DEBUG_LOG("", "unloading module\n");
cdev_del(&chr_dev->cdev);
kfree(chr_dev);
unregister_chrdev_region(MKDEV(chr_major, chr_minor), 1);
DEBUG_LOG("", "success\n");
}
module_init(chr_init);
module_exit(chr_exit);