通俗易懂:mmap與硬體暫存器的關係
分析應用程式獲取按鍵操作的流程
分析應用程式控制LED燈的操作流程
按鍵:按鍵操作,觸發中斷,讀取硬體暫存器,獲取按鍵狀態,喚醒休眠的程序,read操作呼叫copy_to_user將核心的緩衝區資料拷貝到使用者緩衝區
總結:第一次拷貝:從硬體暫存器讀數讀到核心緩衝區
第二次拷貝:從核心緩衝區到使用者緩衝區
燈:第一次拷貝:使用者緩衝區到核心緩衝區,
第二次拷貝:從核心緩衝區到硬體暫存器
總結:以上兩個驅動都要經歷兩次的資料拷貝,如果資料量非常小,基本對系統的效能沒有影響,如果進行大資料量的處理,比如視訊資料採集卡,還要經過這兩次拷貝,無形之中降低了系統的效能。
簡單點來說:我們都知道,我們都是基於linux作業系統下程式設計的,凡是都要經過核心這一層,無論進行什麼操作,都要經過核心,如果應用程式想要訪問物理硬體,並不是直接訪問,比如有一個應用程式想要讀取硬體上的資訊,那麼硬體並不是把資料直接給應用程式,而是先把資料被核心,然後核心再把資料轉交給應用程式,也就是說,核心起到了中轉的作用,那麼有些時候,這樣做的話,就會降低系統的效能,為什麼這樣說呢?我打個比方,我們如果外接了個攝像頭之類的視訊器件,像視訊這麼大的資料量,如果應用程式想要讀取外部視訊這個硬體的話,如果按照以往的方式,就會先把幾百兆的視訊資料先交給核心,然後核心在轉交給應用程式,這樣的話如果視訊是2m的資料,這樣一搞,就會產生4m的資料。系統明線降低了效能,所以,有些時候,需要讓應用程式直接訪問操作硬體,那麼這就是本帖將要解決的問題,我們將使用mmap/select/poll函式把實體地址對映到程序內地址空間內,讓應用程式直接訪問硬體。此時核心有一個機制:mmap,mmap實現的原理就是將硬體的相關內容對映到當前使用者程序的某一個虛擬地址空間裡去,以後應用程式訪問這個虛擬地址就是在操作對應的實體地址資訊。
回顧以前UNIX的api程式設計:
void *gpio_base;
int fd=open("a.txt,O_RDWR");
gpio_base = mmap(NULL,對映的大小,讀寫許可權,fd,0);
這樣在使用者空間就得到了一個虛擬地址gpio_base,以後在使用者空間操作這個虛擬地址就是在操作這個檔案
問:應用程式呼叫mmap,底層核心驅動是否有對應的mmap介面?
答:在struct file_operations裡有
問:底層驅動的mmap作何事?
答:以前驅動在操作硬體暫存器都是將實體地址直接對映到核心的某一個虛擬地址(3g-4g),現在使 用mmap就是將這個實體地址對映到當前程序的某一個虛擬地址(0-3G),所以底層的mmap介面就 是完成實體地址和虛擬地址的對映,建立頁表。
當應用程式呼叫mmap系統呼叫時,核心在當前程序的虛擬地址空間裡找到一個記憶體區域來存放mmap操作的地址資訊,並且為這塊特殊的記憶體區域建立一個結構體來描述mmap使用的虛擬地址資訊(起始地址,結束地址,讀寫訪問許可權等),那麼底層的mmap介面的第二個引數為這個結構體指標,底層mmap只需通過這個指標得到對應程序的虛擬地址然後和實體地址建立對映關係即可。
struct vm_area_struct {
unsigned long vm_start;//應用程式呼叫mmap的返回值儲存在這個欄位
unsigned long vm_end;//結束地址
pgprot_t vm_page_prot;//讀寫許可權
。。。
}
底層驅動mmap:
1.首獲取實體地址
2.通過mmap的第二個形參獲取使用者程序的虛擬地址
vma.vm_start
3.目的就是建立1,2兩個地址的對映
呼叫remap_pfn_range函式建立對映即可
例子:改造led驅動,用mmap介面來實現led驅動
______________________________________________________________________________________
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//定義硬體私有結構體
struct led_hw_priv {
char *name;
int productid;
};
//定義驅動私有結構體
struct led_sf_priv {
struct cdev led_cdev;
unsigned long phy_addr;
int major;
struct class *cls;
struct led_hw_priv *pled_hw;
};
//初始化硬體相關的資訊
static struct led_hw_priv led_info = {
.name = "Tarena",
.productid = 0x654321
};
//初始化硬體資源結構
static struct resource led_res[] = {
[0] = {
.start = 0xe0200000, //頁對齊
.end = 0xe0200000 + 8 - 1,
.flags = IORESOURCE_MEM
}
};
static void led_release(struct device *dev)
{
printk("%s\n", __FUNCTION__);
}
//初始化platform_device
static struct platform_device led_dev = {
.name = "myled",
.id = -1,
.dev = {
.release = led_release,
.platform_data = &led_info
},
.num_resources = ARRAY_SIZE(led_res),
.resource = led_res
};
static int led_open(struct inode *inode,struct file *file)
{
struct led_sf_priv *pled = container_of(inode->i_cdev, struct led_sf_priv, led_cdev);
file->private_data = pled;
printk("Device Name is %s, Device ProductId = %#x\n",pled->pled_hw->name,pled->pled_hw->productid);
return 0;
}
static int led_mmap(struct file *file,struct vm_area_struct *vma)
{
struct led_sf_priv *pled = file->private_data;
int vmasize = vma->vm_end - vma->vm_start;
//修改屬性,保證資料的一致性,關閉cache
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
remap_pfn_range(vma, vma->vm_start, pled->phy_addr >> 12, vmasize,vma->vm_page_prot);
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.mmap = led_mmap
};
static int led_probe(struct platform_device *pdev)
{
struct resource *res = NULL;
dev_t dev_id;
struct led_sf_priv *pled = kzalloc(sizeof(struct led_sf_priv), GFP_KERNEL);
if (pled->major) {
dev_id = MKDEV(pled->major, 0);
register_chrdev_region(dev_id, 1, "led");
} else {
alloc_chrdev_region(&dev_id, 0, 1, "led");
pled->major = MAJOR(dev_id);
}
cdev_init(&pled->led_cdev, &led_fops);
cdev_add(&pled->led_cdev, dev_id, 1);
pled->cls = class_create(THIS_MODULE, "led");
device_create(pled->cls, NULL, dev_id, NULL, "myled");
pled->pled_hw = pdev->dev.platform_data;
res = platform_get_resource(pdev, IORESOURCE_MEM,0);
pled->phy_addr = res->start;
dev_set_drvdata(&pdev->dev, pled);
return 0;
}
static int led_remove(struct platform_device *pdev)
{
struct led_sf_priv *pled = dev_get_drvdata(&pdev->dev);
dev_t dev_id = MKDEV(pled->major, 0);
device_destroy(pled->cls, dev_id);
class_destroy(pled->cls);
cdev_del(&pled->led_cdev);
unregister_chrdev_region(dev_id, 1);
kfree(pled);
return 0;
}
//初始化platform_driver
static struct platform_driver led_drv = {
.driver = {
.name = "myled",
.owner = THIS_MODULE,
},
.probe = led_probe,
.remove = led_remove
};
static int led_init(void)
{
//註冊platform_device
platform_device_register(&led_dev);
//註冊platform_driver
platform_driver_register(&led_drv);
return 0;
}
static void led_exit(void)
{
platform_device_unregister(&led_dev);
platform_driver_unregister(&led_drv);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL v2");
______________________________________________________________________________________
以上程式碼就是將led燈所在介面的暫存器的地址對映到程序空間的虛擬地址的過程,下面開始編寫app
______________________________________________________________________________________
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd;
unsigned char *gpio_base;
unsigned long *gpiocon;
unsigned long *gpiodat;
if (argc != 2) {
printf("usage:\n%s \n", argv[0]);
return -1;
}
fd = open("/dev/myled", O_RDWR);
if (fd < 0) {
printf("open led failed.\n");
return -1;
}
gpio_base = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
//現在gpio_base這個虛擬地址就是暫存器所在的地址
gpiocon = (unsigned long *)(gpio_base + 0x80);
gpiodat = (unsigned long *)(gpio_base + 0x84);
//配置GPIO為輸出
*gpiocon &= ~(0xf << 12);
*gpiocon |= (1 << 12);
if (strcmp(argv[1], "on") == 0)
*gpiodat |= (1 << 3);//開燈操作
else
*gpiodat &= ~(1 << 3);//關燈
close(fd);
munmap(gpio_base, 0x1000);//這一步當然就是解除對映啦\(^o^)/~
return 0;
}
______________________________________________________________________________________
現在的手機的其實就是ARM處理器加外部各種模組為核心實現的,比如GPS模組,藍芽模組等等。比如GPS模組和s5pv210通訊的話,是採用串列埠通訊,wifi模組和s5pv210通訊是採用網路通訊,也就是網路套接字。我們驅動程式做的就是利用核心提供的底層介面函式來操作這些模組。然後應用程式呼叫驅動程式提供的函式來實現工作。現在我們以安卓手機為例,但凡手機都是嵌入式裝置。比如說一個安卓手機上面安裝了一款應用軟體,比如這個應用程式是QQ2013這個軟體,你想想看,QQ是不是支援WIFI功能?在搜尋附近好友的時候,是不是得要啟動手機內建的GPS模組?在使用者要聊天打字時,是不是要操作按鍵?等等,其實以上小編講的那些都是應用程式操作多個硬體裝置的典型例子,現在如果說有這樣一個應用軟體,要同時操作GPS(通過串列埠),按鍵,和wifi網路(通過socket)
首先是不是得先要開啟裝置?所以先用open函式開啟GPS的裝置檔案和其他驅動程式的裝置檔案:
int fd1 = open("/dev/GPS",O_RDWR);//開啟GPS定位模組的裝置檔案來訪問GPS驅動程式
int fd2 = open("/dev/button",O_RDWR);//開啟wifi模組的裝置檔案
int fd3 = socket(...);//開啟其他模組
開啟完裝置之後,接下來讀取三個裝置的資料,你想怎樣讀取?現在有三種方案:
方案1:
主程序:
while(1){
read(fd1,buf,size);//讀取GPS衛星傳來的定位資料
read(fd2,buf,size);//讀取鍵盤按鍵資料
read(fd3,buf,sise);//讀取wifi網路資料包
}
以上的方案是採用輪詢方式依次讀取資料,大家想想看,如果這個時候cpu執行到讀取GPS的時候,來了一個wifi網路資料包,這個時候是不是就會因為沒有讀取到wifi資料而發生丟包?所以這種方案在實際開發顯然是不行的,不然的話,你用qq聊天,可能有時候就會發生在使用騰訊提供的搜尋周圍好友服務的時候,別人正好發qq訊息給你,而這個時候,cpu正在執行讀取GPS資料而來不及讀取網路資料包,就會照成你沒有接受到別人發的qq訊息。所以這樣是不行的。
方案2:
針對方案1這種序列實現的問題,可以為每一個裝置的讀取操作建立一個子程序或者執行緒來輪詢的讀取操作,來實現並行。雖然能夠解決並行問題,但隨著裝置數量的增加,無形會浪費很多的系統資源
方案3:下一節中出現------>>>>使用selec/poll這個函式來實現IO多路監聽。而這個函式就是下節的內容。
因為使用selec/poll來實現同時操作多路資料(GPS,WIFI,藍芽等等)就不會發生因為各種問題而導致的丟失資料的可能。。。。