4-Linux驱动开发-字符设备驱动

说明:Linux 是文件型系统,所有硬件都会在对应的目录 (/dev) 下面用相应的文件表示。特点是访问文件的方式访问设备。
设备种类:

html 复制代码
设备种类:
	字符设备: 应用程序按字节/字符来读写数据的设备。流形式,逐一存储。
	块设备: 支持随机存取和寻址,并使用缓存器。数据的读写只能以块的倍数进行。
	网络设备: 一种特殊设备,并不存在于/dev 下,主要用于网络数据的收发。

设备文件:

html 复制代码
linux 中,使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。
cdev 结构体被内核用来记录设备号,
使用设备时,打开设备节点,通过设备节点的 inode 结构体、file 结构体最终找到 file_operations 结构体,
从 file_operations 结构体中得到操作设备的具体方法。

ls -l /dev
设备文件详情
crw-r--r--  1 root root     10, 235 11月  3 23:11 autofs
第 1 列:文件类型和权限
	第 1 个字符 (文件类型):
		c:字符设备 (Character Device)。
		d:目录 (Directory)。
		l:符号链接 (Symbolic Link) / 快捷方式。
		b:块设备 (Block Device)。
	后 9 个字符 (权限):三组:Owner (所有者), Group (所属组), Others (其他人)。
第 2 列:硬链接数 (1):有多少个文件名指向这个inode(文件的物理数据)。1 或 2,可以暂时忽略。
第 3 列:所有者 (Owner)。
第 4 列:所属组 (Group)。
第 5 列:主设备号, 次设备号
	对于普通文件,这里显示的是文件大小。
	对于字符设备 (c) 和块设备 (b)
		主设备号 (Major Number)。内核用它来识别驱动程序。
		次设备号 (Minor Number)。内核用它来告诉驱动具体是哪个设备。
第 6 列:最后修改时间。
第 7 列:文件名。对于链接 (l)会显示一个"箭头",cdrom -> sr0 意味着 cdrom 只是一个指向 sr0 的"快捷方式"。

字符设备驱动结构

字符cdev 结构体

内核通过一个散列表 (哈希表) 来记录设备编号。哈希表由数组和链表组成,吸收数组查找快,链

表增删效率高,容易拓展等优点。设备号为 cdev_map 编号,使用哈希函数来计算组数下标。

Linux内核,使用cdev结构体描述一个字符设备。

/home/kun/kernal_code/ebf_linux_kernel-ebf_4.19_star/include/linux/cdev.h

c 复制代码
struct cdev {
	struct kobject kobj; //Kernel Object
	//(内核内部使用) 用于 sysfs 表示和引用计数。不用管。
	
	struct module *owner; //Module Owner
	//(必须提供) 指向 THIS_MODULE,用于防止模块在使用时被卸载(cdev_init 会自动设置)。
	
	const struct file_operations *ops;//File Operations
	//(必须提供) 指向file_operations结构体(函数菜单:open, read, write...)。
	
	struct list_head list;//List Head
	//(内核内部使用) 用于将 cdev 链接到内核的全局哈希表中。不用管。
	
	dev_t dev;//Device Number
	//(内核填充) 存储"主设备号"和"起始次设备号"。
	
	unsigned int count;
	//(内核填充) 驱动能管理多少个"连续"的次设备号。
};

相关宏

c 复制代码
//使用以下宏可获得主设备号以及次设备号 
MAJOR(dev_t dev)
MINOR(dev_t dev)
//使用以下宏可根据主设备号以及次设备号生成dev_t
MKDEV(int major,int minor)

操作dev结构体的函数

/home/kun/kernal_code/ebf_linux_kernel-ebf_4.19_star/fs/char_dev.c

c 复制代码
EXPORT_SYMBOL(cdev_init);
EXPORT_SYMBOL(cdev_alloc);
EXPORT_SYMBOL(cdev_del);
EXPORT_SYMBOL(cdev_add);
//初始化cdev成员,并建立cdev和file_operations之间的连接
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}
//动态申请一个cdev内存
struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&p->list);
		kobject_init(&p->kobj, &ktype_cdev_dynamic);
	}
	return p;
}
//cdev_del,cdev_add用于删除添加cdev,用于字符设备的注册与注销
void cdev_del(struct cdev *p)
{
	cdev_unmap(p->dev, p->count);
	kobject_put(&p->kobj);
}
//cdev_add向系统注册字符设备之前要先调用register_chrdev_region或alloc_chrdev_region向系统申请设备号
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
	int error;

	p->dev = dev;
	p->count = count;

	error = kobj_map(cdev_map, dev, count, NULL,
			 exact_match, exact_lock, p);
	if (error)
		return error;

	kobject_get(p->kobj.parent);

	return 0;
}
//用于已知起始设备的设备号
/*from指定字符设备的起始设备号
**count指定要申请的设备号个数
**name:用于指定该设备的名称,可以在/proc/devices 中看到该设备*/
int register_chrdev_region(dev_t from, unsigned count, const char *name);

