Kernel字元裝置驅動框架
Linux裝置分為三大類:字元裝置,塊裝置和網路裝置,這三種裝置基於不同的裝置框架。相較於塊裝置和網路裝置,字元裝置在kernel中是最簡單的,也是唯一沒有基於裝置基礎框架(device結構)的裝置,因此字元裝置無法通過sysfs來訪問。那麼,使用者是如何來訪問字元裝置的?答案是通過裝置節點來訪問字元裝置。從這個角度來說,字元裝置的本質就是提供使用者介面,使得使用者可以通過 VFS 來訪問裝置節點,從而達到訪問字元裝置的目的。下面開始分析字元裝置框架的實現。
1,字元裝置資料結構:
struct cdev {
struct kobject kobj; // 雖然內嵌了kobject結構,但是沒有加入kset連結串列,因此不會在sysfs中生效
struct module *owner; // 所屬模組
const struct file_operations *ops; // 檔案操作介面,使用者通過該介面訪問字元裝置驅動
struct list_head list; // 裝置連結串列節點
dev_t dev; // 裝置號,由主從裝置號組成。裝置號是裝置節點和cdev之間的紐帶
unsigned int count; // 從裝置個數
};
2,字元設備註冊 register_chrdev():
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev
}
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
cd = __register_chrdev_region(major, baseminor, count, name); // 註冊裝置佔用的主從裝置號範圍
if (IS_ERR(cd))
return PTR_ERR(cd);
cdev = cdev_alloc(); // 分配cdev結構
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name); // 初始化cdev
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count); // 執行cdev的註冊
if (err)
goto out;
cd->cdev = cdev; // 將cdev關聯到裝置號結構
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, baseminor, count));
return err;
}
從上面可以看出,註冊一個字元裝置包含兩個步驟:1,申請/註冊裝置號範圍。2,註冊cdev
(1)申請/註冊裝置號範圍:
使用者通過裝置號訪問字元裝置,字元裝置號的分配就需要統一管理起來,防止同一個裝置號被重複分配。字元裝置號的分配通過__register_chrdev_region()介面完成。
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];
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); // 申請 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;
}
// 初始化該char_device_struct
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strlcpy(cd->name, name, sizeof(cd->name));
i = major_to_index(major); // chrdevs本質上是個hash陣列,計算hash code
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; // 將新的char_device_struct加入hash陣列連結串列
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
(2)cdev註冊:
字元設備註冊,本質上就是將cdev加入一個全域性範圍的連結串列上,當用戶通過裝置號查詢裝置時,從該連結串列上找到相應的cdev,呼叫 cdev 下的file_operations對裝置進行操作。實際情況要比這複雜點,考慮到一個裝置可能有多個子裝置,即一次設備註冊請求的裝置號可能橫跨多個主裝置號,linux設計了一個大小為255的hash陣列,以主裝置號為hash key,以 kobj_map 為元素,將1個或連續的多個probe加入以主裝置號為key的hash陣列,其中每個probe都指向該cdev。當查詢某個裝置的時候,以主裝置號為key查詢hash陣列的衝突連結串列,找到目標裝置號落入的probe,取出關聯的cdev。
struct kobj_map {
struct probe {
struct probe *next; // hash衝突連結串列
dev_t dev; // 首個裝置號
unsigned long range; // 子裝置個數
struct module *owner;
kobj_probe_t *get; // 獲取cdev方法
int (*lock)(dev_t, void *);
void *data; // 通常就是cdev
} *probes[255];
struct mutex *lock;
};
裝置加入hash陣列的介面如下:
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
struct module *module, kobj_probe_t *probe,
int (*lock)(dev_t, void *), void *data)
{
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1; // 該裝置橫跨的主裝置號個數
unsigned index = MAJOR(dev); // 起始主裝置號
unsigned i;
struct probe *p;
if (n > 255)
n = 255;
p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL); // 分配1個或多個連續的probe
if (p == NULL)
return -ENOMEM;
for (i = 0; i < n; i++, p++) {
p->owner = module;
p->get = probe;
p->lock = lock;
p->dev = dev;
p->range = range;
p->data = data;
} // 每個probe都關聯該cdev
mutex_lock(domain->lock);
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s;
*s = p;
} // 每個probe加入到相應的hash衝突連結串列
mutex_unlock(domain->lock);
return 0;
}
查詢裝置hash陣列的介面如下:
struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
{
struct kobject *kobj;
struct probe *p;
unsigned long best = ~0UL;
retry:
mutex_lock(domain->lock);
for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) { // 根據主裝置號找到hash衝突連結串列,迴圈訪問連結串列上的每個元素,找到目標裝置號落入的probe
struct kobject *(*probe)(dev_t, int *, void *);
struct module *owner;
void *data;
if (p->dev > dev || p->dev + p->range - 1 < dev)
continue; // 目標裝置號不在該probe範圍內,繼續尋找下一個
if (p->range - 1 >= best)
break;
if (!try_module_get(p->owner))
continue;
owner = p->owner;
data = p->data;
probe = p->get;
best = p->range - 1;
*index = dev - p->dev; // 找到目的probe
if (p->lock && p->lock(dev, data) < 0) {
module_put(owner);
continue;
}
mutex_unlock(domain->lock);
kobj = probe(dev, index, data); // 取出probe指向的 cdev
/* Currently ->owner protects _only_ ->probe() itself. */
module_put(owner);
if (kobj)
return kobj; // 返回 cdev
goto retry;
}
mutex_unlock(domain->lock);
return NULL;
}
(3)字元裝置的開啟:
static int chrdev_open(struct inode *inode, struct file *filp)
{
const struct file_operations *fops;
struct cdev *p;
struct cdev *new = NULL;
int ret = 0;
spin_lock(&cdev_lock);
p = inode->i_cdev;
if (!p) { // 判斷裝置是否已經被開啟,如果沒有被開啟過,查詢已經註冊的字元裝置
struct kobject *kobj;
int idx;
spin_unlock(&cdev_lock);
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, kobj); // 找到cdev
spin_lock(&cdev_lock);
/* Check i_cdev again in case somebody beat us to it while
we dropped the lock. */
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new;
list_add(&inode->i_devices, &p->list);
new = NULL; // 第一次開啟該字元裝置,將cdev賦值給inode,下次開啟時,不必再次查詢字元裝置連結串列
} else if (!cdev_get(p))
ret = -ENXIO;
} else if (!cdev_get(p))
ret = -ENXIO;
spin_unlock(&cdev_lock);
cdev_put(new);
if (ret)
return ret;
ret = -ENXIO;
fops = fops_get(p->ops); // 取出cdev的file_operations
if (!fops)
goto out_cdev_put;
replace_fops(filp, fops); // 替換字元裝置的file_operations
if (filp->f_op->open) {
ret = filp->f_op->open(inode, filp); // 呼叫字元裝置驅動的open()方法
if (ret)
goto out_cdev_put;
}
return 0;
out_cdev_put:
cdev_put(p);
return ret;
}
至此,我們已經分析了整個字元裝置的框架:從字元裝置的註冊,到字元裝置的查詢和開啟,框架架構簡單明瞭。字元裝置作為使用者訪問裝置的向上介面,通常跟i2c,spi,pci等向下介面驅動同時工作,以組成一個完整的硬體字元裝置。在這樣的裝置中,使用者通過字元裝置驅動介面獲取裝置,再通過i2c等驅動介面