Linux字符设备驱动内幕

哈喽,我是老吴,继续记录我的学习心得。

一、保持专注的几个技巧

  • 将最重要的事放在早上做。

  • 待在无干扰环境下,比如图书馆。

  • 意识到刚坐下开始投入工作前,有点负面小情绪是特别正常的现象。

  • 让“开心一刻”成为计划的一部分。

  • 拥有合情合理的日计划和周计划。


二、Linux 字符设备驱动内幕 (1)

正文目录:

1. 什么是字符设备驱动?
2. 快速体验字符设备驱动和应用程序 (超简单的 demo)
3. 字符设备在内核里的抽象3.1 字符设备核心代码概览3.2 对字符设备进行抽象: struct cdev3.3 对字符设备的操作进行抽象:struct file_operations
4. 更多值得学习的知识点
5. 相关参考

写作目的:

  • 探索 Linux 字符设备驱动。

测试环境:

  • Ubuntu 16.04

  • Gcc 5.4.0

1. 什么是字符设备驱动?

  • 现实世界中存在着成千上万的硬件设备,这些设备在硬件特性和使用方式上都各不相同。Linux 系统的大牛们从这些形形色色的设备中提取出共性,将它们抽象为三大类:字符设备、块设备和网络设备

  • 基于代码质量和复用性的考虑,Linux 内核针对每一类硬件设备都提供了对应的驱动模型框架,一般包括基本的内核设施和文件系统接口。开发人员在写某类设备驱动程序时,能一套完整的驱动模型框架可以使用,从而将精力放在硬件设备本身的控制上。

  • 简单的 Linux 设备驱动程序结构图:

  • 详细一点的 Linux 设备驱动程序结构图:


2. 快速体验字符设备驱动和应用程序 (超简单的 demo)

1) 字符设备驱动 (chrdev_drv.c):

字符设备的打开和读函数:

static struct cdev chr_dev; // 字符设备抽象
static dev_t ndev;          // 设备号
static int chr_open(struct inode *nd, struct file *filp)
{printk("chr_open, major=%d, minor=%d\n", MAJOR(nd->i_rdev), MINOR(nd->i_rdev));return 0;
}static ssize_t chr_read(struct file *filp, char __user *u, size_t sz, loff_t *off)
{printk("In chr_read()\n");return 0;
}static int chr_release(struct inode *nd, struct file *filp)
{printk("In chr_release()\n");return 0;
}

文件操作函数集:

struct file_operations chr_ops =
{.owner = THIS_MODULE,.open = chr_open,.read = chr_read,.release = chr_release,
};

模块加载和卸载:

static int demo_init(void)
{int ret;cdev_init(&chr_dev, &chr_ops);ret = alloc_chrdev_region(&ndev, 0, 1, "chr_dev");if(ret < 0)return ret;printk("demo_init():major=%d, minor=%d\n", MAJOR(ndev), MINOR(ndev));ret = cdev_add(&chr_dev, ndev, 1);if(ret < 0)return ret;return 0;
}
static void demo_exit(void)
{printk("demo_exit...\n");cdev_del(&chr_dev);unregister_chrdev_region(ndev, 1);
}

2) 访问字符设备的应用程序:

int main()
{int ret;char buf[32];int fd = open("/dev/chr_dev", O_RDONLY|O_NDELAY);if(fd < 0){printf("open file %s failed!\n", CHR_DEV_NAME);return -1;}read(fd, buf, 32);close(fd);return 0;
}

3) 不用完全理解代码的含义,直接看运行效果:

# 编译驱动程序
$ make KERNELDIR=XXX/linux ARCH=arm CROSS_COMPILE=arm-linux-# 编译应用程序
$ arm-linux-gcc chrdev_app.c -o chrdev_app# 加载驱动模块
$ insmod chrdev_drv.ko
demo_init():major=242, minor=0# 手动创建字符设备文件节点
$ mknod /dev/chr_dev c 242 0# 运行应用程序
$ ./chrdev_app
chr_open, major=242, minor=0
In chr_read()
In chr_release()

从上面测运行结果可知,应用程序调用 chrdev_app.c / open() 会导致驱动程序 chrdev_drv.c / struct file_operations chr_ops->open() 被调用,read 操作也是类似。

内核是如何实现上述功能的?

带着这个困惑来了解字符设备驱动的框架,才不会迷失在内核里各种复杂的代码细节里。

3 字符设备在内核里的抽象

3.1 字符设备核心代码概览

在深入阅读各种代码之前,先整体地概览一遍将会涉及到的程序文件,找出核心主干,能有效地避免陷入繁杂的代码细节中。

1) 分解 C source 文件,fs/char_dev.c (Linux-4.14):

作用:
char_dev.c 是字符设备驱动框架的核心实现文件,它位于 fs 目录中,说明了字符设备驱动和文件系统是紧密联系在一起的。

内容(以重要性排序):
1> public 函数:

// 1. 字符设备子系统初始化
void __init chrdev_init(void) // 2. struct cdev 管理相关
void chrdev_show(struct seq_file *f, off_t offset) 
void cdev_put(struct cdev *p) 
void cd_forget(struct inode *inode) 
int cdev_add(struct cdev *p, dev_t dev, unsigned count) 
void cdev_set_parent(struct cdev *p, struct kobject *kobj) 
int cdev_device_add(struct cdev *cdev, struct device *dev) 
void cdev_device_del(struct cdev *cdev, struct device *dev) 
void cdev_del(struct cdev *p) 
struct cdev *cdev_alloc(void) 
void cdev_init(struct cdev *cdev, const struct file_operations *fops)// 3. 设备号管理相关
__register_chrdev_region(unsigned int major, unsigned int baseminor, 
__unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct) 
int register_chrdev_region(dev_t from, unsigned count, const char *name) 
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, 
int __register_chrdev(unsigned int major, unsigned int baseminor, 
void unregister_chrdev_region(dev_t from, unsigned count) 
void __unregister_chrdev(unsigned int major, unsigned int baseminor,
  • 在 Linux 代码中,双下划线(__)开始的函数名表示这是一个低层接口, 应当小心使用。如果你调用这个函数,确信你知道你在做什么。也就是说,阅读代码时,双下划线开头的函数可以暂时放一边。

  • struct cdev 管理相关:cdev_add() / cdev_del() / cdev_init(),需重点关注。

  • 设备号管理相关:alloc_chrdev_region() / register_chrdev_region(),需重点关注。

2> public 变量:

const struct file_operations def_chr_fops = {.open = chrdev_open,.llseek = noop_llseek,
};
  • 类似于高级语言的多态机制,在 Linux 各个子系统的驱动框架中一般用一个统一的 file_operations->open 函数关联到各个具体的硬件驱动的 struct file_operations,需要重点关注。

3> private 变量:

static struct char_device_struct {struct char_device_struct *next;unsigned int major;unsigned int baseminor;int minorct;char name[64];struct cdev *cdev;  /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];static struct kobj_map *cdev_map; 
static struct kobj_type ktype_cdev_default;
static struct kobj_type ktype_cdev_dynamic;
  • struct char_device_struct *chrdevs[] 数组是字符设备驱动框架的核心数据结构,需重点关注。

4> private 函数:

static inline int major_to_index(unsigned major) 
static int find_dynamic_major(void) 
static struct kobject *cdev_get(struct cdev *p) 
static int chrdev_open(struct inode *inode, struct file *filp) 
static void cdev_purge(struct cdev *cdev) 
static struct kobject *exact_match(dev_t dev, int *part, void *data) 
static int exact_lock(dev_t dev, void *data) 
static void cdev_unmap(dev_t dev, unsigned count) 
static void cdev_default_release(struct kobject *kobj) 
static void cdev_dynamic_release(struct kobject *kobj) 
static struct kobject *base_probe(dev_t dev, int *part, void *data)
  • helper 类函数,不太重要。

2) 分解 C header 文件,include/linux/cdev.h (Linux-4.14):

作用:
包含字符设备驱动相关结构体的定义、以及一些字符设备核心 API 的声明。

内容:
1> struct cdev 结构体:

struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;struct list_head list;dev_t dev;unsigned int count;
} __randomize_layout;
  • struct cdev 是字符设备驱动的核心抽象,需重点关注。

