uio淺析

1uio理論部分


1.1爲什麼出現了UIO?
   硬件設備可以根據功能分爲網絡設備,塊設備,字符設備,或者根據與CPU相連的方式分爲PCI設備,USB設備等。它們被不同的內核子系統支持。這些標準的設備的驅動編寫較爲容易而且容易維護。很容易加入主內核源碼樹。但是,又有很多設備難以劃分到這些子系統中,比如I/O卡,現場總線接口或者定製的FPGA。通常這些非標準設備的驅動被實現爲字符驅動。這些驅動使用了很多內核內部函數和宏。而這些內部函數和宏是變化的。這樣驅動的編寫者必須編寫一個完全的內核驅動,而且一直維護這些代碼。而且這些驅動進不了主內核源碼。於是就出現了用戶空間I/O框架(Userspace I/O framework)。-


1.2UIO 是怎麼工作的?
一個設備驅動的主要任務有兩個:
1. 存取設備的內存
2. 處理設備產生的中斷

對於第一個任務,UIO 核心實現了mmap()可以處理物理內存(physical memory),邏輯內存(logical memory), 虛擬內存(virtual memory)。UIO驅動的編寫是就不需要再考慮這些繁瑣的細節。
     第二個任務,對於設備中斷的應答必須在內核空間進行。所以在內核空間有一小部分代碼用來應答中斷和禁止中斷,但是其餘的工作全部留給用戶空間處理。
     如果用戶空間要等待一個設備中斷,它只需要簡單的阻塞在對 /dev/uioX的read()操作上。 當設備產生中斷時,read()操作立即返回。UIO 也實現了poll()系統調用,你可以使用 select()來等待中斷的發生。select()有一個超時參數可以用來實現有限時間內等待中斷。
對設備的控制還可以通過/sys/class/uio下的各個文件的讀寫來完成。你註冊的uio設備將會出現在該目錄下。 假如你的uio設備是uio0那麼映射的設備內存文件出現在 /sys/class/uio/uio0/maps/mapX,對該文件的讀寫就是 對設備內存的讀寫。

      如下的圖描述了uio驅動的內核部分,用戶空間部分,和uio 框架以及內核內部函數的關係

Center

Figure 1: uio_architecture

     詳細的UIO驅動的編寫可以參考 drivers/uio/下的例子,以及Documentation/DocBook/uio-howto.tmp//tmpl格式的文件可以藉助 docbook-utils (debian下)工具轉化爲pdf或者html合格等。


1.3 “uio 核心的實現 和 uio驅動的內核部分的關係“詳談
重要的結構:

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方法中被調用,執行用戶驅動的/特定操作。

};先看一個uio 核心和 uio 設備之間關係的圖,有個整體印象:

Center

    uio核心部分是一個名爲"uio"的字符設備(下文稱爲“uio核心字符設備“)。用戶驅動的內核部分使用uio_register_device向uio核心部分 註冊uio設備。uio 核心的任務就是管理好這些註冊的uio設備。這些uio設備使用的數據結構是  uio_device。而這些設備屬性,比如name, open(), release()等操作都放在了uio_info結構中,用戶使用 uio_register_device註冊這些驅動之前 要設置好uio_info。
   uio核心字符設備註冊的 uio_open ,uio_fasync, uio_release, uio_poll, uio_read , uio_write 中除了完成相關的維護工作外,還調用了註冊在uio_info中的相關方法。比如,在 uio_open中調用了uio_info中註冊的open方法。
那麼這裏有一個問題,uio核心字符設備怎麼找到相關設備的uio_device結構的呢?
   這就涉及到了內核的idr機制,關於該機制可以參考:
   http://blog.csdn.net/ganggexiongqi/article/details/6737389
    在uio.c中,有如下的定義:
static DEFINE_IDR(uio_idr);
 /* Protect idr accesses */
static DEFINE_MUTEX(minor_lock);
    在你調用uio_register_device(內部調用了__uio_register_device)註冊你的uio 設備時,在__uio_register_device中調用了uio_get_minor函數,在uio_get_minor函數中,利用idr機制(idr_get_new)建立了次設備號和uio_device類型指針之間的聯繫。而uio_device指針指向了代表你註冊的uio設備的內核結構。在uio核心字符設備的打開方法,uio_open中先取得了設備的次設備號(iminor(inode)),再次利用idr機制提供的方法(idr_find)取得了對應的uio_device類型的指針。並且把該指針保存在了uio_listener結構中,以方便以後使用。
