[rCore學習筆記 024]多道程式與協作式調度

来源:https://www.cnblogs.com/chenhan-winddevil/p/18352689
-Advertisement-
Play Games

寫在前面 本隨筆是非常菜的菜雞寫的。如有問題請及時提出。 可以聯繫:[email protected] GitHhub:https://github.com/WindDevil (目前啥也沒有 本節重點 主要是對 任務 的概念進行進一步擴展和延伸:形成 任務運行狀態:任務從開始到結束執行過程中所處的 ...


寫在前面

本隨筆是非常菜的菜雞寫的。如有問題請及時提出。

可以聯繫:[email protected]

GitHhub:https://github.com/WindDevil (目前啥也沒有

本節重點

主要是對 任務 的概念進行進一步擴展和延伸:形成

  • 任務運行狀態:任務從開始到結束執行過程中所處的不同運行狀態:未初始化、準備執行、正在執行、已退出
  • 任務控制塊:管理程式的執行過程的任務上下文,控製程序的執行與暫停
  • 任務相關係統調用:應用程式和操作系統之間的介面,用於程式主動暫停 sys_yield 和主動退出 sys_exit

這裡主要看具體實現,這些概念之前學習RTOS的時候使用是會使用了,但是具體怎麼實現還不好說.

多道程式背景與 yield 系統調用

儘管 CPU 可以一直在跑應用了,但是其利用率仍有上升的空間.

隨著應用需求的不斷複雜,有的時候會在內核的監督下訪問一些外設,它們也是電腦系統的另一個非常重要的組成部分,即 輸入/輸出 (I/O, Input/Output) .

CPU 會把 I/O 請求傳遞給外設,待外設處理完畢之後,CPU 便可以從外設讀到其發出的 I/O 請求的處理結果.

我們暫時考慮 CPU 只能 單向地 通過讀取外設提供的寄存器信息來獲取外設處理 I/O 的完成狀態。

多道程式的思想在於:

  1. 內核同時管理多個應用。如果外設處理 I/O 的時間足夠長,那我們可以先進行任務切換去執行其他應用
  2. 在某次切換回來之後,應用再次讀取設備寄存器,發現 I/O 請求已經處理完畢了,那麼就可以根據返回的 I/O 結果繼續向下執行了

這樣的話,只要同時存在的 應用足夠多 ,就能 一定程度 上隱藏 I/O 外設處理相對於 CPU 的延遲,保證 CPU 不必浪費時間在等待外設上,而是幾乎一直在進行計算。

這種任務切換,是讓應用 主動 調用 sys_yield 系統調用來實現的,這意味著應用主動交出 CPU 的使用權給其他應用。

這一段的描述相當是一種多任務的輪詢,但是在我的腦海中, 外部中斷 還是比多任務輪詢要好得多的. 但是怎麼合理地 利用 外部中斷提高實時性,就是一個問題.

至於主動調用sys_yield就是一件很難的事情,也就是為啥叫做 協作式 , 就是系統的性能要依賴程式員在設計APP的時候釋放CPU.(我自己都想拉滿CPU,誰想管你死活捏)

這裡提到了 一種多道程式執行的典型情況 :

這張圖很好解釋:

  1. 這張圖的 橫軸 是時間軸
  2. 這張圖的 縱軸 是運行實體(任務和IO硬體)
  3. 可以看到是有三個運行實體
    1. I/O Device : 這個是IO硬體
    2. I/O Task : 這個是請求IO硬體的任務
    3. Other Task : 這個是不請求IO硬體的其它任務
  4. 可以看到最開始是 IO Task 在運行.
  5. I/O Start yield 時刻,IO Task 請求了IO硬體,然後釋放了CPU.
  6. Other Task 接手CPU,同時 IO Device 繼續處理硬體上的問題.
  7. 一直執行到 Not Complete yileld again 時段的開頭,Other Task 執行完畢,把CPU釋放.
  8. IO Task 接手之後檢查IO硬體狀態,仍然沒有處理完畢.
  9. Not Complete yileld again 時段的結尾, IO Task 釋放CPU.
  10. Other Task 再次接手CPU,同時 IO Device 繼續處理硬體上的問題.
  11. Other Task 執行期間,發生了 I/O Complete 時刻,但是此時軟體感知不到.
  12. Continue 時刻, ,Other Task 執行完畢,把CPU釋放.
  13. IO Task 接手之後檢查IO硬體狀態,處理完畢,因此繼續執行.

