内核驱动篇三--字符设备驱动基础
- 一、Linux内核对设备的分类
- 二、设备号------内核中同类设备的区分
- 三、申请和注销设备号
- 四、注册字符设备
- 五、字符设备驱动框架解析
- 5.1 两个操作函数中常用的结构体说明
- 5.2 字符设备驱动程序框架分析
- 5.3 参考原理图
- 5.4 常用操作函数说明
- 六、读操作实现
- 七、写操作实现
- 八、ioctl操作实现
- 九、printk
- 十、多个次设备的支持
一、Linux内核对设备的分类
linux的文件种类:
- -:普通文件
- d:目录文件
- p:管道文件
- s:本地socket文件
- l:链接文件
- c:字符设备
- b:块设备
Linux内核按驱动程序实现模型框架的不同,将设备分为三类:
- 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
- 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节,随机访问,设缓存以提高效率
- 网络设备:针对网络数据收发的设备
总体框架图:
二、设备号------内核中同类设备的区分
内核用设备号来区分同类里不同的设备,设备号是一个无符号32位整数,数据类型为dev_t,设备号分为两部分:
- 主设备号:占高12位,用来表示驱动程序相同的一类设备
- 次设备号:占低20位,用来表示被操作的哪个具体设备
应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备。
MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号,用法:
dev_t devno; int major = 251;//主设备号 int minor = 2;//次设备号 devno = MKDEV(major,minor);
MAJOR宏用来从32位设备号中分离出主设备号,用法:
dev_t devno = MKDEV(249,1); int major = MAJOR(devno);
MINOR宏用来从32位设备号中分离出次设备号,用法:
dev_t devno = MKDEV(249,1); int minor = MINOR(devno);
如果已知一个设备的主次设备号,应用层指定好设备文件名,那么可以用mknod命令在/dev目录创建代表这个设备的文件,即此后应用程序对此文件的操作就是对其代表的设备操作,mknod用法如下:
@ cd /dev @ mknod 设备文件名 设备种类(c为字符设备,b为块设备) 主设备号 次设备号 //ubuntu下需加sudo执行
在应用程序中如果要创建设备可以调用系统调用函数mknod,其原型如下:
int mknod(const char *pathname,mode_t mode,dev_t dev); pathname:带路径的设备文件名,无路径默认为当前目录,一般都创建在/dev下 mode:文件权限 位或 S_IFCHR/S_IFBLK dev:32位设备号 返回值:成功为0,失败-1
三、申请和注销设备号
字符驱动开发的第一步是通过模块的入口函数向内核添加本设备驱动的代码框架,主要完成:
- 申请设备号
- 定义、初始化、向内核添加代表本设备的结构体元素
int register_chrdev_region(dev_t from, unsigned count, const char *name) 功能:手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号 参数: from:自己指定的设备号 count:申请的设备数量 name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号 返回值: 成功为0,失败负数,绝对值为错误码
#includeint alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name) 功能:动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号 参数: dev:分配设备号成功后用来存放分配到的设备号 baseminior:起始的次设备号,一般为0 count:申请的设备数量 name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号 返回值: 成功为0,失败负数,绝对值为错误码
分配成功后在/proc/devices 可以查看到申请到主设备号和对应的设备名
- 释放设备号:
#includevoid unregister_chrdev_region(dev_t from, unsigned count) 功能:释放设备号 参数: from:已成功分配的设备号将被释放 count:申请成功的设备数量
查看设备号:cat /proc/devices | grep 文件名
释放后/proc/devices文件对应的记录消失
四、注册字符设备
- 设备结构体
struct cdev
{
struct kobject kobj;//表示该类型实体是一种内核对象
struct module *owner;//填THIS_MODULE,表示该字符设备从属于哪个内核模块
const struct file_operations *ops;//指向空间存放着针对该设备的各种操作函数地址
struct list_head list;//链表指针域
dev_t dev;//设备号
unsigned int count;//设备数量
};
自己定义的结构体中必须有一个成员为 struct cdev cdev,两种方法定义一个设备:
- 直接定义:定义结构体全局变量
- 动态申请: struct cdev * cdev_alloc()
- 设备初始化
void cdev_init(struct cdev *cdev,const struct file_operations *fops)
struct file_operations
{
struct module *owner; //填THIS_MODULE,表示该结构体对象从属于哪个内核模块
int (*open) (struct inode *, struct file *); //打开设备
int (*release) (struct inode *, struct file *); //关闭设备
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //读设备
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //写设备
loff_t (*llseek) (struct file *, loff_t, int); //定位
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//读写设备参数,读设备状态、控制设备
unsigned int (*poll) (struct file *, struct poll_table_struct *); //POLL机制,实现多路复用的支持
int (*mmap) (struct file *, struct vm_area_struct *); //映射内核空间到用户层
int (*fasync) (int, struct file *, int); //信号驱动
//......
};
该对象各个函数指针成员都对应相应的系统调用函数,应用层通过调用系统函数来间接调用这些函数指针成员指向的设备驱动函数:
- 字符设备添加与删除:
int cdev_add(struct cdev *p,dev_t dev,unsigned int count) 功能:将指定字符设备添加到内核 参数: p:指向被添加的设备 dev:设备号 count:设备数量,一般填1
void cdev_del(struct cdev *p) 功能:从内核中移除一个字符设备 参数: p:指向被移除的字符设备
小结:
字符设备驱动开发步骤:
- 如果设备有自己的一些控制数据,则定义一个包含struct cdev cdev成员的结构体struct mydev,其它成员根据设备需求,设备简单则直接用struct cdev
- 定义一个struct mydev或struct cdev的全局变量来表示本设备;也可以定义一个struct mydev或struct cdev的全局指针(记得在init时动态分配)
- 定义三个全局变量分别来表示主设备号、次设备号、设备数
- 定义一个struct file_operations结构体变量,其owner成员置成THIS_MODULE
- module init函数流程:a. 申请设备号 b. 如果是全局设备指针则动态分配代表本设备的结构体元素 c. 初始化struct cdev成员 d. 设置struct cdev的owner成员为THIS_MODULE e. 添加字符设备到内核
- module exit函数:a. 注销设备号 b. 从内核中移除struct cdev c. 如果如果是全局设备指针则释放其指向空间
- 编写各个操作函数并将函数名初始化给struct file_operations结构体变量
验证操作步骤:
- 编写驱动代码mychar.c
- make生成ko文件
- insmod内核模块
- 查阅字符设备用到的设备号(主设备号):cat /proc/devices | grep 申请设备号时用的名字
- 创建设备文件(设备节点) : mknod /dev/??? c 上一步查询到的主设备号 代码中指定初始次设备号
- 编写app验证驱动(testmychar_app.c)
- 编译运行app,dmesg命令查看内核打印信息
五、字符设备驱动框架解析
设备的操作函数如果比喻是桩的话(性质类似于设备操作函数的函数,在一些场合被称为桩函数),则:
驱动实现设备操作函数 ----------- 做桩
insmod调用的init函数主要作用 --------- 钉桩
rmmod调用的exitt函数主要作用 --------- 拔桩
应用层通过系统调用函数间接调用这些设备操作函数 ------- 用桩
5.1 两个操作函数中常用的结构体说明内核中记录文件元信息的结构体
struct inode
{
//....
dev_t i_rdev;//设备号
struct cdev *i_cdev;//如果是字符设备才有此成员,指向对应设备驱动程序中的加入系统的struct cdev对象
//....
}
读写文件内容过程中用到的一些控制性数据组合而成的对象------文件操作引擎(文件操控器)
struct file
{
//...
mode_t f_mode;//不同用户的操作权限,驱动一般不用
loff_t f_pos;//position 数据位置指示器,需要控制数据开始读写位置的设备有用
unsigned int f_flags;//open时的第二个参数flags存放在此,驱动中常用
struct file_operations *f_op;//open时从struct inode中i_cdev的对应成员获得地址,驱动开发中用来协助理解工作原理,内核中使用
void *private_data;//本次打开文件的私有数据,驱动中常来在几个操作函数间传递共用数据
struct dentry *f_dentry;//驱动中一般不用,除非需要访问对应文件的inode,用法flip->f_dentry->d_inode
int refcnt;//引用计数,保存着该对象地址的位置个数,close时发现refcnt为0才会销毁该struct file对象
//...
};
5.2 字符设备驱动程序框架分析
驱动实现端:
驱动使用端:
syscall_open函数实现的伪代码:
int syscall_open(const char *filename,int flag)
{
dev_t devno;
struct inode *pnode = NULL;
struct cdev *pcdev = NULL;
struct file *pfile = NULL;
int fd = -1;
if()
{
devno = pnode->i_rdev;
pnode->i_cdev = pcdev;
}
pfile->f_op = pnode->i_cdev->ops;
pfile->f_flags = flag;
pfile->f_op->open(pnode,pfile,flag);
return fd;
}
syscall_read函数实现的伪代码
int syscall_read(int fd,void *pbuf,int size)
{
struct file *pfile = NULL;
struct file_operations *fops = NULL;
int cnt;
cnt = fops->read(pfile,pbuf,size,&pfile->f_pos);
。。。。
return cnt;
}
5.3 参考原理图
int (*open) (struct inode *, struct file *); //打开设备 int (*release) (struct inode *, struct file *); //关闭设备 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //读设备 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //写设备 loff_t (*llseek) (struct file *, loff_t, int); //数据操作位置的定位 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//读写设备参数,读设备状态、控制设备 unsigned int (*poll) (struct file *, struct poll_table_struct *);//POLL机制,实现对设备的多路复用方式的访问 int (*fasync) (int, struct file *, int); //信号驱动
六、读操作实现
读函数:
ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
完成功能:读取设备产生的数据
参数:
filp:指向open产生的struct file类型的对象,表示本次read对应的那次open
pbuf:指向用户空间一块内存,用来保存读到的数据
count:用户期望读取的字节数
ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置
返回值:
本次成功读取的字节数,失败返回-1
用户空间 向 内核空间 读取数据:
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n)
成功为返回0,失败非0
七、写操作实现
写函数:
ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);
完成功能:向设备写入数据
参数:
filp:指向open产生的struct file类型的对象,表示本次write对应的那次open
pbuf:指向用户空间一块内存,用来保存被写的数据
count:用户期望写入的字节数
ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置
返回值:
本次成功写入的字节数,失败返回-1
用户空间 向 内核空间 写入数据:
unsigned long copy_from_user (void * to, const void __user * from, unsigned long n)
成功为返回0,失败非0
八、ioctl操作实现
已知成员的地址获得所在结构体变量的地址:container_of(成员地址,结构体类型名,成员在结构体中的名称)
long xxx_ioctl (struct file *filp, unsigned int cmd, unsigned long arg);
功能:对相应设备做指定的控制操作(各种属性的设置获取等等)
参数:
filp:指向open产生的struct file类型的对象,表示本次ioctl对应的那次open
cmd:用来表示做的是哪一个操作
arg:和cmd配合用的参数
返回值:成功为0,失败-1
cmd组成
- dir(direction),ioctl 命令访问模式(属性数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据;
- type(device type),设备类型,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如
‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识; - nr(number),命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增;
- size,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数;
#define _IOC(dir,type,nr,size) (((dir)<<_IOC_DIRSHIFT)|
((type)<<_IOC_TYPESHIFT)|
((nr)<<_IOC_NRSHIFT)|
((size)<<_IOC_SIZESHIFT))
// 定义不带参数的 ioctl 命令
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
//定义带读参数的ioctl命令(copy_to_user) size为类型名
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
//定义带写参数的 ioctl 命令(copy_from_user) size为类型名
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
//定义带读写参数的 ioctl 命令 size为类型名
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
九、printk
//日志级别
#define KERN_EMERG "<0>"
#define KERN_ALERT "<1>"
#define KERN_CRIT "<2>"
#define KERN_ERR "<3>"
#define KERN_WARNING "<4>"
#define KERN_NOTICE "<5>"
#define KERN_INFO "<6>"
#define KERN_DEBUG "<7>"
用法:printk(KERN_INFO"....",....)
printk(KERN_INFO"Hello World"); =====> printk("<6>""Hello World") ====> printk("<6>Hello World")
dmesg --level=emerg,alert,crit,err,warn,notice,info,debug
#define HELLO_DEBUG #undef PDEBUG #ifdef HELLO_DEBUG #define PDEBUG(fmt, args...) printk(KERN_DEBUG fmt, ##args) #else #define PDEBUG(fmt, args...) #endif
十、多个次设备的支持
每一个具体设备(次设备不一样的设备),必须有一个struct cdev来代表它
cdev_init
cdev.owner赋值
cdev_add
以上三个操作对每个具体设备都要进行
综合例子代码:
mytest.c:
#include#include #include #include #include #include "mytest.h" #define BUFSIZE 100 //模拟内核空间大小 int major = 11; //主设备号 int minor = 0; //次设备号 int dev_num = 1; //设备数量 struct pdev { struct cdev mydev; //设备结构体 char dev_buf[BUFSIZE]; //模拟内核空间 int buf_seek; //内核空间储存值 }; struct pdev pmydev; int mydev_open(struct inode *pnode, struct file *pfile) //打开设备 { pfile->private_data = (void *)(container_of(pnode->i_cdev,struct pdev,mydev)); //获取设备结构体地址 return 0; } int mydev_release(struct inode *pnode, struct file *pfile) //关闭设备 { return 0; } ssize_t mydev_read(struct file *pfile, char __user *user_buf, size_t user_size, loff_t *seek) //读设备 { int size; //读取的数据量 int ret; struct pdev *gmydev = pfile->private_data; if(user_size > gmydev->buf_seek) //量定读取多少 { size = gmydev->buf_seek; } else { size = user_size; } ret = raw_copy_to_user(user_buf, gmydev->dev_buf, size); //读取数据 if(ret) { printk("copy_to_user fail!"); return -1; } memcpy(gmydev->dev_buf, gmydev->dev_buf+size, BUFSIZE-size); gmydev->buf_seek -= size; return size; } ssize_t mydev_write(struct file *pfile, const char __user *user_buf, size_t user_size, loff_t *seek) //写设备 { int size; int ret; struct pdev *gmydev = pfile->private_data; if(user_size > BUFSIZE-gmydev->buf_seek) //量定写入多少 { size = BUFSIZE-gmydev->buf_seek; } else { size = user_size; } ret = raw_copy_from_user(gmydev->dev_buf+gmydev->buf_seek, user_buf, size); //写入数据 if(ret) { printk("copy_from_user fail!n"); return -1; } gmydev->buf_seek += size; return size; } long mydev_ioctl(struct file *pfile, unsigned int cmd, unsigned long arg) { int __user *pret = (int *)arg; int maxlen = BUFSIZE; int ret; struct pdev *gmydev = pfile->private_data; switch(cmd) { case MYDEV_IOCTL_GET_MUTEX: ret = raw_copy_to_user(pret, &maxlen, sizeof(int)); if(ret) { printk("copy_to_user fail!n"); return -1; } break; case MYDEV_IOCTL_GET_SEEK: ret = raw_copy_to_user(pret, &gmydev->buf_seek, sizeof(int)); if(ret) { printk("copy_to_user fail!n"); return -1; } break; default: printk("cmd error!n"); return -1; } return 0; } struct file_operations fops ={ .open = mydev_open, .release = mydev_release, .read = mydev_read, .write = mydev_write, .unlocked_ioctl = mydev_ioctl, }; int __init mytest1_init(void) { dev_t devno = MKDEV(major, minor); int ret = -1; if((ret = register_chrdev_region(devno, dev_num, "mytest1")) < 0) { ret = alloc_chrdev_region(&devno, minor, dev_num, "mytest1"); if(ret < 0) { printk("alloc_chrdev_region fail!n"); return -1; } major = MAJOR(devno); } fops.owner = THIS_MODULE; cdev_init(&pmydev.mydev, &fops); pmydev.mydev.owner = THIS_MODULE; cdev_add(&pmydev.mydev, devno, dev_num); return 0; } void __exit mytest1_exit(void) { dev_t devno = MKDEV(major, minor); cdev_del(&pmydev.mydev); unregister_chrdev_region(devno, dev_num); } MODULE_LICENSE("GPL"); module_init(mytest1_init); module_exit(mytest1_exit);
mytest.h:
#ifndef MYTEST_H #define MYTEST_H #include #define IOCTL_TYPE 'k' #define MYDEV_IOCTL_GET_MUTEX _IOR(IOCTL_TYPE, 1,int) #define MYDEV_IOCTL_GET_SEEK _IOR(IOCTL_TYPE, 2,int) #endif
test_app:
#include#include #include #include #include #include #include "mytest.h" int main(int argc, const char *argv[]) { char buf[20] = "hello ...."; int ret; int dev_max; //设备空间大小 int dev_seek; //设备当前存储位置 if(argc < 2) { printf("Run without file!n"); } int fd = open("/dev/mydev", O_RDWR); if(fd < 0) { perror("open"); return -1; } ret = ioctl(fd, MYDEV_IOCTL_GET_MUTEX, &dev_max); //读取设备空间大小 if(ret < 0) { perror("ioctl1"); return -1; } printf("Space size of the kernel device is %dn", dev_max); ret = ioctl(fd, MYDEV_IOCTL_GET_SEEK, &dev_seek); //读取设备当前存储位置 if(ret < 0) { perror("ioctl2"); return -1; } printf("Current location of kernel storage is %dn", dev_seek); ret = write(fd, buf, strlen(buf)); if(ret < 0) { perror("write"); return -1; } memset(buf, 0, sizeof(buf)); ret = read(fd, buf, 5); if(ret < 0) { perror("read1"); return -1; } printf("%s ", buf); memset(buf, 0 ,sizeof(buf)); //这里再读一次观察验证内核函数是否正确 ret = read(fd, buf, sizeof(buf)); if(ret < 0) { perror("read2"); return -1; } printf("%sn", buf); close(fd); return 0; }
到这里就结束啦!



