本內核開源免費,歡迎大家下載使用學習,目前內核基礎工作模塊工作正常,有bug可以反饋給我。
內核源碼下載鏈接:https://gitee.com/qidiyun/QDos
此例程是基於 STM32F407ZG 芯片的,STM32F103 的也差不多,自己移植,或者我有空了再放上來。
自制國產實時內核——vnRTOS 所有文檔:
問:當前市場上有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的執行權。
時間片調度法是一個經典的任務調度策略,許多操作系統都採用該調度法,例如windows、linux等。
Vn kernel可選的優先級有64個,不同優先級的任務採用優先級高搶佔低優先級的調度方式,同時,允許有多個任務有着相同的優先級。
相同優先級的任務將會在內核中以雙向鏈表的形式存在,並採用時間片調度方式。時間片長度可設置,考慮到系統頻繁地切換任務會帶來更多的資源消耗,故而時間片長度一般爲10ms。
內核會爲每個任務創建一個任務塊以便描述該任務。在內核中,所有準備就緒的任務塊會以數組鏈表的形式存在。所謂數組鏈表,即在內核中,有一個指針數組,該數組的元素指向任務鏈表。所有任務將根據自身的優先級,添加到對應的鏈表中。其結構大致如下:
圖中第一行爲數組元素,A~G爲任務編號。內核會根據任務的優先,將任務添加到相應的鏈表中,並且,會根據數組的下標,使數組的元素指向相應的任務鏈表頭。
如此,內核便可以根據該指針數組訪問到所有準備就緒的任務。例如上圖有許多任務都準備就緒。內核會找到最高優先級的任務,即 2 所指向的任務 A。由於與任務A相同優先級的任務還有 B 和 C。所以內核將會採用時間片調度的方法,輪流執行這三個任務。
假設這個時候,系統創建了任務 X 該任務的優先級爲 1 。那麼此時,任務X將搶佔當前任務,獲得CPU執行權。此時內核只會執行任務X,除非有更高的優先級任務或者任務X放棄CPU執行權。
考慮到實際產品的升級問題,本文設計了一套基於STM32的bootloader。該bootloader支持啓動內核、系統升級、燒寫flash、參數設置、命令行等功能。對於STM32而言,復位後系統一般都會從0x8000000 處啓動。
故而、bootloader存放在0x8000000處,預留大小爲 10 K。參數存放地址爲0x8003000 ~ 0x8003800 ,大小爲 2 K。內核存在0x8003800處。
關於BootLoader的源碼見這篇文章:BootLoader 源碼鏈接
Bootlader實際演示結果如下圖:
/***************************************************************
定義一個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_NAME
INT8U task_name[TASK_NAME_LEN]; //任務的名字,由用戶指定
#endif
INT8U 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_ADD
INT32U *pdata;
#else
//INT32U *pdata;
#endif
//
}T_task_tcb;
其中最重要的是任務堆棧指針。系統調用task_create 這個函數來創建一個任務,同時爲該任務分配一塊內存,用以存放任務塊、此外,還將額外多分配一塊內存,用以任務的堆棧。
當有一個更高優先級的任務發生時,內核將會觸發一次軟件中斷。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 ;去中斷優先級 0xff
STRB 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, #0
MSR PSP, R0 ;MSR 是特殊指令,用於操作特殊寄存器的
;系統剛啓動時,是不允許任務調度的, if_task_run 爲 0
LDR 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_PENDSVSET
STR R1, [R0]
;當執行到指令時,系統已經觸發了 pendSV 中斷了,那麼系統應該跳到中斷處理函數哪裏了。
;理論上是如此,不過我們得保證中斷時允許的啊,可能前面禁了中斷後忘記打開了。
;開中斷 關中斷和開中斷可以分別由指令CPSID i和CPSIE i實現,
CPSIE I
;----------------------------------------------------------------------------------------------------
LDR R0, =task_shced_user
BLX R0
;接下來是一個死循環,防止系統跑飛。不過系統一般是不會到這裏的。
__cpu_err
B __cpu_err
;************************************************************************
; *
; 任務切換函數 __cpu_shced *
; *
;************************************************************************
; 前面已經說過了,只需簡單地觸發一次 pendSV 中斷即可,真正的任務
; 切換在 中斷處理函數 中完成
;********************************************************************
__cpu_shced
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
;************************************************************************
; *
; 中斷退出調度函數 __cpu_int_shced *
; *
;************************************************************************
; 當一箇中斷退出時, os_int_exit 要調用這個函數,確認是否需要從新調度
__cpu_int_shced
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
;************************************************************************
; *
; pensSV 中斷處理函數 __cpu_pendSV_handler *
; *
;************************************************************************
; 真正的任務切換函數
; 由於 CM3 在中斷時會有一般的寄存器自動保存到任務堆棧裏頭、所以
; OS_CPU_PendSVHandler 只需要保存 R4-R11 並調節堆棧指針即可
__cpu_pendSV_handler
CPSID I ;任務切換需要關中斷
MRS R0, PSP ;讀取 psp 的值
;如果 psp 爲 0 說明是第一次任務調度,則跳過下面的步驟
CBZ R0, __cpu_pendSV_handler_nosave
;if enable the FPU
SUBS R0, R0, #0X40
VSTM 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_user
BLX R0
POP {R14}
;修改 task_tcb_cur
; task_tcb_cur = task_tcb_high
LDR R0, =task_tcb_cur ; r0 = &task_tcb_cur
LDR R1, =task_tcb_high ; r1 = &task_tcb_high
LDR 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_sp
LDM R0, {R4-R11} ; 從地址爲 R0 的內存中讀出內容
ADDS R0, R0, #0X20 ; 知道爲什麼要加 20 不?棧是從高往低增長的
; 我們出了 r4-r11 這 0x20 個字節後,要從新調整棧指針
;if enable FPU
VLDM R0, {S16-S31}
ADDS R0, R0, #0X40
MSR PSP, R0 ; psp = r0
ORR LR, LR, #0X04
;打開中斷
CPSIE I
BX LR
END
內核會將相同優先級的任務放到同一個鏈表中。其雙向鏈表的操作函數如下:
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;
}
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;
}
}
任務除了上面說的就緒態和運行態,任務有時候還需要休眠,讓出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處理器:
任何內核,都是要在具體的CPU上運行纔有意義。本文設計的 vn kernel 是基於STM32F103ZET6這款芯片。
而該芯片採用的ARM公司的Cortex-M3內核架構。
Cortex-M3處理器採用ARMv7-M架構,它包括所有的16位Thumb指令集和基本的32位Thumb-2指令集架構,Cortex-M3處理器不能執行ARM指令集。
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);
}
}
}