Linux裝置號的構成和分配核心機制
我們知道“Linux下一切皆檔案”(當然由於歷史原因,網路裝置除外,它是通過socket進行操作的),我們操作裝置都要通過檔案進行操作也就是所所謂的操作裝置檔案節點,但是在Linux核心中是使用裝置號來唯一的識別和管理裝置,就相當於公民的省份證號碼一樣(其實吧,計算機還是喜歡數字的像標識程序使用程序的PID,管理使用者使用UID,管理磁碟上的檔案使用的inode號,管理網路中的計算機使用IP地址等等)。
那麼裝置號到底是什麼東西?在Linux系統中如何才能保證每個裝置的裝置號是唯一的?下面我們來看一下:
1.裝置號的構成
Linux系統中一個裝置號由主裝置號和次裝置號構成,Linux核心用主裝置號來定位對應的裝置驅動程式(即是主裝置找驅動),而次裝置號用來標識它同個驅動所管理的若干的裝置(次裝置號找裝置)。因此,從這個角度來看,裝置號作為系統資源,必須要進行仔細的管理,以防止裝置號與驅動程式的錯誤對應所帶來的管理裝置的混亂。
下圖為一個嵌入式裝置上的串列埠的裝置檔案節點資訊:
可以看到他們的主裝置號都是252,次裝置號0-5,即是這種串列埠驅動管理了6個不同的裝置。
Linux系統中,使用dev_t型別來標識一個裝置號,他是一個32位的無符號整數:
<include linux/types.h>
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
其中,12為主裝置號,20為次裝置如下圖。
隨著核心版本的演變,上述的主次裝置號的構成也許會發生變化,所以裝置驅動開發者應該避免直接使用主次裝置號鎖佔的位寬來獲得對應的主裝置號或者次裝置號。核心為了保證在主次裝置號位寬發生變化時,現在的程式依然可以工作,核心提供瞭如下的幾個巨集:
<include /linux/kdev_t.h>
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
MAJOR巨集是用來從一個dev_t 型別的裝置號中提取出主裝置號,MINOR
如果之後的核心對主次裝置號所佔的位寬進行調整,例如將MINORBITS改為8,只要驅動程式堅持使用MAJOR,MINOR,MKDEV來操作裝置號,那麼這部分的程式碼無需修改就可以用在新的核心中執行。
在實際的驅動開發中,我們經常已知inode,那麼我們可以通過inode來獲得主次裝置號:
<include /linux/fs.h>
static inline unsigned iminor(const struct inode *inode)
{
return MINOR(inode->i_rdev);
}
static inline unsigned imajor(const struct inode *inode)
{
return MAJOR(inode->i_rdev);
}
iminor用於根據inode獲得次裝置號,imajor用於根據inode獲得主裝置號。
2.裝置號的分配和管理
在核心原始碼中,進行裝置號的分配與管理的函式有一下兩個:register_chrdev_region,alloc_chrdev_region
1)register_chrdev_region函式
此函式用於靜態註冊裝置號,優點是可以在註冊的時候就知道其裝置號,缺點是可能會與系統中已經註冊的裝置號衝突導致註冊失敗。
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
該函式第一個引數from表示是一個裝置號,第二個是連續裝置編號的個數,代表當前驅動所管理的同類裝置的個數,第三個引數name表示裝置或者驅動的名稱。可以看到register_chrdev_region的核心功能體現在__register_chrdev_region函式中,在討論這個函式之前,先要看看一個全域性的指標陣列chrdevs,他是核心用於裝置號分配和管理的核心元素,定義如下:
<fs/char_dev.c>
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;//主裝置號
unsigned int baseminor;//第一個次裝置號
int minorct;//次裝置的個數
char name[64];//驅動名
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
//#define CHRDEV_MAJOR_HASH_SIZE 255
這個陣列的每一項都是一個指向struct char_device_struct型別的指標。系統剛開始執行時,陣列的初始化狀態如下圖:
現在再看看register_chrdev_region函式,函式完成的主要功能是將當前裝置驅動程式要使用的裝置記錄到chrdevs陣列中,而有了這種對裝置號使用情況的跟蹤,系統就可以避免不同的裝置驅動程式使用同一個裝置號的情況出現。這就意味著當驅動程式呼叫這個函式時,事先已經明確知道他要使用的裝置號,之所以呼叫這個函式,是要將所管理的裝置號納入到核心的裝置號管理體系中,防止被的驅動程式錯誤使用到。當然如果它試圖使用的裝置號已經被之前某個驅動程式使用了,呼叫將會失敗,register_chrdev_region將返回錯誤碼給呼叫者,如果呼叫成功,函式將返回0。
下面看看它的核心函式:__register_chrdev_region:
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
struct char_device_struct *cd, **cp;
int ret = 0;
int i;
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);//分配struct char_device_struct結構
if (cd == NULL)
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
/* temporary */
if (major == 0) {
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
if (chrdevs[i] == NULL)
break;
}
if (i == 0) {
ret = -EBUSY;
goto out;
}
major = i;//主裝置號返回給呼叫者
ret = major;
}
cd->major = major;//主裝置號賦值
cd->baseminor = baseminor;//次裝置號賦值
cd->minorct = minorct;//次裝置個數賦值
strlcpy(cd->name, name, sizeof(cd->name));//驅動名賦值
i = major_to_index(major);//主裝置號得到雜湊雜湊索引值
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
/* Check for overlapping minor ranges. */
if (*cp && (*cp)->major == major) {
int old_min = (*cp)->baseminor;
int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
int new_min = baseminor;
int new_max = baseminor + minorct - 1;
/* New driver overlaps from the left. */
if (new_max >= old_min && new_max <= old_max) {
ret = -EBUSY;
goto out;
}
/* New driver overlaps from the right. */
if (new_min <= old_max && new_min >= old_min) {
ret = -EBUSY;
goto out;
}
}
cd->next = *cp;
*cp = cd;
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
函式首先分配一個struct char_device_struct型別的物件cd,然後對其進行一些初始化工作。這個過程完成之後,他就開始搜尋chrdevs陣列,是通過雜湊表的形式進行 的(我們可以看到雜湊表在核心的一些資料查詢的重要地位),首先會通過主裝置號生成一個雜湊關鍵值:
i = major_to_index(major);
在此追尋原始碼,發現major_to_index實現如下:其中CHRDEV_MAJOR_HASH_SIZE是之前看到的255
static inline int major_to_index(unsigned major)
{
return major % CHRDEV_MAJOR_HASH_SIZE;
}
所以i = major %255。
此後,函式將對chrdevs[i]元素管理的連結串列進行掃描,如果chrdevs[i]上已經有了連結串列節點,表明之前有別的驅動程式使用的主裝置號雜湊到chrdevs[i]上,為此函式就需要響應的邏輯確保當前正在操作的裝置號不會與這些已經使用的裝置號發生衝突,如果有衝突函式返回錯誤碼,表明本次呼叫失敗。如果本次呼叫使用的裝置號與chrdevs[i]上已經有的裝置號沒有發生衝突,先前分配的struct char_device_struct物件cd將加入到chrdevs[i]領銜的連結串列中成為一個新的節點。接下來不進行往下分析,看一個具體的例項:
假設chrdevs陣列初始化時,有個裝置的主裝置號為257,次裝置為0,1,2,3(有四個次裝置)。則呼叫如下
register_chrdev_region(MKDEV(257,0),4,”demodev”);
上述的函式呼叫完畢後,chrdevs陣列狀態如下圖:
圖中我們假設新分配的struct char_device_struc節點的記憶體地址為0xc8000004,i=257%255=2 則索引到chrdevs陣列的第二項。
接下來,假設又有一個裝置驅動使用主裝置號為2,次裝置號為0,則呼叫函式register_chrdev_region(MKDEV(2,0),1,”augdev”)來向系統註冊裝置號,i=2%255=2屬於同一個雜湊索引值,也索引到chrdevs陣列的第二項。這時候倆裝置號MKDEV(257,0)和MKDEV(2,0)並不衝突,所以註冊總會成功的。可以看出,節點在插入雜湊表中採用的是插入排序,這導致雜湊表按照major的大小進行遞增排序,此時chrdevs的陣列狀態如下圖:
在上圖的基礎上,**如果有另一個驅動程式也呼叫register_chrdev_region註冊裝置號,主裝置號也是257,那麼只要其次裝置號的範圍在[baseminor,baseminor+minorct]不與裝置“demodev”的次裝置
範圍發生重疊,系統依然會生成一個新的struct char_device_struc節點並加入到對應的雜湊連結串列中。如果在主裝置相同的情況下,如果次裝置號的範圍有重疊,則意味著有裝置號的衝突,將導致register_chrdev_region呼叫失敗。而對於主裝置相同的若干struct char_device_struc物件,當系統加入連結串列時,將根據其baseminor成員的大小進行遞增排序。**
2)alloc_chrdev_region函式
該函式時系統協助動態分配裝置號,分配的主裝置號的範圍在1-254之間
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
這個函式的核心呼叫也是__register_chrdev_region,相對於register_chrdev_region,alloc_chrdev_region在呼叫__register_chrdev_region時第一個引數為0,將導致__register_chrdev_region執行如下邏輯:
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
...
if (major == 0) {
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
if (chrdevs[i] == NULL)
break;
}
if (i == 0) {
ret = -EBUSY;
goto out;
}
major = i;
ret = major;
}
...
}
上述的邏輯很簡單,它在for迴圈中從chrdevs陣列的最後一項(也就是第254項)一次向前掃描,如果發現該陣列中的某項,比如第i項,對應的數值為NULL,那麼就把該項對應的索引值i作為分配的主裝置號返回給驅動程式,同時生成一個struct char_device_struct節點,並將其加入到chrdevs[i]對應的雜湊連結串列中。如果從第254項一直到第一項,其中所有的項對應的指標都不為NULL,那麼函式失敗並返回一非0值,表明動態分配裝置號失敗,分配成功後通過將新分配的裝置號返回給函式的呼叫者。
裝置號作為一種資源,當所對應的裝置驅動程式被解除安裝時,就需要把裝置號歸還給系統,以便分配給其他核心模組使用。無論是靜態分配還是動態分配,系統都是通過unregister_chrdev_region負責釋放裝置號。
下面分析下裝置號的釋放函式:
void unregister_chrdev_region(dev_t from, unsigned count)
{
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
}
static struct char_device_struct *
__unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct)
{
struct char_device_struct *cd = NULL, **cp;
int i = major_to_index(major);
mutex_lock(&chrdevs_lock);
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major == major &&
(*cp)->baseminor == baseminor &&
(*cp)->minorct == minorct)
break;
if (*cp) {
cd = *cp;
*cp = cd->next;
}
mutex_unlock(&chrdevs_lock);
return cd;
}
函式在chrdevs陣列中查詢引數from和count所對應的struct char_device_struct 物件節點,找到以後將其從連結串列中刪除並釋放該節點所佔用的記憶體,從而將對應的裝置號釋放以供其他裝置驅動模組使用。
以上就是Linux裝置號的構成和分配的核心機制,裝置號的管理主要通過chrdevs陣列來跟蹤系統中裝置號的使用情況,以防止實際使用中出現裝置號衝突的情況。