linux中的i/o资源管理

背景

由于一些原因,需要了解一下linux管理I/O资源的机制。这里也解释一下什么是I/O资源,如果做过嵌入式开发相关的工作,应该对arm cpu访问串口寄存器的方式有所了解。我们在实现串口的设备驱动时,其实是根据spec实现一个串口设备的数据结构,然后将数据结构的指针指向串口设备的基址。这一片区域只能由串口驱动的代码访问。cpu通过这种方式访问串口设备与cpu访问RAM类似,被称为内存映射方式(memory-mapped)。还有一种方式,例如在x86架构的cpu中,cpu具有一块独立的地址空间,这块区域只能通过cpu执行特殊的指令(inb/outb, etc)来访问,这种方式被称为I/O映射方式(I/O-mapped)。

在实时操作系统中,我们对于整个系统的地址空间的把控力度很足,并且一般情况下,整个硬件系统都是厂商定制且软件业务一旦确定,对于实时操作系统的代码修改不大,所以使用hardcode的方式既省时省力,效率还很高。但是,在linux中需要考虑兼容性和可移植性,所以针对不同的平台提供不同的接口会导致上一层的开发者非常苦恼。为管理I/O资源开发一套统一的接口就势在必行了。本着严谨的研究态度,我找到了linus在发现这个问题之后发送的邮件(Being more anal about iospace accesses…),从大师骂人的话语中也能了解到大师的思想,哈哈。

下面回归正题,我们从数据结构和接口两个层面了解一下linux中的I/O资源管理机制。作为内核接口的使用者,从接口先入手就再好不过了。

参考代码

linux/kernel/resource.c

include/linux/ioport.h

接口

前段时间看了EOPL,养成了一个好习惯,即将接口和数据结构的实现分离开来。这样的好处当然就是可以随时修改数据结构的实现,而不会影响使用该数据结构的代码。linux中的设计也采用了这种方式,我只能说大佬的思路都是相似的。

现在我们明确了什么是I/O资源,所以我们需要操作的I/O资源其实就分两类:I/O-mapped的资源和memory-mapped的资源。所以我们需要用软件实现能描述这两种资源的数据结构,以及相关的api,通过操作数据结构去访问I/O资源

我将linux中与resource相关的api做了一个分类,如图1所示

linux中的i/o资源管理_第1张图片

图1. resource api

不感知resource数据结构的api

按照EOPL中提供的思路,根据是否需要知道struct resource这个数据结构将api分成两类。作为I/O资源的使用者,我其实不关心resource的具体实现,只需要在想使用某个I/O资源时,我提供I/O资源的起始地址和长度,然后能够确保我是独占这个资源就好了。linux中提供了相应的api,如图1中所示。图中标红的rename_region是个例外,这个api需要了解resource的数据结构。

devres api是带有引用计数自动回收内存机制的api,现代的驱动开发应该多使用devres api

api的功能

这里只简单的介绍一下常用的一些api,其他的请自行看源码。

普通api

  • request_region:
    • 传入参数:有三个,start是I/O资源的起始物理地址,n是该段资源的长度,name是为该资源取的名字。
    • 功能:分配一个资源节点,然后加入到ioport_resource资源树中。这里只要明确函数的功能分配一个数据结构出来让我们能够独占这块资源,资源的范围是[start, start+n];具体的细节放在数据结构的具体实现中分析。
    • 返回值:失败返回NULL,反之返回指向资源节点的指针
  • request_mem_region:
    • 传入参数:与request_region相同
    • 功能:与request_region类似,不同点在新分配的资源节点会加入到iomem_resource资源树中。
    • 返回值:与request_region相同
  • release_region:
    • 传入参数:start是I/O资源的起始物理地址,n是该段资源的长度
    • 功能:从ioport_resource资源树中,找到范围是[start, start+n]的资源节点,然后释放该节点。
    • 返回值:void
  • release_mem_region:
    • 传入参数:与release_region类似。
    • 功能:从iomem_resource资源树中,找到范围是[start, start+n]的资源节点,然后释放该节点。
    • 返回值:void

devres api

  • devm_request_region:类似request_region,多一个参数dev,一旦dev释放,使用该api申请的i/o资源自动释放。
  • devm_request_mem_region:类似request_mem_region,多一个参数dev,一旦dev释放,使用该api申请的i/o资源自动释放。

使用场景

在编写platform device driver的时候,我们使用如下api从dts中获取设备的物理基址

#include 
void __iomem *devm_platform_ioremap_resource(struct platform_device *pdev, unsigned int index);

函数调用栈如下

devm_platform_ioremap_resource
    -> devm_platform_get_and_ioremap_resource
    	-> platform_get_resource
        -> devm_ioremap_resource
        	-> __devm_ioremap_resource
    			-> devm_request_mem_region  /* 这里申请了资源 */
    			-> __devm_ioremap			/* 这里对申请的资源做了重映射,返回的是resource覆盖物理地址对应的虚拟地址 */

