Linux Device Driver 3rd 下

第十一章 内核的数据类型

坚持使用严格的数据类型,并且使用-Wall -Wstrict-prototypes选项编译可以防止大多数的代码缺陷

内核使用的数据类型主要分为三大类:

  • 标准C语言类型,类似int
  • 类似u32这样有确定大小的类型
  • 类似pid_t这样用于特定内核对象的类型

使用标准C语言类型

在不同的体系架构上,普通C语言的数据类型所占空间的大小并不相同。

Linux系统中,指针和long整型的大小总是相同的。

为数据项分配确定的空间大小

有时内核代码需要特定大小的数据项,多半是用来匹配预定义的二进制结构或者和用户口空间进行通讯或者通过在结构体中插入"填白 padding"字段来对齐数据。

当需要知道自己的数据大小时,内核提供了下列数据类型,定义在

  • u8 无符号字节8位
  • u16 无符号字 16位
  • u32 无符号32位
  • u64 无符号64位

相应的有符号类型也存在,只需将名字中的u用s替换就可以了。

接口特定的类型

内核中最常用的数据类型由typedef声明,这样可以防止出现任何移植性问题。

当需要打印一些接口特定的数据类型时,最行之有效的方法就是将其强制转换成可能的最大类型(通常是long或者unsigned long),然后用相应格式。

因为格式和类型相匹配,而且也不会丢失数据位,所以这种做法不会产生错误或者警告。

其他有关移植性的问题

  • 通用原则:

    避免使用显式的常量值,代码通过使用预处理的宏使之参数化。

  • 时间间隔:

    使用jiffies计算时间间隔的时候,应该用HZ(每秒定时器中断的次数)来衡量。

  • 页大小:

    内存页的大小为PAGE_SIZE字节。

  • 字节序:

    u32 cpu_to_le32(u32) u32 le32_to_cpu(u32) 这两个宏将一个CPU使用的值转换成一个无符号的32位小头数值,或者相反。不管CPU是大头还是小头,也不管CPU是否是一个32位处理器,这两个方法都能正常工作。

  • 数据对齐:

    为了编写可以在不同平台之间可移植的数据项的数据结构,应该始终强制数据项的自然对齐。

    • 自然对齐(natural alignment)是指在数据项大小的整数倍(例如 8字节数据项存入8的整数倍的地址)的地址处存储数据项。
  • 链表:

    内核建立了一套标准的循环、双向链表的实现,用于操作系统内核经常需要维护数据结构的列表。链表操作函数定义于,后续使用时再深入研究。

第十二章 PCI驱动程序

待补充

第十三章 USB驱动程序

待补充

第十四章 Linux设备模型

内核提供了统一的设备模型,并且使用该抽象模型支持了多种不同的任务,包括:

  • 电源管理和系统关机

    完成这些工作需要一些对系统结构的理解,比如一个USB宿主适配器,在处理完所有与其连接的设备前是不能被关闭的。设备模型使得操作系统能够以正确的顺序遍历系统硬件

  • 与用户空间通信

    sysfs虚拟文件系统的实现与设备模型密切相关,并且向外界展示了它所表述的结构。向用户空间提供所提供的系统信息,以及改变操作参数的接口,将越来越多地通过sysfs实现,也就是说,通过设备模型实现。

  • 热插拔设备

    越来越多的计算机设备可被动态的热插拔了,也就是说,外围设备可根据用户的需要安装与卸载。内核的热插拔机制可以处理热插拔设备,特别是能够与用户空间进行关于插拔设备的通信,而这种机制也是通过设备模型管理的。

  • 设备类型

    系统中的许多部分对设备如何连接的信息并不感兴趣,但是它们需要知道哪些类型的设备是可以使用的。设备模型包括了将设备分类的
    机制,它会在更高的功能层上描述这些设备,并使得这些设备对用户空间可见。

  • 对象生命周期

    设备模型的实现需要创建一系列机制以处理对象的生命周期、对象之间的关系,以及这些对象在用户空间中的表示。

kobject、kset和子系统

kobject结构所能处理的任务以及它所支持的代码包括:

  • 对象的引用计数

    当一个内核对象被创建时,跟踪此对象生命周期的一个方法是使用引用计数,当内核中没有代码持有该对象的引用时,该对象将结束自己的有效生命周期,并且可以被删除。

  • sysfs表述

    在sysfs中显示的每一个对象,都对应一个kobject,它被用来与内核交互并创建它的可见表述

  • 数据结构关联

    从整体上看,设备模型是一个友好而复杂的数据结构,通过在其间的大量连接而构成一个多层次的体系结构,kobject实现了该结构并把它们聚合在一起。

  • 热插拔事件处理

    当系统中的硬件被热插拔时,在kobject子系统控制下,将产生事件以通知用户空间。

kobject基础知识

