1. 程式人生 > >宋牧春: Linux裝置樹檔案結構與解析深度分析(1)

宋牧春: Linux裝置樹檔案結構與解析深度分析(1)

本文轉載自微信公眾號linuxer

作者簡介

宋牧春,linux核心愛好者,喜歡閱讀各種開原始碼(uboot、linux、ucos、rt-thread等),對於優秀的程式碼框架及其痴迷。現就職於一家手機研發公司,任職Android BSP開發工程師。

正文開始

1. Device Tree簡介

裝置樹就是描述單板資源以及裝置的一種文字檔案。至於出現的原因,大家可以上網查詢更多關於裝置樹的文章。本篇文章主要是更深層次的探討裝置檔案的構成以及kernel解析裝置樹的原理。所以,本篇內容並不是針對沒有任何裝置樹知識的讀者。本篇文章主要針對已經使用過裝置樹或者對裝置已經有所瞭解並想深層次的探究裝置樹的檔案結構和

kernel解析過程的讀者。

2. Device Tree編譯

Device Tree檔案的格式為dts,包含的標頭檔案格式為dtsidts檔案是一種人可以看懂的編碼格式。但是ubootlinux不能直接識別,他們只能識別二進位制檔案,所以需要把dts檔案編譯成dtb檔案。dtb檔案是一種可以被kerneluboot識別的二進位制檔案。把dts編譯成dtb檔案的工具是dtcLinux原始碼目錄下scripts/dtc目錄包含dtc工具的原始碼。在Linuxscripts/dtc目錄下除了提供dtc工具外,也可以自己安裝dtc工具,linux下執行:sudo apt-get install device-tree-compiler安裝

dtc工具。其中還提供了一個fdtdump的工具,可以反編譯dtb檔案。dtsdtb檔案的轉換如圖1所示。

dtc工具的使用方法是:dtc –I dts –O dtb –oxxx.dtb xxx.dts,即可生成dts檔案對應的dtb檔案了。


1 dtsdtb檔案轉換

3. Device Tree頭資訊

fdtdump工具使用,Linux終端執行ftddump –h,輸出以下資訊:

fdtdump -h

Usage: fdtdump [options] <file>

Options: -[dshV]

 -d, --debug   Dump debuginformation while decoding the file

 -s, --scan    Scan for an embeddedfdt in file

 -h, --help    Print this help andexit

 -V, --version Print version and exit

本文采用s5pv21_smc.dtb檔案為例說明fdtdump工具的使用。Linux終端執行fdtdump –sd s5pv21_smc.dtb > s5pv21_smc.txt,開啟s5pv21_smc.txt檔案,部分輸出資訊如下所示:

// magic:                             0xd00dfeed

// totalsize:                         0xce4 (3300)

// off_dt_struct:                   0x38

// off_dt_strings:                 0xc34

// off_mem_rsvmap:            0x28

// version:                           17

// last_comp_version:         16

// boot_cpuid_phys:            0x0

// size_dt_strings:               0xb0

// size_dt_struct:                 0xbfc

以上資訊便是Device Tree檔案頭資訊,儲存在dtb檔案的開頭部分。在Linux核心中使用struct fdt_header結構體描述。struct fdt_header結構體定義在scripts\dtc\libfdt\fdt.h檔案中。

struct  fdt_header  {

    fdt32_t  magic;             /* magic word FDT_MAGIC */

    fdt32_t  totalsize;         /* total size of DT block */

    fdt32_t  off_dt_struct;        /* offset to  structure */

    fdt32_t  off_dt_strings;       /* offset to  strings */

    fdt32_t  off_mem_rsvmap;       /* offset to  memory reserve map */

    fdt32_t  version;               /* format version */

    fdt32_t  last_comp_version;    /* last compatible  version */

    /*  version 2 fields below */

    fdt32_t  boot_cpuid_phys;   /* Which physical CPU  id we're booting on */

    /*  version 3 fields below */

    fdt32_t  size_dt_strings;   /* size of the  strings block */

    /*  version 17 fields below */

