1. 程式人生 > >使用者態驅動--UIO機制的實現【轉】

使用者態驅動--UIO機制的實現【轉】

轉自:https://blog.csdn.net/u013982161/article/details/51584900

1 uio理論部分

 

1.1為什麼出現了UIO?

硬體裝置可以根據功能分為網路裝置,塊裝置,字元裝置,或者根據與CPU相連的方式分為PCI裝置,USB裝置等。它們被不同的核心子系統支援。這些標準的裝置的驅動編寫較為容易而且容易維護。很容易加入主核心原始碼樹。但是,又有很多裝置難以劃分到這些子系統中,比如I/O卡,現場匯流排介面或者定製的FPGA。通常這些非標準裝置的驅動被實現為字元驅動。這些驅動使用了很多核心內部函式和巨集。而這些內部函式和巨集是變化的。這樣驅動的編寫者必須編寫一個完全的核心驅動,而且一直維護這些程式碼。而且這些驅動進不了主核心原始碼。於是就出現了使用者空間I/O框架(Userspace I/O framework)。

1.2 UIO 是怎麼工作的?

一個裝置驅動的主要任務有兩個:

1. 存取裝置的記憶體

2. 處理裝置產生的中斷

對於第一個任務,UIO核心實現了mmap()可以處理實體記憶體(physicalmemory),邏輯記憶體(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 框架以及核心內部函式的關係。

 

 

 

詳細的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; //中斷事件計數
   //該裝置上的非同步等待佇列,關於 “非同步通知“參見LDD3第六章

struct fasync_struct   *async_queue;

//該裝置上的等待佇列,在註冊裝置時(__uio_register_device)初始化

wait_queue_head_t   wait;

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:        theUIO device this info belongs to
 * @name:       device name
 * @version:       device driver version
 * @mem:        list ofmappable memory regions, size==0 for end of list
 * @port:       list of portregions, size==0 for end of list
 * @irq:       interrupt number or UIO_IRQ_CUSTOM
 * @irq_flags:      flagsfor request_irq()
 * @priv:       optionalprivate data
 * @handler:        thedevice's irq handler
 * @mmap:       mmapoperation for this uio device
 * @open:       openoperation 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裝置之間關係的圖,有個整體印象:

 

 

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中,有如下的定義:

staticDEFINE_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設備註冊時,uiocore會為設備註冊一個通用的中斷處理函式(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 handledby 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裝置的次裝置號。

 

2 UIO編寫例項

 

怎麼編寫uio驅動詳解

   為了用最簡單的例子說明問題,我們在我們uio驅動的核心部分只映射了一塊1024位元組的邏輯記憶體。沒有申請中斷。這樣載入上我們的驅動後,將會在/sys/class/uio/uio0/maps/map0中看到addr,name, offset, size。他們分別是對映記憶體的起始地址,對映記憶體的名字,起始地址的頁內偏移,對映記憶體的大小。在uio驅動的使用者空間部分,我們將開啟addr, size以便使用對映好的記憶體。

[plain] viewplaincopy
/**  
*  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的重要特性。

structuio_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裝置向uiocore的註冊。下面講一下,為什麼我們的裝置跟platform之類的東東扯上了關係?

 

我們的驅動不存在真正的物理裝置與之對應。而 Platform  驅動“自動探測“,這個特性是我們在沒有實際硬體的情況下需要的。

 

先從uio_kpart_init開始分析。

1platform_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 beaccessed over\n"  
         "logicaladdress %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:" 
         "logicaladdress %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


 

http://blog.csdn.net/wenwuge_topsec/article/details/9628409