kobject是一种数据结构,定于与

嵌入的kobject

内核代码很少去创建一个单独的kobject对象,相反,kobject用于控制对大型域相关对象的访问,kobject会被嵌入到其它结构中。在C语言汇总不允许直接描述继承关系,因此使用了诸如在一个结构中嵌入另一个结构的技术。

kobject的初始化

  • 将整个kobject设置为0
  • 调用 void kobject_init(struct kobject *kobj),这个方法设置kobject的引用计数为1
  • 调用 int kobject_set_name(struct kobject *kobj, const char *format, ...) 设置kobject的名字。

对引用计数的操作

struct kobject *kobject_get(struct kobject *kobj): 该函数将增加kobject的引用计数,并返回指向kobject的指针。

void kobject_put(struct kobject *kobj): 当引用被释放时调用kobject_put减少引用计数

kobject层次结构、kset和子系统

内核用kobject结构将各个对象连接起来组成一个分层的结构体系,从而与模型化的子系统相匹配,有两种独立的机制用于连接:parent指针和kset

kobject结构的parent成员中,保存了另一个kobject结构的指针,这个结构表示了分层结构中上一层的节点。

例如:一个kobject结构表示了一个USB设备,它的parent指针可能指向了表示USB集线器的对象,而USB设备是插在USB集线器上的。
parent指针最重要的用途是在sysfs分层结构中定位对象。

kset

kset是嵌入相同类型结构的kobject集合,kset的主要功能是包容,可以认为它是kobject的顶层容器类。

kset总是在sysfs中出现,一旦设置了kset并把它添加到系统中,将在sysfs中创建一个目录,kobject不必在sysfs中表示,但是kset中的每一个kobject成员都将在sysfs中得到表述。

创建一个对象时,通常要把一个kobject添加到kset中去,主要过程包括:

  • kobjectkset成员指向目的kset
  • 调用int kobject_add(struct kobject *kobj) 添加kobject

kset 在一个标准的内核链表中保存了它的子节点,在大多数情况下,所包含的kobject会在它们的parent成员中保存kset(严格地说是其内嵌的kobject)的指针.

子系统

子系统是对整个内核中的一些高级部分的表述。
子系统通常显示在sysfs分层结构中的顶层。
内核中的子系统包括block_subsys(对块设备来说是/sys/block)、device_subsys(/sys/devices,设备分层结构的核心)以及内核所知晓的用于各种总线的特定子系统。

低层sysfs操作

kobject是隐藏在sysfs虚拟文件系统后的机制,对于sysfs中的每个目录,内核中都会存在一个对应的kobject,每一个kobject都输出一个或者多个属性,它们在kobjectsysfs目录中表现为文件,其中的内容由内核生成。
中包含了sysfs的工作代码,其中为了理解如何创建sysfs的入口,需要了解如下知识:

  • kobjectsysfs中的入口始终是一个目录,因此对kobject_add的调用将在sysfs中创建一个目录,通常这个目录包含一个或者多个属性。

  • 分配给kobject(使用kobject_set_name函数)的名字是sysfs中的目录名,这样处于sysfs分层结构相同的部分中的kobject必须有唯一的名字,该名字必须是合法的文件名:不能包含反斜杠,并且不要使用空格

  • sysfs入口在目录中的位置对应于kobjectparent指针,如果调用kobject_add的时候,parent是NULL,它将被设置为嵌入到新kobjectkset中的kobject,这样sysfs分层结构通常与kset创建的内部结构相匹配。如果parentkset都是NULL,则会在最高层创建sysfs目录

默认属性

当创建kobject的时候,都会给每个kobject一系列默认属性,这些属性保存在kojb_type结构中:

struct kobj_type {
  void (*release)(struct kobject *);
  struct sysfs_ops *sysfs_ops;
  struct attribute **default_attrs;
};

default_attrs成员保存了属性列表,用于创建该类型的每一个kobjectsysfs_ops提供了实现这些属性的方法。

struct attribute {
  char *name;	//属性名字,在kobject的sysfs目录中显示
  struct moudle *owner;	//指向模块的指针,该模块负责实现这些属性
  ode_t mode;	//属性的保护位
};

kobj_type->sysfs_ops成员: 属性具体的实现

struct sysfs_ops {
  ssize_t (*show)(struct kobject *kobj, struct attribute *attr, char *buffer);
  ssize_t (*store)(struct kobject *kobj, struct attribute *attr, const char *buffer, size_t size);
};

当用户空间读取一个属性时,内核会使用指向kobject的指针和正确的属性结构来调用show方法。

当用户空间写入一个属性时,调用store方法。

非默认属性

如果需要在kobject的sysfs目录中添加新的属性,只需要填写一个attribute结构,并把它传递给下面的函数:

int sysfs_create_file(struct kobject *kobj, struct attribute *attr);

删除属性:

