戈壁老王

戈壁老王 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

做为一个不称职的老年码农,一直疏忽整理笔记,开博记录一下,用来丰富老年生活,

个人动态

戈壁老王 发布了文章 · 2020-12-25

configfs_sample.c 理解

转载:https://www.cnblogs.com/sctb/...

1. 编译运行

代码从如下链接获得:

https://github.com/torvalds/l...

编写 Makefile 文件:

obj-m += configfs_sample.o
all:
     make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
     make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

编译生成内核模块:

make
ls -l
  -rwxr--r-- 1 abin abin 10K Oct 27 16:58 configfs_sample.c?
  -rw-rw-r-- 1 abin abin 13K Oct 29 11:16 configfs_sample.ko
  -rw-rw-r-- 1 abin abin 603 Oct 29 11:16 configfs_sample.mod.c
  -rw-rw-r-- 1 abin abin 2.6K Oct 29 11:16 configfs_sample.mod.o
  -rw-rw-r-- 1 abin abin 12K Oct 29 11:16 configfs_sample.o
  -rw-rw-r-- 1 abin abin 166 Oct 29 11:16 Makefile
  -rw-rw-r-- 1 abin abin  92 Oct 29 11:16 modules.order
  -rw-rw-r-- 1 abin abin  0 Oct 29 11:16 Module.symvers

其中,configfs_sample.ko 使编译好的内核模块,使用如下命令加载该模块:

sudo modprobe configfs_sample.ko

如果出现如下错误:

modprobe: FATAL: Module configfs_sample.ko not found in directory /lib/modules/4.15.0-117-generic

将 configfs_sample.ko 拷贝进 /lib/modules/4.15.0-117-generic 再次尝试。

查看 configfs_sample.ko 内核模块是否已经挂载:

lsmod  | grep configfs_sample
configfs_sample 16384 0

查看 configfs 根目录:

ls -l /sys/kernel/config/
  total 0
  drwxr-xr-x 2 root root 0 Oct 29 11:32 01-childless
  drwxr-xr-x 2 root root 0 Oct 29 11:32 02-simple-children
  drwxr-xr-x 2 root root 0 Oct 29 11:32 03-group-children

如需卸载模块,使用如下命令:

sudo modprobe -r configfs_sample.ko

2. 代码理解

为了理解代码,我们首先整理一下 configfs 中的层级结构:

image

内核模块初始化入口:

module_init(configfs_example_init);

configfs_example_init(void) 函数:

/*
 * 此处是configfs_subsystem结构体数组,分别对应示例中的三个configfs子系统
 */
static struct configfs_subsystem *example_subsys[] = {
  &childless_subsys.subsys,
  &simple_children_subsys,
  &group_children_subsys,
  NULL,
};

static int __init configfs_example_init(void)
{
  int ret;
  int i;
  struct configfs_subsystem *subsys;    //configfs子系统
  
  for (i = 0; example_subsys[i]; i++) {
    subsys = example_subsys[i];
    
    config_group_init(&subsys->su_group);                //初始化 group
    mutex_init(&subsys->su_mutex);                            //初始化 mutex
    ret = configfs_register_subsystem(subsys);    //注册 subsystem
    if (ret) {
      printk(KERN_ERR "Error %d while registering subsystem %s\n", ret, subsys->su_group.cg_item.ci_namebuf);
      goto out_unregister;
    }
  }
  
  return 0;

out_unregister:
  for (i--; i >= 0; i--)
    configfs_unregister_subsystem(example_subsys[i]);
  
  return ret;
}

程序的主要逻辑是通过 struct configfs_subsystem 结构体传递给 configfs 的,下面分别对3个示例进行分析。

2.1 示例01-childless

变量 childless_subsys 的内容:

struct childless {
  struct configfs_subsystem subsys;
  int showme;
  int storeme;
};

static struct childless childless_subsys = {
  .subsys = {
    .su_group = {
      .cg_item = {
        .ci_namebuf = "01-childless",
        .ci_type = &childless_type,        //struct config_item_type,定义操作、属性等
      },
    },
  },
};

childless_type 变量如下:

static const struct config_item_type childless_type = {
  .ct_attrs    = childless_attrs,        //configfs_attribute,只定义了属性,没有定义对item和group操作
  .ct_owner    = THIS_MODULE,
};

childless_attrs 是一个数组,以 NULL 结尾。以下定义了三个属性,在 configfs 中,将表现为3个文件:

static struct configfs_attribute *childless_attrs[] = {
  &childless_attr_showme,
  &childless_attr_storeme,
  &childless_attr_description,
  NULL,
};

childless_attr_showme,childless_attr_storeme 和 childless_attr_description 三个属性是通过以下函数创建的:

CONFIGFS_ATTR_RO(childless_, showme);    //需要定义childless_showme_show()函数
CONFIGFS_ATTR(childless_, storeme);        //需要定义childless_storeme_show()和childless_storeme_store()函数
CONFIGFS_ATTR_RO(childless_, description);    //需要定义childless_description_show()函数

创建属性的函数有3个,在 linux/configfs.h 中:

#define CONFIGFS_ATTR(_pfx, _name)            \
static struct configfs_attribute _pfx##attr_##_name = {    \
    .ca_name    = __stringify(_name),        \
    .ca_mode    = S_IRUGO | S_IWUSR,        \
    .ca_owner    = THIS_MODULE,            \
    .show        = _pfx##_name##_show,        \
    .store        = _pfx##_name##_store,        \
}

#define CONFIGFS_ATTR_RO(_pfx, _name)            \
static struct configfs_attribute _pfx##attr_##_name = {    \
    .ca_name    = __stringify(_name),        \
    .ca_mode    = S_IRUGO,            \
    .ca_owner    = THIS_MODULE,            \
    .show        = _pfx##_name##_show,        \
}

#define CONFIGFS_ATTR_WO(_pfx, _name)            \
static struct configfs_attribute _pfx##attr_##_name = {    \
    .ca_name    = __stringify(_name),        \
    .ca_mode    = S_IWUSR,            \
    .ca_owner    = THIS_MODULE,            \
    .store        = _pfx##_name##_store,        \
}

可以看到,这三个宏定义函数可以根据传入的参数定义不同的结构体变量,变量名为:_pfx_attr_name,同时也会定义相应的 show 和 store 函数名。

CONFIGFS_ATTR(_pfx, _name) 需要定义 show 和 store 函数,相应的函数名分别为:_pfx_name_show 和 _pfx_name_store;

CONFIGFS_ATTR_RO(_pfx, _name)只需要定义 show 函数;

CONFIGFS_ATTR_WO(_pfx, _name) 只需要定义 store 函数。

childless_showme_show(),childless_storeme_show(),childless_storeme_store()和childless_description_show()的定义如下:

/*
 * 传入item,得到该item所在的childless结构体
 */
static inline struct childless *to_childless(struct config_item *item)
{
  return item ? container_of(to_configfs_subsystem(to_config_group(item)), struct childless, subsys) : NULL;
}

//childless_showme_show函数的实现,根据item找到结构体struct childless,输出childless->showme,然后将childless->showme加1
static ssize_t childless_showme_show(struct config_item *item, char *page)
{
  struct childless *childless = to_childless(item);
  ssize_t pos;

  pos = sprintf(page, "%d\n", childless->showme);
  childless->showme++;

  return pos;
}
//childless_storeme_show函数实现,输出结构体struct childless成员storeme的值
static ssize_t childless_storeme_show(struct config_item *item, char *page)
{
  return sprintf(page, "%d\n", to_childless(item)->storeme);
}
//childless_storeme_store函数实现,接受从文件系统输入的值,保存在struct childless成员storeme中
static ssize_t childless_storeme_store(struct config_item *item, const char *page, size_t count)
{
  struct childless *childless = to_childless(item);
  unsigned long tmp;
  char *p = (char *) page;

  tmp = simple_strtoul(p, &p, 10);        //将字符串转化为10进制数字,类型为unsigned long
  if (!p || (*p && (*p != '\n')))
    return -EINVAL;

  if (tmp > INT_MAX)
    return -ERANGE;

  childless->storeme = tmp;

  return count;
}
//childless_description_show函数实现,向page中填充内容
static ssize_t childless_description_show(struct config_item *item, char *page)
{
  return sprintf(page,
                 "[01-childless]\n"
                 "\n"
                 "The childless subsystem is the simplest possible subsystem in\n"
                 "configfs.  It does not support the creation of child config_items.\n"
                 "It only has a few attributes.  In fact, it isn't much different\n"
                 "than a directory in /proc.\n");
}

根据我的理解,page指向一块内存空间,这块空间接收来自文件系统的数据,同时,负责将configfs中的内容输出给文件系统。

showme 文件运行效果:

cat showme
  1
cat showme
  2

storeme 文件运行效果:

cat storeme
  0
echo 1111 > storeme
cat storeme
  1111

2.2 示例02-simple-children

simple_children_subsys 变量的内容:

struct simple_children {
  struct config_group group;
};

static struct configfs_subsystem simple_children_subsys = {
  .su_group = {
    .cg_item = {
      .ci_namebuf = "02-simple-children",
      .ci_type = &simple_children_type,
    },
  },
};

simple_children_type 内容:

static const struct config_item_type simple_children_type = {
  .ct_item_ops    = &simple_children_item_ops,        //item的操作
  .ct_group_ops    = &simple_children_group_ops,        //group的操作
  .ct_attrs    = simple_children_attrs,    //属性,和01相同
  .ct_owner    = THIS_MODULE,
};

可以看到,与01示例相比,02-siimple-chiildren 不光定义了 ct_attrs,还定义了 ct_item_ops 和 ct_group_ops。先看看赋值给 ct_attrs 的变量 simple_children_attrs:

static struct configfs_attribute *simple_children_attrs[] = {
  &simple_children_attr_description,        //属性,configfs中表示为文件
  NULL,
};

simple_children_attrs 定义对象的属性,在 configfs 中表示为文件。simple_children_attr_description 是通过宏函数创建的:

CONFIGFS_ATTR_RO(simple_children_, description);

在 CONFIGFS_ATTR_RO 宏函数中会使用 show 函数,定义如下:

static ssize_t simple_children_description_show(struct config_item *item, char *page)
{
  return sprintf(page,
                 "[02-simple-children]\n"
                 "\n"
                 "This subsystem allows the creation of child config_items.  These\n"
                 "items have only one attribute that is readable and writeable.\n");
}

此处和01示例没什么区别,主要看 ct_item_ops 和 ct_group_ops。simple_children_item_ops 的定义如下:

static struct configfs_item_operations simple_children_item_ops = {
  .release    = simple_children_release,        //实现release函数
};

simple_children_item_ops 是 struct configfs_item_operations 类型,也很简单,只定义了 release 函数,simple_children_release 函数定义如下:

static void simple_children_release(struct config_item *item)
{
  kfree(to_simple_children(item));    //将item转换为simple_children结构体并释放内核分配的内存
}

simple_children_group_ops 的定义如下:

static struct configfs_group_operations simple_children_group_ops = {
  .make_item    = simple_children_make_item,
};

simple_children_group_ops 也很简单,只实现了 make_item 函数。simple_children_make_item 如下:

/*
 * 传入item,得到该item所在的simple_children结构体
 */
static inline struct simple_children *to_simple_children(struct config_item *item)
{
  return item ? container_of(to_config_group(item), struct simple_children, group) : NULL;
}

static struct config_item *simple_children_make_item(struct config_group *group, const char *name)
{
  struct simple_child *simple_child;

  simple_child = kzalloc(sizeof(struct simple_child), GFP_KERNEL);    //为simple_child分配内存
  if (!simple_child)
    return ERR_PTR(-ENOMEM);

  config_item_init_type_name(&simple_child->item, name, &simple_child_type);    //创建新的item时,使用config_item_init_type_name初始化,simple_child_type是子item使用的config_item_type结构体

  simple_child->storeme = 0;    //将simple_child的storeme设置为0

  return &simple_child->item;
}

simple_child_type定义如下:

struct simple_child {
  struct config_item item;
  int storeme;
};

static const struct config_item_type simple_child_type = {
  .ct_item_ops    = &simple_child_item_ops,
  .ct_attrs    = simple_child_attrs,
  .ct_owner    = THIS_MODULE,
};

同上,定义了 ct_attrs 和 ct_item_ops,没有定义 ct_item_ops,simple_child_attrs变量定义如下:

CONFIGFS_ATTR(simple_child_, storeme);

static struct configfs_attribute *simple_child_attrs[] = {
  &simple_child_attr_storeme,
  NULL,
};

需要定义 show 和 store 函数:

static inline struct simple_child *to_simple_child(struct config_item *item)
{
  return item ? container_of(item, struct simple_child, item) : NULL;
}

/*
 * 子item的show函数,将item转换为simple_child结构体并输出storeme的值
 */
static ssize_t simple_child_storeme_show(struct config_item *item, char *page)
{
  return sprintf(page, "%d\n", to_simple_child(item)->storeme);
}

/*
 * 子item的store函数,将从文件系统输入的值保存在simple_child->storeme中
 */
static ssize_t simple_child_storeme_store(struct config_item *item, const char *page, size_t count)
{
  struct simple_child *simple_child = to_simple_child(item);
  unsigned long tmp;
  char *p = (char *) page;

  tmp = simple_strtoul(p, &p, 10);    //将字符串转换为10进制数字
  if (!p || (*p && (*p != '\n')))
    return -EINVAL;

  if (tmp > INT_MAX)
    return -ERANGE;

  simple_child->storeme = tmp;

  return count;
}

运行效果:

make child
ls
  child  description
cd child
ls -l
  total 0
  -rw-r--r-- 1 root root 4096 Nov  3 21:57 storeme

child 文件夹中的 store 文件是自动创建的,这是因为定义了 make_item 函数,初始化 item 时 simple_child_type 变量的作用。

cat storeme
  0
echo 2222 > storeme
cat storeme
  2222

2.3 示例03-group-children

group_children_subsys变量的内容为:

static struct configfs_subsystem group_children_subsys = {
  .su_group = {
    .cg_item = {
      .ci_namebuf = "03-group-children",
      .ci_type = &group_children_type,
    },
  },
};

group_children_type 的定义如下:

static const struct config_item_type group_children_type = {
  .ct_group_ops    = &group_children_group_ops,
  .ct_attrs    = group_children_attrs,
  .ct_owner    = THIS_MODULE,
};

可以看到,和示例02相比,此处没有定义对 item 的操作,只定义了对 group 的操作。先看 group_children_attrs 的定义:

CONFIGFS_ATTR_RO(group_children_, description);

static struct configfs_attribute *group_children_attrs[] = {
  &group_children_attr_description,
  NULL,
};

同样,使用 CONFIGFS_ATTR_RO 宏定义函数需要先定义好 show 函数:

static ssize_t group_children_description_show(struct config_item *item, char *page)
{
  return sprintf(page,
                 "[03-group-children]\n"
                 "\n"
                 "This subsystem allows the creation of child config_groups.  These\n"
                 "groups are like the subsystem simple-children.\n");
}

此处和示例01和02都一样,下面是示例03的重点:

static struct config_group *group_children_make_group( struct config_group *group, const char *name)
{
  struct simple_children *simple_children;    //此处使用的struct simple_children结构体是示例02中定义的结构体

  simple_children = kzalloc(sizeof(struct simple_children), GFP_KERNEL);    //分配内存
  if (!simple_children)
    return ERR_PTR(-ENOMEM);

  config_group_init_type_name(&simple_children->group, name, &simple_children_type);    //初始化group,simple_children_type也是示例02中定义的

  return &simple_children->group;
}

这段代码负责创建group,初始化group时使用 simple_children_type变量,该变量是 struct config_item_type 类型,其中定义的内容就是实例02的内容。

在configfs中的表现为:在 03-group-children 下创建的每个目录,都相当于加载内核模快时创建的 02-simple-children 目录。

运行效果:

mkdir group
ls
  description  group
cd group
ls -l
  total 0
  -r--r--r-- 1 root root 4096 Nov  3 22:20 description
cat description
  [02-simple-children]

  This subsystem allows the creation of child config_items.  These
  items have only one attribute that is readable and writeable.

到这里可以看到,在示例03创建的目录等同于 02-simple-children 目录,下面的操作的示例02效果一样。

mkdir group_child
ls
  description  group_child
cd group_child
ls
  storeme
cat storeme
  0
echo 3333 > storeme
cat storeme
  3333
查看原文

赞 0 收藏 0 评论 0

戈壁老王 发布了文章 · 2020-12-25

configfs-用户空间控制的内核对象配置

转载:https://www.cnblogs.com/sctb/...
互联网上的好东西越来越少,且看且珍惜,请尊重版权。

image

1. 什么是configfs?

configfs 是一个基于内存的文件系统,它提供了与sysfs相反的功能。sysfs 是一个基于文件系统的内核对象视图,而configfs 是一个基于文件系统的内核对象管理器(或称为config_items)。

在 sysfs 中,一个对象在内核中被创建(例如,当内核发现一个设备时),并在 sysfs 中注册,然后它的属性会出现在 sysfs 中,允许用户空间通过 readdir(3)/read(2) 读取,同时,也允许用户通过 write(2) 修改一些属性。很重要的一点,对象是在内核中被创建和销毁的,内核控制着 sysfs 表示内核对象的生命周期,而 sysfs 不能干预。

configfs 的 config_item 是通过用户空间显式操作 mkdir(2) 创建, rmdir(2) 销毁的。对象的属性在 mkdir(2) 时出现,并且可以通过 read(2) 和 write(2) 进行读取或修改。和 sysfs 一样,readdir(3) 可以查询 items 和属性的列表,symlink(2) 可以用来将 items 分组。与 sysfs 不同的是,configfs 表示的生命周期完全由用户空间控制,支持这些 items 的内核模块必须对用户控制做出响应。

sysfs 和 configfs 应该同时存在于一个系统中,任何一个都不能取代另一个。

2. 使用configfs

configfs 可以编译为一个模块,也可以编译到内核中。你可以通过以下命令访问它:

sudo mount -t configfs none /sys/kernel/config/

除非客户端模块也被加载,否则 configfs 树是空的。这些模块将它们的 item 类型作为子系统注册到 configfs 中,一旦客户端子系统被加载,它将作为一个(或多个)子目录出现在 /sys/kernel/config/ 下。和 sysfs 一样,无论是否挂载在 /sys/kernel/config/ 上,configfs树始终存在。

通过 mkdir(2) 创建一个 item。此时,item 的属性也会出现,readdir(3) 可以查看有哪些属性,read(2) 可以查询它们的默认值,write(2) 可以存储新的值。

注意:不要在一个属性文件中存入多个属性。

configfs 属性的两种类型:

  1. 一般属性,与 sysfs 属性类似,是小型 ASCII 文本文件,最大尺寸为一页( PAGE_SIZE ,在 i386 上为 4096 )。每个文件最好只使用一个值,与 sysfs 的注意事项相同。

    configfs 希望 write(2) 能一次性存储整个缓冲区。在写入一般的 configfs 属性时,用户空间进程应该先读取整个文件,修改想要修改的部分,然后再把整个缓冲区写回去。

  2. 二进制属性,与 sysfs 二进制属性有些类似,但在语义上做了一些轻微的改变。不受 PAGE_SIZE 的限制,但整个二进制 item 必须适合于单个内核 vmalloc 分配的buffer。

    用户空间的 write(2) 调用是有缓冲的,属性的 write_bin_attribute 方法会在其被关闭时调用,因此,用户空间必须检查 close(2) 的返回码,以便验证操作是否成功完成。

    为了避免恶意用户对内核进行OOMing( "out of memory",溢出攻击),每个二进制属性都有一个最大的缓冲区值。

当一个 item 需要被销毁时,用 rmdir(2) 删除它。如果一个 item 与(通过symlink(2))其他 item 有链接,则不能销毁该 item。可以通过 unlink(2) 取消链接。

3. 配置FakeNBD:一个例子

想象一下,有一个网络块设备(NBD)驱动程序,它允许你访问远程块设备,我们称它为FakeNBD。FakeNBD 使用 configfs 进行配置,显然,需要提供一个很好用的用户态程序,让系统管理员能够方便地配置 FakeNBD,为了使对 FakeNBD 的配置起作用,这个程序必须将配置的信息告诉驱动,这就是 configfs 的作用。

当加载 FakeNBD 驱动时,它会在 configfs 中注册自己,用户能使用 readdir(3) 看到它。

ls /sys/kernel/config
  fakenbd

用户也可以使用 mkdir(2) 创建 fakenbd 连接,名字是任意的。不过,示例中的名字可能已经被其他工具(uuid 或者磁盘名)使用了。

mkdir /sys/kernel/config/fakenbd/disk1
ls /sys/kernel/config/fakenbd/disk1
  target device rw

target 属性包含 FakeNBD 要连接的服务器的IP地址,device 属性是服务器上的设备,可想而知,rw属性决定了连接是只读还是读写。

echo 10.0.0.1 > /sys/kernel/config/fakenbd/disk1/target
echo /dev/sda1 > /sys/kernel/config/fakenbd/disk1/device
echo 1 > /sys/kernel/config/fakenbd/disk1/rw

就是这样,通过 shell 就已经把设备配置好了。

4. 用 configfs 编程

configfs 中的每个对象都是一个 config_item,config_item 就是子系统中的一个对象,它的属性与该对象上的值相匹配。configfs 处理该对象及其属性的文件系统表示,允许子系统忽略除基本 show/store 之外的其它所有交互。

items 是在 config_group 里面创建和销毁的。一个 group 是共享相同属性和操作的 items 集合。items 由mkdir(2)创建,rmdir(2)删除,这些 configfs 都会处理, group中 有一组操作来执行这些任务。

子系统是客户端模块的顶层。在初始化过程中,客户端模块向 configfs 注册子系统,子系统作为一个目录出现在 configfs 文件系统的最高层(根)。一个子系统也是一个 config_group,可以做所有 config_group 能做的事情。

4.1 config_item 结构体

struct config_item {
    char                    *ci_name;
    char                    ci_namebuf[UOBJ_NAME_LEN];
    struct kref             ci_kref;
    struct list_head        ci_entry;
    struct config_item      *ci_parent;
    struct config_group     *ci_group;
    struct config_item_type *ci_type;
    struct dentry           *ci_dentry;
};

void config_item_init(struct config_item *);
void config_item_init_type_name(struct config_item *, const char *name, struct config_item_type *type);
struct config_item *config_item_get(struct config_item *);
void config_item_put(struct config_item *);

一般来说,config_item 结构体被嵌入到一个 container 结构体中,这个 container 结构体实际上代表了子系统正在做的事情,其中的 config_item 部分就是对象与 configfs 的交互方式。

无论是在源文件中静态定义还是由父 config_group 创建,创建 config_item 都必须调用一个 _init() 函数,这将初始化引用计数器并设置相应的字段。

config_item 的所有用户都应该通过 config_item_get() 引用它,并在完成后通过 config_item_put() 函数放弃这个引用。

就其本身而言,config_item 只能在 configfs 中出现。通常,一个子系统希望这个 item 能够显示和存储属性,并完成一些其他事情,为此,还需要一个 type 结构体。

换句话说,config_item_type 结构体主要用来完成除了显示和存储属性之外的其他事情。

4.2 config_item_type 结构体

struct configfs_item_operations {
    void (*release)(struct config_item *);
    int (*allow_link)(struct config_item *src, struct config_item *target);
    void (*drop_link)(struct config_item *src, struct config_item *target);
};

struct config_item_type {
    struct module                           *ct_owner;
    struct configfs_item_operations         *ct_item_ops;
    struct configfs_group_operations        *ct_group_ops;
    struct configfs_attribute               **ct_attrs;
    struct configfs_bin_attribute                        **ct_bin_attrs;
};

config_item_type 最基本的功能是定义可以对 config_item 进行哪些操作。所有被动态分配的 item 都需要提供 ct_item_ops->release() 方法。当 config_item 的引用计数为零时,就会调用这个方法释放它。

4.3 configfs_attribute结构体

struct configfs_attribute {
    char                    *ca_name;
    struct module           *ca_owner;
    umode_t                 ca_mode;
    ssize_t (*show)(struct config_item *, char *);
    ssize_t (*store)(struct config_item *, const char *, size_t);
}; 

当一个 config_item 希望一个属性以文件的形式出现在项目的 configfs 目录中时,它必须定义一个 configfs_attribute 来描述它。然后,它将属性添加到以 NULL 结尾的数组 config_item_type->ct_attrs 中。当 item 出现在 configfs 中时,属性文件将以configfs_attribute->ca_name 文件名出现,configfs_attribute->ca_mode 指定文件权限。

如果一个属性是可读的,并且提供了一个 ->show 方法,那么每当用户空间要求对该属性进行 read(2) 时,该方法( ->show )就会被调用。如果一个属性是可写的,并且提供了一个 ->store 方法,那么每当用户空间要求对该属性进行 write(2) 时,该方法( ->store )就会被调用。

4.4 configfs_bin_attribute 结构体

struct configfs_bin_attribute {
   struct configfs_attribute     cb_attr;
   void                                             *cb_private;
   size_t                                         cb_max_size;
};

当需要使用二进制blob来显示 item 对应 configfs 目录中文件的内容时,,会使用二进制属性。

BLOB:binary large object,二进制大对象,是一个可以存储二进制文件的容器。

将二进制属性添加到以 NULL 结尾的数组 config_item_type->ct_bin_attrs 中,item 就会出现在 configfs 中。属性文件会以 configfs_bin_attribute->cb_attr.ca_name 作为文件名, configfs_bin_attribute->cb_attr.ca_mode 指定文件权限。

cb_private 成员是提供给驱动程序使用的,cb_max_size 成员则指定了 vmalloc 缓冲区的最大可用空间。

如果二进制属性是可读的,并且 config_item 提供了 ct_item_ops->read_bin_attribute() 方法,那么每当用户空间要求对属性进行 read(2) 时,该方法就会被调用。同理,用户空间的 write(2) 操作会调用 ct_item_ops->write_bin_attribute() 方法。读/写会被缓冲,所以只会执行读/写的一个,属性本身不需要关心。

4.5 config_group 结构体

config_item 不能凭空产生,唯一的方法是通过 mkdir(2) 在 config_group 上创建一个,该操作将触发子 item 的创建。

struct config_group {
    struct config_item        cg_item;
    struct list_head        cg_children;
    struct configfs_subsystem     *cg_subsys;
    struct list_head        default_groups;
    struct list_head        group_entry;
};

void config_group_init(struct config_group *group);
void config_group_init_type_name(struct config_group *group, const char *name, struct config_item_type *type);

config_group 结构包含一个 config_item,正确地配置该 item 意味着一个 group 可以单独作为一个 item。

此外,group 还可以完成更多工作:创建 item 或 group,这是通过在 group 中 config_item_type 指定的 group 操作来实现的。

struct configfs_group_operations {
    struct config_item *(*make_item)(struct config_group *group, const char *name);
    struct config_group *(*make_group)(struct config_group *group, const char *name);
    int (*commit_item)(struct config_item *item);
    void (*disconnect_notify)(struct config_group *group, struct config_item *item);
    void (*drop_item)(struct config_group *group, struct config_item *item);
};

一个 group 通过提供 ct_group_ops->make_item() 方法来创建子项目。如果提供了这个方法,当在 group 目录中使用 mkdir(2) 时,该方法被调用。当 ct_group_ops->make_item() 方法被调用,子系统将分配一个新的 config_item( or 更可能是它的 container 结构体),初始化并将其返回给 configfs,然后,configfs 将填充文件系统树以反映新的 item。

如果子系统希望子 item 本身是一个 group,子系统提供 ct_group_ops->make_group(),其他的操作都是一样,使用 group 上的 group _init() 函数初始化。

最后,当用户空间对 item 或 group 调用 rmdir(2) 时,会调用 ct_group_ops->drop_item() 方法。由于 config_group 也是一个 config_item,所以不需要单独的 drop_group() 方法。子系统必须调用 config_item_put() 函数释放 item 分配时初始化的引用,如果除了该操作,子系统不需要要做其它操作,可以省略 ct_group_ops->drop_item() 方法,configfs 将代表子系统对 item 调用 config_item_put() 方法。

重要:drop_item() 的返回值为 void,因此不能失败。当 rmdir(2) 被调用时,configfs 将会从文件系统树中删除该 item(假设没有子 item 正在使用它),子系统负责对此操作做出响应。如果子系统在其他线程中有对该 item 的引用,那么内存是安全的,该 item 从子系统中真正消失可能还需要一段时间,但它已经从 configfs 中消失了。

当 drop_item() 被调用时,item 的链接已经被拆掉了,它在父 item 上不再有引用,在 item 的层次结构中也没有位置。如果客户端需要在这个拆分发生之前做一些清理工作,子系统可以实现 ct_group_ops->disconnect_notify() 方法。该方法在 configfs 从文件系统结构中删除 item 后,item 从父 group 中删除前被调用,和drop_item()一样,disconnect_notify() 的返回值也为 void,不能失败。客户端子系统不应该在这里删除任何引用,因为这必须在 drop_item() 中进行。

当一个 config_group 还有子 item 的时候,它是不能被删除的,这在 configfs 的 rmdir(2) 代码中有实现。->drop_item() 不会被调用,因为该 item 没有被删除,rmdir(2) 也将失败,因为目录不是空的。

4.6 configfs_subsystem 结构体

一个子系统必须注册自己,通常是在 module_init 的时候,该操作告诉 configfs 让子系统出现在文件树中。

struct configfs_subsystem {
    struct config_group    su_group;
    struct mutex        su_mutex;
};

int configfs_register_subsystem(struct configfs_subsystem *subsys);
void configfs_unregister_subsystem(struct configfs_subsystem *subsys);

一个子系统由一个顶级 config_group 和一个 mutex 组成,这个 group 是创建子 config_items 的地方。对于一个子系统,这个 group 通常是静态定义的,在调用 configfs_register_subsystem() 之前,子系统必须通过 group _init() 函数来初始化这个 group,并且还必须初始化 mutex。

当调用注册函数返回后,子系统会一直存在,并且可以在 configfs 中看到。这时,用户程序可以调用 mkdir(2),子系统必须为此做好准备。

5. 一个例子

理解这些基本概念的最好例子是 samples/configfs/configfs_sample.c 中的 simple_children subsystem/group 和 simple_child item,它们展示了一个显示和存储属性的简单对象,以及一个创建和销毁这些子 item 的简单 group。

configfs_sample.c : https://github.com/torvalds/l...

6. 层次导航和子系统互斥

configfs 还提供了一些额外的功能。由于 config_groups 和 config_items 出现在文件系统中,所以它们被安排在一个层次结构中。一个子系统是绝对不会接触到文件系统部分的,但是子系统可能会对这个层次结构感兴趣。出于这个原因,层次结构是通过 config_group->cg_children 和 config_item->ci_parent 结构体成员表示的。

子系统可以浏览 cg_children 列表和 ci_parent 指针来查看子系统创建的树。这可能会与 configfs 对层次结构的管理发生冲突,所以 configfs 使用子系统的 mutex 来保护修改。无论何时子系统要浏览层次结构,都必须在子系统 mutex 的保护下进行。

当一个新分配的 item 还没有被链接到这个层次结构中时,子系统将无法获得 mutex, 同样,当一个正在被删除的 item 还没有解除链接时,子系统也无法获取mutex。这意味着,当一个 item 在 configfs 中时,项目的 ci_parent 指针永远不会是 NULL,而且,同一时刻,item 只会存在一个父 item 的 cg_children 列表中,这允许子系统在持有 mutex 时信任 ci_parent 和 cg_children。

7. 通过symlink(2)进行item汇总

configfs 通过 group->item 为父/子关系提供了一个简单的 group,但是,通常情况下,在更大的环境中需要在父/子关系之外进行聚合,这是通过 symlink(2) 实现的。

一个 config_item 可以提供 ct_item_ops->allow_link() 和 ct_item_ops->drop_link() 方法。如果 ->allow_link() 方法存在,就可以调用 symlink(2),将 config_item 作为链接的来源。这些链接只允许在 configfs 的 config_items 之间进行,任何在 configfs 文件系统之外的 symlink(2) 调用都会被拒绝。

当 symlink(2) 被调用时,源 config_item 的 ->allow_link() 方法会被自己和一个目标 item 调用,如果源 item 允许链接到目标 item,则返回0,如果源 item 只想链接到某一类型的对象(例如,在它自己子系统中的对象),它可以拒绝该链接。

当对符号链接调用 unlink(2) 时,通过 ->drop_link() 方法通知源 item,和 ->drop_item() 方法一样,这也是一个返回值为 void 的函数,不能失败,子系统负责响应因该函数执行导致的变化。

当一个 config_item 链接到任何其它 item 时,它不能被删除,当一个 item 链接到它时,也不能被删除。在 configfs 中不允许使用软链接。

8. 自动创建分组

一个新的 config_group 可能希望有两种类型的子 config_items,虽然这可以通过在 ->make_item() 中的 magic names 来编写,但更显式的方法是让用户空间能够看到这种不同。

configfs 提供了一种方法,即在创建父 group 时,在其内部自动创建一个或多个子 group,而不是把行为互不相同的 item 放在同一个 group 中。因此,mkdir("parent") 的结果是 "parent","parent/subgroup1",直到 "parent/subgroupN"。现在,type 1 的 item 可以在目录 "parent/subgroup1" 中创建,type N 的 item 可以在目录 "parent/subgroupN" 中创建。

这些自动创建的子 group,或者说默认 group,并不影响父 group 的其他子 group,如果 ct_group_ops->make_group() 存在,其他子 group 也可以直接在父 group 上创建。

configfs 子系统通过 configfs_add_default_group() 函数将默认 group 添加到父 config_group 结构体中来指定它们,每个添加的 group 与父 group 同时被填充到 configfs 树中。同样地,它们也会与父 group 同时被删除,不会另外通知,当一个 ->drop_item() 方法调用通知子系统其父 group 即将消失时,意味着与该父 group 关联的每个默认子 group 也即将消失。

因此,不能直接通过 rmdir(2) 来删除默认 group,当父 group 的 rmdir(2) 检查子 group 时,也不会考虑它们(默认 group)。

9. 附属子系统

有时,某些驱动程序依赖于特定的 configfs item,例如,挂载 ocfs2 依赖于心跳区域 item,如果使用 rmdir(2) 删除该区域 item,则 ocfs2 挂载会出错 或转为 readonly 模式。

configfs 提供了两个额外的 API 调用:configfs_depend_item() 和 configfs_undepend_item(),客户端驱动程序可以在一个现有的 item 上调用 configfs_depend_item() 来告诉 configfs 它是被依赖的。如果其他程序 rmdir(2) 该 item,configfs 将返回 -EBUSY,当这个 item 不再被依赖时,客户端驱动会调用 configfs_undepend_item() 取消依赖。

这些 API 不能在任何的 configfs 回调下调用,因为它们会冲突,不过,它们可以阻塞和重分配。客户端驱动不能凭自己的直觉调用它们,它应该提供一个外部子系统调用的 API。

这是如何工作的呢?想象一下 ocfs2 的挂载过程。当它挂载时,它会要求一个心跳区域 item,这是通过对心跳代码的调用来完成的。在心跳代码中,区域 item 被查找出来,同时,心跳代码会调用 configfs_depend_item(),如果成功了,那么心跳代码就知道这个区域是安全的,可以交给 ocfs2,如果失败了,ocfs2 将被卸载,心跳代码优雅地传递出一个错误。

10. 可提交 item

注:可提交的 item 目前尚未使用。

有些 config_item 不能有一个有效的初始状态,也就是说,不能为 item 的属性指定默认值(指定了默认值, item 才能起作用),用户空间必须配置一个或多个属性后,子系统才可以启动这个 item 所代表的实体。

考虑一下上面的 FakeNBD 设备,如果没有目标地址和目标设备,子系统就不知道要导入什么块设备。这个例子假设子系统只是简单地等待,直到所有属性都配置好了,再开始连接。每次属性存储操作都检查属性是否被初始化的方法确实可行,但这会导致在满足条件(属性都已经初始化)的情况下,每次属性存储操作必定触发连接。

更好的做法是用一个显式的操作来通知子系统 config_item 已经准备好了。更重要的是,显式操作允许子系统提供反馈,说明属性是否以合理的方式被初始化,configfs 以可提交 item (commitable item)的形式提供了这种反馈。

configfs 仍然只使用正常的文件系统操作,通过 rename(2) 提交的一个item,会从一个可修改的目录移动到一个不能修改的目录。

任何提供 ct_group_ops->commit_item() 方法的 group 都有可提交 item,当这个 group 出现在 configfs 中时,mkdir(2) 将不会直接在该 group 中工作,相反,该 group 将有两个子目录 "live" 和 "pending",live" 目录不支持 mkdir(2) 或 rmdir(2) ,它只允许 rename(2),"pending" 目录允许使用 mkdir(2) 和 rmdir(2)。如果在 "pending" 目录中创建了一个 item,它的属性可以随意修改,用户空间通过将 item 重命名到 "live" 目录中来提交,此时,子系统接收到 ->commit_item() 回调。如果所有所需的属性都被填充,该方法返回0,item 被移到 "live" 目录下。

由于 rmdir(2) 在 "live" 目录中不起作用,所以必须关闭一个 item,或者说使其 "uncommitted",同样,这也是通过 rename(2) 来完成的,这次是从 "live" 目录回到 "uncommitted" 目录,并通过 ct_group_ops->uncommit_object() 方法通知子系统。

参考:https://www.kernel.org/doc/Do...
查看原文

赞 0 收藏 0 评论 0

戈壁老王 发布了文章 · 2020-12-24

数字音频接口

最近在调试音频,整理一下音频相关的接口概念,用于备忘。文章所有内容均来于网络,由于参考过多,就不一一列举,在此感谢。

I2S(Integrated Interchip Sound)

I2S 是飞利浦定义的数字音频传输标准,用于数字音频数据在系统内器件之间传输。使用三根数据线:SD(数据线)WS(帧时钟,也称LRCLK)SCLK(位时钟,也称BCLK)I2S 分为 i2s-standard modei2s-MSB-Left-justified modei2s-MSB-Right-justified mode三种模式。标准的 I2S 时序如下图所示,

image

I2S 的三个信号分别为:

  • 串行时钟 SCLK,也叫位时钟(BCLK),即对应数字音频的每一位数据 SCLK 都有1个脉冲。SCLK 的频率=2×采样频率×采样位数。在数据传输过程中,I2S 总线的发送器和接收器都可以作为系统的主机来提供系统的时钟频率。
  • 帧时钟 WS 也称 LRCK),用于切换左右声道的数据。LRCK 为“1”表示正在传输的是右声道的数据,为“0”则表示正在传输的是左声道的数据。LRCK 的频率等于采样频率。
  • 串行数据 SD,就是用二进制补码表示的音频数据。

有时为了使系统间能够更好地同步,还需要另外传输一个信号 MCLK,称为主时钟,也叫系统时钟(Sys Clock),是采样频率的256倍或384倍。

标准 I2S 模式下,信号无论有多少位有效数据,数据的最高位总是出现在 LRCK 变化(也就是一帧开始)后的第2个 SCLK 脉冲处。这就使得接收端与发送端的有效位数可以不同。如果接收端能处理的有效位数少于发送端,可以放弃数据帧中多余的低位数据;如果接收端能处理的有效位数多于发送端,可以自行补足剩余的位。这种同步机制使得数字音频设备的互连更加方便,而且不会造成数据错位。

PCM(Pulse Code Modulation)

PCM 为脉冲编码调制,也称 DSP 模式。通过等时间隔(即采样率时钟周期)采样将模拟信号数字化的方法。 总线接口与 I2S 类似,同样包含 BCLK(位时钟)LRCK(帧时钟)SD(数据线)PCM 分为 Mode-AMode-B 模式。

  • Mode-AMSB 在帧时钟后 BCLK 的第一个下降沿开始传输。
  • Mode-BMSB 在帧时钟的上升沿开始传输。

PCM Mode-A 模式的时序图如下所示,

image

I2S 相比较,PCM 协议更加灵活。标准 I2S 的帧时钟用于控制左右声道数据,所以它只能支持2声道。PCM 没有这样的严格限制,帧时钟仅表示一帧数据的开始,只要位时钟满足需求,它可以采集任意通道的数据。所以,PCM 可以用于多通道的数据传输,也可以应用于 TDM 传输中。

TDM(Time Division Multiplexing)

TDM 表示时分多路复用技术。由于信道的位传输率超过每一路信号的数据传输率,因此可将信道按时间分成若干片段轮换地给多个信号使用。每一时间片由复用的一个信号单独占用,在规定的时间内,多个数字信号都可按要求传输到达,从而也实现了一条物理信道上传输多个数字信号。假设每个信号输入的数据比特率是 9.6kbit/s ,信道线路的最大比特率为 76.8kbit/s ,则可传输8 路信号。在接收端,复杂的解码器通过接收一些额外的信息来准确地区分出不同的数字信号。

PCMI2S 数据通过时分复用技术机型传输,可以将各个信号的抽样值分布到同一信道的不同时间上,达到传输多路信号的目的。在一个 TDM 系统中,各信号在时域上分开的,而在频域上是混叠在一起的。TDM 在音频上的应用可以分为 TMD-I2S modeTDM-DSP-A mode(short frame)TDM-DSP-B mode(long frame) 三种模式。其时序图如下所示,

image

image

image

PDM(Pulse Density Modulation)

PDM 表示脉冲分时复用,一种用数字信号表示模拟信号的调制方法。在 PDM 信号中,模拟信号的幅值使用输出脉冲对应区域的密度表示。PDM使用远高于 PCM 采样率的时钟采样调制模拟分量,只有1位输出,要么为0,要么为1。因此通过 PDM 方式表示的数字音频也被称为 Oversampled 1-bit Audio

