鸿蒙内核源码分析(特殊进程篇)

avatar
作者
猴君
阅读量:0

三个进程

鸿蒙有三个特殊的进程,创建顺序如下:

  • 2号进程,KProcess,为内核态根进程.启动过程中创建.
  • 0号进程,KIdle为内核态第二个进程,它是通过KProcess fork 而来的.这有点难理解.
  • 1号进程,init,为用户态根进程.由任务SystemInit创建.

  • 发现没有在图中看不到0号进程,在看完本篇之后请想想为什么?

家族式管理

  • 进程(process)是家族式管理,总体分为两大家族,用户态家族和内核态家族.
  • 用户态的进程是平民阶层,屌丝矮矬穷,干着各行各业的活,权利有限,人数众多,活动范围有限(用户空间).有关单位肯定不能随便进出.这个阶层有个共同的老祖宗g_userInitProcess (1号进程).
    g_userInitProcess = 1; /* 1: The root process ID of the user-mode process is fixed at 1 *///用户态的根进程     //获取用户态进程的根进程,所有用户进程都是g_processCBArray[g_userInitProcess] fork来的     LITE_OS_SEC_TEXT UINT32 OsGetUserInitProcessID(VOID)     {         return g_userInitProcess;     } 
  • 内核态的进程是贵族阶层,管理平民阶层的,维持平民生活秩序的,拥有超级权限,能访问整个空间和所有资源,人数不多.这个阶层老祖宗是 g_kernelInitProcess(2号进程).
    g_kernelInitProcess = 2; /* 2: The root process ID of the kernel-mode process is fixed at 2 *///内核态的根进程     //获取内核态进程的根进程,所有内核进程都是g_processCBArray[g_kernelInitProcess] fork来的,包括g_processCBArray[g_kernelIdleProcess]进程     LITE_OS_SEC_TEXT UINT32 OsGetKernelInitProcessID(VOID)     {         return g_kernelInitProcess;     } 
  • 两位老祖宗都不是通过fork来的,而是内核强制规定进程ID号,强制写死基因创建的.
  • 这两个阶层可以相互流动吗,有没有可能通过高考改变命运? 答案是: 绝对冇可能!!! 龙生龙,凤生凤,老鼠生儿会打洞.从老祖宗创建的那一刻起就被刻在基因里了,抹不掉了. 因为后续所有的进程都是由这两位老同志克隆(clone)来的,没得商量的继承这份基因.LosProcessCB有专门的标签来processMode区分这两个阶层.整个鸿蒙内核源码并没有提供改变命运机会的set函数.
      #define OS_KERNEL_MODE 0x0U	//内核态       #define OS_USER_MODE   0x1U	//用户态       STATIC INLINE BOOL OsProcessIsUserMode(const LosProcessCB *processCB)//用户模式进程       {           return (processCB->processMode == OS_USER_MODE);       }       typedef struct ProcessCB {           // ...           UINT16               processMode;                  /**< Kernel Mode:0; User Mode:1; */	//0位内核态,1为用户态进程       } LosProcessCB;     

2号进程 KProcess

2号进程为内核态的老祖宗,是内核创建的首个进程,源码过程如下,省略了不相干的代码.

