Linux裝置驅動-核心如何管理裝置號
開篇
本文引用的核心程式碼參考來自版本 linux-5.15.4 。
在 Linux 系統中,每個註冊到系統的裝置都有一個編號,這個編號便是 Linux 系統中的裝置號。
裝置號作為一種系統資源,需要加以管理。否則,如果裝置號與驅動程式對應關係錯誤,就會引起混亂或引起潛在的問題。
通過檢視 /proc/devices 檔案可以得到系統中註冊的裝置,第一列為主裝置號,第二列為裝置名稱
$ cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty 4 ttyS 5 /dev/tty 5 /dev/console 5 /dev/ptmx 5 ttyprintk 6 lp 7 vcs 10 misc 13 input 21 sg ... Block devices: 7 loop 8 sd 9 md 11 sr 65 sd 66 sd ...
裝置號的構成
一個裝置號由主裝置號和次裝置號構成。
主裝置號對應裝置驅動程式,同一類裝置一般使用相同的主裝置號。
次裝置號由驅動程式使用,驅動程式用來描述使用該驅動的裝置的序號,序號一般從 0 開始。
Linux 裝置號用 dev_t 型別的變數進行標識,這是一個 32位 無符號整數,核心原始碼定義為:
/* <include/linux/types.h> */
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
主裝置號用 dev_t 的高 12 位表示,次裝置號用 dev_t 低 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 中提取主裝置號。巨集 MINOR 用來從裝置號 dev 中提取次裝置號。巨集 MKDEV 用來將主裝置號 ma 和 次裝置號 mi 組合成 dev_t 型別的裝置號。
另外,核心也提供了從裝置檔案 i-節點結構(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);
}
通過函式原始碼可知,獲取主裝置號和次裝置號最終是通過巨集定義完成的。
核心管理裝置號
以字元裝置為例,向核心中註冊裝置號,核心是如何分配和管理裝置號的呢?
在編寫字元裝置驅動時,可以通過如下兩個系統呼叫向核心註冊裝置號:
- register_chrdev_region()
註冊一系列連續的字元裝置號,主裝置號需要函式呼叫者指定。此函式的原型為:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
引數 from 為裝置編號,包含主裝置號和次裝置號。引數 count 用於指定連續裝置號的個數,即當前驅動程式所管理的同類裝置的個數。引數 name 為裝置或驅動的名字。
執行成功,返回 0。失敗,則返回一個負值的錯誤碼。
- alloc_chrdev_region()
註冊一系列連續的字元裝置號,主裝置號是由核心動態分配得到的。此函式的原型為:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
引數 dev 為函式的傳出引數,用於記錄動態分配的裝置號,如果申請多個裝置號,則此引數記錄這些連續裝置號的起始值。
引數 baseminor 指定首個次裝置號。引數 count 用於指定連續裝置號的個數。引數 name 為裝置或驅動的名字。
執行成功,返回 0。失敗,則返回一個負值的錯誤碼。
接下來,看看這兩個函式的內部實現流程。
register_chrdev_region()
該函式的核心原始碼為,關鍵部分已加註釋:
/* <fs.char_dev.c> */
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);
}
由程式碼內容可知,這個函式的核心處理流程是通過內部呼叫 __register_chrdev_region()
實現的。
這個函式的主要功能是,將要使用的裝置號註冊到核心的裝置號管理體系中,避免多個驅動程式使用相同的裝置號,而引起的混亂。
如果註冊裝置號已經被使用,則會返回錯誤碼告知呼叫者,即呼叫失敗。如果成功,則函式返回 0。
struct char_device_struct
在呼叫過程中,會涉及到一個關鍵的資料結構 struct char_device_struct
,其定義如下:
#define CHRDEV_MAJOR_HASH_SIZE 255
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];
定義結構體的同時,還定義了一個全域性性的指標陣列 chrdevs,是核心用來分配和管理裝置號的。陣列中的每一個元素都是指向 struct char_device_struct
型別的指標。
函式 register_chrdev_region()
的主要功能是將驅動程式要使用的裝置號記錄到 chrdevs 陣列中。
__register_chrdev_region()
核心處理函式 __register_chrdev_region() 內部,首先會分配一個 struct char_device_struct
型別的指標 cd,然後對其進行初始化(已經去除無關程式碼):
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
...
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
return ERR_PTR(-ENOMEM);
/* 根據主裝置號計算索引,搜尋 chrdevs 陣列,判斷主裝置號是否可用 */
i = major_to_index(major);
for (curr = chrdevs[i]; curr; prev = curr, curr = curr->next)
{
if (curr->major < major)
continue;
if (curr->major > major)
break;
if (curr->baseminor + curr->minorct <= baseminor)
continue;
if (curr->baseminor >= baseminor + minorct)
break;
goto out;
}
/* 初始化資訊 */
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strlcpy(cd->name, name, sizeof(cd->name));
/* 將分配的 cd 加入到 chrdevs[i] 中 */
if (!prev) {
cd->next = curr;
chrdevs[i] = cd;
} else {
cd->next = prev->next;
prev->next = cd;
}
...
}
函式申請完記憶體資源後,開始掃描 chrdevs 陣列,確保當前註冊的裝置號可用。如果裝置號佔用,函式返回錯誤碼,即呼叫失敗。
如果裝置號可用,則用裝置號和名字資訊初始化。初始化完成後,將 struct char_device_struct
加入到核心管理裝置號的連結串列中。
alloc_chrdev_region()
此函式由核心動態分配裝置號,該函式的核心原始碼如下,關鍵部分已加註釋:
/* <fs.char_dev.c> */
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)
{
...
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
return ERR_PTR(-ENOMEM);
/* 查詢可用的主裝置號 */
f (major == 0) {
ret = find_dynamic_major();
if (ret < 0) {
pr_err("CHRDEV \"%s\" dynamic allocation region is full\n",
name);
goto out;
}
major = ret;
}
...
}
在分配完成 struct char_device_struct
記憶體資源之後,通過 find_dynamic_major()
查詢可用的主裝置號。後續處理與 register_chrdev_region()
函式呼叫處理相同。
裝置號分配成功後,將 struct char_device_struct
型別指標返回給 alloc_chrdev_region()
函式。然後再通過如下程式碼將新分配的裝置號返回給 alloc_chrdev_region()
呼叫者:
*dev = MKDEV(cd->major, cd->baseminor);
小結
本文主要介紹了以下幾點內容:
- 裝置號是如何構成的,以及對其操作的巨集定義。
- register_chrdev_region() 和 alloc_chrdev_region() 實現細節。
- 記錄裝置號相關資訊的關鍵資料結構
struct char_device_struct
。 - 核心通過 chrdevs 陣列來跟蹤系統中裝置號的使用情況。
————————————————————————————————————————————————————
關注微信公眾號【一起學嵌入式】,回覆 “linux”,獲取Linux經典書籍資料