本内核开源免费,欢迎大家下载使用学习,目前内核基础工作模块工作正常,有bug可以反馈给我。

内核源码下载链接:https://gitee.com/qidiyun/QDos

此例程是基于 STM32F407ZG 芯片的,STM32F103 的也差不多,自己移植,或者我有空了再放上来。

自制国产实时内核——vnRTOS 所有文档:

第 1 节 内核介绍

第 2 节 代码结构

第 3 节 让内核跑起来

第 4 节 线程的同步与互斥——资源

第 5 节 线程的异步通知

问:当前市场上有ucOS、freeRTOS、RT-thread 等内核、还有开源的linux等,为什么我们还要自己在开发一套内核呢?

ucOS 是商业收费的,freeRTOS 是一个免费的开源的内核,非常好用。RT-thread也是一款国产的实时内核,非常好用,强烈推荐大家使用 RT-thread。至于为什么还要自己开发一套实时内核呢?

一个是源于技术的追求。vnRTOS 是我大学的毕业设计,当时就觉得写内核是一件很 cool 的事情。接下来会有几篇文章详细讲解如何自己写内核,有兴趣的同学可以关注一波。

另外一个是技术的储备。我们写这个 vnRTOS 不是为了去取代 ucOS或者 FreeRTOS、RT-thread等。相反,我们觉得其它实时内核做得非常好,很值得我们去学习。而我们开发 vnRTOS 是为了将来有一天,当我们被别人卡脖子的时候(参考 某为  和 某米国的故事)。当我们没有内核可用时,我们有自己的技术,我们有自己的储备。

简而言之。。。这个 vnRTOS 就是个备胎。。。(不过备胎虽备胎,但是目前我们用这个 vnRTOS做了个物联网项目,目前还算稳定,比较适合一些微小型场合)

一、前言

嵌入式系统是用来控制或者监视机器、装置、工厂等大规模设备的系统。大多数嵌入式系统都是由单个程序实现整个控制逻辑,但也有些嵌入式系统还包含操作系统。

当前比较流行的嵌入式操作系统有:WinCE、嵌入式Linux、Vxwork、ucos II 等。但他们都有着各自的缺陷。WinCE、嵌入式Linux内核较为庞大,不适应一些低端的、资源较少的场合,而Vxwork、ucos II虽然都具有微内核这个特点,但版权费较高。

本文试图自己构建一个简单微小内核,以便在一些低端的、对系统资源要求严格、且成本不高的场合中使用。

本文设计的 vn Kernel采用的是基于优先级的时间片的任务调度思想。而vn 即 John von Neumann。

John von Neumann是一位伟大的数学家。他设计的“冯·诺依曼架构”是计算机架构的一个经典。正是有了这个架构,才有了今天的计算机,也才会有了现今嵌入式系统中的核心——微控制器。

本人在此表示深深的敬意。

二、内核框架:

2.1内核定义:

内核是操作系统最基本的部分。它是为众多应用程序提供对计算机硬件的安全访问的一部分软件,这种访问是有限的,并且内核决定一个程序在什么时候对某部分硬件操作多长时间。内核的分类可分为单内核和双内核以及微内核。

显然,一个微小的内核更适合嵌入式领域。因为在嵌入式系统中,ROM、RAM等资源都特别宝贵,尤其是在一些低端的领域,ROM都只有一百多K,RAM不到一百K,无法运行Win CE、嵌入式Linux这类操作系统。

然而,采用单个程序控制的思路,则在一些对实时性要求特别高的场合下行不通,因而,一个简单有效的微小内核对系统的性能、成本有着重要的影响。

2.2任务调度:

2.1优先级调度法:

Vn Kernel采用任务的思想,支持任意多个任务,这取决于系统的RAM等资源。同时,每个任务都有自身的优先级,总共有 64 个优先级可选。其中、最高优先级和最低两个优先级是内核专有的,用户可用到的优先级有 61 个。优先级数值越大,优先级越低。

实时内核的一个标准就是当前执行的任务是否可被抢占。本文设计的Vn Kernel属于可抢占式内核,高优先级的任务会立即抢占当前任务,获得CPU的执行权。

2.2 时间片调度法:

时间片调度法是一个经典的任务调度策略,许多操作系统都采用该调度法,例如windows、linux等。

Vn kernel可选的优先级有64个,不同优先级的任务采用优先级高抢占低优先级的调度方式,同时,允许有多个任务有着相同的优先级。

