【原創】xenomai UDD介紹與UDD用戶態驅動示例

xenomai UDD與用戶態驅動示例

本文介紹xenomai UDD原理和相關代碼,並給出一個基於UDD的用戶態操作GPIO的示例,以及內核收發網絡包與用戶態操作網卡收發包的CPU耗時對比。

一、UDD介紹

大家可能在看xenomai源碼的時候注意到,driver目錄下有個不起眼的目錄udd,裏面僅一個.c文件,無任何示例,可能也不不知道它是幹什麼的,下面開始介紹它。

UDD全稱User-space Device Driver framework ,即用戶態設備驅動框架,即爲用戶態設備驅動提供的一種機制,用戶態驅動是什麼,有什麼用呢?

我們都知道,在Linux開發中,要操作一個硬件設備,比如一個GPIO、serial等,通常需要在內核開發對應的driver,然後再通過操作系統提供接口來實現對該硬件的訪問,這樣的好處是,操作系統屏蔽了底層實現,統一了與硬件設備的交互接口,方便我們的程序的可移植性。

但是,在一些嵌入式應用場合(我們的主題是xenomai,先以實時應用這個場景來分析~),我們每次訪問操作硬件,比如我要操作一個GPIO口輸出高低電平,都需要先切換到內核態,然後內核經過一系列子系統最終調用GPIO驅動實現高低電平輸出,從整個流程來看有哪些問題:

  • 用戶態內核態切換,CPU執行操作系統代碼需要佔用CPU資源,路徑顯得過於繁瑣
  • 路徑長意味着不確定性增加,進而影響信號輸出的實時性
  • 內核態用戶態之間數據需要拷貝
  • 用戶態內核態頻繁切換cache、TLB抖動

這時候我們會想,“要是能不通過操作系統內核,應用可以直接操作硬件進行輸出多好!” 爲了能夠實現該想法,xenomai爲我們提供了UDD,只需要我們在內核態通過UDD實現很少一部分,然後在用戶態實現GPIO相關驅動,達到用戶態應用程序直接操作硬件的目的。

UDD是xenomai特有的嗎?不是,在Linux中,這是2006年就存在的東西,叫UIO(Userspace I/O)即,運行在用戶空間的I/O技術,且應用廣泛。

舉個UIO應用例子,互聯網行業中,經典的 C10KC1000K 問題不斷解決(C10K 就是單機同時處理 1 萬個請求(併發連接 1 萬)的問題,而 C1000K 也就是單機支持處理 100 萬個請求(併發連接 100 萬)的問題),人們對於性能的要求是無止境的。再進一步,有沒有可能在單機中,同時處理 1000 萬的請求呢?即 C10M 問題。在 C10M 問題中,各種軟件、硬件的優化很可能都已經做到頭了。特別是當升級完硬件(比如足夠多的內存、帶寬足夠大的網卡、更多的網絡功能卸載等)後,你可能會發現,無論你怎麼優化應用程序和內核中的各種網絡參數,想實現 1000 萬請求的併發,都是極其困難的。

究其根本,還是 Linux 內核協議棧做了太多太繁重的工作。從網卡中斷帶來的硬中斷處理程序開始,到軟中斷中的各層網絡協議處理,最後再到應用程序,這個路徑實在是太長了,就會導致網絡包的處理優化,到了一定程度後,就無法更進一步了

要解決這個問題,最重要就是跳過內核協議棧的冗長路徑,把網絡包直接送到要處理的應用程序那裏去。這裏有兩種常見的機制,DPDK 和 XDP,其中的DPDK應用就是UIO技術,它跳過內核協議棧,利用UIO將硬件操作映射到用戶空間,在用戶態實現網卡驅動直接操作硬件,用戶態進程通過輪詢的方式,來處理網絡接收。這樣減少了頻繁的內核態用戶態上下文切換和數據拷貝,可以極大提高數據處理性能和吞吐量

注:DPDK不僅可以通過輪詢的方式來處理網絡接收,也可以通過中斷的方式來接收。

對於我們實時應用場合,也有同樣的需求,我們希望,在響應某個實時事件後能跳過內核,直接操作硬件更快的進行結果的輸出,同時節省內核路徑所需的CPU資源。既然UIO可以實現,所以2014年發佈的xenomai3中引入了適合xenomai RTDM的UIO機制,稱爲UDD。大家可以看到,xenomai官方源碼裏並沒有任何UDD示例驅動,大家可能不知道怎麼用,這也是爲什麼寫本文的原因。

