【嵌入式开发——Linux操作系统】9驱动管理模块

在系列博客中前言部分就提到过,芯片开发板上器件众多,但驱动器件"去办事",背后是有一套模型的,本质的操作就是给器件读写操作,至于原因大家可以往前翻看。

1 设备类型

Linux中,设备被分为以下三类:

块设备、字符设备、网络设备。

块设备 :通常缩写是blkdev,它是可寻址的,寻址以块为单位,块大小随设备不同而不同,支持重定位(seeking)操作,也就是对数据随机访问。常见块设备有:磁盘,光碟,Flash等存储设备;块设备通过"块设备节点"这样的特殊文件来访问,并通常挂载为文件系统。
字符设备 :通常缩写为cdev,它是不可寻址的,仅提供数据的流式访问,即一个个字符,或一个个字节。常见字符设备有键盘,鼠标,打印机等;字符设备通过"字符设备节点"这样的特殊文件来访问,与块设备不同,应用程序直接访问设备节点与字符设备交互。
网络设备 :通常以以太网(ethernet devices)来称呼,它提供了对网络(如Internet)的访问,这是通过一个物理适配器和一种特定协议(如IP协议)进行的;网络设备打破了Unix的"一切皆文件"的设计原则,其不是通过设备节点来访问,而是通过套接字API这样的特殊接口来访问。

Linux还有一些其他设备类型,但都是针对单个任务而非通用的,一个特例是"杂项设备"(miscellaneous device),通常简写为miscdev,它实际是个简化的字符设备。

还有一些设备驱动不表示物理设备而是虚拟的,仅供访问内核功能,这种称为伪设备(pseudo device),最常见的如内核随机发生器,空设备,零设备,满设备,内存设备等。

设备模型

Linux 2.6内核引入统一设备模型(device model),以提供一个独立的机制专门来表示设备,并描述其在系统中的拓扑结构,形成一个设备树,内核可以沿设备树的叶子方向依次遍历,以保证系统以正确顺序关闭设备电源。

1.1 kobject

设备模型的核心部分就是kobject,描述符如下:

c 复制代码
struct kobject {
const char          *name;
struct list_head     entry;
struct kobject      *parent;
struct kset         *kset;
struct kobj_type    *ktype;
struct sysfs_dirent *sd;
struct kref          kref;
unsigned int         state_initialized:1;
unsigned int         state_in_sysfd:1;
unsigned int         state_add_uevent_sent:1;
unsigned int         state_remove_uevent_sent:1;
unsigned int         uevent_suppress:1;
};

kobject通常是嵌入其他结构中,单独意义不大,如嵌入到字符设备:

c 复制代码
struct cdev {
struct kobject                 kobj;
struct module                 *owner;
const struct file_operations  *ops;
struct list_head               list;
dev_t                          dev;
unsigned int                   count;
};

当kobject被嵌入到其他结构时,该结构便有了kobject提供的标准功能。更重要的一点是,嵌入kobject的结构体可以成为设备对象层次架构中的一部分,比如cdev结构体可以通过其父指针cdev->kobj.parent和链表cdev->kobj.entry插入到对象层次结构中。

1.2 ktype

kobject对象被关联到一种特殊的类型,即ktype,ktype由kobj_type结构体表示:

c 复制代码
struct kobj_type {
void (*release)(struct kobject *);
const struct sysfs_ops   *sysfs_ops;
struct attribute        **default_attrs;
};

ktype的存在是为了描述一族kobject所具有的的普遍特性,如此一来,不再需要对每个kobject都分别定义自己的特性,而是将这些普遍特性在ktype结构中一次定义。

release指针指向kobject引用计数减至零时要被调用的析构函数,负责释放kobject所用的内存和其他相关清理工作。

sysfs_ops描述了sysfs文件读写时的特性。

default_attrs指向一个attribute结构体数组,里面定义了该kobject相关的默认属性。

1.3 kset

是kobject对象的集合体,把它看成一个容器,可将所有相关的kobject对象,如全部块设备,置于同一位置。kset描述符如下:

c 复制代码
struct kset {
struct list_head         list;
spinlock_t               list_lock;
struct kobject           kobj;
struct kset_uevent_ops  *uevent_ops;
};