PDM 方式的逻辑相对复杂,但它只需要两根信号线,即时钟和数据。PDM 在诸如手机和平板等对于空间限制严格的场合有着广泛的应用前景。在数字麦克风领域,应用最广的就是 PDM 接口,其次为 I2S 接口。PDM 格式的音频信号可以在比如 LCD 屏这样 Noise 干扰强的电路附近走线。PDM 的时序图如下所示,

image

SPDIF(SONY/PHILIPS Digital Interface)

SPDIF 是一个数字音频信号传输协议,使用 IEC958 标准规范,可以通过光纤或同轴线传输。SPDIF 不同于之前讲述的音频接口,它有完整的数据包概念。一个完整的数据包为一个 Block,一个 Block 包含 192帧,每帧由分为两个子帧,子帧又定义了严格的数据格式。

  • 1 Block(192 * 64 = 12288 bits ) = 192 Frames
  • 1 Frame(32*2 = 64 bits ) = 2 SubFrames
  • 1 SubFrame(32 bits) = 1 Preamble(4 bits) + 1 Channel(28 bits)
  • 1 Channel(28 bits) = 1 AuxData(4 bits) + 1 AudioData(20 bits) + 1 MiscData(4 bits)

SPDIF 的数据格式图下图所示,

image

image

image

一个子帧为 32 bits,内部数据的具体定义如下,

  • 0-3 头码(Preamble) : 用来表示一个Sub Frame的开头,有三种型态,分别表示该Sub Frame为Channel A、Channel B或者是一个Block的起始Sub Frame(为Channel A)。
  • 4-7 辅助数据(Aux. Data) : 原始此区块的设计是用来传递一些使用者自行添加的信息,不过目前比较常见的用途是当音讯数据超过20Bit取样时,这四个Bit用来储存多出的取样Bit,比如说当要传送24Bit取样的数据时,用来存放末四个Bit的音讯数据。
  • 8-27 音频数据(Audio Data) : 存放实际的取样数据,长度为20 Bit,以LSB优先的方式传送,当取样低于20 Bit时,没有用到的LSB Bits要设定为零,举例来说,当我们要传送16 Bit的数据时,只会用到12-27 Bit的位置(LSB在12 Bit),而8-11 Bit为零。
  • 28 有效位(Validity Bit):此位设定了这一个Sub Frame内的数据是不是正确,如果设定为0,代表此Sub Frame内的数据是正确可被接收的,反之如果此Bit为1,则代表接收端应该忽略此组Sub Frame。比如说CD转盘读取CD数据时若是有某一个Sample读不到就会将代表该组Sample的Sub Frame中的有效位设为1。
  • 29 使用者位(User Bit) : 此位为使用者自行定义的位,每组Sample传送一位,直到192组Sample传完后组成192位的信息,两声道各自有一组192位的使用者信息。
  • 30 信道状态位(Channel Status Bit) : 此位与使用者位一样,每组Sample传送一位,最后组成两声道各自一组192位的信道状态信息(Channel Status)。这个192位信道状态信息分为专业(Professional)与一般家用(Consumer)两种不同的结构,以第一个位决定,设为1的时后为Professional模式,设为0的时后为Consumer模式。
  • 31 同位检查位(Parity Bit) : 同位检查是用来判别是否有奇数个位是发生错误,是一种简便错误检查方法,这边是使用偶位同位检查(Even Parity Check)。

USB音频(USB Audio Class)

UAC 是为使用 USB 接口来传输音频数据而定义的协议。USB 是一个通用的数据总线,可以应用于各类数据的传输。USB设备工作组制定了音频数据流规范,并应用于各种具体的音频应用中,如麦克风、扬声器、耳机、电话、乐器等。UAC 中定义了三种不同的音频子类用于规范不同的数据:

  1. AudioControl Interface Subclass 音频控制接口子类
  2. AudioStreaming Interface Subclass 音频流接口子类
  3. MIDIStreaming Interface Subclass MIDI流接口子类

UAC标准又有三个版本1.0、2.0和3.0。

  • UAC 1.0 支持基本的音频数据,典型的是 PCM 编码,最高支持双声道192Khz 16bit的音源。
  • UAC 2.0 引入了对高速 USB 的支持,支持更大的带宽、更高的采样率、更低的延迟。 UAC 2.0 也提高了对音频编码的支持,如DSD、音效、 channel clustering、用户控制和设备描述。最高可以支持15声道 384Khz 32bit 的音源。
  • UAC 3.0 主要对便携式设备进行了改进,如降低设备功耗。

HDMI Audio

HDMI 是高清晰度多媒体接口,它可以支持高带宽的视频信号传输,同时可以传输音频信号。HDMI 的音频信号不占用额外的通道,而是采用和其他辅助信息一起组成数据包,利用3个 TMDS 通道在视频信号传输的消隐期,以岛屿数据的形式传送。随着 HDMI 的版本升级,对音频的支持也越来越丰富。到 HDMI 2.0,音频方面支持最多32个声道,以及最高1536kHz采样率。同时支持:DVD-Audio、DSD、Dolby TrueHD、DTS-HD Master Audio、ARC、Auto Lip-Sync等功能。

Bluetooth Audio

蓝牙协议是蓝牙设备间交换信息所应该遵守的规则。与开放系统互联(OSI)模型一样,蓝牙技术的协议体系也采用了分层结构,从底层到高层形成了蓝牙协议栈,各层协议定义了所完成的功能和使用数据分组格式,以保证蓝牙产品间的互操作性。音频相关的协议包括,

  • HFP(Hands-free Profile):让蓝牙设备可以控制电话,如接听、挂断、拒接、语音拨号等,拒接、语音拨号要视蓝牙耳机及电话是否支持。 
  • HSPHSP 描述了 Bluetooth 耳机如何与计算机或其它 Bluetooth 设备(如手机)通信。连接和配置好后,耳机可以作为远程设备的音频输入和输出接口。
  • A2DP(Advanced Audio Distribution Profile):蓝牙音频传输模型协议。A2DP 能够采用耳机内的芯片来堆栈数据,达到声音的高清晰度。有 A2DP 的耳机就是蓝牙立体声耳机,声音能达到44.1kHz,一般的耳机只能达到8kHz
  • AVRCP(Audio/Video Remote Control Profile):是音频/视频远程控制规范。AVRCP 设计用于提供控制 TVHi-Fi 设备等的标准接口。此配置文件用于许可单个远程控制设备(或其它设备)控制所有用户可以接入的 A/V设备。它可以与 A2DP 或 VDP 配合使用。 

蓝牙协议中常见的编码格式有,

  • SBC(Sub-band coding):子带编码,最早的编码格式。SBCA2DP 协议强制规定的编码格式,所有的蓝牙都会支持这个协议。
  • ACC(Advanced Audio Coding):高级音频编码。ACC 是杜比实验室为音乐社区提供的技术,是一种高压缩比的编码算法。
  • aptXaptXCSR 公司的专利编码算法,在被高通收购后,aptX 在安卓手机里面推广力度很大。aptX 分为三种:aptXaptX HDaptX Low LatencyaptX 传输码率并不高,得益于高效的编码,使得声音保留的细节更多。aptX 的宣传也是称其可以达到 CD 级别的听感。
  • LDACLDACSony 的一项新音频技术,可让您通过蓝牙欣赏高品质的无线音频。LDAC 可传输约三倍于普通 Bluetooth 的数,可为您的所有音乐提供增强的无线聆听体验。LDAC 的码率高,抗干扰能力差,在干扰比较多的情况下需要降低码率来保持连接稳定。
查看原文

赞 0 收藏 0 评论 0

戈壁老王 发布了文章 · 2020-12-24

Android log 输出控制

很简单的话题。当我听到有人在讨论自己实现机制控制 log 输出时,我觉得还是有必要记录一下。最近让我比较困扰的是,很多 Android 基本的技巧都不被知晓。许多人的“锤子”意识很严重,一直使用以往的经验处理一切问题。

影响 Android log 输出的属性

Android 日志存储在由 logd 维护的一组化环形缓冲区,这组缓冲区包括:

  • main:用于存储大多数应用日志。
  • radio:用于存储通信相关的日志。
  • events:用于存储 Event 事件日志。
  • system:用于存储源自 Android 操作系统的消息。
  • crash:用于存储崩溃日志。
  • stats:用于存储系统状态事件日志。
  • security:用于存储安全相关事件日志。
  • kernel:用于存储 Linux 内核日志,其输出受其他几个属性的影响,详见 Logcat 读取 Kernel Log

Android 日志的每个条目都包含一个优先级、一个日志所属模块标记以及实际的日志消息。日志优先级代表日志输出的级别,其优先级为:VERBOSE(V) < DEBUG(D) < INFO(I) < WARNING(W) < ERROR(E) < FATAL(A) < SILENT(S)。日志系统根据日志级别来输出,仅输出当前级别及以下级别的日志。日志级别可以通过以下四个属性来控制,其优先级如下,

  • persist.log.tag.<MODULE_TAG>:模块日志级别,优先级高于系统日志级别,永久存储。
  • log.tag.<MODULE_TAG>:模块日志级别,优先级高于系统日志级别,临时存储,重启后消失。
  • persist.log.tag:系统日志级别,永久存储
  • log.tag:系统日志级别,临时存储,重启后消失。

persist.log.tag/log.tag 控制系统的日志级别,当模块日志级别没有设置时(默认都不会设置),所有日志都根据这个级别输出,其中persist.log.tag 优先级大于 log.tag

persist.log.tag.<MODULE_TAG>/log.tag.<MODULE_TAG> 控制模块的日志级别,但它的优先级大于系统日志级别。当模块日志级别存在时,改模块的日志输出仅由模块日志级别决定,不会参考系统日志级别。其中 persist.log.tag.<MODULE_TAG> 优先级大于 log.tag.<MODULE_TAG>

通过上述的四个属性,可以灵活的控制系统和模块的日志输出。通常,模块日志级别是不被设置的,但它对于开发调试是十分有用的。只要模块中含有足够的日志,就不需要重新编译模块,只要通过属性设置就可以得到需要的调试信息。当上述的四个属性都没有被设置时,系统会使用默认的日志级别,根据系统版本的不同,可能是 V 或 I。

如何使用 isLoggable

isLoggablelandroid.util.Log 提供的一个方法,它可以获取指定模块的日志级别。isLoggablel 提供了另一种控制日志输出的方法,可以通过指定模块的日志级别来控制当前日志输出。isLoggablel 的定义如下,

frameworks/base/core/java/android/util/Log.java
    
    /**
     * Checks to see whether or not a log for the specified tag is loggable at the specified level.
     *
     *  The default level of any tag is set to INFO. This means that any level above and including
     *  INFO will be logged. Before you make any calls to a logging method you should check to see
     *  if your tag should be logged. You can change the default level by setting a system property:
     *      'setprop log.tag.<YOUR_LOG_TAG> <LEVEL>'
     *  Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPPRESS will
     *  turn off all logging for your tag. You can also create a local.prop file that with the
     *  following in it:
     *      'log.tag.<YOUR_LOG_TAG>=<LEVEL>'
     *  and place that in /data/local.prop.
     *
     * @param tag The tag to check.
     * @param level The level to check.
     * @return Whether or not that this is allowed to be logged.
     * @throws IllegalArgumentException is thrown if the tag.length() > 23
     *         for Nougat (7.0) releases (API <= 23) and prior, there is no
     *         tag limit of concern after this API level.
     */
    public static native boolean isLoggable(String tag, int level);

isLoggable 在模块开发过程中十分有用,主要的使用场景为,

  • 根据当前模块的日志级别来控制模块的调试级别,根据调试级别可以控制代码流程和日志输出。
  • 获取相关模块的日志级别来控制当前模块的代码流程和日志输出。

isLoggable 比较定义的日志级别和参数中需要检验的级别。当 level 大于等于当前日志级别时,log 输出可以记录到日志中,函数返回 true。否则返回 false。一个典型的应用如下,

frameworks/base/core/java/android/bluetooth/BluetoothMapClient.java
    
public final class BluetoothMapClient implements BluetoothProfile {