    fdt32_t  size_dt_struct;       /* size of the  structure block */

};

fdtdump工具的輸出資訊即是以上結構中每一個成員的值,struct fdt_header結構體包含了Device Tree的私有資訊。例如: fdt_header.magicfdt的魔數,固定值為0xd00dfeedfdt_header.totalsizefdt檔案的大小。使用二進位制工具開啟s5pv21_smc.dtb驗證。s5pv21_smc.dtb二進位制檔案頭資訊如圖2所示。從圖2中可以得到Device Tree的檔案是以大端模式儲存。並且,頭部資訊和fdtdump的輸出資訊一致。


2頭資訊

Device Tree中的節點資訊舉例如圖3所示。


3裝置樹全景試圖

上述.dts檔案並沒有什麼真實的用途,但它基本表徵了一個Device Tree原始檔的結構。1root結點"/"root結點下面含一系列子結點,本例中為"[email protected]""[email protected]";結點"[email protected]"下又含有一系列子結點,本例中為"[email protected]";各結點都有一系列屬性。這些屬性可能為空,如" an-empty-property";可能為字串,如"a-string-property";可能為字串陣列,如"a-string-list-property";可能為Cells(由u32整陣列成),如"second-child-property",可能為二進位制數,如"a-byte-data-property"Device Tree原始檔的結構分為headerfill_areadt_structdt_string四個區域。header為頭資訊,fill_area為填充區域,填充數字0dt_struct儲存節點數值及名稱相關資訊,dt_string儲存屬性名。例如:a-string-property就儲存在dt_string區,"A string"node1就儲存在dt_struct區域。

我們可以給一個裝置節點新增lable,之後可以通過&lable的形式訪問這個lable,這種引用是通過phandlepointer handle)進行的。例如,圖3中的node1就是一個lable[email protected]的子節點[email protected]通過&node1引用[email protected]節點。像是這種phandle的節點,在經過DTC工具編譯之後,&node1會變成一個特殊的整型數字n,假設n值為1,那麼在[email protected]節點下自動生成兩個屬性,屬性如下:

linux,phandle = <0x00000001>;

phandle = <0x00000001>;

[email protected]的子節點[email protected]中的a-reference-to-something = <&node1>會變成a-reference-to-something = < 0x00000001>。此處0x00000001就是一個phandle得值,每一個phandle都有一個獨一無二的整型值,在後續kernel中通過這個特殊的數字間接找到引用的節點。通過檢視fdtdump輸出資訊以及dtb二進位制檔案資訊,得到struct fdt_header和檔案結構之間的關係資訊如所示。


4 struct fdt_header和檔案結構之間的關係

4. Device Tree檔案結構

通過以上分析,可以得到Device Tree檔案結構如圖5所示。dtb的頭部首先存放的是fdt_header的結構體資訊,接著是填充區域,填充大小為off_dt_struct – sizeof(struct fdt_header),填充的值為0。接著就是struct fdt_property結構體的相關資訊。最後是dt_string部分。


5 Device Tree檔案結構

Device Tree原始檔的結構分為headerfill_areadt_structdt_string四個區域。fill_area區域填充數值0。節點(node)資訊使用struct fdt_node_header結構體描述。屬性資訊使用struct fdt_property結構體描述。各個結構體資訊如下:

struct fdt_node_header  {

    fdt32_t  tag;

    char  name[0];

};

struct  fdt_property  {

    fdt32_t  tag;

    fdt32_t  len;

    fdt32_t  nameoff;

    char  data[0];

};

struct fdt_node_header描述節點資訊,tag是標識node的起始結束等資訊的標誌位,name指向node名稱的首地址。tag的取值如下:

#define FDT_BEGIN_NODE   0x1    /*  Start node: full name */

#define FDT_END_NODE  0x2    /*  End node */

#define FDT_PROP          0x3    /*  Property: name off, size, content */

#define FDT_NOP            0x4    /* nop */

#define FDT_END            0x9

