一文详解Linux UIO技术

描述

1.UIO简介

1.1 什么是UIO技术

UIO(Userspace I/O)是运行在用户空间的I/O技术,Linux 系统中一般的驱动设备都是运行在内核空间,应用程序在用户空间调用即可。UIO 则是将驱动的小部分运行在内核空间,在用户空间实现驱动的绝大多数功能,使用 UIO 可以避免设备的驱动程序需要随着内核的更新而更新的问题。

1.2 UIO主要任务

设备驱动的主要任务为存取设备的内存、处理设备产生的中断。对于第一个任务,UIO 核心实现了 mmap() 可以处理物理内存 (physicalmemory),逻辑内存(logicalmemory),虚拟内存(virtualmemory)。UIO 驱动的编写是就不需要再考虑这些繁琐的细节。

对于第二个任务,设备中断的应答必须在内核空间进行。所以在内核空间有一小部分代码用来应答中断和禁止中断,其余的工作全部留给用户空间处理。如果用户空间要等待一个设备中断,它只需要阻塞在对 /dev/uioX 的 read() 操作上,当设备产生中断时,read() 操作立即返回。UIO 也实现了 poll() 系统调用,可以使用 select() 来等待中断的发生,select() 有一个超时参数可以用来实现有限时间内等待中断。对设备的控制还可以通过 /sys/class/uio 下的各个文件的读写来完成。注册的 uio 设备将会出现在该目录下,假设 uio 设备是 uio0,那么映射的设备内存文件出现在 /sys/class/uio/uio0/maps/mapX,对该文件的读写就是对设备内存的读写。

1.3 UIO 框架

uio 代码可以分为三个部分:内核 uio 框架及内核内部函数,uio 内核驱动部分,uio 用户驱动部分。

内核 uio 框架通过配置内核选项 CONFIG_UIO=y 使能 Userspace I/O drivers,在内核初始化时会调用 uio_init 创建 uio_class。

igb_uio 内核驱动通过编译运行 igb_uio.ko 加载并注册一个 pci 设备,但是igbuio_pci_driver 对应保存 pci 设备信息的 id_table 指针为空,这样在内核注册此 pci 设备时,会找不到匹配的设备,就不会调用 igb_uio 驱动中的探测 probe 函数 uio 用户态驱动,运行 dpdk 提供的 Python 脚本 dpdk-devbind.py 绑定网卡设备后才会执行其 probe 函数。

uio 用户态驱动则是在 dpdk 实例程序初始化 EAL 环境抽象层时才会进行驱动与设备匹配加载。

内核

上图为 UIO 框架,运行在内核空间的驱动程序功能为:分配、记录设备需要的资源,注册 uio 设备,中断应答处理。

2.内核UIO框架

2.1 UIO 的内核使能及初始化

uio 的支持需要进行内核配置,配置路径为 Device Drivers ---> Userspace I/O drivers,具体配置如下图所示。

内核

2.2 UIO 重要数据结构

内核态 uio 驱动有两个重要的数据结构,分别为 uio_device 和 uio_info,该结构定义如下所示。

struct uio_device {
struct module *owner;
struct device dev;
int minor; /* 次设备号 */
atomic_t event; /* 中断事件计数 */
struct fasync_struct *async_queue; /* 异步等待队列 */
wait_queue_head_t wait; /* 等待队列     */
struct uio_info *info; /* uio设备信息 */
struct mutex info_lock;
struct kobject *map_dir;
struct kobject *portio_dir;
};


struct uio_info {
struct uio_device *uio_dev; /* uio_info所属的uio设备 */
const char *name; /* 设备名称 */
const char *version; /* 设备驱动版本 */
struct uio_mem mem[MAX_UIO_MAPS]; /* 可映射内存区域列表 */
struct uio_port port[MAX_UIO_PORT_REGIONS]; /* 端口区域列表 */
long irq; /* 中断号 */
unsigned long irq_flags; /* 中断请求标志 */
void *priv; /* 可选的私有数据 */
/* 设备中断处理程序 */
irqreturn_t (*handler)(int irq, struct uio_info *dev_info);
/* 此uio设备的mmap操作 */
int (*mmap)(struct uio_info *info, struct vm_area_struct *vma);
/* 此uio设备的open操作 */
int (*open)(struct uio_info *info, struct inode *inode);
/* 此uio设备的release操作 */
int (*release)(struct uio_info *info, struct inode *inode);
/* 禁止/使能操作,向/dev/uioX写入0/1 */
int (*irqcontrol)(struct uio_info *info, s32 irq_on);
};