//用于设备号未知,向系统动态申请未被占用的设备号,得到的设备号放入dev,可以自动避开设备冲突
/*dev存放分配到的设备编号的起始值
**baseminor次设备号的起始值,通常情况下,设置为 0
**count、name指定需要分配的设备编号的个数以及设备的名称*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
//删除
void unregister_chrdev_region(dev_t from, unsigned count);

内核数据结构

文件操作方式(file_operations)

html 复制代码
file_operation 关联系统调用和驱动程序。这个结构的每一个成员都对应着一个系统调用。
file_operations 结构体定义在/home/kun/kernal_code/ebf_linux_kernel-ebf_4.19_star/include/linux/fs.h中
fs.h文件太长了ctrl+f 查找file_operations结构体 

字符设备驱动用到以下部分:

c 复制代码
struct file_operations {
	struct module *owner;
	//llseek:用于修改文件的当前读写位置,并返回偏移后的位置。
	loff_t (*llseek) (struct file *, loff_t, int);
	//read:用于读取设备中的数据,并返回成功读取的字节数。
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	//write:用于向设备写入数据,并返回成功写入的字节数。
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	//nlocked_ioctl:提供设备执行相关控制命令的实现方法,它对应于应用程序的 fcntl 函数以
及 ioctl 函数。
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	//open:设备驱动第一个被执行的函数,一般用于硬件的初始化。
	int (*open) (struct inode *, struct file *);
	//release:当 file 结构体被释放时,将会调用该函数。
	int (*release) (struct inode *, struct file *);
}

字符设备驱动程序框架

c 复制代码
==============================================================================
//定义字符设备只取其一
//变量定义方式
static struct cdev chrdev;
//内核提供的动态分配方式
struct cdev *cdev_alloc(void);

//上述分配对应的移除内核中移除某个字符设备
void cdev_del(struct cdev *p);
==============================================================================
/*由于cdev_add向系统注册字符设备之前
要先调用register_chrdev_region或alloc_chrdev_region向系统申请设备号
因此注册流程如下:*/
//申请设备号只取其一
//指定字符设备的起始设备号注册
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用于关联字符操作结构体(file_operations)与字符设备结构体(cdev)
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
==============================================================================
/*激活驱动*/
//向内核的 cdev_map 散列表添加一个新的字符设备
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
//从系统中删除 cdev
void cdev_del(struct cdev *p)
==============================================================================
/*为了让 udev 自动在 /dev/ 目录下创建文件,必须先创建一个"设备类"(Class),例如 /sys/class/my_driver_class/*/
//创建设备类
struct class *class_create(struct module *owner, const char *name);
/*struct module *owner: 指向"拥有"这个 Class 的内核模块。
**const char *name:要创建的"设备类"的名字*/
//销毁设备类
void class_destroy(struct class *cls);
==============================================================================
/*自动化 mknod内核会通知 udev 服务,udev 会自动在 /dev/my_device_name 创建设备文件,并链接到主次设备号。*/
//创建设备节点
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
/*class:指向这个设备应该注册到的 struct 类的指针;
**parent:指向此新设备的父结构设备(如果有)的指针;
**devt:要添加的 char 设备的开发;
**drvdata:要添加到设备进行回调的数据;
**fmt:输入设备名称。*/
//删除设备节点
void device_destroy(struct class *class, dev_t devt)
html 复制代码
创建设备号,绑定设备和操作,内核散列添加,创建节点设备注册(自上到下构建策略)
加载顺序:1. alloc_chrdev_region -> 2. cdev_init -> 3. cdev_add -> 4. class_create -> 5. device_create。

删除节点设备注销,内核散列移除,注销设备号(连根拔起顺序)
卸载"顺序是:1. device_destroy -> 2. class_destroy -> 3. cdev_del -> 4. unregister_chrdev_region。

字符设备驱动结构:

chrdev.c驱动部分代码