FDT_BEGIN_NODEFDT_END_NODE標識node節點的起始和結束,FDT_PROP標識node節點下面的屬性起始符,FDT_END標識Device Tree的結束識別符號。因此,對於每個node節點的tag識別符號一般為FDT_BEGIN_NODE,對於每個node節點下面的屬性的tag識別符號一般是FDT_PROP

描述屬性採用struct fdt_property描述,tag標識是屬性,取值為FDT_PROPlen為屬性值的長度(包括‘\0’,單位:位元組);nameoff為屬性名稱儲存位置相對於off_dt_strings的偏移地址。

例如:compatible ="samsung,goni", "samsung,s5pv210";compatible是屬性名稱,"samsung,goni", "samsung,s5pv210"是屬性值。compatible屬性名稱字串存放的區域是dt_string"samsung,goni", "samsung,s5pv210"存放的位置是fdt_property.data後面。因此fdt_property.data指向該屬性值。fdt_property.tag的值為屬性標識,len為屬性值的長度(包括‘\0’,單位:位元組),此處len = 29nameoffcompatible字串的位置相對於off_dt_strings的偏移地址,即&compatible = nameoff +off_dt_strings

dt_structDevice Tree中的結構如圖6所示。節點的巢狀也帶來tag識別符號的巢狀。


6 dt_struct結構圖

5. kernel解析Device Tree

Device Tree檔案結構描述就以上struct fdt_headerstruct fdt_node_headerstruct fdt_property三個結構體描述。kernel會根據Device Tree的結構解析出kernel能夠使用的struct property結構體。kernel根據Device Tree中所有的屬性解析出資料填充struct property結構體。struct property結構體描述如下:

struct property  {

    char  *name;                          /* property full name */

    int  length;                          /* property  value length */

    void  *value;                         /* property  value */

    struct  property *next;             /* next property  under the same node */

    unsigned  long _flags;

    unsigned  int unique_id;

    struct  bin_attribute attr;        /*屬性檔案,與sysfs檔案系統掛接 */

};

總的來說,kernel根據Device Tree的檔案結構資訊轉換成struct property結構體,並將同一個node節點下面的所有屬性通過property.next指標進行連結,形成一個單鏈表。

kernel中究竟是如何解析Device Tree的呢?下面分析函式解析過程。函式呼叫過程如圖7所示。kernelC語言階段的入口函式是init/main.c/stsrt_kernel()函式,在early_init_dt_scan_nodes()中會做以下三件事:

(1) 掃描/chosen或者/[email protected]節點下面的bootargs屬性值到boot_command_line,此外,還處理initrd相關的property,並儲存在initrd_startinitrd_end這兩個全域性變數中;

(2) 掃描根節點下面,獲取{size,address}-cells資訊,並儲存在dt_root_size_cellsdt_root_addr_cells全域性變數中;

(3) 掃描具有device_type = memory”屬性的/memory或者/[email protected]節點下面的reg屬性值,並把相關資訊儲存在meminfo中,全域性變數meminfo儲存了系統記憶體相關的資訊。


7函式呼叫過程

Device Tree中的每一個node節點經過kernel處理都會生成一個struct device_node的結構體,struct device_node最終一般會被掛接到具體的struct device結構體。struct device_node結構體描述如下:

struct device_node  {

    const  char *name;              /* node的名稱,取最後一次“/”“@”之間子串 */

    const  char *type;              /* device_type的屬性名稱,沒有為<NULL> */

    phandle  phandle;               /* phandle屬性值 */

    const  char *full_name;        /*指向該結構體結束的位置,存放node的路徑全名,例如:/chosen */

    struct  fwnode_handle fwnode;

    struct property *properties;  /*指向該節點下的第一個屬性,其他屬性與該屬性連結串列相接 */

    struct property *deadprops;   /* removed properties */

    struct device_node *parent;   /*父節點 */

    struct device_node *child;    /*子節點 */

    struct device_node *sibling;  /*姊妹節點,與自己同等級的node */

    struct kobject kobj;            /* sysfs檔案系統目錄體現 */

    unsigned  long _flags;          /*當前node狀態標誌位,見/include/linux/of.h line124-127 */

