使用者態驅動--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