int sysfs_remove_file(struct kobject *kobj, struct attribute *attr);
符号链接

sysfs文件系统具有常用的树形结构,以反映kobject之间的组织层次关系。通常内核中各对象之间的关系比较复杂,比如一个sysfs的子树(/sys/devices)表示了所有系统知晓的设备。而其他的子树(在/sys/bus下)表示了设备的驱动程序。但是这些树并不能表示驱动程序及其管理的设备之间的关系,为了表示这种关系,还需要其他的指针,在sysfs中,通过符号链接实现了这个目的。

创建符号链接:
int sysfs_create_link(struct kobject *kobj, struct kobject *target, char *name);
删除符号链接:
void sysfs_remove_link(struct kobject *kobj, char *name);
热插拔事件的产生:

一个热插拔事件是从内核空间发送到用户空间的通知,它表明系统配置出现了变化,无论kobject被创建还是被删除,都会产生这种事件。当把kobject传递给kobject_add或者kobject_del时,会产生这些事件。

热插拔操作:

对热插拔事件的实际控制,是由保存在kset_hotplug_ops结构中的函数完成的。

struct kset_hotplug_ops {
  int (*filter)(struct kset *kset, struct kobject *kobj);
  char *(name)(struct kset *kset, struct kobject *kobj);
  int (*hotplug)(...);
};

如果在kset中不包含一个指定的kobject,内核将在分层结构中进行搜索(通过parent指针),直到找到一个包含有ksetkobject为止,然后使用这个kset的热插拔操作。

无论何时,当内核要为指定的kobject产生事件时,都要调用filter函数,如果该方法返回0,将不产生事件。所以这样就让kset自行决定是否向用户空间传递特定的事件。

总线、设备和驱动程序

总线

总线是处理器与一个或者多个设备之间的通道。在设备模型中,所有的设备都通过总线相连。
Linux设备模型中,用bus_type结构表示总线,它的定义包含在中,其结构如下:

struct bus_type {
  char *name;	//总线名字
  struct subsystem subsys; //子系统
  struct kset drivers;	//总线的驱动程序
  struct kset devices;	//插入总线的所有设备
  int (*match)(struct device *dev, struct device_driver *drv);
  struct device *(*add)(struct device *parent, char *bus_id);
  int (*hotplug)(...);
  ...
};

总线的注册

填充总线的名字和必要方法后调用bus_register进行注册。注册成功后,可以在sysfs/sys/bus目录下看到它。

删除总线

void bus_unregister(struct bus_type *bus);

总线方法

添加新设备或驱动

当一个总线上的新设备或者新驱动程序被添加时,会一次或者多次调用下面函数。

int (*match)(struct device *dev, struct device_driver *drv);
添加环境变量

在为用户空间产生热插拔事件前,下面这个方法允许总线添加环境变量。

int (*hotplug)(...);
对设备和驱动程序的迭代

如果需要对注册到总线的所有设备或者驱动程序执行某些操作时,可以使用以下方法进行迭代。

int bus_for_each_dev(struct bus_type *bus, ..., int (*fn)(struct device *, void *));
对于驱动程序的迭代操作
int bus_for_each_drv(struct bus_type *bus, ..., int (*fn)(struct device_driver *, void *));

注意: fn(…)就是对注册到总线的所有设备需要进行的特定操作,这两个函数在工作期间,都会拥有总线子系统的读取者/写入者信号量,所以同时使用这两个函数会发生死锁。

总线属性

几乎在Linux设备模型的每一层都提供了添加属性的函数,总线层也不例外。

创建属于总线的任何属性
int bus_create_file(struct bus_type *bus, struct bus_attribute *attr);
删除属于总线的属性:
void bus_remove_file(struct bus_type *bus, struct bus_attribute *attr);

设备

在最底层,Linux系统中的每一个设备都用device结构的一个实例来表示:

struct device {
  struct device *parent; //设备的父设备--指的是该设备所属的设备。在大多数情况下,一个父设备通常是某种总线或者
                         //是宿主控制器,如果parent是NULL,表示该设备是顶层设备。
  struct kobject kobj;	//表示该设备并把它连接到结构体系中的kobject.
  char bus_id[BUS_ID_SIZE];	//在总线上唯一标识该设备的字符串
  struct bus_type *bus;	//标识了该设备连接在何种类型的总线上
  struct device_driver *driver;	//管理该设备的驱动程序。
  void *driver_data;	//有设备驱动程序使用的私有数据成员
  void (*release)(struct device *dev);//当指向设备的最后一个引用被删除时,内核调用该方法
  ...
};

注意: 在注册device结构前,至少要设置parent、bus_id、bus和release成员

设备注册
int device_register(struct device *dev);	//注册
void device_unregister(struct device *dev);	//注销
设备属性
int device_create_file(struct device *device, struct device_attribute *entry);	//添加属性
void device_remove_file(struct device *dev, struct device_attribute *attr);	//删除属性
设备结构的嵌入

