1. 程式人生 > 其它 >Linux裝置驅動-核心如何管理裝置號

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經典書籍資料