uio 核心是名为 "uio" 的字符设备;用户驱动的内核部分(igb_uio)使用 uio_register_device 向 uio 核心部分注册 uio 设备,uio 核心的任务就是管理好注册的 uio 设备,uio 设备使用的数据结构是 uio_devic;设备属性,比如 name,open(),release()等操作都放在了 uio_info 结构中,用户使用 uio_register_device 注册这些驱动之前要设置好 uio_info。

static const struct file_operations uio_fops = {
  .owner = THIS_MODULE,
  .open = uio_open,
  .release = uio_release,
  .read = uio_read,
  .write = uio_write,
  .mmap = uio_mmap,
  .poll = uio_poll,
  .fasync = uio_fasync,
  .llseek = noop_llseek,
};

uio 核心字符设备注册 uio_open ,uio_release,uio_read ,uio_write 中除了完成相关的维护工作外,还调用了注册在 uio_info 中的相关方法。比如,在 uio_open 中调用了 uio_info 中注册的 open 方法。

2.3 UIO 设备注册接口

在用户驱动的内核部分 igbuio_pci_probe() 调用 uio_register_device((宏定义为__uio_register_device)注册 uio 设备时,在 __uio_register_device 中调用 uio_get_minor 函数,在 uio_get_minor 函数中,利用 idr 机制建立了次设备号(整数ID)和 uio_device 类型指针之间的联系,uio_device 指针指向了代表注册的 uio 设备的内核结构,device_add()调用完毕后在 /sys/class/uio/ 下就会出现代表 uio 设备的 uioX 文件夹,其中 X 为 uio 设备的次设备号,执行流程如下。

/* 用户驱动的内核部分igb_uio */
igbuio_pci_probe(); /* 设备与驱动匹配后执行 */
  uio_register_device(); /* 设备注册 */
/* 内核部分uio核心 */
    __uio_register_device();
     kzalloc(); /* 为uio设备申请空间 */
     init_waitqueue_head(&idev->wait); /* 初始化等待队列 */
     atomic_set(&idev->event, 0); /* 清空中断事件计数器 */
     uio_get_minor(idev); /* 获取次设备号 */
     device_initialize(&idev->dev); /* 设备初始化 */
     dev_set_name(&idev->dev, "uio%d", idev->minor); /* 设置设备名称 */
     device_add(&idev->dev); /* 设备添加 */

3.UIO内核驱动

3.1 驱动模块编译

DPDK 在编译生成 igb_uio.ko 时需要依赖运行系统的内核,具体编译方法可参考DPDK官方文档或者本专栏的《DPDK交叉编译》。

3.2 驱动模块加载

在模块编译成功后,需要在 DPDK 运行环境中插入 igb_uio 模块,igb_uio 驱动主要做的就是注册一个 pci 设备,但是 igbuio_pci_driver 对应保存 pci 设备信息的 id_table 指针为空,这样在内核注册此 pci 设备时,会找不到匹配的设备,就不会调用 igb_uio 驱动中的探测 probe 函数(对应igb_uio的igbuio_pci_probe()不会被调用到),只会在 /sys/bus/pci/drivers/ 目录下创建 igb_uio 相应的目录。

3.3 绑定网卡

DPDK 工程中提供了绑定网卡的 Python 脚本(dpdk-devbind.py),使用脚本或命令将指定的网卡绑定到igb_uio模块后,igb_uio 的 probe 函数会执行(id_table非空),这是因为扫描到了匹配的设备,同时生成 /dev/uioX 设备(X为次设备号),/sys/class/uio 目录下也产生与 /dev/uioX 设备对应的内容。

3.4 probe 函数执行

UIO 内核驱动注册机制与其他驱动类似,通过调用 linux 提供的 uio API 接口进行注册,在注册之前所做的主要工作如下。

a. 分配一个封装的 UIO 设备数据结构,包括了 uio_info。

/* A structure describing the private information for a uio device. */
struct rte_uio_pci_dev {
  struct uio_info info;
  struct pci_dev *pdev;
  enum rte_intr_mode mode;
  atomic_t refcnt;
};


igbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
    struct rte_uio_pci_dev *udev;
    udev = kzalloc(sizeof(struct rte_uio_pci_dev), GFP_KERNEL);
    /* 省略无关代码 */
}

