[架构之路-46]:目标系统 - 系统软件 - Linux OS硬件设备驱动-UIO用户空间IO驱动框架与用户空间协议栈

Posted 文火冰糖的硅基工坊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[架构之路-46]:目标系统 - 系统软件 - Linux OS硬件设备驱动-UIO用户空间IO驱动框架与用户空间协议栈相关的知识,希望对你有一定的参考价值。

目录

第1章 用户空间IO驱动概述

1.1 概述

1.2 UIO的本质

1.3 UIO的应用场景

1.4 为什么我们要把I/O放在用户空间呢? 

第2章  如何在用户空间使用UIO设备

2.1 概述

2.2 分层架构

2.3 操作uio设备的简单流程为:

第3章 uio驱动程序框架与原理

3.1 在内核硬件硬件相关层(*.ko程序)

3.2 内核核心层(UIO内核驱动框架)

3.3 在用户空间部分(*.so程序)


第1章 用户空间IO驱动概述

1.1 概述

uio全称为用户空间IO(Userspace I/O),是一种在用户空间编写设备驱动程序的框架。

一般而言,Linux的驱动是运行在内核空间的,即设备驱动本身是作为内核源码的一部分进行编译的,这样的驱动程序能够访问系统的所有资源,但是稍有处理不当就容易引起内核奔溃。

而uio驱动是在用户空间就行开发的,其本质就是一个应用开发,因此这类驱动就与内核空间隔离开,即使驱动奔溃也不会影响到整个系统。

1.2 UIO的本质

对于驱动程序来说,一般有两个重要的功能:

(1)配置管理:读写某一段内存地址空间

采用统一编址的处理器,实际上就是将处理器能够访问的地址空间划分一部分出来作为外设寄存器的地址,处理器在访问外设寄存器的时候,就可以通过地址来操作。

(2)数据收发:响应外部中断

响应外部中断是驱动程序异步收发设备数据的一种通用的方式。

uio驱动框架以及硬件相关的驱动程序,就是试图尽可能在用户空间完成上述两个主要功能。

1.3 UIO的应用场景

那么这种应用场景在什么地方呢?

有些SOC芯片内部集成了一个arm核和fpag核。FPGA的寄存器外设挂载在arm的地址空间上。

要实现FPGA的功能,只需要ARM CPU访问设置FPGA的寄存器就可以了。

UIO驱动,就是提供了这样的一种机制:把内核硬件设备的地址空间直接暴露到用户空间,而由用户空间的程序完成对硬件设备寄存器的操作、控制硬件设备的功能,甚至直接在用户空间完成硬件设备的数据包的收发,而不是像传统内核驱动,由内核驱动在内核空间完成对硬件设备的控制。

DPDK操作网卡硬件设备,就是采用的这种方法。

1.4 为什么我们要把I/O放在用户空间呢? 

(1)增加非标准硬件设备的适应性

第一,硬件设备可以根据功能分为网络设备,块设备,字符设备,或者根据与CPU相连的方式分为PCI设备,USB设备等。它们被不同的内核子系统支持,有不同的内核驱动框架。这些标准的设备的驱动编写较为容易而且容易维护。很容易加入主内核源码树。

但是,又有很多设备难以划分到这些子系统中,比如I/O卡,现场总线接口或者定制的FPGA

通常这些非标准设备的驱动被实现为字符驱动。这些驱动使用了很多内核内部函数和。而这些内部函数和宏是变化的。这样驱动的编写者必须编写一个完全的内核驱动,而且一直维护这些代码。像这种设备如果把驱动放入Linux内核,不但增大了内核的负担,而且还很少使用,更没有人愿意免费的花费大量精力去维护这样的驱动,

使用UIO使驱动的开发可以利用所有的用户空间的应用程序开发工具和库,而且当系统内核发生变化时,只要更改UIO框架与其他内核程序交互的接口即可,不需要更改UIO框架下面的硬件相关的driver。

(2)增加驱动程序编程语言的适应性和方便性。