相同优先级的任务将会在内核中以双向链表的形式存在,并采用时间片调度方式。时间片长度可设置,考虑到系统频繁地切换任务会带来更多的资源消耗,故而时间片长度一般为10ms。

2.3 任务块链表:

内核会为每个任务创建一个任务块以便描述该任务。在内核中,所有准备就绪的任务块会以数组链表的形式存在。所谓数组链表,即在内核中,有一个指针数组,该数组的元素指向任务链表。所有任务将根据自身的优先级,添加到对应的链表中。其结构大致如下:

图中第一行为数组元素,A~G为任务编号。内核会根据任务的优先,将任务添加到相应的链表中,并且,会根据数组的下标,使数组的元素指向相应的任务链表头。

如此,内核便可以根据该指针数组访问到所有准备就绪的任务。例如上图有许多任务都准备就绪。内核会找到最高优先级的任务,即 2 所指向的任务 A。由于与任务A相同优先级的任务还有 B 和 C。所以内核将会采用时间片调度的方法,轮流执行这三个任务。

假设这个时候,系统创建了任务 X 该任务的优先级为 1 。那么此时,任务X将抢占当前任务,获得CPU执行权。此时内核只会执行任务X,除非有更高的优先级任务或者任务X放弃CPU执行权。

三、引导代码:

bootloader:

考虑到实际产品的升级问题,本文设计了一套基于STM32的bootloader。该bootloader支持启动内核、系统升级、烧写flash、参数设置、命令行等功能。对于STM32而言,复位后系统一般都会从0x8000000 处启动。

故而、bootloader存放在0x8000000处,预留大小为 10 K。参数存放地址为0x8003000 ~ 0x8003800 ,大小为 2 K。内核存在0x8003800处。

关于BootLoader的源码见这篇文章:BootLoader 源码链接

Bootlader实际演示结果如下图:

四、内核源码分析:

4.1任务块结构体:

/***************************************************************
定义一个task_tcb结构体,用于记录任务的相关信息
***************************************************************/
typedef struct task_tcb *PT_task_tcb;//#pragma pack(1)typedef struct task_tcb{INT32U            *task_sp;           //任务堆栈指针/* *以下两个元素看似必须,实则不需要。*那么系统是怎么根据TCB找到执行函数 和 参数指针的呢?*实际上,当我们为TCB初始化任务栈的时候,就已经将这两个参数*传递进入了。想想看, 任务栈是什么?所有寄存器,也就是*有 R0 (ARM架构函数的第一个参数存放在R0) PC*现在知道为什么TCB没有指明任务的执行函数,却能找到该函数了吧。*///void (*task_fun)(void *pd);            //任务的执行函数//void             *pdata;                 //任务处理函数的参数指针   INT32U          task_id;                //任务的ID,由系统统一分配#if TASK_IF_NAMEINT8U         task_name[TASK_NAME_LEN];   //任务的名字,由用户指定
#endifINT8U         task_prio;              //任务优先级INT8U            task_state;             //任务状态INT32U            task_runtime;           //任务的时间片长度INT32U            task_delaytime;         //任务如果需要等待,那么等待的时间长度/* 以下这个元素,用于就绪表中优先级相同的情况下 */PT_task_tcb       task_rdy_next;      PT_task_tcb     task_rdy_prev;
//  PT_task_tcb     task_waitlist;/* 以下两个仅用于构成双向链表,实际中作用不大 */PT_task_tcb     task_prev;          //指向上一个PT_task_tcb      task_next;          //指向下一个struct list_head list;               //链表,用于任务等待/* 每个任务都可以获取资源,下面是任务已经申请到的资源当任务被删除时,要释放资源*/struct os_resource  *task_resource;#if STACK_ADDINT32U *pdata;
#else//INT32U *pdata;
#endif
//  }T_task_tcb;

其中最重要的是任务堆栈指针。系统调用task_create 这个函数来创建一个任务,同时为该任务分配一块内存,用以存放任务块、此外,还将额外多分配一块内存,用以任务的堆栈。

4.2任务抢占:

当有一个更高优先级的任务发生时,内核将会触发一次软件中断。Cortex-M3架构中,提供一个可悬起中断——pendSV_handler。内核的实际任务切换工作是在该中断完成的。

内核首先将当前所有寄存器压栈。并找到最高优先级的任务的任务栈,并将里面的数据出栈。