list连接该集合kset中所有的kobject对象,list_lock是保护这个链表中元素的自旋锁,kobj指向kobject对象代表了该集合的基类。uevent_ops指向一个结构体,用于处理kobject对象的热插拔操作。uevent就是用户事件(user event)的缩写,提供了用户空间热插拔信息进行通信的机制。

1.4 kobject,ktype和kset的相互关系

kobject引入诸如引用计数,父子关系等基本对象道具,并且以统一方式提供这些功能,不过它自身意义不大,需要嵌入其他数据结构中;ktype定义了一些kobject相关的默认属性:析构行为,sysf行为等;kobjec又归入kset中,将每个kobject链表起来,具有相同ktype的kobject可以被分组到不同的kset,在Linux中,只有少数的ktype,但有多个kset。

kref

引用计数kref是kobject的一个重要功能。初始化后,kobject的引用计数置为1.只要引用计数不为0,该对象就会继续保留在内存中。任何包含对象引用的代码首先要增加对该对象的引用计数,当代码结束后减少它的引用计数。

kref描述如下:

c 复制代码
struct kref {
atomic_t refcount;
};

其中唯一的字段是用来存放原子变量。在使用kref前,必须通过kref_init()来初始化:

c 复制代码
void kref_init(struct kref *kref)
{
    atomic_set(&kref->refcount, 1);
}

(1)增加引用计数:
struct kobject* kobject_get(struct kobject *kobj); ---> 进一步调用kref_get()

(2)减少引用计数:
void kobject_put(struct kobject *kobj); ---> 进一步调用kref_put()

(3)sysfs

sysfs文件系统是一个处于内存中的虚拟文件系统,它提供kobject对象层次结构的视图,帮助用户能以一个简单的文件系统方式来观察各种设备的拓扑结构。

(4)添加一个kobject:

仅仅初始化kobject不能将其自动导入到sysfs中,若把kobject导入sysfs中,需调用:
int kobject_add(struct kobject* kobj, struct kobject *parent, const char *fmt, ...);

如果parent指针被设置,则该kobject在sysfs中被映射到父目录下的子目录;如果parent指针为空,则kobject被映射为kset->kobj的子目录;如果parent和kset都没被设置,则认为kobject没有父对象,会被映射为sysfs下的根目录。

辅助函数kobject_create_and_add把kobject_create()和kobject_add()所做的工作放在一个函数中:
struct kobject* kobject_create_and_add(const char *name, struct kobject *parent);

(5)删除一个kobject:

从sysfs中删除一个kobject对应的文件目录,需调用:void kobject_del(struct kobject *kobj);

2 模块

模块(module)化机制是Linux一大创新,有以下优点:

1.在系统运行动态加载模块,扩充内核功能,不需要时可以卸载。

2.修改内核功能,不必重新全部编译整改内核,只需编译相应模块即可。

3.模块目标代码一旦被加载重定位到内核,其作用域和静态链接的代码完全等价。

module_init()是一个宏调用,里面的唯一参数是模块初始化参数,该初始化参数必须符合下面形式:

int my_init(void); ---> 一般初始化函数用于初始化硬件,分配数据结构等。如果被静态编译到内核映像,将会被内核启动时运行。

module_exit()是一个宏调用,里面是模块的出口函数,其退出函数必须符合以下形式:

void my_exit(); ---> 可能会负责清理资源,以保证硬件处于一致状态。

模块代码有两种运行方式:

1.静态编译链接进内核,在系统启动过程中进行初始化;

2.编译成动态可加载的module,并通过insmod来动态加载,接着初始化。

有些模块是必须要编译进内核的,和内核一起运行,从不卸载,如vfs,platform_bus等。

静态链接和初始化:

Make menuconfig时选择 将模块编译进内核 即为静态编译,或直接在Makefile文件中指定为

obj-y += hello.o

编译后会被链接到section(.initcall6.init),并在内核启动时,被kernel_init()调用。

动态链接和初始化:

Make menuconfig时选择 将模块编译成模块 即为动态编译,或直接在Makefile文件中指定为

obj-m += hello.o

有个类型为module的全局变量__this_module,其成员init就是init_module,也就是hello_init,并被链接到section(".gnu.linkonce.this_module").