device结构中包含了设备模型核心用来模拟系统的信息,然而,大多数子系统记录了它们所拥有设备的其他信息,因此,单纯用device结构表示的设备是很少见的,而是通常把类似 kobject这样的结构内嵌在设备的高层表示之中。

设备驱动程序

设备模型跟踪所有系统所知道的设备,进行跟踪的主要目的是让驱动程序核心协调驱动程序与新设备之间的关系。

驱动程序结构定义
struct device_driver {
  char *name;	//驱动程序名字
  struct bus_type *bus;	//该驱动程序所操作测总线类型
  truct kobject kobj;	//必须的kobject
  struct list_head devices;	//当前驱动程序能操作的设备链表
  int (*probe)(struct device *dev);	//查询特定设备是否存在的函数
  int (*remove)(struct device *dev);	//从系统中删除该设备
  void (*shudown)(struct device *dev);	//关机的时候调用shutdown函数关闭设备
  ...
};
注册函数
int driver_register(struct device_driver *drv);
释放函数
void driver_unregister(struct device_driver *drv);
驱动程序结构的嵌入

对于大多数驱动程序核心结构来说, device_driver 结构通常被包含在高层和总线相关的结构中。

类是一个设备的高层视图,它抽象出了底层的实现细节

几乎所有的类都显示在/sys/class目录中。

例如:所有的网络接口都集中在/sys/class/net下,输入设备集中在/sys/class/input下,串行设备都集中在/sys/class/tty中,其中块设备比较特殊,它集中在/sys/block下。在许多情况下,类子系统是向用户空间导出信息的最好方法。

完整的类接口
管理类

类结构

struct class {
  char *name;	//类名字
  struct class_attribute *class_attrs;	//属性
  struct class_device_attribute *class_dev_attrs;	//每个设备的一组默认属性
  int (*hotplug)(...);	//当热插拔事件发生时,使用该方法添加环境变量
  void (*release)(struct class_device *dev);	//从类中删除设备
  void (*class_release)(struct class *class);	//释放类本身
  ...
};

类操作

int class_register(struct class *cls);	//注册
void class_unregister(strut class *cls);	//释放

类设备
类存在的真正目的是,给作为类成员的各个设备提供一个容器,这里使用 class_device 结构表示类的成员。

类接口
设备加入或者离开类时获得信息的触发机制。

各环节的整合

添加一个设备

以PCI为例进行分析:

  • PCI子系统声明了一个bus_type结构,在将PCI子系统装载到内核中时,通过调用bus_register,该bus_type变量将向驱动程序核心注册,此后驱动程序将在/sys/bus/pci中创建一个sysfs目录,其中包含了两个目录:devicesdrivers

  • 所有的PCI驱动程序都必须定义一个pci_driver结构变量,在该变量包含了这个PCI驱动程序所提供的不同功能函数,这个结构中包含了一个device_driver结构,在注册PCI驱动程序时,这个结构将被初始化:

    drv->driver.name = drv->name;
    drv->driver.bus = &pci_bus_type;
    drv->driver.probe = pci_device_probe;
    drv->driver.remove = pci_device_remove;
    drv->driver.kobj.ktype = &pci_drvier_kobj_type;
    

    上面这段代码用来为驱动程序设置总线,它将驱动程序的总线指向pci_bus_type,并且将proberemove函数指向pci核心中的相应函数,为了让pci驱动程序中的属性能正常工作,程序的kobjectktype设置成pci_driver_kobj_type,然后PCI核心向驱动程序核心注册PCI驱动程序:error = driver_register(&drv->driver); //向核心注册,现在驱动程序可与其所支持的任何PCI设备绑定.
    在能与PCI总线交互的特定体系架构代码的帮助下,PCI核心开始探测PCI地址空间,查找所有的PCI设备.当一个PCI设备被找到时,PCI核心在内存中创建一个pci_dev类型的结构变量.

    struct pci_dev {
    ...
    unsigned int devfn;
    unsigned short vendor;
    unsigned short device;
    unsigned short subsystem_vendor;
    unsigned short subsystem_device;
    unsigned int class;
    ...
    struct pci_driver *driver;
    ...
    struct device dev;
    ...
    };
    

    这个PCI设备中与总线相关的成员将由PCI核心初始化(devfn/vendor/device以及其它成员),并且device结构变量的parent变量被设置为该PCI设备所在的总线设备.bus变量被设置指向pci_bus_type结构,接着设置namebus_id变量,其值取决于从PCI设备中读取的名字和ID.

  • 当PCI的device结构被初始化后,使用device_register(&dev->dev) 向驱动程序核心注册设备在device_register 函数中,驱动程序核心对device中的许多成员进行初始化,向核心注册设备的kobject(这里将产生一个热插拔事件),然后将该设备添加到设备列表中,该设备列表为包含该设备的父节点所拥有.完成这些工作后,所有的设备都可以通过正确的顺序访问,并且知道每个设备都挂在层次结构的哪一点上.

  • 接着设备将被添加到总线相关的所有设备链表中,这个链表包含了所有向总线注册的设备,遍历这个链表,并且为每个驱动程序调用该总线的match函数,同时指定该设备.这里对于pci_bus_type总线来说,PCI核心在把设备提交给驱动程序核心前,将match函数指向pci_bus_match函数.pci_bus_match函数将把驱动程序核心传递给它的device结构转换为pci_dev结构.它还把device_driver结构转换为pci_driver结构,并且查看设备和驱动程序中的PCI设备相关信息,以确定驱动程序是否能支持这类设
    备,如果这样的匹配工作没能正确执行,该函数会向驱动程序核心返回0,接着驱动程序核心考虑再起链表中的下一个驱动程序.

  • 如果匹配工作完成,函数向驱动程序核心返回1,这将导致驱动程序核心将device结构中的driver指针指向这个驱动程序,然后调用device_driver结构中指定的probe函数.

    在PCI驱动程序向驱动程序核心注册前,probe变量被设置为指向pci_device_probe函数.该函数将device结构转换为pci_dev结构,并且把在device中设置的driver结构转换为pci_driver结构.它也将检测这个驱动程序的状态,以确保其能支持这个设备,增加设备的引用计数,然后用绑定的pci_dev结构指针为参数,调用PCI驱动程序的probe函数.

  • 如果PCI驱动程序的probe函数判定不能处理这个设备,其将返回负的错误值给驱动程序核心,这将导致驱动程序核心继续在驱动程序列表中搜索,已匹配这个设备.如果probe函数探测到了设备,为了能正常操作设备,它将做所有的初始化工作,然后向驱动程序核心返回0.这会使驱动程序核心将该设备添加到于此驱动程序绑定的设备链表中,并且在sysfs中的drivers目录到当前控制设备之间建立符号链接.这个符号链接显示了驱动程序与设备的链接情况.