上面我們是通過“避免無謂的外設等待來提高 CPU 利用率”這一切入點來引入 sys_yield 。但其實調用 sys_yield 不一定與外設有關 。隨著內核功能的逐漸複雜,我們還會遇到 其他需要等待的事件 ,我們都可以立即調用 sys_yield 來避免等待過程造成的浪費。

sys_yield 的缺點

這一部分和我最開始考慮的關於實時性問題的思考是有一定關聯的.

當應用調用它主動交出 CPU 使用權之後,它下一次再被允許使用 CPU 的時間點與內核的調度策略與當前的總體應用執行情況有關,很有可能遠遠遲於該應用等待的事件(如外設處理完請求)達成的時間點。這就會造成該應用的響應延遲不穩定或者很長。比如,設想一下,敲擊鍵盤之後隔了數分鐘之後才能在屏幕上看到字元,這已經超出了人類所能忍受的範疇。但也請不要擔心,我們後面會有更加優雅的解決方案。

sys_yield 的標準介面

思考我們之前提到的兩種syscall.

內核層 實現的:

//os/syscall/mod

const SYSCALL_WRITE: usize = 64;
const SYSCALL_EXIT: usize = 93;

mod fs;
mod process;

use fs::*;
use process::*;

/// handle syscall exception with `syscall_id` and other arguments
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
    match syscall_id {
        SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
        SYSCALL_EXIT => sys_exit(args[0] as i32),
        _ => panic!("Unsupported syscall_id: {}", syscall_id),
    }
}

用戶層 實現的:

//user/syscall

use core::arch::asm;

const SYSCALL_WRITE: usize = 64;
const SYSCALL_EXIT: usize = 93;

fn syscall(id: usize, args: [usize; 3]) -> isize {
    let mut ret: isize;
    unsafe {
        asm!(
            "ecall",
            inlateout("x10") args[0] => ret,
            in("x11") args[1],
            in("x12") args[2],
            in("x17") id
        );
    }
    ret
}

pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
    syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
}

pub fn sys_exit(exit_code: i32) -> isize {
    syscall(SYSCALL_EXIT, [exit_code as usize, 0, 0])
}

這裡如果能理解到這裡的同名的syscall,sys_write,sys_exit不是同一個函數,說明才 理解到位 .

現在要 繼續實現 一個 系統調用 sys_yield.

於是要在 用戶層 實現介面:

// user/src/syscall.rs

pub fn sys_yield() -> isize {
    syscall(SYSCALL_YIELD, [0, 0, 0])
}

// user/src/lib.rs

pub fn yield_() -> isize { sys_yield() }

SYSCALL_YIELD同樣是一個 需要定義 的常量.

這裡有個小問題,由於yield是rust的 關鍵字 ,因此定義函數名字的時候 增加了一個_ .

於是在 內核層syscall裡邊也需要增加一個判別,現在我只寫成偽代碼,因為具體我也 不知道 參數怎麼填寫:

pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
    match syscall_id {
		// 這裡是偽代碼
	    SYSCALL_YIELD => sys_yield(...)
	    // 這裡是偽代碼
        SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
        SYSCALL_EXIT => sys_exit(args[0] as i32),
        _ => panic!("Unsupported syscall_id: {}", syscall_id),
    }
}

任務控制塊與任務運行狀態

思考上一章實現的AppManager,它包含了三部分:

  1. 應用的 數量 .
  2. 當前 運行應用.
  3. 應用的 入口地址 .

但是考慮當前的任務的狀態,可能 不是 簡單地如上圖兩任務的情況一樣,而是存在更多的任務和更複雜的情景.

想到我們本節 開頭 時候所說,要建立一個 任務運行狀態 的概念,把任務歸類為如下幾種狀態:

  1. 未初始化
  2. 準備執行
  3. 正在執行
  4. 已退出

因此可以使用rust構建這樣一個結構體:

// os/src/task/task.rs

#[derive(Copy, Clone, PartialEq)]
pub enum TaskStatus {
    UnInit, // 未初始化
    Ready, // 準備運行
    Running, // 正在運行
    Exited, // 已退出
}

