说明: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");