删除设备:

相对于添加设备,PCI核心对删除设备做了很少的工作

  • 当删除一个PCI设备时,要调用pci_remove_bus_device函数,该函数做一些PCI相关的清理工作,然后使用指向pci_dev中的device结构的指针,调用device_unregister函数.

  • device_unregister函数中,驱动程序核心只是删除了从绑定设备的驱动程序到sysfs文件的符号链接,从内部设备链表中删除了该设备,并且以device结构中的kobject结构指针为参数,调用kobject_del函数.该函数引起了用户空间的hotplug调用,表明kobject现在从系统中删除,然后删除全部与kobject相关的sysfs文件和sysfs目录,而这些目录和文件都是kobject以前创建的.

  • kobject_del函数还删除了设备的kobject引用,如果该引用是最后一个就要调用PCI设备的release函数–pci_release_dev.该函数只是释放了pci_dev 结构所占用的空间.

通过以上的一系列操作,PCI设备被完全从系统中删除.

添加驱动程序:

当调用pci_register_drvier函数时,一个PCI驱动程序被添加到PCI核心中,该函数初始化了包含pci_drvier结构中的device_driver结构.PCI核心用包含在pci_driver结构中的device_drvier结构指针作为参数,在驱动程序核心内调用driver_register函数.

driver_register 函数初始化了几个devce_driver中的锁,然后调用bus_add_driver函数,该函数按以下步骤操作:

  • 查找与驱动程序相关的总线,如果没有找到该总线,函数立刻返回
  • 根据驱动程序的名字以及相关的总线,创建驱动程序的sysfs目录.
  • 获取总线内部的锁,接着遍历所有向总线注册的设备,然后如同添加新设备一样,为这些设备调用match函数.如果成功,便开始绑定过程的剩余步骤.

删除驱动程序:

对于PCI驱动程序,驱动程序调用pci_unregister_driver函数,该函数只是用包含在pci_driver结构中的device_driver结构作为参数,调用驱动程序核心函数driver_unregister

driver_unregister函数通过清除在sysfs树中属于驱动程序的sysfs属性,来完成以下基本管理工作,然后它遍历所有属于该驱动程序的设备,并为其调用release函数,

热插拔:

内核角度

热插拔是在硬件、内核、内核驱动程序之间的交互

用户的角度

热插拔是在内核与用户之间,通过调用/sbin/hotplug程序的交互。

/sbin/hotplug工具

当用户向系统添加或者删除设备时,会产生热插拔事件,这会导致内核调用用户空间程序/sbin/hotplug

第十五章 内存映射和DMA

Linux的内存管理

地址类型