#[derive]這個註解有點類似於 Kotlin ,可以讓 編譯器自動 幫你實現一些方法:

  • 實現了 Clone Trait 之後就可以調用 clone 函數完成拷貝;
  • 實現了 PartialEq Trait 之後就可以使用 == 運算符比較該類型的兩個實例,從邏輯上說只有 兩個相等的應用執行狀態才會被判為相等,而事實上也確實如此。
  • Copy 是一個標記 Trait,決定該類型在按值傳參/賦值的時候採用移動語義還是複製語義。

回想起上一節提到的TaskContext,我們的 任務控制塊 中需要保存的兩部分也就知道了:

  1. TaskContext保存任務上下文
  2. TaskStatus保存任務狀態

因此用rust構建這樣一個結構體:

// os/src/task/task.rs

#[derive(Copy, Clone)]
pub struct TaskControlBlock {
    pub task_status: TaskStatus,
    pub task_cx: TaskContext,
}

任務管理器

那麼有了TaskControlBlock,就可以實現一個任務管理器.

任務管理器需要管理多個任務,於是就需要知道:

  1. app 總數
  2. 當前 的任務
  3. 每個任務的 控制塊
    1. 任務 狀態
    2. 任務 上下文

這裡使用了 常量和變數分離的方法 來實現它.

// os/src/task/mod.rs

pub struct TaskManager {
    num_app: usize,
    inner: UPSafeCell<TaskManagerInner>,
}

struct TaskManagerInner {
    tasks: [TaskControlBlock; MAX_APP_NUM],
    current_task: usize,
}

這是因為num_app是常量不需要變化,而inner是變數,需要用UPSafeCell,保證其 內部可變性單核時 安全的借用能力.

這裡在官方文檔里提到了:

  1. 在第二章的AppManager是可以通過current_app推測上/下任務 的.
  2. 但是在TaskManger里的TaskManagerInnercurrent_task只能 感知當前任務.

TaskManager創建全局實例TASK_MANAGER,仍然使用 懶初始化 的方法:

// os/src/task/mod.rs

lazy_static! {
    pub static ref TASK_MANAGER: TaskManager = {
        let num_app = get_num_app();
        let mut tasks = [
            TaskControlBlock {
                task_cx: TaskContext::zero_init(),
                task_status: TaskStatus::UnInit
            };
            MAX_APP_NUM
        ];
        for i in 0..num_app {
            tasks[i].task_cx = TaskContext::goto_restore(init_app_cx(i));
            tasks[i].task_status = TaskStatus::Ready;
        }
        TaskManager {
            num_app,
            inner: unsafe { UPSafeCell::new(TaskManagerInner {
                tasks,
                current_task: 0,
            })},
        }
    };
}

這個初始化順序是:

  1. 使用 上一節實現get_num_app來獲取任務數量
  2. 創建一個TaskControlBlock數組 ,大小為 設定好的 MAX_APP_NUM.
  3. 然後通過 上一節實現的 init_app_cx 來獲取每個 已經載入到記憶體 的任務上下文.
  4. 把所有的任務都 初始化Ready 狀態.
  5. 然後用 匿名函數 的方式得到的 task 和初始化為0的current_task創建一個匿名 TaskManagerInner,隨後包裹在 UPSafeCell 之中,和num_app一起創建一個TaskManager,傳給TASK_MANAGER.

實現 sys_yield 和 sys_exit 系統調用

類似於上一章實現的 內核層syscall函數中會根據 函數代碼 調用函數.

我們需要理解到的一點就是:

  1. 應用層syscall 函數只是使用 ecall 觸發Trap.
  2. 內核層syscall函數才是真的具體實現.

我們現在講的是 內核層具體實現 調用的函數,其作用是在syscall中作為一個 分支 :

// os/src/syscall/process.rs

use crate::task::suspend_current_and_run_next;

pub fn sys_yield() -> isize {
    suspend_current_and_run_next();
    0
}

這個是sys_yield,用於暫停當前的應用並切換到下個應用.

看它的具體實現實際上是 抽象化suspend_current_and_run_next介面,使得介面名稱 一致 .

這時候要考慮我們上一章實現的sys_exit:

//! App management syscalls
use crate::loader::run_next_app;
use crate::println;

/// task exits and submit an exit code
pub fn sys_exit(exit_code: i32) -> ! {
    println!("[kernel] Application exited with code {}", exit_code);
    run_next_app()
}

