目录
基于宋宝华老师的《Linux设备驱动开发详解》
Linux字符设备驱动结构
cdev结构体
cdev.h
头文件下
struct cdev {
struct kobject kobj; //内嵌的kobject对象
struct module *owner; //所属模块
const struct file_operations *ops; //文件操作结构体
struct list_head list;
dev_t dev; //设备号,高12位为主设备号,低20位为次设备号
unsigned int count;
};
- 获得主设备号:
MAJOR(dev_t dev)
- 获得次设备号:
MINOR(dev_t dev)
- 通过主设备号和次设备号生成
dev_t
设备号:MKDEV(int major,int minor)
cdev_init()
:初始化cdev
。从栈中获取内存cdev_alloc()
:动态申请一个cdev
内存。cdev_add()
:向系统添加一个cdev
,即注册。通常用于__init
函数cdev_del()
:删除一个cdev
,即注销。通常用于__exit
函数
分配和释放设备号
register_chrdev_region()
:静态向系统中注册设备号,用于已知起始设备的设备号的情况alloc_chrdev_region()
:动态向系统申请未被占用的设备号。unregister_chrdev_region()
:释放申请的设备号
file_opreation结构体
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (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 (*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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
u64);
};
部分解释如下
llseek()
:修改一个文件的当前读写位置,类似用户空间中的lseek()
read()
:对应用户空间中的read()
write()
:对应用户空间中的write()
,如果该函数没有实现,用户进行write()
时,返回-EINVAL
unlocked_ioctl()
:提供设备相关控制指令的实现,调用成功,返回非负值。对应用户空间中的fcntl()
和ioctl()
mmap()
:将设备内存映射到进程的虚拟地址空间中,如果驱动未实现该函数,但用户空间进行了mmap()
,则返回-ENODEV
。主要用于帧缓冲等设备。对应用户空间的mmap()
open()
:对应open()
。可以不实现,这种情况下,设备的打开操作永远成功release()
:用户空间中没有,主要是被自动的回调,用于释放设备资源和清理poll()
:用于询问设备是否可被非阻塞地立即读写aio_read()
:异步读aio_write()
:异步写
Linux字符设备驱动的组成
copy_to_user()
:完成内核空间到用户空间缓冲区的复制copy_from_user
:完成用户空间缓冲区到内核空间的复制、- 均返回不能被复制的字节数。
- 完全复制成功,返回0。复制失败,返回负值。
- 简单类型的传递
get_user
:用户空间到内核put_user
:内核到用户空间
access_ok()
:检查传递的用户空间的缓冲区的合法性VERIFY_WRITE
:检查写VERIFY_READ
:检查读
- 不检查合法性,进行内存传递
__get_user()
__put_user()
支持N个简单字符设备的驱动
ioctl()命令
- Linux内核推荐采用一套统一的
ioctl()
命令生成方式,格式如下- 8位设备类型+8位序列号+2位方向+13/14位数据尺寸
- 设备类型字段:为一个“幻数”,范围:0~0xff,内核中的
ioctl-number.txt
给出了一些推荐的和已经被使用的“幻数”,新设备驱动定义时要避免重读。 - 2位方向:表示从应用程序的角度来看的数据传送的方向
_IOC_NONE
:无数据传输_IOC_READ
:读_IOC_WRITE
:写_IOC_READ|_IOC_WRITE
:双向
- 辅助生成命令的宏
_IO()
_IOR(type,nr,size)
_IOW(设备类型,序列号,数据尺寸)
_IOWR()
- 预定义命令
- 内核中已经预定义好了一些
I/O
控制命令,这些命令是会被内核处理,所以我没在自定义设备驱动的命令时要注意避免冲突。- 如果冲突了,那么当内核收到命令时,这些命令会被内核处理,而不是交给设备驱动处理。
- 常用的预定义命令
FIOCLEX
:对文件设置专用标志,通知内核当exec()
系统调用发生时自动关闭打开的文件FIONCLEX
:清除FIOCLEX
命令设置的标志FIOQSIE
:获得一个文件或者目录的大小,用于设备文件时会返回ENOTTY
错误FIONBIO
:调用修改在file->f_flags
中的O_NONBLOCK
标志
- 内核中已经预定义好了一些
private_data
指针一般指向自定义的设备结构体container_of(目标结构体成员指针,整个结构体的类型,目标结构体成员的类型)
:通过结构体成员的指针找到对应结构体的指针- 代码中通过申请:
sizeof(struct globalmem_dev)*DEVICE_NUM
这么大一个空间来存储所有的结构体,所以这里其实可以视为一个数组
手动加载驱动
sudo insmod globalmem.ko
:加载驱动lsmod
:可以查看到加载后的驱动cat /proc/devices
:可以查看到主设备号为230的globalmem
字符设备驱动sudo mknod /dev/globalmem c 230 0
:创建设备节点- 如向创建多个设备节点,那就多次运行
mknod
- 如:
sudo mknod /dev/globalmem1 c 230 1
- 如:
- 可能还需要
chmod 666 /dev/globalmem
,修改设备权限 - 测试就是用
echo "xxx" >> /dev/globalmem
命令和cat /dev/globalmem
命令
- 如向创建多个设备节点,那就多次运行
- 启用了
sysfs
文件系统的情况下,会多出/sys/module/globalmem
目录
示例代码
源代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define GLOBALMEM_SIZE 0x1000
#define MEM_CLEAR 0x1
#define GLOBALMEM_MAJOR 230
#define DEVICE_NUM 10
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 ssize_t globalmem_read (struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;//获得相对于文件开头的偏移
unsigned int count = size;
int ret = 0;
struct globalmem_dev* dev = filp->private_data;//将存放在私有数据空间中的自定义结构体取出
//整个文件大小,也就是缓冲区大小为自定义的GLOBALMEM_SIZE
if(p >= GLOBALMEM_SIZE){
return 0;
}
if(count > GLOBALMEM_SIZE - p){
count = GLOBALMEM_SIZE - p;
}
//复制数据到用户空间
if(copy_to_user(buf,dev->mem+p,count)){
ret = -EFAULT;
}else {
*ppos += count;
ret = count;
printk(KERN_INFO "read %u bytes(s) from %lu\n",count,p);
}
return ret;
}
static ssize_t globalmem_write (struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;
if(p >= GLOBALMEM_SIZE){
return 0;
}
if(count > GLOBALMEM_SIZE - p){
count = GLOBALMEM_SIZE - p;
}
//将数据copy到内核空间
if(copy_from_user(dev->mem+p,buf,count)){
ret = -EFAULT;
}else {
*ppos += count;
ret = count;
printk(KERN_INFO "written %u bytes(s) from %lu\n", count,p);
}
return ret;
}
//orig这一位代表lseek的最后一位参数,offset表示传进来的偏移量
static loff_t globalmem_llseek (struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
switch(orig){
case 0:{//从文件头开始
if(offset < 0){
ret = -EINVAL;
break;
}
if((unsigned int)offset > GLOBALMEM_SIZE){
ret = -EINVAL;
break;
}
//file结构体中的f_pos代表的是文件的当前读写位置
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
}
case 1:{//从文件的当前位置开始
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;
break;
}
case 2:{//从文件的尾巴
if(offset > 0){
ret = -EINVAL;
break;
}
if((unsigned int)offset < -GLOBALMEM_SIZE){
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int)offset + GLOBALMEM_SIZE;
ret = filp->f_pos;
break;
}
default:
ret = -EINVAL;
break;
}
return ret;
}
static long globalmem_ioctl (struct file *filp, unsigned int cmd, unsigned long arg)
{
struct globalmem_dev* dev = filp->private_data;
switch(cmd){
case MEM_CLEAR:{
//清空
memset(dev->mem,0,GLOBALMEM_SIZE);
//将偏移量转回0
filp->f_pos = 0;
printk(KERN_INFO "globalmem is set to zero\n");
break;
}
default:
return -EINVAL;
}
return 0;
}
static int globalmem_open (struct inode *inode, struct file *filp)
{
struct globalmem_dev* dev = container_of(inode->i_cdev,struct globalmem_dev,cdev);
filp->private_data =dev;
return 0;
}
static int globalmem_release(struct inode *inode, struct file *filp)
{
return 0;
}
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 void globalmem_setup_cdev(struct globalmem_dev *dev,int index)
{
//生成设备号
int err, devno=MKDEV(globalmem_major,index);
//初始化cdev
cdev_init(&dev->cdev,&globalmem_fops);
dev->cdev.owner = THIS_MODULE;
//向系统添加一个cdev
err = cdev_add(&dev->cdev,devno,1);
if(err){
printk(KERN_NOTICE "Error %d adding globalmem%d",err,index);
}
}
static int __init globalmem_init(void)
{
int ret;
int i;
dev_t devno = MKDEV(globalmem_major,0);//生成设备号
//判断globalmem_major是否为0,为0代表动态分配
if(globalmem_major){
ret = register_chrdev_region(devno,DEVICE_NUM,"globalmem");
}else {
ret = alloc_chrdev_region(&devno,0,DEVICE_NUM,"globalmem");
globalmem_major = MAJOR(devno);
}
if(ret < 0){
return ret;
}
globalmem_devp = kzalloc(sizeof(struct globalmem_dev)*DEVICE_NUM,GFP_KERNEL);//GFP_KERNEL,阻塞等待内存分配完成
if(!globalmem_devp){
ret = -ENOMEM;
goto fail_malloc;
}
for(i=0; i<DEVICE_NUM;++i){
globalmem_setup_cdev(globalmem_devp,i);
}
return 0;
fail_malloc:
unregister_chrdev_region(devno,DEVICE_NUM);
return ret;
}
module_init(globalmem_init);
static void __exit globalmem_exit(void)
{
int i;
for(i=0;i<DEVICE_NUM;++i){
cdev_del(&(globalmem_devp+i)->cdev);
}
kfree(globalmem_devp);
unregister_chrdev_region(MKDEV(globalmem_major,0),DEVICE_NUM);
}
module_exit(globalmem_exit);
MODULE_AUTHOR("Barry song <111>");
MODULE_LICENSE("GPL v2");
makefile
# Kernel modules
obj-m += globalmem.o
# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0
build: kernel_modules
kernel_modules:
make -C /交叉编译后的Linux内核源码目录 M=$(CURDIR) modules
clean:
make -C /交叉编译后的Linux内核源码目录 M=$(CURDIR) clean