    private static final String TAG = "BluetoothMapClient";
    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
    private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
    ......
    private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
            new IBluetoothStateChangeCallback.Stub() {
                public void onBluetoothStateChange(boolean up) {
                    if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
                    if (!up) {
                        if (VDBG) Log.d(TAG, "Unbinding service...");
    ......

上述代码中,当 log.tag.BluetoothMapClient=D 时,DBG=true; VDBG=false 。这时 DBG 控制的相关 log 可以输出。当 log.tag.BluetoothMapClient=V 时,DBG=true; VDBG=trueDEBVDBG 控制的 log 都可以输出。

Native 日志输出控制

在 Android Native 中,通常使用 ALOGx() 系列函数来输出日志。 ALOGx() 函数与 Log.x() 系列函数一样,也受到日志级别的影响。同样,控制日志级别的四个属性对于 ALOGx() 系列函数也同样有效。这与 Java 层的日志输出并没有区别。ALOGx() 系列函数包括,

  • ALOGV(...):输出 VERBOSE 级别的日志。
  • ALOGD(...):输出 DEBUG 级别的日志。
  • ALOGI(...):输出 INFO 级别的日志。
  • ALOGW(...):输出 WARNING 级别的日志。
  • ALOGE(...):输出 ERROR 级别的日志。

除了使用 ALOGx() 输出日志外,还可以使用 ALOGx_IF() 系列函数来打印日志。ALOGx_IF() 函数根据输入的条件来决定是否打印日志,其函数包括,

  • ALOGV_IF(cond, ...)cond 为 true 时输出 VERBOSE 级别的日志。
  • ALOGD_IF(cond, ...)cond 为 true 时输出 DEBUG 级别的日志。
  • ALOGI_IF(cond, ...)cond 为 true 时输出 INFO 级别的日志。
  • ALOGW_IF(cond, ...)cond 为 true 时输出 WARNING 级别的日志。
  • ALOGE_IF(cond, ...)cond 为 true 时输出 ERROR 级别的日志。

此外,Native 中还有一个 IF_ALOGx() 系列函数,能够起到与 isLoggable 类似的功能,用于判断需求的级别是否大于当前日志级别,从而可以控制日志输出。IF_ALOGx() 系列函数包括,

  • IF_ALOGV():当日志级别小于等于 VERBOSE 时,返回 true。
  • IF_ALOGD():当日志级别小于等于 DEBUG 时,返回 true。
  • IF_ALOGI():当日志级别小于等于 INFO 时,返回 true。
  • IF_ALOGW():当日志级别小于等于 WARNING 时,返回 true。
  • IF_ALOGE():当日志级别小于等于 ERROR 时,返回 true。

最后

具体的代码就不撸了,Android 日志涉及的代码太多了,就不一点点看了,很多我也没看懂。在开发过程中能够做到合理使用日志系统就可以给我们带来很大的帮助。大部分情况,我们并不需要很清楚日志系统的实现,只需要做到充分的利用日志系统。切记摆脱“锤子”思维。

“手中有锤子的人,把世界上的一切都看成是钉子。” —— 查理·芒格

查看原文

赞 0 收藏 0 评论 0

戈壁老王 发布了文章 · 2020-11-25

Logcat 读取 Kernel Log

这个话题真的是非常简单的一个问题,以至于网上很难找到正确的解读。

最近做 Android 系统开发,系统开发通常要建立自己的 Log 系统,抓取 Android Log、Kernel Log 和其他一些特定的 Log。对于 Kernel Log 的需求也很简单,

  • 抓取 Kernel 打印的 Log,存储到文件中。
  • Log 使用墙上时钟(wall clock)显示时间戳,这样方便问题分析。

看似简单的需求却得到五花八门的实现,可见码农的创造力无限。一个方案是抓取 "/proc/kmsg" 信息,但它不是以墙上时钟显示的,又在 Kernel 中定时打印墙上时间用于对照。另一个方案是使用 “dmesg -T -c” 来抓取信息,这样是能得到墙上时间的 Log,但它清空了 Kernel Log buffer,等于是造成了破坏。还有网络上描述的各种解决方案。看到这些方法时,我就迷惑了,一个简单正确的获取 Kernel Log 的方法难道不是通过 Logcat 获取吗?发生了什么导致很多人并不知道这个方法,并且网上也很少提及。

正确的读取 Kernel Log

先说结论,偷懒的就省得看后面了。从 Android 6.0开始,收集 Kernel Log 只需两步,任何需求都满足了。

  1. 设置 ”ro.logd.kernel=true“
  2. 通过 ”logcat -b kernel“ 抓取。

接下来说一下具体的影响因素。本来在 debuggable 版本的系统,可以下面的命令读取 Kernel Log,

# logcat -b kernel

但是~~~,你会发现在许多系统中上面的命令并不能读到信息,因为它还受一些 property 的限制。在源码中的一个文档记录了与 logd 相关的 property,摘取 Kernel Log 相关的描述如下。

file: /system/core/logd/README.property

The properties that logd and friends react to are:

name                       type default  description

ro.logd.kernel             bool+ svelte+ Enable klogd daemon
......
ro.debuggable              number        if not "1", logd.statistics &
                                         ro.logd.kernel default false.
......
ro.config.low_ram          bool   false  if true, logd.statistics,
                                         ro.logd.kernel default false,
                                         logd.size 64K instead of 256K.
......
NB:
- auto - managed by /init
- bool+ - "true", "false" and comma separated list of "eng" (forced false if
  ro.debuggable is not "1") or "svelte" (forced false if ro.config.low_ram is
  true).
- svelte - see ro.config.low_ram for details.
- svelte+ - see ro.config.low_ram and ro.debuggable for details.
......

上面描述表明,ro.logd.kernel 用于控制是否使能 klogd 模块,klogd 就是用来采集 Kernel Log 的进程。ro.logd.kernel 的值为 bool+,可以设置为 "true"、"false" 或者通过逗号分隔符连接 "eng" 或 "svelte"。设置为 "eng" 时,如果 ro.debuggable 不为 "1",则 ro.logd.kernel 为 false。 设置为 "svelte" 时,如果 ro.config.low_ram 为 "true",则 ro.logd.kernel 为 false。

上面这段真的很乱,我看了半天+撸代码总算稍微看懂了,我们用下面的表格来说明一下。

Valuesvelte+Return
ro.logd.kernel=true,xxxN/Atrue
ro.logd.kernel=false,xxxN/Afalse
ro.logd.kernel=xxx,eng (xxx != true or false)ro.debuggable=1true
ro.logd.kernel=xxx,eng (xxx != true or false)ro.debuggable!=1false
ro.logd.kernel=xxx,svelte (xxx != true or false)ro.config.low_ram=truefalse
ro.logd.kernel=xxx,svelte (xxx != true or false)ro.config.low_ram=falsetrue

默认 ro.logd.kernel 是没有被设置的,这时会根据ro.debuggablero.config.low_ram来决定缺省值。

svelte+ro.logd.kernel default value
ro.config.low_ram=truefalse
ro.config.low_ram!=true && ro.debuggable!=1false
otherstrue

所以,当 logcat 无法抓取 Kernel Log 时,需要检查三个 property :ro.logd.kernelro.config.low_ramro.debuggable 的状态。而需要强制打开 Kernel Log 时,只要设置 ”ro.logd.kernel=true“。

为什么无人提及

首先,这个方法实在太简单了,简单的都不值得去认真分析它。但为何还有很多人不知道呢?我遇到的情况多数都是由于历史继承性引起的。在 Android 6.0 之前,抓取 Kernel Log 只能自己想办法,于是码农们实现了各种各样的方式来获取 Log,并加上时间戳。但是当系统版本升级时,这部分抓取 Log 日志的系统做为可贵的自研代码保留了下来,之后一直继承着。部分系统从来没重新审视这部分代码的合理性,因为它一致能用,就接着用。类似的问题在许多代码中都存在,很难有人跳出来重新整理。

代码分析

简单分析一下 Kernel Log 相关的代码,要不这个文档就更没有价值了。Android 的 Log 都由 logd 来管理,Kernel Log 当然也时一样,只是多开辟了一个 kernel buffer。

file: /system/core/logd/main.cpp
    
int main(int argc, char* argv[]) {
    ......
    static const char dev_kmsg[] = "/dev/kmsg"; //使用这个节点来输出logd中的打印
    fdDmesg = android_get_control_file(dev_kmsg);
    ......
    bool klogd = __android_logger_property_get_bool( //获取klogd状态
        "ro.logd.kernel",
        BOOL_DEFAULT_TRUE | BOOL_DEFAULT_FLAG_ENG | BOOL_DEFAULT_FLAG_SVELTE);
    if (klogd) {
        static const char proc_kmsg[] = "/proc/kmsg"; //用来读取Kernel Log的节点
        fdPmesg = android_get_control_file(proc_kmsg);
        ......
    }
    ......
    logBuf = new LogBuffer(times); //创建logBuffer,用于保存日志
    ......
    //LogReader监听/dev/socket/logdr,logcat通过它来获取日志
    LogReader* reader = new LogReader(logBuf); 
    if (reader->startListener()) {
        exit(1);
    }
    ......
    //LogListener监听/dev/socket/logdw,用于写入日志
    LogListener* swl = new LogListener(logBuf, reader);
    if (swl->startListener(600)) {
        exit(1);
    }
    // CommandListener监听/dev/socket/logd,用于命令传输
    CommandListener* cl = new CommandListener(logBuf, reader, swl);
    if (cl->startListener()) {
        exit(1);
    }
    ......
    LogKlog* kl = nullptr; //LogKlog用于存储Kernel Log
    if (klogd) {
        kl = new LogKlog(logBuf, reader, fdDmesg, fdPmesg, al != nullptr);
    }

    readDmesg(al, kl); //启动时读取kernel buffer中缓存的日志

    if (kl && kl->startListener()) { //启动Kernel log监听
        delete kl;
    }
    ......
}

重点的几个地方:

  • 打开 "/dev/kmsg" 用来输出 logd 中的一些日志。这个设计挺巧妙的,在 logd 准备好之前,日志是无法写入 Android Log buffer 中的。这里将日志写入 "/dev/kmsg" 中,等于写入了 Kernel Log buffer 中,通过 dmesg 或 "/proc/kmsg" 就可以获取相关日志。
  • 使用 "/proc/kmsg" 来获取 Kernel Log。"/proc/kmsg" 会记录上次读取的位置,与 dmesg 不同,读取时不会输出整个 buffer 内容。只要监听这个节点就可以实时获取 Kernel Log。
  • LogKlog 是一个 socket listener,用来监听 "/proc/kmsg",然后将 Kernel Log 写入到 Android Log buffer 中。

接下来简单看一下 Kernel Log 真正写入的代码,

file: /system/core/logd/LogKlog.cpp
    
// 当监听到有 Kernel Log 写入时会调用该方法    
bool LogKlog::onDataAvailable(SocketClient* cli) {
    ......
    char buffer[LOGGER_ENTRY_MAX_PAYLOAD]; //每次最多读取4068字节的日志
    ssize_t len = 0;

    for (;;) {
        ssize_t retval = 0;
        if (len < (ssize_t)(sizeof(buffer) - 1)) {
            retval = //读取 "/proc/kmsg" 中的日志
                read(cli->getSocket(), buffer + len, sizeof(buffer) - 1 - len);
        }
        ......
        for (char *ptr = nullptr, *tok = buffer;
             !!(tok = android::log_strntok_r(tok, len, ptr, sublen));
             tok = nullptr) {
            ......
            if ((sublen > 0) && *tok) {
                log(tok, sublen); // 一行行的将日志写入 buffer
            }
        }
    }

    return true;
}
......
// 写入日志,对 Kernel Log 做了大量解析,转换为 Android Log 格式。
// 可能 Kernel Log 太随意了,这段代码看起来都晕,大致理解一下。
int LogKlog::log(const char* buf, ssize_t len) {
    // 如果 auditd 在运行,它会记录相关日志,所以在 LogKlog 不会重复记录该日志。
    if (auditd && android::strnstr(buf, len, auditStr)) {
        return 0;
    }
    
    // 解析 Kernel Log 中的优先级信息
    const char* p = buf;
    int pri = parseKernelPrio(p, len);
    
    // 解析时间信息
    log_time now;
    sniffTime(now, p, len - (p - buf), false);
    ......
    // 解析 pid、tid、uid
    const pid_t pid = sniffPid(p, len - (p - buf));
    const pid_t tid = pid;
    uid_t uid = AID_ROOT;
    if (pid) {
        logbuf->wrlock();
        uid = logbuf->pidToUid(pid);
        logbuf->unlock();
    }
    
    // 解析 Kernel Log 中的 tag 信息
    while ((p < &buf[len]) && (isspace(*p) || !*p)) {
        ++p;
    }
    ......
    // 根据解析的信息创建 Android Log
    char newstr[n];
    char* np = newstr;
    
    // 将优先级转换为 Android Log 的优先级格式
    *np = convertKernelPrioToAndroidPrio(pri);
    ++np;
    
    // 复制解析的 tag 信息
    memcpy(np, tag, taglen);
    np += taglen;
    *np = '\0';
    ++np;

    // 复制主信息
    memcpy(np, p, b);
    np[b] = '\0';
    
    // 时间修正
    if (!isMonotonic()) {
        ......
    }

    // 将日志写入 LOG_ID_KERNEL buffer 中
    int rc = logbuf->log(LOG_ID_KERNEL, now, uid, pid, tid, newstr,
                         (unsigned short)n);

    // 通知 readers
    if (rc > 0) {
        reader->notifyNewLog(static_cast<log_mask_t>(1 << LOG_ID_KERNEL));
    }

    return rc;
}
查看原文

赞 1 收藏 0 评论 1

戈壁老王 发布了文章 · 2020-06-18

Android ANR 原理

ANR 简介

ANR:Application Not Responding,即“应用程序无响应”。Android 运行时,AMS 和 WMS 会监测应用程序的响应时间,如果应用程序主线程(即UI线程)在超时时间内对输入事件没有处理完毕,或者对特定操作没有执行完毕,就会上报 ANR。

ANR 的触发分为以下几类,

  • InputDispatching Timeout:输入事件(包括按键和触屏事件)在5秒内无响应,就会弹出 ANR 提示框,供用户选择继续等待程序响应或者关闭这个应用程序(也就是杀掉这个应用程序的进程)。输入超时类的 ANR 可以细分为以下两类:

    • 处理消息超时:顾名思义这一类是指因为消息处理超时而发生的 ANR,在 log,会看到 “Input dispatching timed out (Waiting because the focused window has not finished processing the input events that were previously delivered to it.)”
    • 无法获取焦点:这一类通常因为新窗口创建慢或旧窗口退出慢而造成窗口无法获得焦点从而发生 ANR,典型 Log “Reason: Waiting because no window has focus but there is a focused application that may eventually add a window when it finishes starting up.”
  • Broadcast Timeout:BroadcastReceiver在规定时间内(前台广播10秒,后台广播60秒)无法处理完成,即报出广播超时的消息。这一类型没有提示框弹出,多发于 statusbar,settings 应用中。
  • Service Timeout:Service在规定时间内(前台服务20秒,后台服务200秒)无法处理完成,即报出服务超时。这一类也不会弹框提示,偶尔会在 Bluetooth 和 wifi 中出现,但是很少碰到。
  • ContentProvider Timeout:ContentProvider 的 publish 在10s内没有完成,会报出此类 ANR。多发于android.process.media中。

产生 ANR 需要满足以下条件,

  1. 只有应用程序进程的主线程(UI 线程)响应超时才会产生 ANR。
  2. 只有达到超时时间才能触发 ANR。产生 ANR 的上下文不同,超时时间也会不同。
  3. 只有输入事件或特定操作才能触发 ANR。输入事件是指按键、触屏等设备输入事件,特定操作是指 BroadcastReceiver 和 Service 的生命周期中的各个函数。产生 ANR 的上下文不同,导致 ANR 的原因也会不同。

防止产生 ANR 的方法主要就是避免在主线程中执行耗时的操作,可以降耗时操作放入子线程中执行。耗时操作包括:

  • 数据库操作。 数据库操作尽量采用异步方法做处理
  • 初始化的数据和控件太多
  • 频繁的创建线程或者其它大对象;
  • 加载过大数据和图片;
  • 对大数据排序和循环操作;
  • 过多的广播和滥用广播;
  • 大对象的传递和共享;
  • 网络操作

InputDispatching Timeout

在 Android Input 系统中,InputDispatcher 负责将输入事件分发给 UI 主线程。UI 主线程接收到输入事件后,使用 InputConsumer 来处理事件。经过一系列的 InputStage 完成事件分发后,执行 finishInputEvent() 方法来告知 InputDispatcher 事件已经处理完成。InputDispatcher 中使用 handleReceiveCallback() 方法来处理 UI 主线程返回的消息,最终将 dispatchEntry 事件从等待队列中移除。

InputDispatching Timeout ANR 就产生于输入事件分发的过程中。InputDispatcher 分发事件过程中会检测上一个输入事件的状态,如果上一个输入事件在限定时间内没有完成分发,就会触发 ANR。InputDispatching Timeout 的默认限定时间的5s,有两处对其进行定义。

frameworks/native/services/inputflinger/InputDispatcher.cpp
constexpr nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL; // 5 sec

frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
static final long DEFAULT_INPUT_DISPATCHING_TIMEOUT_NANOS = 5000 * 1000000L;

Input 分发事件时,通过 InputDispatcherThread 的 threadLoop 来循环读取 Input 事件,然后使用 dispatchOnce() 进行事件分发,实际的实现在 dispatchOnceInnerLocked() 中。

frameworks/native/services/inputflinger/InputDispatcher.cpp

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    nsecs_t currentTime = now(); // 记录事件分发时间
    ......
    // 没有正在分发的事件时,获取一个事件
    if (! mPendingEvent) {
        if (mInboundQueue.isEmpty()) {
            // Inbound Queue 为空时,处理应用 switch key,根据状态生成 repeat key
            ......
        } else {
            // 从 Inbound Queue 中获取一个事件
            mPendingEvent = mInboundQueue.dequeueAtHead();
            traceInboundQueueLengthLocked();
        }
          ......
        // 重置 ANR Timeout 状态
        resetANRTimeoutsLocked();
    }
    ......
    // 根据事件类型进行分发
    switch (mPendingEvent->type) {
    case EventEntry::TYPE_CONFIGURATION_CHANGED: {
        // 配置改变
        ......
    case EventEntry::TYPE_DEVICE_RESET: {
        // 设备重置
        ......
    case EventEntry::TYPE_KEY: {
        // 按键输入
        ......
        done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
        break;
    }

    case EventEntry::TYPE_MOTION: {
        // 触摸屏输入
        ......
        done = dispatchMotionLocked(currentTime, typedEntry,
                &dropReason, nextWakeupTime);
        break;
    }
    ......
    if (done) {
        // 根据 dropReason 来决定是否丢弃事件
        if (dropReason != DROP_REASON_NOT_DROPPED) {
            dropInboundEventLocked(mPendingEvent, dropReason);
        }
        mLastDropReason = dropReason;
        
        releasePendingEventLocked(); // 释放正在处理的事件,也会重置 ANR timeout
        *nextWakeupTime = LONG_LONG_MIN;  // force next poll to wake up immediately
    }
}

接下来看具体输入事件的分发,以按键输入为例,会使用 dispatchKeyLocked() 进行分发。

frameworks/native/services/inputflinger/InputDispatcher.cpp

bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, KeyEntry* entry,
        DropReason* dropReason, nsecs_t* nextWakeupTime) {
    // 新事件需要进行预处理
    if (! entry->dispatchInProgress) {
        ......
    }

    // 处理 try again 事件
    if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) {
        ......
    }
    
    // 如果 flag 为 POLICY_FLAG_PASS_TO_USER,注册事件拦截
    if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) {
        ......
    }
    ......
    // 寻找当前的焦点窗口,这里可能会触发 ANR
    Vector<InputTarget> inputTargets;
    int32_t injectionResult = findFocusedWindowTargetsLocked(currentTime,
            entry, inputTargets, nextWakeupTime);
    if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {
        return false;
    }
    ......
    // 分发按键
    dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}

我们主要关注 ANR 的触发,看一下 findFocusedWindowTargetsLocked() 的实现。

frameworks/native/services/inputflinger/InputDispatcher.cpp

int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,
        const EventEntry* entry, Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime) {
    int32_t injectionResult;
    std::string reason;

    // 丢弃无窗口无应用的事件
    if (mFocusedWindowHandle == NULL) {
        if (mFocusedApplicationHandle != NULL) {
            // 有应用无窗口的状态
            injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
                    mFocusedApplicationHandle, NULL, nextWakeupTime,
                    "Waiting because no window has focus but there is a "
                    "focused application that may eventually add a window "
                    "when it finishes starting up.");
            goto Unresponsive;
        }

        ALOGI("Dropping event because there is no focused window or focused application.");
        injectionResult = INPUT_EVENT_INJECTION_FAILED;
        goto Failed;
    }

    // 权限检查
    if (! checkInjectionPermission(mFocusedWindowHandle, entry->injectionState)) {
        injectionResult = INPUT_EVENT_INJECTION_PERMISSION_DENIED;
        goto Failed;
    }

    // 检查窗口是否 ready
    reason = checkWindowReadyForMoreInputLocked(currentTime,
            mFocusedWindowHandle, entry, "focused");
    if (!reason.empty()) {
        injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
                mFocusedApplicationHandle, mFocusedWindowHandle, nextWakeupTime, reason.c_str());
        goto Unresponsive;
    }

    // 窗口已经准备好
    injectionResult = INPUT_EVENT_INJECTION_SUCCEEDED;
    addWindowTargetLocked(mFocusedWindowHandle,
            InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS, BitSet32(0),
            inputTargets);
    ......
    return injectionResult;
}

上述代码使用 checkWindowReadyForMoreInputLocked() 来检查窗口是否准备就绪,它使用字符串返回 window connection 的状态,当窗口正常时返回空。接下来会使用 handleTargetsNotReadyLocked() 来处理窗口未就绪的情形。

frameworks/native/services/inputflinger/InputDispatcher.cpp

int32_t InputDispatcher::handleTargetsNotReadyLocked(nsecs_t currentTime,
        const EventEntry* entry,
        const sp<InputApplicationHandle>& applicationHandle,
        const sp<InputWindowHandle>& windowHandle,
        nsecs_t* nextWakeupTime, const char* reason) {
    if (applicationHandle == NULL && windowHandle == NULL) {
        if (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_SYSTEM_NOT_READY) {
            // 等待系统就绪,无窗口无应用的情形进入一次
            ......
        }
    } else {
        if (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY) {
            // 等待应用就绪,一个窗口进入一次
            ......
            // 设置超时,默认值都是5s
            nsecs_t timeout; 
            ......
            mInputTargetWaitCause = INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY;
            mInputTargetWaitStartTime = currentTime; // 当前事件第一次分发时间
            mInputTargetWaitTimeoutTime = currentTime + timeout; // 超时时间
            mInputTargetWaitTimeoutExpired = false;
            mInputTargetWaitApplicationHandle.clear(); // 清除当前等待的应用

            // 设置当前等待的应用
            if (windowHandle != NULL) {
                mInputTargetWaitApplicationHandle = windowHandle->inputApplicationHandle;
            }
            if (mInputTargetWaitApplicationHandle == NULL && applicationHandle != NULL) {
                mInputTargetWaitApplicationHandle = applicationHandle;
            }
        }
    }

    if (mInputTargetWaitTimeoutExpired) {
        return INPUT_EVENT_INJECTION_TIMED_OUT;
    }

    if (currentTime >= mInputTargetWaitTimeoutTime) {
        // 事件分发超时,触发 ANR
        onANRLocked(currentTime, applicationHandle, windowHandle,
                entry->eventTime, mInputTargetWaitStartTime, reason);

        // ANR 触发时,立即唤醒下一个轮询
        *nextWakeupTime = LONG_LONG_MIN;
        return INPUT_EVENT_INJECTION_PENDING;
    } else {
        // 超时时强制唤醒轮询
        if (mInputTargetWaitTimeoutTime < *nextWakeupTime) {
            *nextWakeupTime = mInputTargetWaitTimeoutTime;
        }
        return INPUT_EVENT_INJECTION_PENDING;
    }
}

上述代码显示了一个正常输入事件分发过程中触发 ANR 的过程,触发 ANR 的判定是在一个限定的时间内是否可以完成事件分发。正常的分发过程中会有两处位置重置 ANR timeout,

  • 获取一个新的分发事件时。
  • 事件完成分发时。
void InputDispatcher::resetANRTimeoutsLocked() {
    // ANR timeout 重置就是重置了等待状态,清除了等待应用
    mInputTargetWaitCause = INPUT_TARGET_WAIT_CAUSE_NONE;
    mInputTargetWaitApplicationHandle.clear();
}

系统运行时,主要是以下4个场景,会有机会执行resetANRTimeoutsLocked:

  • 解冻屏幕, 系统开/关机的时刻点 (thawInputDispatchingLw, setEventDispatchingLw)
  • wms聚焦app的改变 (WMS.setFocusedApp, WMS.removeAppToken)
  • 设置input filter的过程 (IMS.setInputFilter)
  • 再次分发事件的过程(dispatchOnceInnerLocked)

触发 ANR 后,会调用 onANRLocked() 来捕获 ANR 的相关信息。大致的调用流程为,

InputDispatcher::onANRLocked --> InputDispatcher::doNotifyANRLockedInterruptible
    --> InputManagerService.notifyANR --> InputMonitor.notifyANR
    --> ActivityManagerService.inputDispatchingTimedOut --> AppErrors.appNotResponding

最终在 AppErrors.appNotResponding() 中打印 log 信息、dump 栈信息、打印 CPU 信息。

Broadcast Timeout

Android 的广播机制在接收到广播进行处理时,可能会出现 receiver 处理很慢从而影响后续 receiver 接收的情形。因此,Android 对广播的接收处理增加了一个限定时间,超出限定时间将触发 ANR。需要说明,广播超时值会出现在串行广播中。并行广播因为并不存在传输的依赖关系,所以不会发生广播超时。对于不同的广播存在两个限定时间:前台广播10s和后台广播60s。

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
    
    // How long we allow a receiver to run before giving up on it.
    static final int BROADCAST_FG_TIMEOUT = 10*1000;
    static final int BROADCAST_BG_TIMEOUT = 60*1000;
    ......
    public ActivityManagerService(Context systemContext) {
        ......
        mHandlerThread = new ServiceThread(TAG,
                THREAD_PRIORITY_FOREGROUND, false /*allowIo*/);
        mHandlerThread.start();
        mHandler = new MainHandler(mHandlerThread.getLooper());
        ......
        // 创建广播队列,前台和后台
        mFgBroadcastQueue = new BroadcastQueue(this, mHandler,
                "foreground", BROADCAST_FG_TIMEOUT, false);
        mBgBroadcastQueue = new BroadcastQueue(this, mHandler,
                "background", BROADCAST_BG_TIMEOUT, true);
        mBroadcastQueues[0] = mFgBroadcastQueue;
        mBroadcastQueues[1] = mBgBroadcastQueue;
        ......
    }

广播队列创建时会设置超时时间,接下来直接看下广播的处理过程。

