Linux內核驅動:cdev、misc以及device三者之間的聯系和區別
背景
我想在cdev中使用dev_err
等log打印函數,但是跟蹤了一下cdev中的原型,發現並不是我想要的。
常見的驅動是這樣子使用dev_err
的:
// 某個驅動,這里是電池有關的
static int32_t oz8806_read_byte(struct oz8806_data *data, uint8_t index, uint8_t *dat)
{
struct i2c_client *client = data->myclient;
// ...
dev_err(&client->dev, "%s: err %d, %d times\n", __func__, ret, i);
// ...
}
而i2c_client
原型是這樣子的,dev就是一個device:
// include/linux/i2c.h
struct i2c_client {
// ...
struct device dev; /* the device structure */
// ...
};
那么,我想只要找到cdev
中的dev
,也可以這樣子用,對吧?但是:
// include/linux/cdev.h
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
而dev_t
長這個樣子:
// include/linux/types.h
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
我在困惑dev_t
是什么東西以后,找到了下面這篇文章。
原文(有刪改):https://blog.csdn.net/leochen_career/article/details/78540916
從/dev目錄說起
從事Linux嵌入式驅動開發的人,都很熟悉下面的一些基礎知識, 比如,對於一個char類型的設備,我想對其進行read wirte 和ioctl操作,那么:
1、我們通常會在內核驅動中實現一個file_operations
結構體,然后分配主次設備號,調用cdev_add函數進行注冊。
2、從/proc/devices
下面找到注冊的設備的主次設備號,在用mknod /dev/char_dev c major minor
命令行創建設備節點。
3、在用戶空間open /dev/char_dev
這個設備,然后進行各種操作。
OK,字符設備模型就這么簡單,很多ABC教程都是一個類似的實現。
然后我們去看內核代碼時,突然一臉懵逼。。。怎么內核代碼里很多常用的驅動的實現不是這個樣子的?沒看到有file_operations
結構體,我怎么使用這些驅動?看到了/dev
目錄下有需要的char設備,可是怎么使用呢?
Linux驅動模型的一個重要分界線
linux2.6版本以前,普遍的用法就像我上面說的一樣。但是linux2.6版本以后,引用了Linux設備驅動模型,開始使用了基於sysfs的文件系統,出現讓我們不是太明白的那些Linux內核驅動了。
也就是說,我們熟悉的那套驅動模式是2.6版本以前的(當然這是基礎,肯定要會的)
我們不熟悉的驅動模型是2.6版本以后的。
cdev、misc以及device
cdev和device的區別和聯系
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
struct device {
struct device *parent;
struct device_private *p;
struct kobject kobj;
const char *init_name; /* initial name of the device */
struct device_driver *driver; /* which driver has allocated this device */
void * driver_data ; /* Driver data, set and get with dev_set/get_drvdata */
dev_t devt; /* dev_t, creates the sysfs "dev" */
u32 id; /* device instance */
void (*release)( struct device *dev);
// ...
};
通過看這兩個結構體發現,其實cdev和device之間沒啥直接的聯系,只有一個 dev_t
和kobject
是相同的。 dev_t 這個是聯系兩者的一個紐帶了 。
通常可以這么理解: cdev是傳統驅動的設備核心數據結構,device是Linux設備驅動模型中的核心數據結構。
miscdevice和device的區別和聯系
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group groups;
const char *nodename;
umode_t mode;
};
從定義可以看出,miscdevice是 device的子類,是從device派生出來的結構體,也是屬於device范疇的,也就是該類設備會統一在/sys目錄下進行管理了。
miscdevice和cdev的區別和聯系
通過上面的數據結構可以看到,兩者都有一個file_operations
和 dev_t
(misdevice由於主設備號固定,所以結構體里只有一個minor)。
從數據結構上看,miscdevice是device和cdev的結合。
注冊device與cdev的不同
要注冊一個device設備,需要調用核心函數device_register()
(或者說是device_add()
函數) ;
要注冊一個cdev設備,需要調用核心函數register_chrdev()
(或者說是cdev_add()
函數)
device_register函數與cdev、misc以及device
為了方便理解cdev、misc以及device這3者的關系,我們看看device_register()
的實際調用。
有關的代碼位於:
drivers/base/core.c
device_register
device_add // 其中包含2個關鍵函數
// 將相關信息添加到/sys文件系統中(略)
device_create_file
// 將相關信息添加到/devtmpfs文件系統中
devtmpfs_create_node
device_create_file
不做詳細解析,因為devices本來就是/sys文件系統中的重要概念
關鍵是devtmpfs_create_node
什么是Devtmpfs 的概念
Devtmpfs lets the kernel create a tmpfs very early at kernel initialization , before any driver core device is registered . Every device with a major / minor will have a device node created in this tmpfs instance . After the rootfs is mounted by the kernel , the populated tmpfs is mounted at
/ dev
.
devtmpfs_create_node()
函數的核心是調用了內核的 vfs_mknod()
函數,這樣就在devtmpfs系統中創建了一個設備節點,當devtmpfs被內核mount到/dev目錄下時,該設備節點就存在於/dev目錄下,比如/dev/char_dev之類的。
vfs_mknod
函數中會調用init_special_inode
.
init_special_inode
如果node是一個char設備,會給i_fop 賦值一個默認的def_chr_fops,也就是說對該node節點,有一個默認的操作。在open一個字符設備文件時,最終總會調用chrdev_open,然后調用各個char設備自己的file_operations 定義的open函數。
void init_special_inode( struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
} else if (S_ISFIFO(mode))
inode->i_fop = &pipefifo_fops;
else if (S_ISSOCK(mode))
; /* leave it no_open_fops */
else
printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
" inode %s:%lu\n" , mode, inode->i_sb->s_id,
inode->i_ino);
}
/*
* Dummy default file-operations: the only thing this does
* is contain the open that then fills in the correct operations
* depending on the special file...
*/
const struct file_operations def_chr_fops = {
.open = chrdev_open ,
.llseek = noop_llseek,
};
static int chrdev_open( struct inode *inode, struct file *filp)
{
ret = -ENXIO;
fops = fops_get(p->ops);
if (!fops)
goto out_cdev_put;
replace_fops (filp, fops);
if (filp->f_op->open) {
ret = filp->f_op-> open (inode, filp);
if (ret)
goto out_cdev_put;
}
return 0;
out_cdev_put:
cdev_put(p);
return ret;
}
上面分析的核心意思是:device_register()
函數除了注冊在/sys下面外;
還通過devtmpfs_create_node()
在/dev目錄下創建了一個設備節點(inode
),這個設備節點有一個默認的file_operations
。
cdev_add函數的實質
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add( struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map (cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
kobj_map()
函數:用來把字符設備編號和 cdev 結構變量一起保存到 cdev_map 這個散列表里 。當后續要打開一個字符設備文件時,通過調用 kobj_lookup() 函數, 根據設備編號就可以找到 cdev 結構變量,從而取出其中的 ops 字段 。
此處只是簡單的一個保存過程,並沒有將cdev和inode關聯起來。
有了這個關聯之后,當我們使用mknod 命令,就會創建一個inode節點,並且通過 dev_t
將inode
和cdev_map
里面的cdev聯系起來了。
dev_t是聯系inode/cdev/device三者的紐帶
在創建device設備時,如果定義了dev_t
,那么會創建一個inode
節點,並且綁定一個默認帶有一個file_operations
的cdev。
如果針對該dev_t
,定義了我們自己的file_operations
,再調用cdev_add()
,那么就會用自己定義的file_operations
替換掉默認的file_operations
。
這樣,device
和cdev
以及自己定義的file_operations
就關聯起來了。
struct class *my_class;
struct cdev my_cdev[N_MINORS];
dev_t dev_num;
static int __init my_init( void )
{
int i;
dev_t curr_dev;
/* Request the kernel for N_MINOR devices */
alloc_chrdev_region(&dev_num, 0, N_MINORS, "my_driver" );
/* Create a class : appears at /sys/class */
my_class = class_create(THIS_MODULE, "my_driver_class" );
/* Initialize and create each of the device(cdev) */
for (i = 0; i < N_MINORS; i++) {
/* Associate the cdev with a set of file_operations */
cdev_init(&my_cdev[i], &fops);
/* Build up the current device number. To be used further */
curr_dev = MKDEV(MAJOR(dev_num), MINOR(dev_num) + i);
/* Create a device node for this device. Look, the class is
* being used here. The same class is associated with N_MINOR
* devices. Once the function returns, device nodes will be
* created as /dev/my_dev0, /dev/my_dev1,... You can also view
* the devices under /sys/class/my_driver_class.
*/
device_create (my_class, NULL, curr_dev , NULL, "my_dev%d" , i);
/* Now make the device live for the users to access */
cdev_add (&my_cdev[i], curr_dev , 1);
}
return 0;
}
misdevice
在misdevice的實現中,就將device
和cdev
關聯了起來,並定義自己的file_operations
。
因此,misdevice
同時具有device
的標准設備模型,也定義了自己的file_operations
。
按照上面的思路,推測出misdevice的注冊過程應該是如下流程:
- 調用核心device注冊函數
device_add()
- 調用核心cdev注冊函數
cdev_add()
但是分析misc_register()
函數,發現只調用了device_add()
函數,並沒有調用cdev_add()
,那么自己定義的file_operations是如何和cdev關聯起來的呢?
其實misc.c中用了另外一套思路,但是原理是一樣的。
創建cdev_map
在misc_init()中, 通過register_chrdev()函數將主設備號為0,次設備號為0-255的所有cdev都通過cdev_add()進行了注冊;
也就是說將256個cdev放入到了cdev_map中,然后綁定的file_operations為默認的misc_open操作函數;
static int __init misc_init( void )
{
int err;
if ( register_chrdev (MISC_MAJOR, "misc" ,&misc_fops))
goto fail_printk;
misc_class->devnode = misc_devnode;
return 0;
// ...
}
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
cd = __register_chrdev_region(major, baseminor, count, name);
cdev = cdev_alloc();
cdev->ops = fops;
err = cdev_add (cdev, MKDEV(cd->major, baseminor), count);
// ...
}
在misc_register中進行鏈接
當調用misc_register()時,內核通過維護一個 misc_list 鏈表,misc設備在misc_register注冊的時候鏈接到這個鏈表。
/**
* misc_register - register a miscellaneous device
* @misc: device structure
*
* Register a miscellaneous device with the kernel. If the minor
* number is set to %MISC_DYNAMIC_MINOR a minor number is assigned
* and placed in the minor field of the structure. For other cases
* the minor number requested is used.
*
* The structure passed is linked into the kernel and may not be
* destroyed until it has been unregistered. By default, an open()
* syscall to the device sets file->private_data to point to the
* structure. Drivers don't need open in fops for this.
*
* A zero is returned on success and a negative errno code for
* failure.
*/
int misc_register( struct miscdevice * misc)
{
dev_t dev;
int err = 0;
INIT_LIST_HEAD(&misc->list);
dev = MKDEV(MISC_MAJOR, misc->minor);
misc->this_device =
device_create_with_groups(misc_class, misc->parent, dev,
misc, misc->groups, "%s" , misc->name);
/*
* Add it to the front, so that later devices can "override"
* earlier defaults
*/
list_add (&misc->list, &misc_list);
}
查找並替換新的file_operations
通過步驟2,可以操作對應dev_t的某個cdev的inode了,但此時的open為綁定的file_operations提供的默認的misc_open;
在此函數中,會根據dev_t在內核的misc_list中進行查找,然后用自己定義的file_operations替換到misc提供的那個默認的file_operations。
static int misc_open( struct inode * inode, struct file * file)
{
int minor = iminor(inode);
struct miscdevice *c;
int err = -ENODEV;
const struct file_operations *new_fops = NULL;
mutex_lock(&misc_mtx);
list_for_each_entry(c, &misc_list, list) {
if (c->minor == minor) {
new_fops = fops_get(c->fops);
break ;
}
}
if (!new_fops) {
mutex_unlock(&misc_mtx);
request_module( "char-major-%d-%d" , MISC_MAJOR, minor);
mutex_lock(&misc_mtx);
list_for_each_entry(c, &misc_list, list) {
if (c->minor == minor) {
new_fops = fops_get(c->fops);
break ;
}
}
if (!new_fops)
goto fail;
}
/*
* Place the miscdevice in the file's
* private_data so it can be used by the
* file operations, including f_op->open below
*/
file->private_data = c;
err = 0;
replace_fops(file, new_fops);
if (file->f_op->open)
err = file->f_op->open(inode,file);
fail:
mutex_unlock(&misc_mtx);
return err;
}
因此, miscdevice就是通過上述的步驟,實現了device cdev和自定義 file_operations的關聯。
一個簡單的小測試
通過命令mknod建立一個misc設備,然后去打開該設備,看下返回值,如下
root@imx6qsabresd:/dev# mknod test c 10 100
root@imx6qsabresd:/dev# cat /dev/test
cat: can't open '/dev/test': No such device
通過上面的分析,我們知道cat時,調用的open函數會最后調到misc_open(),該函數中返回的錯誤就是 -ENODEV,和看到的現象一致。
通過mknod建立一個設備,主次設備號隨便,看下返回值,如下
root@imx6qsabresd:/dev# mknod aaa c 1 0
root@imx6qsabresd:/dev# cat /dev/aaa
cat: can't open '/dev/aaa': No such device or address
通過上面的分析,我們知道,調用的open函數會最后調用到chrdev_open
,該函數中返回的錯誤就是-ENXIO
,和看到的錯誤提示一致。
Linux設備驅動模型下的cdev
通過上面的分析,我們知道當device_add()
注冊device時,會調用devtmpfs_create_node()
但是這個調用是有個約束條件的, 這個約束條件是device中必須定義了devt這個設備號。
所以,對於很多的平台設備platform_devices
(也就是那些在dts文件中定義的devices),在進行platform_device_add
()時,並不會在/dev下面出現inode節點。
但是i2c控制器,作為一個平台設備,確在/dev下面出現了設備節點,比如/dev/i2c-0
、/dev/i2c-1
。
這是什么原因的?分析內核代碼,我們發現i2c-dev.c
這個文件,其實這個文件就是用來創建/dev/i2c-0
這些設備的,它是一個我們熟悉的cdev設備驅動。
i2c_dev_init()
函數中使用了一種 內核通知機制,通過回調,在i2cdev_attach_adapter()
中創建了一個帶devt的device,那么自然會出現/dev/i2c-0節點了。內核通知機制的具體原理沒有研究。
Linux設備驅動模型下的eeprom驅動
我們傳統的用法是習慣在/dev
下注冊eeprom設備,也就是所謂的cdev設備,然后操作。
但是內核中用的是基於/sys系統的devices驅動模型,使用這個模型時,我們在dts文件中,在相應的i2c控制器下配置好好,就能在通過/sys文件系統進行訪問了。
比如如下的dts配置,我再i2c4下掛了一個eeprom芯片。內核會在初始化時將device於at24的驅動進行綁定(具體過程是dts以及platform設備模型的相關知識)
&i2c4{
eerpom: at24c04@54{
compatible = "24c04";
reg = <0x54>;
status = "okay";
};
};
設備注冊成功后,我們能看到如下信息
root@imx6qsabresd:/sys/class/i2c-dev/i2c-3/device/3-0054# ls
driver modalias of_node subsystem eeprom name power uevent
該目錄下有很多屬性文件,如果我們想讀寫eeprom,那么我們可以通過操作eeprom這個文件實現讀寫,這些都是設備驅動模型和sys文件系統的相關知識了。
那么,如果我想基於這個架構,增加一個/dev/eeprom
設備,該怎么實現呢?
首先,需要清楚,在內核調用at24_probe()
函數之前,eprom作為devices,已經被注冊到了系統中(這個過程是內核在解析dts配置文件時自動完成的,不是使用者自己注冊的)。
由於在內核注冊devices時,並沒有分配dev_t
這個設備號,所以不會在/dev下面創建設備節點。
在沒有設備號的情況下如何在/dev中創建設備
那么現在想創建一個cdev設備,但是這個設備號內核注冊時並沒有分配給這個device,怎么辦?
有兩種方案實現:
方案一:注冊misc設備
在at24_probe()
函數中,直接注冊一個misc設備,實現file_operations
,從而能夠建/dev/eeprom節點。
使用該方法時,需要理解一點,其實你是將eeprom這個芯片注冊了兩次設備(devices),一次是作為i2c的下掛的設備,由平台自動注冊的;還有一次是自己注冊的misc設備。這兩個設備都在/sys系統下存在。這兩個設備對sys來說是完全獨立的,只是因為我們自己將他們的驅動寫在了一起實現。
root@imx6qsabresd:/sys/class/i2c-dev/i2c-3/device/3-0054# ls
driver modalias of_node subsystem eeprom name power uevent
root@imx6qsabresd:/sys/devices/virtual/misc/eeprom# ls
dev power subsystem uevent
root@imx6qsabresd:/# ls -al /dev/eeprom
crw------- 1 root root 10, 100 Jan 1 1970 /dev/eeprom
方案二: 手動分配設備號
在at24_probe()
函數中,給入參i2c_client
里面的device里的dev_t
分配一個設備號,然后通過cdev_init()
、cdev_add()
函數將該設備號與file_operations
進行關聯,再通過mknod
創建節點。
使用此方法,實現了在/sys系統中只注冊了一個device
,但既能夠通過sys系統訪問,也能夠通過/dev/eeprom
設備節點的形式訪問了。
下面是自己嘗試在at24.c里面增加的代碼
static int eeprom_open( struct inode *inode, struct file *filp) {
unsigned int major, minor;
major = imajor(inode);
minor = iminor(inode);
printk( "%s, major = %d, minor = %d!\n" , func, major, minor);
return 0;
}
static ssize_t eeprom_read( struct file *filp, char *buf,
size_t len, loff_t *offset) {
printk( "%s, len = %d\n" , func, len);
return 0;
}
static ssize_t eeprom_write( struct file *filp, const char *buf,
size_t len, loff_t *offset) {
printk( "%s, len = %d\n" , func, len);
return 0;
}
static int eeprom_close( struct inode *inode, struct file *filp) {
printk( "%s\n" , func);
return 0;
}
static struct file_operations eeprom_fops = {
.open = eeprom_open,
.read = eeprom_read,
.write = eeprom_write,
.release = eeprom_close,
};
static int at24_probe( struct i2c_client *client, const struct i2c_device_id *id)
{
// ...
/*
* add the char device to system
*/
dev = &client->dev;
dev->devt = MKDEV(100, 0);
err = register_chrdev_region(dev->devt, 1, "eeprom" );
if (err < 0) {
dev_err(&client->dev, "Can't static register chrdev region!\n" );
return err;
}
cdev_init(&eeprom_cdev, &eeprom_fops);
err = cdev_add(&eeprom_cdev, dev->devt, 1);
if (err < 0) {
dev_err(&client->dev, "Can't add char device!\n" );
return err;
}
dev_err(&client->dev, "add char eeprom device!\n" );
return 0;
}
測試如下。
root@imx6qsabresd:/sys/class/i2c-dev/i2c-3/device/3-0054# ls
driver modalias of_node subsystem eeprom name power uevent
root@imx6qsabresd:/# ls -al /dev/eeprom
crw------- 1 root root 100, 0 Jan 1 1970 /dev/eeprom