动态加载需要用到insmod这个用户层命令,代码会移到内核指定位置中,init函数也会被调用

3 块设备

块设备的管理是个艺术活,有必要单独讲下,其背后包含如何处理延时写的思想;在嵌入式中实时处理是很关键的,而块设备一般访问速率比较慢,单次读写数据量很可能也比较大,所以如何处理块设备的读写是个很重要的事情。

3.1 块I/O层

系统中能随机(不需要按顺序)访问固定大小的数据片(chunks)的硬件设别称为块设备,这些固定大小的数据片就称为块,常见的块设备有磁盘,闪存等。

块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,最常见的是512字节。扇区的大小是设备的物理属性,而因为各种软件用途不同,它们会有自己的最小逻辑可寻址单元------块。块是文件系统的一种抽象------只能基于块访问文件系统。块不能比扇区还小,只能数倍于扇区大小,且不能超过一个页的大小。通常块大小是512B,1KB,4KB等。

3.2 缓冲区和缓冲区头

当一个块调入内存时,它要存储在一个缓冲区中。每个缓冲区与一个块对应,它相当于是磁盘块在内存中的映射。缓冲区描述符如下:

c 复制代码
struct buffer_head {
unsigned long         b_state;            /* 缓冲区状态标志 */
struct buffer_head   *b_this_page;        /* 页面中的缓冲区 */
struct page          *b_page;             /* 存储缓冲区的页面 */
sector_t              b_blocknr;          /* 起始块号 */
size_t                b_size;             /* 映像中大小 */
char                 *b_data;             /* 页面内的数据指针 */
struct block_device  *b_bdev;             /* 相关联的块设备 */
bh_end_io_t          *b_end_io;           /* I/O完成方法 */
void                 *b_private;          /* io完成方法 */
struct list_head      b_assoc_buffers;    /* 相关的映射链表 */
struct address_space *b_assoc_map;        /* 相关的地址空间 */
atomic_t              b_count;            /* 缓冲区使用计数 */
};

目前内核中块I/O操作的基本容器由bio结构体表示,

c 复制代码
struct bio {
sector_t            bi_sector;           /* 磁盘上相关的扇区 */
struct bio         *bi_next;             /* 请求链表 */
struct block_device  *bi_bdev;           /* 相关的块设备 */
unsigned long       bi_flags;            /* 状态和命令标志 */
unsigned long       bi_rw;               /* 读还是写 */
unsigned short      bi_vcnt;             /* bio_vecs偏移个数 */
unsigned short      bi_idx;              /* bio_io_vect的当前索引 */
unsigned short      bi_phys_segments;    /* 结合后的片段数目 */
unsigned int        bi_size;             /* I/O计数 */
unsigned int        bi_seg_front_size;   /* 第一个可合并的段大小 */
unsigned int        bi_seg_back_size;    /* 最后一个可合并的段大小 */
unsigned int        bi_max_vecs;         /* bio_vecs数目上限 */
unsigned int        bi_comp_cpu;         /* 结束CPU */
atomic_t            bi_cnt;              /* 使用计数 */
struct bio_vec     *bi_io_vec;           /* bio_vecs链表 */
bio_end_io_t       *bi_end_io;           /* I/O完成方法 */
void               *bi_private;          /* 拥有者的私有方法 */
bio_destructor_t   *bi_destructor;       /* 撤销方法 */
struct bio_vec      bi_inline_vecs[0];   /* 内嵌bio向量 */
};

使用bio结构体的目的主要是代表正在现场执行的I/O操作,该结构体的主要域都是用来管理相关信息的。

3.3 I/O调度程序

块设备将挂起的块I/O请求保存在请求队列中,该队列由request_queue结构体表示。如果简单地以内核产生请求次序直接请求块设备的话,性能会很差,因为磁盘寻址是整个计算机中最慢的操作之一,每一次寻址(定位磁盘头到指定块位置)需要花费不少时间。

为了优化寻址操作,内核会先执行合并与排序的预操作。
Linus电梯

Linus电梯能执行合并与排序的预处理,当有新的请求加入队列时,它会检查其他每个挂起的请求是否可以合并。
最终期限(deadline)I/O调度程序