bl     main  @带LR的子程序跳转, LR = pc - 4 ,执行C层main函数 /****************************************************************************** 内核入口函数,由汇编调用,见于reset_vector_up.S 和 reset_vector_mp.S up指单核CPU, mp指多核CPU bl        main ******************************************************************************/ LITE_OS_SEC_TEXT_INIT INT32 main(VOID)//由主CPU执行,默认0号CPU 为主CPU  {     // ... 省略     uwRet = OsMain();// 内核各模块初始化 } LITE_OS_SEC_TEXT_INIT INT32 OsMain(VOID) {     // ...      ret = OsKernelInitProcess();// 创建内核态根进程     // ...     ret = OsSystemInit(); //中间创建了用户态根进程 } //初始化 2号进程,即内核态进程的老祖宗 LITE_OS_SEC_TEXT_INIT UINT32 OsKernelInitProcess(VOID) {     LosProcessCB *processCB = NULL;     UINT32 ret;      ret = OsProcessInit();// 初始化进程模块全部变量,创建各循环双向链表     if (ret != LOS_OK) {         return ret;     }      processCB = OS_PCB_FROM_PID(g_kernelInitProcess);// 以PID方式得到一个进程     ret = OsProcessCreateInit(processCB, OS_KERNEL_MODE, "KProcess", 0);// 初始化进程,最高优先级0,鸿蒙进程一共有32个优先级(0-31) 其中0-9级为内核进程,用户进程可配置的优先级有22个(10-31)     if (ret != LOS_OK) {         return ret;     }      processCB->processStatus &= ~OS_PROCESS_STATUS_INIT;// 进程初始化位 置1     g_processGroup = processCB->group;//全局进程组指向了KProcess所在的进程组     LOS_ListInit(&g_processGroup->groupList);// 进程组链表初始化     OsCurrProcessSet(processCB);// 设置为当前进程     return OsCreateIdleProcess();// 创建一个空闲状态的进程 } 

解读

  • main函数在系列篇中会单独讲,请留意自行翻看,它是在开机之初在SVC模式下创建的.
  • 内核态老祖宗的名字叫 KProcess,优先级为最高 0 级,KProcess进程是长期活跃的,很多重要的任务都会跑在其之下.例如:
    • Swt_Task
    • oom_task
    • system_wq
    • tcpip_thread
    • SendToSer
    • SendToTelnet
    • eth_irq_task
    • TouchEventHandler
    • USB_GIANT_Task
      此处不细讲这些任务,在其他篇幅有介绍,但光看名字也能猜个八九,请自行翻看.
  • 紧接着KProcess 以CLONE_FILES的方式 fork了一个 名为KIdle的子进程(0号进程).
  • 内核态的所有进程都来自2号进程这位老同志,子子孙孙,代代相传,形成一颗家族树,和人类的传承所不同的是,它们往往是白发人送黑发人,子孙进程往往都是短命鬼,老祖宗最能活,子孙都死绝了它还在,有些收尸的工作要交给它干.

0 号进程 KIdle

0号进程是内核创建的第二个进程,在OsKernelInitProcess的末尾将KProcess设为当前进程后,紧接着就fork了0号进程.为什么一定要先设置当前进程,因为fork需要一个父进程,而此时系统处于启动阶段,并没有当前进程. 是的,您没有看错.进程是操作系统为方便管理资源而衍生出来的概念,系统并不是非要进程,任务才能运行的. 开机阶段就是啥都没有,默认跑在svc模式下,默认起始地址reset_vector都是由硬件上电后规定的. 进程,线程都是跑起来后慢慢赋予的意义.OsCurrProcessSet是从软件层面赋予了此为当前进程的这个概念.KProcess是内核设置的第一个当前进程.有了它,就可以fork, fork, fork !