1.4關於設備中斷的處理
   在__uio_register_device中,爲uio設備註冊了統一的中斷處理函數uio_interrupt,在該函數中,調用了uio設備自己提供的中斷處理函數handler(uio_info結構中)。並調用了uio_event_notify函數對uio設備的中斷事件計數器增一, 通知各個讀進程“有數據可讀”。每個uio設備的中斷處理函數都是單獨註冊的。


 關於中斷計數: uio_listener
            struct uio_listener {
                struct uio_device *dev; //  保存uio設備的指針,便於訪問
               s32 event_count; //跟蹤uio設備的中斷事件計數器
           };
        對於每一個註冊的uio 設備(uio_device), 都關聯一個這樣的結構。它的作用就是跟蹤每個uio設備(uio_device)的中斷事件計數器值。在用戶空間進行文件打開操作(open)時,與uio設備關聯的uio_listener結構就被分配,指向它的指針被保存在filep指針的private_data字段以供其他操作使用。
      在用戶空間執行文件關閉操作時,和uio設備關聯的uio_listener結構就被銷燬。在uio設備註冊時,uio core會爲設備註冊一個通用的中斷處理函數(uio_interrupt),在該函數中,會調用uio設備自身的中斷處理函數(handler). 中斷髮生時,uio_event_notify將被調用,用來對設備的中斷事件計數器()增一,並通知各讀進程,有數據可讀。uio_poll 操作判斷是否有數據可讀的依據就是 listener中的中斷事件計數值(event_count)和uio設備中的中斷事件計數器值不一致(前者小於後者)。因爲listener的值除了在執行文件打開操作時被置爲被賦值外,只在uio_read操作中被更新爲uio設備的中斷事件計數器值。    

 疑問1:
對於中斷事件計數器,uio_device中定義爲 atomic_t 類型,又有
            typedef struct {
                int counter;
            } atomic_t;
   需不需要考慮溢出問題?
  同樣的問題存在在uio_listener的event_count字段。

   關於uio_device的event字段 uio_howto中:
  event: The total number of interrupts handled by the driver since the last time the device node
               was read.
   【如果中斷事件產生的頻率是100MHZ的話,(2^32)/(10^8) = 42 秒 】counter計數器就會溢出。所以,依賴於counter的操作可能會出現問題。//補充:中斷髮生的頻率最多爲kHz不會是 Mhz,所以[]中的假設是不合理的,但是溢出會發生,而且,依賴counter值的應用可能會出現問題!!

   我們可以添加一個timer,在timer 處理函數中,調用uio_event_notify增加counter的值,
  很快會觀察到溢出。<<<<<<< 例子,還沒有寫 (^_^)
   //其實,可以在我們註冊的函數中,得到uio_device的指針,可以直接修改event的值。



   ===========關於 sysfs文件創建
  sysfs下uio相關的文件結構如下
sys  
 ├───uio  
       ├───uio0  
       │     ├───maps  
       │          ├───mapX  
       ├───uio1  
             ├───maps                          
             │    ├───mapX        
             ├───portio  
                  ├───portX  




     其中的uio是uio模塊加載時,uio_init調用init_uio_class調用class_register註冊到內核空間的。關於這個類的方法有個疑問,就是比如在show_event方法中,struct uio_device *idev = dev_get_drvdata(dev);//具體的uio設備相關的信息這個uio_device相關的信息是怎麼跟 uio class聯繫上的?
    在調用__uio_register_device註冊uio設備時,通過
   idev->dev = device_create(&uio_class, parent,
                  MKDEV(uio_major, idev->minor), idev,
                  "uio%d", idev->minor);
     其中,idev就是 uio_device類型的指針,它作爲drvdata被傳入,device_create調用了device_create調用了device_create_vargs調用了dev_set_drvdata。
    這樣在uio class的 show_event方法中,就可以使用
    struct uio_device *idev = dev_get_drvdata(dev);
     得到了uio設備的結構體的指針。
    device_create調用完畢後在 /sys/class/uio/下就會出現 代表uio設備的uioX文件夾,
    其中X爲uio設備的次設備號。