c 复制代码
/*
此代码模拟的128字节的U盘
当向这个文件写入数据时 (echo "hello" > /dev/embed_char_dev)数据会被复制并存储到内核的一个 128 字节的缓冲区 (vbuf) 中。
当从这个文件读取数据时 (cat /dev/embed_char_dev)驱动会将 vbuf 缓冲区中的内容返回。
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEV_NAME            "EmbedCharDev"
#define DEV_CNT                 (1)
#define BUFF_SIZE               128
//定义字符设备的设备号(主设备号,次设备号)
static dev_t devno;
//定义字符设备结构体chr_dev,用来注册驱动
static struct cdev chr_dev;
//数据缓冲区
static char vbuf[BUFF_SIZE];
static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);
//告诉内核用户相应操作时调用哪个函数,上面预先定义,下面进行函数实现
static struct file_operations  chr_dev_fops = 
{
    .owner = THIS_MODULE,
    .open = chr_dev_open,
    .release = chr_dev_release,
    .write = chr_dev_write,
    .read = chr_dev_read,
};

static int chr_dev_open(struct inode *inode, struct file *filp)
{
    printk("\nopen\n");
    return 0;
}

static int chr_dev_release(struct inode *inode, struct file *filp)
{
    printk("\nrelease\n");
    return 0;
}

static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
    //读写位置,偏移量
    unsigned long p = *ppos;
    int ret;
    //写入数量
    int tmp = count ;
    //每次写入位置不能超过BUFF_SIZE
    if(p > BUFF_SIZE)
        return 0;
    //写入超过BUFF_SIZE - p的部分进行截断
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    //从用户空间 (buf) 获取数据,并存入内核空间 (vbuf) 的方法
    ret = copy_from_user(vbuf, buf, tmp);
    *ppos += tmp;
    //返回实际写入的字节数
    return tmp;
}

static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    //统计 chr_dev_read 这个函数总共被调用了多少次无用
    static int i = 0;
    i++;
    //超出读范围
    if(p >= BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    //将内核数据 (vbuf+p) 发送回用户空间 (buf) 的方法
    ret = copy_to_user(buf, vbuf+p, tmp);
    *ppos +=tmp;
    return tmp;
}

//注册流程
static int __init chrdev_init(void)
{
    int ret = 0;
    printk("chrdev init\n");
    //第一步
    //采用动态分配的方式,获取设备编号,次设备号为0,
    //设备名称为EmbedCharDev,可通过命令cat  /proc/devices查看
    //DEV_CNT为1,当前只申请一个设备编号
    ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
    if(ret < 0){
        printk("fail to alloc devno\n");
        goto alloc_err;
    }
    //第二步
    //关联字符设备结构体cdev与文件操作结构体file_operations
    cdev_init(&chr_dev, &chr_dev_fops);
    //第三步
    //添加设备至cdev_map散列表中
    ret = cdev_add(&chr_dev, devno, DEV_CNT);
    if(ret < 0)
    {
        printk("fail to add cdev\n");
        goto add_err;
    }
    return 0;

add_err:
    //添加设备失败时,需要注销设备号
    unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
    return ret;
}
module_init(chrdev_init);

static void __exit chrdev_exit(void)
{
    printk("chrdev exit\n");
    unregister_chrdev_region(devno, DEV_CNT);

    cdev_del(&chr_dev);
}
module_exit(chrdev_exit);

MODULE_LICENSE("GPL");

main.c测试程序部分代码

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
char *wbuf = "Hello World\n";
char rbuf[128];
int main(void)
{
    printf("EmbedCharDev test\n");
    //打开文件
    int fd = open("/dev/chrdev", O_RDWR);
    //写入数据
    write(fd, wbuf, strlen(wbuf));
    //写入完毕,关闭文件
    close(fd);
    //打开文件
     fd = open("/dev/chrdev", O_RDWR);
    //读取文件内容
    read(fd, rbuf, 128);
    //打印读取的内容
    printf("The content : %s", rbuf);
    //读取完毕,关闭文件
    close(fd);
    return 0;
}

make后进行cp chrdev.ko然后进行NFS挂载

linux板子上

bash 复制代码
#插入模块
sudo insmod chrdev.ko

#虚拟文件用来实时向用户汇报系统状态。 process
cat /proc/devices
#243 EmbedCharDev 主设备号为 243

#创建新设备节点
sudo mknod /dev/chrdev c 243 0

#Makefile中会产生内核模块.ko以及一个c代码文件chrdev_test
#方案一:执行查看结果
root@lubancat:/mnt/host_projects# ./chrdev_test
EmbedCharDev test
The content : Hello World
 test
debian@lubancat:/mnt/host_projects$ echo "EmbedCharDev test" > /dev/chrdev
#方案二:通过 echo 和 cat 命令,来测试设备驱动程序。
root@lubancat:~# cat /dev/chrdev
EmbedCharDev test

#不需要内核模块进行卸载并删除相应设备文件
rmmod chrdev.ko
rm /dev/chrdev

说明:由于没有class_create 和device_create,所以需要insmod之后进行mknode手动注册。

一个驱动支持多个设备

html 复制代码
区别于单个设备的部分
使用一套驱动代码,管理两个独立的 128 字节"U 盘"(DEV_CNT=2)
#define DEV_CNT                 (2)

把一个设备所有相关的东西都打包在一起
struct chr_dev{
    struct cdev dev;//字符设备
    char vbuf[BUFF_SIZE];//私有数据缓冲区
};

为两个设备分别创建了实例
static struct chr_dev vcdev1; // "设备 0" 的所有数据
static struct chr_dev vcdev2; // "设备 1" 的所有数据

注册"设备 0" 注意 count 是 1
cdev_init(&vcdev1.dev, ...) / cdev_add(&vcdev1.dev, devno+0, 1)
注册"设备 1" 注意 count 是 1
cdev_init(&vcdev2.dev, ...) / cdev_add(&vcdev2.dev, devno+1, 1)

查到主设备号后(驱动号)要进行两次节点注册
sudo mknod /dev/EmbedCharDev0 c 254 0
sudo mknod /dev/EmbedCharDev1 c 254 1

echo和cat时候通过不同的文件名进行操作
/dev/EmbedCharDev0 或 /dev/EmbedCharDev1
c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEV_NAME            "EmbedCharDev"
//向内核申请2 个连续的设备号。
#define DEV_CNT                 (2)
#define BUFF_SIZE               128
//定义字符设备的设备号,(major,minor)
static dev_t devno;

//虚拟字符设备
struct chr_dev{
    struct cdev dev;
    char vbuf[BUFF_SIZE];
};
//字符设备1
static struct chr_dev vcdev1;
//字符设备2
static struct chr_dev vcdev2;

static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);

static struct file_operations chr_dev_fops = {
    .owner = THIS_MODULE,
    .open = chr_dev_open,
    .release = chr_dev_release,
    .write = chr_dev_write,
    .read = chr_dev_read,
};
static int chr_dev_open(struct inode *inode, struct file *filp)
{
    printk("open\n");
    filp->private_data = container_of(inode->i_cdev, struct chr_dev, dev);
    return 0;
}

static int chr_dev_release(struct inode *inode, struct file *filp)
{
    printk("\nrelease\n");
    return 0;
}

static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    //获取文件的私有数据
    struct chr_dev *dev = filp->private_data;
    char *vbuf = dev->vbuf;

    int tmp = count ;
    if(p > BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_from_user(vbuf, buf, tmp);
    *ppos += tmp;
    return tmp;
}

static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    //获取文件的私有数据
    struct chr_dev *dev = filp->private_data;
    char *vbuf = dev->vbuf;
    if(p >= BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_to_user(buf, vbuf+p, tmp);
    *ppos +=tmp;
    return tmp;
}


static int __init chrdev_init(void)
{
    //ret返回值只用来记录是否成功
    int ret;
    printk("chrdev init\n");
    //申请设备号
    ret = alloc_chrdev_region(&devno, 0, DEV_CNT,  DEV_NAME);
    if(ret < 0)
        goto alloc_err;

    //关联第一个设备:vdev1
    cdev_init(&vcdev1.dev, &chr_dev_fops);
    //1 cdev 对象只管理 1 个次设备号。
    ret = cdev_add(&vcdev1.dev, devno+0, 1);
    if(ret < 0){
        printk("fail to add vcdev1 ");
        goto add_err1;
    }
    //关联第二个设备:vdev2
    cdev_init(&vcdev2.dev, &chr_dev_fops);
    ret = cdev_add(&vcdev2.dev, devno+1, 1);
    if(ret < 0){
        printk("fail to add vcdev2 ");
        goto add_err2;
    }
    return 0;
add_err2:
    cdev_del(&(vcdev1.dev));
add_err1:
    unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
    return ret;

}

module_init(chrdev_init);



static void __exit chrdev_exit(void)
{
    printk("chrdev exit\n");
    unregister_chrdev_region(devno, DEV_CNT);
    cdev_del(&(vcdev1.dev));
    cdev_del(&(vcdev2.dev));
}
module_exit(chrdev_exit);


MODULE_LICENSE("GPL");
相关推荐
2401_865854882 小时前
AI软件可以帮助我自动化哪些日常任务?
运维·人工智能·自动化
遇见火星2 小时前
Linux 网络性能测试实战:用 iperf3 精准测出真实带宽与丢包率
linux·网络·php·iperf3
赖small强2 小时前
【Linux驱动开发】Linux块设备驱动开发详解
linux·驱动开发·块设备·字符设备
qq_401700412 小时前
Linux 信号机制
linux·运维·服务器
!chen3 小时前
Zabbix 配置中文界面、监控告警以及Windows、Linux主/被监控模板
linux·windows·zabbix
_Stellar3 小时前
Linux 服务器配置 rootless docker Quick Start
linux·服务器·docker
石像鬼₧魂石3 小时前
Kali Linux 中对某(靶机)监控设备进行漏洞验证的完整流程(卧室监控学习)
linux·运维·学习
Hqst_xiangxuajun3 小时前
服务器主板选用网络变压器及参数配置HX82409S
运维·服务器·网络
CS创新实验室3 小时前
练习项目:基于 LangGraph 和 MCP 服务器的本地语音助手
运维·服务器·ai·aigc·tts·mcp