    void   *data;

};

/* flag descriptions (need to be  visible even when !CONFIG_OF) */

#define OF_DYNAMIC        1 /* node and properties were  allocated via kmalloc */

#define OF_DETACHED       2  /* node has been detached from the device tree*/

#define OF_POPULATED      3  /* device already created for the node */

#define OF_POPULATED_BUS  4 /* of_platform_populate recursed to children of this node */

struct device_node結構體中的每個成員作用已經備註了註釋資訊,下面分析以上資訊是如何得來的。Device Tree的解析首先從unflatten_device_tree()開始,程式碼列出如下:

/**

 * unflatten_device_tree - create tree of  device_nodes from flat blob

 *

 * unflattens the device-tree passed by the  firmware, creating the

 * tree of struct device_node. It also fills  the "name" and "type"

 * pointers of the nodes so the normal  device-tree walking functions

 * can be used.

 */

void  __init unflatten_device_tree(void)

{

    __unflatten_device_tree(initial_boot_params,  &of_root,

              early_init_dt_alloc_memory_arch);

    /*  Get pointer to "/chosen" and "/aliases" nodes for use  everywhere */

    of_alias_scan(early_init_dt_alloc_memory_arch);

}

/**

 * __unflatten_device_tree - create tree of  device_nodes from flat blob

 *

 * unflattens a device-tree, creating the

 * tree of struct device_node. It also fills  the "name" and "type"

 * pointers of the nodes so the normal  device-tree walking functions

 * can be used.

 * @blob: The blob to expand

 * @mynodes: The device_node tree created by  the call

 * @dt_alloc: An allocator that provides a  virtual address to memory

 * for the resulting tree

 */

static void __unflatten_device_tree(const  void *blob,

                struct device_node **mynodes,

                void * (*dt_alloc)(u64 size, u64 align))

{

    unsigned  long size;

    int  start;

    void  *mem;

     /*省略部分不重要部分 */

    /*  First pass, scan for size */

    start  = 0;

    size  = (unsigned long)unflatten_dt_node(blob, NULL, &start, NULL, NULL, 0,  true);

    size  = ALIGN(size, 4);

    /*  Allocate memory for the expanded device tree */

    mem  = dt_alloc(size + 4, __alignof__(struct device_node));

    memset(mem,  0, size);

    /*  Second pass, do actual unflattening */

    start  = 0;

    unflatten_dt_node(blob,  mem, &start, NULL, mynodes, 0, false);

}

分析以上程式碼,在unflatten_device_tree()中,呼叫函式__unflatten_device_tree(),引數initial_boot_params指向Device Tree在記憶體中的首地址,of_root在經過該函式處理之後,會指向根節點,early_init_dt_alloc_memory_arch是一個函式指標,為struct device_nodestruct property結構體分配記憶體的回撥函式(callback)。在__unflatten_device_tree()函式中,兩次呼叫unflatten_dt_node()函式,第一次是為了得到Device Tree轉換成struct device_nodestruct property結構體需要分配的記憶體大小,第二次呼叫才是具體填充每一個struct device_nodestruct property結構體。__unflatten_device_tree()程式碼列出如下:

/**

 * unflatten_dt_node - Alloc and populate a  device_node from the flat tree

 * @blob: The parent device tree blob

 * @mem: Memory chunk to use for allocating  device nodes and properties

 * @poffset: pointer to node in flat tree

 * @dad: Parent struct device_node

 * @nodepp: The device_node tree created by  the call

 * @fpsize: Size of the node path up at the  current depth.

 * @dryrun: If true, do not allocate device  nodes but still calculate needed

 * memory size

 */

static void * unflatten_dt_node(const  void *blob,

              void  *mem,

              int  *poffset,

              struct  device_node *dad,

              struct  device_node **nodepp,

              unsigned  long fpsize,

              bool  dryrun)