第二, 如果是内核驱动的开发,则必须使用C语言加上少量汇编代码。

如果是uio用户空间驱动,则可以使用C++Java ...等高级语言极大的方便了开发。

我们可以发现,有很多的卡的驱动在内核空间已经有实现,这样,我们可以参考已经存在的代码,极大的提高的开发的速度,和降低了开发成本。

而且内核驱动的调试会比用户空间程序调试复杂很多。我们经常遇到死机,涉及到多个子系统,棘手。

放在用户空间的话如果驱动程序死了,并不影响系统的正常运行并且方便了我们的开发。

(3)避免数据拷贝,增加性能。

内核空间与用户空间至少有一次内存拷贝,如果在硬件空间实现硬件的数据收发,则至少进行一次内存拷贝,用户空间才能收到才数据。如果通过UIO,直接在用户空间收发硬件设备的数据,避免了至少一次的内存数据拷贝,增加了系统的性能。

DPDK采用UIO机制,主要的原因就源于此。

第2章  如何在用户空间使用UIO设备

2.1 概述

无论是网卡,还是FPGA, 如果被设计成UIO设备,在用户空间看到的就是一个个设备文件和一段可以读写操作的内存空间而已。

下图为uio驱动框架的整体框图,在内核空间uio驱动的内核代码部分会在/dev目录下生成uio设备节点,节点名称/dev/uiox,其中x从0开始递增。

同时,在sysfs文件下还会生成与uio设备相关的属性,读写这些属性文件就可以获得该uio设备的相关信息。

2.2 分层架构

(1)内核空间

  • UIO Framework:UIO设备文件驱动程序框架。
  • Driver:UIO设备硬件直接相关程序,操作硬件地址和寄存器。

(2)接口

  • sysfs:设备信息接口
  • /dev/uiox:设备文件接口

(3)用于空间

  • read:读取硬件设备数据
  • write:向硬件设备写数据
  • mmap:把映射设备的内核地址映射到用户空间,用户空间可以直接操作这段地址空间。

2.3 操作uio设备的简单流程为:

  • 调用open函数打开uio设备节点/dev/uio0,得到文件描述符fd。
  • 调用write向uio设备写1打开中断,写0会关闭中断。
  • 调用read函数读uio设备,此时会阻塞等待uio设备中断,若产生中断则会继续执行。

第3章 uio驱动程序框架与原理

UIO分成3个部分,主要是内核部分和用户空间部分。

在UIO框架下,用户空间下的驱动程序比运行在内核空间的驱动要多得多。

3.1 在内核硬件硬件相关层(*.ko程序)

(1)分配和记录设备需要的资源和注册uio设备

  • 主要是实现硬件寄存器内存映射(struct uio_info -> struct uio_mem)及读写操作。
  • 定义设备对象、实现设备操作函数、调用设备对象的注册函数进行注册
  • 内存映射:UIO 核心实现了mmap()可以处理物理内存(physical memory),逻辑内存(logical memory),虚拟内存(virtual memory)。UIO驱动的编写是就不需要再考虑这些繁琐的细节。 如果有些设备的总线不是PCI总线, 那么仍需要做相关的处理   
  • 将uio设备的uio_mem映射到本地(mmap), 这样就可以实现在用户空间访问硬件设备寄存器的目的,

(2)必须*在内核空间实现的小部分中断应答函数

  • 处理设备产生的中断:对于设备中断的应答必须在内核空间进行。所以在内核空间有一小部分代码 用来应答中断和禁止中断,但是其余的工作全部留给用户空间处理。  

(3)代码示例

 struct uio_portio 
    struct kobject kobj;
    struct uio_port *port;
;
/**
 * struct uio_port - description of a UIO port region
 * @name:       name of the port region for identification
 * @start:      start of port region
 * @size:       size of port region
 * @porttype:       type of port (see UIO_PORT_* below)
 * @portio:     for use by the UIO core only.
 */