//创建一个名叫"KIdle"的0号进程,给CPU空闲的时候使用 STATIC UINT32 OsCreateIdleProcess(VOID) {     UINT32 ret;     CHAR *idleName = "Idle";     LosProcessCB *idleProcess = NULL;     Percpu *perCpu = OsPercpuGet();     UINT32 *idleTaskID = &perCpu->idleTaskID;//得到CPU的idle task      ret = OsCreateResourceFreeTask();// 创建一个资源回收任务,优先级为5 用于回收进程退出时的各种资源     if (ret != LOS_OK) {         return ret;     } 	//创建一个名叫"KIdle"的进程,并创建一个idle task,CPU空闲的时候就待在 idle task中等待被唤醒     ret = LOS_Fork(CLONE_FILES, "KIdle", (TSK_ENTRY_FUNC)OsIdleTask, LOSCFG_BASE_CORE_TSK_IDLE_STACK_SIZE);     if (ret < 0) {//内核进程的fork并不会一次调用,返回两次,此子进程执行的开始位置是参数OsIdleTask         return LOS_NOK;     }     g_kernelIdleProcess = (UINT32)ret;//返回 0号进程      idleProcess = OS_PCB_FROM_PID(g_kernelIdleProcess);//通过ID拿到进程实体     *idleTaskID = idleProcess->threadGroupID;//绑定CPU的IdleTask,或者说改变CPU现有的idle任务     OS_TCB_FROM_TID(*idleTaskID)->taskStatus |= OS_TASK_FLAG_SYSTEM_TASK;//设定Idle task 为一个系统任务 #if (LOSCFG_KERNEL_SMP == YES)     OS_TCB_FROM_TID(*idleTaskID)->cpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());//多核CPU的任务指定,防止乱串了,注意多核才会有并行处理 #endif     (VOID)memset_s(OS_TCB_FROM_TID(*idleTaskID)->taskName, OS_TCB_NAME_LEN, 0, OS_TCB_NAME_LEN);//task 名字先清0     (VOID)memcpy_s(OS_TCB_FROM_TID(*idleTaskID)->taskName, OS_TCB_NAME_LEN, idleName, strlen(idleName));//task 名字叫 idle     return LOS_OK; } 

