android6.0系统Healthd深入分析[转载]

概述 Healthd是android4.4之后提出来的一种中介模型,该模型向下监听来自底层的电池事件,向上传递电池数据信息给Framework层的BatteryService用以计算电池电量相关状态信息,BatteryServcie通过传递来的数据来计算电池电量显示,剩余电量,电量级别等信息,如果收到过温报警或者严重低电报警等信息,系统会直接关机,保护硬件。

主模块处理流程 Healthd模块代码是在system/core/healthd/,其模块入口在healthd的main函数,函数代码如下:

1  int main(int argc, char **argv) {
2  
3  int ch;
4  
5  int ret;
6  
7  klog_set_level(KLOG_LEVEL);
8  
9  healthd_mode_ops = &android_ops;
10 
11 
12 
13 if (!strcmp(basename(argv[0]), "charger")) {
14 
15      healthd_mode_ops = &charger_ops;
16 
17 } else {
18 
19      while ((ch = getopt(argc, argv, "cr")) != -1) {
20 
21          switch (ch) {
22 
23          case 'c':
24 
25              healthd_mode_ops = &charger_ops;
26 
27              break;
28 
29          case 'r':
30 
31              healthd_mode_ops = &recovery_ops;
32 
33              break;
34 
35          case '?':
36 
37          default:
38 
39              KLOG_ERROR(LOG_TAG, "Unrecognized healthd option: %c\n",
40 
41                      optopt);
42 
43              exit(1);
44 
45          }
46 
47      }
48 
49  }
50 
51 ret = healthd_init();
52 
53  if (ret) {
54 
55      KLOG_ERROR("Initialization failed, exiting\n");
56 
57      exit(2);
58 
59  }
60 
61 
62 
63  healthd_mainloop();
64 
65  KLOG_ERROR("Main loop terminated, exiting\n");
66 
67  return 3;
68 
69 }

可以看出Main函数并不长,但是其作用确实巨大的,main函数起着一个统筹兼顾的作用,其他各个模块函数去做一些具体相应的工作,最后汇总到main函数中被调用。 代码中开始便是解析参数,healthd_mode_ops是一个关于充电状态结构体变量,结构体变量里的参数是函数指针,在初始化时指向各个不同的操作函数,当开机充电时变量赋值为&android_ops,关机充电时候变量赋值为&charger_ops。 在ret = healthd_init();中进行一些初始化工作。

1  static int healthd_init() {
2  
3  epollfd = epoll_create(MAX_EPOLL_EVENTS);
4  
5      if (epollfd == -1) {
6  
7         KLOG_ERROR(LOG_TAG,
8  
9                     "epoll_create failed; errno=%d\n",
10 
11                    errno);
12 
13        return -1;
14 
15    }
16 
17  
18 
19     healthd_board_init(&healthd_config);
20 
21     healthd_mode_ops->init(&healthd_config);
22 
23     wakealarm_init();
24 
25     uevent_init();
26 
27     gBatteryMonitor = new BatteryMonitor();
28 
29     gBatteryMonitor->init(&healthd_config);
30 
31     return 0;
32 
33 }

创建一个epoll的变量将其赋值给epollfd,在healthd_board_init中未作任何事便返回了。 healthd_mode_ops->init调用有两种情况:关机情况下调用charger_ops的init函数;开机情况下调用android_ops的init函数,这里就开机情况来分析。android_ops的init函数指针指向healthd_mode_android_init函数 代码如下:

1  void healthd_mode_android_init(struct healthd_config* /*config*/) {
2      ProcessState::self()->setThreadPoolMaxThreadCount(0);//线程池里最大线程数
3      IPCThreadState::self()->disableBackgroundScheduling(true);//禁用后台调度
4      IPCThreadState::self()->setupPolling(&gBinderFd);//
5  
6      if (gBinderFd >= 0) {
7          if (healthd_register_event(gBinderFd, binder_event))
8              KLOG_ERROR(LOG_TAG,
9                         "Register for binder events failed\n");
10     }
11 
12     gBatteryPropertiesRegistrar = new BatteryPropertiesRegistrar();
13     gBatteryPropertiesRegistrar->publish();
14 }

