学习记录 字符设备驱动框架

本文围绕Linux字符设备驱动开发展开,介绍了设备分类,包括字符、块和网络设备;阐述设备号组成及相关宏;讲解函数指针、内存四区和回调函数;还涉及注册字符设备方法、驱动框架解析,以及读、写、ioctl操作实现等内容,最后提及避免用全局变量和多设备支持。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、设备分类

文件种类
  1. -:普通文件
  2. d:目录文件
  3. p:管道文件
  4. s:本地socket文件
  5. l:连接文件
  6. c:字符设备。/dev/
  7. b:块设备
文件信息
  • 文件内容
    • p、s、c、b无文件内容
  • 文件名
  • 元信息(文件信息,时间戳、用户权限等)
设备分类

Linux内核按驱动程序实现模型框架的不同,将设备分为三类:

  1. 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
  2. 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节,随机访问,设缓存以提高效率
  3. 网络设备:针对网络数据收发的设备,非文件

2、设备号

设备号

内核用设备号来区分同类里不同的设备,设备号是一个无符号32位整数,数据类型为dev_t,设备号分为两部分:

  1. 主设备号:占高12位,用来表示驱动程序相同的一类设备
  2. 次设备号:占低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
申请设备号

字符驱动开发的第一步是通过模块的入口函数向内核添加本设备驱动的代码框架,主要完成:

  1. 申请设备号
  2. 定义、初始化、向内核添加代表本设备的结构体元素

批量验证设备号(手动申请)

int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
参数:
	from:自己指定的设备号
	count:申请的设备数量
	name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号
返回值:
	成功为0,失败负数,绝对值为错误码

动态分配设备号

int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name)
功能:动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
参数:
	dev:分配设备号成功后用来存放分配到的设备号
	baseminior:起始的次设备号,一般为0
	count:申请的设备数量
	name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号
返回值:
	成功为0,失败负数,绝对值为错误码

注销设备号

释放设备号

void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:
	from:已成功分配的设备号将被释放
	count:申请成功的设备数量
释放后/proc/devices文件对应的记录消失

3、函数指针

内存四区
  • 堆区
  • 栈区
  • 数据区
  • 代码区
数据访问

直接访问:通过所在空间名称访问
间接访问:*地址运算结果,堆区只能用间接访问

函数调用方式

直接调用:通过函数名调用
间接调用:通过函数在的代码区对应的空间的首地址去调用

int *pf(int a, int b);//函数声明

int (*pf)(int ,int);//定义一个函数指针

pf = &func;//&运算符后面如果是函数名可以省略
pf = func

y = (*pf)(2.3);//间接调用,*后面是函数指针类型可省略 
y = pf(2,3);

typedef int myint;
typedef int (*)(int, int) pft;//语法错误
typedef int (*pft)(int, int);//别名为pft
pft pt;
适用场合
  • A处:决定B处使用的函数名
    • pf = func1;
  • B处:调用A处指定函数
    • pf(2,3);
回调函数
#include <stdio.h>

void myfuncA(){
	printf("A");
}
void myfuncB(){
	printf("B");
}

void program(void (*callback)()){
	printf("program");
	callback();
}

int main(int argc, char **argv){
	program(myfuncB);
}

4、注册字符设备

方法
  1. 申请设备号,设备名字(设备种类名)
  2. struct cdev mydev 注册
  3. struct file_operations myops定义系统调用
  4. cdev_init(&mydev, &myops);指定操作函数集
  5. mydev.owner = THIS_MODULE;cdev_add(&mydev, devno, mychar_num);将 struct cdev对象添加到内核对应的数据结构里
  6. cdev_del(&mydev);在内核移除字符设备
  7. 将模块添加到内核
  8. 在/dev/下创建字符设备名字sudo mknod /dev/mychar c 11 0
  9. app调用sudo ./test /dev/mychar
相关函数
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;//设备数量
};

/*
	有两种定义方式,一般用手动:
	1、直接定义:定义结构体全局变量
	2、动态申请:struct cdev *cdev_alloc()
*/
/*
	该对象各个函数指针成员都对应相应的系统调用函数,应用层通过调用系统函数来间接调用这些函数指针成员指向的设备驱动函数:
	一般定义一个struct file_operations类型的全局变量并用自己实现各种操作函数名对其进行初始化
*/
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:指向被移除的字符设备
代码实现
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>