Linux是一个虚拟内存系统,所以用户程序所使用的地址与硬件使用的物理地址是不等同的.虚拟内存的存在可以让系统中运行的程序分配比物理内存更多的内存.

Linux使用的存在如下地址类型
  • 用户虚拟地址

    这是在用户空间程序所能看到的常规地址.用户地址有可能是32位或者64位的,每个都有自己的虚拟地址空间

  • 物理地址

    该地址在处理器和系统内存之间使用.物理地址也分为32位或者64位的,在某些情况下32位系统也能使用64位物理内存

  • 总线地址

    该地址在外围总线和内存之间使用.通常它们与处理器使用的物理地址相同(这也不是必需的).一些计算机体系架构提供了I/O内存管理单元(IOMMU),它实现总线和主内存之前的重新映射.

  • 内核逻辑地址

    内核逻辑地址组成了内核常规地址空间.该地址映射了部分(或全部)内存,并经常被视为物理地址.在大多数体系架构中,逻辑地址和与其相关联的物理地址仅仅相差一个固定的偏移量.逻辑地址使用硬件内建的指针大小,因此在安装了大量内存的32位系统中,它无法寻址全部的物理地址.逻辑地址通常保存在unsigned long 或者void *这样类型的变量中.kmalloc返回的内存就是内核逻辑地址.

  • 内核虚拟地址

    内核虚拟地址和逻辑地址的相同之处在于他们都将内核空间的地址映射到物理地址上.内核虚拟地址与物理地址的映射不必是线性的和一对一的.然而逻辑地址确是线性且一对一的.许多逻辑地址都是内核虚拟地址,但是许多内核虚拟地址不是逻辑地址.如果有一个逻辑地址,宏__pa()(在)返回其对应的物理地址;使用__va()也能将物理地址逆向映射到逻辑地址,但这只对低端内存页有效.

  • 物理地址和页

    物理地址被分成离散的单元,称之为页.系统内部许多对内存的操作都是基于单个页的.
    页PAGE_SIZE的定义在中,目前大多数系统都是用每页4096个字节.
    内存地址,无论是虚拟的还是物理的,他们都被分为页号和一个页内的偏移量.
    如果忽略了地址偏移量,并将除去偏移量的剩余位移到右端,称该结果为页帧数,移动位在页帧数和地址间进行转换是一个常用操作;而PAGE_SHIFT显示了必须移动多少位才能完成这个转换.

高端与低端内存

使用32位只能在4GB的内存中寻址.

内核将4GB的虚拟地址空间分割为用户空间和内核空间;在二者的上下文中使用同样的映射.一般的分割是将3GB分配给用户空间,1GB分配给内核空间.内核代码和数据结构必须与这样的空间相匹配,但是占用内核地址空间最大的部分是物理内存的虚拟映射.内核无法直接操作没有映射到内核地址空间的内存.

低端内存

存在于内核空间上的逻辑地址内存,现在绝大部分系统,它们的内存都是低端内存

高端内存

指那些不存在逻辑地址的内存,这是因为它们处于内核虚拟地址之上.

内存映射和页结构

page结构,这个数据结构用来保存内核需要知道的所有物理内存信息;对系统中每个物理页,都有一个page结构相对应,下面介绍下该结构中的几个成员:

  • atomic_t count

    对该页的访问计数,当计数值为0时,该页将返回给空闲链表

  • void *virtual

    如果页面被映射,则指向页的内核虚拟地址;如果未被映射则为NULL.低端内存总是被映射;而高端内存页通常不被映射.

  • unsigned long flags

    描述页状态的一系列标志.其中,PG_locked表示内存中的页已经被锁住,而PG_reserved表示禁止内存管理系统访问该页.

内核维护了一个或者多个page结构数组,用来跟踪系统中的物理内存.在一些系统中,有一个单独数组称之为mem_map.非一致性内存访问(NUMA)系统和有大量不连续物理内存的系统会有多个内存映射数据.

下面是一些函数和宏用来在page结构指针与虚拟地址之间进行转换:

  • struct page *virt_to_page(void *kaddr)

    该宏在中定义,负责将内核逻辑地址转换为相应的page结构指针.由于它需要一个逻辑地址,因此它不能操作vmalloc生成的地址以及高端内存.

  • struct page *pfn_to_page(int pfn)

    针对给定的页帧号,返回page结构指针,如果需要的化,在将页帧号传递给pfn_to_page前,使用pfn_valid检查帧号的合法性

  • void *page_address(struct page *page)

    如果地址存在的话,则返回页的内核虚拟地址.对于高端内存来说,只有当内存页被映射后改地址才存在.

  • #include
  • void *kmap(struct page *page)
  • void kunmap(struct page *page)

    kmap为系统中的也返回内核虚拟地址.对于低端内存页来说,它只返回页的逻辑地址;对于高端内存,kmap在专用的内核地址空间创建特殊的映射,因此不要持有映射过长的时间.kmap调用维护了一个计数器,因此如果两个或是多个函数对同一页调用kmap,操作也是正常的.

  • #include
  • #include
  • void *kmap_atomic(struct page *page, enum km_type type)
  • void kunmap_atomic(void *addr, enum km_type type)

    kmap_atomic是kmap的高性能版本,每隔体系架构都为原子的kmap维护着一个槽(专用也表入口)的列表.