列印了LOG之後,使用run_next_app切換到下一個APP.

那麼考慮到現在run_next_app已經不適合於當前的有 任務調度 的系統,所以也要對sys_exit的具體實現進行修改.

// os/src/syscall/process.rs

use crate::task::exit_current_and_run_next;

pub fn sys_exit(exit_code: i32) -> ! {
    println!("[kernel] Application exited with code {}", exit_code);
    exit_current_and_run_next();
    panic!("Unreachable in sys_exit!");
}

可以看到現在的具體實現是 抽象化exit_current_and_run_next介面,使得介面名稱 一致 .

接下來我們只需要 具體實現 ,剛剛提到的兩個介面就行了:

// os/src/task/mod.rs

pub fn suspend_current_and_run_next() {
    mark_current_suspended();
    run_next_task();
}

pub fn exit_current_and_run_next() {
    mark_current_exited();
    run_next_task();
}

這裡摘抄出具體實現,但是具體實現中還是有三個函數 有待實現 :

  1. mark_current_suspended
  2. mark_current_exited
  3. run_next_task

他們的具體實現要和上一章和上一節的實現對比:

  1. 上一章: 載入應用 然後 修改程式指針 直接開始運行 .
  2. 上一節:直接 修改程式指針 直接開始運行.

這一章的實現是不同的,是通過 修改用戶的狀態 ,解決.

// os/src/task/mod.rs

fn mark_current_suspended() {
    TASK_MANAGER.mark_current_suspended();
}

fn mark_current_exited() {
    TASK_MANAGER.mark_current_exited();
}

impl TaskManager {
    fn mark_current_suspended(&self) {
        let mut inner = self.inner.borrow_mut();
        let current = inner.current_task;
        inner.tasks[current].task_status = TaskStatus::Ready;
    }

    fn mark_current_exited(&self) {
        let mut inner = self.inner.borrow_mut();
        let current = inner.current_task;
        inner.tasks[current].task_status = TaskStatus::Exited;
    }
}

然後再通過run_next_task來(根據狀態) 決定(可以叫調度嗎?對的...不對...對的對的...不對) 下一步要運行哪個Task.

// os/src/task/mod.rs

fn run_next_task() {
    TASK_MANAGER.run_next_task();
}

impl TaskManager {
    fn run_next_task(&self) {
        if let Some(next) = self.find_next_task() {
            let mut inner = self.inner.exclusive_access();
            let current = inner.current_task;
            inner.tasks[next].task_status = TaskStatus::Running;
            inner.current_task = next;
            let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
            let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
            drop(inner);
            // before this, we should drop local variables that must be dropped manually
            unsafe {
                __switch(
                    current_task_cx_ptr,
                    next_task_cx_ptr,
                );
            }
            // go back to user mode
        } else {
            panic!("All applications completed!");
        }
    }
}

這裡也是分為兩部分:

  1. run_next_task是對TASK_MANAGER.run_next_task();的封裝.
  2. TaskManager結構體的run_next_task方法的實現.
    1. 首先就是if let這種模式匹配寫法,最開始沒有掌握rust的開發技術,因此不懂.
      1. 這時候查閱Rust聖經.關於if let的部分.
        1. 當只需要進行一次匹配的時候就可以使用這個方法.
        2. 使用匹配是為瞭解決用簡單的 == 不能解決 複雜類型 匹配的情況.
        3. 使用if let而不是match是為瞭解決只有None和非None兩種情況的簡單寫法.
      2. 查閱Rust聖經.關於Some的部分.
        1. Option枚舉有兩種可能
          1. Some代表有值,Some包裹的內容就是它的值
            1. 一個在 定義 枚舉類型的時候是 Some(T),T代表的是類型.Some(i32)就代表可以存儲i32類型的值.
            2. 在實例的時候Some(T)可以被實例化Some(3),就代表這個值存在且值為3.
          2. None代表沒值
      3. 因此這一段的結果意思是:
        1. 如果self.find_next_task()的結果不是None,那麼對應的返回值應該是Some(next).
        2. 下麵的邏輯里的next就是返回的Some()里包裹的next.代表 下一個任務的任務號 .
    2. 隨後獲取TaskManager.inner的單線程可變借用.
    3. 從上一步的結果中獲取 當前任務.
    4. 下一個任務 的狀態改為 運行中 .
    5. 把當前任務號改為 剛剛獲取到的下一個任務號 .
    6. 分別獲取 當前和下一個 任務上下文.
    7. 主動釋放獲取到的TaskManager.inner.
      1. 因為如果不去主動釋放要等函數運行結束才能繼續訪問這個TaskManager.inner里的內容.
      2. __switch需要操作TaskManager.inner里的task.task_cx的內容.
    8. 使用 上一節實現的 __switch 完成任務棧切換,如果已經忘了可以回去看看.

