我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Linux内核设备驱动之字符设备驱动笔记整理

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Linux内核设备驱动之字符设备驱动笔记整理


(1)字符设备驱动介绍

字符设备是指那些按字节流访问的设备,针对字符设备的驱动称为字符设备驱动。

此类驱动适合于大多数简单的硬件设备。比如并口打印机,我们通过在/dev下建立一个设备文件(如/dev/printer)来访问它。

用户应用程序用标准的open函数打开dev/printer,然后用write向文件中写入数据,用read从里面读数据。

调用流程:

  • write(): 用户空间 -->
  • sys_write(): VFS -->
  • f_op->write: 特定设备的写方法

所谓驱动,就是提供最后的write函数,通过访问打印机硬件的寄存器直接和打印机对话

(2)主设备号和次设备号

a.设备编号介绍

对字符设备的访问是通过文件系统内的设备文件进行的。这些文件位于/dev。用"ls -l"查看。

设备通过设备号来标识。设备号分两部分,主设备号和次设备号。

通常,主设备号标示设备对应的驱动程序,linux允许多个驱动共用一个主设备号;

而次设备号用于确定设备文件所指的设备。

在内核中,用dev_t类型<linux/types.h>保存设备编号。

2.4内核中采用16位设备号(8位主,8位从),而2.6采用32位,12位主,20位从。

在驱动中访问设备号应该用<linux/kdev_t.h>中定义的宏。

获取设备号:

  • MAJOR(dev_t dev)
  • MINOR(dev_t dev)
  • MKDEV(int major, int minor)

b.分配和释放设备编号

在建立一个字符设备前,驱动需要先获得设备编号。

分配:


#include <linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, char *name);
//first:要分配的设备编号范围的起始值(次设备号常设为0)
//count: 所请求的连续编号范围
//name: 和编号关联的设备名称(见/proc/devices)

也可以要求内核动态分配:


int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
//firstminor: 通常为0
/
}

b.初始化


void cdev_init(struct cdev *cdev, struct file_operations *fops)

c.设定cdev中的内容

  • dev->cdev.owner = THIS_MODULE;
  • dev->cdev.ops = &scull_fops;

d.向内核添加设定好的cdev


int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
//num: 设备对应的第一个编号
//count: 和设备关联的设备编号的数量,常取UtJCUalm1
//一旦cdev_add返回,内核就认为设备可以使用了,所以要在调用之前完成设备的硬件初始化。

(5)老式的注册函数

2.4中的老式注册函数仍然在驱动函数中大量存在,但新的代码不应该使用这些代码。

注册:


int register_chrdev(unsigned int major,
  const char *name,
  struct file_operations *fops);
//为给定的主设备号注册0~255作为次设备号,并为每个设备建立一个对应的默认cdev结构

注销:


int unregister_chrdev(unsigned int major,
  const char *name);

(6)open和release

a.open

在驱动的open方法中完成设备的初始化工作,open完成后,硬件就可以使用,用户程序可以通过write等访问设备,open的工作有:

  • *检查设备的特定错误
  • *如果设备首次打开,则对其进行初始化(有可能多次调用open)
  • *如有必要,更新f_op指针
  • *分配并填写置于filp->private_data中的数据

open原型;


int (*open) (struct inode *inode, struct file *filp);
//在open中通过inode获得dev指针,并将其赋给file->private_data
//struct scull_dev *dev;
//dev = contain_of(inode->i_cdev, struct scull_dev, cdev);
//filp->private_data = dev;
//(如果dev是静态分配的,则在open或write等方法中可以直接访问dev,但如果dev是在module_init时动态分配的,则只能通过上面的方法获得其指针)

b.release

并不是每个close调用都会引起对release方法的调用,只有当file的计数器归零时,才会调用release,从而释放dev结构)

(7)read和write

read和write的工作是从用户空间拷贝数据到内核,或是将内核数据拷贝到用户空间。其原型为:


ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
//buff: 用户空间的缓冲区指针
//offp: 用户在文件中进行存取操作的位置
//在read和write中,拷贝完数据后,应该更新offp,并将实际完成的拷贝字节数返回。

(8)和用户空间交换数据

