linux 設(shè)備驅(qū)動(dòng)編程
驅(qū)動(dòng)
Linux系統(tǒng)支持三種類型的硬件設(shè)備:字符設(shè)備、塊設(shè)備和網(wǎng)絡(luò)設(shè)備。
字符設(shè)備是直接讀取的,不必使用緩沖區(qū)。例如,系統(tǒng)的串行口/dev/cua0和/dev/cua1。塊設(shè)備每次只能讀取一定大小的塊的倍數(shù),通常一塊是512或者1024字節(jié)。塊設(shè)備通過(guò)緩沖區(qū)讀寫,并且可以隨機(jī)地讀寫。塊設(shè)備可以通過(guò)它們的設(shè)備文件存取,但通常是通過(guò)文件系統(tǒng)存取。只有塊設(shè)備支持掛接的文件系統(tǒng)。網(wǎng)絡(luò)設(shè)備是通過(guò)BSD套接字界面存取的。
Linux系統(tǒng)支持多種設(shè)備,這些設(shè)備的驅(qū)動(dòng)程序之間有一些共同的特點(diǎn):
* 內(nèi)核代碼:設(shè)備驅(qū)動(dòng)程序是系統(tǒng)內(nèi)核的一部分,所以如果驅(qū)動(dòng)程序出現(xiàn)錯(cuò)誤的話,將可能嚴(yán)重地破壞整個(gè)系統(tǒng)。
* 內(nèi)核接口:設(shè)備驅(qū)動(dòng)程序必須為系統(tǒng)內(nèi)核或者它們的子系統(tǒng)提供一個(gè)標(biāo)準(zhǔn)的接口。例如,一個(gè)終端驅(qū)動(dòng)程序必須為L(zhǎng)inux內(nèi)核提供一個(gè)文件I/O接口;一個(gè)SCSI設(shè)備驅(qū)動(dòng)程序應(yīng)該為SCSI子系統(tǒng)提供一個(gè)SCSI設(shè)備接口,同時(shí)SCSI子系統(tǒng)也應(yīng)為系統(tǒng)內(nèi)核提供文件I/O和緩沖區(qū)。
* 內(nèi)核機(jī)制和服務(wù):設(shè)備驅(qū)動(dòng)程序利用一些標(biāo)準(zhǔn)的內(nèi)核服務(wù),例如內(nèi)存分配等。
* 可裝入:大多數(shù)的Linux設(shè)備驅(qū)動(dòng)程序都可以在需要時(shí)裝入內(nèi)核,在不需要時(shí)卸載。
* 可設(shè)置:Linux系統(tǒng)設(shè)備驅(qū)動(dòng)程序可以集成為系統(tǒng)內(nèi)核的一部分,至于哪一部分需要集成到內(nèi)核中,可以在系統(tǒng)編譯時(shí)設(shè)置。
I/O端口
申明:這份文檔是按照自由軟件開(kāi)放源代碼的精神發(fā)布的,任何人可以免費(fèi)獲得、使用和重新發(fā)布,但是你沒(méi)有限制別人重新發(fā)布你發(fā)布內(nèi)容的權(quán)利。發(fā)布本文的目的是希望它能對(duì)讀者有用,但沒(méi)有任何擔(dān)保,甚至沒(méi)有適合特定目的的隱含的擔(dān)保。更詳細(xì)的情況請(qǐng)參閱GNU通用公共許可證(GPL),以及GNU自由文檔協(xié)議(GFDL)。
幾乎每一種外設(shè)都是通過(guò)讀寫設(shè)備上的寄存器來(lái)進(jìn)行的。外設(shè)寄存器也稱為“I/O端口”,通常包括:控制寄存器、狀態(tài)寄存器和數(shù)據(jù)寄存器三大類,而且一個(gè)外設(shè)的寄存器通常被連續(xù)地編址。CPU對(duì)外設(shè)IO端口物理地址的編址方式有兩種:一種是I/O映射方式(I/O-mapped),另一種是內(nèi)存映射方式(Memory-mapped)。而具體采用哪一種則取決于CPU的體系結(jié)構(gòu)。
有些體系結(jié)構(gòu)的CPU(如,PowerPC、m68k等)通常只實(shí)現(xiàn)一個(gè)物理地址空間(RAM)。在這種情況下,外設(shè)I/O端口的物理地址就被映射到CPU的單一物理地址空間中,而成為內(nèi)存的一部分。此時(shí),CPU可以象訪問(wèn)一個(gè)內(nèi)存單元那樣訪問(wèn)外設(shè)I/O端口,而不需要設(shè)立專門的外設(shè)I/O指令。這就是所謂的“內(nèi)存映射方式”(Memory-mapped)。
而另外一些體系結(jié)構(gòu)的CPU(典型地如X86)則為外設(shè)專門實(shí)現(xiàn)了一個(gè)單獨(dú)地地址空間,稱為“I/O地址空間”或者“I/O端口空間”。這是一個(gè)與CPU地RAM物理地址空間不同的地址空間,所有外設(shè)的I/O端口均在這一空間中進(jìn)行編址。CPU通過(guò)設(shè)立專門的I/O指令(如X86的IN和OUT指令)來(lái)訪問(wèn)這一空間中的地址單元(也即I/O端口)。這就是所謂的“I/O映射方式”(I/O-mapped)。與RAM物理地址空間相比,I/O地址空間通常都比較小,如x86 CPU的I/O空間就只有64KB(0-0xffff)。這是“I/O映射方式”的一個(gè)主要缺點(diǎn)。
Linux將基于I/O映射方式的或內(nèi)存映射方式的I/O端口通稱為“I/O區(qū)域”(I/O region)。在討論對(duì)I/O區(qū)域的管理之前,我們首先來(lái)分析一下Linux是如何實(shí)現(xiàn)“I/O資源”這一抽象概念的。
1.Linux對(duì)I/O資源的描述
Linux設(shè)計(jì)了一個(gè)通用的數(shù)據(jù)結(jié)構(gòu)resource來(lái)描述各種I/O資源(如:I/O端口、外設(shè)內(nèi)存、DMA和IRQ等)。該結(jié)構(gòu)定義在include/linux/ioport.h頭文件中:
struct resource {
const char *name;
unsigned long start, end;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
各成員的含義如下:
1. name指針:指向此資源的名稱。
2. start和end:表示資源的起始物理地址和終止物理地址。它們確定了資源的范圍,也即是一個(gè)閉區(qū)間[start,end]。
3. flags:描述此資源屬性的標(biāo)志(見(jiàn)下面)。
4. 指針parent、sibling和child:分別為指向父親、兄弟和子資源的指針。
屬性flags是一個(gè)unsigned long類型的32位標(biāo)志值,用以描述資源的屬性。比如:資源的類型、是否只讀、是否可緩存,以及是否已被占用等。下面是一部分常用屬性標(biāo)志位的定義(ioport.h):
/*
* IO resources have these defined flags.
*/
#define IORESOURCE_BITS 0x000000ff /* Bus-specific bits */
#define IORESOURCE_IO 0x00000100 /* Resource type */
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_PREFETCH 0x00001000 /* No side effects */
#define IORESOURCE_READONLY 0x00002000
#define IORESOURCE_CACHEABLE 0x00004000
#define IORESOURCE_RANGELENGTH 0x00008000
#define IORESOURCE_SHADOWABLE 0x00010000
#define IORESOURCE_BUS_HAS_VGA 0x00080000
#define IORESOURCE_UNSET 0x20000000
#define IORESOURCE_AUTO 0x40000000
#define IORESOURCE_BUSY 0x80000000
/* Driver has marked this resource busy */
指針parent、sibling和child的設(shè)置是為了以一種樹(shù)的形式來(lái)管理各種I/O資源。
2. Linux對(duì)I/O資源的管理
2.1 I/O資源的申請(qǐng)
假設(shè)某類資源有如下這樣一顆資源樹(shù):
節(jié)點(diǎn)root、r1、r2和r3實(shí)際上都是一個(gè)resource結(jié)構(gòu)類型。子資源r1、r2和r3通過(guò)sibling指針鏈接成一條單向非循環(huán)鏈表,其表頭由root節(jié)點(diǎn)中的child指針定義,因此也稱為父資源的子資源鏈表。r1、r2和r3的parent指針均指向他們的父資源節(jié)點(diǎn),在這里也就是圖中的root節(jié)點(diǎn)。
假設(shè)想在root節(jié)點(diǎn)中分配一段I/O資源(由圖中的陰影區(qū)域表示)。函數(shù)request_resource()實(shí)現(xiàn)這一功能。它有兩個(gè)參數(shù):①root指針,表示要在哪個(gè)資源根節(jié)點(diǎn)中進(jìn)行分配;②new指針,指向描述所要分配的資源(即圖中的陰影區(qū)域)的resource結(jié)構(gòu)。該函數(shù)的源代碼如下(kernel/resource.c):
int request_resource(struct resource *root, struct resource *new)
{
struct resource *conflict;
write_lock(resource_lock);
conflict = __request_resource(root, new);
write_unlock(resource_lock);
return conflict ? -EBUSY : 0;
}
對(duì)上述函數(shù)的NOTE如下:
①資源鎖resource_lock對(duì)所有資源樹(shù)進(jìn)行讀寫保護(hù),任何代碼段在訪問(wèn)某一顆資源樹(shù)之前都必須先持有該鎖。其定義如下(kernel/Resource.c):
static rwlock_t resource_lock = RW_LOCK_UNLOCKED;
?、诳梢钥闯?,函數(shù)實(shí)際上是通過(guò)調(diào)用內(nèi)部靜態(tài)函數(shù)__request_resource()來(lái)完成實(shí)際的資源分配工作。如果該函數(shù)返回非空指針,則表示有資源沖突;否則,返回NULL就表示分配成功。
③最后,如果conflict指針為NULL,則request_resource()函數(shù)返回返回值0,表示成功;否則返回-EBUSY表示想要分配的資源已被占用。
函數(shù)__request_resource()完成實(shí)際的資源分配工作。如果參數(shù)new所描述的資源中的一部分或全部已經(jīng)被其它節(jié)點(diǎn)所占用,則函數(shù)返回與new相沖突的resource結(jié)構(gòu)的指針。否則就返回NULL。該函數(shù)的源代碼如下
(kernel/Resource.c):
/* Return the conflict entry if you can't request it */
static struct resource * __request_resource
(struct resource *root, struct resource *new)
{
unsigned long start = new->start;
unsigned long end = new->end;
struct resource *tmp, **p;
if (end start)
return root;
if (start root->start)
return root;
if (end > root->end)
return root;
p = root->child;
for (;;) {
tmp = *p;
if (!tmp || tmp->start > end) {
new->sibling = tmp;
*p = new;
new->parent = root;
return NULL;
}
p = tmp->sibling;
if (tmp->end start)
continue;
return tmp;
}
}
對(duì)函數(shù)的NOTE:
①前三個(gè)if語(yǔ)句判斷new所描述的資源范圍是否被包含在root內(nèi),以及是否是一段有效的資源(因?yàn)閑nd必須大于start)。否則就返回root指針,表示與根結(jié)點(diǎn)相沖突。
?、诮酉聛?lái)用一個(gè)for循環(huán)遍歷根節(jié)點(diǎn)root的child鏈表,以便檢查是否有資源沖突,并將new插入到child鏈表中的合適位置(child鏈表是以I/O資源物理地址從低到高的順序排列的)。為此,它用tmp指針指向當(dāng)前正被掃描的resource結(jié)構(gòu),用指針p指向前一個(gè)resource結(jié)構(gòu)的sibling指針成員變量,p的初始值為指向root->sibling。For循環(huán)體的執(zhí)行步驟如下:
l 讓tmp指向當(dāng)前正被掃描的resource結(jié)構(gòu)(tmp=*p)。
l 判斷tmp指針是否為空(tmp指針為空說(shuō)明已經(jīng)遍歷完整個(gè)child鏈表),或者當(dāng)前被掃描節(jié)點(diǎn)的起始位置start是否比new的結(jié)束位置end還要大。只要這兩個(gè)條件之一成立的話,就說(shuō)明沒(méi)有資源沖突,于是就可以把new鏈入child鏈表中:①設(shè)置new的sibling指針指向當(dāng)前正被掃描的節(jié)點(diǎn)tmp(new->sibling=tmp);②當(dāng)前節(jié)點(diǎn)tmp的前一個(gè)兄弟節(jié)點(diǎn)的sibling指針被修改為指向new這個(gè)節(jié)點(diǎn)(*p=new);③將new的parent指針設(shè)置為指向root。然后函數(shù)就可以返回了(返回值NULL表示沒(méi)有資源沖突)。
l 如果上述兩個(gè)條件都不成立,這說(shuō)明當(dāng)前被掃描節(jié)點(diǎn)的資源域有可能與new相沖突(實(shí)際上就是兩個(gè)閉區(qū)間有交集),因此需要進(jìn)一步判斷。為此它首先修改指針p,讓它指向tmp->sibling,以便于繼續(xù)掃描child鏈表。然后,判斷tmp->end是否小于new->start,如果小于,則說(shuō)明當(dāng)前節(jié)點(diǎn)tmp和new沒(méi)有資源沖突,因此執(zhí)行continue語(yǔ)句,繼續(xù)向下掃描child鏈表。否則,如果tmp->end大于或等于new->start,則說(shuō)明tmp->[start,end]和new->[start,end]之間有交集。所以返回當(dāng)前節(jié)點(diǎn)的指針tmp,表示發(fā)生資源沖突。
2.2 資源的釋放
函數(shù)release_resource()用于實(shí)現(xiàn)I/O資源的釋放。該函數(shù)只有一個(gè)參數(shù)——即指針old,它指向所要釋放的資源。起源代碼如下:
int release_resource(struct resource *old)
{
int retval;
write_lock(resource_lock);
retval = __release_resource(old);
write_unlock(resource_lock);
return retval;
}
可以看出,它實(shí)際上通過(guò)調(diào)用__release_resource()這個(gè)內(nèi)部靜態(tài)函數(shù)來(lái)完成實(shí)際的資源釋放工作。函數(shù)__release_resource()的主要任務(wù)就是將資源區(qū)域old(如果已經(jīng)存在的話)從其父資源的child鏈表重摘除,它的源代碼如下:
static int __release_resource(struct resource *old)
{
struct resource *tmp, **p;
p = old->parent->child;
for (;;) {
tmp = *p;
if (!tmp)
break;
if (tmp == old) {
*p = tmp->sibling;
old->parent = NULL;
return 0;
}
p = tmp->sibling;
}
return -EINVAL;
}
對(duì)上述函數(shù)代碼的NOTE如下:
同函數(shù)__request_resource()相類似,該函數(shù)也是通過(guò)一個(gè)for循環(huán)來(lái)遍歷父資源的child鏈表。為此,它讓tmp指針指向當(dāng)前被掃描的資源,而指針p則指向當(dāng)前節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)的sibling成員(p的初始值為指向父資源的child指針)。循環(huán)體的步驟如下:
?、偈紫?,讓tmp指針指向當(dāng)前被掃描的節(jié)點(diǎn)(tmp=*p)。
?、谌绻鹴mp指針為空,說(shuō)明已經(jīng)遍歷完整個(gè)child鏈表,因此執(zhí)行break語(yǔ)句推出for循環(huán)。由于在遍歷過(guò)程中沒(méi)有在child鏈表中找到參數(shù)old所指定的資源節(jié)點(diǎn),因此最后返回錯(cuò)誤值-EINVAL,表示參數(shù)old是一個(gè)無(wú)效的值。
?、劢酉聛?lái),判斷當(dāng)前被掃描節(jié)點(diǎn)是否就是參數(shù)old所指定的資源節(jié)點(diǎn)。如果是,那就將old從child鏈表中去除,也即讓當(dāng)前結(jié)點(diǎn)tmp的前一個(gè)兄弟節(jié)點(diǎn)的sibling指針指向tmp的下一個(gè)節(jié)點(diǎn),然后將old->parent指針設(shè)置為NULL。最后返回0值表示執(zhí)行成功。
④如果當(dāng)前被掃描節(jié)點(diǎn)不是資源old,那就繼續(xù)掃描child鏈表中的下一個(gè)元素。因此將指針p指向tmp->sibling成員。
2.3 檢查資源是否已被占用
函數(shù)check_resource()用于實(shí)現(xiàn)檢查某一段I/O資源是否已被占用。其源代碼如下:
int check_resource(struct resource *root, unsigned long start, unsigned long len)
{
struct resource *conflict, tmp;
tmp.start = start;
tmp.end = start + len - 1;
write_lock(resource_lock);
conflict = __request_resource(root, tmp);
if (!conflict)
__release_resource(tmp);
write_unlock(resource_lock);
return conflict ? -EBUSY : 0;
}
對(duì)該函數(shù)的NOTE如下:
?、贅?gòu)造一個(gè)臨時(shí)資源tmp,表示所要檢查的資源[start,start+end-1]。
?、谡{(diào)用__request_resource()函數(shù)在根節(jié)點(diǎn)root申請(qǐng)tmp所表示的資源。如果tmp所描述的資源還被人使用,則該函數(shù)返回NULL,否則返回非空指針。因此接下來(lái)在conflict為NULL的情況下,調(diào)用__release_resource()將剛剛申請(qǐng)的資源釋放掉。
?、圩詈蟾鶕?jù)conflict是否為NULL,返回-EBUSY或0值。
2.4 尋找可用資源
函數(shù)find_resource()用于在一顆資源樹(shù)中尋找未被使用的、且滿足給定條件的(也即資源長(zhǎng)度大小為size,且在[min,max]區(qū)間內(nèi))的資源。其函數(shù)源代碼如下:
/*
* Find empty slot in the resource tree given range and alignment.
*/
static int find_resource(struct resource *root, struct resource *new,
unsigned long size,
unsigned long min, unsigned long max,
unsigned long align,
void (*alignf)(void *, struct resource *, unsigned long),
void *alignf_data)
{
struct resource *this = root->child;
new->start = root->start;
for(;;) {
if (this)
new->end = this->start;
else
new->end = root->end;
if (new->start min)
new->start = min;
if (new->end > max)
new->end = max;
new->start = (new->start + align - 1) ~(align - 1);
if (alignf)
alignf(alignf_data, new, size);
if (new->start new->end new->end - new->start + 1 >= size)
{
new->end = new->start + size - 1;
return 0;
}
if (!this)
break;
new->start = this->end + 1;
this = this->sibling;
}
return -EBUSY;
}
對(duì)該函數(shù)的NOTE如下:
同樣,該函數(shù)也要遍歷root的child鏈表,以尋找未被使用的資源空洞。為此,它讓this指針表示當(dāng)前正被掃描的子資源節(jié)點(diǎn),其初始值等于root->child,即指向child鏈表中的第一個(gè)節(jié)點(diǎn),并讓new->start的初始值等于root->start,然后用一個(gè)for循環(huán)開(kāi)始掃描child鏈表,對(duì)于每一個(gè)被掃描的節(jié)點(diǎn),循環(huán)體執(zhí)行如下操作:
?、偈紫?,判斷this指針是否為NULL。如果不為空,就讓new->end等于this->start,也即讓資源new表示當(dāng)前資源節(jié)點(diǎn)this前面那一段未使用的資源區(qū)間。
?、谌绻鹴his指針為空,那就讓new->end等于root->end。這有兩層意思:第一種情況就是根結(jié)點(diǎn)的child指針為NULL(即根節(jié)點(diǎn)沒(méi)有任何子資源)。因此此時(shí)先暫時(shí)將new->end放到最大。第二種情況就是已經(jīng)遍歷完整個(gè)child鏈表,所以此時(shí)就讓new表示最后一個(gè)子資源后面那一段未使用的資源區(qū)間。
?、鄹鶕?jù)參數(shù)min和max修正new->[start,end]的值,以使資源new被包含在[min,max]區(qū)域內(nèi)。
?、芙酉聛?lái)進(jìn)行對(duì)齊操作。
?、萑缓?,判斷經(jīng)過(guò)上述這些步驟所形成的資源區(qū)域new是否是一段有效的資源(end必須大于或等于start),而且資源區(qū)域的長(zhǎng)度滿足size參數(shù)的要求(end-start+1>=size)。如果這兩個(gè)條件均滿足,則說(shuō)明我們已經(jīng)找到了一段滿足條件的資源空洞。因此在對(duì)new->end的值進(jìn)行修正后,然后就可以返回了(返回值0表示成功)。
?、奕绻鲜鰞蓷l件不能同時(shí)滿足,則說(shuō)明還沒(méi)有找到,因此要繼續(xù)掃描鏈表。在繼續(xù)掃描之前,我們還是要判斷一下this指針是否為空。如果為空,說(shuō)明已經(jīng)掃描完整個(gè)child鏈表,因此就可以推出for循環(huán)了。否則就將new->start的值修改為this->end+1,并讓this指向下一個(gè)兄弟資源節(jié)點(diǎn),從而繼續(xù)掃描鏈表中的下一個(gè)子資源節(jié)點(diǎn)。
2.5 分配接口allocate_resource()
在find_resource()函數(shù)的基礎(chǔ)上,函數(shù)allocate_resource()實(shí)現(xiàn):在一顆資源樹(shù)中分配一條指定大小的、且包含在指定區(qū)域[min,max]中的、未使用資源區(qū)域。其源代碼如下:
/*
* Allocate empty slot in the resource tree given range and alignment.
*/
int allocate_resource(struct resource *root, struct resource *new,
unsigned long size,
unsigned long min, unsigned long max,
unsigned long align,
void (*alignf)(void *, struct resource *, unsigned long),
void *alignf_data)
{
int err;
write_lock(resource_lock);
err = find_resource(root, new, size, min, max, align, alignf, alignf_data);
if (err >= 0 __request_resource(root, new))
err = -EBUSY;
write_unlock(resource_lock);
return err;
}
2.6 獲取資源的名稱列表
函數(shù)get_resource_list()用于獲取根節(jié)點(diǎn)root的子資源名字列表。該函數(shù)主要用來(lái)支持/proc/文件系統(tǒng)(比如實(shí)現(xiàn)proc/ioports文件和/proc/iomem文件)。其源代碼如下:
int get_resource_list(struct resource *root, char *buf, int size)
{
char *fmt;
int retval;
fmt = %08lx-%08lx : %s
;
if (root->end 0x10000)
fmt = %04lx-%04lx : %s
;
read_lock(resource_lock);
retval = do_resource_list(root->child, fmt, 8, buf, buf + size) - buf;
read_unlock(resource_lock);
return retval;
}
可以看出,該函數(shù)主要通過(guò)調(diào)用內(nèi)部靜態(tài)函數(shù)do_resource_list()來(lái)實(shí)現(xiàn)其功能,其源代碼如下:
/*
* This generates reports for /proc/ioports and /proc/iomem
*/
static char * do_resource_list(struct resource *entry, const char *fmt,
int offset, char *buf, char *end)
{
if (offset 0)
offset = 0;
while (entry) {
const char *name = entry->name;
unsigned long from, to;
if ((int) (end-buf) 80)
return buf;
from = entry->start;
to = entry->end;
if (!name)
name = ;
buf += sprintf(buf, fmt + offset, from, to, name);
if (entry->child)
buf = do_resource_list(entry->child, fmt, offset-2, buf, end);
entry = entry->sibling;
}
return buf;
}
函數(shù)do_resource_list()主要通過(guò)一個(gè)while{}循環(huán)以及遞歸嵌套調(diào)用來(lái)實(shí)現(xiàn),較為簡(jiǎn)單,這里就不在詳細(xì)解釋了。
3.管理I/O Region資源
Linux將基于I/O映射方式的I/O端口和基于內(nèi)存映射方式的I/O端口資源統(tǒng)稱為“I/O區(qū)域”(I/O Region)。I/O Region仍然是一種I/O資源,因此它仍然可以用resource結(jié)構(gòu)類型來(lái)描述。下面我們就來(lái)看看Linux是如何管理I/O Region的。
3.1 I/O Region的分配
在函數(shù)__request_resource()的基礎(chǔ)上,Linux實(shí)現(xiàn)了用于分配I/O區(qū)域的函數(shù)__request_region(),如下:
struct resource * __request_region(struct resource *parent,
unsigned long start, unsigned long n, const char *name)
{
struct resource *res = kmalloc(sizeof(*res), GFP_KERNEL);
if (res) {
memset(res, 0, sizeof(*res));
res->name = name;
res->start = start;
res->end = start + n - 1;
res->flags = IORESOURCE_BUSY;
write_lock(resource_lock);
for (;;) {
struct resource *conflict;
conflict = __request_resource(parent, res);
if (!conflict)
break;
if (conflict != parent) {
parent = conflict;
if (!(conflict->flags IORESOURCE_BUSY))
continue;
}
/* Uhhuh, that didn't work out.. */
kfree(res);
res = NULL;
break;
}
write_unlock(resource_lock);
}
return res;
}
NOTE:
?、偈紫龋{(diào)用kmalloc()函數(shù)在SLAB分配器緩存中分配一個(gè)resource結(jié)構(gòu)。
②然后,相應(yīng)的根據(jù)參數(shù)值初始化所分配的resource結(jié)構(gòu)。注意!flags成員被初始化為IORESOURCE_BUSY。
?、劢酉聛?lái),用一個(gè)for循環(huán)開(kāi)始進(jìn)行資源分配,循環(huán)體的步驟如下:
l 首先,調(diào)用__request_resource()函數(shù)進(jìn)行資源分配。如果返回NULL,說(shuō)明分配成功,因此就執(zhí)行break語(yǔ)句推出for循環(huán),返回所分配的resource結(jié)構(gòu)的指針,函數(shù)成功地結(jié)束。
l 如果__request_resource()函數(shù)分配不成功,則進(jìn)一步判斷所返回的沖突資源節(jié)點(diǎn)是否就是父資源節(jié)點(diǎn)parent。如果不是,則將分配行為下降一個(gè)層次,即試圖在當(dāng)前沖突的資源節(jié)點(diǎn)中進(jìn)行分配(只有在沖突的資源節(jié)點(diǎn)沒(méi)有設(shè)置IORESOURCE_BUSY的情況下才可以),于是讓parent指針等于conflict,并在conflict->flagsIORESOURCE_BUSY為0的情況下執(zhí)行continue語(yǔ)句繼續(xù)for循環(huán)。
l 否則如果相沖突的資源節(jié)點(diǎn)就是父節(jié)點(diǎn)parent,或者相沖突資源節(jié)點(diǎn)設(shè)置了IORESOURCE_BUSY標(biāo)志位,則宣告分配失敗。于是調(diào)用kfree()函數(shù)釋放所分配的resource結(jié)構(gòu),并將res指針置為NULL,最后用break語(yǔ)句推出for循環(huán)。
?、茏詈螅祷厮峙涞膔esource結(jié)構(gòu)的指針。
3.2 I/O Region的釋放
函數(shù)__release_region()實(shí)現(xiàn)在一個(gè)父資源節(jié)點(diǎn)parent中釋放給定范圍的I/O Region。實(shí)際上該函數(shù)的實(shí)現(xiàn)思想與__release_resource()相類似。其源代碼如下:
void __release_region(struct resource *parent,
unsigned long start, unsigned long n)
{
struct resource **p;
unsigned long end;
p = parent->child;
end = start + n - 1;
for (;;) {
struct resource *res = *p;
if (!res)
break;
if (res->start = start res->end >= end) {
if (!(res->flags IORESOURCE_BUSY)) {
p = res->child;
continue;
}
if (res->start != start' 'res->end != end)
break;
*p = res->sibling;
kfree(res);
return;
}
p = res->sibling;
}
printk(Trying to free nonexistent resource %08lx-%08lx>
, start, end);
}
類似地,該函數(shù)也是通過(guò)一個(gè)for循環(huán)來(lái)遍歷父資源parent的child鏈表。為此,它讓指針res指向當(dāng)前正被掃描的子資源節(jié)點(diǎn),指針p指向前一個(gè)子資源節(jié)點(diǎn)的sibling成員變量,p的初始值為指向parent->child。For循環(huán)體的步驟如下:
?、僮宺es指針指向當(dāng)前被掃描的子資源節(jié)點(diǎn)(res=*p)。
②如果res指針為NULL,說(shuō)明已經(jīng)掃描完整個(gè)child鏈表,所以退出for循環(huán)。
?、廴绻鹯es指針不為NULL,則繼續(xù)看看所指定的I/O區(qū)域范圍是否完全包含在當(dāng)前資源節(jié)點(diǎn)中,也即看看[start,start+n-1]是否包含在res->[start,end]中。如果不屬于,則讓p指向當(dāng)前資源節(jié)點(diǎn)的sibling成員,然后繼續(xù)for循環(huán)。如果屬于,則執(zhí)行下列步驟:
l 先看看當(dāng)前資源節(jié)點(diǎn)是否設(shè)置了IORESOURCE_BUSY標(biāo)志位。如果沒(méi)有設(shè)置該標(biāo)志位,則說(shuō)明該資源節(jié)點(diǎn)下面可能還會(huì)有子節(jié)點(diǎn),因此將掃描過(guò)程下降一個(gè)層次,于是修改p指針,使它指向res->child,然后執(zhí)行continue語(yǔ)句繼續(xù)for循環(huán)。
l 如果設(shè)置了IORESOURCE_BUSY標(biāo)志位。則一定要確保當(dāng)前資源節(jié)點(diǎn)就是所指定的I/O區(qū)域,然后將當(dāng)前資源節(jié)點(diǎn)從其父資源的child鏈表中去除。這可以通過(guò)讓前一個(gè)兄弟資源節(jié)點(diǎn)的sibling指針指向當(dāng)前資源節(jié)點(diǎn)的下一個(gè)兄弟資源節(jié)點(diǎn)來(lái)實(shí)現(xiàn)(即讓*p=res->sibling),最后調(diào)用kfree()函數(shù)釋放當(dāng)前資源節(jié)點(diǎn)的resource結(jié)構(gòu)。然后函數(shù)就可以成功返回了。
3.3 檢查指定的I/O Region是否已被占用
函數(shù)__check_region()檢查指定的I/O Region是否已被占用。其源代碼如下:
int __check_region(struct resource *parent, unsigned long start, unsigned long n)
{
struct resource * res;
res = __request_region(parent, start, n, check-region);
if (!res)
return -EBUSY;
release_resource(res);
kfree(res);
return 0;
}
該函數(shù)的實(shí)現(xiàn)與__check_resource()的實(shí)現(xiàn)思想類似。首先,它通過(guò)調(diào)用__request_region()函數(shù)試圖在父資源parent中分配指定的I/O Region。如果分配不成功,將返回NULL,因此此時(shí)函數(shù)返回錯(cuò)誤值-EBUSY表示所指定的I/O Region已被占用。如果res指針不為空則說(shuō)明所指定的I/O Region沒(méi)有被占用。于是調(diào)用__release_resource()函數(shù)將剛剛分配的資源釋放掉(實(shí)際上是將res結(jié)構(gòu)從parent的child鏈表去除),然后調(diào)用kfree()函數(shù)釋放res結(jié)構(gòu)所占用的內(nèi)存。最后,返回0值表示指定的I/O Region沒(méi)有被占用。
4 .管理I/O端口資源
我們都知道,采用I/O映射方式的X86處理器為外設(shè)實(shí)現(xiàn)了一個(gè)單獨(dú)的地址空間,也即“I/O空間”(I/O Space)或稱為“I/O端口空間”,其大小是64KB(0x0000-0xffff)。Linux在其所支持的所有平臺(tái)上都實(shí)現(xiàn)了“I/O端口空間”這一概念。
由于I/O空間非常小,因此即使外設(shè)總線有一個(gè)單獨(dú)的I/O端口空間,卻也不是所有的外設(shè)都將其I/O端口(指寄存器)映射到“I/O端口空間”中。比如,大多數(shù)PCI卡都通過(guò)內(nèi)存映射方式來(lái)將其I/O端口或外設(shè)內(nèi)存映射到CPU的RAM物理地址空間中。而老式的ISA卡通常將其I/O端口映射到I/O端口空間中。
Linux是基于“I/O Region”這一概念來(lái)實(shí)現(xiàn)對(duì)I/O端口資源(I/O-mapped 或 Memory-mapped)的管理的。
4.1 資源根節(jié)點(diǎn)的定義
Linux在kernel/Resource.c文件中定義了全局變量ioport_resource和iomem_resource,來(lái)分別描述基于I/O映射方式的整個(gè)I/O端口空間和基于內(nèi)存映射方式的I/O內(nèi)存資源空間(包括I/O端口和外設(shè)內(nèi)存)。其定義如下:
struct resource ioport_resource =
{ PCI IO, 0x0000, IO_SPACE_LIMIT, IORESOURCE_IO };
struct resource iomem_resource =
{ PCI mem, 0x00000000, 0xffffffff, IORESOURCE_MEM };
其中,宏IO_SPACE_LIMIT表示整個(gè)I/O空間的大小,對(duì)于X86平臺(tái)而言,它是0xffff(定義在include/asm-i386/io.h頭文件中)。顯然,I/O內(nèi)存空間的大小是4GB。
4.2 對(duì)I/O端口空間的操作
基于I/O Region的操作函數(shù)__XXX_region(),Linux在頭文件include/linux/ioport.h中定義了三個(gè)對(duì)I/O端口空間進(jìn)行操作的宏:①request_region()宏,請(qǐng)求在I/O端口空間中分配指定范圍的I/O端口資源。②check_region()宏,檢查I/O端口空間中的指定I/O端口資源是否已被占用。③release_region()宏,釋放I/O端口空間中的指定I/O端口資源。這三個(gè)宏的定義如下:
#define request_region(start,n,name)
__request_region(ioport_resource, (start), (n), (name))
#define check_region(start,n)
__check_region(ioport_resource, (start), (n))
#define release_region(start,n)
__release_region(ioport_resource, (start), (n))
其中,宏參數(shù)start指定I/O端口資源的起始物理地址(是I/O端口空間中的物理地址),宏參數(shù)n指定I/O端口資源的大小。
4.3 對(duì)I/O內(nèi)存資源的操作
基于I/O Region的操作函數(shù)__XXX_region(),Linux在頭文件include/linux/ioport.h中定義了三個(gè)對(duì)I/O內(nèi)存資源進(jìn)行操作的宏:①request_mem_region()宏,請(qǐng)求分配指定的I/O內(nèi)存資源。②check_ mem_region()宏,檢查指定的I/O內(nèi)存資源是否已被占用。③release_ mem_region()宏,釋放指定的I/O內(nèi)存資源。這三個(gè)宏的定義如下:
#define request_mem_region(start,n,name)
__request_region(iomem_resource, (start), (n), (name))
#define check_mem_region(start,n)
__check_region(iomem_resource, (start), (n))
#define release_mem_region(start,n)
__release_region(iomem_resource, (start), (n))
其中,參數(shù)start是I/O內(nèi)存資源的起始物理地址(是CPU的RAM物理地址空間中的物理地址),參數(shù)n指定I/O內(nèi)存資源的大小。
4.4 對(duì)/proc/ioports和/proc/iomem的支持
Linux在ioport.h頭文件中定義了兩個(gè)宏:
get_ioport_list()和get_iomem_list(),分別用來(lái)實(shí)現(xiàn)/proc/ioports文件和/proc/iomem文件。其定義如下:
#define get_ioport_list(buf) get_resource_list(ioport_resource, buf, PAGE_SIZE)
#define get_mem_list(buf) get_resource_list(iomem_resource, buf, PAGE_SIZE)
5 .訪問(wèn)I/O端口空間
在驅(qū)動(dòng)程序請(qǐng)求了I/O端口空間中的端口資源后,它就可以通過(guò)CPU的IO指定來(lái)讀寫這些I/O端口了。在讀寫I/O端口時(shí)要注意的一點(diǎn)就是,大多數(shù)平臺(tái)都區(qū)分8位、16位和32位的端口,也即要注意I/O端口的寬度。
Linux在include/asm/io.h頭文件(對(duì)于i386平臺(tái)就是include/asm-i386/io.h)中定義了一系列讀寫不同寬度I/O端口的宏函數(shù)。如下所示:
?、抛x寫8位寬的I/O端口
unsigned char inb(unsigned port);
void outb(unsigned char value,unsigned port);
其中,port參數(shù)指定I/O端口空間中的端口地址。在大多數(shù)平臺(tái)上(如x86)它都是unsigned short類型的,其它的一些平臺(tái)上則是unsigned int類型的。顯然,端口地址的類型是由I/O端口空間的大小來(lái)決定的。
⑵讀寫16位寬的I/O端口
unsigned short inw(unsigned port);
void outw(unsigned short value,unsigned port);
?、亲x寫32位寬的I/O端口
unsigned int inl(unsigned port);
void outl(unsigned int value,unsigned port);
5.1 對(duì)I/O端口的字符串操作
除了上述這些“單發(fā)”(single-shot)的I/O操作外,某些CPU也支持對(duì)某個(gè)I/O端口進(jìn)行連續(xù)的讀寫操作,也即對(duì)單個(gè)I/O端口讀或?qū)懸幌盗凶止?jié)、字或32位整數(shù),這就是所謂的“字符串I/O指令”(String Instruction)。這種指令在速度上顯然要比用循環(huán)來(lái)實(shí)現(xiàn)同樣的功能要快得多。
Linux同樣在io.h文件中定義了字符串I/O讀寫函數(shù):
?、?位寬的字符串I/O操作
void insb(unsigned port,void * addr,unsigned long count);
void outsb(unsigned port ,void * addr,unsigned long count);
⑵16位寬的字符串I/O操作
void insw(unsigned port,void * addr,unsigned long count);
void outsw(unsigned port ,void * addr,unsigned long count);
?、?2位寬的字符串I/O操作
void insl(unsigned port,void * addr,unsigned long count);
void outsl(unsigned port ,void * addr,unsigned long count);
5.2 Pausing I/O
在一些平臺(tái)上(典型地如X86),對(duì)于老式總線(如ISA)上的慢速外設(shè)來(lái)說(shuō),如果CPU讀寫其I/O端口的速度太快,那就可能會(huì)發(fā)生丟失數(shù)據(jù)的現(xiàn)象。對(duì)于這個(gè)問(wèn)題的解決方法就是在兩次連續(xù)的I/O操作之間插入一段微小的時(shí)延,以便等待慢速外設(shè)。這就是所謂的“Pausing I/O”。
對(duì)于Pausing I/O,Linux也在io.h頭文件中定義了它的I/O讀寫函數(shù),而且都以XXX_p命名,比如:inb_p()、outb_p()等等。下面我們就以out_p()為例進(jìn)行分析。
將io.h中的宏定義__OUT(b,”b”char)展開(kāi)后可得如下定義:
extern inline void outb(unsigned char value, unsigned short port) {
__asm__ __volatile__ (outb % b 0,% w 1
: : a (value), Nd (port));
}
extern inline void outb_p(unsigned char value, unsigned short port) {
__asm__ __volatile__ (outb % b 0,% w 1
__FULL_SLOW_DOWN_IO
: : a (value), Nd (port));
}
可以看出,outb_p()函數(shù)的實(shí)現(xiàn)中被插入了宏__FULL_SLOWN_DOWN_IO,以實(shí)現(xiàn)微小的延時(shí)。宏__FULL_SLOWN_DOWN_IO在頭文件io.h中一開(kāi)始就被定義:
#ifdef SLOW_IO_BY_JUMPING
#define __SLOW_DOWN_IO
jmp 1f
1: jmp 1f
1:
#else
#define __SLOW_DOWN_IO
outb %%al,$0x80
#endif
#ifdef REALLY_SLOW_IO
#define __FULL_SLOW_DOWN_IO __SLOW_DOWN_IO
__SLOW_DOWN_IO __SLOW_DOWN_IO __SLOW_DOWN_IO
#else
#define __FULL_SLOW_DOWN_IO __SLOW_DOWN_IO
#endif
顯然,__FULL_SLOW_DOWN_IO就是一個(gè)或四個(gè)__SLOW_DOWN_IO(根據(jù)是否定義了宏REALLY_SLOW_IO來(lái)決定),而宏__SLOW_DOWN_IO則被定義成毫無(wú)意義的跳轉(zhuǎn)語(yǔ)句或?qū)懚丝?x80的操作(根據(jù)是否定義了宏SLOW_IO_BY_JUMPING來(lái)決定)。
6 .訪問(wèn)I/O內(nèi)存資源
盡管I/O端口空間曾一度在x86平臺(tái)上被廣泛使用,但是由于它非常小,因此大多數(shù)現(xiàn)代總線的設(shè)備都以內(nèi)存映射方式(Memory-mapped)來(lái)映射它的I/O端口(指I/O寄存器)和外設(shè)內(nèi)存?;趦?nèi)存映射方式的I/O端口(指I/O寄存器)和外設(shè)內(nèi)存可以通稱為“I/O內(nèi)存”資源(I/O Memory)。因?yàn)檫@兩者在硬件實(shí)現(xiàn)上的差異對(duì)于軟件來(lái)說(shuō)是完全透明的,所以驅(qū)動(dòng)程序開(kāi)發(fā)人員可以將內(nèi)存映射方式的I/O端口和外設(shè)內(nèi)存統(tǒng)一看作是“I/O內(nèi)存”資源。
從前幾節(jié)的闡述我們知道,I/O內(nèi)存資源是在CPU的單一內(nèi)存物理地址空間內(nèi)進(jìn)行編址的,也即它和系統(tǒng)RAM同處在一個(gè)物理地址空間內(nèi)。因此通過(guò)CPU的訪內(nèi)指令就可以訪問(wèn)I/O內(nèi)存資源。
一般來(lái)說(shuō),在系統(tǒng)運(yùn)行時(shí),外設(shè)的I/O內(nèi)存資源的物理地址是已知的,這可以通過(guò)系統(tǒng)固件(如BIOS)在啟動(dòng)時(shí)分配得到,或者通過(guò)設(shè)備的硬連線(hardwired)得到。比如,PCI卡的I/O內(nèi)存資源的物理地址就是在系統(tǒng)啟動(dòng)時(shí)由PCI BIOS分配并寫到PCI卡的配置空間中的BAR中的。而ISA卡的I/O內(nèi)存資源的物理地址則是通過(guò)設(shè)備硬連線映射到640KB-1MB范圍之內(nèi)的。但是CPU通常并沒(méi)有為這些已知的外設(shè)I/O內(nèi)存資源的物理地址預(yù)定義虛擬地址范圍,因?yàn)樗鼈兪窃谙到y(tǒng)啟動(dòng)后才已知的(某種意義上講是動(dòng)態(tài)的),所以驅(qū)動(dòng)程序并不能直接通過(guò)物理地址訪問(wèn)I/O內(nèi)存資源,而必須將它們映射到核心虛地址空間內(nèi)(通過(guò)頁(yè)表),然后才能根據(jù)映射所得到的核心虛地址范圍,通過(guò)訪內(nèi)指令訪問(wèn)這些I/O內(nèi)存資源。
6.1 映射I/O內(nèi)存資源
Linux在io.h頭文件中聲明了函數(shù)ioremap(),用來(lái)將I/O內(nèi)存資源的物理地址映射到核心虛地址空間(3GB-4GB)中,如下:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
void iounmap(void * addr);
函數(shù)用于取消ioremap()所做的映射,參數(shù)addr是指向核心虛地址的指針。這兩個(gè)函數(shù)都是實(shí)現(xiàn)在mm/ioremap.c文件中。具體實(shí)現(xiàn)可參考《情景分析》一書(shū)。
6.2 讀寫I/O內(nèi)存資源
在將I/O內(nèi)存資源的物理地址映射成核心虛地址后,理論上講我們就可以象讀寫RAM那樣直接讀寫I/O內(nèi)存資源了。但是,由于在某些平臺(tái)上,對(duì)I/O內(nèi)存和系統(tǒng)內(nèi)存有不同的訪問(wèn)處理,因此為了確保跨平臺(tái)的兼容性,Linux實(shí)現(xiàn)了一系列讀寫I/O內(nèi)存資源的函數(shù),這些函數(shù)在不同的平臺(tái)上有不同的實(shí)現(xiàn)。但在x86平臺(tái)上,讀寫I/O內(nèi)存與讀寫RAM無(wú)任何差別。如下所示(include/asm-i386/io.h):
#define readb(addr) (*(volatile unsigned char *) __io_virt(addr))
#define readw(addr) (*(volatile unsigned short *) __io_virt(addr))
#define readl(addr) (*(volatile unsigned int *) __io_virt(addr))
#define writeb(b,addr) (*(volatile unsigned char *) __io_virt(addr) = (b))
#define writew(b,addr) (*(volatile unsigned short *) __io_virt(addr) = (b))
#define writel(b,addr) (*(volatile unsigned int *) __io_virt(addr) = (b))
#define memset_io(a,b,c) memset(__io_virt(a),(b),(c))
#define memcpy_fromio(a,b,c) memcpy((a),__io_virt(b),(c))
#define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))
上述定義中的宏__io_virt()僅僅檢查虛地址addr是否是核心空間中的虛地址。該宏在內(nèi)核2.4.0中的實(shí)現(xiàn)是臨時(shí)性的。具體的實(shí)現(xiàn)函數(shù)在arch/i386/lib/Iodebug.c文件。
顯然,在x86平臺(tái)上訪問(wèn)I/O內(nèi)存資源與訪問(wèn)系統(tǒng)主存RAM是無(wú)差別的。但是為了保證驅(qū)動(dòng)程序的跨平臺(tái)的可移植性,我們應(yīng)該使用上面的函數(shù)來(lái)訪問(wèn)I/O內(nèi)存資源,而不應(yīng)該通過(guò)指向核心虛地址的指針來(lái)訪問(wèn)。
評(píng)論