我們介紹完UDD和UIO(UDD和UIO原理一致,使用上基本一致,後文沒有特殊說明均指UDD),接下來會分析UDD原理和相關代碼,最後給出一個基於UDD的用戶態操作GPIO的示例,以及對比用戶態操作網卡收發包的CPU耗時。

二、UDD原理及框架

1. 內存映射

首先我們要思考如何實現在用戶態實現直接操作硬件?

以GPIO爲例,回想MCU裸機點燈的時候,我們操作GPIO輸出高低電平是通過寫指定寄存器來完成的,這些寄存器位於MCU物理尋址範圍的固定地址處。回到我們Linux應用,由於操作系統和MMU的存在,每個進程讀寫的是虛擬地址,如果需要訪問指定的物理內存地址,就需要操作系統爲該進程添加物理內存到進程虛擬內存之間的映射表,同時設置該片內存的訪問權限和一些標識。

所以要在用戶態實現硬件的直接操作,UDD需要提供一個機制將硬件所在物理地址映射到進程地址空間,實現用戶態對這些物理地址的讀寫操作。

對嵌入式linux開發稍有經驗的朋友可能都知道,將物理內存到進程虛擬內存的映射可以通過對設備節點/dev/mem使用O_SYNC標識打開,進行mmap()函數操作就能做到,那還要UDD幹嘛?是的如果我們只是訪問物理內存,比如通過GPMC進行外部ram數據讀寫,並沒有用到UDD或UIO就可完成。其實是因爲這些外設沒有涉及設備中斷,UDD存在的主要目的是提供中斷通知處理機制,接下來我們看看中斷。

2. 中斷處理

基本所有的外設(網卡、SPI、MMC...)都需要中斷來通知CPU處理。通常,OS運行在CPU的高特權級,中斷產生後的處理也是在高特權級,linux應用程序運行在CPU低特權級的用戶態,所以用戶態的驅動無法直接處理產生的中斷,這就需要內核態輔助實現中斷的控制處理,這是UDD的一個重要機制。

前幾個月,記得在哪裏看到過有開發者往upstream推用戶態中斷處理機制的代碼,最近找不到了。

UDD與UIO的區別

中斷響應時間是RTOS實時性的重要指標 ,對於一個實時外設設備,如果我們在用戶態空間來驅動它,那它產生中斷如何快速響應(響應時間確定)?如果使用linux UIO,前面文章說過,linux的中斷響應時間不是確定的,那就無實時性可言了,不能使用linux UIO。

這就是xenomai UDD存在的原因,UDD保證了實時設備中斷響應的實時性,UDD與UIO主要區別在於中斷的處理和通知機制,UDD基於RTDM和xenomai調度,全路徑爲實時上下文。

需要說明的是:如果你的實時設備僅作爲輸出,比如驅動GPIO輸出一個電平,並無中斷需求,那麼使用UDD還是UIO或/dev/mem無任何區別

另外,有朋友問我,一些驅動不開源的PCI控制卡/採集卡/數字輸出卡,xenomai能不能用?通常我們使用一個外設作爲實時設備時,需要專門的爲這個設備編寫xenomai實時驅動程序。但是,一般這些驅動不開源的PCI控制卡,它的驅動大部分在用戶態實現,以庫的方式提供,內核部分僅做一些io映射(這部分開源,與UIO原來一致), 這類板卡如果對中斷處理依賴不高,或者你的應用用不到它中斷相關的部分,那麼是直接可以在xenomai上創建實時任務調用它給的庫來使用的。

3. linux UIO與xenomai UDD框架對比

3.1 UIO機制

linux UIO框架如下:

img

內核態UIO Framework爲UIO 設備Driver提供機制和穩定接口,

  • 用戶態驅動通過mmap函數實現對/dev/uioX驅動中mmap方法完成物理地址到進程地址空間的映射。
  • 異步IOpoll/epoll()select()完成內核態中斷到用戶的通知,結合read()等待和處理。

3.2 UDD機制

linux UIO框架如下:

內核態UIO Framework爲UIO 設備Driver提供機制和穩定接口,

  • 用戶態驅動通過mmap函數實現對/dev/uioX驅動中mmap方法完成物理地址到進程地址空間的映射。
  • xenomai只支持poll來等待中斷和處理,結合read()進行中斷應答。

三、UDD應用示例

1. UDD GPIO操作

以ti am335x爲例實現用戶態GPIO操作。內核模塊代碼如下:

#include <linux/module.h>
#include <linux/types.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <rtdm/driver.h>
#include <rtdm/udd.h>
#include <linux/platform_device.h>

#define OMAP_MAX_GPIO           192

#define AM33XX_GPIO0_BASE       0x44E07000	/*gpio0 物理起始地址*/
#define AM33XX_GPIO1_BASE       0x4804C000	/*gpio1 物理起始地址*/
#define AM33XX_GPIO2_BASE       0x481AC000	/*gpio2 物理起始地址*/
#define AM33XX_GPIO3_BASE       0x481AE000  /*gpio3 物理起始地址*/

#define RTDM_SUBCLASS_OMAP_GPIO       0
#define DEVICE_NAME                 "udd_gpio"

MODULE_DESCRIPTION("UDD driver for OMAP3 GPIO");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wsg1100");

static struct udd_device device = {
    .device_name = DEVICE_NAME,
    .device_flags               =       RTDM_NAMED_DEVICE|RTDM_EXCLUSIVE,
    .device_subclass = RTDM_SUBCLASS_OMAP_GPIO,
    .mem_regions[0].name = "gpio0_addr",
    .mem_regions[0].addr = AM33XX_GPIO0_BASE,
    .mem_regions[0].len = 4096,
    .mem_regions[0].type = UDD_MEM_PHYS,

    .mem_regions[1].name = "gpio1_addr",
    .mem_regions[1].addr = AM33XX_GPIO1_BASE,
    .mem_regions[1].len = 4096,
    .mem_regions[1].type = UDD_MEM_PHYS,

    .mem_regions[2].name = "gpio2_addr",
    .mem_regions[2].addr = AM33XX_GPIO2_BASE,
    .mem_regions[2].len = 4096,
    .mem_regions[2].type = UDD_MEM_PHYS,

    .mem_regions[3].name = "gpio3_addr",
    .mem_regions[3].addr = AM33XX_GPIO3_BASE,
    .mem_regions[3].len = 4096,
    .mem_regions[3].type = UDD_MEM_PHYS,
};

static int udd_gpio_probe(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "mem region len: %d\n",device.mem_regions[0].len);
    dev_info(&pdev->dev, "mem region addr: 0x%08lx\n",device.mem_regions[0].addr);
    dev_info(&pdev->dev, "mem region len: %d\n",device.mem_regions[1].len);
    dev_info(&pdev->dev, "mem region addr: 0x%08lx\n",device.mem_regions[1].addr);
    dev_info(&pdev->dev, "mem region len: %d\n",device.mem_regions[2].len);
    dev_info(&pdev->dev, "mem region addr: 0x%08lx\n",device.mem_regions[2].addr);
    dev_info(&pdev->dev, "mem region len: %d\n",device.mem_regions[3].len);
    dev_info(&pdev->dev, "mem region addr: 0x%08lx\n",device.mem_regions[3].addr);

    return udd_register_device (&device);
}

static int udd_gpio_remove(struct platform_device *pdev)
{
        udd_unregister_device (&device);

        return 0;
}

static const struct of_device_id udd_gpio_ids[] = {
        { .compatible = "ti,omap3-udd-gpio" },
        {},
};

MODULE_DEVICE_TABLE(of, udd_gpio_ids);

static struct platform_driver udd_gpio_platform_driver = {
        .driver = {
                .name           = DEVICE_NAME,
                .of_match_table = udd_gpio_ids,
        },
        .probe                  = udd_gpio_probe,
        .remove                 = udd_gpio_remove,
};

int __init omap_gpio_init(void)
{
        return platform_driver_register(&udd_gpio_platform_driver);
}

void __exit omap_gpio_exit(void)
{
    return platform_driver_unregister(&udd_gpio_platform_driver);
}

module_init(omap_gpio_init);
module_exit(omap_gpio_exit);

用戶態代碼詳見gitee:

注意: 由於中斷級聯,對於每個bank GPIO控制器下的每個GPIO來說,它們產生中斷後,不能直接通知GIC,而是先通知GPIO中斷控制器,然後gpio控制器再通過SPI通知GIC,然後GIC會通過irq或者firq觸發某個CPU中斷。每個UDD設備驅動只能註冊處理一箇中斷處理,如果爲每個gpio都註冊中斷,處理將很麻煩,所以這裏將全部gpio通過一個UDD設備註冊,沒有通過UDD註冊中斷,所以只作爲輸入輸出,沒有中斷事件的處理。

2. 網絡包收發