read和write中的__user *buff 是用户空间的指针,内核不能直接引用其中的内容(也就是不能直接对buff进行取值操作),需要通过内核提供的函数进行数据拷贝。其原因是:

  • a.在不同架构下,在内核模式中运行时,用户空间的指针可能是无效的。
  • b.用户空间的内存是分页的,系统调用执行时,buff指向的内存可能根本不在RAM中(被交换到磁盘中了)
  • c.这可能是个无效或者恶意指针(比如指向内核空间)

内核和用户空间交换数据的函数见<asm/uaccess.h>

如:

1. unsigned long copy_to_user(
      void __user *to, 
      const void *from, 
      unsigned long count);
//向用户空间拷贝数据

2. unsigned long copy_from_user(
      void *to, 
      const void __user *from, 
      unsigned long count);
//从用户空间获得数据

3. int put_user(datum, ptr)
//向用户空间拷贝数据。字节数由sizeof(*ptr)决定
//返回值为0成功,为负错误。

4. int get_user(local, ptr);
//从用户空间获得数据。字节数由sizeof(*ptr)决定
//返回值和local都是从用户空间获得的数据

任何访问用户空间的函数都必须是可睡眠的,这些函数需要可重入。

copy_to_user等函数如果返回值不等于0,则read或write应向用户空间返回-EFAULT

主设备号用来表示设备驱动, 次设备号表示使用该驱动的设备

在内核dev_t 表示设备号, 设备号由主设备号和次设备号组成


#include <linux/kdev_t.h>
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //根据设备号获取主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //获取次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))     //根据指定的主设备和次设备号生成设备号
#include <linux/fs.h> 
//静态:申请指定的设备号, from指设备号, count指使用该驱动有多少个设备(次设备号), 设备名 
int register_chrdev_region(dev_t from, unsigned count, const char *name);
//name的长度不能超过64字节 
//动态申请设备号, 由内核分配没有使用的主设备号, 分配好的设备存在dev, baseminor指次设备号从多少开始, count指设备数, name设备名 
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
//释放设备号, from指设备号, count指设备数
void unregister_chrdev_region(dev_t from, unsigned count)
//cat /proc/devices 可查看设备使用情况
在内核源码的documentations/devices.txt可查看设备号的静态分配情况
///内核里使用struct cdev来描述一个字符设备驱动 
#include <linux/cdev.h>
struct cdev {
struct kobject kobj;    //内核用于管理字符设备驱动 
struct module *owner;   //通常设为THIS_MODULE, 用于防止驱动在使用中时卸载驱动模块
const struct file_operations *ops; //怎样操作(vfs)
struct list_head list;   //因多个设备可以使用同一个驱动, 用链表来记录
dev_t dev;         //设备号
unsigned int count;    //设备数
};

////////字符设备驱动//////////

1. 申请设备号

2. 定义一个cdev的设备驱动对象


struct cdev mycdev; 
//定义一个file_operations的文件操作对象
struct file_operations fops = {
.owner = THIS_MODULE,
.read = 读函数
....
};

3. 把fops对象与mycdev关联起来


cdev_init(&mycdev, &fops); //mycdev.ops = &fops;
mycdev.owner = THIS_MODULE; 

4. 把设备驱动加入内核里, 并指定该驱动对应的设备号

cdev_add(&mycdev, 设备号, 次设备号的个数);

5. 卸载模块时, 要把设备驱动从内核里移除, 并把设备号反注册


cdev_del(&mycdev);
///////////创建设备文件
mknod /dev/设备文件名 c 主设备号 次设备号
////////inode节点对象描述一个文件/设备文件, 包括权限,设备号等信息
struct inode {
...
dev_t i_rdev;   //设备文件对应的设备号
struct cdev *i_cdev; //指向对应的设备驱动对象的地址
...
};
////file对象描述文件描述符, 在文件打开时创建, 关闭时销毁
struct file {
...
const struct file_operations *f_op; //对应的文件操作对象的地址
unsigned int f_flags; //文件打开的标志
fmode_t f_mode; //权限
loff_t f_pos;  //文件描述符的偏移
struct fown_struct f_owner; //属于哪个进程
unsigned int f_uid, f_gid; 
void *private_data; //给驱动程序员使用
...
};

通file里的成员f_path.dentry->d_inode->i_rdev可以获取到设备文件的设备号

///错误码在<asm/errno.h> ////

/////////struct file_operations ////

inode表示应用程序打开的文件的节点对象,  file表示打开文件获取到的文件描述符

成功返回0, 失败返回错误码

int (*open) (struct inode *, struct file *);

buf指向用户进程里的缓冲区, len表示buf的大小(由用户调用read时传进来的)

off表示fl文件描述符的操作偏移, 返回值为实际给用户的数据字节数.

ssize_t (*read) (struct file *fl, char __user *buf, size_t len, loff_t *off);

用户进程把数据给驱动

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

to指用户进程的缓冲区, from指驱动里装数据的缓冲区, n多少字节, 返回值是0

extern inline long copy_to_user(void __user *to, const void *from, long n)

to指驱动的...   from用户...    n多少字节, ....


static inline unsigned long __must_check copy_to_user(void __user *to, const
void *from, unsigned long n)
{
if (access_ok(VERIFY_WRITE, to, n))
n = __copy_to_user(to, from, n);
return n; //返回值为剩下多少字节没拷贝
}

extern inline long copy_from_user(void *to, const void __user *from, long n)
  • 如果与用户进程交互的数据是1,2,4,8字节的话, 可用put_user(x,p) //x为值, p为地址
  • 如果从用户进程获取1,2,4字节的话, 可用get_user(x,p) 

///////////
///动态申请内存, 并清零. size为申请多大(不要超过128K),
//flags为标志(常为GFP_KERNEL). 成功返回地址, 失败返回NULL
// GFP_ATOMIC, 使用系统的内存紧急池
void *kmalloc(size_t size, gfp_t flags);//申请后要内存要清零
void *kzalloc(size_t size, gfp_t flags); //申请出来的内存已清零
void kfree(const void *objp); //回收kmalloc/kzalloc的内存
void *vmalloc(unsigned long size); //申请大内存空间
void vfree(const void *addr); //回收vmalloc的内存
// kmalloc申请出来的内存是物理地址连续的, vmalloc不一定是连续的
///// container_of(ptr, type, member) type包括member成员的结构体,
//ptr是type类型 结构体的member成员的地址.
//此宏根据结构体成员的地址获取结构体变量的首地址
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
 15 typedef struct led_dev_t {
 16     dev_t mydevid;
 17     unsigned int *rLEDCON;
 18     unsigned int *rLEDDAT;
 19     struct cdev mycdev;
 20 }LED_DEV;
 LED_DEV myled;
 //ind->i_cdev是指向myled.mycdev成员的地址
 //结构体变量myled首地址可由container_of(ind->i_cdev, LED_DEV, mycdev)获取;

/////// 自动创建设备文件 ////


#include <linux/device.h>

1.  


struct class *cl; 
cl = class_create(owner, name) ; //owner指属于哪个模块, name类名
//创建出来后可以查看 /sys/class/类名
void class_destroy(struct class *cls); //用于销毁创建出来的类

2. 创建设备文件


struct device *device_create(struct class *cls, struct device *parent,
  dev_t devt, void *drvdata,
  const char *fmt, ...)
  __attribute__((format(printf, 5, 6)));
device_create(所属的类, NULL, 设备号, NULL, "mydev%d", 88); //在/dev/目录下产生名字为mydev88的设备文件
void device_destroy(struct class *cls, dev_t devt); //用于销毁创建出来的设备文件
////////
int register_chrdev(unsigned int major, const char *name,
  const struct file_operations *fops) ; //注册设备号并创建驱动对象
void unregister_chrdev(unsigned int major, const char *name); //反注册设备号并删除驱动对象
static inline int register_chrdev(unsigned int major, const char *name,
 const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
int __register_chrdev(unsigned int major, unsigned int baseminor,
   unsigned int count, const char *name,
   const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
cd = __register_chrdev_region(major, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, baseminor, count));
return err;
}

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Linux内核设备驱动之字符设备驱动笔记整理

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Linux内核设备驱动之字符设备驱动笔记整理

(1)字符设备驱动介绍 字符设备是指那些按字节流访问的设备,针对字符设备的驱动称为字符设备驱动。 此类驱动适合于大多数简单的硬件设备。比如并
2022-06-04