前面三条语句做初始化工作,设置线程池最大线程数,禁用后台调度,以及将gBinderfd加入到epoll中。healthd_register_event将binder_event事件注册到gBinderfd文件节点用以监听Binder事件。gBatteryPropertiesRegistrar->publish将"batteryproperties"这个Service注册到ServiceManager中。

再来看看wakealarm_init函数:

1  static void wakealarm_init(void) {
2      wakealarm_fd = timerfd_create(CLOCK_BOOTTIME_ALARM, TFD_NONBLOCK);
3      if (wakealarm_fd == -1) {
4          KLOG_ERROR(LOG_TAG, "wakealarm_init: timerfd_create failed\n");
5          return;
6      }
7  
8      if (healthd_register_event(wakealarm_fd, wakealarm_event))
9          KLOG_ERROR(LOG_TAG,
10                    "Registration of wakealarm event failed\n");
11 
12     wakealarm_set_interval(healthd_config.periodic_chores_interval_fast);
13 }

首先创建一个wakealarm_fd的定时器与之对应的文件描述符,healthd_register_event将wakealarm事件注册到wakealarm_fd文件节点用以监听wakealarm事件,wakealarm_set_interval设置alarm唤醒的间隔

再看看uevent_init函数:

1  static void uevent_init(void) {
2      uevent_fd = uevent_open_socket(64*1024, true);
3  
4      if (uevent_fd < 0) {
5          KLOG_ERROR(LOG_TAG, "uevent_init: uevent_open_socket failed\n");
6          return;
7      }
8  
9      fcntl(uevent_fd, F_SETFL, O_NONBLOCK);
10     if (healthd_register_event(uevent_fd, uevent_event))
11         KLOG_ERROR(LOG_TAG,
12                    "register for uevent events failed\n");
13 }

创建并打开一个64k的socket文件描述符uevent_fd,设置文件状态标志为非阻塞模,将uevent事件注册到uevent_fd文件节点用以监听uevent事件。

我们可以看到android利用epoll监听了三个文件节点的改变事件,分别是:通过gBinderfd监听线程Binder通信事件;通过wakealarm_fd监听wakealarm事件;通过uevent_fd监听wakealarm事件。至于如何监听后面做详细分析

在healthd_init中最后创建BatteryMonitor的对象,并将其初始化。BatteryMonitor主要接受healthd传来的数据,做电池状态的计算并更新。

我们可以看到在BatterMonitor中的init函数中有以下语句:

1  DIR* dir = opendir(POWER_SUPPLY_SYSFS_PATH);
2  
3  struct dirent* entry;
4  
5  ......
6  
7      while ((entry = readdir(dir))) {
8  
9          const char* name = entry->d_name;
10 ......
11 
12 }

POWER_SUPPLY_SYSFS_PATH定义为"/sys/class/power_supply",在init函数中打开系统该文件夹,然后一一读取该文件夹下的文件内容,在while循环中判断该文件夹下各个文件节点的内容,并将其初始化给相关的参数.

至此,healthd_init函数就分析完了,其主要工作就是:创建了三个文件节点用来监听相应的三种事件改变;创建BatteryMonitor对象,并通过读取/sys/class/power_supply将其初始化。

Healthd_init走完之后,接着就是调用healthd_mainloop函数,该函数维持了一个死循环,代码如下:

1  static void healthd_mainloop(void) {
2  
3  while (1) {
4  
5          struct epoll_event events[eventct];
6  
7          int nevents;
8  
9          int timeout = awake_poll_interval;
10 
11         int mode_timeout;
12 
13         mode_timeout = healthd_mode_ops->preparetowait();
14 
15         if (timeout < 0 || (mode_timeout > 0 && mode_timeout < timeout))
16 
17             timeout = mode_timeout;
18 
19         nevents = epoll_wait(epollfd, events, eventct, timeout);
20 
21         if (nevents == -1) {
22 
23             if (errno == EINTR)
24 
25                 continue;
26 
27             KLOG_ERROR(LOG_TAG, "healthd_mainloop: epoll_wait failed\n");
28 
29             break;
30 
31         }
32 
33         for (int n = 0; n < nevents; ++n) {
34 
35             if (events[n].data.ptr)
36 
37                 (*(void (*)(int))events[n].data.ptr)(events[n].events);
38 
39         }
40 
41         if (!nevents)
42 
43             periodic_chores();
44 
45         healthd_mode_ops->heartbeat();
46 
47     }
48 
49     return;
50 
51 }