可以看到find_next_task是一個重要的方法,它的實現是這樣的:

// os/src/task/mod.rs

impl TaskManager {
    fn find_next_task(&self) -> Option<usize> {
        let inner = self.inner.exclusive_access();
        let current = inner.current_task;
        (current + 1..current + self.num_app + 1)
            .map(|id| id % self.num_app)
            .find(|id| {
                inner.tasks[*id].task_status == TaskStatus::Ready
            })
    }
}

它在獲取TaskManager.inner的單線程可變借用之後對current_task為開頭( 不包含它本身 )把整個數組看成一個 環形隊列 然後逐個去 查詢狀態 , 直到找到 第一個 狀態為準備的任務.

這裡關於Rust語言,每次我們遇到不會了的,不是光把它搞懂,還要把它上一層的偏概念性的東西搞懂.

這裡用到的就是 閉包迭代器 的知識:

  1. 迭代器跟 for 迴圈頗為相似,都是去遍歷一個集合,但是實際上它們存在不小的差別,其中最主要的差別就是:是否通過索引來訪問集合
    1. Iterator Trait 的 map 方法: Rust中的迭代器(Iterator)有一個map方法,它接收一個閉包(closure),並將迭代器中的每個元素傳遞給這個閉包。map方法會生成一個新的迭代器,其中的元素是閉包返回的結果。
    2. 迭代器有一個find方法,它接收一個閉包作為參數。該閉包定義了要查找的條件,當迭代器中的元素滿足這個條件時,find方法就會返回一個Option類型的結果,其中包含找到的第一個匹配項或者None如果沒有任何元素滿足條件。
  2. 閉包一種匿名函數,它可以賦值給變數也可以作為參數傳遞給其它函數,不同於函數的是,它允許捕獲調用者作用域中的值.
    1. 有點像是某種C里的 函數巨集 ,用 do...while封裝起來的這種.因此可以偷取別的作用域的變數來用.

這張圖太好了:

第一次進入用戶態

回想上一章,我們使用run_next_app調用了__restore調用sret回到用戶態.

目前我們要第一次進入用戶態應該也需要sret才可以.

但是思考一下上一章我們學到的__switch的實現,顯然它是 不改變 特權級的.

因此第一次進入用戶態還是要依賴__restore.

為了使用__restore則需要構建Trap上下文,把 上一節 實現的init_app_cx,移動到loader.rs:

// os/src/loader.rs

pub fn init_app_cx(app_id: usize) -> usize {
    KERNEL_STACK[app_id].push_context(
        TrapContext::app_init_context(get_base_i(app_id), USER_STACK[app_id].get_sp()),
    )
}

再給TaskContext構造一個 構建第一次執行任務的上下文 的方法:

// os/src/task/context.rs

impl TaskContext {
    pub fn goto_restore(kstack_ptr: usize) -> Self {
        extern "C" { fn __restore(); }
        Self {
            ra: __restore as usize,
            sp: kstack_ptr,
            s: [0; 12],
        }
    }
}

在這個操作之中,

  1. 傳入了一個 內核棧指針 .
  2. 使用如下內容構建一個 TaskContext.
    1. 內核棧指針作為 任務上下文的棧指針 .
    2. __restore的函數地址作為 函數調用完畢返回地址 .也就是說 __switchret執行完畢之後執行__restore.
    3. 空的s0~s12.

需要註意的是, __restore 的實現需要做出變化:它 不再需要 在開頭 mv sp, a0 了。因為在 __switch 之後,sp 就已經正確指向了我們需要的 Trap 上下文地址。