页表

页表表示一种将虚拟地址转换为相应的物理地址的机制.它基本上是一个多层树形结构,结构化的数据中包含了虚拟地址到物理地址的映射和相关的标志位.

虚拟内存区

虚拟内存区(VMA)用于管理进程地址空间中不同区域的内核数据结构.一个VMA表示在进程的虚拟内存中的一个同类区域:拥有同样权限标志位和被同样对象(一个文件或者交换空间)备份的一个连续的虚拟内存地址范围.

进程的内存映射至少包含下面这些区域:

  • 程序的可执行代码(通常成为text)区域
  • 多个数据区,其中包含初始化数据(在开始执行的时候就拥有明确的值),非初始化数据(BSS)以及程序堆栈.
  • 与每个活动的内存映射对应的区域

通过查看/proc/pid/maps(其中pid要替换为具体的进程ID)文件就能了解进程的内存区域.而 /proc/self是一个特殊的文件,因为它始终指向当前进程.

通过 cat /proc/pid/maps所获取到的数据都是以如下形式表示的:
start-end perm offset major:minor inode image

vm_area_struct 结构

当用户空间进程调用mmap,将设备内存映射到它的地址空间时,系统通过创建一个表示该映射的新VMA作为响应.

Tips: 这里我的理解是,由于VMA维护的是一个单独的进程地址空间,当用户空间调用mmap的时候,就将设备所申请的内存(设备驱动会维护自己的一个内存区域)映射到这个进程里面。支持mmap的驱动程序需要帮助进程完成VMA的初始化.