b. 使能 PCI 设备。

c. 填充 uio_info 结构体的信息。

/* fill uio infos */
udev->info.name = "igb_uio";
udev->info.version = "0.1";
udev->info.irqcontrol = igbuio_pci_irqcontrol;
udev->info.open = igbuio_pci_open;
udev->info.release = igbuio_pci_release;
udev->info.priv = udev;
udev->pdev = dev;

d. 映射 UIO 设备 PCI 资源空间(PCI 设备的物理地址及大小)。

调用函数为 igbuio_setup_bars()--->igbuio_pci_setup_iomem(),并且填充 uio_info 结构体的内存信息,映射 UIO 设备的内存空间后,就可以在用户态直接对设备内存进行操作。

e. 根据 uio_info 的信息注册 UIO 设备。

f. 中断注册。

初始化中断的中断号、中断模式、中断标志等,并初始化 uio_info 的 handler 字段,在产生中断时,中断处理函数将会被调用。如果有实际的硬件设备,那么 irq 应该是硬件设备实际使用的中断号。

4.用户驱动加载

网卡驱动模型一般包含三层,即 PCI 总线设备、网卡设备以及网卡设备的私有数据结构,即将设备的共性一层层的抽象,PCI 总线设备包含网卡设备,网卡设备又包含其私有数据结构。在 DPDK 中,首先会注册设备驱动,然后查找当前系统有哪些 PCI 设备,并通过 PCI_ID 为 PCI 设备找到对应的驱动,最后调用驱动初始化设备。

4.1 网卡驱动注册

网卡驱动的注册使用了 GCC attribute 扩展属性的 constructor 属性,使得网卡驱动的注册在程序 MAIN 函数之前就执行了,以 e1000 网卡驱动为例。

/* 驱动注册调用的宏 */
RTE_PMD_REGISTER_PCI(net_e1000_igb, rte_igb_pmd);
RTE_PMD_REGISTER_PCI(net_e1000_igb_vf, rte_igbvf_pmd);


/* 对RTE_PMD_REGISTER_PCI宏进行展开 */
#define RTE_PMD_REGISTER_PCI(nm, pci_drv) \\
RTE_INIT(pciinitfn_ ##nm) \\
{\\
  (pci_drv).driver.name = RTE_STR(nm);\\
  rte_pci_register(&pci_drv); \\
} \\
RTE_PMD_EXPORT_NAME(nm, __COUNTER__)


#define RTE_INIT(func) RTE_INIT_PRIO(func, LAST) 


#ifndef RTE_INIT_PRIO /* Allow to override from EAL */
#define RTE_INIT_PRIO(func, prio) \\
static void __attribute__((constructor(RTE_PRIO(prio)), used)) func(void)
#endif

使用 attribute 的 constructor 属性,在 main 函数执行前执行 rte_pci_register 函数,将net_e1000_igb驱动挂载到全局rte_pci_bus.driver_list 链表上,其他的网卡驱动也是使用相同的方式挂载到该链表上。

4.2 扫描当前系统 PCI 设备

DPDK 例程中,main 函数调用rte_eal_init()—>rte_bus_scan()—>rte_pci_scan() 函数,查找当前系统中有哪些网卡,分别是什么类型,并将它们挂到全局链表 rte_pci_bus.device_list 上。

rte_pci_scan() 通过读取 "/sys/bus/pci/devices/" 目录下的信息,扫描当前系统的PCI设备,并按照PCI地址从大到小的顺序将设备挂载到 rte_pci_bus.device_list 链表上。

4.3 PCI 驱动注册

在 4.1 节的分析中,网卡的驱动在 main 函数执行之前已经注册了,挂载到全局rte_pci_bus.driver_list 链表上,4.2 节流程中也将设备保存在 rte_pci_bus.device_list 链表上,接下来分析设备与驱动是如何进行匹配的,匹配的流程如下。

rte_eal_init
  rte_bus_probe
    pci_probe
      pci_probe_all_drivers
        rte_pci_probe_one_drive

当设备与驱动的 vendor/device ID 等信息匹配时,就会执行驱动的 probe 函数。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分