鸿蒙内核源码分析(信号量篇) | 谁在负责解决任务的同步

基本概念

信号量(Semaphore) 是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。

一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况:

0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。
正值,表示该信号量当前可被获取。

以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同:

用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时,建议使用二值信号量,一种类似于互斥锁的机制。

用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。

信号量运作原理

信号量初始化,为配置的N个信号量申请内存(N值可以由用户自行配置,通过 LOSCFG_BASE_IPC_SEM_LIMIT 宏实现),并把所有信号量初始化成未使用,加入到未使用链表中供系统使用。

  • 信号量创建,从未使用的信号量链表中获取一个信号量,并设定初值。
  • 信号量申请,若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量,
    等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。
  • 信号量释放,若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。
  • 信号量删除,将正在使用的信号量置为未使用信号量,并挂回到未使用链表。

信号量允许多个任务在同一时刻访问共享资源,但会限制同一时刻访问此资源的最大任务数目。
当访问资源的任务数达到该资源允许的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。

信号量长什么样?


typedef struct {UINT8 semStat; /**< Semaphore state *///信号量的状态UINT16 semCount; /**< Number of available semaphores *///有效信号量的数量UINT16 maxSemCount;  /**< Max number of available semaphores *///有效信号量的最大数量UINT32 semID; /**< Semaphore control structure ID *///信号量索引号LOS_DL_LIST semList; /**< Queue of tasks that are waiting on a semaphore *///等待信号量的任务队列,任务通过阻塞节点挂上去
} LosSemCB;

semList,这又是一个双向链表, 双向链表是内核最重要的结构体, 查看双向链表篇, LOS_DL_LIST像狗皮膏药一样牢牢的寄生在宿主结构体上semList上挂的是未来所有等待这个信号量的任务.

初始化信号量模块

#ifndef LOSCFG_BASE_IPC_SEM_LIMIT
#define LOSCFG_BASE_IPC_SEM_LIMIT 1024 //信号量的最大个数
#endifLITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID)//信号量初始化
{LosSemCB *semNode = NULL;UINT32 index;LOS_ListInit(&g_unusedSemList);//初始/* system resident memory, don't free */g_allSem = (LosSemCB *)LOS_MemAlloc(m_aucSysMem0, (LOSCFG_BASE_IPC_SEM_LIMIT * sizeof(LosSemCB)));//分配信号池if (g_allSem == NULL) {return LOS_ERRNO_SEM_NO_MEMORY;}for (index = 0; index < LOSCFG_BASE_IPC_SEM_LIMIT; index++) {semNode = ((LosSemCB *)g_allSem) + index;//拿信号控制块, 可以直接g_allSem[index]来嘛semNode->semID = SET_SEM_ID(0, index);//保存IDsemNode->semStat = OS_SEM_UNUSED;//标记未使用LOS_ListTailInsert(&g_unusedSemList, &semNode->semList);//通过semList把 信号块挂到空闲链表上}if (OsSemDbgInitHook() != LOS_OK) {return LOS_ERRNO_SEM_NO_MEMORY;}return LOS_OK;
}

分析如下:

  • 初始化创建了信号量池来统一管理信号量, 默认 1024 个信号量
  • 信号ID范围从 [0,1023]
  • 未分配使用的信号量都挂到了全局变量 g_unusedSemList 上.

小建议:鸿蒙内核其他池(如进程池,任务池)都采用free来命名空闲链表,而此处使用unused,命名风格不太严谨,有待改善.

创建信号量

LITE_OS_SEC_TEXT_INIT UINT32 OsSemCreate(UINT16 count, UINT16 maxCount, UINT32 *semHandle)
{unusedSem = LOS_DL_LIST_FIRST(&g_unusedSemList);//从未使用信号量池中取首个LOS_ListDelete(unusedSem);//从空闲链表上摘除semCreated = GET_SEM_LIST(unusedSem);//通过semList挂到链表上的,这里也要通过它把LosSemCB头查到. 进程,线程等结构体也都是这么干的.semCreated->semCount = count;//设置数量semCreated->semStat = OS_SEM_USED;//设置可用状态semCreated->maxSemCount = maxCount;//设置最大信号数量LOS_ListInit(&semCreated->semList);//初始化链表,后续阻塞任务通过task->pendList挂到semList链表上,就知道哪些任务在等它了.*semHandle = semCreated->semID;//参数带走 semIDOsSemDbgUpdateHook(semCreated->semID, OsCurrTaskGet()->taskEntry, count);return LOS_OK;ERR_HANDLER:OS_RETURN_ERROR_P2(errLine, errNo);
}