解读

  • 看过fork篇的可能发现了一个参数, KIdle被创建的方式和通过系统调用创建的方式不一样,一个用的是CLONE_FILES,一个是 CLONE_SIGHAND 具体的创建方式如下:
      #define CLONE_VM       0x00000100	//子进程与父进程运行于相同的内存空间       #define CLONE_FS       0x00000200	//子进程与父进程共享相同的文件系统,包括root、当前目录、umask       #define CLONE_FILES    0x00000400	//子进程与父进程共享相同的文件描述符(file descriptor)表       #define CLONE_SIGHAND  0x00000800	//子进程与父进程共享相同的信号处理(signal handler)表       #define CLONE_PTRACE   0x00002000	//若父进程被trace,子进程也被trace       #define CLONE_VFORK    0x00004000	//父进程被挂起,直至子进程释放虚拟内存资源       #define CLONE_PARENT   0x00008000	//创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”       #define CLONE_THREAD   0x00010000	//Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群 
  • KIdle创建了一个名为Idle的任务,任务的入口函数为OsIdleTask,这是个空闲任务,啥也不干的.专门用来给cpu休息的,cpu空闲时就待在这个任务里等活干.
      LITE_OS_SEC_TEXT WEAK VOID OsIdleTask(VOID)       {           while (1) {//只有一个死循环       #ifdef LOSCFG_KERNEL_TICKLESS //低功耗模式开关, idle task 中关闭tick               if (OsTickIrqFlagGet()) {                   OsTickIrqFlagSet(0);                   OsTicklessStart();               }       #endif               Wfi();//WFI指令:arm core 立即进入low-power standby state,进入休眠模式,等待中断.           }       } 
  • fork 内核态进程和fork用户态进程有个地方会不一样,就是SP寄存器的值.fork用户态的进程一次调用两次返回(父子进程各一次),返回的位置一样(是因为拷贝了父进程陷入内核时的上下文).所以可以通过返回值来判断是父还是子返回.这个在fork篇中有详细的描述.请自行翻看. 但fork内核态进程虽也有两次返回,但是返回的位置却不一样,子进程的返回位置是由内核指定的,例如:Idle任务的入口函数为OsIdleTask.详见代码:
    //任务初始化时拷贝任务信息       STATIC VOID OsInitCopyTaskParam(LosProcessCB *childProcessCB, const CHAR *name, UINTPTR entry, UINT32 size,                                       TSK_INIT_PARAM_S *childPara)       {           LosTaskCB *mainThread = NULL;           UINT32 intSave;            SCHEDULER_LOCK(intSave);           mainThread = OsCurrTaskGet();//获取当前task,注意变量名从这里也可以看出 thread 和 task 是一个概念,只是内核常说task,上层应用说thread ,概念的映射.            if (OsProcessIsUserMode(childProcessCB)) {//用户态进程               childPara->pfnTaskEntry = mainThread->taskEntry;//拷贝当前任务入口地址               childPara->uwStackSize = mainThread->stackSize;	//栈空间大小               childPara->userParam.userArea = mainThread->userArea;		//用户态栈区栈顶位置               childPara->userParam.userMapBase = mainThread->userMapBase;	//用户态栈底               childPara->userParam.userMapSize = mainThread->userMapSize;	//用户态栈大小           } else {//注意内核态进程创建任务的入口由外界指定,例如 OsCreateIdleProcess 指定了OsIdleTask               childPara->pfnTaskEntry = (TSK_ENTRY_FUNC)entry;//参数(sp)为内核态入口地址               childPara->uwStackSize = size;//参数(size)为内核态栈大小           }           childPara->pcName = (CHAR *)name;					//拷贝进程名字           childPara->policy = mainThread->policy;				//拷贝调度模式           childPara->usTaskPrio = mainThread->priority;		//拷贝优先级           childPara->processID = childProcessCB->processID;	//拷贝进程ID           if (mainThread->taskStatus & OS_TASK_FLAG_PTHREAD_JOIN) {               childPara->uwResved = OS_TASK_FLAG_PTHREAD_JOIN;           } else if (mainThread->taskStatus & OS_TASK_FLAG_DETACHED) {               childPara->uwResved = OS_TASK_FLAG_DETACHED;           }            SCHEDULER_UNLOCK(intSave);       } 
  • 结论是创建0号进程中的OsCreateIdleProcess调用LOS_Fork后只会有一次返回.而且返回值为0,因为 g_freeProcess中0号进程还没有被分配.详见代码,注意看最后的注释:
    //进程模块初始化,被编译放在代码段 .init 中       LITE_OS_SEC_TEXT_INIT UINT32 OsProcessInit(VOID)       {           UINT32 index;           UINT32 size;            g_processMaxNum = LOSCFG_BASE_CORE_PROCESS_LIMIT;//默认支持64个进程           size = g_processMaxNum * sizeof(LosProcessCB);//算出总大小            g_processCBArray = (LosProcessCB *)LOS_MemAlloc(m_aucSysMem1, size);// 进程池,占用内核堆,内存池分配            if (g_processCBArray == NULL) {               return LOS_NOK;           }           (VOID)memset_s(g_processCBArray, size, 0, size);//安全方式重置清0            LOS_ListInit(&g_freeProcess);//进程空闲链表初始化,创建一个进程时从g_freeProcess中申请一个进程描述符使用           LOS_ListInit(&g_processRecyleList);//进程回收链表初始化,回收完成后进入g_freeProcess等待再次被申请使用            for (index = 0; index < g_processMaxNum; index++) {//进程池循环创建               g_processCBArray[index].processID = index;//进程ID[0-g_processMaxNum-1]赋值               g_processCBArray[index].processStatus = OS_PROCESS_FLAG_UNUSED;// 默认都是白纸一张,贴上未使用标签               LOS_ListTailInsert(&g_freeProcess, &g_processCBArray[index].pendList);//注意g_freeProcess挂的是pendList节点,所以使用要通过OS_PCB_FROM_PENDLIST找到进程实体.           }            g_userInitProcess = 1; /* 1: The root process ID of the user-mode process is fixed at 1 *///用户态的根进程           LOS_ListDelete(&g_processCBArray[g_userInitProcess].pendList);// 将1号进程从空闲链表上摘出去            g_kernelInitProcess = 2; /* 2: The root process ID of the kernel-mode process is fixed at 2 *///内核态的根进程           LOS_ListDelete(&g_processCBArray[g_kernelInitProcess].pendList);// 将2号进程从空闲链表上摘出去            //注意:这波骚操作之后,g_freeProcess链表上还有,0,3,4,...g_processMaxNum-1号进程.创建进程是从g_freeProcess上申请           //即下次申请到的将是0号进程,而 OsCreateIdleProcess 将占有0号进程.            return LOS_OK;       } 

1号进程 init

1号进程为用户态的老祖宗.创建过程如下, 省略了不相干的代码.

LITE_OS_SEC_TEXT_INIT INT32 OsMain(VOID) {     // ...      ret = OsKernelInitProcess();// 创建内核态根进程     // ...     ret = OsSystemInit(); //中间创建了用户态根进程 } UINT32 OsSystemInit(VOID) {     //..     ret = OsSystemInitTaskCreate();//创建了一个系统任务, }  STATIC UINT32 OsSystemInitTaskCreate(VOID) {     UINT32 taskID;     TSK_INIT_PARAM_S sysTask;      (VOID)memset_s(&sysTask, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));     sysTask.pfnTaskEntry = (TSK_ENTRY_FUNC)SystemInit;//任务的入口函数,这个函数实现由外部提供     sysTask.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;//16K     sysTask.pcName = "SystemInit";//任务的名称     sysTask.usTaskPrio = LOSCFG_BASE_CORE_TSK_DEFAULT_PRIO;// 内核默认优先级为10      sysTask.uwResved = LOS_TASK_STATUS_DETACHED;//任务分离模式 #if (LOSCFG_KERNEL_SMP == YES)     sysTask.usCpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());//cpu 亲和性设置,记录执行过任务的CPU,尽量确保由同一个CPU完成任务周期 #endif     return LOS_TaskCreate(&taskID, &sysTask);//创建任务并加入就绪队列,并立即参与调度 } //SystemInit的实现由由外部提供 比如..\vendor\hi3516dv300\module_init\src\system_init.c void SystemInit(void) {     // ...     if (OsUserInitProcess()) {//创建用户态进程的老祖宗         PRINT_ERR("Create user init process faialed!\n");         return;     } } //用户态根进程的创建过程 LITE_OS_SEC_TEXT_INIT UINT32 OsUserInitProcess(VOID) {     INT32 ret;     UINT32 size;     TSK_INIT_PARAM_S param = { 0 };     VOID *stack = NULL;     VOID *userText = NULL;     CHAR *userInitTextStart = (CHAR *)&__user_init_entry;//代码区开始位置 ,对应 LITE_USER_SEC_ENTRY     CHAR *userInitBssStart = (CHAR *)&__user_init_bss;// 未初始化数据区(BSS)。在运行时改变其值 对应 LITE_USER_SEC_BSS     CHAR *userInitEnd = (CHAR *)&__user_init_end;// 结束地址     UINT32 initBssSize = userInitEnd - userInitBssStart;     UINT32 initSize = userInitEnd - userInitTextStart;      LosProcessCB *processCB = OS_PCB_FROM_PID(g_userInitProcess);//"Init进程的优先级是 28"     ret = OsProcessCreateInit(processCB, OS_USER_MODE, "Init", OS_PROCESS_USERINIT_PRIORITY);// 初始化用户进程,它将是所有应用程序的父进程     if (ret != LOS_OK) {         return ret;     }      userText = LOS_PhysPagesAllocContiguous(initSize >> PAGE_SHIFT);// 分配连续的物理页     if (userText == NULL) {         ret = LOS_NOK;         goto ERROR;     }      (VOID)memcpy_s(userText, initSize, (VOID *)&__user_init_load_addr, initSize);// 安全copy 经加载器load的结果 __user_init_load_addr -> userText     ret = LOS_VaddrToPaddrMmap(processCB->vmSpace, (VADDR_T)(UINTPTR)userInitTextStart, LOS_PaddrQuery(userText),                                initSize, VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE |                                VM_MAP_REGION_FLAG_PERM_EXECUTE | VM_MAP_REGION_FLAG_PERM_USER);// 虚拟地址与物理地址的映射     if (ret < 0) {         goto ERROR;     }      (VOID)memset_s((VOID *)((UINTPTR)userText + userInitBssStart - userInitTextStart), initBssSize, 0, initBssSize);// 除了代码段,其余都清0      stack = OsUserInitStackAlloc(g_userInitProcess, &size);//分配任务在用户态下的运行栈,大小为1M     if (stack == NULL) {         PRINTK("user init process malloc user stack failed!\n");         ret = LOS_NOK;         goto ERROR;     }      param.pfnTaskEntry = (TSK_ENTRY_FUNC)userInitTextStart;// 从代码区开始执行,也就是应用程序main 函数的位置     param.userParam.userSP = (UINTPTR)stack + size;// 用户态栈底     param.userParam.userMapBase = (UINTPTR)stack;// 用户态栈顶     param.userParam.userMapSize = size;// 用户态栈大小     param.uwResved = OS_TASK_FLAG_PTHREAD_JOIN;// 可结合的(joinable)能够被其他线程收回其资源和杀死     ret = OsUserInitProcessStart(g_userInitProcess, &param);// 创建一个任务,来运行main函数     if (ret != LOS_OK) {         (VOID)OsUnMMap(processCB->vmSpace, param.userParam.userMapBase, param.userParam.userMapSize);         goto ERROR;     }      return LOS_OK;  ERROR:     (VOID)LOS_PhysPagesFreeContiguous(userText, initSize >> PAGE_SHIFT);//释放物理内存块     OsDeInitPCB(processCB);//删除PCB块     return ret; } 

解读

  • 从代码中可以看出用户态的老祖宗创建过程有点意思,首先它的源头和内核态老祖宗一样都在OsMain.
  • 通过创建一个分离模式,优先级为10的系统任务 SystemInit,来完成.任务的入口函数 SystemInit()的实现由平台集成商来指定. 本篇采用了hi3516dv300的实现.也就是说用户态祖宗的创建是在 sysTask.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;//16K 栈中完成的.这个任务归属于内核进程KProcess.
  • 用户态老祖宗的名字叫 Init,优先级为28级.
  • 用户态的每个进程有独立的虚拟进程空间vmSpace,拥有独立的内存映射表(L1,L2表),申请的内存需要重新映射,映射过程在内存系列篇中有详细的说明.
  • init创建了一个任务,任务的入口地址为 __user_init_entry,由编译器指定.
  • 用户态进程是指应有程序运行的进程,通过动态加载ELF文件的方式启动.具体加载流程系列篇有讲解,不细说.用户态进程运行在用户空间,但通过系统调用可陷入内核空间.具体看这张图:

经常有很多小伙伴抱怨说:不知道学习鸿蒙开发哪些技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?

为了能够帮助到大家能够有规划的学习,这里特别整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

在这里插入图片描述

《鸿蒙 (Harmony OS)开发学习手册》(共计892页):https://gitcode.com/HarmonyOS_MN

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

在这里插入图片描述

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

在这里插入图片描述

鸿蒙开发面试真题(含参考答案):https://gitcode.com/HarmonyOS_MN

在这里插入图片描述

OpenHarmony 开发环境搭建

图片

《OpenHarmony源码解析》:https://gitcode.com/HarmonyOS_MN

  • 搭建开发环境
  • Windows 开发环境的搭建
  • Ubuntu 开发环境搭建
  • Linux 与 Windows 之间的文件共享
  • ……
  • 系统架构分析
  • 构建子系统
  • 启动流程
  • 子系统
  • 分布式任务调度子系统
  • 分布式通信子系统
  • 驱动子系统
  • ……

图片

OpenHarmony 设备开发学习手册:https://gitcode.com/HarmonyOS_MN

图片

写在最后

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙

  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往在这里插入图片描述

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!