實時工業以太網應用廣發,通常實時工業以太網基於實時操作系統來實現。實時操作系統在操作系統調度層面保證了事件響應的實時性,但事件的響應結果輸出,依賴操作系統以太網的實時性,這涉及操作系統以太網硬件驅動具體實現、操作系統網絡協議棧等

以EtherCAT工業以太網(二層網絡)爲例,高檔的數控系統爲獲得更高的加工精度,需要很短的插補週期(比如500us、125us、62.5us),即系統調度-插補運算-協議棧處理-網絡輸出-幀傳輸延時四者所需總時間需要在一個插補週期內完成,這需要系統各部分具有很強的實時性,其中幀傳輸延時是確定的,與以太網絡速率相關;系統調度與操作系統實時性相關;網絡輸出是重要的一環,不僅要求輸出時間確定,且網絡輸出CPU佔用儘可能小,這樣能留出更多的CPU時間用於插補控制運算。

最直接的方案是通過操作系統提供的原始套接字(Raw Socket)接口進行鏈路層以太網幀的收發,但是存在文章開頭說的問題(RTNet協議棧相比Linux網絡協議棧已經足夠簡單,執行路徑足夠確定,仍存在上下文切換等開銷,間後文測試)。

這裏我們使用UDD來編寫用戶態網卡驅動,直接驅動硬件進行EtherCAT數據收發,下面是做的簡單對比測試。

實時任務定時發幀間隔爲250us,總次數爲10萬次,EtherCAT數據幀長度約1200字節,其內容爲重複的0x130報文(第一個報文M位爲1),以下爲收發CPU耗時統計,其中發送時間包括協議棧組幀-拷貝-發送,接收時間包括接收-協議棧解析,其中,接收時協議棧只處理第一個報文,可簡單認爲處理時間恆定,在此基礎上我們來看耗時對比。

UDD 時間分佈分佈如下:

# 00:00:00 (recv, priority 79)
#    ---- min|   ---- avg|   ---- max|
#       0.603|      0.757|      4.035|
0 1
0.5 95841
1.5 2909
2.5 1234
3.5 19
4.5 2
5 1
# 00:00:00 (send, priority 79)
#    ---- min|   ---- avg|   ---- max|
#       0.096|      0.433|      3.476|
0 1
0.5 97127
1.5 2786
2.5 88
3.5 4
4 1

RTNET PF_PACKET時間分佈分佈如下:

# 00:00:00 (recv, priority 79)
#    ---- min|   ---- avg|   ---- max|
#       1.356|      1.607|      6.248|
1 1
1.5 96610
2.5 1374
3.5 1743
4.5 258
5.5 19
6.5 2
7 1
# 00:00:00 (send, priority 79)
#    ---- min|   ---- avg|   ---- max|
#       1.053|      1.460|      4.755|
1 1
1.5 97318
2.5 2408
3.5 269
4.5 10
5 1

rtnet-euio

需要注意的是,這裏主要測試需要的CPU時間差異,我們認爲函數執行完畢即爲網絡包已發送到網線上,但是你需要清楚,raw packet一般受操作內核和網絡協議棧機制的影響,函數執行完不一定代表已操作硬件發送(這裏使用的是xenomai rtnet,不存在該問題)。另外,就算用戶態驅動直接操作了網卡硬件,但是數據的傳輸還是受DMA不確定性的影響,實際網絡包出現在網線上也會有偏差。本人沒有硬件時間戳抓包器,否則可以對比他們實際到網線上的差異,那纔是實時性的真實體現。

四 總結

1. 作用

將硬件操作映射到用戶空間,用戶態直接操作硬件,減少用戶態與內核態之間的數據拷貝與交互。

2. 優點

實時性方面,由於直接在用戶態操作硬件,減少了系統調用路徑中的不確定性。

性能方面,減少內核頁表切換開銷、cache換入換出等,釋放部分CPU資源,提高CPU性能,同時降低抖動延遲。

調試方面,解決內核態容易造成系統崩潰死機等問題。

3. 注意事項

  1. 適用重輸出場景,中斷少的場景,如網卡發包、IO輸出。
  2. UDD用戶態驅動中斷的處理比內核態驅動中斷處理路徑長,需要權衡內核態與用戶態的收益進行選擇。
  3. 無中斷情況下,UDD與UIO一致,xenomai可直接使用基於UIO的linux用戶態驅動。

linux有多重網絡包收發的方式,後續從實時的角度寫一篇文章看看一下這些方式,敬請關注。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章