然後在創建 TaskManager 的全局實例 TASK_MANAGER 的時候為 每個任務上下文 , 初始化為由如下內容組成的TaskContext:

  1. 鏈接進去 的任務記憶體位置決定的 每個任務的內核棧指針 作為棧指針.
  2. __restore作為 函數調用完畢返回地址 .
  3. 空的s0~s12.

TaskContext構建一個 執行第一個任務 的方法:

impl TaskManager {
    fn run_first_task(&self) -> ! {
        let mut inner = self.inner.exclusive_access();
        let task0 = &mut inner.tasks[0];
        task0.task_status = TaskStatus::Running;
        let next_task_cx_ptr = &task0.task_cx as *const TaskContext;
        drop(inner);
        let mut _unused = TaskContext::zero_init();
        // before this, we should drop local variables that must be dropped manually
        unsafe {
            __switch(
                &mut _unused as *mut TaskContext,
                next_task_cx_ptr,
            );
        }
        panic!("unreachable in run_first_task!");
    }

這段代碼可以這樣理解:

  1. 獲取 單線程的借用 .
  2. 獲取第一個 任務塊的指針 .
  3. 隨後把這個任務設置為 運行狀態 .
  4. 獲取這個任務的 上下文 .
  5. 由於後續要使用__switch因此需要 主動釋放 這個借用.
  6. 使用__switch調用
    1. zero_init構建的一個 全空 的上下文.
    2. 第一個任務 的上下文.

這時候這個執行順序有點亂了,我嘗試畫一個流程圖.

首先是這章實現的結構體TaskManager的結構:

初始化 的流程為:

初始化後的TASK_MANAGER:

調用run_fist_app之後發生了什麼:

這時候考慮APP發生掛起的時候會發生什麼:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 .NET許可權管理及快速開發框架、最好用的許可權工作流系統。 基於經典領域驅動設計的許可權管理及快速開發框架,源於Martin Fowler企業級應用開發思想及最新技術組合(SqlSugar、EF、Quartz、AutoFac、WebAPI、Swagger、Mock、NUnit、Vue2/3、Ele ...
  • 本章將和大家分享Linux系統中的管道命令、grep命令、sed命令和awk命令。廢話不多說,下麵我們直接進入主題。 一、管道命令 Linux 中的管道命令(pipe)是一種非常強大的特性,它允許你將一個命令的輸出作為另一個命令的輸入。管道命令極大地增強了命令行的靈活性和功能,使得複雜的數據處理任務 ...
  • 1. 配置 1.1 如果是配置全局文件,則編輯/etc/mail.rc 1.2 如果是配置當前用戶,則編輯~/.mailrc 2. 配置文件內容 # 這裡填入smtp地址,這裡的xxx為qq或者163等,如果用的雲伺服器,安全組策略要開放465/25埠,入站和出站都要開放該埠 set smtp= ...
  • 一、背景 在公司軟體的實際開發中,當一個版本的客戶端安裝包本地調試、測試驗證都沒問題後外發,到用戶實際機器上出問題了,怎麼辦? 很多人說,讓客戶給出復現步驟,我來試試!但是按照步驟操作之後還是沒效果。這時你又想到了是不是環境的差異,但是又說不上來是哪裡出問題。提供兩個辦法:1.寫日誌,編一個相近版本 ...
  • 1、用戶操作 阿裡雲預設是 root 用戶,我們一般要自己創建一個用戶,然後給該用戶 sudo 許可權 添加用戶 sudo adduser newUserName 賦予sudo許可權 sudo usermod -aG sudo newUserName 刪除用戶 sudo deluser --remove ...
  • 1、Docker 基本概念 什麼是 Docker? Docker 是一個開源的容器化平臺,允許開發者封裝他們的應用程式及其所有依賴項到一個標準化的單元中,這個單元被稱為“容器”。容器可以在任何支持 Docker 的環境中運行,從而確保應用程式的可移植性和一致性。 Docker 的優勢 一致性和可移植 ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是i.MXRT1050在GPIO上增加RC延時電路後導致邊沿中斷誤觸發問題探析。 前段時間有一個 RT1052 客戶反饋了一個有趣的問題,他們設計得是一個帶 LCD 屏交互的應用,應用以官方 SDK 里的 lvgl_demo_widget ...
  • 家裡的機頂盒淘汰下來,博主想要物盡其用,看看是否能將其改造為一臺Linux"開發機",為其安裝Ubuntu系統,故開始倒騰 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...