Healthd_mainloop中维持了一个死循环,死循环中变量nevents 表示从epollfd中轮循中监听得到的事件数目,这里介绍一下轮询机制中重要函数epoll_waite(). epoll_wait运行的道理是:等侍注册在epfd上的socket fd的事务的产生,若是产生则将产生的sokct fd和事务类型放入到events数组中。且timeout如果为-1则为阻塞式,timeowout为0则表示非阻塞式。可以看到代码中timeout为-1,故为阻塞式轮询,当epollfd上有事件发生,则会走到下面的处理逻辑。事件处理主要在for循环中:

在periodic_chores()中调用到healthd_battery_update()更新电池状态。

1  void healthd_battery_update(void) {
2  
3     int new_wake_interval = gBatteryMonitor->update() ?
4  
5         healthd_config.periodic_chores_interval_fast :
6  
7             healthd_config.periodic_chores_interval_slow;
8  
9   
10 
11     if (new_wake_interval != wakealarm_wake_interval)
12 
13             wakealarm_set_interval(new_wake_interval);
14 
15     if (healthd_config.periodic_chores_interval_fast == -1)
16 
17         awake_poll_interval = -1;
18 
19     Else
20 
21         awake_poll_interval = new_wake_interval == healthd_config.periodic_chores_interval_fast ?
22 
23                 -1 : healthd_config.periodic_chores_interval_fast * 1000;
24 
25 }

可以看出该函数并不长,new_wake_interval表示新的wakealarm唤醒间隔,通过调用BatteryMonitor的update函数(后面详细分析如何更新),其返回值为是否处于充电状态,当处于充电状态,则唤醒间隔为healthd_config.periodic_chores_interval_fast(短间隔),当不再充电状态时唤醒间隔为healthd_config.periodic_chores_interval_slow(长间隔) 当新的间隔变量new_wake_interval与旧的变量wakealarm_wake_interval不一样,则将新的唤醒间隔设置成wakealarm的唤醒间隔; awake_poll_internal作为下一次epoll_waite的timeout参数,在这里将其更新,在充电状态下awake_poll_internal为-1,没有充电的状态下awake_poll_internal为60000ms

healthd主流程都是在main函数中处理,至此main已经分析完成,其简要流程图如下

Healthd处理逻辑 初始化处理 前面将healthd模块中main函数分析完了,其主要工作流程有个大概的了解,但是其详细处理逻辑并未做分析,在此之后,对Healthd的初始化,事件处理,状态更新将做一个详细的分析。 前面已经说过在healthd_init中创建了三个文件节点gBinderfd,uevent_fd,wakealarm_fd,并用以注册监听三种事件,注册监听都是通过healthd_register_event函数实现的。

healthd_register_event(gBinderFd, binder_event);
healthd_register_event(wakealarm_fd, wakealarm_event);
healthd_register_event(uevent_fd, uevent_event); 

其healthd_register_event实现代码如下:

1  int healthd_register_event(int fd, void (*handler)(uint32_t)) {
2  
3  struct epoll_event ev;
4  
5      ev.events = EPOLLIN | EPOLLWAKEUP;
6  
7      ev.data.ptr = (void *)handler;
8  
9      if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
10 
11         KLOG_ERROR(LOG_TAG,
12 
13                    "epoll_ctl failed; errno=%d\n", errno);
14 
15         return -1;
16 
17     }
18 
19     eventct++;
20 
21     return 0;
22 
23 }

函数将相应的文件节点事件赋值为函数的第二个形参,也就是说相应的gBinderfd的事件处理函数为binder_event函数,同理wakealarm_fd,ueven_fd的事件事件处理分别为wakealarm_event,uevent_event函数。然后将其三个文件节点加入到epollfd中。

事件获取与处理 Healthd中维持了一个阻塞式的死循环healthd_mainloop,在该函数中提供阻塞式的监听已发送的事件函数epoll_wait(),healthd_mainloop中有如下代码

1  nevents = epoll_wait(epollfd, events, eventct, timeout);
2  
3   
4  
5  for (int n = 0; n < nevents; ++n) {
6  
7  if (events[n].data.ptr)
8  
9          (*(void (*)(int))events[n].data.ptr)(events[n].events);
10 
11 }