2UIO編寫實例
  怎麼編寫uio驅動詳解
======================
   爲了用最簡單的例子說明問題,我們在我們uio驅動的內核部分只映射了一塊1024字節的邏輯內存。沒有申請中斷。這樣加載上我們的驅動後,將會在/sys/class/uio/uio0/maps/map0中看到addr,name, offset, size。他們分別是映射內存的起始地址, 映射內存的名字,起始地址的頁內偏移, 映射內存 的大小。 在uio驅動的用戶空間部分,我們將打開addr, size以便使用映射好的內存。
[plain] view plaincopy
/**  
*  This is a simple demon of uio driver.  
*  Last modified by  
        09-05-2011   Joseph Yang(Yang Honggang)<[email protected]>  
*  
* Compile:    
*   Save this file name it simple.c  
*   # echo "obj-m := simple.o" > Makefile  
*   # make -Wall -C /lib/modules/`uname -r`/build M=`pwd` modules  
* Load the module:  
*   #modprobe uio  
*   #insmod simple.ko  
*/  

#include <linux/module.h>  
#include <linux/platform_device.h>  
#include <linux/uio_driver.h>  
#include <linux/slab.h> /* kmalloc, kfree */  
struct uio_info kpart_info = {  
        .name = "kpart",  
        .version = "0.1",  
        .irq = UIO_IRQ_NONE,  
};  

static int drv_kpart_probe(struct device *dev);  
static int drv_kpart_remove(struct device *dev);  
static struct device_driver uio_dummy_driver = {  
        .name = "kpart",  
        .bus = &platform_bus_type,  
        .probe = drv_kpart_probe,  
        .remove = drv_kpart_remove,  
};  

static int drv_kpart_probe(struct device *dev)  
{  
        printk("drv_kpart_probe( %p)\n", dev);  
        kpart_info.mem[0].addr = (unsigned long)kmalloc(1024,GFP_KERNEL);  

        if(kpart_info.mem[0].addr == 0)  
                return -ENOMEM;  
        kpart_info.mem[0].memtype = UIO_MEM_LOGICAL;  
        kpart_info.mem[0].size = 1024;  

        if( uio_register_device(dev, &kpart_info))  
                return -ENODEV;  
        return 0;  
}  

static int drv_kpart_remove(struct device *dev)  
{  
    uio_unregister_device(&kpart_info);  

    return 0;  
}  

static struct platform_device * uio_dummy_device;  

static int __init uio_kpart_init(void)  
{  
        uio_dummy_device = platform_device_register_simple("kpart", -1,  
                        NULL, 0);  

        return driver_register(&uio_dummy_driver);  
}  

static void __exit uio_kpart_exit(void)  
{  
        platform_device_unregister(uio_dummy_device);  
        driver_unregister(&uio_dummy_driver);  
}  

module_init(uio_kpart_init);  
module_exit(uio_kpart_exit);  

MODULE_LICENSE("GPL");  
MODULE_AUTHOR("Benedikt Spranger");  
MODULE_DESCRIPTION("UIO dummy driver");  


 這個文件是我們uio驅動的內核部分。下面做下簡要解釋。
一個uio驅動的註冊過程簡單點說有兩個步驟:
  1. 初始化設備相關的 uio_info結構。
  2. 調用uio_register_device 分配並註冊一個uio設備。

 uio驅動必須要提供並初始化的結構 uio_info, 它包含了您要註冊的uio_device的重要特性。
struct uio_info kpart_info = {
        .name = "kpart",
        .version = "0.1",
        .irq = UIO_IRQ_NONE,//我們沒有使用中斷,所以初始爲UIO_IRQ_NONE
};
當你沒有實際的硬件設備,但是,還想產生中斷的話,就可以把irq設置爲UIO_IRQ_CUSTOM,
並初始化uio_info的handler字段,那麼在產生中斷時,你註冊的中斷處理函數將會被調用。
如果有實際的硬件設備,那麼irq應該是您的硬件設備實際使用的中斷號。


   然後,在drv_kpart_probe函數(先不要管爲什麼在這個函數中進行這些操作)中,完成了