对于Cortex-M3架构,其经典的任务栈操作汇编代码如下:


;********************************************************************
;                                                                   *
;                   第一次任务调度                                 *
;                                                                   *
;********************************************************************
;   状态分析:
;                                   *
;       当系统第一次调度任务之前,很显然,此时系统还不算存于多任务系统
;   可以看成裸机状态,那么此时系统是在运行那个任务呢?
;       显然,没有任务,可以理解为 bootloader 阶段。显然,当我们进入到
;   多任务阶段后,是不想系统再回到 之前的阶段。
;       而且,最重要的是,第一次任务切换时,触发 pendSV 中断时,系统会
;   自动将 xPSR,PC,LR,R12,R0~R3 压栈。
;       此时,系统进入中断之前不是存于多任务系统状态,那么就不需要再
;   将 R8 ~ R11 入栈。
;       之后,系统存于多任务系统状态,那么就需要对 R8 ~ R11 入栈
;
;**********************************************************************
;
;*********************************************************************
;   特权级-用户级 分析:
;
;       此外,当系统刚复位时,系统是处于 线程特权模式 请参考
;   Cortex-M3 权威指南.pdf  25页
;       此时,系统缺省值的 是 MSP ,主程序堆栈。但是,当我们运行任务
;   时,希望系统使用的是 PSP ,线程堆栈。
;   当系统进入异常时,处于 特权级handler 模式,使用的一定是 MSP
;       如何从 告诉系统要使用 psp 呢?
;   方法一:
;       在中断退出时,修改LR
;            ORR     LR, LR, #0x04
;   方法二:
;       请参考 Cortex-M3 权威指南.pdf  40页
;
;*********************************************************************
;   实现步骤
;       1.  设置 pendSV 中断的优先级
;       2.  设置 PSP 为 0 ,告诉系统,这是第一次调用
;       3.  触发中断
;********************************************************************
__cpu_start_shced
;设置 pendSV 中断优先级LDR     R0,     =NVIC_SYSPRI14     ;取中断优先级寄存器地址LDR     R1,     =NVIC_PENDSV_PRI   ;去中断优先级 0xffSTRB    R1,     [R0]                ;将 R1 写入到 [r0] 中;需要注意的是 strb 是写入一个字节;也就是写入 0xf 8位,因为中断优先级寄存器;都是 8 位的。;请参考 Cortex-M3 权威指南.pdf  404 页;设置 psp 为 0 。是否记得前面说过, msp psp 的区别?
;那么现在的问题是:当 stm32 执行到这里的时候,stm32 处于何种状态?
;显然,前面说过复位后是 特权级线程模式 ,那么可见的是 msp 。那是否意味着我们不能使用
; psp 呢? 不是,所谓可见是对于 push pop 操作而言的,那么复位后 psp 的值时多少呢?
;我不知道,没去查,但由于一般的程序,复位后都没有去更改stm的特权级,也没去修改 sp 是哪个。
;所以就没有用过 psp
;但是现在,我们希望系统第一次任务调度后,使用的是 psp 。而该 psp 指向当前任务的栈。为何?
;因为前面说过,当系统执行异常时,使用的一定是 msp 。这样就能把 系统栈 跟 任务栈很好的分开了。;设置 psp 为0 ,告诉系统,这是第一次调度,之后再任务切换函数里头,会去修改 psp 使其指向任务栈MOVS  R0,     #0MSR       PSP,    R0                  ;MSR 是特殊指令,用于操作特殊寄存器的;系统刚启动时,是不允许任务调度的,    if_task_run 为 0LDR      R0,     =if_task_run       ;将 if_task_run 的地址写入到 R0 中,这是一个伪指令MOVS   R1,     #1                  ;STRB   R1,     [R0]                ;将 R1 的值写入到地址为 R0 的内存中;触发一次 pendSV 中断,只要往中断控制寄存器中的第 28 位写入 1 ,即可触发一次软件中断LDR       R0,     =NVIC_INT_CTRL     ;把 NVIC_INT_CTRT 展开,可以得到; ldr r0, =0xE000ED04;如果等于号后面是一个数值,则表示 r0 = 0xE000ED04;如果等于号后面是一个变量名,或标号,则 取其地址LDR        R1,     =NVIC_PENDSVSETSTR     R1,     [R0];当执行到指令时,系统已经触发了 pendSV 中断了,那么系统应该跳到中断处理函数哪里了。
;理论上是如此,不过我们得保证中断时允许的啊,可能前面禁了中断后忘记打开了。
;开中断 关中断和开中断可以分别由指令CPSID i和CPSIE i实现,CPSIE  I;----------------------------------------------------------------------------------------------------LDR       R0, =task_shced_userBLX        R0;接下来是一个死循环,防止系统跑飞。不过系统一般是不会到这里的。
__cpu_errB      __cpu_err;************************************************************************
;                                                                       *
;                   任务切换函数  __cpu_shced                         *
;                                                                       *
;************************************************************************
;       前面已经说过了,只需简单地触发一次 pendSV 中断即可,真正的任务
;   切换在 中断处理函数 中完成
;********************************************************************
__cpu_shcedLDR      R0, =NVIC_INT_CTRLLDR      R1, =NVIC_PENDSVSETSTR     R1, [R0]BX      LR;************************************************************************
;                                                                       *
;                   中断退出调度函数    __cpu_int_shced                 *
;                                                                       *
;************************************************************************
;       当一个中断退出时, os_int_exit 要调用这个函数,确认是否需要从新调度
__cpu_int_shcedLDR     R0, =NVIC_INT_CTRLLDR     R1, =NVIC_PENDSVSETSTR     R1, [R0]BX      LR    ;************************************************************************
;                                                                       *
;                   pensSV 中断处理函数   __cpu_pendSV_handler            *
;                                                                       *
;************************************************************************
;   真正的任务切换函数
;   由于 CM3 在中断时会有一般的寄存器自动保存到任务堆栈里头、所以
;   OS_CPU_PendSVHandler    只需要保存 R4-R11 并调节堆栈指针即可
__cpu_pendSV_handlerCPSID   I                   ;任务切换需要关中断MRS       R0,     PSP         ;读取 psp 的值
;如果 psp 为 0 说明是第一次任务调度,则跳过下面的步骤CBZ       R0,     __cpu_pendSV_handler_nosave;if enable the FPUSUBS    R0, R0, #0X40VSTM    R0, {S16-S31};如果不是 0 ,那么保存 R4 ~ R11 到任务栈
;为什么要减去 0x20呢? 0x20 是32,也就是 8 个寄存器(一个寄存器4个字节)因为还要入栈
;数数看, R4 ~ R11 是不是 8 个寄存器SUBS    R0,     R0, #0X20   ;后缀 S 是要求更新 APSR 中的相关标志STM      R0,     {R4-R11}    ;将 {R4-R11} 压入到 地址为 R0 的内存中,注意不是压栈操作;所以要先把 R0 - 0x32 ,之后低地址是 r4 高地址是 r11;那么 R0 是多少呢? 显然,前面已经令其为 psp 了;第一次任务调度时,是不会执行这段的,但是当任务开始;调度后,psp 不再是0 ,而是当前任务的 任务栈
;修改任务的 TCB 的栈指针,请注意,TCB 结构体得第一个元素就是该任务栈的指针
;task_tcb_cur->task_ps = r0 (r0 是 psp 偏移后的值)LDR     R1,     =task_tcb_cur  ;当前任务 tcb 的地址LDR        R1,     [R1]            ;从地址中读出值;读出来的值时什么呢? 就是 tcb 的第一个元素;这是一个指针,任务栈指针STR     R0,     [R1]            ;将 R0 写入到 地址为 R1 的内存中;这一段比较难理解,我们可以转换成 C 语言来看;首先, task_tcb_cur 是一个指针,指向当前 任务 TCB 的内存地址;上面 3 句等价与下面 3 句; 1. r1 = &tcb; 2.  r1 = *(&tcb) = tcb; 3.    *(r1) = r0;将 2 代入到 3 式中; 4.    **(&tcb) = r0;也就是 *tcb = r0;前面已经所过了, tcb 是执行当前任务块得指针 假设当前任务块 是 TCB;那么:代入到 4 中   ; 5.    *(&TCB) = r0       也就是:;        TCB = r0       (这样写不恰当,应该是 TCB 的第一个元素);也就是 tcb->sp = r0 = psp__cpu_pendSV_handler_nosave
;调用用户的函数,不过一般都置空PUSH {R14}LDR        R0, =task_shced_userBLX        R0POP       {R14}
;修改 task_tcb_cur
;   task_tcb_cur = task_tcb_highLDR        R0,     =task_tcb_cur  ;   r0 = &task_tcb_curLDR      R1,     =task_tcb_high ;   r1 = &task_tcb_highLDR     R2,     [R1]            ;   r2 = *(&task_tcb_high)STR      R2,     [R0]            ;   *(&task_tcb_cur) = *(&task_tcb_high);  约掉 * 和 & 得到;    task_tcb_cur = task_tcb_high
;接下来要出之前压入的 r4 ~ r11
;当对于第一次调度而言,之前根本就没有压入过 r4 ~ r11 啊
;请参考 os_cpu.c 中的 task_init_ptop 任务栈初始化函数
;在任务第一次创建时,就已经初始化栈了,r4 ~ r11 被手工放入,所以要先出手工放入的值LDR      R0,     [R2]            ;   R0 = *(*(&task_tcb_high))  ;   这句相当于 r0 = task_tcb_high 指向的 TCB 的第一个元素;   也就是 r0 = task_tcb_high->task_spLDM      R0,     {R4-R11}        ;   从地址为 R0 的内存中读出内容ADDS    R0,     R0, #0X20       ;   知道为什么要加 20 不?栈是从高往低增长的;  我们出了 r4-r11 这 0x20 个字节后,要从新调整栈指针;if enable FPUVLDM    R0, {S16-S31}ADDS    R0, R0, #0X40MSR      PSP,    R0              ;   psp = r0ORR        LR,     LR, #0X04
;打开中断CPSIE  IBX     LR  END

4.3:双向链表的操作

内核会将相同优先级的任务放到同一个链表中。其双向链表的操作函数如下:

void add_tcb_list(struct task_tcb *head, struct task_tcb *ptcb)
{head->task_rdy_prev->task_rdy_next   = ptcb;ptcb->task_rdy_next              = head;ptcb->task_rdy_prev              = head->task_rdy_prev;head->task_rdy_prev                = ptcb;
}void del_tcb_list(struct task_tcb *ptcb)
{ptcb->task_rdy_next->task_rdy_prev = ptcb->task_rdy_prev;ptcb->task_rdy_prev->task_rdy_next = ptcb->task_rdy_next;
}

4.4:时间片调度算法

Cortex-M3内核提供一个系统时基定时器——Tick定时器。可以作为10ms定时功能。当发生中断时,内核会找到当前正在运行的任务,将其时间片长度减并判断其值,如果为0,在当前任务的链表中中找到下一个任务块,并执行任务切换,其代码如下:

if(task_tcb_cur->task_state == TASK_STATE_RUNING){task_tcb_cur->task_runtime --;     //时间片长度--if(task_tcb_cur->task_runtime == 0){task_tcb_cur->task_runtime = TASK_RUNTIME;//task_tab[task_tcb_cur->task_prio] = task_tcb_cur->task_rdy_next;flag = 1;}}

4.5:任务休眠

任务除了上面说的就绪态和运行态,任务有时候还需要休眠,让出CPU执行权,内核中的休眠函数的源码如下:

/*******************************************************************************任务休眠
*******************************************************************************/
void task_sleep(INT32U ms)
{INT32U cpu_sr;sys_interrupt_disable();tcb_tab_del(task_tcb_cur,task_tcb_cur->task_prio);        //删除task_tcb_cur->task_delaytime = ms;                      //休眠时间list_add((struct list_head *)&task_tcb_cur->list,&sleep_list);             //添加到休眠链表task_shced();                                          //任务调度/* 在这里更改状态 */task_tcb_cur->task_state = TASK_STATE_SLEEP;         //更改状态sys_interrupt_enable();
}

休眠的任务将会从就绪链表中删除,并加入到一个休眠链表中——sleep_list 。定时器周期产生中断,并对休眠链表中的所有任务进行休眠时间查询。如果该任务的休眠时间到了,则将任务从休眠链表中删除,并加入到就绪链表中,再做一次任务调度。

/* 休眠链表 */list_for_each_entry_safe_reverse(pos,n,&sleep_list,list,struct task_tcb){pos->task_delaytime --;if(pos->task_delaytime == 0){pos->task_state = TASK_STATE_READY;list_del((struct list_head *)&pos->list);                   //从休眠链表中删除task_tab_add(pos,pos->task_prio);      //加入到就绪链表中flag = 1;}}if(flag == 1){  __task_shced_timer();//不相等时才要做切换.if(task_tcb_high != task_tcb_cur){__cpu_int_shced();}}

五、Cortex-M3处理器:

5.1简介:

任何内核,都是要在具体的CPU上运行才有意义。本文设计的 vn kernel 是基于STM32F103ZET6这款芯片。

而该芯片采用的ARM公司的Cortex-M3内核架构。

Cortex-M3处理器采用ARMv7-M架构,它包括所有的16位Thumb指令集和基本的32位Thumb-2指令集架构,Cortex-M3处理器不能执行ARM指令集。

5.2工作模式:

Cortex-M3处理器支持2种工作模式:线程模式和处理模式。当处理器复位时,处理器处于 “特权级线程模式”,而发生异常时,处理将会进入“特权级handle”模式,异常返回时回到“特权级线程模式”。

然而,不管是“handle”还是“线程”模式,只要处理器处于“特权级”,那么处理器将使用的程序主堆栈——MSP。

除此之外,处理器还支持“用户线程级模式”,在该模式下,处理器将使用线程堆栈——PSP。显然,我们希望任务时处于“线程级模式”,内核是处于“特权级模式”。

六、实验结果:

本文编写了一个简单的测代码。基于 STM32F407 内核工程文件如下:

Main函数如下:

一开始时处理器相关的一些初始化工作,之后调用 core_init() ,对内核进行初始化。Debug_1() 则是创建两个任务 led1 led2 。分别控制两个LED灯闪烁。 core_start() 则是开始启动内核。

两个任务的代码大致相同,如下所示:

int main(void)
{ find_stack_direction();SystemInit();LED_Init();           //初始化LED端口/* 重定义向量表 */NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0000);core_init();debug_1();core_start();
/*-------------------------------------------------------------------*/
//  led1_task((void *)0);while(1){u8 t;t++;}
}

debug1函数内如下:

void debug_1(void)
{led1_id = task_create(led1_task, (void *)0, 50, 24, "led1");led2_id = task_create(led2_task, (void *)0, 50, 25, "led2"); res_id1 = task_create(debug_resource1, (void *)0, 50, 26, "debug1");res_id2 = task_create(debug_resource2, (void *)0, 50, 27, "debug2");
//  res_id3 = task_create(debug_resource3, (void *)0, 5, 24, "debug3");
}

void led1_task(void *p)
{volatile INT32U i;LED_Init();for(;;){  //task_change_prio(TASK_SELF, 25);for(i = 0; i < 2; i++){res_id5 = task_create(debug_resource1, (void *)0, 50, 26, "debug1");res_id6 = task_create(debug_resource2, (void *)0, 50, 27, "debug2");GPIO_WriteBit(GPIOE, GPIO_Pin_3, Bit_SET);task_sleep(100); //delay(1000); //task_sleep(100);GPIO_WriteBit(GPIOE, GPIO_Pin_3, Bit_RESET);task_sleep(200);   //task_sleep(100);task_delete(res_id5);task_delete(res_id6);}}
}void led2_task(void *p)
{int i;LED_Init();for(;;){//task_change_prio(TASK_SELF, 20);for(i = 0; i < 2; i++){res_id3 = task_create(debug_resource1, (void *)0, 50, 26, "debug1");res_id4 = task_create(debug_resource2, (void *)0, 50, 27, "debug2");GPIO_WriteBit(GPIOE, GPIO_Pin_4, Bit_SET);task_sleep(300);   //task_sleep(100);  //GPIO_WriteBit(GPIOE, GPIO_Pin_4, Bit_RESET);task_sleep(200);  //delay(1000); //task_delete(res_id3);task_delete(res_id4);}}
}

物联网实时内核 vnRTOS 免费开源相关推荐

  1. 使用Red Hat Enterprise Linux的实时内核

    运行实时内核并评估其对应用程序的潜力和性能优势是值得的. https://www.redhat.com/sysadmin/real-time-kernel 目录 什么是实时内核? 实时安装RHEL W ...

  2. PbootCMS是全新内核且永久开源免费的PHP企业网站开发建设管理系统

    简介: PbootCMS PbootCMS是全新内核且永久开源免费的PHP企业网站开发建设管理系统,是一套高效.简洁. 强悍的可免费商用的PHP CMS源码,能够满足各类企业网站开发建设的需要.系统采 ...

  3. 【免费开源】2020年独家全球疫情大数据接口(实时获取)

    武汉加油,中国加油!!!为了做点贡献,特此免费开源接口,供大家在各平台使用. 数据来自官方通报, 全国与各省通报数据可能存在差异.请勿滥用,后果自负. 一. 接口: https://www.maomi ...

  4. 实时内核(Core)和实时操作系统(RTOS)有何不同?

    大家都知道什么是实时操作系统,但是,大家熟知的实时操作系统,站在更广的角度来看,其实它就是一个实时内核. 一.实时内核和实时操作系统 一个实时内核是管理微处理器(MPU).微控制器(MCU)或数字信号 ...

  5. 国内四家物联网实时操作系统浅析

    国内四家物联网实时操作系统浅析 如果把云计算比作地球的大脑,那么物联网终端就是遍布全球的"神经末梢".如果把云计算比作地球的心脏,那么物联网终端就是遍布全球的"毛细血管& ...

  6. Odoo免费开源信息化平台满足所有企业应用场景与需求

    本文节选 Odoo亚太金牌服务·开源智造 老杨 所编写的<免费开源企业信息化快速开发平台·Odoo14开发指南(第四版)>第一章企业信息化首选开发平台Odoo介绍篇的节选内容,本书籍持续创 ...

  7. java分布式免费开源搜索引擎 Elasticsearch 详细学习笔记

    网页右边,向下滑有目录索引,可以根据标题跳转到你想看的内容 如果右边没有就找找左边 此文是学习尚硅谷Elasticsearch课程的笔记 Elasticsearch 全文检索引擎 Lucene 是 A ...

  8. 【企业信息化】第3集 免费开源ERP: Odoo 16 POS终端管理系统

    文章目录 前言 一.概览 二.硬件 三.使用功能 前言 世界排名第一的免费开源ERP: Odoo 16 POS终端管理系统.几分钟内完成设置,几秒内完成销售. 一.概览 Odoo POS 基于智能界面 ...

  9. 世界排名第一的永久免费开源ERP:Odoo生产制造管理功能概述

    本文节选自Odoo亚太金牌服务机构[开源智造]所编写的<ERP真的免费不花钱--Odoo应用指南>如需获取完整的知识内容,请至开源智造官网免费获取.感谢网友一键三连:点赞.转发.收藏,您的 ...

最新文章

  1. C语言中函数参数传递
  2. 以下哪个选项不是单例模式的优点_深度解密Python单例模式
  3. API标准化成为技术团队面临的最大挑战
  4. oracle升级后出现 ora-02055,分布式更新失败 0ra-02055错误 请各位指点(在线等待)...
  5. leetcode —— 938. 二叉搜索树的范围和
  6. 这才是男朋友该说的话^_^
  7. 最快的存储过程分页 50W
  8. uml学习之图书借阅简化用例图创建
  9. 【信息系统项目管理师】第10章 上篇-项目沟通管理 知识点详细整理
  10. PROC文件及文件夹创建
  11. JZOJ6734. 【2020.06.18省选模拟】航行
  12. java做度量衡换算器,磅换算计算器(公斤和磅在线换算器)
  13. 一切皆是映射:浅谈操作系统内核的缺页异常(Page Fault)
  14. 中国科技论文在线期刊模板出现了格式问题,怎么解决?
  15. 探访广东电子垃圾第一镇:家庭作坊饮鸩止渴---ESM
  16. 计算机网络atm功能,现代计算机网络原理4ATM交换技术.ppt
  17. SEO优化与SEM(竞价)区别在哪?
  18. Jsrpc学习——网易云热评加密函数逆向
  19. [附源码]Python计算机毕业设计高校教室管理系统
  20. ibm笔记本修复计算机开机按,IBM X280开机报错,按了Esc后正常解决方法

热门文章

  1. batch norm的作用
  2. 一次正交设计和组合设计之旅
  3. Scrapy中对xpath使用re
  4. “风雨如晦,鸡鸣不已。既见君子,云胡不喜。” 王者荣耀李白王昭君情侣头像故事...
  5. 技术博客1鸿蒙系统和安卓的区别有哪些
  6. php网站恶意注册表,利用注册表对付恶意网站篡改IE主页
  7. gitlab基本概念
  8. 重点来了,具有优质脂肪的坚果居然是减肥的好帮手!
  9. 四氨基钴酞菁[Co(TAPC)],cas203860-42-8,齐岳生物提供定制材料
  10. 哈希表解决冲突的两种方式