当epoll_waite接受到gBinderfd,wakealarm_fd,uevent_fd其中的事件,便会将监听到的事件加入到event数组中。在for循环中做处理,for循环中代码看起来非常难懂,其实if判断的便是event有没有相应的处理函数,在前面注册事件时候已经提到,三种句柄上的事件都有对应的处理函数,也就是当收到gBinderfd上的事件,便用binder_event函数处理,当收到uevent_fd上的事件便用uevent_event处理,当收到wakealarm_fd上的事件便用wakealarm_event处理。 这里以较为重要的uevent_event事件处理为例:

1  #define UEVENT_MSG_LEN 2048
2  
3  static void uevent_event(uint32_t /*epevents*/) {
4  
5  char msg[UEVENT_MSG_LEN+2];
6  
7      char *cp;
8  
9      int n;
10 
11  
12 
13     n = uevent_kernel_multicast_recv(uevent_fd, msg, UEVENT_MSG_LEN);
14 
15     if (n <= 0)
16 
17         return;
18 
19     if (n >= UEVENT_MSG_LEN)   /* overflow -- discard */
20 
21         return;
22 
23  
24 
25     msg[n] = '\0';
26 
27     msg[n+1] = '\0';
28 
29     cp = msg;
30 
31  
32 
33     while (*cp) {
34 
35         if (!strcmp(cp, "SUBSYSTEM=" POWER_SUPPLY_SUBSYSTEM)) {
36 
37             healthd_battery_update();
38 
39             break;
40 
41         }
42 
43  
44 
45         /* advance to after the next \0 */
46 
47         while (*cp++)
48 
49             ;
50 
51     }
52 
53 }

处理函数首先从uevent_fd 获取事件数目,然后循环判断是否是来自与power_supply目录下的事件,如果是,则调用到healthd_battery_update中去更新电池状态。

更新电池状态 当收到事件,做一些判断工作便需要更新电池状态,其更新函数为healthd.cpp下的healthd_battery_update函数,但是主要更新并不在heathd中完成的,而是在BatteryMonitor中的update函数,其代码较多,分段分析:

1   props.chargerAcOnline = false;
2   
3   props.chargerUsbOnline = false;
4   
5   props.chargerWirelessOnline = false;
6   
7   props.batteryStatus = BATTERY_STATUS_UNKNOWN;
8   
9   props.batteryHealth = BATTERY_HEALTH_UNKNOWN;
10  
11   
12  
13  if (!mHealthdConfig->batteryPresentPath.isEmpty())
14  
15  props.batteryPresent = getBooleanField(mHealthdConfig->batteryPresentPath);
16  
17  else
18  
19      props.batteryPresent = mBatteryDevicePresent;
20  
21   
22  
23  props.batteryLevel = mBatteryFixedCapacity ?
24  
25      mBatteryFixedCapacity :
26  
27      getIntField(mHealthdConfig->batteryCapacityPath);
28  
29  props.batteryVoltage = getIntField(mHealthdConfig->batteryVoltagePath) / 1000;
30  
31   
32  
33  props.batteryTemperature = mBatteryFixedTemperature ?
34  
35      mBatteryFixedTemperature :
36  
37      getIntField(mHealthdConfig->batteryTemperaturePath);
38  
39  const int SIZE = 128;
40  
41  char buf[SIZE];
42  
43  String8 btech;
44  
45   
46  
47  if (readFromFile(mHealthdConfig->batteryStatusPath, buf, SIZE) > 0)
48  
49      props.batteryStatus = getBatteryStatus(buf);
50  
51   
52  
53  if (readFromFile(mHealthdConfig->batteryHealthPath, buf, SIZE) > 0)
54  
55      props.batteryHealth = getBatteryHealth(buf);
56  
57   
58  
59  if (readFromFile(mHealthdConfig->batteryTechnologyPath, buf, SIZE) > 0)
60  
61      props.batteryTechnology = String8(buf);

在init函数中将healthd_config 对象传入,并且将里面的成员的一些地址信息去初始化保存起来。主要是保存一些地址信息,以及充电方式。在BatteryMonitor初始化中,heathd_config传入init函数中,赋值为mHealthdConfig,上面一段主要是读取/sys/class/power_supply下的文件节点信息初更新电池数据属性值,