{

    const  __be32 *p;

    struct  device_node *np;

    struct  property *pp, **prev_pp = NULL;

    const  char *pathp;

    unsigned  int l, allocl;

    static  int depth;

    int  old_depth;

    int  offset;

    int  has_name = 0;

    int  new_format = 0;

    /*  獲取node節點的name指標到pathp */

    pathp  = fdt_get_name(blob, *poffset, &l);

    if  (!pathp)

       return  mem;

    allocl  = ++l;

    /*  version 0x10 has a more compact unit name here instead of the full

     * path. we accumulate the full path size  using "fpsize", we'll rebuild

     * it later. We detect this because the first  character of the name is

     * not '/'.

     */

    if  ((*pathp) != '/') {

       new_format  = 1;

       if  (fpsize == 0) {

           /*  root node: special case. fpsize accounts for path

            * plus terminating zero. root node only has  '/', so

            * fpsize should be 2, but we want to avoid  the first

            * level nodes to have two '/' so we use  fpsize 1 here

            */

           fpsize  = 1;

           allocl  = 2;

           l  = 1;

           pathp  = "";

       }  else {

           /*  account for '/' and path size minus terminal 0

            * already in 'l'

            */

           fpsize  += l;

           allocl  = fpsize;

       }

    }

    /*  分配struct device_node記憶體,包括路徑全稱大小 */

    np  = unflatten_dt_alloc(&mem, sizeof(struct device_node) + allocl,

              __alignof__(struct  device_node));

    if  (!dryrun) {

       char  *fn;

       of_node_init(np);

       /*  填充full_namefull_name指向該node節點的全路徑名稱字串 */

       np->full_name  = fn = ((char *)np) + sizeof(*np);

       if  (new_format) {

           /*  rebuild full path for new format */

           if  (dad && dad->parent) {

              strcpy(fn,  dad->full_name);

              fn  += strlen(fn);

           }

           *(fn++)  = '/';

       }

       memcpy(fn,  pathp, l);

       /*  節點掛接到相應的父節點、子節點和姊妹節點 */

       prev_pp  = &np->properties;

       if  (dad != NULL) {

           np->parent  = dad;

           np->sibling  = dad->child;

           dad->child  = np;

       }

    }

    /*  處理該node節點下面所有的property */

    for  (offset = fdt_first_property_offset(blob, *poffset);

         (offset >= 0);

         (offset = fdt_next_property_offset(blob,  offset))) {

       const  char *pname;

       u32  sz;

       if  (!(p = fdt_getprop_by_offset(blob, offset, &pname, &sz))) {

           offset  = -FDT_ERR_INTERNAL;

           break;

       }

       if  (pname == NULL) {

           pr_info("Can't  find property name in list !\n");

           break;

       }

       if  (strcmp(pname, "name") == 0)

           has_name  = 1;

       pp  = unflatten_dt_alloc(&mem, sizeof(struct property),

                  __alignof__(struct  property));

       if  (!dryrun) {

           /*  We accept flattened tree phandles either in

            * ePAPR-style "phandle"  properties, or the

            * legacy "linux,phandle"  properties.  If both

            * appear and have different values, things

            * will get weird.  Don't do that. */

           /*  處理phandle,得到phandle */

           if  ((strcmp(pname, "phandle") == 0) ||

               (strcmp(pname, "linux,phandle")  == 0)) {

              if  (np->phandle == 0)

                  np->phandle  = be32_to_cpup(p);

           }

           /*  And we process the "ibm,phandle" property

            * used in pSeries dynamic device tree

            * stuff */

           if  (strcmp(pname, "ibm,phandle") == 0)

              np->phandle  = be32_to_cpup(p);

           pp->name  = (char *)pname;

           pp->length  = sz;

           pp->value  = (__be32 *)p;

           *prev_pp  = pp;

           prev_pp  = &pp->next;

       }

    }

    /*  with version 0x10 we may not have the name property, recreate

     * it here from the unit name if absent

     */

    /*  為每個node節點新增一個name的屬性 */

    if  (!has_name) {

       const  char *p1 = pathp, *ps = pathp, *pa = NULL;

       int  sz;

       /*  屬性namevalue值為node節點的名稱,取“/”“@”之間的子串 */

       while  (*p1) {