3.2 对字符设备进行抽象: struct cdev

编写字符设备驱动程序就是为了管理和控制字符设备,Linux 内核将字符设备抽象为一 个数据结构:struct cdev。这个结构体似乎从 Linux-2.6 到现在 Linux-5.8 都没有发生变化。

1) struct cdev 成员简介:

目前没必要完全理解这些成员的作用,有个大概印象就好:

  • struct kobject kobj: 内嵌的内核对象,与 Linux 设备驱动模型相关,后续需专门写一篇文章来介绍。

  • struct module *owner: 指向拥有这个结构体的模块的指针,这个成员用来在它的操作还在被使用时阻止模块被卸载,一般初始化为 THIS_MODULE;

  • struct file_operations *ops: 在Linux通用文件模型下,字符设备的文件操作函数集,后续详解。

  • struct list_head list:用于管理 struct cdev 的链表,后续需专门写一篇文章来介绍。

  • 字符设备的设备号,由主设备号和次设备号构成,后续需专门写一篇文章来介绍。

2) 两种方式创建 struct cdev 对象:

这里用对象一词,是为了引导大家用面向对象的思维来看待 Linux 内核的设计

面向过程还是面向对象,不应该和语言绑定在一起,应该理解为 2 种不同的编程思维。人脑是很美妙的,在不同的场景,只要你愿意,它就能应用不同的思维方式来解决问题。设计 Linux 内核代码的神牛们,可谓是各个都是面向对象编程的大牛,练习编程就应该练习对事物的抽象能力,C 程序员如果觉得自己缺乏这方面的能力,不如学习一下 Java 编程。

静态定义:

static struct cdev chr_dev;

动态分配:

struct cdev *my_cdev = cdev_alloc();

cdev_alloc() 不仅会为struct 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;
}

一个值得注意的点:
我搜索了一下内核源码,发现大多数驱动都选择静态定义 struct cdev,这样更简单省事。

3) 某个真实字符硬件的抽象:

数据结构 struct cdev 作为字符设备的抽象,仅仅是为了满足 Linux 内核对字符设备驱动程序框架结构设计的需要

现实中,一个具体的字符硬件设备的数据结构的抽象往往要复杂得多,在这种情况下 struct cdev 常常作为一种内嵌的成员变量出现在实际设备的数据结构中。

例如 drvier/watchdog/watchdog_dev.c:

struct watchdog_core_data {struct kref kref;struct cdev cdev;struct watchdog_device *wdd;struct mutex lock;unsigned long last_keepalive;unsigned long last_hw_keepalive;struct delayed_work work;unsigned long status;  /* Internal status bits */
};

4) 初始化 cdev 对象:

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_init() 最重要的作用就是将 struct cdev 对象和 struct file_operations 对象绑定在一起。

一些值得注意的点

  • cdev_init() 和 cdev_alloc() 中有一部分功能是重叠的(例如 kobject_init()),所以 cdev_init() 只能搭配静态定义 struct cdev 的方式来使用。

  • 如果采用 cdev_alloc() 动态分配 struct cdev 对象,则需自行 cdev->ops = fops 。

3.3 对字符设备的操作进行抽象:struct file_operations

struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);[...]int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);[...];} __randomize_layout;
  • 目前没必要完全理解这些成员的作用,有个大概印象就好。

  • 字符设备驱动程序中一个极其关键的数据结构,字符设备驱动程序的编写,就是是围绕着如何实现 struct file_operations 中的那些函数指针成员而展开的。

  • 通过内核文件系统组件在其间的穿针引线,应用程序中对文件类函数(open、read、write)的调用,将最终被转接到 struct file_operations 中对应函数指针的具体实现上,后续需专门写一篇文章来介绍。

鉴于大多数人的注意力无法在一篇文章里上集中太久,更多的内容将放在后面的文章里。建议大家可以先自行阅读相关书籍,不是自己理解到的东西是消化不了的。

4. 更多值得学习的知识点

  • 字符设备号的构成与管理

  • 字符设备的注册

  • 生成字符设备文件的方式有哪些?

  • 创建字符设备文件是发生了什么?

  • 字符设备文件是如何关联到字符设备驱动的?

  • 分析一些真实的字符设备驱动