struct uio_port 
    const char      *name;
    unsigned long       start;
    unsigned long       size;
    int         porttype;
    struct uio_portio   *portio;
;
/* defines for uio_port->porttype */
#define UIO_PORT_NONE   0
#define UIO_PORT_X86    1
#define UIO_PORT_GPIO   2
#define UIO_PORT_OTHER  3
  /*
  * struct uio_mem - description of a UIO memory region
 * @name:       name of the memory region for identification
 * @addr:       address of the device's memory
 * @size:       size of IO
 * @memtype:        type of memory addr points to
 * @internal_addr:  ioremap-ped version of addr, for driver internal use
 * @map:        for use by the UIO core only.
 */
struct uio_mem 
    const char      *name;// 内存映射的名字
    unsigned long       addr; // 内存块的地址
    unsigned long       size; //addr所指向的内存块的大小
    int         memtype; //UIO_MEM_PHYS,UIO_MEM_LOGICAL(kmalloc()),UIO_MEM_VIRTUAL( virtual memory)
    void __iomem        *internal_addr; // If you have to access this memory region from within your kernel module,
                                                               // you will want to map it internally by using something like ioremap().
    struct uio_map      *map;
;
 struct uio_map 
    struct kobject kobj;
    struct uio_mem *mem;
;
 static const struct vm_operations_struct uio_vm_ops = 
    .open = uio_vma_open,
    .close = uio_vma_close,
    .fault = uio_vma_fault,
;
 static struct device_attribute uio_class_attributes[] = 
    __ATTR(name, S_IRUGO, show_name, NULL),
    __ATTR(version, S_IRUGO, show_version, NULL),
    __ATTR(event, S_IRUGO, show_event, NULL),
    
;
 /* UIO class infrastructure */
static struct class uio_class = 
    .name = "uio",// /sys/class/uio
    .dev_attrs = uio_class_attributes,
;
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,
;
/* Protect idr accesses */
static DEFINE_MUTEX(minor_lock);
static DEFINE_IDR(uio_idr);
//关于idr机制。參见 http://blog.csdn.net/ganggexiongqi/article/details/6737389
struct uio_device 
    struct module       *owner;
    struct device       *dev; //在__uio_register_device中初始化
    int         minor; // 次设备id号,uio_get_minor
    atomic_t        event; //中断事件计数
    struct fasync_struct    *async_queue;//该设备上的异步等待队列//
                                                               // 关于 “异步通知“ //參见LDD3第六章
    wait_queue_head_t   wait; //该设备上的等待队列,在注冊设备时(__uio_register_device)初始化
    int         vma_count;
    struct uio_info     *info;// 指向用户注冊的uio_info,在__uio_register_device中被赋值的
    struct kobject      *map_dir;
    struct kobject      *portio_dir;
;
/*
 * struct uio_info - UIO device capabilities
 * @uio_dev:        the UIO device this info belongs to
 * @name:       device name
 * @version:        device driver version
 * @mem:        list of mappable memory regions, size==0 for end of list
 * @port:       list of port regions, size==0 for end of list
 * @irq:        interrupt number or UIO_IRQ_CUSTOM
 * @irq_flags:      flags for request_irq()
 * @priv:       optional private data
 * @handler:        the device's irq handler
 * @mmap:       mmap operation for this uio device
 * @open:       open operation for this uio device
 * @release:        release operation for this uio device
 * @irqcontrol:     disable/enable irqs when 0/1 is written to /dev/uioX
 */