1  path.appendFormat("%s/%s/online", POWER_SUPPLY_SYSFS_PATH,
2  
3                    mChargerNames[i].string());
4  
5   
6  
7  if (readFromFile(path, buf, SIZE) > 0) {
8  
9  if (buf[0] != '0') {
10 
11         path.clear();
12 
13         path.appendFormat("%s/%s/type", POWER_SUPPLY_SYSFS_PATH,
14 
15                           mChargerNames[i].string());
16 
17         switch(readPowerSupplyType(path)) {
18 
19         case ANDROID_POWER_SUPPLY_TYPE_AC:
20 
21             props.chargerAcOnline = true;
22 
23             break;
24 
25         case ANDROID_POWER_SUPPLY_TYPE_USB:
26 
27             props.chargerUsbOnline = true;
28 
29             break;
30 
31         case ANDROID_POWER_SUPPLY_TYPE_WIRELESS:
32 
33             props.chargerWirelessOnline = true;
34 
35             break;
36 
37         default:
38 
39             KLOG_WARNING(LOG_TAG, "%s: Unknown power supply type\n",
40 
41                          mChargerNames[i].string());
42 
43         }
44 
45     }
46 
47 }

这一段代码其实实现的功能很简单,通过读取/sys/class/power_supply/文件夹下不同类型的充电设备的online节点,如果不为空则表示处于充电状态,判断充电类型为AC充电器,usb充电还是无线充电并将其相应的属性置true。

1  char dmesgline[256];
2  
3   
4  
5  if (props.batteryPresent) {
6  
7  snprintf(dmesgline, sizeof(dmesgline),
8  
9           "battery l=%d v=%d t=%s%d.%d h=%d st=%d",
10 
11          props.batteryLevel, props.batteryVoltage,
12 
13          props.batteryTemperature < 0 ? "-" : "",
14 
15          abs(props.batteryTemperature / 10),
16 
17          abs(props.batteryTemperature % 10), props.batteryHealth,
18 
19          props.batteryStatus);
20 
21  
22 
23     if (!mHealthdConfig->batteryCurrentNowPath.isEmpty()) {
24 
25         int c = getIntField(mHealthdConfig->batteryCurrentNowPath);
26 
27         char b[20];
28 
29  
30 
31         snprintf(b, sizeof(b), " c=%d", c / 1000);
32 
33         strlcat(dmesgline, b, sizeof(dmesgline));
34 
35     }

将电池当前的电量级别,电压,温度,健康状况,电池状态以及充放电倍率存入dmesgline变量中,在后面会将电池充电类型,电池使用时间都以字符串存入dmesgline变量中,然后:

1 KLOG_WARNING(LOG_TAG, "%s\n", dmesgline);//向log记录电池当前各种状态信息
2 }
3  
4 healthd_mode_ops->battery_update(&props);//更新电池
5 return props.chargerAcOnline | props.chargerUsbOnline |
6         props.chargerWirelessOnline;//返回是否在充电状态

整个update函数做完更新数据,记录数据到log之后,然后调用到BatteryPropertiesRegistrar的update函数继续更新电池状态,最后返回值为是否处于充电状态。 BatteryPropertiesRegistrar的update函数未作任何操作调用Healthd_mode_android.cpp中的healthd_mode_android_battery_update函数,我们可以看看该函数

1  void healthd_mode_android_battery_update(
2  
3  struct android::BatteryProperties *props) {
4  
5      if (gBatteryPropertiesRegistrar != NULL)
6  
7          gBatteryPropertiesRegistrar->notifyListeners(*props);
8  
9   
10 
11     return;
12 
13 }

这里这里直接调用到BatteryPropertiesRegistrar的notifyListeners去通知props改变了,props是什么呢?props是定义的一个BatteryProperties属性集,里面的成员变量包含了所有的电池状态信息,在update开始便通过读取各个文件节点的实时数据更新电池属性props,更新完成后通过BatteryPropertiesRegistrar通知其属性监听者去更新状态,但是谁是监听呢? 我们可以看到framework层中的BatteryService.java的onStart函数中有如下代码:

1  public void onStart() {
2  
3  IBinder b = ServiceManager.getService("batteryproperties");
4  
5      final IBatteryPropertiesRegistrar batteryPropertiesRegistrar =
6  
7              IBatteryPropertiesRegistrar.Stub.asInterface(b);
8  
9      try {
10 
11         batteryPropertiesRegistrar.registerListener(new BatteryListener());
12 
13     } catch (RemoteException e) {
14 
15         // Should never happen.
16 
17     }

我们在初始化的时候已经提到过,当healthd初始化时候会创建BatteryPropertiesRegistrar的对象并将其publish注册到系统服务中,注册服务的语句如下:

defaultServiceManager()->addService(String16("batteryproperties"), this);

所以BatteryService在这里获取该服务,并以此注册其监听器为BatteryListener(),该监听器监听到BatteryProperties改变便会调用到BatteryService的update函数,去做电池电量相关计算以及显示。 至此更新操作基本分析完成,其简要流程如下图所示

总结 Healthd是framework层传递来自底层电池事件信息并调用相关模块更新电池状态的一个中间层,其向下监听来自底层PMU驱动上报的uevent电池事件,向上调用BatteryService去计算电池,电量,使用等相关信息,它通过一个阻塞式的死循环不断监听底层三个文件节点上的事件信息,当监听到事件便调用到BatteryMonitor执行更新操作,通过BatteryService.java中注册监听电池属性改变的函数,当电池属性信息发生改变,即回调到BatteryService中做更新操作,更新完成一次电池事件的上报到更新整个流程就完成;总之Healthd是连接Battery模块framework中java层与HAL层交互的主要通道。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大数据和云计算技术

hdfs auditlog(审计日志)

hdfs审计日志(Auditlog)记录了用户针对hdfs的所有操作,详细信息包括操作成功与否、用户名称、客户机地址、操作命令、操作的目录等。对于用户的每一个...

35330
来自专栏酷玩时刻

PC 微信扫码登陆

网站应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统。进一步了解OAuth2.0-----理解OAuth2.0 官方介绍资料

2.1K40
来自专栏跟着阿笨一起玩NET

url传递中文的解决方案

本文转载:http://www.cnblogs.com/ghd258/archive/2005/10/23/260241.html

35320
来自专栏刘望舒

Retrofit2与服务端实例讲解

网络上对 Retrofit2 的各种介绍文章已经很多了,不过往往只是对其用法进行介绍,而缺少相应的实践,这一方面是因为网络上的免费API接口返回的数据格式和访问...

13230
来自专栏带你撸出一手好代码

Java并发编程之守护线程(短文)

Java多线程中有一种线程叫做守护线程。我们可以通过一个比喻来理解守护线程。通常,一般工厂都会聘请一批保安,保安的职责是维护工厂的治安,保护工厂的财产,保安的职...

34650
来自专栏精讲JAVA

java面试线程必备知识点,怼死面试官,从我做起

内存屏障:限制命令操作顺序,有LoadLoad、LoadStore、LoadStore、StroreStreo四种屏障

16140
来自专栏Flutter知识集

Flutter与Native通信 - PlatformChannel源码分析

Flutter是一个跨平台的方案,在UI、触控及基本的网络请求上已经基本做到平台无关,但是在某些平台特性的功能上,还是必须要对不同的平台做处理。这就涉及到与Na...

1.5K00
来自专栏FreeBuf

秒爆十万字典:奇葩技巧快速枚举“一句话后门”密码

对于一句话大家都不陌生,有时会需要爆破。爆破的速度和目标的响应速度就有很大的关系了。那如果我们爆破的速度可以提升至少1000倍呢? 首先如下图↓ ? 变量=ec...

29070
来自专栏程序员宝库

正则表达式实例

来源:寒青 链接:https://segmentfault.com/a/1190000012806098 1. 校验基本日期格式 var reg1 = /^\...

422110
来自专栏Seebug漏洞平台

从补丁到漏洞分析——记一次joomla漏洞应急

作者:LoRexxar'@知道创宇404实验室 2018年1月30日,joomla更新了3.8.4版本,这次更新修复了4个安全漏洞,以及上百个bug修复。 ht...

364120

扫码关注云+社区

领取腾讯云代金券