frameworks/base/services/core/java/com/android/server/am/BroadcastQueue.java 

    final void processNextBroadcastLocked(boolean fromMsg, boolean skipOomAdj) {
        BroadcastRecord r;
        ......
        // 处理并行广播
        while (mParallelBroadcasts.size() > 0) {
            ......
        }
        
        // 广播正在处理时,检查进程是否存活
        if (mPendingBroadcast != null) {
            ......
        }
        
        boolean looped = false;
        // 处理串行广播
        do {
            ......
            r = mOrderedBroadcasts.get(0);
            boolean forceReceive = false;
            
            // 如果广播处理超时,强行结束它
            int numReceivers = (r.receivers != null) ? r.receivers.size() : 0;
            if (mService.mProcessesReady && r.dispatchTime > 0) {
                long now = SystemClock.uptimeMillis();
                if ((numReceivers > 0) &&
                        (now > r.dispatchTime + (2*mTimeoutPeriod*numReceivers))) {
                    broadcastTimeoutLocked(false); // forcibly finish this broadcast
                    forceReceive = true;
                    r.state = BroadcastRecord.IDLE;
                }
            }
            ......
            if (r.receivers == null || r.nextReceiver >= numReceivers
                    || r.resultAbort || forceReceive) {
                if (r.resultTo != null) {
                    try {
                        // 处理完广播,发送最终结果
                        performReceiveLocked(r.callerApp, r.resultTo,
                            new Intent(r.intent), r.resultCode,
                            r.resultData, r.resultExtras, false, false, r.userId);
                        r.resultTo = null;
                    ......
                }
                    
                // 撤销 BROADCAST_TIMEOUT_MSG 消息
                cancelBroadcastTimeoutLocked();
                ....
            }
        } while (r == null);
            
        // 获取下一个广播
        int recIdx = r.nextReceiver++;
        
        r.receiverTime = SystemClock.uptimeMillis();
        if (recIdx == 0) {
            // 在 receiver 启动时开启跟踪
            r.dispatchTime = r.receiverTime;
            r.dispatchClockTime = System.currentTimeMillis();
            ......
        }
        if (! mPendingBroadcastTimeoutMessage) {
            // 设置广播超时时间,发送 BROADCAST_TIMEOUT_MSG
            long timeoutTime = r.receiverTime + mTimeoutPeriod;
            setBroadcastTimeoutLocked(timeoutTime);
        }
            
        final BroadcastOptions brOptions = r.options;
        final Object nextReceiver = r.receivers.get(recIdx);

        // 处理动态注册的广播
        if (nextReceiver instanceof BroadcastFilter) {
            ......
        }
            
        // 处理静态注册的广播
        ResolveInfo info =
            (ResolveInfo)nextReceiver;
        ComponentName component = new ComponentName(
                info.activityInfo.applicationInfo.packageName,
                info.activityInfo.name);
        ....
        // 获取 receiver 对应的进程
        String targetProcess = info.activityInfo.processName;
        ProcessRecord app = mService.getProcessRecordLocked(targetProcess,
                info.activityInfo.applicationInfo.uid, false);
        ......
        // 如果相应进程存在,直接进行处理
        if (app != null && app.thread != null && !app.killed) {
            try {
                app.addPackage(info.activityInfo.packageName,
                        info.activityInfo.applicationInfo.versionCode, mService.mProcessStats);
                processCurBroadcastLocked(r, app, skipOomAdj);
                return;
            } catch (RemoteException e) {
            } catch (RuntimeException e) {.
                logBroadcastReceiverDiscardLocked(r);
                finishReceiverLocked(r, r.resultCode, r.resultData,
                        r.resultExtras, r.resultAbort, false);
                scheduleBroadcastsLocked();
                r.state = BroadcastRecord.IDLE;
                return;
            }
        }
            
        // 如果相应进程不存在,则创建进程
        if ((r.curApp=mService.startProcessLocked(targetProcess,
                info.activityInfo.applicationInfo, true,
                r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND,
                "broadcast", r.curComponent,
                (r.intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false, false))
                        == null) {
            logBroadcastReceiverDiscardLocked(r);
            finishReceiverLocked(r, r.resultCode, r.resultData,
                    r.resultExtras, r.resultAbort, false);
            scheduleBroadcastsLocked();
            r.state = BroadcastRecord.IDLE;
            return;
        }

        mPendingBroadcast = r;
        mPendingBroadcastRecvIndex = recIdx;
    }

上述代码是广播处理的简单流程,与 ANR 触发相关的主要是两个地方,

  • 设置广播超时的消息,setBroadcastTimeoutLocked()
  • 撤销广播超时消息,cancelBroadcastTimeoutLocked()

在开始处理一个广播时,会根据超时时间来设置一个延迟发送的消息。在限定时间内,如果该消息没有被撤销就会触发 ANR。

frameworks/base/services/core/java/com/android/server/am/BroadcastQueue.java

    final void setBroadcastTimeoutLocked(long timeoutTime) {
        if (! mPendingBroadcastTimeoutMessage) {
            // 发送延迟消息 BROADCAST_TIMEOUT_MSG
            Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
            mHandler.sendMessageAtTime(msg, timeoutTime);
            mPendingBroadcastTimeoutMessage = true;
        }
    }
frameworks/base/services/core/java/com/android/server/am/BroadcastQueue.java

    private final class BroadcastHandler extends Handler {
        public BroadcastHandler(Looper looper) {
            super(looper, null, true);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                ....
                case BROADCAST_TIMEOUT_MSG: {
                    synchronized (mService) {
                        broadcastTimeoutLocked(true);
                    }
                } break;
            }
        }
    }
    ......
    final void broadcastTimeoutLocked(boolean fromMsg) {
        ......
        long now = SystemClock.uptimeMillis();
        BroadcastRecord r = mOrderedBroadcasts.get(0);
        if (fromMsg) {
            ......
            long timeoutTime = r.receiverTime + mTimeoutPeriod;
            if (timeoutTime > now) {
                // 如果当前时间没有达到限定时间,重新设置超时消息
                setBroadcastTimeoutLocked(timeoutTime);
                return;
            }
        }

        BroadcastRecord br = mOrderedBroadcasts.get(0);
        if (br.state == BroadcastRecord.WAITING_SERVICES) {
            // 广播已经处理完,正在等待 service 执行。继续处理下一条广播
            br.curComponent = null;
            br.state = BroadcastRecord.IDLE;
            processNextBroadcast(false);
            return;
        }
        ......
        // 获取 APP 进程
        if (curReceiver != null && curReceiver instanceof BroadcastFilter) {
            BroadcastFilter bf = (BroadcastFilter)curReceiver;
            if (bf.receiverList.pid != 0
                    && bf.receiverList.pid != ActivityManagerService.MY_PID) {
                synchronized (mService.mPidsSelfLocked) {
                    app = mService.mPidsSelfLocked.get(
                            bf.receiverList.pid);
                }
            }
        } else {
            app = r.curApp;
        }
        ......
        // 继续处理下一条广播
        finishReceiverLocked(r, r.resultCode, r.resultData,
                r.resultExtras, r.resultAbort, false);
        scheduleBroadcastsLocked();

        if (!debugging && anrMessage != null) {
            // 触发 ANR
            mHandler.post(new AppNotResponding(app, anrMessage));
        }
    }

在广播处理完成时会撤销超时消息。

    final void cancelBroadcastTimeoutLocked() {
        if (mPendingBroadcastTimeoutMessage) {
            mHandler.removeMessages(BROADCAST_TIMEOUT_MSG, this);
            mPendingBroadcastTimeoutMessage = false;
        }
    }

Service Timeout

Service Timeout 发生在 Service 的启动过程中,如果在限定时间内无法完成启动就会触发 ANR。根据 Service 类型的不同,赋予前台服务和后台服务不同的超时时间。

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

    // How long we wait for a service to finish executing.
    static final int SERVICE_TIMEOUT = 20*1000;

    // How long we wait for a service to finish executing.
    static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;    

在 Service 的启动过程中,会根据限定时间来设置一个延迟消息,用来触发启动超时。

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

    private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
        ......
                scheduleServiceTimeoutLocked(r.app);
        ......
    }
    ......
    private final void realStartServiceLocked(ServiceRecord r,
            ProcessRecord app, boolean execInFg) throws RemoteException {
        ......
        r.app = app;
        r.restartTime = r.lastActivity = SystemClock.uptimeMillis();

        final boolean newService = app.services.add(r);
        bumpServiceExecutingLocked(r, execInFg, "create"); // 发送超时消息
        mAm.updateLruProcessLocked(app, false, null); // 更新 LRU
        updateServiceForegroundLocked(r.app, /* oomAdj= */ false);
        mAm.updateOomAdjLocked(); // 更新 OOM ADJ

        boolean created = false;
        try {
            ......
            mAm.notifyPackageUse(r.serviceInfo.packageName,
                                 PackageManager.NOTIFY_PACKAGE_USE_SERVICE);
            app.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_SERVICE);
            // 最终执行服务的 onCreate() 方法
            app.thread.scheduleCreateService(r, r.serviceInfo,
                    mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
                    app.repProcState);
            r.postNotification();
            created = true;
        ......
    }
    ......
    void scheduleServiceTimeoutLocked(ProcessRecord proc) {
        if (proc.executingServices.size() == 0 || proc.thread == null) {
            return;
        }
        // 设置延迟消息,用于触发服务启动超时
        Message msg = mAm.mHandler.obtainMessage(
                ActivityManagerService.SERVICE_TIMEOUT_MSG);
        msg.obj = proc;
        mAm.mHandler.sendMessageDelayed(msg,
                proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
    }

当服务启动超时,会向 AMS 发送一个 SERVICE_TIMEOUT_MSG 消息。

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

    final class MainHandler extends Handler {
        public MainHandler(Looper looper) {
            super(looper, null, true);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            ......
            case SERVICE_TIMEOUT_MSG: {
                mServices.serviceTimeout((ProcessRecord)msg.obj);
            } break;
            ......
            }
        }
    };
frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

    void serviceTimeout(ProcessRecord proc) {
        String anrMessage = null;

        synchronized(mAm) {
            if (proc.executingServices.size() == 0 || proc.thread == null) {
                return;
            }
            final long now = SystemClock.uptimeMillis();
            final long maxTime =  now -
                    (proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
            ServiceRecord timeout = null;
            long nextTime = 0;
            for (int i=proc.executingServices.size()-1; i>=0; i--) {
                // 寻找超时的服务
                ......
            }
            if (timeout != null && mAm.mLruProcesses.contains(proc)) {
                // 服务超时,生成超时信息
                ......
                mAm.mHandler.removeCallbacks(mLastAnrDumpClearer);
                mAm.mHandler.postDelayed(mLastAnrDumpClearer, LAST_ANR_LIFETIME_DURATION_MSECS);
                anrMessage = "executing service " + timeout.shortName;
            } else {
                // 服务未超时,重置超时信息
                Message msg = mAm.mHandler.obtainMessage(
                        ActivityManagerService.SERVICE_TIMEOUT_MSG);
                msg.obj = proc;
                mAm.mHandler.sendMessageAtTime(msg, proc.execServicesFg
                        ? (nextTime+SERVICE_TIMEOUT) : (nextTime + SERVICE_BACKGROUND_TIMEOUT));
            }
        }

        if (anrMessage != null) {
            // 触发 ANR
            mAm.mAppErrors.appNotResponding(proc, null, null, false, anrMessage);
        }
    }

上面描述了 Service Timeout 的产生过程,如果想避免超时消息的产生,就需要在限定时间内将消息移除。移除操作在服务启动完成后进行,下面看一下真正进行服务启动的代码。

frameworks/base/core/java/android/app/ActivityThread.java
    
    private void handleCreateService(CreateServiceData data) {
        unscheduleGcIdler();

        LoadedApk packageInfo = getPackageInfoNoCheck(
                data.info.applicationInfo, data.compatInfo);
        Service service = null;
        try {
            // 创建 service
            java.lang.ClassLoader cl = packageInfo.getClassLoader();
            service = packageInfo.getAppFactory()
                    .instantiateService(cl, data.info.name, data.intent);
        ......
        try {
            // 创建ContextImpl对象
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            context.setOuterContext(service);

            // 创建Application对象
            Application app = packageInfo.makeApplication(false, mInstrumentation);
            service.attach(context, this, data.info.name, data.token, app,
                    ActivityManager.getService());
            service.onCreate(); //     调用服务onCreate()方法
            mServices.put(data.token, service);
            try {
                // 服务启动完成
                ActivityManager.getService().serviceDoneExecuting(
                        data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
            ......
    }
frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

    private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,
            boolean finishing) {
        r.executeNesting--;
        if (r.executeNesting <= 0) {
            if (r.app != null) {
                r.app.execServicesFg = false;
                r.app.executingServices.remove(r);
                if (r.app.executingServices.size() == 0) {
                    // 当前进程中没有正在执行的 service 时,移除服务超时消息
                    mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
                ......

ContentProvider Timeout

ContentProvider Timeout 发生在应用启动过程中。如果应用启动时,Provider 发布超过限定时间就会触发 ANR。应用进程创建后,会调用 attachApplicationLocked() 进行初始化。

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

    // How long we wait for an attached process to publish its content providers
    // before we decide it must be hung.
    static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10*1000;
    ......
    private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid, int callingUid, long startSeq) {
        ......
        // 如果应用存在 Provider,设置延迟消息处理 Provider 超时
        if (providers != null && checkAppInLaunchingProvidersLocked(app)) {
            Message msg = mHandler.obtainMessage(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG);
            msg.obj = app;
            mHandler.sendMessageDelayed(msg, CONTENT_PROVIDER_PUBLISH_TIMEOUT);
        }
        ......
    }

当在限定时间内没有完成 Provider 发布时,会发送消息 CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG,Handler 会进行相应处理。

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
    
    final class MainHandler extends Handler {
        public MainHandler(Looper looper) {
            super(looper, null, true);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            ......
            case CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG: {
                // 处理 Provider 超时消息
                ProcessRecord app = (ProcessRecord)msg.obj;
                synchronized (ActivityManagerService.this) {
                    processContentProviderPublishTimedOutLocked(app);
                }
            } break
            ......
        }
    }
    ......
    boolean removeProcessLocked(ProcessRecord app,
            boolean callerWillRestart, boolean allowRestart, String reason) {
        final String name = app.processName;
        final int uid = app.uid;
        ......
        // 移除mProcessNames中的相应对象
        removeProcessNameLocked(name, uid);
        ......
        boolean needRestart = false;
        if ((app.pid > 0 && app.pid != MY_PID) || (app.pid == 0 && app.pendingStart)) {
            int pid = app.pid;
            if (pid > 0) {
                // 杀进程前处理一些相关状态
                ......
            }
            // 判断是否需要重启进程
            boolean willRestart = false;
            if (app.persistent && !app.isolated) {
                if (!callerWillRestart) {
                    willRestart = true;
                } else {
                    needRestart = true;
                }
            }
            app.kill(reason, true); // 杀死进程
            handleAppDiedLocked(app, willRestart, allowRestart); // 回收资源
            if (willRestart) {
                removeLruProcessLocked(app);
                addAppLocked(app.info, null, false, null /* ABI override */);
            }
        } else {
            mRemovedProcesses.add(app);
        }

        return needRestart;
    }
    ......
    private final void processContentProviderPublishTimedOutLocked(ProcessRecord app) {
        // 清理 Provider
        cleanupAppInLaunchingProvidersLocked(app, true);
        // 清理应用进程
        removeProcessLocked(app, false, true, "timeout publishing content providers");
    }
    ......
    boolean cleanupAppInLaunchingProvidersLocked(ProcessRecord app, boolean alwaysBad) {
        boolean restart = false;
        for (int i = mLaunchingProviders.size() - 1; i >= 0; i--) {
            ContentProviderRecord cpr = mLaunchingProviders.get(i);
            if (cpr.launchingApp == app) {
                if (!alwaysBad && !app.bad && cpr.hasConnectionOrHandle()) {
                    restart = true;
                } else {
                    // 移除死亡的 Provider
                    removeDyingProviderLocked(app, cpr, true);
                }
            }
        }
        return restart;
    }

可以看到 ContentProvider Timeout 发生时并没有调用 AMS.appNotResponding() 方法,仅仅杀死问题进程及清理相关信息。Provider 的超时消息会在发布成功时被清除,相关代码如下。

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

    public final void publishContentProviders(IApplicationThread caller,
            List<ContentProviderHolder> providers) {
        ......
        synchronized (this) {
            final ProcessRecord r = getRecordForAppLocked(caller);
            ......
            final int N = providers.size();
            for (int i = 0; i < N; i++) {
                ContentProviderHolder src = providers.get(i);
                ......
                ContentProviderRecord dst = r.pubProviders.get(src.info.name);
                if (dst != null) {
                    ComponentName comp = new ComponentName(dst.info.packageName, dst.info.name);
                    mProviderMap.putProviderByClass(comp, dst);
                    ......
                    if (wasInLaunchingProviders) {
                        // 移除超时消息
                        mHandler.removeMessages(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG, r);
                    }
                    ......
                }
            }

            Binder.restoreCallingIdentity(origId);
        }
    }
参考文档:

理解Android ANR的触发原理
Android ANR:原理分析及解决办法
Input系统—ANR原理分析

查看原文

赞 1 收藏 1 评论 0

戈壁老王 发布了文章 · 2020-04-23

IoT OS 初步设想

昨天在群里聊到 IoT OS 的趋势,在脑子大致想了想,有了一个粗略的框架。细节还没有考虑,先记录下来等以后再丰富起来。

iot-os.png

整体 IoT OS 的构想结构如上图所示,关键点的解释如下。

  • IoT OS 主要是运行在嵌入式设备上。因为嵌入式设备的硬件千差万别,性能也差距很大。为兼容所有嵌入式设备,IoT OS 必须保持轻小的特点和良好的兼容性。同时保证其灵活性和易扩展,从而可以适应不同的外围设备。
  • 系统的 Core 趋向于轻薄发展,只保留支撑核心硬件和系统运行的相关软件,类似目前的微内核概念。但我感觉当前的微内核在性能和资源占用上还无法满足需求。
  • Device SW 是支撑设备运行的软件,Core + Device SW 才能构成一个完整的 OS。系统是面向设备设计的,一个设备构建一个系统。包含复杂设备的平台可能会同时运行多个系统,各系统间是独立的。
  • 每个 IoT OS 对外的接口统一为 IoT Int/erface。这个接口必然是灵活的,易扩展的。
  • IoT 不同的通信协议和软件框架在 IoT Framework 中完成。IoT Framework 应运行在担任服务器或路由的设备上,用于发现和绑定不同的设备。
  • 每个设备都视作一个 Resource,也就意味着每个 OS 最终对外呈现为一个 Resource。应用可以使用 IoT 网络上的任何一个或多个 Resource。
  • 从应用的角度,将看到 Platform 和 内部的 Resource。为实现边缘计算和分布式计算,在性能足够的 Platform 上也会运行一部分的 Framework。
  • 这个构想很粗略,细节还没仔细考虑。因作者水平有限,难免有很多不合理的地方。
查看原文

赞 0 收藏 0 评论 0

戈壁老王 发布了文章 · 2020-04-17

Android 中看不见的外部存储路径

这个问题起源一个bug的分析过程,APP的cache路径无法通过adb进行访问。

基于Android 5.1代码进行分析

在 Android 应用中,获取存储路径的方法通常使用以下几个,

  • Environment.getDownloadCacheDirectory():/cache ,cache目录路径。
  • Environment.getRootDirectory():/system,system目录路径。
  • Environment.getDataDirectory():/data,内部存储的根路径。
  • Context.getFilesDir():/data/user/0/<packname>/files,内部存储中的files路径。
  • Context.getCacheDir():/data/user/0/<packname>/cache ,内部存储中的cache路径。
  • Context.getPackageCodePath():程序的安装包路径。
  • Context.getPackageResourcePath():当前应用程序对应的 apk 文件的路径。
  • Context.getDatabasePath(“xxx”):通过Context.openOrCreateDatabase 创建的数据库文件。
  • Context.getDir(“xxx”, Context.MODE_PRIVATE):获取某个应用在内部存储中的自定义路径。
  • Context.Environment.getExternalStorageDirectory():/storage/emulated/0,获得外部存储路径。
  • Context.Environment.getExternalStoragePublicDirectory(“xxx”):获得外部存储上的公共路径。
  • Context.getExternalFilesDir():/storage/emulated/0/Android/data/<packname>/files,外部存储中的files路径。
  • Context.getExternalCacheDir():/storage/emulated/0/Android/data/<packname>/cache,外部存储中的cache路径。

上述的接口多数与我们这次要讨论的无关,我们主要关注的是外部存储路径。外部存储路径在不同的 Android 版本上可能不同,上述是 Android 5.1 上显示的结果。外部存储代表的意义在不同版本上也不同。在 Android 4.4 之前的版本,外部存储指的就是扩展的SD卡,也就是通过插槽连接的SD卡。从 Android 4.4 开始,系统将机身存储划分为内部存储和外部存储,这样在没有扩展SD卡时,外部存储就是机身存储的一部分,指向/storage/emulated/0。当有扩展SD卡插入时,系统将获得两个外部存储路径。

getExternalStorageDirectory() 在不同 Android 版本下获取到的路径如下:

系统版本外部存储路径扩展SD卡路径
4.0/mnt/sdcard/mnt/sdcard
4.1/storage/sdcard0/storage/sdcard0
4.2/storage/sdcard0/storage/sdcard1
4.4/storage/emulated/0/storage/sdcard1
6.0/storage/emulated/0/storage/<product-id>

上表的路径我并没有真实验证过,并且设备厂商会修改自己的SD卡路径,可能与实际的路径不同。Android 4 上存储路径本身就是一个混乱状态,这也不是我们关心的重点。我们关心的是 /storage/emulated/0,这个路径是使用机身内部存储模拟的外部存储。在应用中,我们通过 getExternalStorageDirectory() 返回的就是这个路径,所以应用本身的数据会写到这个路径下。但是当我们使用 adb 连接记性调试时,会发现这个路径并不存在,只能看到下面的结果:

# ls -l /storage/emulated/                                   
lrwxrwxrwx root     root              1970-01-01 08:00 legacy -> /mnt/shell/emulated/0
# ls -l /mnt/shell/emulated
drwxrwx--x root     sdcard_r          2020-04-15 11:07 0
drwxrwx--x root     sdcard_r          1970-01-01 08:03 legacy
drwxrwx--x root     sdcard_r          1970-01-01 08:00 obb

我们都知道应用的数据会写到 /mnt/shell/emulated/0 中,但这个路径是如何与 /storage/emulated/0 关联的呢?这就要提到 Linux 的 Mount 命名空间。

Mount 命名空间提供了一个用户或者容器独立的文件系统树。它隔离了每个进程可以看到的挂载点列表,换句话说,每个 Mount 命名空间都有它们自己的挂载点列表,意味着在不同命名空间中的进程都可以看到且控制不同的目录层次结构(目录树)。简单的说就是每个进程可以有自己独立的 Mount 节点,其他进程是无法看到的。下面看看 Android 中是如何使用的。在应用的启动时会通过下面的流程挂载模拟存储。

Zygote.forkAndSpecialize() ->
    Dalvik_dalvik_system_Zygote_forkAndSpecialize() ->
        forkAndSpecializeCommon() ->
            mountEmulatedStorage()

MountEmulatedStorage()完成具体的模拟存储挂载。

frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

static bool MountEmulatedStorage(uid_t uid, jint mount_mode, bool force_mount_namespace) {
  if (mount_mode == MOUNT_EXTERNAL_NONE && !force_mount_namespace) {
    return true;
  }

  // 在当前进程创建一个新的命名空间
  if (unshare(CLONE_NEWNS) == -1) {
    ......
  }
  ......

  // 将uid转换为用户id,一个用户最多有10000个uid
  userid_t user_id = multiuser_get_user_id(uid);

  // Bind模拟存储
  if (mount_mode == MOUNT_EXTERNAL_MULTIUSER || mount_mode == MOUNT_EXTERNAL_MULTIUSER_ALL) {
    // These paths must already be created by init.rc
    const char* source = getenv("EMULATED_STORAGE_SOURCE");
    const char* target = getenv("EMULATED_STORAGE_TARGET");
    const char* legacy = getenv("EXTERNAL_STORAGE");
    ......
    // /mnt/shell/emulated/0
    const String8 source_user(String8::format("%s/%d", source, user_id));
    // /storage/emulated/0
    const String8 target_user(String8::format("%s/%d", target, user_id));

    if (fs_prepare_dir(source_user.string(), 0000, 0, 0) == -1
        || fs_prepare_dir(target_user.string(), 0000, 0, 0) == -1) {
      return false;
    }

    if (mount_mode == MOUNT_EXTERNAL_MULTIUSER_ALL) {
      ......
    } else {
      // 将 /mnt/shell/emulated/0 绑定到 /storage/emulated/0 上
      if (TEMP_FAILURE_RETRY(
              mount(source_user.string(), target_user.string(), NULL, MS_BIND, NULL)) == -1) {
        ......
      }
    }

    if (fs_prepare_dir(legacy, 0000, 0, 0) == -1) {
        return false;
    }

    // 将 /storage/emulated/0 绑定到 /storage/emulated/legacy 上
    if (TEMP_FAILURE_RETRY(
            mount(target_user.string(), legacy, NULL, MS_BIND | MS_REC, NULL)) == -1) {
      ......
    }
  } else {
    ......
  }

  return true;       
}

实现过程也是很简单,就是在应用启动过程中创建了独立的 Mount 命名空间,在该命名空间中使用 MS_BIND 将 /mnt/shell/emulated/0/storage/emulated/0 绑定,/storage/emulated/0/storage/emulated/legacy 绑定,这样访问 /storage/emulated/0 就等同与访问 /mnt/shell/emulated/0 。因为 adb 并不是通过 zygote 启动的,所以就看不到 /storage/emulated/0 目录。

模拟存储的挂载需要使用一些环境变量,这些变量在 init.rc 中设定。下面是 init.rc 中与存储路径相关的代码。

    export EXTERNAL_STORAGE /storage/emulated/legacy # 模拟存储目录
    export EMULATED_STORAGE_SOURCE /mnt/shell/emulated # 模拟存储的源目录
    export EMULATED_STORAGE_TARGET /storage/emulated # 模拟存储的目标目录
    export SECONDARY_STORAGE /storage/sdcard1 # 扩展SD卡目录

    # Support legacy paths
    # 将模拟存储链接到/sdcard,/mnt/sdcard,/storage/sdcard0
    symlink /storage/emulated/legacy /sdcard 
    symlink /storage/emulated/legacy /mnt/sdcard
    symlink /storage/emulated/legacy /storage/sdcard0
    # 启动时将/mnt/shell/emulated/0链接为模拟存储,应用启动时会重新绑定
    symlink /mnt/shell/emulated/0 /storage/emulated/legacy
    # 外接存储的路径
    mkdir /storage/external_storage 0555 root root
    symlink /storage/sdcard1 /storage/external_storage/sdcard1
    symlink /storage/udisk0 /storage/external_storage/udisk0
    symlink /storage/sr0 /storage/external_storage/sr0

参考文档:

彻底搞懂Android文件存储---内部存储,外部存储以及各种存储路径解惑

查看原文

赞 0 收藏 0 评论 0

戈壁老王 发布了文章 · 2020-04-02

Linux 设备树(Device Tree)(转载)

本文参考以下两篇文章,整合为一篇。

ARM Linux 3.x的设备树(Device Tree)
Linux设备树(Device Tree)机制

ARM Device Tree起源

Linus Torvalds在2011年3月17日的ARM Linux邮件列表宣称 "this whole ARM thing is a f*cking pain in the ass",引发ARM Linux社区的地震,随后ARM社区进行了一系列的重大修正。在过去的ARM Linux中,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,相当多数的代码只是在描述板级细节,而这些板级细节对于内核来讲,不过是垃圾,如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data。读者有兴趣可以统计下常见的s3c2410、s3c6410等板级目录,代码量在数万行。

社区必须改变这种局面,于是PowerPC等其他体系架构下已经使用的Flattened Device Tree(FDT)进入ARM社区的视野。Device Tree是一种描述硬件的数据结构,它起源于 OpenFirmware (OF)。在Linux 2.6中,ARM架构的板极硬件细节过多地被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx,采用Device Tree后,许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。

Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性,其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被hard code到kernel中):

  • CPU的数量和类别
  • 内存基地址和大小
  • 总线和桥
  • 外设连接
  • 中断控制器和中断使用情况
  • GPIO控制器和GPIO使用情况
  • Clock控制器和Clock使用情况

它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。

Device Tree组成和结构

整个Device Tree牵涉面比较广,即增加了新的用于描述设备硬件信息的文本格式,又增加了编译这一文本的工具,同时Bootloader也需要支持将编译后的Device Tree传递给Linux内核。

设备树包含DTC(device tree compiler),DTS(device tree source和DTB(device tree blob)。其对应关系如下,

devtree-1.jpg

DTS (device tree source)

.dts文件是一种ASCII 文本格式的Device Tree描述,此文本格式非常人性化,适合人类的阅读习惯。基本上,在ARM Linux在,一个.dts文件对应一个ARM的machine,一般放置在内核的arch/arm/boot/dts/目录。

DTS 基本结构

.dts(或者其include的.dtsi)基本元素即为前文所述的结点和属性:

/ {
    node1 {
        a-string-property = "A string";
        a-string-list-property = "first string", "second string";
        a-byte-data-property = [0x01 0x23 0x34 0x56];
        child-node1 {
            first-child-property;
            second-child-property = <1>;
            a-string-property = "Hello, world";
        };
        child-node2 {
        };
    };
    node2 {
        an-empty-property;
        a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
        child-node1 {
        };
    };
};

上述.dts文件并没有什么真实的用途,但它基本表征了一个Device Tree源文件的结构:

  • 1个root结点"/";
  • root结点下面含一系列子结点,本例中为"node1" 和 "node2";
  • 结点"node1"下又含有一系列子结点,本例中为"child-node1" 和 "child-node2";
  • 各结点都有一系列属性。这些属性可能为空,如" an-empty-property";可能为字符串,如"a-- string-property";可能为字符串数组,如"a-string-list-property";可能为Cells(由u32整数组成),如"second-child-property",可能为二进制数,如"a-byte-data-property"。

DTSI

由于一个SoC可能对应多个machine(一个SoC可以对应多个产品和电路板),势必这些.dts文件需包含许多共同的部分,Linux内核为了简化,把SoC公用的部分或者多个machine共同的部分一般提炼为.dtsi,类似于C语言的头文件。其他的machine对应的.dts就include这个.dtsi。譬如,对于VEXPRESS而言,vexpress-v2m.dtsi就被vexpress-v2p-ca9.dts所引用, vexpress-v2p-ca9.dts有如下一行:

/include/ "vexpress-v2m.dtsi"

当然,和C语言的头文件类似,.dtsi也可以include其他的.dtsi,譬如几乎所有的ARM SoC的.dtsi都引用了skeleton.dtsi。skeleton.dtsi的内容如下,

/ {
    #address-cells = <1>;
    #size-cells = <1>;
    chosen { };
    aliases { };
    memory { device_type = "memory"; reg = <0 0>; };
};

上述内容表明,以“/”根节点为parent的子节点中,reg属性address和length字段的长度分别为1。具体节点的描述如下。

chosen node

chosen node 主要用来描述由系统指定的runtime parameter,它并没有描述任何硬件设备节点信息。原先通过tag list传递的一些linux kernel运行的参数,可以通过chosen节点来传递。如command line可以通过bootargs这个property来传递。如果存在chosen node,它的parent节点必须为“/”根节点。

chosen {
    bootargs = "tegraid=40.0.0.00.00 vmalloc=256M video=tegrafb console=ttyS0,115200n8 earlyprintk";
};
aliases node

aliases node用来定义别名,类似C++中引用。

aliases {
    i2c6 = &pca9546_i2c0;
    i2c7 = &pca9546_i2c1;
    i2c8 = &pca9546_i2c2;
    i2c9 = &pca9546_i2c3;
};

上面是一个在.dtsi中的典型应用,当使用i2c6时,也即使用pca9546_i2c0,使得引用节点变得简单方便。例:当.dts include 该.dtsi时,将i2c6的status属性赋值为okay,则表明该主板上的pca9546_i2c0处于enable状态;反之,status赋值为disabled,则表明该主板上的pca9546_i2c0处于disenable状态。

memory node

对于memory node,device_type必须为memory。

memory {
    device_type = "memory";
    reg = <0x00000000 0x20000000>; /* 512 MB */
};

上述描述表明该memory node是以0x00000000为起始地址,以0x20000000为结束地址的512MB的空间。一般而言,在.dts中不对memory进行描述,而是通过bootargs中类似521M@0x00000000的方式传递给内核。

DTS 基本描述

下面以一个最简单的machine为例来看如何写一个.dts文件。假设此machine的配置如下:

  • 1个双核ARM Cortex-A9 32位处理器;
  • ARM的local bus上的内存映射区域分布了2个串口(分别位于0x101F1000 和 0x101F2000)、GPIO控制器(位于0x101F3000)、SPI控制器(位于0x10170000)、中断控制器(位于0x10140000)和一个external bus桥;
  • External bus桥上又连接了SMC SMC91111 Ethernet(位于0x10100000)、I2C控制器(位于0x10160000)、64MB NOR Flash(位于0x30000000);
  • External bus桥上连接的I2C控制器所对应的I2C总线上又连接了Maxim DS1338实时钟(I2C地址为0x58)。

其对应的.dts文件为:

/ {
    compatible = "acme,coyotes-revenge";
    #address-cells = <1>;
    #size-cells = <1>;
    interrupt-parent = <&intc>;

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a9";
            reg = <0>;
        };
        cpu@1 {
            compatible = "arm,cortex-a9";
            reg = <1>;
        };
    };
    
    serial@101f0000 {
        compatible = "arm,pl011";
        reg = <0x101f0000 0x1000 >;
        interrupts = < 1 0 >;
    };
    
    serial@101f2000 {
        compatible = "arm,pl011";
        reg = <0x101f2000 0x1000 >;
        interrupts = < 2 0 >;
    };
    
    gpio@101f3000 {
        compatible = "arm,pl061";
        reg = <0x101f3000 0x1000
               0x101f4000 0x0010>;
        interrupts = < 3 0 >;
    };
    
    intc: interrupt-controller@10140000 {
        compatible = "arm,pl190";
        reg = <0x10140000 0x1000 >;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
    
    spi@10115000 {
        compatible = "arm,pl022";
        reg = <0x10115000 0x1000 >;
        interrupts = < 4 0 >;
    };
    
    external-bus {
        #address-cells = <2>
        #size-cells = <1>;
        ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet
                  1 0  0x10160000   0x10000     // Chipselect 2, i2c controller
                  2 0  0x30000000   0x1000000>; // Chipselect 3, NOR Flash
    
        ethernet@0,0 {
            compatible = "smc,smc91c111";
            reg = <0 0 0x1000>;
            interrupts = < 5 2 >;
        };
    
        i2c@1,0 {
            compatible = "acme,a1234-i2c-bus";
            #address-cells = <1>;
            #size-cells = <0>;
            reg = <1 0 0x1000>;
            interrupts = < 6 2 >;
            rtc@58 {
                compatible = "maxim,ds1338";
                reg = <58>;
                interrupts = < 7 3 >;
            };
        };
    
        flash@2,0 {
            compatible = "samsung,k8f1315ebm", "cfi-flash";
            reg = <2 0 0x4000000>;
        };
    };
};
compatible

上述.dts文件中,root结点"/"的compatible 属性compatible = "acme,coyotes-revenge";定义了系统的名称,它的组织形式为:<manufacturer>,<model>。Linux内核透过root结点"/"的compatible 属性即可判断它启动的是什么machine。

在.dts文件的每个设备,都有一个compatible 属性,compatible属性用户驱动和设备的绑定。compatible 属性是一个字符串的列表,列表中的第一个字符串表征了结点代表的确切设备,形式为"<manufacturer>,<model>",其后的字符串表征可兼容的其他设备。可以说前面的是特指,后面的则涵盖更广的范围。如在arch/arm/boot/dts/vexpress-v2m.dtsi中的Flash结点:

flash@0,00000000 {
     compatible = "arm,vexpress-flash", "cfi-flash";
     reg = <0 0x00000000 0x04000000>,
     <1 0x00000000 0x04000000>;
     bank-width = <4>;
 };

compatible属性的第2个字符串"cfi-flash"明显比第1个字符串"arm,vexpress-flash"涵盖的范围更广。再比如,Freescale MPC8349 SoC含一个串口设备,它实现了国家半导体(National Semiconductor)的ns16550 寄存器接口。则MPC8349串口设备的compatible属性为compatible = "fsl,mpc8349-uart", "ns16550"。其中,fsl,mpc8349-uart指代了确切的设备, ns16550代表该设备与National Semiconductor 的16550 UART保持了寄存器兼容。

在设备匹配驱动过程中,优先级为从左向右。本例中flash的驱动优先寻找“arm,vexpress-flash”驱动,若没有找到,则通过“cfi-flash”来继续寻找合适的驱动。

设备节点

示例中root结点"/"的cpus子结点下面又包含2个cpu子结点,描述了此machine上的2个CPU,并且二者的compatible 属性为"arm,cortex-a9"。注意cpus和cpus的2个cpu子结点的命名,它们遵循的组织形式为:<name>[@<unit-address>],<>中的内容是必选项,[]中的则为可选项。name是一个ASCII字符串,用于描述结点对应的设备类型,如3com Ethernet适配器对应的结点name宜为ethernet,而不是3com509。如果一个结点描述的设备有地址,则应该给出@unit-address。多个相同类型设备结点的name可以一样,只要unit-address不同即可,如本例中含有cpu@0、cpu@1以及serial@101f0000与serial@101f2000这样的同名结点。设备的unit-address地址也经常在其对应结点的reg属性中给出。ePAPR标准给出了结点命名的规范。

reg

可寻址的设备使用如下信息来在Device Tree中编码地址信息:

        reg
        #address-cells
        #size-cells

reg描述了memory-mapped IO register的address和length。address 和 length 字段是可变长的,父结点的#address-cells和#size-cells分别决定了子结点的reg属性的address和length字段的长度。reg的组织形式为

reg = <address1 length1 [address2 length2] [address3 length3] ... >

其中的每一组address length表明了设备使用的一个地址范围。address为1个或多个32位的整型(即cell),而length则为cell的列表或者为空(若#size-cells = 0)。

在本例中,root结点的#address-cells = <1>;和#size-cells = <1>;决定了serial、gpio、spi等结点的address和length字段的长度分别为1。cpus 结点的#address-cells = <1>;和#size-cells = <0>;决定了2个cpu子结点的address为1,而length为空,于是形成了2个cpu的reg = <0>;和reg = <1>;。external-bus结点的#address-cells = <2>和#size-cells = <1>;决定了其下的ethernet、i2c、flash的reg字段形如reg = <0 0 0x1000>;、reg = <1 0 0x1000>;和reg = <2 0 0x4000000>;。其中,address字段长度为0,开始的第一个cell(0、1、2)是对应的片选,第2个cell(0,0,0)是相对该片选的基地址,第3个cell(0x1000、0x1000、0x4000000)为length。特别要留意的是i2c结点中定义的 #address-cells = <1>;和#size-cells = <0>;又作用到了I2C总线上连接的RTC,它的address字段为0x58,是设备的I2C地址。

ranges

root结点的子结点描述的是CPU的视图,因此root子结点的address区域就直接位于CPU的memory区域。但是,经过总线桥后的address往往需要经过转换才能对应的CPU的memory映射。external-bus的ranges属性定义了经过external-bus桥后的地址范围如何映射到CPU的memory区域。

        ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet
                  1 0  0x10160000   0x10000     // Chipselect 2, i2c controller
                  2 0  0x30000000   0x1000000>; // Chipselect 3, NOR Flash

ranges是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。映射表中的子地址、父地址分别采用子地址空间的#address-cells和父地址空间的#address-cells大小。对于本例而言,子地址空间的#address-cells为2,父地址空间的#address-cells值为1,因此0 0 0x10100000 0x10000的前2个cell为external-bus后片选0上偏移0,第3个cell表示external-bus后片选0上偏移0的地址空间被映射到CPU的0x10100000位置,第4个cell表示映射的大小为0x10000。ranges的后面2个项目的含义可以类推。

interrupt

Device Tree中还可以中断连接信息,对于中断控制器而言,它提供如下属性:

  • interrupt-controller – 这个属性为空,中断控制器应该加上此属性表明自己的身份;
  • interrupt-cells – 与#address-cells 和 #size-cells相似,它表明连接此中断控制器的设备的interrupts属性的cell大小。

在整个Device Tree中,与中断相关的属性还包括:

  • interrupt-parent – 设备结点透过它来指定它所依附的中断控制器的phandle,当结点没有指定interrupt-parent 时,则从父级结点继承。对于本例而言,root结点指定了interrupt-parent = <&intc>;其对应于intc: interrupt-controller@10140000,而root结点的子结点并未指定interrupt-parent,因此它们都继承了intc,即位于0x10140000的中断控制器。
  • interrupts – 用到了中断的设备结点透过它指定中断号、触发方法等,具体这个属性含有多少个cell,由它依附的中断控制器结点的#interrupt-cells属性决定。而具体每个cell又是什么含义,一般由驱动的实现决定,而且也会在Device Tree的binding文档中说明。譬如,对于ARM GIC中断控制器而言,#interrupt-cells为3,它3个cell的具体含义

Documentation/devicetree/bindings/arm/gic.txt就有如下文字说明:

01   The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI
02   interrupts.
03
04   The 2nd cell contains the interrupt number for the interrupt type.
05   SPI interrupts are in the range [0-987].  PPI interrupts are in the
06   range [0-15].
07
08   The 3rd cell is the flags, encoded as follows:
09         bits[3:0] trigger type and level flags.
10                 1 = low-to-high edge triggered
11                 2 = high-to-low edge triggered
12                 4 = active high level-sensitive
13                 8 = active low level-sensitive
14         bits[15:8] PPI interrupt cpu mask.  Each bit corresponds to each of
15         the 8 possible cpus attached to the GIC.  A bit set to '1' indicated
16         the interrupt is wired to that CPU.  Only valid for PPI interrupts.

另外,值得注意的是,一个设备还可能用到多个中断号。对于ARM GIC而言,若某设备使用了SPI的168、169号2个中断,而言都是高电平触发,则该设备结点的interrupts属性可定义为:interrupts = <0 168 4>, <0 169 4>;

除了中断以外,在ARM Linux中clock、GPIO、pinmux都可以透过.dts中的结点和属性进行描述。对于Device Tree中的结点和属性具体是如何来描述设备的硬件细节的,一般需要文档来进行讲解,文档的后缀名一般为.txt。这些文档位于内核的Documentation/devicetree/bindings目录,其下又分为很多子目录。

DTC (device tree compiler)

将.dts编译为.dtb的工具。DTC的源代码位于内核的scripts/dtc目录,在Linux内核使能了Device Tree的情况下,编译内核的时候主机工具dtc会被编译出来,对应scripts/dtc/Makefile中的“hostprogs-y := dtc”这一hostprogs编译target。

在Linux内核的arch/arm/boot/dts/Makefile中,描述了当某种SoC被选中后,哪些.dtb文件会被编译出来,如与VEXPRESS对应的.dtb包括:

dtb-$(CONFIG_ARCH_VEXPRESS) += vexpress-v2p-ca5s.dtb \
        vexpress-v2p-ca9.dtb \
        vexpress-v2p-ca15-tc1.dtb \
        vexpress-v2p-ca15_a7.dtb \
        xenvm-4.2.dtb

在Linux下,我们可以单独编译Device Tree文件。当我们在Linux内核下运行make dtbs时,若我们之前选择了ARCH_VEXPRESS,上述.dtb都会由对应的.dts编译出来。因为arch/arm/Makefile中含有一个dtbs编译target项目。

Device Tree Blob (.dtb)

.dtb是.dts被DTC编译后的二进制格式的Device Tree描述,可由Linux内核解析。通常在我们为电路板制作NAND、SD启动image时,会为.dtb文件单独留下一个很小的区域以存放之,之后bootloader在引导kernel的过程中,会先读取该.dtb到内存。

DTB由三部分组成:头(Header)、结构块(device-tree structure)、字符串块(device-tree string),它的布局结构如下,

devtree-2.png

Header

在kernelincludelinuxof_fdt.h文件中有相关定义,

devtree-3.png

device-tree structure

设备树结构块是一个线性化的结构体,是设备树的主体,以节点的形式保存了主板上的设备信息。

在结构块中,以宏OF_DT_BEGIN_NODE标志一个节点的开始,以宏OF_DT_END_NODE标识一个节点的结束,整个结构块以宏OF_DT_END (0x00000009)结束。在kernelincludelinuxof_fdt.h中有相关定义,我们把这些宏称之为token。

  • FDT_BEGIN_NODE (0x00000001)。该token描述了一个node的开始位置,紧挨着该token的就是node name(包括unit address)
  • FDT_END_NODE (0x00000002)。该token描述了一个node的结束位置。
  • FDT_PROP (0x00000003)。该token描述了一个property的开始位置,该token之后是两个u32的数据,分别是length和name offset。length表示该property value data的size。name offset表示该属性字符串在device tree strings block的偏移值。length和name offset之后就是长度为length具体的属性值数据。
  • FDT_NOP (0x00000004)。
  • FDT_END (0x00000009)。该token标识了一个DTB的结束位置。

一个节点的结构如下:

  1. 节点开始标志:一般为OF_DT_BEGIN_NODE(0x00000001)。
  2. 节点路径或者节点的单元名(version<3以节点路径表示,version>=0x10以节点单元名表示)
  3. 填充字段(对齐到四字节)
  4. 节点属性。每个属性以宏OF_DT_PROP(0x00000003)开始,后面依次为属性值的字节长度(4字节)、属性名称在字符串块中的偏移量(4字节)、属性值和填充(对齐到四字节)。
  5. 如果存在子节点,则定义子节点。
  6. 节点结束标志OF_DT_END_NODE(0x00000002)。

device-tree string

通过节点的定义知道节点都有若干属性,而不同的节点的属性又有大量相同的属性名称,因此将这些属性名称提取出一张表,当节点需要应用某个属性名称时,直接在属性名字段保存该属性名称在字符串块中的偏移量。

memory reserve map

这个区域包括了若干的reserve memory描述符。每个reserve memory描述符是由address和size组成。其中address和size都是用U64来描述。

有些系统,我们也许会保留一些memory有特殊用途(例如DTB或者initrd image),或者在有些DSP+ARM的SOC platform上,有些memory被保留用于ARM和DSP进行信息交互。这些保留内存不会进入内存管理系统。

Bootloader

Uboot mainline 从 v1.1.3开始支持Device Tree,其对ARM的支持则是和ARM内核支持Device Tree同期完成。为了使能Device Tree,需要编译Uboot的时候在config文件中加入

#define CONFIG_OF_LIBFDT

在Uboot中,可以从NAND、SD或者TFTP等任意介质将.dtb读入内存,假设.dtb放入的内存地址为0x71000000,之后可在Uboot运行命令fdt addr命令设置.dtb的地址,如:

U-Boot> fdt addr 0x71000000

fdt的其他命令就变的可以使用,如fdt resize、fdt print等。

对于ARM来讲,可以透过bootz kernel_addr initrd_address dtb_address的命令来启动内核,即dtb_address作为bootz或者bootm的最后一次参数,第一个参数为内核映像的地址,第二个参数为initrd的地址,若不存在initrd,可以用 -代替。

Device Tree引发的BSP和驱动变更

有了Device Tree后,大量的板级信息都不再需要,譬如过去经常在arch/arm/plat-xxx和arch/arm/mach-xxx实施的事情,都可以通过Device Tree转化为统一的标准处理。

注册platform_device,绑定resource,即内存、IRQ等板级信息。

透过Device Tree后,形如

    90 static struct resource xxx_resources[] = {
    91         [0] = {
    92                 .start  = …,
    93                 .end    = …,
    94                 .flags  = IORESOURCE_MEM,
    95         },
    96         [1] = {
    97                 .start  = …,
    98                 .end    = …,
    99                 .flags  = IORESOURCE_IRQ,
    100         },
    101 };
    102
    103 static struct platform_device xxx_device = {
    104         .name           = "xxx",
    105         .id             = -1,
    106         .dev            = {
    107                                 .platform_data          = &xxx_data,
    108         },
    109         .resource       = xxx_resources,
    110         .num_resources  = ARRAY_SIZE(xxx_resources),
    111 };

之类的platform_device代码都不再需要,其中platform_device会由kernel自动展开。而这些resource实际来源于.dts中设备结点的reg、interrupts属性。典型地,大多数总线都与“simple_bus”兼容,而在SoC对应的machine的.init_machine成员函数中,调用of_platform_bus_probe(NULL, xxx_of_bus_ids, NULL); 即可自动展开所有的platform_device。譬如,假设我们有个XXX SoC,则可在arch/arm/mach-xxx/的板文件中透过如下方式展开.dts中的设备结点对应的platform_device:

    18 static struct of_device_id xxx_of_bus_ids[] __initdata = {
    19         { .compatible = "simple-bus", },
    20         {},
    21 };
    22
    23 void __init xxx_mach_init(void)
    24 {
    25         of_platform_bus_probe(NULL, xxx_of_bus_ids, NULL);
    26 }
    32
    33 #ifdef CONFIG_ARCH_XXX
    38
    39 DT_MACHINE_START(XXX_DT, "Generic XXX (Flattened Device Tree)")
    41         …
    45         .init_machine   = xxx_mach_init,
    46         …
    49 MACHINE_END
    50 #endif

注册i2c_board_info,指定IRQ等板级信息。

形如

    145 static struct i2c_board_info __initdata afeb9260_i2c_devices[] = {
    146         {
    147                 I2C_BOARD_INFO("tlv320aic23", 0x1a),
    148         }, {
    149                 I2C_BOARD_INFO("fm3130", 0x68),
    150         }, {
    151                 I2C_BOARD_INFO("24c64", 0x50),
    152         },
    153 };

之类的i2c_board_info代码,目前不再需要出现,现在只需要把tlv320aic23、fm3130、24c64这些设备结点填充作为相应的I2C controller结点的子结点即可,类似于前面的

          i2c@1,0 {
                compatible = "acme,a1234-i2c-bus";
                …
                rtc@58 {
                    compatible = "maxim,ds1338";
                    reg = <58>;
                    interrupts = < 7 3 >;
                };
            };

Device Tree中的I2C client会透过I2C host驱动的probe()函数中调用of_i2c_register_devices(&i2c_dev->adapter); 被自动展开。

注册spi_board_info,指定IRQ等板级信息。

形如

    79 static struct spi_board_info afeb9260_spi_devices[] = {
    80         {       /* DataFlash chip */
    81                 .modalias       = "mtd_dataflash",
    82                 .chip_select    = 1,
    83                 .max_speed_hz   = 15 * 1000 * 1000,
    84                 .bus_num        = 0,
    85         },
    86 };

之类的spi_board_info代码,目前不再需要出现,与I2C类似,现在只需要把mtd_dataflash之类的结点,作为SPI控制器的子结点即可,SPI host驱动的probe函数透过spi_register_master()注册master的时候,会自动展开依附于它的slave。

多个针对不同电路板的machine,以及相关的callback。

过去,ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围起来的针对这个machine的一系列callback,譬如:

    373 MACHINE_START(VEXPRESS, "ARM-Versatile Express")
    374         .atag_offset    = 0x100,
    375         .smp            = smp_ops(vexpress_smp_ops),
    376         .map_io         = v2m_map_io,
    377         .init_early     = v2m_init_early,
    378         .init_irq       = v2m_init_irq,
    379         .timer          = &v2m_timer,
    380         .handle_irq     = gic_handle_irq,
    381         .init_machine   = v2m_init,
    382         .restart        = vexpress_restart,
    383 MACHINE_END

这些不同的machine会有不同的MACHINE ID,Uboot在启动Linux内核时会将MACHINE ID存放在r1寄存器,Linux启动时会匹配Bootloader传递的MACHINE ID和MACHINE_START声明的MACHINE ID,然后执行相应machine的一系列初始化函数。

引入Device Tree之后,MACHINE_START变更为DT_MACHINE_START,其中含有一个.dt_compat成员,用于表明相关的machine与.dts中root结点的compatible属性兼容关系。如果Bootloader传递给内核的Device Tree中root结点的compatible属性出现在某machine的.dt_compat表中,相关的machine就与对应的Device Tree匹配,从而引发这一machine的一系列初始化函数被执行。

    489 static const char * const v2m_dt_match[] __initconst = {
    490         "arm,vexpress",
    491         "xen,xenvm",
    492         NULL,
    493 };
    495 DT_MACHINE_START(VEXPRESS_DT, "ARM-Versatile Express")
    496         .dt_compat      = v2m_dt_match,
    497         .smp            = smp_ops(vexpress_smp_ops),
    498         .map_io         = v2m_dt_map_io,
    499         .init_early     = v2m_dt_init_early,
    500         .init_irq       = v2m_dt_init_irq,
    501         .timer          = &v2m_dt_timer,
    502         .init_machine   = v2m_dt_init,
    503         .handle_irq     = gic_handle_irq,
    504         .restart        = vexpress_restart,
    505 MACHINE_END

Linux倡导针对多个SoC、多个电路板的通用DT machine,即一个DT machine的.dt_compat表含多个电路板.dts文件的root结点compatible属性字符串。之后,如果的电路板的初始化序列不一样,可以透过int of_machine_is_compatible(const char *compat) API判断具体的电路板是什么。

譬如arch/arm/mach-exynos/mach-exynos5-dt.c的EXYNOS5_DT machine同时兼容 "samsung,exynos5250"和"samsung,exynos5440":

    158 static char const *exynos5_dt_compat[] __initdata = {
    159         "samsung,exynos5250",
    160         "samsung,exynos5440",
    161         NULL
    162 };
    163
    177 DT_MACHINE_START(EXYNOS5_DT, "SAMSUNG EXYNOS5 (Flattened Device Tree)")
    178         /* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
    179         .init_irq       = exynos5_init_irq,
    180         .smp            = smp_ops(exynos_smp_ops),
    181         .map_io         = exynos5_dt_map_io,
    182         .handle_irq     = gic_handle_irq,
    183         .init_machine   = exynos5_dt_machine_init,
    184         .init_late      = exynos_init_late,
    185         .timer          = &exynos4_timer,
    186         .dt_compat      = exynos5_dt_compat,
    187         .restart        = exynos5_restart,
    188         .reserve        = exynos5_reserve,
    189 MACHINE_END

它的.init_machine成员函数就针对不同的machine进行了不同的分支处理:

    126 static void __init exynos5_dt_machine_init(void)
    127 {
    128         …
    149
    150         if (of_machine_is_compatible("samsung,exynos5250"))
    151                 of_platform_populate(NULL, of_default_bus_match_table,
    152                                      exynos5250_auxdata_lookup, NULL);
    153         else if (of_machine_is_compatible("samsung,exynos5440"))
    154                 of_platform_populate(NULL, of_default_bus_match_table,
    155                                      exynos5440_auxdata_lookup, NULL);
    156 }

使用Device Tree后,驱动需要与.dts中描述的设备结点进行匹配,从而引发驱动的probe()函数执行。对于platform_driver而言,需要添加一个OF匹配表,如前文的.dts文件的"acme,a1234-i2c-bus"兼容I2C控制器结点的OF匹配表可以是:

    436 static const struct of_device_id a1234_i2c_of_match[] = {
    437         { .compatible = "acme,a1234-i2c-bus ", },
    438         {},
    439 };
    440 MODULE_DEVICE_TABLE(of, a1234_i2c_of_match);
    441
    442 static struct platform_driver i2c_a1234_driver = {
    443         .driver = {
    444                 .name = "a1234-i2c-bus ",
    445                 .owner = THIS_MODULE,
    449                 .of_match_table = a1234_i2c_of_match,
    450         },
    451         .probe = i2c_a1234_probe,
    452         .remove = i2c_a1234_remove,
    453 };
    454 module_platform_driver(i2c_a1234_driver);

对于I2C和SPI从设备而言,同样也可以透过of_match_table添加匹配的.dts中的相关结点的compatible属性,如sound/soc/codecs/wm8753.c中的:

    1533 static const struct of_device_id wm8753_of_match[] = {
    1534         { .compatible = "wlf,wm8753", },
    1535         { }
    1536 };
    1537 MODULE_DEVICE_TABLE(of, wm8753_of_match);
    1587 static struct spi_driver wm8753_spi_driver = {
    1588         .driver = {
    1589                 .name   = "wm8753",
    1590                 .owner  = THIS_MODULE,
    1591                 .of_match_table = wm8753_of_match,
    1592         },
    1593         .probe          = wm8753_spi_probe,
    1594         .remove         = wm8753_spi_remove,
    1595 };
    1640 static struct i2c_driver wm8753_i2c_driver = {
    1641         .driver = {
    1642                 .name = "wm8753",
    1643                 .owner = THIS_MODULE,
    1644                 .of_match_table = wm8753_of_match,
    1645         },
    1646         .probe =    wm8753_i2c_probe,
    1647         .remove =   wm8753_i2c_remove,
    1648         .id_table = wm8753_i2c_id,
    1649 };

不过这边有一点需要提醒的是,I2C和SPI外设驱动和Device Tree中设备结点的compatible 属性还有一种弱式匹配方法,就是别名匹配。compatible 属性的组织形式为<manufacturer>,<model>,别名其实就是去掉compatible 属性中逗号前的manufacturer前缀。关于这一点,可查看drivers/spi/spi.c的源代码,函数spi_match_device()暴露了更多的细节,如果别名出现在设备spi_driver的id_table里面,或者别名与spi_driver的name字段相同,SPI设备和驱动都可以匹配上:

    90 static int spi_match_device(struct device *dev, struct device_driver *drv)
    91 {
    92         const struct spi_device *spi = to_spi_device(dev);
    93         const struct spi_driver *sdrv = to_spi_driver(drv);
    94
    95         /* Attempt an OF style match */
    96         if (of_driver_match_device(dev, drv))
    97                 return 1;
    98
    99         /* Then try ACPI */
    100         if (acpi_driver_match_device(dev, drv))
    101                 return 1;
    102
    103         if (sdrv->id_table)
    104                 return !!spi_match_id(sdrv->id_table, spi);
    105
    106         return strcmp(spi->modalias, drv->name) == 0;
    107 }
    71 static const struct spi_device_id *spi_match_id(const struct spi_device_id *id,
    72                                                 const struct spi_device *sdev)
    73 {
    74         while (id->name[0]) {
    75                 if (!strcmp(sdev->modalias, id->name))
    76                         return id;
    77                 id++;
    78         }
    79         return NULL;
    80 }

解析DTB的函数及相关数据结构

machine_desc结构

struct machine_desc {
    unsigned int        nr;     /* architecture number  */
    const char      *name;      /* architecture name    */
    unsigned long       atag_offset;    /* tagged list (relative) */
    const char *const   *dt_compat; /* array of device tree
                         * 'compatible' strings */

    unsigned int        nr_irqs;    /* number of IRQs */

#ifdef CONFIG_ZONE_DMA
    phys_addr_t     dma_zone_size;  /* size of DMA-able area */
#endif

    unsigned int        video_start;    /* start of video RAM   */
    unsigned int        video_end;  /* end of video RAM */

    unsigned char       reserve_lp0 :1; /* never has lp0    */
    unsigned char       reserve_lp1 :1; /* never has lp1    */
    unsigned char       reserve_lp2 :1; /* never has lp2    */
    enum reboot_mode    reboot_mode;    /* default restart mode */
    struct smp_operations   *smp;       /* SMP operations   */
    bool            (*smp_init)(void);
    void            (*fixup)(struct tag *, char **,
                     struct meminfo *);
    void            (*init_meminfo)(void);
    void            (*reserve)(void);/* reserve mem blocks  */
    void            (*map_io)(void);/* IO mapping function  */
    void            (*init_early)(void);
    void            (*init_irq)(void);
    void            (*init_time)(void);
    void            (*init_machine)(void);
    void            (*init_late)(void);
#ifdef CONFIG_MULTI_IRQ_HANDLER
    void            (*handle_irq)(struct pt_regs *);
#endif
    void            (*restart)(enum reboot_mode, const char *);
};

内核将机器信息记录为machine_desc结构体(该定义在/arch/arm/include/asm/mach/arch.h),并保存在_arch_info_begin_arch_info_end之间(_arch_info_begin_arch_info_end为虚拟地址,是编译内核时指定的,此时mmu还未进行初始化。它其实通过汇编完成地址偏移操作)

machine_desc结构体用宏MACHINE_START进行定义,一般在/arch/arm/子目录,与板级相关的文件中进行成员函数及变量的赋值。由linker将machine_desc聚集在.arch.info.init节区形成列表。

bootloader引导内核时,ARM寄存器r2会将.dtb的首地址传给内核,内核根据该地址,解析.dtb中根节点的compatible属性,将该属性与内核中预先定义machine_desc结构体的dt_compat成员做匹配,得到最匹配的一个machine_desc

在代码中,内核通过在start_kernel->setup_arch中调用setup_machine_fdt来实现上述功能,该函数的具体实现可参见/arch/arm/kernel/devtree.c

设备节点结构体

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  device_node *next;  /* next device of same type */
    struct  device_node *allnext;   /* next in list of all nodes */
    struct  proc_dir_entry *pde;    /* this node's proc directory */
    struct  kref kref;
    unsigned long _flags;
    void    *data;
#if defined(CONFIG_SPARC)
    const char *path_component_name;
    unsigned int unique_id;
    struct of_irq_controller *irq_trans;
#endif
};

记录节点信息的结构体。.dtb经过解析之后将以device_node列表的形式存储节点信息。

属性结构体

struct property {
    char    *name; // 属性名
    int length; // 属性值长度
    void    *value; // 属性值
    struct property *next; // 指向下一个属性
    unsigned long _flags; // 标志
    unsigned int unique_id;
};

device_node结构体中的成员结构体,用于描述节点属性信息。

uboot下的相关结构体

首先我们看下uboot用于记录os、initrd、fdt信息的数据结构bootm_headers,其定义在/include/image.h中,这边截取了其中与dtb相关的一小部分。

typedef struct bootm_headers {
    ......
#if defined(CONFIG_FIT)
    ......
    void        *fit_hdr_fdt;   /* FDT blob FIT image header */
    const char  *fit_uname_fdt; /* FDT blob subimage node unit name */
    int     fit_noffset_fdt;/* FDT blob subimage node offset */
    ......
#endif
    ......
#ifdef CONFIG_LMB
    struct lmb  lmb;        /* for memory mgmt */
#endif
} bootm_headers_t;

fit_hdr_fdt指向DTB设备树镜像的头。

lmb为uboot下的一种内存管理机制,全称为logical memory blocks。用于管理镜像的内存。lmb所记录的内存信息最终会传递给kernel。这里对lmb不做展开描述。在/include/lmb.h和/lib/lmb.c中有对lmb的接口和定义的具体描述。有兴趣的读者可以看下,所包含的代码量不多。

DTB加载及解析过程

UBoot处理

devtree-4.png

先从uboot里的do_bootm出发,根据之前描述,DTB在内存中的地址通过bootm命令进行传递。在bootm中,它会根据所传进来的DTB地址,对DTB所在内存做一系列操作,为内核解析DTB提供保证。上图为对应的函数调用关系图。

在do_bootm中,主要调用函数为do_bootm_states,第四个参数为bootm所要处理的阶段和状态。

在do_bootm_states中,bootm_start会对lmb进行初始化操作,lmb所管理的物理内存块有三种方式获取。起始地址,优先级从上往下:

  1. 环境变量“bootm_low”
  2. 宏CONFIG_SYS_SDRAM_BASE(在tegra124中为0x80000000)
  3. gd->bd->bi_dram[0].start

大小:

  1. 环境变量“bootm_size”
  2. gd->bd->bi_dram[0].size

经过初始化之后,这块内存就归lmb所管辖。接着,调用bootm_find_os进行kernel镜像的相关操作,这里不具体阐述。

还记得之前讲过bootm的三个参数么,第一个参数内核地址已经被bootm_find_os处理,而接下来的两个参数会在bootm_find_other中执行操作。

  • 首先,bootm_find_other根据第二个参数找到ramdisk的地址,得到ramdisk的镜像;然后根据第三个参数得到DTB镜像,同检查kernel和ramdisk镜像一样,检查DTB镜像也会进行一系列的校验工作,如果校验错误,将无法正常启动内核。另外,uboot在确认DTB镜像无误之后,会将该地址保存在环境变量“fdtaddr”中。
  • 接着,uboot会把DTB镜像reload一次,使得DTB镜像所在的物理内存归lmb所管理:

    1. boot_fdt_add_mem_rsv_regions会将原先的内存DTB镜像所在的内存置为reserve,保证该段内存不会被其他非法使用,保证接下来的reload数据是正确的;
    2. boot_relocate_fdt会在bootmap区域中申请一块未被使用的内存,接着将DTB镜像内容复制到这块区域(即归lmb所管理的区域)
注:若环境变量中,指定“fdt_high”参数,则会根据该值,调用lmb_alloc_base函数来分配DTB镜像reload的地址空间。若分配失败,则会停止bootm操作。因而,不建议设置fdt_high参数。

接下来,do_bootm会根据内核的类型调用对应的启动函数。与linux对应的是do_bootm_linux

  1. boot_prep_linux:为启动后的kernel准备参数
  2. boot_jump_linux:

        ......
        if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
            r2 = (unsigned long)images->ft_addr;
        else
            r2 = gd->bd->bi_boot_params;
    
        if (!fake) {
            ......
                kernel_entry(0, machid, r2);
        }  

    以上是boot_jump_linux的片段代码,可以看出:若使用DTB,则原先用来存储ATAG的寄存器R2,将会用来存储.dtb镜像地址。boot_jump_linux最后将调用kernel_entry,将.dtb镜像地址传给内核。

内核处理

arch/arm/kernel/head.S中,有这样一段:

    /*
     * r1 = machine no, r2 = atags or dtb,
     * r8 = phys_offset, r9 = cpuid, r10 = procinfo
     */
    bl  __vet_atags

_vet_atags定义在/arch/arm/kernel/head-common.S中,它主要对DTB镜像做了一个简单的校验。

__vet_atags:
    tst r2, #0x3            @ aligned?
    bne 1f

    ldr r5, [r2, #0] 
#ifdef CONFIG_OF_FLATTREE
    ldr r6, =OF_DT_MAGIC        @ is it a DTB?
    cmp r5, r6
    beq 2f
#endif
    cmp r5, #ATAG_CORE_SIZE     @ is first tag ATAG_CORE?
    cmpne   r5, #ATAG_CORE_SIZE_EMPTY
    bne 1f
    ldr r5, [r2, #4] 
    ldr r6, =ATAG_CORE
    cmp r5, r6
    bne 1f

2:  mov pc, lr              @ atag/dtb pointer is ok

1:  mov r2, #0
    mov pc, lr
ENDPROC(__vet_atags)

真正解析处理dbt的开始部分,是setup_arch->setup_machine_fdt

devtree-5.png

如图,是setup_machine_fdt中的解析过程。

  1. 解析chosen节点将对boot_command_line进行初始化。
  2. 解析根节点的{size,address}将对dt_root_size_cellsdt_root_addr_cells进行初始化。为之后解析memory等其他节点提供依据。
  3. 解析memory节点,将会把节点中描述的内存,加入memory的bank。为之后的内存初始化提供条件。

解析设备树在函数unflatten_device_tree中完成,它将.dtb解析成device_node结构,并构成单项链表,以供OF的API接口使用。

下面主要结合代码分析:/drivers/of/fdt.c

void __init unflatten_device_tree(void)
{
    /* 解析设备树,将所有的设备节点链如全局链表 of_allnodes 中 */
    __unflatten_device_tree(initial_boot_params, &of_allnodes,
                early_init_dt_alloc_memory_arch);

    /* 设置内核输出终端,以及遍历“aliases”节点下的所有属性,加入相应链表 */
    /* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
    of_alias_scan(early_init_dt_alloc_memory_arch);
}
static void __unflatten_device_tree(struct boot_param_header *blob,
                 struct device_node **mynodes,
                 void * (*dt_alloc)(u64 size, u64 align))
{
    unsigned long size;
    int start;
    void *mem;
    struct device_node **allnextp = mynodes;
    ......

    /* 检查设备树 magic */
    if (be32_to_cpu(blob->magic) != OF_DT_HEADER) {
        pr_err("Invalid device tree blob header\n");
        return;
    }

    /* 找到设备树的设备节点起始地址 *//
    start = 0;
    /* 第一次调用mem传0,allnextpp传NULL,实际上是为了计算整个设备树所要的空间 */
    size = (unsigned long)unflatten_dt_node(blob, 0, &start, NULL, NULL, 0);
    size = ALIGN(size, 4);

    pr_debug("  size is %lx, allocating...\n", size);

    /* 调用early_init_dt_alloc_memory_arch函数,为设备树分配内存空间 */
    mem = dt_alloc(size + 4, __alignof__(struct device_node));
    memset(mem, 0, size);

    /* 设备树结束处赋值0xdeadbeef,为了后面检查是否有数据溢出 */
    *(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);

    pr_debug("  unflattening %p...\n", mem);

    /* 再次获取设备树的设备节点起始地址 */
    start = 0;
    /* mem为设备树分配的内存空间,allnextp指向全局变量of_allnodes,生成整个设备树 */
    unflatten_dt_node(blob, mem, &start, NULL, &allnextp, 0);
    if (be32_to_cpup(mem + size) != 0xdeadbeef)
        pr_warning("End of tree marker overwritten: %08x\n",
               be32_to_cpup(mem + size));
    *allnextp = NULL;

    pr_debug(" <- unflatten_device_tree()\n");
}
static void * unflatten_dt_node(struct boot_param_header *blob,
                void *mem,
                int *poffset,
                struct device_node *dad,
                struct device_node ***allnextpp,
                unsigned long fpsize)
{
    ......
    /* 获得节点名或节点路径名 */
    pathp = fdt_get_name(blob, *poffset, &l);
    if (!pathp)
        return mem;
    
    allocl = l++; // 节点名称的长度

    /* 如果是节点名则进入,若是节点路径名则(*pathp) == '/' */
    if ((*pathp) != '/') {
        new_format = 1;
        if (fpsize == 0) {
            ......
        } else {
            fpsize += l; // 待分配的长度=本节点名称长度+父亲节点绝对路径的长度
            allocl = fpsize;
        }
    }

    /* 分配一个设备节点结构device_node,mem记录了分配空间大小,最终会累加,计算出该设备树总共分配的空间 */
    np = unflatten_dt_alloc(&mem, sizeof(struct device_node) + allocl,
                __alignof__(struct device_node));
    /* 第一次调用unflatten_dt_alloc时,allnextpp=NULL。第二次时, allnextpp指向全局变量of_allnodes */
    if (allnextpp) {
        char *fn;
        np->full_name = fn = ((char *)np) + sizeof(*np); // full_name保存完整节点名,包括各级父节点
        /* 若new_format=1,表示pathp保存的是节点名,不是路径名,所以需要加上父节点名称 */
        if (new_format) {
            if (dad && dad->parent) {
                strcpy(fn, dad->full_name); // 拷贝父节点绝对路径
                fn += strlen(fn);
            }
            *(fn++) = '/';
        }
        memcpy(fn, pathp, l); // 拷贝本节点名称

        prev_pp = &np->properties; // prev_pp指向节点的属性链表
        **allnextpp = np;
        *allnextpp = &np->allnext;
        /* 若父节点不为空,则设置该节点的parent */
        if (dad != NULL) {
            np->parent = dad; // 指向父节点
            if (dad->next == NULL)
                dad->child = np; // child指向第一个孩子
            else
                dad->next->sibling = np; // 把np插入next,这样子节点形成链表
            dad->next = np;
        }
        kref_init(&np->kref);
    }
    /* 处理该节点的属性 */
    for (offset = fdt_first_property_offset(blob, *poffset);
         (offset >= 0);
         (offset = fdt_next_property_offset(blob, offset))) {
        ......
        /* 获取属性名称 */
        if (!(p = fdt_getprop_by_offset(blob, offset, &pname, &sz))) {
            offset = -FDT_ERR_INTERNAL;
            break;
        }
        ......
        /* 是否有名称为name的属性 */
        if (strcmp(pname, "name") == 0)
            has_name = 1;
        /* 为该属性分配一个属性结构,即struct property */
        pp = unflatten_dt_alloc(&mem, sizeof(struct property),
                    __alignof__(struct property));
        if (allnextpp) {
            ......
            pp->name = (char *)pname; // 属性名
            pp->length = sz; // 属性值长度
            pp->value = (__be32 *)p; // 属性值
            *prev_pp = pp; // 将属性插入该节点的属性链表
            prev_pp = &pp->next;
        }
    }
    /* 如果该节点没有“name”的属性,则生成一个name属性,插入到该节点的属性链表 */
    if (!has_name) {
        ......
    }
    /* allnextpp被设置时,构建np */
    if (allnextpp) {
        ......
    }

    old_depth = depth;
    *poffset = fdt_next_node(blob, *poffset, &depth);
    if (depth < 0)
        depth = 0;
    /* 遍历子节点 */
    while (*poffset > 0 && depth > old_depth)
        mem = unflatten_dt_node(blob, mem, poffset, np, allnextpp,
                    fpsize);
    ......
    return mem;
}
void of_alias_scan(void * (*dt_alloc)(u64 size, u64 align))
{
    struct property *pp;

    /* 根据全局链表of_allnodes,查找"/chosen"或"/chosen@0"节点,赋值给全局变量of_chosen */
    of_chosen = of_find_node_by_path("/chosen");
    if (of_chosen == NULL)
        of_chosen = of_find_node_by_path("/chosen@0");

    /* 如果of_chosen存在,则查找"linux,stdout-path",该属性为标准终端设备节点路径名,内核以此做为默认终端 */
    if (of_chosen) {
        const char *name;

        name = of_get_property(of_chosen, "linux,stdout-path", NULL);
        if (name)
            of_stdout = of_find_node_by_path(name);
    }

    /* 查找"/aliases"节点,赋值给全局变量of_aliases */
    of_aliases = of_find_node_by_path("/aliases");
    if (!of_aliases)
        return;

    /* 遍历of_aliases下的所有属性 */
    for_each_property_of_node(of_aliases, pp) {
        ......
        /* 跳过一些属性 */
        if (!strcmp(pp->name, "name") ||
            !strcmp(pp->name, "phandle") ||
            !strcmp(pp->name, "linux,phandle"))
            continue;

        /* 根据属性找到对应的设备节点 */
        np = of_find_node_by_path(pp->value);
        if (!np)
            continue;

        /* 去除属性名中尾部的数字,即设备id */
        while (isdigit(*(end-1)) && end > start)
            end--;
        len = end - start;

        /* 将属性名中尾部的数字转化为十进制数,做为设备id号 */
        if (kstrtoint(end, 10, &id) < 0)
            continue;

        /* 分配alias_prop结构 */
        ap = dt_alloc(sizeof(*ap) + len + 1, 4);
        if (!ap)
            continue;
        memset(ap, 0, sizeof(*ap) + len + 1);
        ap->alias = start;
        /* 将该设备的alias指向对应的device_node,并且链入aliases_lookup链表中 */
        of_alias_add(ap, np, id, start, len);
    }
}

总的归纳为

  1. kernel入口处获取到uboot传过来的.dtb镜像的基地址
  2. 通过early_init_dt_scan()函数来获取kernel初始化时需要的bootargs和cmd_line等系统引导参数。
  3. 调用unflatten_device_tree函数来解析dtb文件,构建一个由device_node结构连接而成的单向链表,并使用全局变量of_allnodes保存这个链表的头指针。
  4. 内核调用OF的API接口,获取of_allnodes链表信息来初始化内核其他子系统、设备等。

常用OF API

在Linux的BSP和驱动代码中,还经常会使用到Linux中一组Device Tree的API,这些API通常被冠以of_前缀,它们的实现代码位于内核的drivers/of目录。这些常用的API包括:

int of_device_is_compatible(const struct device_node device,const char compat);

​ 判断设备结点的compatible 属性是否包含compat指定的字符串。当一个驱动支持2个或多个设备的时候,这些不同.dts文件中设备的compatible 属性都会进入驱动 OF匹配表。因此驱动可以透过Bootloader传递给内核的Device Tree中的真正结点的compatible 属性以确定究竟是哪一种设备,从而根据不同的设备类型进行不同的处理。如drivers/pinctrl/pinctrl-sirf.c即兼容于"sirf,prima2-pinctrl",又兼容于"sirf,prima2-pinctrl",在驱动中就有相应分支处理:

    1682 if (of_device_is_compatible(np, "sirf,marco-pinctrl"))
    1683      is_marco = 1;

struct device_node of_find_compatible_node(struct device_node from, const char type, const char compatible);

​ 根据compatible属性,获得设备结点。遍历Device Tree中所有的设备结点,看看哪个结点的类型、compatible属性与本函数的输入参数匹配,大多数情况下,from、type为NULL。

int of_property_read_u8_array(const struct device_node np, const char propname, u8 *out_values, size_t sz);

int of_property_read_u16_array(const struct device_node np, const char propname, u16 *out_values, size_t sz);

int of_property_read_u32_array(const struct device_node np, const char propname, u32 *out_values, size_t sz);

int of_property_read_u64(const struct device_node np, const char propname, u64 *out_value);

​ 读取设备结点np的属性名为propname,类型为8、16、32、64位整型数组的属性。对于32位处理器来讲,最常用的是of_property_read_u32_array()。如在arch/arm/mm/cache-l2x0.c中,透过如下语句读取L2 cache的"arm,data-latency"属性:

    534         of_property_read_u32_array(np, "arm,data-latency",
    535                                    data, ARRAY_SIZE(data));

​ 在arch/arm/boot/dts/vexpress-v2p-ca9.dts中,含有"arm,data-latency"属性的L2 cache结点如下:

    137         L2: cache-controller@1e00a000 {
    138                 compatible = "arm,pl310-cache";
    139                 reg = <0x1e00a000 0x1000>;
    140                 interrupts = <0 43 4>;
    141                 cache-level = <2>;
    142                 arm,data-latency = <1 1 1>;
    143                 arm,tag-latency = <1 1 1>;
    144         }

​ 有些情况下,整形属性的长度可能为1,于是内核为了方便调用者,又在上述API的基础上封装出了更加简单的读单一整形属性的API,它们为int of_property_read_u8()、of_property_read_u16()等,实现于include/linux/of.h:

    513 static inline int of_property_read_u8(const struct device_node *np,
    514                                        const char *propname,
    515                                        u8 *out_value)
    516 {
    517         return of_property_read_u8_array(np, propname, out_value, 1);
    518 }
    519
    520 static inline int of_property_read_u16(const struct device_node *np,
    521                                        const char *propname,
    522                                        u16 *out_value)
    523 {
    524         return of_property_read_u16_array(np, propname, out_value, 1);
    525 }
    526
    527 static inline int of_property_read_u32(const struct device_node *np,
    528                                        const char *propname,
    529                                        u32 *out_value)
    530 {
    531         return of_property_read_u32_array(np, propname, out_value, 1);
    532 }

int of_property_read_string(struct device_node np, const char propname, const char out_string);**

int of_property_read_string_index(struct device_node np, const char propname, int index, const char output);**

​ 前者读取字符串属性,后者读取字符串数组属性中的第index个字符串。如drivers/clk/clk.c中的of_clk_get_parent_name()透过of_property_read_string_index()遍历clkspec结点的所有"clock-output-names"字符串数组属性。

    1759 const char *of_clk_get_parent_name(struct device_node *np, int index)
    1760 {
    1761         struct of_phandle_args clkspec;
    1762         const char *clk_name;
    1763         int rc;
    1764
    1765         if (index < 0)
    1766                 return NULL;
    1767
    1768         rc = of_parse_phandle_with_args(np, "clocks", "#clock-cells", index,
    1769                                         &clkspec);
    1770         if (rc)
    1771                 return NULL;
    1772
    1773         if (of_property_read_string_index(clkspec.np, "clock-output-names",
    1774                                   clkspec.args_count ? clkspec.args[0] : 0,
    1775                                           &clk_name) < 0)
    1776                 clk_name = clkspec.np->name;
    1777
    1778         of_node_put(clkspec.np);
    1779         return clk_name;
    1780 }
    1781 EXPORT_SYMBOL_GPL(of_clk_get_parent_name);

static inline bool of_property_read_bool(const struct device_node np, const char propname);

​ 如果设备结点np含有propname属性,则返回true,否则返回false。一般用于检查空属性是否存在。

void __iomem of_iomap(struct device_node node, int index);

​ 通过设备结点直接进行设备内存区间的 ioremap(),index是内存段的索引。若设备结点的reg属性有多段,可通过index标示要ioremap的是哪一段,只有1段的情况,index为0。采用Device Tree后,大量的设备驱动通过of_iomap()进行映射,而不再通过传统的ioremap。

unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

​ 透过Device Tree或者设备的中断号,实际上是从.dts中的interrupts属性解析出中断号。若设备使用了多个中断,index指定中断的索引号。

还有一些OF API,这里不一一列举,具体可参考include/linux/of.h头文件。

总结

ARM社区一贯充斥的大量垃圾代码导致Linus盛怒,因此社区在2011年到2012年进行了大量的工作。ARM Linux开始围绕Device Tree展开,Device Tree有自己的独立的语法,它的源文件为.dts,编译后得到.dtb,Bootloader在引导Linux内核的时候会将.dtb地址告知内核。之后内核会展开Device Tree并创建和注册相关的设备,因此arch/arm/mach-xxx和arch/arm/plat-xxx中大量的用于注册platform、I2C、SPI板级信息的代码被删除,而驱动也以新的方式和.dts中定义的设备结点进行匹配。

查看原文

赞 0 收藏 0 评论 0

戈壁老王 发布了文章 · 2020-03-26

FrameBuffer 架构

FrameBuffer 是 Linux 系统中的一种显示驱动接口。FrameBuffer 将显示硬件进行抽象,对用户表现为一块显示内存,用户空间进程可以直接操作这块内存空间完成写屏操作。FrameBuffer 在设备上表现为一个字符设备,设备节点为 /dev/fb*。用户对设备节点进程 open、mmap、ioctl、read、write 等操作,就可以控制最终的显示输出。

fb.png

FrameBuffer 使用的简单流程

FrameBuffer 在结构上是一个相对简单的驱动,操作流程也比较简单,一般的流程为:

  1. 使用 open() 打开 fb 设备节点。
  2. 通过 ioctl() 设置和获取显示区域的显示属性(FBIOPUT_VSCREENINFO / FBIOGET_VSCREENINFO)。
  3. 通过 ioctl() 获取硬件上显示缓存区的参数(FBIOGET_FSCREENINFO)。
  4. 使用 mmap() 结合上面获取的信息映射显示缓冲区。
  5. 在映射的缓冲区上写入图像数据,就可以获得对应的显示输出。
  6. 完成显示后,调用 close() 关闭 fb 设备。

FrameBuffer 中的数据结构

FrameBuffer 驱动的实现与硬件关系非常大,很难有一个统一的开发流程,所以实现细节也不介绍了。在软件实现上,很多都是围绕驱动中的几个数据结构来实现的。

fb_struct.png

  • fb_info:记录了 FrameBuffer 驱动中全部重要信息,包括驱动状态、设备属性、操作方法等等,并且关联到其他结构体。一个 FrameBuffer 对应一个 fb_info 。
  • fb_ops:FrameBuffer 中的操作函数。
  • fb_cmap:记录 FrameBuffer 驱动中的调色板信息,通过 FBIOGETCMAP 和 FBIOPUTCMAP 进行操作。我在实际使用中并没有用到这个结构体,我对它的理解是用户与驱动对颜色的一种约定,通过这个调色板,用户和驱动可以统一某些颜色。
  • fb_var_screeninfo:存储了用户可以修改的显示属性,例如屏幕分辨率、像素比特数、透明度、像素时钟时序等。通过 FBIOPUT_VSCREENINFO 和 FBIOGET_VSCREENINFO 进行操作。
  • fb_fix_screeninfo:存储了硬件固有的显示属性,如缓冲区的物理地址、缓冲区的长度、显示色彩模式、内存映射的开始位置等。这个结构体在驱动程序初始化时就已经确定,不能由用户修改。通过FBIOGET_FSCREENINFO 进行操作。

其他

FrameBuffer 驱动结构上比较简单,但这并不意味着开发会很简单,开发复杂度与硬件复杂度有很大关系。如果仅仅是一个显示控制,那么实现对应的驱动接口可能就差不多了。但是如果显示单元中包含显示后处理,画质调节等其他硬件单元时,显示驱动就可能变得比较复杂,并且调试难度也增加很多。

FrameBuffer 这种简单的结构也带来了很大的灵活性,许多复杂的处理可以交给上层应用或更底层的硬件来做。例如多个图层渲染,可以由应用使用软件或 GPU 先进行合成,再交给 FrameBuffer 显示。如果硬件支持多个显示层,也可以将每个图层送给 FrameBuffer,然后由硬件进行合成。

另一个常见的问题就是显示流畅性。当使用单缓冲时,很难确保显示画面没有撕裂。单缓冲的流畅显示需要保证软件送显与硬件显示保持相同的帧率,并且显示缓冲的准备时间要尽量短,必须小于帧周期。使用双缓冲时,要求就小很多,更容易保证显示流畅。双缓冲使用乒乓结构,当一个缓冲用于硬件显示时,另一个缓冲用来软件写入。双缓冲可以在应用层或驱动层实现,驱动实现时通过 FBIOPAN_DISPLAY 来交换缓冲。为了确保流畅性,现在有使用三缓冲或更多缓冲来实现,缓冲越多越有利于流畅性,但消耗的内存会更多。设计架构时需要根据实际情况来选择缓冲数。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 29 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-09-26
个人主页被 1.1k 人浏览