int major = 11;
int minor = 0;
int mychar_num = 1;

struct cdev mydev;

int mychar_open(struct inode *pnode, struct file *pfile)
{
    printk("mychar_open is called\n");
    return 0;
}

int mychar_close(struct inode *pnode, struct file *pfile)
{
    printk("mychar_close is called\n");
    return 0;
}

struct file_operations myops = {
    .owner = THIS_MODULE,
    .open = mychar_open,
    .release = mychar_close,
};

int mychar_init(void)
{
    int ret = 0;
    dev_t devno = MKDEV(major, minor);

    /* 申请设备号 */
    ret = register_chrdev_region(devno, mychar_num, "mychar");
    if (ret) {
        ret = alloc_chrdev_region(&devno, minor, mychar_num, "mychar");
        if (ret) {
            printk("get devno failed\n");
            return -1;
        }
		major = MAJOR(devno); // 容易遗漏,注意
    }

    /* 给struct cdev对象指定操作函数集 */
    cdev_init(&mydev, &myops);

    /* 将 struct cdev对象添加到内核对应的数据结构里 */
    mydev.owner = THIS_MODULE;
    cdev_add(&mydev, devno, mychar_num);

    return 0;
}

void __exit mychar_exit(void)
{
    dev_t devno = MKDEV(major, minor);

    cdev_del(&mydev);

    unregister_chrdev_region(devno, mychar_num);
}

//表示支持GPL的开源协议
MODULE_LICENSE("GPL");

module_init(mychar_init);
module_exit(mychar_exit);

5、字符设备驱动框架解析

open函数
 int (*open) (struct inode *, struct file *);	//打开设备
inode结构体
内核中记录文件元信息的结构体
struct inode
{
	//....
	dev_t  i_rdev;//设备号
	struct cdev  *i_cdev;//如果是字符设备才有此成员,指向对应设备驱动程序中的加入系统的struct cdev对象。在创建inode链表时通过设备号查找填充
	//....
}
/*
	1. 内核中每个该结构体对象对应着一个实际文件,一对一
	2. open一个文件时如果内核中该文件对应的inode对象已存在则不再创建,不存在才创建
	3. 内核中用此类型对象关联到对此文件的操作函数集(对设备而言就是关联到具体驱动代码)
*/
file结构体
读写文件内容过程中用到的一些控制性数据组合而成的对象------文件操作引擎(文件操控器)
struct file
{
	//...
	mode_t f_mode;//不同用户的操作权限,驱动一般不用
	loff_t f_pos;//position 数据位置指示器,需要控制数据开始读写位置的设备有用
	unsignedint f_flags;//open时的第二个参数flags存放在此,驱动中常用
	structfile_operations*f_op;//open时从struct inode中i_cdev的对应成员获得地址,驱动开发中用来协助理解工作原理,内核中使用
	void*private_data;//本次打开文件的私有数据,驱动中常来在几个操作函数间传递共用数据
	structdentry*f_dentry;//驱动中一般不用,除非需要访问对应文件的inode,用法flip->f_dentry->d_inode
    int refcnt;//引用计数,保存着该对象地址的位置个数,close时发现refcnt为0才会销毁该struct file对象
	//...
};
/*
	1. open函数被调用成功一次,则创建一个该对象,因此可以认为一个该类型的对象对应一次指定文件的操作
	2. open同一个文件多次,每次open都会创建一个该类型的对象
	3. 文件描述符数组中存放的地址指向该类型的对象
	4. 每个文件描述符都对应一个struct file对象的地址
*/

6、读操作实现

read
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
copy_to_user
unsigned long copy_to_user(void *to, const void *from, unsigned long n);

	to:目标地址(用户空间)
	from:源地址(内核空间)
	n:要拷贝的字节数
	
	成功为返回0,失败非0
memcpy
void *memcpy(void *str1, const void *str2, size_t n)
	str1:目标地址
	str2:源地址
	n:要复制的字节

7、写操作实现

write
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
copy_from_user
unsigned long copy_to_user(void *to, const void *from, unsigned long n);

	to:目标地址(用户空间)
	from:源地址(内核空间)
	n:要拷贝的字节数
	
	成功为返回0,失败非0