struct uio_info 
    struct uio_device   *uio_dev; // 在__uio_register_device中初始化
    const char      *name; // 调用__uio_register_device之前必须初始化
    const char      *version; //调用__uio_register_device之前必须初始化
    struct uio_mem      mem[MAX_UIO_MAPS];
    struct uio_port     port[MAX_UIO_PORT_REGIONS];
    long            irq; //分配给uio设备的中断号,调用__uio_register_device之前必须初始化
    unsigned long       irq_flags;// 调用__uio_register_device之前必须初始化
    void            *priv; //
    irqreturn_t (*handler)(int irq, struct uio_info *dev_info); //uio_interrupt中调用。用于中断处理
                                                                // 调用__uio_register_device之前必须初始化
    int (*mmap)(struct uio_info *info, struct vm_area_struct *vma); //在uio_mmap中被调用,
                                                                    // 运行设备打开特定操作
    int (*open)(struct uio_info *info, struct inode *inode);//在uio_open中被调用,运行设备打开特定操作
    int (*release)(struct uio_info *info, struct inode *inode);//在uio_device中被调用。运行设备打开特定操作
    int (*irqcontrol)(struct uio_info *info, s32 irq_on);//在uio_write方法中被调用,运行用户驱动的
                                                                                       //特定操作。
;

3.2 内核核心层(UIO内核驱动框架)

  • 实现类似字符设备注册那一套操作。
  • 实现UIO设备公共的操作。

3.3 在用户空间部分(*.so程序)

(1)用户空间主要操作

  • 控制硬件设备的寄存器。 
  • 如果用户空间要等待一个设备中断,它只需要简单的阻塞在对 /dev/uioX的read()操作上。当设备产生中断时,read()操作立即返回。
  • UIO 也实现了poll()系统调用,你可以使用  select()来等待中断的发生。select()有一个超时参数可以用来实现有限时间内等待中断。
  •  对设备的控制还可以通过/sys/class/uio下的各个文件的读写来完成。你注册的uio设备将会出现在该目录下。假如你的uio设备是uio0那么映射的设备内存文件出现在 /sys/class/uio/uio0/maps/mapX,对该文件的读写就是对设备内存的读写。

(2)用户空间程序案例


    int32_t irq_count;

    int fd = open("/dev/uio0", O_RDWR);

    /* Map the register regions to proccess's virtual memspace */
    void * access = mmap(NULL, 4096,
        // 寄存器的读写操作,可用过普通内存读写的方式完成
        PROT_READ | PROT_WRITE,
        MAP_SHARED, fd, 0);// [1]
    
    while (read(fd, &irq_count, 4) == 4) 
        
        printf("Interrupt number %d\\n", irq_count);
    

第4章 UIO的不足

(1)UIO不支持用户空间的DMA

用户空间采用的是逻辑地址,通过MMU映射到到物理地址。

在没有IOMMU的情况下,DMA使用的物理地址。

因此,UIO只能在用户空间操作硬件设备的寄存器空间,无法支持通过DMA把内核空间的数据传送到用户空间 ,只用通过用户空间的程序读取硬件设备寄存器空间的数据,因此通过DMA传输大流量数据的IO设备,如网卡、显卡等设备,无法使用UIO框架。

主要适合于低速设备,以及不需要通过DMA硬件设备与用户空间传递大量数据的场合。

(2)UIO不支持用户空间中断,中断必须在内核空间实现。

用户空间程序通过两种方式范围硬件设备:

  • 设备暴露(mmap)到用户空间的地址来访问设备
  • read/write/ioctl

(3)UIO不支持虚拟机设备

UIO不支持在虚拟机中访问物理机中的设备,更无非实现多个虚拟机共享共一个物理机的硬件设备。

以上是关于[架构之路-46]:目标系统 - 系统软件 - Linux OS硬件设备驱动-UIO用户空间IO驱动框架与用户空间协议栈的主要内容,如果未能解决你的问题,请参考以下文章

[架构之路-56]:目标系统 - 平台软件 - 总体架构概述

[架构之路-28]:目标系统 - 系统软件 - Linux OS内核功能架构图解内核构建内核启动流程

[架构之路-25]:目标系统 - 系统软件 - bootloader uboot内存映射与启动流程

[架构之路-21]:目标系统 - 系统软件 - 计算机系统架构计算机指令系统结构化程序与分层编程。

[架构之路-29]:目标系统 - 系统软件 - Linux OS内核以及内核驱动的调试技术

[架构之路-26]:目标系统 - 系统软件 - bootloader uboot使用方法常用命令