Linux內核驅動:cdev、misc以及device三者之間的聯系和區別


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_tinodecdev_map里面的cdev聯系起來了。

dev_t是聯系inode/cdev/device三者的紐帶

在創建device設備時,如果定義了dev_t,那么會創建一個inode節點,並且綁定一個默認帶有一個file_operations的cdev。

如果針對該dev_t,定義了我們自己的file_operations,再調用cdev_add(),那么就會用自己定義的file_operations替換掉默認的file_operations

這樣,devicecdev以及自己定義的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的實現中,就將devicecdev關聯了起來,並定義自己的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 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM