在SylixOS 中退出中断和内核都会系统调度任务,任务调度底层切换上下文,底层实现参考链接

在SyliOS默认优先级是0-255,数值越小优先级越高。在SylixOS 任务就绪队列是通过位图的方式去查找,如果当前优先级有任务则对应的位图位就会变成1。在查找到最后优先级后,从对应链表中取出任务控制块tcb,对比是否比当前任务优先级高,决定是否切换任务。

在arm中支持硬件实现前导零计数,所以能够快速的从位图中查找出来。

首先看几个重要的结构体

第一个结构体体是位图,由于32位并不能够满足0-255优先级的需求 ,所以这里使用了两级位图。

第二个结构体是对应优先级的队列。每个优先级都有一个对应的队列,如果要找此优先级的任务,就从这个队列中查找。

第三个结构体是上面两个结构的结合。将位图,链表结合在一起就实现了查找就绪任务时间复杂度为O(1)。

在内核退出的时候会调用_Schedule函数

/*********************************************************************************************************
** 函数名称: Schedule
** 功能描述: 内核退出时, 会调用此调度函数 (进入内核状态并关中断被调用)
** 输 入  : NONE
** 输 出  : 线程上下文返回值
** 全局变量:
** 调用模块:
*********************************************************************************************************/
INT  _Schedule (VOID)
{ULONG            ulCPUId;PLW_CLASS_CPU    pcpuCur;PLW_CLASS_TCB    ptcbCur;PLW_CLASS_TCB    ptcbCand;INT              iRetVal = ERROR_NONE;ulCPUId = LW_CPU_GET_CUR_ID();                                      /*  当前 CPUID                  */pcpuCur = LW_CPU_GET(ulCPUId);                                      /*  当前 CPU 控制块             */ptcbCur = pcpuCur->CPU_ptcbTCBCur;#if LW_CFG_SMP_EN > 0if (LW_UNLIKELY(ptcbCur->TCB_plineStatusReqHeader)) {               /*  请求当前任务改变状态        */if (__LW_STATUS_CHANGE_EN(ptcbCur, pcpuCur)) {                  /*  是否可以进行状态切换        */_ThreadStatusChangeCur(pcpuCur);                            /*  检查是否需要进行状态切换    */}}
#endif                                                                  /*  LW_CFG_SMP_EN               */ptcbCand = _SchedGetCand(pcpuCur, 1ul);                             /*  获得需要运行的线程          */if (ptcbCand != ptcbCur) {                                          /*  如果与当前运行的不同, 切换  */__LW_SCHEDULER_BUG_TRACE(ptcbCand);                             /*  调度器 BUG 检测             */#if LW_CFG_SMP_EN > 0                                                   /*  SMP 系统                    */__LW_SPINLOCK_BUG_TRACE(pcpuCur);
#endif                                                                  /*  LW_CFG_SMP_EN > 0           */pcpuCur->CPU_bIsIntSwitch = LW_FALSE;                           /*  非中断调度                  */pcpuCur->CPU_ptcbTCBHigh  = ptcbCand;/**  TASK CTX SAVE();*  SWITCH to SAFE stack if in SMP system;*  _SchedSwp();*  TASK CTX LOAD();*/archTaskCtxSwitch(pcpuCur);                                     /*  线程切换,并释放内核自旋锁   */
#if !defined(__SYLIXOS_ARM_ARCH_M__) || (LW_CFG_CORTEX_M_SVC_SWITCH > 0)LW_SPIN_KERN_LOCK_IGNIRQ();                                     /*  内核自旋锁重新加锁          */
#endif                                                                  /*  LW_CFG_CORTEX_M_SVC_SWITCH  */}
#if LW_CFG_SMP_EN > 0                                                   /*  SMP 系统                    */else {__LW_SMP_NOTIFY(ulCPUId);                                       /*  SMP 调度通知                */return  (iRetVal);}
#endif                                                                  /*  LW_CFG_SMP_EN               */LW_TCB_GET_CUR(ptcbCur);                                            /*  获得新的当前 TCB            */iRetVal = ptcbCur->TCB_iSchedRet;                                   /*  获得调度器信号的返回值      */ptcbCur->TCB_iSchedRet = ERROR_NONE;                                /*  清空                        */return  (iRetVal);
}

_Schedule函数首先判断当前任务的状态是否需要改变,如果需要切换任务状态。

然后调用_SchedGetCand函数查找需要运行的任务。

PLW_CLASS_TCB  _SchedGetCand (PLW_CLASS_CPU  pcpuCur, ULONG  ulCurMaxLock)
{if (!__COULD_SCHED(pcpuCur, ulCurMaxLock)) {                        /*  当前执行线程不能调度        */return  (pcpuCur->CPU_ptcbTCBCur);} else {                                                            /*  可以执行线程切换            */
#if LW_CFG_SMP_EN > 0LW_CPU_CLR_SCHED_IPI_PEND(pcpuCur);
#endif                                                                  /*  LW_CFG_SMP_EN > 0           */if (LW_CAND_ROT(pcpuCur)) {                                     /*  产生优先级卷绕              */_CandTableUpdate(pcpuCur);                                  /*  尝试更新候选表, 抢占调度    */}return  (LW_CAND_TCB(pcpuCur));}
}

此函数首先通过__COULD_SCHED判断是否能够调度任务,在中断嵌套,或者任务被锁住禁止调度等是不能够调度的。__COULD_SCHED函数这里就不详细展开。

如果是多核SMP 情况下会使用LW_CPU_CLR_SCHED_IPI_PEND 发送一个核间中断,通知其他核心进行调度。先不讨论核间中断的实现细节(不同处理器实现不同)。

_CandTableUpdate函数是实现找到最高优先级任务的函数,函数的内容如下:

VOID _CandTableUpdate (PLW_CLASS_CPU   pcpu)
{UINT8              ucPriority;REGISTER PLW_CLASS_TCB      ptcbCand;PLW_CLASS_PCBBMAP  ppcbbmap;BOOL               bNeedRotate = LW_FALSE;#if LW_CFG_SMP_EN > 0                                                   /*  SMP 多核                    */PLW_CLASS_TCB      ptcbNew;
#endifif (!LW_CPU_IS_ACTIVE(pcpu)) {                                      /*  CPU 必须为激活状态          */return;}ptcbCand = LW_CAND_TCB(pcpu);if (ptcbCand == LW_NULL) {                                          /*  当前没有候选线程            */_CandTableFill(pcpu);goto    __update_done;}ppcbbmap = _CandTableSeek(pcpu, &ucPriority);                       /*  当前就绪表中最高优先级      */if (ppcbbmap == LW_NULL) {LW_CAND_ROT(pcpu) = LW_FALSE;                                   /*  清除优先级卷绕标志          */return;}#if LW_CFG_SMP_EN > 0if (LW_CPU_ONLY_AFFINITY_GET(pcpu) && !ptcbCand->TCB_bCPULock) {    /*  强制运行亲和度任务          */bNeedRotate = LW_TRUE;                                          /*  当前普通任务需要让出 CPU    */} else
#endif                                                                  /*  LW_CFG_SMP_EN > 0           */{if (ptcbCand->TCB_usSchedCounter == 0) {                        /*  已经没有时间片了            */if (LW_PRIO_IS_HIGH_OR_EQU(ucPriority, ptcbCand->TCB_ucPriority)) {     /*  是否需要轮转                */bNeedRotate = LW_TRUE;}} else {if (LW_PRIO_IS_HIGH(ucPriority, ptcbCand->TCB_ucPriority)) {bNeedRotate = LW_TRUE;}}}if (bNeedRotate) {                                                  /*  存在更需要运行的线程        */_CandTableEmpty(pcpu);                                          /*  清空候选表                  */_CandTableResel(pcpu, ppcbbmap, ucPriority);                    /*  重新选择任务执行            */#if LW_CFG_SMP_EN > 0                                                   /*  SMP 多核                    */ptcbNew = LW_CAND_TCB(pcpu);if ((ptcbNew != ptcbCand) && !ptcbCand->TCB_bCPULock) {         /*  是否需要尝试标记其他 CPU    */_CandTableNotify(pcpu, ptcbCand);                           /*  通知其他 CPU 进行调度查看   */}
#endif                                                                  /*  LW_CFG_SMP_EN > 0           */}__update_done:LW_CAND_ROT(pcpu) = LW_FALSE;                                       /*  清除优先级卷绕标志          */
}

从上面的代码中可以看到在进行了一些情况判断后,调用了_CandTableSeek函数。_CandTableSeek函数实现的功能是在0-255优先级中那个有任务需要调度。如果存在有优先级比当前任务高的或者当前任务的时间片已经用完,那么从这个优先级对应的链表中选出任务。_CandTableResel函数就是实现选处任务的。

_CandTableSeek 是一个内联函数

static LW_INLINE PLW_CLASS_PCBBMAP  _CandTableSeek (PLW_CLASS_CPU  pcpu, UINT8 *pucPriority)
{
#if LW_CFG_SMP_EN > 0REGISTER UINT8  ucGlobal;if (_BitmapIsEmpty(LW_CPU_RDY_BMAP(pcpu))) {if (LW_CPU_ONLY_AFFINITY_GET(pcpu) ||_BitmapIsEmpty(LW_GLOBAL_RDY_BMAP())) {                     /*  就绪表为空                  */return  (LW_NULL);}*pucPriority = _BitmapHigh(LW_GLOBAL_RDY_BMAP());return  (LW_GLOBAL_RDY_PCBBMAP());                              /*  从全局就绪表选择            */} else {*pucPriority = _BitmapHigh(LW_CPU_RDY_BMAP(pcpu));              /*  本地就绪表最高优先级获取    */if (LW_CPU_ONLY_AFFINITY_GET(pcpu) || _BitmapIsEmpty(LW_GLOBAL_RDY_BMAP())) {return  (LW_CPU_RDY_PCBBMAP(pcpu));                         /*  选择本地就绪任务            */}ucGlobal = _BitmapHigh(LW_GLOBAL_RDY_BMAP());if (LW_PRIO_IS_HIGH_OR_EQU(*pucPriority, ucGlobal)) {           /*  同优先级, 优先执行 local    */return  (LW_CPU_RDY_PCBBMAP(pcpu));} else {*pucPriority = ucGlobal;return  (LW_GLOBAL_RDY_PCBBMAP());}}#elseif (_BitmapIsEmpty(LW_GLOBAL_RDY_BMAP())) {                         /*  就绪表中无任务              */return  (LW_NULL);} else {*pucPriority = _BitmapHigh(LW_GLOBAL_RDY_BMAP());return  (LW_GLOBAL_RDY_PCBBMAP());}
#endif                                                                  /*  LW_CFG_SMP_EN > 0           */
}

因为一个soc上可能有多个核心,所以存在全局还是本地。全局指的是全部soc,本地指的是当前soc。

查找最高优先级是通过_BitmapHigh函数实现的。

UINT8  _BitmapHigh (PLW_CLASS_BMAP  pbmap)
{UINT32  uiHigh = (UINT32)archFindLsb((INT)pbmap->BMAP_uiMap);UINT32  uiLow;if (uiHigh == 0) {return  (0);}uiHigh--;uiLow = (UINT32)archFindLsb((INT)pbmap->BMAP_uiSubMap[uiHigh]) - 1;return  ((UINT8)((uiHigh << 5) | uiLow));
}

此函数首先通过 archFindLsb查找位图的第一层。第一层的每一位对应的32位。查找到位图的第一层后,再次通过archFindLsb查找位图的第二层。最后将两个组合成最大优先级数值。

archFindLsb


FUNC_DEF(archFindLsb)MOV     W1 , #0SUB     W1 , W1 , W0AND     W0 , W1 , W0MOV     W1 , #32CLZ     W0 , W0SUB     W0 , W1 , W0RETFUNC_END()

在arm中有个CLZ指令,该指令由硬件实现计算最高位1前面的0个数。比如2前面有30个0,1前面有31个0。我们想找最低位为1的前面有多少个零,所以这里使用

    MOV     W1 , #0SUB     W1 , W1 , W0AND     W0 , W1 , W0

这三条命令让最低位1保留,其他全部变为0.加入W0传入进来为5,最后W0值是1.因为只保留最低位的1。

调用CLZ命令计算最低位1前面有多少个0.在用32减前面零的个数,就能快速计算出当前最低位1是在哪一位。因为在SylixOS中0-255 值越小优先级越大。所以这里使用的找到最低位为1的位置。

_BitmapHigh函数中位图第一层已经通过上面方式找到,位图的第二层

 uiLow = (UINT32)archFindLsb((INT)pbmap->BMAP_uiSubMap[uiHigh]) - 1;

可以看出也是调用archFindLsb函数查找到最低位1位置。

最后使用下面组合

((UINT8)((uiHigh << 5) | uiLow));

组合成当前最高优先级(也就是0-255当前存在最小值)。

此时查找最高优先级数值的任务就结束,继续回到_CandTableUpdate函数,下面需要从对应的优先级链表中找到任务控制块tcb,以下为_CandTableUpdate函数的部分

    ppcbbmap = _CandTableSeek(pcpu, &ucPriority);                       /*  当前就绪表中最高优先级      */if (ppcbbmap == LW_NULL) {LW_CAND_ROT(pcpu) = LW_FALSE;                                   /*  清除优先级卷绕标志          */return;}#if LW_CFG_SMP_EN > 0if (LW_CPU_ONLY_AFFINITY_GET(pcpu) && !ptcbCand->TCB_bCPULock) {    /*  强制运行亲和度任务          */bNeedRotate = LW_TRUE;                                          /*  当前普通任务需要让出 CPU    */} else
#endif                                                                  /*  LW_CFG_SMP_EN > 0           */{if (ptcbCand->TCB_usSchedCounter == 0) {                        /*  已经没有时间片了            */if (LW_PRIO_IS_HIGH_OR_EQU(ucPriority, ptcbCand->TCB_ucPriority)) {     /*  是否需要轮转                */bNeedRotate = LW_TRUE;}} else {if (LW_PRIO_IS_HIGH(ucPriority, ptcbCand->TCB_ucPriority)) {bNeedRotate = LW_TRUE;}}}if (bNeedRotate) {                                                  /*  存在更需要运行的线程        */_CandTableEmpty(pcpu);                                          /*  清空候选表                  */_CandTableResel(pcpu, ppcbbmap, ucPriority);                    /*  重新选择任务执行            */#if LW_CFG_SMP_EN > 0                                                   /*  SMP 多核                    */ptcbNew = LW_CAND_TCB(pcpu);if ((ptcbNew != ptcbCand) && !ptcbCand->TCB_bCPULock) {         /*  是否需要尝试标记其他 CPU    */_CandTableNotify(pcpu, ptcbCand);                           /*  通知其他 CPU 进行调度查看   */}
#endif                                                                  /*  LW_CFG_SMP_EN > 0           */}

_CandTableSeek执行完毕后,就要查看找出的优先级是否大于当前任务的,还有当前任务的时间片是否到,满足其中一个条件就进行任务的切换。调用_CandTableResel函数找对应的任务控制块tcb。

/*********************************************************************************************************
** 函数名称: _CandTableResel
** 功能描述: 选择一个最该执行线程放入候选表.
** 输 入  : pcpu          CPU 结构
**           ppcbbmap      优先级位图
**           ucPriority    需要运行任务的优先级
** 输 出  : NONE
** 全局变量:
** 调用模块:
*********************************************************************************************************/
static VOID  _CandTableResel (PLW_CLASS_CPU   pcpu, PLW_CLASS_PCBBMAP  ppcbbmap, UINT8  ucPriority)
{REGISTER PLW_CLASS_TCB      ptcb;REGISTER PLW_CLASS_PCB      ppcb;ptcb = _CandTableNext(ppcbbmap, ucPriority);                        /*  确定可以候选运行的线程      */ppcb = &ppcbbmap->PCBM_pcb[ucPriority];LW_CAND_TCB(pcpu) = ptcb;                                           /*  保存新的可执行线程          */ptcb->TCB_ulCPUId = LW_CPU_GET_ID(pcpu);                            /*  记录 CPU 号                 */ptcb->TCB_bIsCand = LW_TRUE;                                        /*  进入候选运行表              */_DelTCBFromReadyRing(ptcb, ppcb);                                   /*  从就绪环中退出              */if (_PcbIsEmpty(ppcb)) {__DEL_RDY_MAP(ptcb);                                            /*  从就绪表中删除              */}
}

在此函数中首先调用了  _CandTableNext函数,函数是根据优先级,从队列中取出一个任务,函数内容如下:

static PLW_CLASS_TCB  _CandTableNext (PLW_CLASS_PCBBMAP  ppcbbmap, UINT8  ucPriority)
{REGISTER PLW_CLASS_PCB      ppcb;REGISTER PLW_CLASS_TCB      ptcb;ppcb = &ppcbbmap->PCBM_pcb[ucPriority];ptcb = _LIST_ENTRY(ppcb->PCB_pringReadyHeader, LW_CLASS_TCB, TCB_ringReady);                                  /*  从就绪环中取出一个线程      */if (ptcb->TCB_ucSchedPolicy == LW_OPTION_SCHED_FIFO) {              /*  如果是 FIFO 直接运行        */return  (ptcb);} else if (ptcb->TCB_usSchedCounter == 0) {                         /*  缺少时间片                  */ptcb->TCB_usSchedCounter = ptcb->TCB_usSchedSlice;              /*  补充时间片                  */_list_ring_next(&ppcb->PCB_pringReadyHeader);                   /*  下一个                      */ptcb = _LIST_ENTRY(ppcb->PCB_pringReadyHeader, LW_CLASS_TCB, TCB_ringReady);}return  (ptcb);
}

从就绪链表中取出一个任务后,首先判断调度方式,如果是先进先出则执行执行,如果不是判断是否时间片用完,时间片用完补充时间片,然后从优先级链表中在取出其下一个来执行。

在_CandTableNext执行完成后,最高优先级任务的tcb已经找到,然后在_CandTableResel函数中调用

 LW_CAND_TCB(pcpu) = ptcb; 
#define LW_CAND_TCB(pcpu)   pcpu->CPU_cand.CAND_ptcbCand

此宏是将当前tcb加入到对用cpu的候选任务中。

然后在_CandTableResel函数中调用_DelTCBFromReadyRing函数,将任务从优先级队列中删除。最后调用如下函数

if (_PcbIsEmpty(ppcb)) {__DEL_RDY_MAP(ptcb);                                            /*  从就绪表中删除              */}

功能是判断当前优先级队列是否为空,如果为空就将位图的对应优先级的位变为0.

在_CandTableUpdate函数最后,根据情况是否触发核间中断通知其他核心调度。_CandTableUpdate函数执行完毕后

在_SchedGetCand函数执行如下函数

 if (LW_CAND_ROT(pcpuCur)) {                            /*  产生优先级卷绕              */_CandTableUpdate(pcpuCur);                          /*  尝试更新候选表, 抢占调度    */
}
return  (LW_CAND_TCB(pcpuCur));

最后返回了当前cpu的就绪任务,因为前面通过_CandTableResel函数查找到最高优先级任务加入到了对应cpu的就绪中。

如果找到的线程不是当前线程此时就会进行任务切换,切换函数如下

FUNC_DEF(archTaskCtxSwitch)LDR     X18, [X0]                                                   ;/*  获取当前 TCB 的 REG_CTX 地址*/SAVE_SMALL_REG_CTX                                                  ;/*  保存小寄存器上下文          */MOV     X19 , X0                                                    ;/*  X19 暂存 X0                 */#if LW_CFG_SMP_EN > 0    BL      _SchedSafeStack                                             ;/*  _SchedSafeStack();          */MOV     SP , X0                                                     ;/*  设置 SP                     */MOV     X0 , X19                                                    ;/*  恢复 X0                     */
#endifBL      _SchedSwp                                                   ;/*  _SchedSwp();                */LDR     X18, [X19]                                                  ;/*  获取当前 TCB 的 REG_CTX 地址*/LDR     X9 , [X18, #CTX_TYPE_OFFSET]                                ;/*  获得上下文类型              */CMP     X9 , #0B.NE    _RestoreSmallCtxRESTORE_BIG_REG_CTX                                                 ;/*  恢复大寄存器上下文          */LINE_LABEL(_RestoreSmallCtx)RESTORE_SMALL_REG_CTX                                               ;/*  恢复小寄存器上下文          */FUNC_END()

首先保存上下文,然后获得调度栈,跳转到_SchedSwp执行,_SchedSwp函数最主要是如下一段代码

    REGISTER PLW_CLASS_TCB      ptcbCur      = pcpuCur->CPU_ptcbTCBCur;REGISTER PLW_CLASS_TCB      ptcbHigh     = pcpuCur->CPU_ptcbTCBHigh;REGISTER LW_OBJECT_HANDLE   ulCurId      = ptcbCur->TCB_ulId;REGISTER LW_OBJECT_HANDLE   ulHighId     = ptcbHigh->TCB_ulId;BOOL               bIsIntSwitch = pcpuCur->CPU_bIsIntSwitch;
............
省略
...........
pcpuCur->CPU_ptcbTCBCur = ptcbHigh;                                 /*  切换任务                    */

将pcpuCur->CPU_ptcbTCBHigh保存的任务保存到pcpuCur->CPU_ptcbTCBCur。在前面的函数将查找到高优先级的任务保存到了  pcpuCur->CPU_ptcbTCBHigh中。

执行完_SchedSwp 函数后,就是进行任务的切换。

SylixOS 任务调度源代码分析相关推荐

  1. Quartz.NET 架构与源代码分析系列 part 1 :Quartz.NET 入门

    概述 作业调度的目标在于按照预先确定的时间和指定的顺序来确保高效的数据处理流程,从而最大限度的使用系统资源.批处理流程是一种在无需最终用户干预的方式下在后台通过顺序方式运行的操作. Windows X ...

  2. Android系统默认Home应用程序(Launcher)的启动过程源代码分析

    在前面一篇文章中,我们分析了Android系统在启动时安装应用程序的过程,这些应用程序安装好之后,还需要有一个Home应用程序来负责把它们在桌面上展示出来,在Android系统中,这个默认的Home应 ...

  3. 《LINUX3.0内核源代码分析》第一章:内存寻址

    https://blog.csdn.net/ekenlinbing/article/details/7613334 摘要:本章主要介绍了LINUX3.0内存寻址方面的内容,重点对follow_page ...

  4. Scrapy源代码分析-经常使用的爬虫类-CrawlSpider(三)

    CrawlSpider classscrapy.contrib.spiders.CrawlSpider 爬取一般站点经常使用的spider.其定义了一些规则(rule)来提供跟进link的方便的机制. ...

  5. Android 中View的绘制机制源代码分析 三

    到眼下为止,measure过程已经解说完了,今天開始我们就来学习layout过程.只是在学习layout过程之前.大家有没有发现我换了编辑器,哈哈.最终下定决心从Html编辑器切换为markdown编 ...

  6. Android应用程序进程启动过程的源代码分析(1)

    Android应用程序框架层创建的应用程序进程具有两个特点,一是进程的入口函数是ActivityThread.main,二是进程天然支持Binder进程间通信机制:这两个特点都是在进程的初始化过程中实 ...

  7. AFNetworking 源代码分析

    关于其他 AFNetworking 源代码分析的其他文章: AFNetworking 概述(一) AFNetworking 的核心 AFURLSessionManager(二) 处理请求和响应 AFU ...

  8. Hadoop源代码分析 - MapReduce(转载)

    1. Hadoop源代码分析(MapReduce概论) http://caibinbupt.javaeye.com/blog/336467

  9. RTMPdump(libRTMP) 源代码分析 3: AMF编码

    2019独角兽企业重金招聘Python工程师标准>>> 注:此前写了一些列的分析RTMPdump(libRTMP)源代码的文章,在此列一个列表: RTMPdump 源代码分析 1: ...

  10. Android系统默认Home应用程序(Launcher)的启动过程源代码分析(3)

    Step 13.  ActivityStack.startActivityLocked 这个函数定义在frameworks/base/services/java/com/android/server/ ...

最新文章

  1. Android读写XML(上)
  2. Jupiter:Facebook的高性能job-matching服务
  3. 数据库查找出list数据,进行处理
  4. 漫画 | 这样的男朋友,让我分分钟想剖腹自尽!
  5. 聊聊统一身份认证服务
  6. 领域驱动设计(DDD)相关架构介绍与演变过程分析(图文详解)
  7. c# 取余数 浮点数_浮点数精度问题透析:小数计算不准确+浮点数精度丢失根源
  8. 2018自然语言处理与机器学习论文发表统计
  9. 霍尔传感器的工作原理、分类及应用
  10. 【iOS篇】在iPhone上安装描述文件
  11. 面经手册 · 第1篇《认知自己的技术栈盲区》
  12. HTTP 错误 404.5 - Not Found
  13. M4A音频格式是如何转成MP3格式的?
  14. 拼多多微信登陆服务器请求失败,拼多多客服网页无法登陆/卡死,怎么办?
  15. 内网渗透-横向渗透2
  16. 广播(Broadcast)的发送与接收
  17. java完成经典坦克大战项目源码
  18. 某医药公司北亚数据恢复报告书
  19. 三角形面积,周长的计算(C++)
  20. BMS(电池管理系统)第11课—动力电池系统安全

热门文章

  1. Visual Assist X V10.4.1626.0 不错的软件,让VC++也能自动提示
  2. python发布代码图片_gitpython模块与代码发布项目流程图
  3. 一条SQL语句查询出成绩名次 排名 (转)
  4. IAR编译仿真时提示“__vector_table symbol not found”
  5. 查找指定时间段内的文件
  6. MongoDB实战-面向文档的数据(找到最合适的数据建模方式)
  7. 轻松搞定RabbitMQ(四)——发布/订阅
  8. ntldr is missing什么意思应该如何解决
  9. tortoiseHg查看后一个版本和parent版本的不同
  10. Hystrix断路器---SpringCloud(四)