kpart_info.mem[0].addr, kpart_info.mem[0].memtype,kpart_info.mem[0].size字段的初始化。這是內存映射必須要設置的。
   其中,  kpart_info.mem[0].memtype可以是 UIO_MEM_PHYS,那麼被映射到用戶空間的是你設備的物理內存。也可以是UIO_MEM_LOGICAL,那麼被映射到用戶空間的是邏輯內存(比如使用 kmalloc分配的內存)。還可以是UIO_MEM_VIRTUAL,那麼被映射到用戶空間的是虛擬內存(比如用vmalloc分配的內存).這樣就完成了uio_info結構的初始化。
   下一步,還是在drv_kpart_probe中,調用uio_register_device完成了uio設備向uio core的註冊。
   下面講一下,爲什麼我們的設備跟platform之類的東東扯上了關係?
    我們的驅動不存在真正的物理設備與之對應。而 Platform  驅動“自動探測“,這個特性是我們在沒有實際硬件的情況下需要的。
   先從uio_kpart_init開始分析。
1 platform_device_register_simple的調用創建了一個簡單的platform設備。
2註冊device_driver類型的uio_dummy_driver變量到bus。
這裏需要注意一個問題,就是 device_driver結構中的name爲“kpart", 我們創建的platform設備名稱也是"kpart"。而且先創建platform設備,後註冊驅動。這是因爲,創建好設備後,註冊驅動時,驅動依靠name來匹配設備。之後 drv_kpart_probe就完成了uio_info的初始化和uio設備的註冊。


--------------- user_part.c ----------------------
這個是uio驅動的用戶空間部分。
[plain] view plaincopy
#include <stdio.h>  
#include <fcntl.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/mman.h>  
#include <errno.h>  

#define UIO_DEV "/dev/uio0"  
#define UIO_ADDR "/sys/class/uio/uio0/maps/map0/addr"  
#define UIO_SIZE "/sys/class/uio/uio0/maps/map0/size"  

static char uio_addr_buf[16], uio_size_buf[16];  

int main(void)  
{  
  int uio_fd, addr_fd, size_fd;  
  int uio_size;  
  void* uio_addr, *access_address;  

  uio_fd = open(UIO_DEV, /*O_RDONLY*/O_RDWR);  
  addr_fd = open(UIO_ADDR, O_RDONLY);  
  size_fd = open(UIO_SIZE, O_RDONLY);  
  if( addr_fd < 0 || size_fd < 0 || uio_fd < 0) {  
       fprintf(stderr, "mmap: %s\n", strerror(errno));  
       exit(-1);  
  }  
  read(addr_fd, uio_addr_buf, sizeof(uio_addr_buf));  
  read(size_fd, uio_size_buf, sizeof(uio_size_buf));  
  uio_addr = (void*)strtoul(uio_addr_buf, NULL, 0);  
  uio_size = (int)strtol(uio_size_buf, NULL, 0);  

  access_address = mmap(NULL, uio_size, PROT_READ | PROT_WRITE,  
                     MAP_SHARED, uio_fd, 0);  
  if ( access_address == (void*) -1) {  
      fprintf(stderr, "mmap: %s\n", strerror(errno));  
      exit(-1);  
  }  
  printf("The device address %p (lenth %d)\n"  
         "can be accessed over\n"  
         "logical address %p\n", uio_addr, uio_size, access_address);  
 //讀寫操作  
/*  
 access_address = (void*)mremap(access_address, getpagesize(), uio_size + getpagesize() + 11111, MAP_SHARED);  

  if ( access_address == (void*) -1) {  
      fprintf(stderr, "mremmap: %s\n", strerror(errno));  
      exit(-1);  
  }  
  printf(">>>AFTER REMAP:"  
         "logical address %p\n", access_address);  
*/  
  return 0;  
}  
-------------------------------------------------------
加載uio模塊
#modprobe uio


加載simple.ko
#insmod simple.ko
# ls /dev/ | grep uio0
   uio0
# ls /sys/class/uio/uio0
   ...
如果相應的文件都存在,那麼加載用戶空間驅動部分。
#./user_part
The device address 0xee244400 (lenth 1024)
can be accessed over
logical address 0xb78d4000


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