在设备驱动程序对mmap的实现中会使用到这些成员:

  • unsigned long vm_start
  • unsigned long vm_end;

    该VMA所覆盖的虚拟地址范围,这是/proc/*/maps中最前面的两个成员

  • struct file *vm_file

    指向与该区域相关联的file结构指针

  • unsigned long vm_pgoff

    以页为单位,文件中该区域的偏移量,当映射一个文件或者设备时,它是该区域中被映射的第一页在文件中的位置.

  • unsigned long vm_flags

    描述该区域的一套标志.驱动程序最感兴趣的标志是VM_IO和VM_RESERVED.VM_IO将VMA设置成一个内存映射I/O区域.VM_IO会阻止系统将该区域包含在进程的核心转储中,为什么?因为核心转储会优化内存,从而影响I/O命令.VM_RESERVED告诉内存管理系统不要将该VMA交换出去,大多数设备映射中都设置该标志.

  • struct vm_operations_struct *vm_ops

    内核能调用的一套函数,用来对该内存区进行操作.它的存在表示内存区域是一个内核对象,这点类似与file结构
    vm_operations_struct 结构定义在中.这些操作只是用来处理进程的内存需求:

    • void (*open)(struct vm_area_struct *vma);
      内核调用open函数,以允许实现VMA的子系统初始化该区域.
    • void (*close)(struct vm_area_struct *vma);
      当销毁一个区域时,内核将调用close操作.
    • struct page *(*nopage)(…)
      当一个进程要访问属于合法VMA的页,但该页又不在内存中时,则为相关区域调用nopage函数.在将物理页从辅助存储器中读入后,该函数返回指向物理页的page结构指针,如果在该区域没有定义nopage函数,则内核将为其分配一个空页.
  • void *vm_private_data

    驱动程序用来保存自身信息的成员

内存映射处理

在系统中的每个进程(除了内核空间的一些辅助线程外)都拥有一个struct mm_struct结构(在中定义),其中包含了虚拟内存区域链表/页表以及其它大量内存管理信息,还包含了一个信号灯(mmap_sem)和一个自旋锁(page_table_lock). 在task结构中能找到该结构的指针,在少数情况下当驱动程序需要访问它时,常用的办法是使用current->mm(多个进程可以共享内存管理结构,Linux就是用这种方式实现线程的).

mmap设备操作

对于驱动程序来说,内存映射可以提供给用户程序直接访问设备内存的能力.

映射一个设备意味着将用户空间的一段内存与设备内存关联起来,无论何时当程序在分配的地址范围内读写时,实际上访问的就是设备.

例如:在X服务器的例子中,使用mmap就能迅速而便捷地访问显卡内存.

对于mmap的另一个限制是:必须以PAGE_SIZE为单位进行映射.内核只能在页表一级上对虚拟地址进行管理,因此那些被映射的区域必须是PAGE_SIZE的整数倍,并且在物理内存中的其实地址也要求是PAGE_SIZE整数倍.

mmap方法是file_operations结构的一部分,并且执行mmap系统调用时将调用该方法.
有两种建立页表的方法:

  • 使用remap_pfn_range函数一次全部建立该方法负责为一段物理地址建立新的页表.
  • 通过nopage VMA方法每次建立一个页表

Tips:为了实现对mmapd的更好的灵活性,提倡使用VMADE nopage方法实现内存映射.

重映射特定的I/O区域

一个典型的驱动程序只映射与其外围设备相关的一小段地址,而不是映射全部地址。
为了向用户空间只映射部分内存的需要,驱动程序只需要使用偏移量即可。

重新映射RAM

remap_pfn_range 函数无法处理RAM表明:想某些基于内存的设备无法简单地实现mmap,因为它的设备内存是通用的RAM,而不是I/O内存,对于任何需要将RAM映射到用户空间的驱动程序来说,可以使用nopage函数来到达目的。

直接内存访问

DMA是一种硬件机制,它允许外围设备和主内存之间直接传输它们的I/O数据,而不需要系统处理器的参与。
这种机制可以大大提高与设备通信的吞吐量,因为免除了大量的CPU计算开销.

DMA数据传输概览

有两种方式引发数据传输:

  • 软件对数据的请求(比如通过read函数)

    • 当进程调用read,驱动程序函数分配一个DMA缓冲区,并让硬件将数据传输到这个缓冲区中,进程处于睡眠状态
    • 硬件将数据写入到DMA缓冲区中,当写入完毕,产生一个中断。
    • 中断处理程序获得输入的数据,应答中断,并且唤醒进程,该进程现在即可读取数据。
  • 硬件异步地将数据传递给系统

    • 硬件产生中断,宣告新数据的到来
    • 中断处理程序分配一个缓冲区,并且告诉硬件向哪里传输数据。
    • 外围设备将数据写入缓冲区,完成后产生另外一个中断
    • 处理程序分发新数据,唤醒任何相关进程,然后执行清理工作
DMA缓冲区:

DMA需要设备驱动程序分配一个或者多个适合执行DMA的特殊缓冲区。

分配DMA缓冲区

并不是所有的内存区间都适合DMA操作,比如高端内存就不能用于DMA,因为外围设备不能使用高端内存的地址

总线地址

使用DMA的设备驱动程序将于连接到总线接口上的硬件通讯,硬件使用的是物理地址,而程序代码使用的是虚拟地址。

DMA映射

根据DMA缓冲区期望保留的时间长短,PCI代码区分两种类型的DMA映射:

  • 一致性DMA映射

这种类型的映射存在与驱动程序声明周期中,一致性映射的缓冲区必须可同时被CPU和外围设备访问(其他类型的映射)。因此一致性映射必须保存在一致性缓存中。建立和使用一致性映射的开销是很大的

  • 流式DMA映射

通常为单独的操作建立流式映射。

内核开发者建议尽量使用流式映射,然后再考虑一致性映射。这么做有两个原因:

  • 在支持映射寄存器的系统中,每个DMA映射使用总线上的一个或者多个映射寄存器。一致性映射具有很长的生命周期,因此会在相当长的时间内占用这些寄存器,甚至在不使用它们的时候也不释放所有权。
  • 在一些硬件中,流式映射可以被优化,但优化的方法对一致性映射无效。

建立一致性DMA映射的操作:

建立流式DMA映射
关于流式DMA映射的几条原则:

  • 缓冲区只能用于这样的传送,即其传送方向匹配与映射时给定的方向值
  • 一旦缓冲区被映射,它将属于设备而不是处理器。直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。所以在包含了所有要写入的数据之前,不能映射要写入的设备缓冲区。
  • 在DMA处于活动期间内,不能撤销对缓冲区的映射,否则会严重破坏系统的稳定性。

为什么在缓冲区被映射后,驱动程序不能访问它?

因为当一个缓冲区建立DMA映射时,内核必须保证在该缓冲区内的全部数据都被写入了内存。当调用dma_unmap_single函数时,很可能有一些数据还在处理器的缓存中,因此必须被显式刷新,再刷新动作后,处理器写入缓冲区的数据对设备是不可见的。

为什么获得一个正确的传输方向是一个重要的问题?

因为DMA_BIDIRECTIONAL回弹缓冲区在操作前后都要拷贝数据,这通常会浪费不必要的CPU指令周期。

DMA通道是一个可共享的资源,如果多于一个的处理器要同时对其进行编程,则会产生冲突,因此有一个叫作dma_spin_lock的自旋锁保护控制器,但驱动程序不能直接操作该锁,必须使用特定的函数进行操作,这里就不做介绍了

你可能感兴趣的:(Linux Device Driver 3rd 下)