Linux 内核:设备树(2)dtb转换成device_node
Linux 内核:设备树(2)dtb转换成device_node
背景
前面我们了解到dtb的内存分布以后(dtb格式),接下来就来看看内核是如何把设备树解析成所需的device_node
。
原文(有删改):https://www.cnblogs.com/downey-blog/p/10485596.html
基于arm平台,Linux 4.14
设备树的执行入口setup_arch
linux最底层的初始化部分在HEAD.s中,这是汇编代码,我们暂且不作过多讨论。
在head.s完成部分初始化之后,就开始调用C语言函数,而被调用的第一个C语言函数就是start_kernel
:
asmlinkage __visible void __init start_kernel(void)
{
//...
setup_arch(&command_line);
//...
}
而对于设备树的处理,基本上就在setup_arch()
这个函数中。
可以看到,在start_kernel()
中调用了setup_arch(&command_line);
void __init setup_arch(char **cmdline_p)
{
const struct machine_desc *mdesc;
// 根据传入的设备树dtb的首地址完成一些初始化操作
mdesc = setup_machine_fdt(__atags_pointer);
// ...
// 保证设备树dtb本身存在于内存中而不被覆盖
arm_memblock_init(mdesc);
// ...
// 对设备树具体的解析
unflatten_device_tree();
// ...
}
这三个被调用的函数就是主要的设备树处理函数:
setup_machine_fdt()
:根据传入的设备树dtb的首地址完成一些初始化操作。arm_memblock_init()
:主要是内存相关函数,为设备树保留相应的内存空间,保证设备树dtb本身存在于内存中而不被覆盖。用户可以在设备树中设置保留内存,这一部分同时作了保留指定内存的工作。unflatten_device_tree()
:对设备树具体的解析,事实上在这个函数中所做的工作就是将设备树各节点转换成相应的struct device_node
结构体。
下面我们再来通过代码跟踪仔细分析。
setup_machine_fdt
const struct machine_desc *mdesc;
// 根据传入的设备树dtb的首地址完成一些初始化操作
mdesc = setup_machine_fdt(__atags_pointer);
__atags_pointer
这个全局变量存储的就是r2的寄存器值,是设备树在内存中的起始地址,将设备树起始地址传递给setup_machine_fdt
,对设备树进行解析。
const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
const struct machine_desc *mdesc, *mdesc_best = NULL;
// 内存地址检查
if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
return NULL;
// 读取 compatible 属性
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
// 扫描各个子节点
early_init_dt_scan_nodes();
// ...
}
setup_machine_fdt
主要是获取了一些设备树提供的总览信息。
内存地址检查
先将设备树在内存中的物理地址转换为虚拟地址
然后再检查该地址上是否有设备树的魔数(magic),魔数就是一串用于识别的字节码:
- 如果没有或者魔数不匹配,表明该地址没有设备树文件,函数返回失败
- 否则验证成功,将设备树地址赋值给全局变量
initial_boot_params
。
读取compatible属性
逐一读取设备树根目录下的compatible
属性。
/**
* of_flat_dt_match_machine - Iterate match tables to find matching machine.
*
* @default_match: A machine specific ptr to return in case of no match.
* @get_next_compat: callback function to return next compatible match table.
*
* Iterate through machine match tables to find the best match for the machine
* compatible string in the FDT.
*/
const void * __init of_flat_dt_match_machine(const void *default_match,
const void * (*get_next_compat)(const char * const**))
{
const void *data = NULL;
const void *best_data = default_match;
const char *const *compat;
unsigned long dt_root;
unsigned int best_score = ~1, score = 0;
// 获取首地址
dt_root = of_get_flat_dt_root();
// 遍历
while ((data = get_next_compat(&compat))) {
// 将compatible中的属性一一与内核中支持的硬件单板相对比,
// 匹配成功后返回相应的machine_desc结构体指针。
score = of_flat_dt_match(dt_root, compat);
if (score > 0 && score < best_score) {
best_data = data;
best_score = score;
}
}
// ...
pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());
return best_data;
}
machine_desc
结构体中描述了单板相关的一些硬件信息,这里不过多描述。
主要的的行为就是根据这个compatible
属性选取相应的硬件单板描述信息;一般compatible
属性名就是"厂商,芯片型号"。
扫描各子节点
第三部分就是扫描设备树中的各节点,主要分析这部分代码。
void __init early_init_dt_scan_nodes(void)
{
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
of_scan_flat_dt(early_init_dt_scan_root, NULL);
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}
出人意料的是,这个函数中只有一个函数的三个调用,每次调用时,参数不一样。
of_scan_flat_dt
首先of_scan_flat_dt()
这个函数接收两个参数,一个是函数指针it,一个为相当于函数it执行时的参数。
/**
* of_scan_flat_dt - scan flattened tree blob and call callback on each.
* @it: callback function
* @data: context data pointer
*
* This function is used to scan the flattened device-tree, it is
* used to extract the memory information at boot before we can
* unflatten the tree
*/
int __init of_scan_flat_dt(int (*it)(unsigned long node,
const char *uname, int depth,
void *data),
void *data)
{
unsigned long p = ((unsigned long)initial_boot_params) +
be32_to_cpu(initial_boot_params->off_dt_struct);
int rc = 0;
int depth = -1;
do {
u32 tag = be32_to_cpup((__be32 *)p);
const char *pathp;
p += 4;
if (tag == OF_DT_END_NODE) {
depth--;
continue;
}
if (tag == OF_DT_NOP)
continue;
if (tag == OF_DT_END)
break;
if (tag == OF_DT_PROP) {
u32 sz = be32_to_cpup((__be32 *)p);
p += 8;
if (be32_to_cpu(initial_boot_params->version) < 0x10)
p = ALIGN(p, sz >= 8 ? 8 : 4);
p += sz;
p = ALIGN(p, 4);
continue;
}
if (tag != OF_DT_BEGIN_NODE) {
pr_err("Invalid tag %x in flat device tree!\n", tag);
return -EINVAL;
}
depth++;
pathp = (char *)p;
p = ALIGN(p + strlen(pathp) + 1, 4);
if (*pathp == '/')
pathp = kbasename(pathp);
rc = it(p, pathp, depth, data);
if (rc != 0)
break;
} while (1);
return rc;
}
结论:of_scan_flat_dt()
函数的作用就是扫描设备树中的节点,然后对各节点分别调用传入的回调函数。
那么重点关注函数指针,在上述代码中,传入的参数分别为
early_init_dt_scan_chosen
- ``early_init_dt_scan_root`
early_init_dt_scan_memory
从名称可以猜测,这三个函数分别是处理chosen节点、root节点中除子节点外的属性信息、memory节点。
early_init_dt_scan_chosen
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
boot_command_line
,boot_command_line
是一个静态数组,存放着启动参数,
int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,int depth, void *data){
// ...
p = of_get_flat_dt_prop(node, "bootargs", &l);
if (p != NULL && l > 0)
strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));
// ...
}
int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,
int depth, void *data)
{
unsigned long l;
char *p;
pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname);
if (depth != 1 || !data ||
(strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
return 0;
early_init_dt_check_for_initrd(node);
/* Retrieve command line */
// 找到设备树中的的chosen节点中的bootargs,并作为cmd_line
p = of_get_flat_dt_prop(node, "bootargs", &l);
if (p != NULL && l > 0)
strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));
// ...
pr_debug("Command line is: %s\n", (char*)data);
/* break now */
return 1;
}
经过代码分析,early_init_dt_scan_chosen
的作用是获取从chosen节点
中获取bootargs
,然后将bootargs
放入boot_command_line
中,作为启动参数。
而非字面意思的处理整个
chosen
。
以我之前调过的zynq平台为例:
/ {
model = "ZynqMP ZCU104 RevA";
compatible = "xlnx,zynqmp-zcu104-revA", "xlnx,zynqmp-zcu104", "xlnx,zynqmp";
aliases {
ethernet0 = &gem3;
gpio0 = &gpio;
i2c0 = &i2c1;
mmc0 = &sdhci1;
rtc0 = &rtc;
serial0 = &uart0;
serial1 = &uart1;
serial2 = &dcc;
spi0 = &qspi;
usb0 = &usb0;
};
chosen {
bootargs = "earlycon";
stdout-path = "serial0:115200n8";
};
memory@0 {
device_type = "memory";
reg = <0x0 0x0 0x0 0x80000000>;
};
};
在支持设备树的嵌入式系统中,实际上:
- uboot基本上可以不通过显式的
bootargs=xxx
来传递给内核,而是在env
拿出,并存放进设备树中的chosen
节点中 - Linux也开始在设备树中的
chosen
节点中获取出来,
这样子就可以做到针对uboot与Linux在bootargs
传递上的统一。
early_init_dt_scan_root
int __init early_init_dt_scan_root(unsigned long node, const char *uname,int depth, void *data)
{
dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT;
prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
if (prop)
dt_root_size_cells = be32_to_cpup(prop);
prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
if (prop)
dt_root_addr_cells = be32_to_cpup(prop);
// ...
}
通过进一步代码分析,early_init_dt_scan_root
为了将root节点中的#size-cells
和#address-cells
属性提取出来,并非获取root节点中所有的属性,放到全局变量dt_root_size_cells
和dt_root_addr_cells
中。
size-cells和address-cells表示对一个属性(通常是reg属性)的地址需要多少个四字节描述,而地址的长度需要多少个四字节描述,数据长度基本单位为4。
// 表示数据大小为一个4字节描述,32位
#size-cells = 1
// 表示地址由一个四字节描述
#address-cells = 1
// 而reg属性由四个四字节组成,所以存在两组地址描述,
// 第一组是起始地址为0x12345678,长度为0x100,
// 第二组起始地址为0x22,长度为0x4,
// 因为在<>中,所有数据都是默认为32位。
reg = <0x12345678 0x100 0x22 0x4>
early_init_dt_scan_memory
int __init early_init_dt_scan_memory(unsigned long node, const char *uname,int depth, void *data){
// ...
if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
return 0;
reg = of_get_flat_dt_prop(node, "reg", &l);
endp = reg + (l / sizeof(__be32));
while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
base = dt_mem_next_cell(dt_root_addr_cells, ®);
size = dt_mem_next_cell(dt_root_size_cells, ®);
early_init_dt_add_memory_arch(base, size);
}
}
函数先判断节点的unit name是memory@0,如果不是,则返回。然后将所有memory相关的reg属性取出来,根据address-cell和size-cell的值进行解析,然后调用early_init_dt_add_memory_arch()
来申请相应的内存空间。
memory@0 {
device_type = "memory";
reg = <0x0 0x0 0x0 0x80000000>, <0x8 0x00000000 0x0 0x80000000>;
};
到这里,setup_machine_fdt()函数对于设备树的第一次扫描解析就完成了,主要是获取了一些设备树提供的总览信息。
arm_memblock_init
// arch/arm/mm/init.c
void __init arm_memblock_init(const struct machine_desc *mdesc)
{
// ...
early_init_fdt_reserve_self();
early_init_fdt_scan_reserved_mem();
// ...
}
对于设备树的初始化而言,主要做了两件事:
- 调用
early_init_fdt_reserve_self
,根据设备树的大小为设备树分配空间,设备树的totalsize在dtb头部中有指明,因此当系统启动之后,设备树就一直存在在系统中。 - 扫描设备树节点中的"
reserved-memory
"节点,为其分配保留空间。
memblock_init
对于设备树的部分解析就完成了,主要是为设备树指定保留内存空间。
unflatten_device_tree
这一部分就进入了设备树的解析部分:
注意of_root
这个对象,我们后续文章中会提到它。实际上,解析以后的数据都是放在了这个对象里面。
void __init unflatten_device_tree(void)
{
// 展开设备树
__unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false);
// 扫描设备树
of_alias_scan(early_init_dt_alloc_memory_arch);
// ...
}
展开设备树
property原型
struct property {
char *name;
int length;
void *value;
struct property *next;
// ...
};
在设备树中,对于属性的描述是key = value
,这个结构体中的name和value分别对应key和value,而length表示value的长度;
next指针指向下一个struct property结构体(用于构成单链表)。
__unflatten_device_tree
__unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false);
我们再来看最主要的设备树解析函数:
void *__unflatten_device_tree(const void *blob,struct device_node *dad,
struct device_node **mynodes,void *(*dt_alloc)(u64 size, u64 align),bool detached)
{
int size;
// ...
size = unflatten_dt_nodes(blob, NULL, dad, NULL);
// ...
mem = dt_alloc(size + 4, __alignof__(struct device_node));
// ...
unflatten_dt_nodes(blob, mem, dad, mynodes);
}
主要的解析函数为unflatten_dt_nodes()
,在__unflatten_device_tree()
函数中,unflatten_dt_nodes()
被调用两次:
- 第一次是扫描得出设备树转换成device node需要的空间,然后系统申请内存空间;
- 第二次就进行真正的解析工作,我们继续看unflatten_dt_nodes()函数:
值得注意的是,在第二次调用unflatten_dt_nodes()时传入的参数为unflatten_dt_nodes(blob, mem, dad, mynodes);
unflatten_dt_nodes
第一个参数是设备树存放首地址,第二个参数是申请的内存空间,第三个参数为父节点,初始值为NULL,第四个参数为mynodes,初始值为of_node.
static int unflatten_dt_nodes(const void *blob,void *mem,struct device_node *dad,struct device_node **nodepp)
{
// ...
for (offset = 0;offset >= 0 && depth >= initial_depth;offset = fdt_next_node(blob, offset, &depth)) {
populate_node(blob, offset, &mem,nps[depth],fpsizes[depth],&nps[depth+1], dryrun);
// ...
}
}
这个函数中主要的作用就是从根节点开始,对子节点依次调用populate_node()
,从函数命名上来看,这个函数就是填充节点,为节点分配内存。
device_node原型
// include/linux/of.h
struct device_node {
const char *name;
const char *type;
phandle phandle;
const char *full_name;
// ...
struct property *properties;
struct property *deadprops; /* removed properties */
struct device_node *parent;
struct device_node *child;
struct device_node *sibling;
struct kobject kobj;
unsigned long _flags;
void *data;
// ...
};
name
:设备节点中的name
属性转换而来。type
:由设备节点中的device_type
转换而来。phandle
:有设备节点中的"phandle
"和"linux,phandle
"属性转换而来,特殊的还可能由"ibm,phandle
"属性转换而来。full_name
:这个指针指向整个结构体的结尾位置,在结尾位置存储着这个结构体对应设备树节点的unit_name
,意味着一个struct device_node
结构体占内存空间为sizeof(struct device_node)+strlen(unit_name)+字节对齐
。properties
:这是一个设备树节点的属性链表,属性可能有很多种,比如:"interrupts","timer","hwmods"等等。parent
,child
,sibling
:与当前属性链表节点相关节点,所以相关链表节点构成整个device_node的属性节点。kobj
:用于在/sys目录下生成相应用户文件。
populate_node
static unsigned int populate_node(const void *blob,int offset,void **mem,
struct device_node *dad,unsigned int fpsize,struct device_node **pnp,bool dryrun){
struct device_node *np;
// 申请内存
// 注,allocl是节点的unit_name长度(类似于chosen、memory这类子节点描述开头时的名字,并非.name成员)
np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,__alignof__(struct device_node));
// 初始化node(设置kobj,接着设置node的fwnode.ops。)
of_node_init(np);
// 将device_node的full_name指向结构体结尾处,
// 即,将一个节点的unit name放置在一个struct device_node的结尾处。
np->full_name = fn = ((char *)np) + sizeof(*np);
// 设置其 父节点 和 兄弟节点(如果有父节点)
if (dad != NULL) {
np->parent = dad;
np->sibling = dad->child;
dad->child = np;
}
// 为节点的各个属性分配空间
populate_properties(blob, offset, mem, np, pathp, dryrun);
// 获取,并设置device_node节点的name和type属性
np->name = of_get_property(np, "name", NULL);
np->type = of_get_property(np, "device_type", NULL);
if (!np->name)
np->name = "<NULL>";
if (!np->type)
np->type = "<NULL>";
// ...
}
一个设备树中节点转换成一个struct device_node
结构的过程渐渐就清晰起来,现在我们接着看看populate_properties()
这个函数,看看属性是怎么解析的,
populate_properties
static void populate_properties(const void *blob,int offset,void **mem,struct device_node *np,const char *nodename,bool dryrun){
// ...
for (cur = fdt_first_property_offset(blob, offset);
cur >= 0;
cur = fdt_next_property_offset(blob, cur))
{
fdt_getprop_by_offset(blob, cur, &pname, &sz);
unflatten_dt_alloc(mem, sizeof(struct property),__alignof__(struct property));
if (!strcmp(pname, "phandle") || !strcmp(pname, "linux,phandle")) {
if (!np->phandle)
np->phandle = be32_to_cpup(val);
pp->name = (char *)pname;
pp->length = sz;
pp->value = (__be32 *)val;
*pprev = pp;
pprev = &pp->next;
// ...
}
}
}
从属性转换部分的程序可以看出,对于大部分的属性,都是直接填充一个struct property
属性;
而对于"phandle"
属性和"linux,phandle"
属性,直接填充struct device_node
的phandle
字段,不放在属性链表中。
扫描节点:of_alias_scan
从名字来看,这个函数的作用是解析根目录下的alias
struct device_node *of_chosen;
struct device_node *of_aliases;
void of_alias_scan(void * (*dt_alloc)(u64 size, u64 align)){
of_aliases = of_find_node_by_path("/aliases");
of_chosen = of_find_node_by_path("/chosen");
if (of_chosen) {
if (of_property_read_string(of_chosen, "stdout-path", &name))
of_property_read_string(of_chosen, "linux,stdout-path",
&name);
if (IS_ENABLED(CONFIG_PPC) && !name)
of_property_read_string(of_aliases, "stdout", &name);
if (name)
of_stdout = of_find_node_opts_by_path(name, &of_stdout_options);
}
for_each_property_of_node(of_aliases, pp) {
// ...
ap = dt_alloc(sizeof(*ap) + len + 1, __alignof__(*ap));
if (!ap)
continue;
memset(ap, 0, sizeof(*ap) + len + 1);
ap->alias = start;
of_alias_add(ap, np, id, start, len);
// ...
}
}
of_alias_scan()
函数先是处理设备树chosen节点中的"stdout-path"或者"stdout"属性(两者最多存在其一),然后将stdout指定的path赋值给全局变量of_stdout_options
,并将返回的全局struct device_node
类型数据赋值给of_stdout
,指定系统启动时的log输出。
接下来为aliases节点申请内存空间,如果一个节点中同时没有name
/phandle
/linux,phandle
,则被定义为特殊节点,对于这些特殊节点将不会申请内存空间。
然后,使用of_alias_add()
函数将所有的aliases内容放置在aliases_lookup
链表中。
转换过程总结
此后,内核就可以根据device_node
来创建设备。
Linux 内核:设备树(2)dtb转换成device_node的更多相关文章
- linux设备驱动程序-设备树(1)-dtb转换成device_node
linux设备驱动程序-设备树(1)-dtb转换成device_node 本设备树解析基于arm平台 从start_kernel开始 linux最底层的初始化部分在HEAD.s中,这是汇编代码,我们暂 ...
- Linux内核 设备树操作常用API【转】
转自:https://www.linuxidc.com/Linux/2017-02/140818.htm 一文中介绍了设备树的语法,这里主要介绍内核中提供的操作设备树的API,这些API通常都在&qu ...
- Linux内核 设备树操作常用API
Linux设备树语法详解一文中介绍了设备树的语法,这里主要介绍内核中提供的操作设备树的API,这些API通常都在"include/of.h"中声明. device_node 内核中 ...
- linux设备驱动程序-设备树(0)-dtb格式
linux设备树dtb格式 设备树的一般操作方式是:开发人员根据开发需求编写dts文件,然后使用dtc将dts编译成dtb文件. dts文件是文本格式的文件,而dtb是二进制文件,在linux启动时被 ...
- Linux dts 设备树详解(二) 动手编写设备树dts
Linux dts 设备树详解(一) 基础知识 Linux dts 设备树详解(二) 动手编写设备树dts 文章目录 前言 硬件结构 设备树dts文件 前言 在简单了解概念之后,我们可以开始尝试写一个 ...
- Linux dts 设备树详解(一) 基础知识
Linux dts 设备树详解(一) 基础知识 Linux dts 设备树详解(二) 动手编写设备树dts 文章目录 1 前言 2 概念 2.1 什么是设备树 dts(device tree)? 2. ...
- (转)Linux内核基数树应用分析
Linux内核基数树应用分析 ——lvyilong316 基数树(Radix tree)可看做是以二进制位串为关键字的trie树,是一种多叉树结构,同时又类似多层索引表,每个中间节点包含指向多个节点的 ...
- Linux 获取设备树源文件(DTS)里描述的资源
Linux 获取设备树源文件(DTS)里的资源 韩大卫@吉林师范大学 在linux使用platform_driver_register() 注册 platform_driver 时, 需要在 plat ...
- Linux下将UTF8编码批量转换成GB2312编码的方法
Linux下将UTF8编码批量转换成GB2312编码的方法 在sqlplus中导入UTF8编码的sql脚本就会出现乱码错误,这时就需要将UTF8编码转换成GB2312编码,下面为大家介绍下在Linux ...
- Linux 获取设备树源文件(DTS)里的资源【转】
本文转载自:http://blog.csdn.net/keleming1/article/details/51036000 http://www.cnblogs.com/dyllove98/archi ...
随机推荐
- 数仓OLAP技术
数据应用,是真正体现数仓价值的部分,包括且又不局限于 数据可视化.BI.OLAP.即席查询,实时大屏,用户画像,推荐系统,数据分析,数据挖掘,人脸识别,风控反欺诈,ABtest等等 OLAP(On-L ...
- 《最新出炉》系列入门篇-Python+Playwright自动化测试-43-分页测试
1.简介 分页测试,这种一般都是公共的方法系统中都写好了,这种一般出现是数据展示比较多的时候,会采取分页的方法,而且比较固定,一般是没有问题的,因此它非常适合自动化测试,但是如何使用playwrigh ...
- rails 之下载
控制器 def index #传给前端展示层当前的id @id = 6 end # http://127.0.0.1:3000/admin/category_statistics/export_tab ...
- SpringBoot 二维码生成
一.基于Google开发工具包ZXing生成二维码 1.引入需要的依赖 <!-- zxing生成二维码 --> <dependency> <groupId>com. ...
- ABP-VNext 用户权限管理系统实战06---实体的创建标准及迁移
在apb-vnext的实体的创建中可以确实字段的长度.说明.对应的表.表中给字段加的索引 以项目中的订单表为例,如下: [Comment("订单主表")] [Table(" ...
- spring boot整合maybatis plus 的 文件生成代码
/** * 代码生成 */public class AutoGenerator_ { public static void main(String[] args) { AutoGenerator ge ...
- navicat premium 15 下载和激活
Navicat Premium 15 下载地址 链接:https://pan.baidu.com/s/1bL-M3-hkEa4M-547giVjYQ?pwd=1107 推荐安装参考地址:https:/ ...
- Android 12(S) MultiMedia(十二)MediaCodecList & IOmxStore
这节来了解下MediaCodecList相关代码路径: frameworks/av/media/libstagefright/MediaCodecList.cpp frameworks/av/medi ...
- ABP邮件发送
ABP Vnext发邮件要使用AbpMailKitModule的实现IEmailSender,要检查添加了Volo.Abp.MailKit,其dependon 要添加typeof() 它使用Sett ...
- Part1--软件规范总纲
开发人员规范 软件代码编写规范 套话 目的:统一公司编码风格:提高代码易读性.可靠性和稳定性:减少软件维护成本提高生产力 基本原则:维持代码易读.可维护:保持代码清晰:尽可能复用代码 实用规则 缩进 ...