8、避免使用全局变量

通过宏实现
pfile->private_data = container_of(成员地址, 结构体类型名, 成员在结构体中的名称)

9、ioctl操作实现

ioctl概念

为了处理设备非数据的操作,内核将对设备的控制操作委派给ioctl接口,ioctl也是一个系统调用,函数原型为:
int ioctl(int fd, int request, ...);

cmd组成
dirsizetypenr
2bit14bit8bit8bit
  • dir
    • ioctl 命令访问模式(属性数据传输方向),占据 2 bit,可以为:
    • _IOC_NONE:无数据
    • _IOC_READ:读数据
    • _IOC_WRITE:写数据
    • _IOC_READ | _IOC_WRITE:读写数据
  • size
    • 涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数;
  • tpye
    • 设备类型,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如 ‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识;
  • nr
    • 命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增;
宏定义实现

#define _IOC(dir,type,nr,size) (((dir)<<_IOC_DIRSHIFT)| \
                               ((type)<<_IOC_TYPESHIFT)| \
                               ((nr)<<_IOC_NRSHIFT)| \
                               ((size)<<_IOC_SIZESHIFT))
/* used to create numbers */

// 定义不带参数的 ioctl 命令
#define _IO(type,nr)   _IOC(_IOC_NONE,(type),(nr),0)

//命令需要从驱动中获取数据(copy_to_user) size为类型名
#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))

//命令需要把数据写入驱动(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)))
驱动代码
#include <asm/ioctl.h>

#define MY_CHAR_MAGIC 'k'

#define MYCHAR_IOCTL_GET_MAXLEN _IOR(MY_CHAR_MAGIC,1,int*)
#define MYCHAR_IOCTL_GET_CURLEN _IOR(MY_CHAR_MAGIC,2,int*)

long mychar_ioctrl(struct file *pfile, unsignet int cmd, unsigned long arg){
	int __user *pret = (int *)arg;
	int maxlen;
	int curlen;
	switch(cmd){
		case MYCHAR_IOCTL_GET_MAXLEN;
			copy_to_user(pret, &maxlen, sizeof(int));
			break;
		case MYCHAR_IOCTL_GET_CURLEN;
			copy_to_user(pret, &curlen, sizeof(int));
			break;
	}
}

struct file_operations myops={
	.unlocked_ioctrl = mychar_ioctrl,
}
应用层代码
ioctl(fd, MYCHAR_IOCTL_MAXLEN, &max);

10、printk

//日志级别
#define	KERN_EMERG	"<0>"	/* system is unusable			*/
#define	KERN_ALERT	"<1>"	/* action must be taken immediately	*/
#define	KERN_CRIT	"<2>"	/* critical conditions			*/
#define	KERN_ERR	"<3>"	/* error conditions			*/

#define	KERN_WARNING	"<4>"	/* warning conditions			*/

#define	KERN_NOTICE	"<5>"	/* normal but significant condition	*/
#define	KERN_INFO	"<6>"	/* informational			*/
#define	KERN_DEBUG	"<7>"	/* debug-level messages			*/

用法:printk(KERN_INFO"....",....)
查看:dmesg --level
//常用方式
#define HELLO_DEBUG
#undef PDEBUG
#ifdef HELLO_DEBUG
#define PDEBUG(fmt, args...) printk(KERN_DEBUG fmt, ##args)
#else
#define PDEBUG(fmt, args...)
#endif

11、多个次设备的支持

每一个具体设备(次设备不一样的设备),必须有一个struct cdev来代表它
cdev_init
cdev.owner赋值
cdev_add
以上三个操作对每个具体设备都要进行

驱动代码
int mychar_num = 3;//支持3个次设备

struct cdev mydev[mychar_num];

int __init mychar_init(void){
	int i;
	for(i = 0;i < mychar_num; i++){
		devno = MKDEV(major,minor+i);
		cdev_init(&mydev[i], &myops);
		
		mydev[i].owner = THIS_MODULE;
		cdev_add(&mydev[i], devno, 1);
	}
}

void __exit mychar_exit(void){
	int i = 0;
	for(i = 0;i < mychar_dev;i++){
		cdev_del(&mydev[i]);
	}
	unregister_chrdev_region(devno,mychar_num);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值