首页 文章 精选 留言 我的

精选列表

搜索[harmonyos开发],共10000篇文章
优秀的个人博客,低调大师

[鸿蒙从零到一]HarmonyOS任务调度与并发模型实战:taskpool、Worker 与可取消任务

ArkTS 应用中的耗时工作如果直接挤在 UI 线程上,最直观的结果就是点击反馈延迟、动画掉帧,甚至触发应用无响应。网络请求本身通常是异步的,但 JSON 解析、图片处理、文件扫描、数据聚合等计算仍可能长时间占用执行线程。要解决这类问题,不能只是在函数前加上 async,而要先判断任务属于 I/O 等待还是 CPU 计算,再选择合适的并发工具。

优秀的个人博客,低调大师

| 中文注解HarmonyOS源码 | v25.01

鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析,深挖地基工程,大脑永久记忆,四大源码仓每日同步更新 本篇说清楚并发并行 读本篇之前建议先读 鸿蒙内核源码分析(总目录) 进程+线程篇,会对并行并发更深的理解. 理解并发概念 并发(Concurrent):多个线程在单个核心运行,同一时间只能一个线程运行,内核不停切换线程,看起来像同时运行,实际上是线程被高速的切换. 通俗好理解的比喻就是高速单行道,单行道指的是CPU的核数,跑的车就是线程(任务),进程就是管理车的公司,一个公司可以有很多台车.并发和并行跟CPU的核数有关.车道上同时只能跑一辆车,但因为指挥系统很牛,够快,在毫秒级内就能换车跑,人根本感知不到切换.所以外部的感知会是同时在进行,实现了微观上的串行,宏观上的并行. 线程切换的本质是CPU要换场地上班,去哪里上班由哪里提供场地,那个场地就是任务栈,每个任务栈中保存了上班的各种材料,来了就行立马干活.那些材料就是任务上下文.简单的说就是上次活干到那里了,回来继续接着干.上下文由任务栈自己保存,CPU不管的,它来了只负责任务交过来的材料,材料显示去哪里搬砖它就去哪里搬砖. 记住一个单词就能记住并行并发的区别, 发单,发单(并发单行). 理解并行概念 并行(Parallel)每个线程分配给独立的CPU核运行,多线程真正的同时运行. 通俗好理解的比喻就是高速多行道,实现了微观和宏观上同时进行. 并行当然是快,人多了干活就不那么累,但干活人多了必然会带来人多的管理问题,会把问题变复杂,请想想会出现哪些问题? 理解协程概念 这里说下协程,例如go语言是有协程支持的,其实协程跟内核层没有关系,是应用层的概念.是在线程之上更高层的封装,用通俗的比喻来说就是在车内另外搞了几条车道玩.其对内核来说没有新东西,内核只负责车的调度,至于车内你想怎么弄那是应用程序自己的事.本质的区别是CPU根本没有换地方上班(没有被调度),而并发/并行都是换地方上班了. 内核如何描述CPU typedef struct { SortLinkAttribute taskSortLink; /* task sort link */ //每个CPU core 都有一个task排序链表 SortLinkAttribute swtmrSortLink; /* swtmr sort link */ //每个CPU core 都有一个定时器排序链表 UINT32 idleTaskID; /* idle task id */ //空闲任务ID 见于 OsIdleTaskCreate UINT32 taskLockCnt; /* task lock flag */ //任务锁的数量,当 > 0 的时候,需要重新调度了 UINT32 swtmrHandlerQueue; /* software timer timeout queue id */ //软时钟超时队列句柄 UINT32 swtmrTaskID; /* software timer task id */ //软时钟任务ID UINT32 schedFlag; /* pending scheduler flag */ //调度标识 INT_NO_RESCH INT_PEND_RESCH #if (LOSCFG_KERNEL_SMP == YES) UINT32 excFlag; /* cpu halt or exc flag */ //CPU处于停止或运行的标识 #endif } Percpu; Percpu g_percpu[LOSCFG_KERNEL_CORE_NUM];//全局CPU数组 这是内核对CPU的描述,主要是两个排序链表,一个是任务的排序,一个是定时器的排序.什么意思? 在系列篇中多次提过,任务是内核的调度单元,注意可不是进程,虽然调度也需要进程参与,也需要切换进程,切换用户空间.但调度的核心是切换任务,每个任务的代码指令才是CPU的粮食,它吃的是一条条的指令.每个任务都必须指定取粮地址(即入口函数). 另外还有一个东西能提供入口函数,就是定时任务.很重要也很常用,没它某宝每晚9点的准时秒杀实现不了.在内核每个CPU都有自己独立的任务和定时器链表. 每次Tick的到来,处理函数会去扫描这两个链表,看有没有定时器超时的任务需要执行,有则立即执行定时任务,定时任务是所有任务中优先级最高的,0号优先级,在系列篇中有专门讲定时器任务,可自行翻看. LOSCFG_KERNEL_SMP # if (LOSCFG_KERNEL_SMP == YES) # define LOSCFG_KERNEL_CORE_NUM LOSCFG_KERNEL_SMP_CORE_NUM //多核情况下支持的CPU核数 # else # define LOSCFG_KERNEL_CORE_NUM 1 //单核配置 # endif 多CPU核的操作系统有3种处理模式(SMP+AMP+BMP) 鸿蒙实现的是 SMP 的方式 非对称多处理(Asymmetric multiprocessing,AMP)每个CPU内核运行一个独立的操作系统或同一操作系统的独立实例(instantiation)。 对称多处理(Symmetric multiprocessing,SMP)一个操作系统的实例可以同时管理所有CPU内核,且应用并不绑定某一个内核。 混合多处理(Bound multiprocessing,BMP)一个操作系统的实例可以同时管理所有CPU内核,但每个应用被锁定于某个指定的核心。 宏LOSCFG_KERNEL_SMP表示对多CPU核的支持,鸿蒙默认是打开LOSCFG_KERNEL_SMP的。 多CPU核支持 鸿蒙内核对CPU的操作见于 los_mp.c ,因文件不大,这里把代码都贴出来了. #if (LOSCFG_KERNEL_SMP == YES) //给参数CPU发送调度信号 VOID LOS_MpSchedule(UINT32 target)//target每位对应CPU core { UINT32 cpuid = ArchCurrCpuid(); target &= ~(1U << cpuid);//获取除了自身之外的其他CPU HalIrqSendIpi(target, LOS_MP_IPI_SCHEDULE);//向目标CPU发送调度信号,核间中断(Inter-Processor Interrupts),IPI } //硬中断唤醒处理函数 VOID OsMpWakeHandler(VOID) { /* generic wakeup ipi, do nothing */ } //硬中断调度处理函数 VOID OsMpScheduleHandler(VOID) {//将调度标志设置为与唤醒功能不同,这样就可以在硬中断结束时触发调度程序。 /* * set schedule flag to differ from wake function, * so that the scheduler can be triggered at the end of irq. */ OsPercpuGet()->schedFlag = INT_PEND_RESCH;//给当前Cpu贴上调度标签 } //硬中断暂停处理函数 VOID OsMpHaltHandler(VOID) { (VOID)LOS_IntLock(); OsPercpuGet()->excFlag = CPU_HALT;//让当前Cpu停止工作 while (1) {}//陷入空循环,也就是空闲状态 } //MP定时器处理函数, 递归检查所有可用任务 VOID OsMpCollectTasks(VOID) { LosTaskCB *taskCB = NULL; UINT32 taskID = 0; UINT32 ret; /* recursive checking all the available task */ for (; taskID <= g_taskMaxNum; taskID++) { //递归检查所有可用任务 taskCB = &g_taskCBArray[taskID]; if (OsTaskIsUnused(taskCB) || OsTaskIsRunning(taskCB)) { continue; } /* 虽然任务状态不是原子的,但此检查可能成功,但无法完成删除,此删除将在下次运行之前处理 * though task status is not atomic, this check may success but not accomplish * the deletion; this deletion will be handled until the next run. */ if (taskCB->signal & SIGNAL_KILL) {//任务收到被干掉信号 ret = LOS_TaskDelete(taskID);//干掉任务,回归任务池 if (ret != LOS_OK) { PRINT_WARN("GC collect task failed err:0x%x\n", ret); } } } } //MP(multiprocessing) 多核处理器初始化 UINT32 OsMpInit(VOID) { UINT16 swtmrId; (VOID)LOS_SwtmrCreate(OS_MP_GC_PERIOD, LOS_SWTMR_MODE_PERIOD, //创建一个周期性,持续时间为 100个tick的定时器 (SWTMR_PROC_FUNC)OsMpCollectTasks, &swtmrId, 0);//OsMpCollectTasks为超时回调函数 (VOID)LOS_SwtmrStart(swtmrId);//开始定时任务 return LOS_OK; } #endif 代码一一都加上了注解,这里再一一说明下: 1.OsMpInit 多CPU核的初始化, 多核情况下每个CPU都有各自的编号, 内核有分成主次CPU, 0号默认为主CPU, OsMain()由主CPU执行,被汇编代码调用. 初始化只开了个定时任务,只干一件事就是回收不用的任务.回收的条件是任务是否收到了被干掉的信号. 例如shell命令 kill 9 14 ,意思是干掉14号线程的信号,这个信号会被线程保存起来. 可以选择自杀也可以等着被杀. 这里要注意,鸿蒙有两种情况下任务不能被干掉, 一种是系统任务不能被干掉的, 第二种是正在运行状态的任务. 2.次级CPU的初始化 同样由汇编代码调用,通过以下函数执行,完成每个CPU核的初始化 //次级CPU初始化,本函数执行的次数由次级CPU的个数决定. 例如:在四核情况下,会被执行3次, 0号通常被定义为主CPU 执行main LITE_OS_SEC_TEXT_INIT VOID secondary_cpu_start(VOID) { #if (LOSCFG_KERNEL_SMP == YES) UINT32 cpuid = ArchCurrCpuid(); OsArchMmuInitPerCPU();//每个CPU都需要初始化MMU OsCurrTaskSet(OsGetMainTask());//设置CPU的当前任务 /* increase cpu counter */ LOS_AtomicInc(&g_ncpu); //统计CPU的数量 /* store each core's hwid */ CPU_MAP_SET(cpuid, OsHwIDGet());//存储每个CPU的 hwid HalIrqInitPercpu(); //CPU硬件中断初始化 OsCurrProcessSet(OS_PCB_FROM_PID(OsGetKernelInitProcessID())); //设置内核进程为CPU进程 OsSwtmrInit(); //定时任务初始化,每个CPU维护自己的定时器队列 OsIdleTaskCreate(); //创建空闲任务,每个CPU维护自己的任务队列 OsStart(); //本CPU正式启动在内核层的工作 while (1) { __asm volatile("wfi");//wait for Interrupt 等待中断,即下一次中断发生前都在此hold住不干活 }//类似的还有 WFE: wait for Events 等待事件,即下一次事件发生前都在此hold住不干活 #endif } 可以看出次级CPU有哪些初始化步骤: 第一:初始化MMU,OsArchMmuInitPerCPU 第二:设置当前任务 OsCurrTaskSet 第三:初始化硬件中断 HalIrqInitPercpu 第四:初始化定时器队列 OsSwtmrInit 第五:创建空任务 OsIdleTaskCreate, 外面没有任务的时CPU就待在这个空任务里自己转圈圈. 第六:开始自己的工作流程 OsStart,正式开始工作,跑任务 多CPU核还有哪些问题? 1.CPU之间抢资源的情况要怎么处理? 2.CPU之间通讯(也叫核间通讯)怎么解决? 3.如果确保两个CPU不会同时执行同一个任务? 4.汇编代码如何实现对各CPU的调动 请前往系列篇或直接前往内核注解代码查看.这里不再做说明. 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 鸿蒙内核源码分析系列篇 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新

优秀的个人博客,低调大师

| 中文注解HarmonyOS源码 | v22.01

鸿蒙内核源码注释中文版 < Gitee仓 | CSDN仓 | Github仓 | Coding仓 >精读内核源码,中文注解分析,深挖地基工程,构建底层网图,四大码仓每日同步更新 鸿蒙源码分析系列篇 < CSDN | OSCHINA | WeHarmony | 源动力 >故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新 本篇通过拆解一段很简单的汇编代码来快速认识汇编,为读懂鸿蒙汇编打基础.系列篇后续将逐个剖析鸿蒙的汇编文件. 第一: 要认定汇编语言一定是简单的,没有高深的东西,无非就是数据的搬来搬去,运行时数据主要待在两个地方:内存和寄存器。寄存器是CPU内部存储器,离运算器最近,所以最快. 第二: 运行空间(栈空间)就是CPU打卡上班的地方,内核设计者规定谁请CPU上班由谁提供场地,用户程序提供的场地叫用户栈,敏感工作CPU要带回公司做,公司提供的场地叫内核栈,敏感工作叫系统调用,系统调用的本质理解是CPU要切换工作模式即切换办公场地。 第三:CPU的工作顺序是流水线的,它只认指令,而且只去一个地方(指向代码段的PC寄存器)拿指令运算消化。指令集是告诉外界我CPU能干什么活并提供对话指令,汇编语言是人和CPU能愉快沟通不拧巴的共识语言。一一对应了CPU指令,又能确保记性不好的人类能模块化的设计idea, 先看一段C编译成汇编代码再来说模块化。 //编译器: armv7-a clang (trunk) //++++++++++++ square(c -> 汇编)++++++++++++++++++++++++ int square(int a,int b){ return a*b; } square(int, int): sub sp, sp, #8 @sp减去8,意思为给square分配栈空间,只用2个栈空间完成计算 str r0, [sp, #4] @第一个参数入栈 str r1, [sp] @第二个参数入栈 ldr r1, [sp, #4] @取出第一个参数给r1 ldr r2, [sp] @取出第二个参数给r2 mul r0, r1, r2 @执行a*b给R0,返回值的工作一直是交给R0的 add sp, sp, #8 @函数执行完了,要释放申请的栈空间 bx lr @子程序返回,等同于mov pc,lr,即跳到调用处 //++++++++++++ square(c -> 汇编)++++++++++++++++++++++++ int fp(int b) { int a = 1; return square(a+b,a+b); } fp(int): push {r11, lr} @r11(fp)/lr入栈,保存调用者main的位置 mov r11, sp @r11用于保存sp值,函数栈开始位置 sub sp, sp, #8 @sp减去8,意思为给fp分配栈空间,只用2个栈空间完成计算 str r0, [sp, #4] @先保存参数值,放在SP+4,此时r0中存放的是参数 mov r0, #1 @r0=1 str r0, [sp] @再把1也保存在SP的位置 ldr r0, [sp] @把SP的值给R0 ldr r1, [sp, #4] @把SP+4的值给R1 add r1, r0, r1 @执行r1=a+b mov r0, r1 @r0=r1,用r0,r1传参 bl square(int, int)@先mov lr, pc 再mov pc square(int, int) mov sp, r11 @函数执行完了,要释放申请的栈空间 pop {r11, lr} @弹出r11和lr,lr是专用标签,弹出就自动复制给lr寄存器 bx lr @子程序返回,等同于mov pc,lr,即跳到调用处 //++++++++++++ main(c -> 汇编)++++++++++++++++++++++++ int main() { int sum = 0; for(int a = 0;a < 100; a++){ sum = sum + fp(a); } return sum; } main: push {r11, lr} @r11(fp)/lr入栈,保存调用者的位置 mov r11, sp @r11用于保存sp值,函数栈开始位置 sub sp, sp, #16 @sp减去8,意思为给main分配栈空间,只用2个栈空间完成计算 mov r0, #0 @初始化r0 str r0, [r11, #-4] @执行sum = 0 str r0, [sp, #8] @sum将始终占用SP+8的位置 str r0, [sp, #4] @a将始终占用SP+4的位置 b .LBB1_1 @跳到循环开始位置 .LBB1_1: @循环开始位置入口 ldr r0, [sp, #4] @取出a的值给r0 cmp r0, #99 @跟99比较 bgt .LBB1_4 @大于99,跳出循环 mov pc .LBB1_4 b .LBB1_2 @继续循环,直接 mov pc .LBB1_2 .LBB1_2: @符合循环条件入口 ldr r0, [sp, #8] @取出sum的值给r0,sp+8用于写SUM的值 str r0, [sp] @先保存SUM的值,SP的位置用于读SUM值 ldr r0, [sp, #4] @r0用于传参,取出A的值给r0作为fp的参数 bl fp(int) @先mov lr, pc再mov pc fp(int) mov r1, r0 @fp的返回值为r0,保存到r1 ldr r0, [sp] @取出SUM的值 add r0, r0, r1 @计算新sum的值,由R0保存 str r0, [sp, #8] @将新sum保存到SP+8的位置 b .LBB1_3 @无条件跳转,直接 mov pc .LBB1_3 .LBB1_3: @完成a++操作入口 ldr r0, [sp, #4] @SP+4中记录是a的值,赋给r0 add r0, r0, #1 @r0增加1 str r0, [sp, #4] @把新的a值放回SP+4里去 b .LBB1_1 @跳转到比较 a < 100 处 .LBB1_4: @循环结束入口 ldr r0, [sp, #8] @最后SUM的结果给R0,返回值的工作一直是交给R0的 mov sp, r11 @函数执行完了,要释放申请的栈空间 pop {r11, lr} @弹出r11和lr,lr是专用标签,弹出就自动复制给lr寄存器 bx lr @子程序返回,跳转到lr处等同于 MOV PC, LR 代码有点长,都加了注释,如果能直接看懂那么恭喜你,鸿蒙内核的6个汇编文件基于也就懂了。这是以下C文件全貌 #include <stdio.h> #include <math.h> int square(int a,int b){ return a*b; } int fp(int b) { int a = 1; return square(a+b,a+b); } int main() { int sum = 0; for(int a = 0;a < 100; a++){ sum = sum + fp(a); } return sum; } 代码很简单谁都能看懂,代码很典型,具有代表性,有循环,有判断,有运算,有多级函数调用。编译后的汇编代码基本和C语言的结构差不太多, 区别是对循环的实现用了四个模块,四个模块也好理解: 一个是开始块(LBB1_1), 一个符合条件的处理块(LBB1_2),一个条件发生变化块(LBB1_3),最后收尾块(LBB1_4). 按块逐一剖析. 先看最短的那个 int square(int a,int b){ return a*b; } //编译成 square(int, int): sub sp, sp, #8 @sp减去8,意思为给square分配栈空间,只用2个栈空间完成计算 str r0, [sp, #4] @第一个参数入栈 str r1, [sp] @第二个参数入栈 ldr r1, [sp, #4] @取出第一个参数给r1 ldr r2, [sp] @取出第二个参数给r2 mul r0, r1, r2 @执行a*b给R0,返回值的工作一直是交给R0的 add sp, sp, #8 @函数执行完了,要释放申请的栈空间 bx lr @子程序返回,等同于mov pc,lr,即跳到调用处 首先上来一句 sub sp, sp, #8 等同于 sp = sp - 8 ,CPU运行需要场地,这个场地就是栈 ,SP是指向栈的指针,表示此时用栈的刻度. 代码和鸿蒙内核用栈方式一样,都采用了递减满栈的方式(FD). 什么是递减满栈? 递减指的是栈底地址高于栈顶地址,栈的生长方向是递减的, 满栈指的是SP指针永远指向栈顶. 每个函数都有自己独立的栈底和栈顶,之间的空间统称栈帧.可以理解为分配了一块 区域给函数运行,sub sp, sp, #8 代表申请2个栈空间,一个栈空间按四个字节算. 用完要不要释放?当然要,add sp, sp, #8 就是释放栈空间. 是一对的,减了又加回去,空间就归还了. ldr r1, [sp, #4] 的意思是取出SP+4这个虚拟地址的值给r1寄存器,而SP的指向并没有改变的,还是在栈顶, 为什么要+呢, +就是往回数, 定位到分配的栈空间上. 一定要理解递减满栈,这是关键! 否则读不懂内核汇编代码. 入参方式 一般都是通过寄存器(r0..r10)传参,fp调用square之前会先将参数给(r0..r10) add r1, r0, r1 @执行r1=a+b mov r0, r1 @r0=r1,用r0,r1传参 bl square(int, int)@先mov lr, pc 再mov pc square(int, int) 到了square中后,先让 r0,r1入栈,目的是保存参数值, 因为 square中要用r0,r1 , str r0, [sp, #4] @先入栈保存第一个参数 str r1, [sp] @再入栈保存第二个参数 ldr r1, [sp, #4] @再取出第一个参数给r1,(a*b)中a值 ldr r2, [sp] @再取出第二个参数给r2,用于计算 (a*b)中b值 是不是感觉这段汇编很傻,直接不保存计算不就完了吗,这个是流程问题,编译器统一先保存参数,至于你想怎么用它不管,也管不了. 另外返回值都是默认统一给r0保存. square中将(a*b)的结果给了r0,回到fp中取出R0对fp来说这就是square的返回值,这是规定. 函数调用 main 和 fp 中都需要调用其他函数,所以都出现了 push {r11, lr} //.... pop {r11, lr} 这哥俩也是成对出现的,这是函数调用的必备装备,作用是保存和恢复调用者的现场,例如 main -> fp, fp要保存main的栈帧范围和指令位置, lr保存的是main函数执行到哪个指令的位置, r11的作用是指向main的栈顶位置,如此fp执行完后return回main的时候,先mov pc,lr, PC寄存器的值一变, 表示执行的代码就变了,又回到了main的指令和栈帧继续未完成的事业. 内存和寄存器数据怎么搬? 数据主要待在两个地方:内存和寄存器. 寄存器<->寄存器 , 内存<->寄存器 , 内存<->内存 搬运指令都不一样. str r1, [sp] @ 寄存器->内存 ldr r1, [sp, #4] @ 内存->寄存器 这又是一对,用于 内存<->寄存器之间,熟知的 mov r0, r1 用于 寄存器<->寄存器 追问三个问题 第一:如果是可变参数怎么办? 100个参数怎么整, 通过寄存器总共就12个,不够传参啊 第二:返回值可以有多个吗? 第三:数据搬运可以不经过CPU吗? 喜欢就请加点源动力吧 鸿蒙内核源码必读篇 < CSDN | OSCHINA | WeHarmony | 源动力 > 鸿蒙内核源码精华篇 < CSDN | OSCHINA | WeHarmony | 源动力 >

优秀的个人博客,低调大师

[鸿蒙从零到一] HarmonyOS WebSocket 实战:断线重连、心跳保活与连接状态机设计

在鸿蒙应用里做 IM、行情推送、协同编辑这类实时业务,@ohos.net.webSocket 是绕不开的基础能力。但直接裸用官方 API 上线,几乎必然会遇到三类问题:弱网下连接悄悄死掉却收不到 close 事件、重连风暴打爆服务端、断线期间的消息丢失。这篇文章不停留在"怎么建立连接",而是把一个生产可用的 WebSocket 客户端拆成三层:心跳保活、指数退避重连、显式状态机,并给出完整可运行的 ArkTS 实现。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册