为了解决Linus电梯所带来的的饥饿问题。因为为了减少磁盘寻址时间,对某个磁盘区域上操作比较频繁,这会导致其他位置的操作得不到运行机会。在最后期限I/O调度程序中,每个请求都有一个超时时间,默认读请求超时时间是500ms,写请求超时时间是5s。

其他还有预测I/O调度,完全公正的排队I/O调度等。

3.4 页高速缓存和页回写

页高速缓存(cache)是Linux内核实现磁盘缓存,主要用来减少对磁盘的I/O操作。具体来讲,是把对磁盘的数据写到物理内存中,把对磁盘的访问变成对物理内存的访问

缓存手段

页高速缓存是由内存中的物理页组成的,其内容对应磁盘上的物理块,页高速缓存大小能动态调整。当内核开始读操作,它首先会检查需要的数据是否在页高速缓存中,如果在,就放弃访问磁盘,直接从内存中读取数据,这个行为称作缓存命中。如果缓存中没有相应数据,即未命中,则内核必须调度块I/O操作从磁盘中读取数据。

写缓存

写缓存一般有如下三种策略:

1.不缓存(nowrite),即绕过缓存,直接把数据写进磁盘,并且回头使缓存中的数据失效,后续有读操作的话,必须从磁盘中读取出来;该策略使用很少,因为不但不去写缓存,还额外费力使缓存数据失效。

2.写透缓存(write-through),写操作更新缓存,同时也更新磁盘文件,写操作会立即穿透缓存到磁盘中;好处是能保证缓存和磁盘数据同步,且不用使缓存失效;

3.回写缓存(write-back),也是Linux采用的,写操作直接写到缓存中,但不会立即更新磁盘,而是将页高速缓存被写入的页面标记为"脏",并加入到脏页表中,然后由专门进程(回写进程)周期性地将脏页写回磁盘中,写完后清理"脏"标记。这里的"脏"更好描述是"未同步"。

缓存回收

1.最近最少使用LRU

LRU需要跟踪每个页面的访问踪迹,以便能回收最老时间的页面,该策略的良好效果来源于缓存的数据越久未被访问,则越不大可能近期再被访问。

2.双链策略

Linux的双链策略,维护的不再是一个LRU链表,而是两个链表:活跃链表和非活跃链表。活跃链表上的页面被认为不会被换出,非活跃链表上的页面可以被换出。双链策略中:

每个页面有2个标记位:

PG_active - 标记页面是否活跃; PG_referenced - 表示页面是否被进程访问到。

页面移动流程如下:

  • a.当页面首次被访问时,PG_active置1,加入到活动链表;
  • b.当页面再次被访问时,PG_referenced置1,如果此时页面在非活动链表中,则将其移到活跃链表,并将PG_active置1,PG_referenced置0;
  • c.系统中的daemon会定时扫描活跃链表,定时将PG_referenced位置0;
  • d.系统中daemon定时检查页面PG_referenced,如果PG_referenced=0,则此页面的PG_active置0,同时移动到非活跃链表。
相关推荐
7yewh1 分钟前
嵌入式硬件电子电路设计(五)MOS管详解(NMOS、PMOS、三极管跟mos管的区别)
stm32·嵌入式硬件·mcu·物联网·硬件架构·硬件工程·pcb工艺
ParanoidRoye20 分钟前
Linux设置socks代理
linux·运维·服务器
Java Fans25 分钟前
离线安装GDAL与MapServer:在银河麒麟V10上的快速指南
linux
Peter_chq29 分钟前
【计算机网络】网络框架
linux·c语言·开发语言·网络·c++·后端·网络协议
可乐鸡翅好好吃1 小时前
关于stm32中IO映射的一些问题
stm32·单片机·嵌入式硬件
极客星辰1 小时前
Linux redis-6.2.6安装
linux·运维·redis
Chris-zz1 小时前
Linux:进程间通信
linux·运维·服务器·c++
屋外雨大,惊蛰出没2 小时前
安装双系统(linux操作系统(debian)安装)
linux·运维·debian
PenguinLetsGo2 小时前
关于 Android15 GKI2407R40 导致梆梆加固软件崩溃
android·linux
NightReader2 小时前
Ubuntu 22.04使用nitrogen设置双屏显示
linux·运维·ubuntu