1. c++入口函数lk_main
zircon微内核的代码是用c++写的,c++和c的基础语法差不多,c++新加入了一些面向对象的东西,在内核中没有界面化那些c++的api的情况下,区别基本就是c语言结构体的回调函数是一个.c++则是::,不用另外学习c++语法就可以看懂,但是为什么zircon使用了面向对象语言c++?这里你必须对面向对象的思想有最基本的认识,首先就是抽象出来共同的属性操作,这样就不用写代码对不同的模块写重复的代码,而且结构更加的清晰,其次对象的安全性也更好,可以参考zcore入门-面向对象的rust微内核里面介绍的面向对象os设计的好处,再次印证那句“编程语言和操作系统的设计是相辅相成的”。
进入正题系统启动整体流程为:kernel-》userboot-》bootfs镜像-》文件系统-》可执行文件-》组件管理器-》拉起进程。
先说kernel的启动,lk_main()入口函数在代码中的位置在kernel/top/main.cc中
//日志打印开关定义dlog_bypass_init_early(); thread_init_early():percpu::initializeboot();//创建一个precpu的对象 thread_t*t = &percpu::get(0).idle_thread; //找到cpu 0对应的空闲线程thread_construct_first(t,bootstrap);
这t就是一个线程,线程在zircon中也是一个对象,相关的对象为job->process->thread
zircon 公开了三个运行代码的主要内核对象:
线程:给定地址空间内的执行线程。
进程:在私有、隔离的地址空间中运行的一组可执行指令。
作业:一组相关的流程和作业。所有作业形成一个单根树。
线程对象是表示分时cpu 执行上下文的构造。thread 对象与特定的process object相关联 ,它为 i/o 和计算所需的其他对象提供内存和句柄。
线程是调度的基本单位,并且调度有优先级,在thread_construct_first()函数中,会设置线程的信息,例如base_priority优先级为highest_priority,set_current_thread设置此线程为当前正在运行的thread
list_add_head(&thread_list,&t->thread_list_node);//把线程加入线程表中,这样调度的时候就可以找到这个线程。
call_constructors();//调用全局构造函数
for (void (*const* a)() = __init_array_start; a != __init_array_end;a++)(*a)();
函数上打了 attribute ((constructor))则为全局构造函数,编译器将其编译到.init 段,而__init_array_start 和__init_array_end 是该段的开始和结尾。
2. 内核分层初始化lk_primary_cpu_init_level
在zircon中对于系统启动进行分层设计,这样结构很明朗好理解,而且新添加内容的时候方便找到对应的地方,在出错调试的时候也更加的方便,真是简单实用又高深的方法,大道至简也就这样吧,在我们的编程中可以借鉴。
lk_primary_cpu_init_level(lk_init_level_earliest,lk_init_level_arch_early - 1); lk_init_level(lk_init_flag_primary_cpu,start_level, stop_level);for (const struct lk_init_struct* ptr =__start_lk_init; ptr != __stop_lk_init; ptr++) { /* keep the lowest one we haven't called yet */ if (ptr->level >= start_level && ptr->level >last_called_level) { found = ptr; continue; } if (ptr->level == last_called_level && ptr != last &&seen_last) { found = ptr; break;}found->hook(found->level);
可见去__start_lk_init结构体数组中按照level找元素,找到就执行hook()回调函数。
在kernel/kernel.ld中定义
provide_hidden(__start_lk_init = .);keep(*(.data.rel.ro.lk_init))provide_hidden(__stop_lk_init = .);
ld文件规定了二进制文件的组织形式,在数据段中存在这个数组,这个数组里面的元素按照.data.rel.ro. lk_init字符串去组织
在kernel/include/lk/init.h中
#define lk_init_hook_flags(_name, _hook,_level, _flags) __aligned(sizeof(void *)) __used__section(.data.rel.ro.lk_init) static const struct lk_init_struct _init_struct_##_name = { .level = _level, .flags = _flags, .hook = _hook, .name = #_name, };#define lk_init_hook(_name, _hook, _level)lk_init_hook_flags(_name, _hook, _level, lk_init_flag_primary_cpu)
lk_init_hook这个宏用于声明这个数据段的结构体,我们添加初始化的模块函数时就可以在模块尾部声明这个lk_init_hook。
lk_init_level_earliest这个level的结构体不存在,只是空跑下算初始化
lk_primary_cpu_init_level(lk_init_level_arch_early,lk_init_level_platform_early - 1);
就会执行
kernel/lib/code_patching/code_patching.cc中apply_startup_code_patches函数
lk_init_hook(code_patching,apply_startup_code_patches,
lk_init_level_arch_early)
这个函数好像没做什么。
3. 使用chatgpt解释看不懂代码
arch_early_init();
x86_mmu_early_init();
mmu就是内存管理单元,主要负责物理内存和虚拟内存的映射,这里是early_init,具体不详细说明了,这里说一个技巧那就是chatgpt,直接把代码复制到chatgpt提问让解析一下
最近我也经常使用chatgpt,而且越难的东西它越擅长,这里说的难就是用的人少,需要大量经验,难入门的东西,一个典型就是汇编语言,只是看汇编就交给chatgpt,自己完全没必要学习汇编,学了又会忘记。
chatgpt有一个缺点就是不保真,需要你是一个行内的人,你提问它回答给你启发,你可以自己大致的辨别有用的信息,并且求证,这不就是导师干的事么,最重要的还是方向啊,现在最牛逼的老师领进门了,那修行还不事半功倍。
platform_early_init(void)
对x86来说kernel/platform/pc/platform.cc中实现
初始化串口,之后串口就可以使用了
platform_save_bootloader_dataif (_zbi_base != null) {zbi_header_t* bd = (zbi_header_t*)x86_phys_to_virt(_zbi_base);process_zbi(bd, (uintptr_t)_zbi_base);}
_zbi_base在kernel/arch/x86/start.s中赋值,multiboot传给start.s的是imag的基地址
// _zbi_base is in bss, so now it's safe to set it.
mov %ebx,phys(_zbi_base)
这个zbi类型的数据,之前介绍过zbi,zbi有很多类型,这里为container
boot_reserve_init();保留kernel所在的区域
pmm: boot reserve add [0x100000, 0x334fff]platform_preserve_ramdisk();保留ramdis所在的区域pmm: boot reserve add [0xb4e000, 0x1598fff]pc_mem_init-》platform_mem_range_init-》mem_arena_init-》pmm_add_arena-》pmmarena* arena = new(boot_alloc_mem(sizeof(pmmarena))) pmmarena();
会分配boot内存0x1599000,0x1599040 这个都是物理内存如果是虚拟内存转换关系为:
paddr_to_physmap ()中:
0xffffff8000000000ul + 物理内存= 虚拟内存
lk_primary_cpu_init_level(lk_init_level_platform_early,lk_init_level_target_early - 1);关于平台早期初始化的钩子函数,主要是一些驱动、中断等
lk_init_hook(platform_dev_init,platform_dev_init, lk_init_level_platform)
这个会初始化一堆的驱动,驱动从zbi中找到zbi_type_kernel_driver类型的,继续执行pdev_init_driver()会执行.data.rel.ro.lk_pdev_init数据段中结构体初始化,
lk_pdev_init驱动通过这个宏就可以注册到.data.rel.ro.lk_pdev_init
然后就可以看到我们的打印了
vm_init_preheap();创建供内核使用的虚存空间vmaspace
上面提到boot的内存起始为0x1599000,分配到了0x1599040,
markpagesinusephys在页表中标记内存页已经使用
heap_init();
zircon 的内核堆由内部的 cmpctmalloc 实现。
cmpct_init
vm_init();虚拟内存初始化
找到内核镜像的各个段,及初始化读写策略,例如
regions[] = {{.name = kernel_code,.base = (vaddr_t)__code_start,.size = roundup((uintptr_t)__code_end - (uintptr_t)__code_start,page_size),.arch_mmu_flags = arch_mmu_flag_perm_read | arch_mmu_flag_perm_execute,},
// 遍历上面的几个段,并设置策略
for (uint i = 0; i base));dprintf(info, vm: reserving kernel region [%# prixptr, %# prixptr ) flags %#x name '%s',region->base,region->base + region->size, region->arch_mmu_flags, region->name);// 在vmm中标记一块虚拟内存,这块虚拟内存抽象为vmregion类,拥有自己的底层mmu相关的配置zx_status_t status = aspace->reservespace(region->name,region->size, region->base);assert(status == zx_ok);// 对某vmregion对应的虚拟内存设置内存保护的相关参数status = protectregion(aspace, region->base, region->arch_mmu_flags);assert(status == zx_ok);}// 标记映射表// reserve the kernel aspace where the physmap isaspace->reservespace(physmap, physmap_size,physmap_base);
·reservespace:在 vmm 中标记一块虚拟内存,这块虚拟内存抽象为vmregion类,拥有自己的底层mmu相关的配置
·protectregion:对某vmregion对应的虚拟内存设置内存保护的相关参数
物理内存一块区域存储的数据,可以映射为内核里面一个vmregion类,这个类里面有其虚拟内存信息,这一过程就是内存映射。
kernel_init();
mp_init();多核初始化
// 多核初始化void mp_init(void) {// 核间中断任务表初始化mp.ipi_task_lock = spin_lock_initial_value;for (uint i = 0; i mutable_handles()创建一个可变引用handles指向其中的handle数组,这个数组里面存了很多信息
enum handleindex : uint32_t {// these describe userboot itself.kprocself, // 记录新进程中指向新进程本身的handle号kvmarrootself, // 记录为新进程创建的vmar的handlekrootjob, //一个process属于一个jobkrootresource,// essential vmo handles.kzbi,kfirstvdso,klastvdso = kfirstvdso + static_cast(vdsovariant::count)- 1,// these get passed along to userland to be recognized by zx_prop_name.// the decompressor is also used by userboot itself.// the remainder are vmo handles that userboot doesn't care about.kuserbootdecompressor,kfirstkernelfile = kuserbootdecompressor,kcrashlog,kcounternames,kcounters,#if enable_entropy_collector_testkentropytestdata,#endifkfirstinstrumentationdata,khandlecount = kfirstinstrumentationdata +instrumentationdata::vmo_count()};
创建process,其包含一个vmar,每个进程都从一个单一的虚拟内存地址区域 (vmar) 开始,进程根vmar 跨越整个用户地址空间。vmar 用于映射虚拟内存对象(vmo),虚拟内存对象将程序所需的代码、数据、匿名和共享内存页面提供到进程的地址空间中。
// it also gets many vmos for vdsos and other things.
const vdso* vdso = vdso::create();
vdso->getvariants(&handles[kfirstvdso]);
vdso(virtualdynamic shared object),zircon vdso 是 zircon 内核访问系统调用的唯一方法(作为系统调用的跳板)。它之所以是虚拟的,是因为它不是从文件系统中的elf文件加载的,而是由内核直接提供的vdso镜像
bootstrap_vmos(handles);embeddedvmo decompress_zbi(lib/hermetic/decompress-zbi.so,decompress_zbi_image,decompress_zbi_data_end);handles[kuserbootdecompressor] =decompress_zbi.vmo_handle().release();
这里开始解压缩zbi格式的ramdisk文件,之前我们知道镜像分为了kernel+ranmdisk,现在kernel初始化差不多了,要开始读这个ramdis里面的内容了,原则上这里面的内容都是用户进程相关的东西。
platform_get_ramdisk(&rsize);
ramdisk_base和ramdisk_size
zx_status_t status = vmobjectpaged::createfromwiredpages(
rbase, rsize, true, &rootfs_vmo);
创建一个vmo对象,就是虚拟内存对象,指向这个ramdisk
get_vmo_handle(rootfs_vmo, false, nullptr,&handles[kzbi]);
handles的kzbi里面存的这个vmo
status = get_vmo_handle(ktl::move(kcounters_vmo), true, nullptr,
&handles[kcounters]);
内核计数器放入handles
// make the channel that will hold the message.kernelhandle user_handle, kernel_handle;status = channeldispatcher::create(&user_handle, &kernel_handle,&rights);assert(status ==zx_ok);
创建一个channel,有两头一个头是user一头是kernel,用于通信
status = kernel_handle.dispatcher()->write(zx_koid_invalid,
ktl::move(msg));
内核侧先写入点数据
process->addhandle(ktl::move(user_handle_owner));
用户进程把这个channel通过handle绑定到自己身上
job->process->handle->channel
status = userboot.map(vmar, &vdso_base,&entry);
映射就是把vmo映射到vmar上面,vmar是process里面的数据。entry就是userboot的入口地址,vdso_base就是进行系统调用的基地址
// map userboot proper.status = rodso::map(vmar_handle.dispatcher(), 0);if (status == zx_ok) {*entry =vmar_handle.dispatcher()->vmar()->base() + userboot_entry;// map the vdso right after it.*vdso_base =vmar_handle.dispatcher()->vmar()->base() + rodso::size();// releasing |vmar_handle| is safe because it has a no-op// on_zero_handles(), otherwise the mapping routines would have// to take ownership of the handle and manage its lifecycle.status = vdso_->map(vmar_handle.release(), rodso::size());}
6. userboot如何在vdso中取得系统调用
当内核将userboot映射到第一个用户进程时,会像正常程序那样,在内存中选择一个随机地址进行加载。而在映射userboot的vdso时,并不采用上述随机的方式,而是将vdso映像直接放在内存中userboot的映像之后。这样一来,vdso代码与userboot的偏移量总是固定的。
在编译阶段中,系统调用的入口点符号表会从vdso elf映像中提取出来,随后写入到链接脚本的符号定义中。利用每个符号在vdso映像中相对固定的偏移地址,可在链接脚本提供的_end符号的固定偏移量处,定义该符号。通过这种方式,userboot代码可以直接调用到放在内存中,其映像本身之后的,每个确切位置上的vdso入口点。
vdso会映射到userboot的vmar中
status = vmobjectpaged::create(pmm_alloc_flag_any, 0u,stack_size, &stack_vmo);status = vmar->map(0, ktl::move(stack_vmo), 0, stack_size,zx_vm_perm_read |zx_vm_perm_write,&stack_mapping);
新建一个堆栈的vmo,然后映射到vmar上面
uintptr_t sp =
compute_initial_stack_pointer(stack_base, stack_size);
计算线程的栈地址
status =
threaddispatcher::move(process), 0, userboot,
&(), &rights);
创建userboot线程,线程是调度的基本单位。
auto arg1 = static_cast(hv); // 传给userboot线程的第一个参数为一个handle的指针的编号(实例在process结构中) // 第二个参数为vdso的基地址status = thread->start(threaddispatcher::entrystate{entry, sp, arg1, vdso_base},参数为:入口地址、堆栈地址、handels、vdso地址
kernel如何启用userboot?
与任何其他进程一样,userboot必须从已经映射到其地址空间的vdso开始,这样它才能进行系统调用。内核将userboot和vdso映射到第一个用户进程,然后在userboot的入口处启动它。
userboot的入口处在哪里?
userbootimage userboot(vdso);
userboot.map(vmar, &vdso_base, &entry);
从vdso中找到userboot
const vdso* vdso = vdso::create();
vdso->getvariants(&handles[kfirstvdso]);
bootstrap_vmos(handles);
vdso获取
kernel/lib/userabi/userboot/build.gn中编译
loadable_module(userboot) {sources = [bootdata.cc,bootfs.cc, loader-service.cc,option.cc,start.cc,userboot-elf.cc,util.cc,]configs += [ $zx/public/gn/config:rodso ]ldflags = [ -wl,-e,_start ]libs = [ vdso_syms_ld ]
_start是入口函数,在
kernel/lib/userabi/userboot/start.cc中定义
extern c [[noreturn]] void_start(zx_handle_t arg) {bootstrap(zx::channel{arg});}
这里我们开始进入userboot的代码了。
7. userboot进程代码
zx_status_t status = channel.read(0, child_message.cmdline,handles.data(),
sizeof(child_message.cmdline),
handles.size(), &cmdline_len, &nhandles);
读出来init的时候发的消息,里面主要是一些参数,解析出来为
options o;
child_message.pargs.environ_num =
parse_options(log.get(), child_message.cmdline, cmdline_len, &o);
把这option都存入o里面
zx::vmo bootfs_vmo{bootdata_get_bootfs(log.get(), vmar_self.get(),handles[krootjob],handles[kuserbootdecompressor],handles[kfirstvdso],handles[kzbi])};
定位bootfs里面第一个程序
bootdata_get_bootfsbootdata_t bootdata;zx_status_t status = zx_vmo_read(bootdata_vmo, &bootdata,off,sizeof(bootdata));
读出bootdate的头,这里bootdata.type是bootdata_bootfs_boot
zx::create(bootdata.extra, 0,&bootfs_vmo);创建一个vmostatus = decompressor(job,engine_vmo, vdso_vmo)(*zx::unowned_vmo{bootdata_vmo},off + sizeof(bootdata),bootdata.length,bootfs_vmo, 0,bootdata.extra);
拿到vdso
handles[kbootfsvmo] =
bootfs_vmo.release(); // bootfs_vmo实际在用户态也只是维护一个zx_handle_t
const char* root_option =
o.value[option_root];拿到root进程为pkg/bootsvc
zx::process proc;zx::vmar vmar;zx::unowned_job root_job{handles[krootjob]}; // 本进程的job也就是子进程的job,传承下去const char* filename = o.value[option_filename];filename 是bin/bootsvc//创建子进程status = zx::create(*root_job, filename,static_cast(strlen(filename)), 0,&proc, &vmar);check(log.get(), status, zx_process_create);load_child_process(log.get(), &o, &bootfs, root_prefix,handles[kfirstvdso],proc.get(), vmar.get(),thread.get(), to_child.get(),&entry,&vdso_base, &stack_size,loader_service_channel.reset_and_get_address());加载bin/bootsvc,elf程序elf_load_bootfs-》bootfs_open-》loadbootfs_open -》bootfs_searchzx_status_t status = zx_vmo_create_child(fs->vmo,zx_vmo_child_copy_on_write,e->data_off, e->data_len, &vmo);status =zx_vmo_replace_as_executable(vmo, zx_handle_invalid, &vmo);elf_load_vmo 这个的vdso从handles[kfirstvdso]传进来的load(log, vdso, vmar, vmo,null, null, null, null, false, false)
会把vdso这个elf格式文件读入进来
sp =
compute_initial_stack_pointer(stack_base, stack_size);
新建堆栈空间
// 同时给子进程发送消息和handle数组。注意,这时子进程还没启动
status = to_child.write(0, &child_message, sizeof(child_message),
handles.data(),handles.size());
status = proc.start(thread, entry, sp,
std::move(child_start_handle), vdso_base);
启动bootsvc
8. bootsvc
在system/core/bootsvc/main.cc中
zx_handle_close(dl_set_loader_service(zx_handle_invalid));关闭跟userboot的通道,这时候
ldsvc.serve(std::move(loader_service_channel));会接收到,然后继续往下执行
zx::vmo bootfs_vmo(zx_take_startup_handle(pa_hnd(pa_vmo_bootfs, 0)));
从handle里面获取bootfs_vmo
status =
bootsvc::create(loop.dispatcher(),&bootfs_svc);
status =
bootfs_svc->addbootfs(std::move(bootfs_vmo));
创建一个bootfs的服务,关联bootfs_vmo
status = bootsvc::retrievebootimage(&image_vmo, &item_map,&factory_item_map);
找回boot信息
loadbootargs(bootfs_svc, &args_vmo,&args_size);把参数信息读入到vmo
const char* config_path = /config/devmgr;fbl::vector buf;zx::vmo config_vmo;uint64_t file_size;zx_status_t status = bootfs->open(config_path, &config_vmo,&file_size);zx::resource root_resource_handle(zx_take_startup_handle(pa_hnd(pa_resource,0)));
找到系统资源
fbl::refptr svcfs_svc =
bootsvc::create(loop.dispatcher());
创建svcfs服务
status =
bootsvc::create(bootfs_svc,loop.dispatcher(), &loader_svc);
创建loader服务
std::thread(launchnextprocess, bootfs_svc, svcfs_svc, loader_svc,std::cref(log)).detach();
启动launchnextprocess
std::thread(launchnextprocess, bootfs_svc, svcfs_svc, loader_svc,std::cref(log)).detach();void launchnextprocess(fbl::refptrbootfs,fbl::refptr svcfs,fbl::refptr loader_svc,const zx::debuglog&log) {const char* bootsvc_next = getenv(bootsvc.next);if(bootsvc_next == nullptr) {bootsvc_next = bin/devcoordinator;}
9. devcoordinator
system/core/devmgr/devcoordinator/main.cc中有main函数
status =
startsvchost(root_job, require_system, &coordinator, std::move(fshost_client));
/boot/bin/svchost启动
devmgr_vfs_init(&coordinator, devmgr_args, needs_svc_mount,std::move(fshost_server));
devmgr_vfs_init -》
fshost_start-》
devmgr_launch -》
devmgr_launch_with_loader
fshost启动
intret =
thrd_create_with_name(&t, pwrbtn_monitor_starter, nullptr,pwrbtn-monitor-starter);
pwrbtn-monitor启动
ret=
thrd_create_with_name(&t, service_starter, &coordinator,service-starter);
const char* args[] = {/boot/bin/miscsvc, nullptr};
devmgr::devmgr_launch(g_handles.svc_job, miscsvc, args,nullptr, -1, handles, types,
countof(handles),nullptr, fs_boot | fs_dev | fs_svc | fs_volume);
miscsvc启动
zx_status_t status =
devmgr::devmgr_launch(g_handles.svc_job,netsvc, args, nullptr, -1, nullptr, nullptr, 0,&proc, fs_all);
netsvc启动
devmgr::devmgr_launch(g_handles.svc_job, virtual-console,args, env.get(), -1, handles, types,
handle_count,nullptr, fs_all);
virtual-console启动
intret =
thrd_create_with_name(&t, fuchsia_starter, coordinator,fuchsia-starter);
coordinator.prepareproxy(coordinator.sys_device(), nullptr);
'devhost:sys'启动
coordinator.prepareproxy(coordinator.test_device(), nullptr);
'devhost:test'启动
coordinator.binddrivers();
for(driver& drv : drivers_) {zx_status_t status = binddriver(&drv);if (status != zx_ok && status != zx_err_unavailable) {log(error, devcoordinator: failed to bind driver '%s': %s,drv.name.data(),zx_status_get_string(status));}}
绑定了devhost:root和devhost:misc
coordinator.set_running(true);
status = loop.run();
设置状态为运行,进入循环,启动四个devhost
10. devhost
zircon内核中,设备驱动程序以elf格式的共享库形式存在,由devhost进程按需动态加载(实现代码参见
zircon/system/core/devmgr/devhost/目录)
核心设备管理进程(devmgr),包含具有跟踪设备与驱动关联的devcoordinator进程,同时管理着驱动程序发现,devhost进程创建和控制,还要维护设备文件系统(devfs),通过devfs机制,用户层的服务和应用实现对设备的操作。
进程devcoordinator将设备看做是一个统一树状结构。树的分支(和子分支)由一定数量的隶属于devhost进程的设备组成。关于如何将整棵设备树划分以分配到多个devhost进程中,取决于系统的策略:基于安全或者稳定性原因的驱动隔离;以及为了性能原因将驱动进行并置。
参考:
https://blog.csdn.net/sinat_20184565/article/details/92002908
dm dump可以查看进程树
devhost[proxy]devhost[sys/platform]devhost[sys/platform/001b]devhost[sys/platform/acpi/acpi-pwrbtn]devhost[sys/platform/acpi/i8042]devhost[00:01.0]devhost[00:1f.2]
当program loader设置了一个新进程后,使该进程能够进行系统调用的唯一方法是:program loader在新进程的第一个线程开始运行之前,将vdso映射到新进程的虚拟地址空间(地址随机)。因此,在启动其他能够进行系统调用的进程的每个进程自己本身都必须能够访问vdso的vmo。
vdso映像在编译时嵌入到内核中。内核将它作为只读vmo公开给用户空间。内核启动时,会通过计算得到它所在的物理页。
苹果正式宣布于2020年采用高通骁龙的X55基带
智能家居,运营商的一块难啃的“骨头”
通常以典型方式测试“典型”
这摄像头略moto 乐视Pro3双摄AI版发布
基于微控制器的DS28E40解决方案
zircon微内核启动代码分析
新区企业拥抱“智造”引入物联网请来机器人
iPhone8手机将携手这款iPad登陆,满屏占比OLED屏
顺造双速冲击钻上架小米有品 售价249元
新海马S5到底如何?真的逆袭了吗?
利用SigmaDSP最大限度地降低汽车音频系统的噪声和功耗
8TB的M.2 SSD,迄今为止三星推出的容量最大的NVME SSD
英特尔全新16nm制程工艺有何优势
GAMS建模语言系统概述
开关电源的五种纹波噪声如何抑制?
Altium Designer3个封装库分享
诺基亚智能电视曝光,其整体设计不同寻常
采用Cyclone FPGA,实现智能电网自动化
针对老年人定制产品与服务 京东发布暖阳行动进一步消除数字鸿沟
关于车身与舒适系统的性能分析和介绍