分析如下:

  • 从未使用的空闲链表中拿首个信号量供分配使用.
  • 信号量的最大数量和信号量个数都由参数指定.
  • 信号量状态由 OS_SEM_UNUSED 变成了 OS_SEM_USED
  • semHandle带走信号量ID,外部由此知道成功创建了一个编号为 *semHandle 的信号量

申请信号量

LITE_OS_SEC_TEXT UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout)
{UINT32 intSave;LosSemCB *semPended = GET_SEM(semHandle);//通过ID拿到信号体UINT32 retErr = LOS_OK;LosTaskCB *runTask = NULL;if (GET_SEM_INDEX(semHandle) >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) {OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID);}if (OS_INT_ACTIVE) {PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_INTERR!!!\n");OsBackTrace();return LOS_ERRNO_SEM_PEND_INTERR;}runTask = OsCurrTaskGet();//获取当前任务if (runTask->taskStatus & OS_TASK_FLAG_SYSTEM_TASK) {OsBackTrace();return LOS_ERRNO_SEM_PEND_IN_SYSTEM_TASK;}SCHEDULER_LOCK(intSave);if ((semPended->semStat == OS_SEM_UNUSED) || (semPended->semID != semHandle)) {retErr = LOS_ERRNO_SEM_INVALID;goto OUT;}/* Update the operate time, no matter the actual Pend success or not */OsSemDbgTimeUpdateHook(semHandle);if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了semPended->semCount--;//资源少了一个goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 } else if (!timeout) {retErr = LOS_ERRNO_SEM_UNAVAILABLE;goto OUT;}if (!OsPreemptableInSched()) {//不能申请调度 (不能调度的原因是因为没有持有调度任务自旋锁)PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_IN_LOCK!!!\n");OsBackTrace();retErr = LOS_ERRNO_SEM_PEND_IN_LOCK;goto OUT;}runTask->taskSem = (VOID *)semPended;//标记当前任务在等这个信号量retErr = OsTaskWait(&semPended->semList, timeout, TRUE);//任务进入等待状态,当前任务会挂到semList上,并在其中切换任务上下文if (retErr == LOS_ERRNO_TSK_TIMEOUT) {//注意:这里是涉及到task切换的,把自己挂起,唤醒其他task runTask->taskSem = NULL;retErr = LOS_ERRNO_SEM_TIMEOUT;}OUT:SCHEDULER_UNLOCK(intSave);return retErr;
}

分析如下:
这个函数有点复杂,大量的goto,但别被它绕晕了,盯着返回值看.
先说结果只有一种情况下申请信号量能成功(即 retErr == LOS_OK)

    if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了semPended->semCount--;//资源少了一个goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 }

其余申请失败的原因有:

  • 信号量ID超出范围(默认1024)
  • 中断发生期间
  • 系统任务
  • 信号量状态不对,信号量ID不匹配

以上都是异常的判断,再说正常情况下 semPended->semCount = 0时的情况,没有资源了怎么办?
任务进入 OsTaskWait 睡眠状态,怎么睡,睡多久,由参数 timeouttimeout 值分以下三种模式:

无阻塞模式:即任务申请信号量时,入参 timeout 等于0。若当前信号量计数值不为0,则申请成功,否则立即返回申请失败。

永久阻塞模式:即任务申请信号量时,入参timeout 等于0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。
否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该信号量,阻塞任务才会重新得以执行。

定时阻塞模式:即任务申请信号量时,0<timeout<0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。
否则,该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,
超时前如果有其他任务释放该信号量,则该任务可成功获取信号量继续执行,若超时前未获取到信号量,接口将返回超时错误码。

OsTaskWait 中,任务将被挂入semList链表,semList上挂的都是等待这个信号量的任务.

释放信号量