Linux内核设备驱动之高级字符设备驱动笔记整理

(1)ioctl 除了读取和写入设备外,大部分驱动程序还需要另外一种能力,即通过设备驱动程序执行各种类型的硬件控制。比如弹出介质,改变波特率编程
2022-06-04

Linux内核设备驱动之Linux内核基础笔记整理

1. linux内核驱动模块机制 静态加载, 把驱动模块编进内核, 在内核启动时加载 动态加载, 把驱动模块编为ko, 在内核启动后,需要用时加载 2. 编写内核驱动#include #include
2022-06-04

Linux内核设备驱动之内存管理笔记整理

到目前为止,内存管理是unix内核中最复杂的活动。我们简单介绍一下内存管理,并通过实例说明如何在内核态获得内存。 (1)各种
2022-06-04

Linux内核设备驱动之系统调用笔记整理

(1)什么是系统调用 系统调用是内核和应用程序间的接口,应用程序要访问硬件设备和其他操作系统资源,必须通
2022-06-04

Linux内核设备驱动之内核的时间管理笔记整理

(1)内核中的时间概念 时间管理在linux内核中占有非常重要的作用。 相对于事件驱动而言,内核中有大量函数是基于时间驱动的。 有些函数
2022-06-04

Linux内核设备驱动之内核的调试技术笔记整理

(1)内核源代码中的一些与调试相关的配置选项 内核的配置选项中包含了一些与内核调试相关的选项,都集中在"kernel hacking"菜单中。包括
2022-06-04

Linux内核设备驱动之proc文件系统笔记整理

(1)/proc文件系统的特点和/proc文件的说明 /proc文件系统是一种特殊的、由软件创建的文件系统
2022-06-04

Linux内核设备驱动之内核中链表的使用笔记整理

(1)介绍 在linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在include
2022-06-04

Linux内核设备驱动之Linux内核模块加载机制笔记整理

#include 1. 模块参数 在驱动定义变量static int num = 0; //当加载模块不指定num的值时则为0 module_param(变量名, 类型, 权限);类型:
2022-06-04

Linux内核设备驱动之虚拟文件系统笔记整理

(1)VFS介绍 虚拟文件系统VFS作为内核的子系统,为用户空间程序提供了文件系统的相关接口。 VFS使得用户可以直接使用open()
2022-06-04

Linux设备之网络驱动介绍

有线网络:以太网 无线网络:4G、wifi、蓝牙、5G总结:内部MAC + 外部PHY + RJ45 座(内置网络变压器)就组成了一个完整的嵌入式网络接口硬件。 内部的 MAC 外设会通过 MII 或者 wOBQgBgZpCRMII 接口来
2022-06-04

Linux设备驱动之workqueue怎么使用

在Linux设备驱动中,workqueue是一种用于延迟执行任务的机制。它是由内核提供的一种工作队列,可以用来在后台执行一些需要延迟处理的任务,而不会阻塞当前的进程或线程。要使用workqueue,你需要按照以下步骤进行操作:定义一个wo
2023-10-24

笔记本电脑更新设备驱动异常怎么解决

本文小编为大家详细介绍“笔记本电脑更新设备驱动异常怎么解决”,内容详细,步骤清晰,细节处理妥当,希望这篇“笔记本电脑更新设备驱动异常怎么解决”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。一、首先按下快捷键wind
2023-06-28

笔记本电脑如何取消自动搜索安装设备驱动程序

这篇文章主要讲解了“笔记本电脑如何取消自动搜索安装设备驱动程序”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“笔记本电脑如何取消自动搜索安装设备驱动程序”吧!1、按win+r打开运行窗口,输入
2023-06-28

Linux设备驱动指的定时与延时如何理解

本篇文章为大家展示了Linux设备驱动指的定时与延时如何理解,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。Linux通过系统硬件定时器以规律的间隔(由HZ度量)产生定时器中断,每次中断使得一个内核计
2023-06-16

如何显示Linux系统的内置模块和设备驱动列表

这篇文章主要讲解了“如何显示Linux系统的内置模块和设备驱动列表”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何显示Linux系统的内置模块和设备驱动列表”吧!提问:我想要知道Linux
2023-06-12

编程热搜

目录