所以,linux的i/o资源管理机制需要结合dts一起使用

  1. 在dts中设置reg属性。
  2. 在driver的probe函数中调用devm_platform_ioremap_resource,可以得到reg中物理地址对应的虚拟地址base。
  3. 之后cpu通过访问base可以访问到I/O资源。

下面是我打印的树莓派soc上所有设备的物理地址,如图2所示
linux中的i/o资源管理_第2张图片

图2. 树莓派iomem

操作struct resource数据结构的api

实现这些api实际上是实现了一种描述I/O资源的数据结构,linux中叫struct resource。这种实现的细节放在下一节中描述。其实这些api对于驱动开发者来说应该是透明的,这里我就简单的描述一下api的作用。

api的功能

  • request_resource:
    • 传入参数:资源树节点指针root,新的资源节点指针new
    • 功能:由__request_resource实现,该函数的功能是将需要使用new插入到root的子节点中,如果与资源树的子节点中已经申请的资源区域重合,则返回冲突的资源,如果没有冲突,则插入新的资源块后返回NULL。
    • 返回值:request_resource函数插入成功返回0,反之返回-EBUSY
  • release_resource:
    • 传入参数:要释放的资源节点new
    • 功能由__release_resource实现,该函数的功能是将资源块new从该资源树节点new所在的父节点的树中释放,如果第二个参数设置为true,那么该资源块节点的所有子节点都释放,反之,将该节点所有的子节点上升一级插入到原节点的父节点下。release_resource是第二个参数设置为true的__release_resource
    • 返回值:如果成功返回0,如果失败返回-EINVAL。
  • insert_resource:
    • 传入参数:资源树节点指针parent,要插入的资源节点指针new
    • 功能由__insert_resource实现,该函数的功能是将资源块new插入到资源树节点parent所在的资源树中。
  • remove_resource:
    • 传入参数:
    • 功能由__release_resource实现,传入的第二个参数为false。
  • lookup_resource:
  • allocate_resource:
    • 从资源树root中分配一个struct resource结构。

struct resource的实现

linux中是以资源树的形式管理的I/O资源的,所以上一节中操作struct resource的api其实是对资源树操作的实现。这下就感受到接口和数据结构分离的好处。我完全可以使用其他类型的数据结构表示struct resource,重新实现相关的api,但是用户使用的api不会受到任何影响。当然linux对于资源树的实现也是相当精妙,下面我们就来着重研究一下struct resource的实现,当然仅包括对基本功能的分析,一些高级的属性有需要的话请自行分析哈。

首先在include/linux/ioport.h中struct resource的定义如下:

struct resource {
    resource_size_t start;  /* I/O资源的起始物理地址 */
    resource_size_t end;    /* I/O资源的终止物理地址,闭区间 */
    const char *name;       /* 该I/O资源的名字 */
    unsigned long flags;    /* 描述此资源属性的标记 */
    unsigned long desc;
    struct resource *parent, *sibling, *child;
};

这个结构和b+树的实现类似,区别是资源树的每一层节点都用指针连起来了,而b+树只有最底一层的叶子节点用指针连接起来。

然后在linux/kernel/resource.c中,实现了对I/O-mapped类型和memory-mapped类型的资源树,代码如下

struct resource ioport_resource = {
    .name   = "PCI IO",
    .start  = 0,
    .end    = IO_SPACE_LIMIT,
    .flags  = IORESOURCE_IO,
};
EXPORT_SYMBOL(ioport_resource);

struct resource iomem_resource = {
    .name   = "PCI mem",
    .start  = 0,
    .end    = -1,
    .flags  = IORESOURCE_MEM,
};
EXPORT_SYMBOL(iomem_resource);

i/o-mapped类型的资源的大小是[0, IO_SPACE_LIMIT],IO_SPACE_LIMIT的值由硬件决定,各个体系结构的core对应的值不一定相同,如图3所示。
linux中的i/o资源管理_第3张图片

图3. IO_SPACE_LIMIT

memory-mapped类型的资源的大小期望是[0, 最大的物理地址],这里由于各个soc的内存大小各不相同所以将end设置为-1表示最大的物理地址。

这里结合上面的api,以PCI IO资源树为例,我画了图4,方便理解struct resource。

linux中的i/o资源管理_第4张图片

图4. PCI IO resource-tree

实现思路

  • 根节点的地址范围覆盖整个芯片所有可访问的I/O资源的地址空间
  • 所有子节点的地址范围都是父节点地址范围的子集
  • 兄弟节点之前是按地址大小顺序排列,且地址范围不重合
  • 父节点的child指针指向地址范围位于最低的子节点

图中,b和d,e节点是冲突的,但是b节点设置了特殊的属性,所以d,e节点变成了b节点的子节点,在pci 设备的driver中经常有这种情况。

其他功能

linux/kernel/resource.c中,还有针对其他高级功能的代码,这里就不一一分析了,有兴趣的可以自己去分析,比如

  • 内存热插拔相关的资源管理,有static struct resource *bootmem_resource_free;资源树去管理boot阶段内存热插拔的资源
  • 在proc目录下面创建了两个seq_file,ioports,iomem。用于查看ioport_resource资源树和iomem_resource资源树的信息。

你可能感兴趣的:(操作系统,linux)