LITE_OS_SEC_TEXT UINT32 OsSemPostUnsafe(UINT32 semHandle, BOOL *needSched)
{LosSemCB *semPosted = NULL;LosTaskCB *resumedTask = NULL;if (GET_SEM_INDEX(semHandle) >= LOSCFG_BASE_IPC_SEM_LIMIT) {return LOS_ERRNO_SEM_INVALID;}semPosted = GET_SEM(semHandle);if ((semPosted->semID != semHandle) || (semPosted->semStat == OS_SEM_UNUSED)) {return LOS_ERRNO_SEM_INVALID;}/* Update the operate time, no matter the actual Post success or not */OsSemDbgTimeUpdateHook(semHandle);if (semPosted->semCount == OS_SEM_COUNT_MAX) {//当前信号资源不能大于最大资源量return LOS_ERRNO_SEM_OVERFLOW;}if (!LOS_ListEmpty(&semPosted->semList)) {//当前有任务挂在semList上,要去唤醒任务resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));//semList上面挂的都是task->pendlist节点,取第一个task下来唤醒resumedTask->taskSem = NULL;//任务不用等信号了,重新变成NULL值OsTaskWake(resumedTask);//唤醒任务,注意resumedTask一定不是当前任务,OsTaskWake里面并不会自己切换任务上下文,只是设置状态if (needSched != NULL) {//参数不为空,就返回需要调度的标签*needSched = TRUE;//TRUE代表需要调度}} else {//当前没有任务挂在semList上,semPosted->semCount++;//信号资源多一个}return LOS_OK;
}LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle)
{UINT32 intSave;UINT32 ret;BOOL needSched = FALSE;SCHEDULER_LOCK(intSave);ret = OsSemPostUnsafe(semHandle, &needSched);SCHEDULER_UNLOCK(intSave);if (needSched) {//需要调度的情况LOS_MpSchedule(OS_MP_CPU_ALL);//向所有CPU发送调度指令LOS_Schedule();发起调度}return ret;
}

分析如下:

  • 注意看在什么情况下 semPosted->semCount 才会 ++ ,是在LOS_ListEmpty为真的时候,semList是等待这个信号量的任务.
    semList上的任务是在OsTaskWait中挂入的.都在等这个信号.
  • 每次OsSemPost都会唤醒semList链表上一个任务,直到semList为空.
  • 掌握信号量的核心是理解 LOS_SemPendLOS_SemPost

编程示例

本实例实现如下功能:

  • 测试任务Example_TaskEntry创建一个信号量,锁任务调度,创建两个任务Example_SemTask1、Example_SemTask2,Example_SemTask2优先级高于Example_SemTask1,两个任务中申请同一信号量,解锁任务调度后两任务阻塞,测试任务Example_TaskEntry释放信号量。

  • Example_SemTask2得到信号量,被调度,然后任务休眠20Tick,Example_SemTask2延迟,Example_SemTask1被唤醒。

  • Example_SemTask1定时阻塞模式申请信号量,等待时间为10Tick,因信号量仍被Example_SemTask2持有,Example_SemTask1挂起,10Tick后仍未得到信号量,
    Example_SemTask1被唤醒,试图以永久阻塞模式申请信号量,Example_SemTask1挂起。

  • 20Tick后Example_SemTask2唤醒, 释放信号量后,Example_SemTask1得到信号量被调度运行,最后释放信号量。

  • Example_SemTask1执行完,40Tick后任务Example_TaskEntry被唤醒,执行删除信号量,删除两个任务。

/* 任务ID */
static UINT32 g_testTaskId01;
static UINT32 g_testTaskId02;
/* 测试任务优先级 */
#define TASK_PRIO_TEST  5
/* 信号量结构体id */
static UINT32 g_semId;VOID Example_SemTask1(VOID)
{UINT32 ret;printf("Example_SemTask1 try get sem g_semId ,timeout 10 ticks.\n");/* 定时阻塞模式申请信号量,定时时间为10ticks */ret = LOS_SemPend(g_semId, 10);/*申请到信号量*/if (ret == LOS_OK) {LOS_SemPost(g_semId);return;}/* 定时时间到,未申请到信号量 */if (ret == LOS_ERRNO_SEM_TIMEOUT) {printf("Example_SemTask1 timeout and try get sem g_semId wait forever.\n");/*永久阻塞模式申请信号量*/ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);printf("Example_SemTask1 wait_forever and get sem g_semId .\n");if (ret == LOS_OK) {LOS_SemPost(g_semId);return;}}
}VOID Example_SemTask2(VOID)
{UINT32 ret;printf("Example_SemTask2 try get sem g_semId wait forever.\n");/* 永久阻塞模式申请信号量 */ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);if (ret == LOS_OK) {printf("Example_SemTask2 get sem g_semId and then delay 20ticks .\n");}/* 任务休眠20 ticks */LOS_TaskDelay(20);printf("Example_SemTask2 post sem g_semId .\n");/* 释放信号量 */LOS_SemPost(g_semId);return;
}UINT32 ExampleTaskEntry(VOID)
{UINT32 ret;TSK_INIT_PARAM_S task1;TSK_INIT_PARAM_S task2;/* 创建信号量 */LOS_SemCreate(0,&g_semId);/* 锁任务调度 */LOS_TaskLock();/*创建任务1*/(VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask1;task1.pcName       = "TestTsk1";task1.uwStackSize  = OS_TSK_DEFAULT_STACK_SIZE;task1.usTaskPrio   = TASK_PRIO_TEST;ret = LOS_TaskCreate(&g_testTaskId01, &task1);if (ret != LOS_OK) {printf("task1 create failed .\n");return LOS_NOK;}/* 创建任务2 */(VOID)memset_s(&task2, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask2;task2.pcName       = "TestTsk2";task2.uwStackSize  = OS_TSK_DEFAULT_STACK_SIZE;task2.usTaskPrio   = (TASK_PRIO_TEST - 1);ret = LOS_TaskCreate(&g_testTaskId02, &task2);if (ret != LOS_OK) {printf("task2 create failed .\n");return LOS_NOK;}/* 解锁任务调度 */LOS_TaskUnlock();ret = LOS_SemPost(g_semId);/* 任务休眠40 ticks */LOS_TaskDelay(40);/* 删除信号量 */LOS_SemDelete(g_semId);/* 删除任务1 */ret = LOS_TaskDelete(g_testTaskId01);if (ret != LOS_OK) {printf("task1 delete failed .\n");return LOS_NOK;}/* 删除任务2 */ret = LOS_TaskDelete(g_testTaskId02);if (ret != LOS_OK) {printf("task2 delete failed .\n");return LOS_NOK;}return LOS_OK;
}

实例运行结果:

Example_SemTask2 try get sem g_semId wait forever.
Example_SemTask1 try get sem g_semId ,timeout 10 ticks.
Example_SemTask2 get sem g_semId and then delay 20ticks .
Example_SemTask1 timeout and try get sem g_semId wait forever.
Example_SemTask2 post sem g_semId .
Example_SemTask1 wait_forever and get sem g_semId .

鸿蒙全栈开发全新学习指南

也为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线【包含了大APP实战项目开发】

本路线共分为四个阶段:

第一阶段:鸿蒙初中级开发必备技能

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:https://gitee.com/MNxiaona/733GH

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

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

开发基础知识:gitee.com/MNxiaona/733GH

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.……

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:gitee.com/MNxiaona/733GH

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/669905.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据交换和异步请求(JSONAjax))

目录 一.JSON介绍1.JSON的特点2.JSON的结构3.JSON的值JSON示例4.JSON与字符串对象转换5.注意事项 二.JSON在Java中的使用1.Javabean to json2.List to json3.Map to JSONTypeToken底层解析 三.Ajax介绍1.介绍2.Ajax经典应用场景 四.Ajax原理示意图1. 传统web应用2.Ajax方法 五.…

RockChip Android13 NFC SL6320移植

环境:RK3568 Android13 一:驱动移植 1、驱动 将SL6320驱动代码拷贝至kernel-5.10/drivers/misc/sl6320/ 特殊说明:勿将驱动代码放置于kernel-5.10/drivers/nfc/目录下,会导致sl6320驱动生成设备节点时因/dev/nfc节点以创建而加载失败。 2、DTS 本次硬件设计电路走I2C协…

CWDM、DWDM、MWDM、LWDM:快速了解光波复用技术

在现代光纤通信领域&#xff0c;波分复用&#xff08;WDM&#xff09;技术作为一项先进的创新脱颖而出。它通过将多个不同波长和速率的光信号汇聚到一根光纤中来有效地传输数据。本文将深入探讨几种关键的 WDM 技术&#xff08;CWDM、DWDM、MWDM 和 LWDM&#xff09;&#xff0…

深度学习中的归一化:BN,LN,IN,GN的优缺点

目录 深度学习中归一化的作用常见归一化的优缺点 深度学习中归一化的作用 加速训练过程 归一化可以加速深度学习模型的训练过程。通过调整输入数据的尺度&#xff0c;归一化有助于改善优化算法的收敛速度。这是因为归一化后的数据具有相似的尺度&#xff0c;使得梯度下降等优化…

微信小程序修改radio的样式,以及获取radio选择的值(自定义radio样式)

博主介绍&#xff1a;本人专注于Android/java/数据库/微信小程序技术领域的开发&#xff0c;以及有好几年的计算机毕业设计方面的实战开发经验和技术积累&#xff1b;尤其是在安卓&#xff08;Android&#xff09;的app的开发和微信小程序的开发&#xff0c;很是熟悉和了解&…

谷歌推广和seo留痕具体怎么操作?

留痕跟谷歌推广其实是一回事&#xff0c;你能在谷歌上留痕&#xff0c;其实就是推广了自己的信息&#xff0c;本质上留痕就是在各大网站留下自己的记录&#xff0c;这个记录可以是品牌信息&#xff0c;联系方式&#xff0c;看你想留下什么 如果要问自己怎么操作&#xff0c;正常…

C++基础理论学习

一、常量及符号 常量就是在程序运行过程中不可以改变的数值。例如&#xff0c;每个人的身份证号码就是一常量&#xff0c;是不能被更改的。常量可分为整型常量、浮点型常量、字符常量和字符串常量。 上面的代码通过com输出4行内容&#xff0c;cot是输出流&#xff0c;实现输出…

事务的使用 @Transactional

更新操作多个数据表的时候需要使用到事务 事务&#xff1a;要么都执行&#xff0c;要么都不执行。 1.Transactional 如果有异常&#xff0c;只有RunTimeException和Error时&#xff0c;事务才会生效&#xff0c;否则事务不会生效&#xff0c;需要手动开启事务currentTransacti…

FTP和NFS

一、FTP 1.FTP原理 FTP&#xff08;file Transfer Protocol&#xff0c;文件传输协议&#xff09;&#xff0c;是典型的C/S架构的应用层协议&#xff0c;由客户端软件和服务端软件两个部分共同实现文件传输功能&#xff0c;FTP客户端和服务器之间的连接时可靠的&#xff0c;面…

【微服务】服务保护(通过Sentinel解决雪崩问题)

Sentinel解决雪崩问题 雪崩问题服务保护方案服务降级保护 服务保护技术SentinelFallback服务熔断 雪崩问题 在微服务调用链中如果有一个服务的问题导致整条链上的服务都不可用&#xff0c;称为雪崩 原因 微服务之间的相互调用&#xff0c;服务提供者出现故障服务的消费者没有…

【双曲几何-05 庞加莱模型】庞加来上半平面模型的几何属性

文章目录 一、说明二、双曲几何的上半平面模型三、距离问题四、弧长微分五、面积问题 一、说明 庞加莱圆盘模型是表示双曲几何的一种方法&#xff0c;对于大多数用途来说它都非常适合几何作图。然而&#xff0c;另一种模型&#xff0c;称为上半平面模型&#xff0c;使一些计算变…

全栈低代码:前后端业务需求实现100%覆盖!

工具背景&#xff1a; 织信低代码平台“组件设计器”功能专为对个性化定制页面需求较为强烈的用户准备的&#xff0c;该功能组件十分丰富和强大&#xff0c;还融合了AI智能&#xff0c;能够帮助用户0成本起步&#xff0c;平均花1-2个小时就能快速构建一套网站、APP、小程序。 …