5. 相关参考

  • 《Linux 内核文档》

  • 《Linux设备驱动程序》(ldd) / 第 3 章节

  • 《深入Linux设备驱动程序内核机制》(ildd) / 第 2 章节

  • 《精通Linux设备驱动程序开发》(eldd) / 第 2 章节

  • 《Linux设备驱动开发详解》(ldds) / 第 6 章节

  • 《深入Linux内核架构》(plka) / 第 6 章节

  • 《嵌入式应用开发完全手册》


#推荐阅读:

    专辑|Linux文章汇总

    专辑|程序人生

    专辑|C语言

嵌入式Linux

微信扫描二维码,关注我的公众号 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/467134.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

旧地重游

光阴飞逝1991年跟随父母搬迁至长沙&#xff0c;至今已有16年。2007年春节舅妈大寿借此机会回到儿时旧地以了多年来的心愿。经过1个小时左右颠簸终于快回到儿时生长的地方&#xff0c;那里的路面年久失修&#xff0c;经过昨天的大雨&#xff0c;已经坑坑洼洼路面到处积水&#x…

我要不要离职?

1#读者提问来到四线城市的小公司&#xff0c;其他员工都是公司主动找到他们转正的&#xff0c;有的一个月有的三个月&#xff0c;而我半年过去了&#xff0c;以为时间到了也跟他们一样自动帮我转正。然而没有&#xff0c;昨天忍不住问了公司&#xff0c;今天就拿转正表来给我填…

搞懂进程组、会话、控制终端关系,才能明白守护进程干嘛的?

守护进程概念&#xff1a;守护进程&#xff0c;也就是通常所说的Daemon进程&#xff0c;是Linux中的后台服务进程。周期性的执行某种任务或等待处理某些发生的事件。Linux系统有很多守护进程&#xff0c;大多数服务都是用守护进程实现的。比如&#xff1a;像我们的tftp&#xf…

关于编译C#文件

使用csc.exe编译非控制台应用程序,常使用/target选项此选项可简写为/t,用来指定要创建的文件类型. 如编译一个类库文件(dll)Class1.cs: namespaceTest...{ /**//// <summary> /// Class1 的摘要说明。 /// </summary> public class Class1 ...{ …

【速来抢】iPhone12、STM32开发板、1024元现金红包…打包免费送!!!

没错&#xff01;华清远见在做1024狂欢节活动今年他们“玩”的有点大参与活动&#xff0c;抽取幸运锦鲤下面21件惊喜大礼&#xff0c;打包全部带回家????参与方式&#xff1a;扫码下方二维码&#xff0c;进活动群获取抽奖链接&#xff0c;参与抽奖????福利2除了万元锦鲤…

1024,第 15 届「中国内核开发者大会」 参会指南(议程全剧透)

各位好&#xff0c;第 15 届「中国内核开发者大会」即将开幕&#xff0c;这些参会指南请提前收藏好&#xff1a;2020「中国内核开发者大会」&#xff08;以下简称 CLK&#xff09;将在 2020 年 10 月 24 日举办&#xff0c;线上线下同步进行&#xff0c;线上由 CSDN 进行全网直…

1024对话内核大神谢宝友

我看了CSDN的采访&#xff0c;感觉比较官方&#xff0c;不知道是不是编辑的原因把一些内容给隐藏了&#xff0c;所以我还是想完整的内容放出来给大家看看&#xff0c;这些问题&#xff0c;可能是很多后辈程序员非常关心的。今天是1024节&#xff0c;这个数字对于写在电脑前写代…

javascript 里Array的一些方法

1. join()方法&#xff1a;join()方法的用途是连接字符串值&#xff0c;join()方法只有一个参数&#xff0c;即数组项之间使用的字符串如&#xff1a;varaColors ["red","green","blue"];alert(aCloors.join("-"));//output "red…

很多人都不清楚HEX文件格式

Intel HEX文件是由一行行符合Intel HEX文件格式的文本所构成的ASCII文本文件。在Intel HEX文件中&#xff0c;每一行包含一个HEX记录。这些记录由对应机器语言码和/或常量数据的十六进制编码数字组成。Intel HEX文件通常用于传输将被存于ROM或者EPROM中的程序和数据。大多数EPR…

如何在asp.net中实现listbox item值上下移动?(转)

全部.net为&#xff1a;1 <form id"myform"runat"server">2 <div>3 <asp:ObjectDataSource ID"ObjectDataSource1"runat"server"OldValuesParameterFormatString"original_{0}"4 …

洛谷1345 [Usaco5.4]奶牛的电信

题目描述 农夫约翰的奶牛们喜欢通过电邮保持联系&#xff0c;于是她们建立了一个奶牛电脑网络&#xff0c;以便互相交流。这些机器用如下的方式发送电邮&#xff1a;如果存在一个由c台电脑组成的序列a1,a2,...,a(c)&#xff0c;且a1与a2相连&#xff0c;a2与a3相连&#xff0c;…

有意思,USB资料分享

前几天&#xff0c;有个同学在微信问我 「发哥&#xff0c;能不能给我分享一些USB的资料吗&#xff1f;」-- 然后就有了今天的文章。我对USB印象很深&#xff0c;源于两个事情第一件事情是&#xff0c;我有一个大学同学&#xff0c;他的名字里面有一个「发」字&#xff0c;我们…

不知道的,还以为是555牌香烟

你认为历史上最成功的芯片是什么&#xff1f;就是出货很多很多那种。Intel的酷睿系列&#xff1f;NO&#xff01;AMD&#xff1f;错&#xff01;别猜了&#xff0c;原来是它&#xff01;这就是电工的神器——555定时器&#xff0c;从诞生到现在&#xff0c;销量过百亿&#xff…

Linux操作寄存器前为什么要ioremap

1. 原因这里只考虑有 MMU 的芯片&#xff0c;Linux 为了实现进程虚拟地址空间&#xff0c;在启用 MMU 后&#xff0c;在内核中操作的都是虚拟地址&#xff0c;内核访问不到物理地址。如果在驱动里直接访问物理地址&#xff0c;等于访问了一个非法地址&#xff0c;会导致内核崩溃…

Loading页的实现代码

用了几种Loading页的代码&#xff0c;还是算这个好用了。真的佩服牛人们的思想。有的说用DIV来实现遮盖后面的东西&#xff0c;当FORM加载完后&#xff0c;执行CLOSE()d的JS函数。我试验了几次&#xff0c;效果不是那么好。也主要是由于我吧东西都放在我本地&#xff0c;感觉不…

做算法,为什么建议你一定要学懂C++?

在工业界&#xff0c;有这样一个规律&#xff1a;“ 但凡能用其他语言的都不会用C&#xff0c;只能用C的必然用C。”但是&#xff0c;C的学习和项目开发都比较困难。一个有经验的老手也经常搞出野指针&#xff0c;内存泄露等bug&#xff0c;包括我自己在学C的时候也非常痛苦。所…

AI一键注释代码、阅读整个项目、转换编程语言。已开源!

获取github源码地址和国内url、key方式&#xff1a;在文章底部 大家好今天给大家介绍一款开源项目&#xff0c;这个项目是由渡码维护的&#xff0c;这个项目支持自己的大模型和使用openai两种方式&#xff0c;本文章介绍使用openai的方式&#xff01; 使用过程中发现两个问题…

为什么别人赚钱比自己容易?

之前的文章提到自己的一个同事&#xff0c;在一家公司干了10年&#xff0c;最近跳槽找了份5万薪水的工作&#xff0c;大家都很想听他的故事。为了方便记录&#xff0c;我叫他文哥文哥比我们一般的毕业生厉害&#xff0c;他比我大将近9岁&#xff0c;我在初中的时候他已经是一个…

Linux下遍历目录下的文件

方法1&#xff1a;http://tag.csdn.net/Article/ef431d9b-68b3-419f-9f85-1fb9279f904a.html//ListFile.cpp#include <stdio.h>#include <dirent.h>#include <sys/types.h>intmain(intargc, char*argv[]){ if (2 ! argc){ printf("Usage:ListFileSource…