[自製操作系統] 第19回 實現用戶進程(下)

来源:https://www.cnblogs.com/Lizhixing/archive/2022/09/04/15984901.html
-Advertisement-
Play Games

目錄 一、前景回顧 二、進程的創建與初始化 三、如何進行進程的切換 四、運行測試 五、原書勘誤 一、前景回顧 在上一回我們大概講述了任務切換的發展,並且知道Linux採用的是一個CPU使用一個TSS的方式,在最後我們成功實現了tss。現在萬事俱備,我們正式來實現用戶進程。 二、進程的創建與初始化 進 ...


目錄
一、前景回顧
二、進程的創建與初始化
三、如何進行進程的切換
四、運行測試
五、原書勘誤

 

一、前景回顧

  在上一回我們大概講述了任務切換的發展,並且知道Linux採用的是一個CPU使用一個TSS的方式,在最後我們成功實現了tss。現在萬事俱備,我們正式來實現用戶進程。

二、進程的創建與初始化

  進程的創建與線程的創建很相似,這裡直接上圖來對比分析:

  
  我們使用process_execute函數來創建初始化進程。

 1 /*創建用戶進程*/
 2 void process_execute(void *filename, char *name)
 3 {
 4     /*pcb內核的數據結構,由內核來維護進程信息,因此要在內核記憶體池中申請*/
 5     struct task_struct *thread = get_kernel_pages(1);
 6     init_thread(thread, name, 31);    
 7     thread_create(thread, start_process, filename);
 8     create_user_vaddr_bitmap(thread);    //創建虛擬地址的點陣圖
 9     thread->pgdir = create_page_dir();   //用戶進程的頁目錄表的物理地址,這裡傳進來的是頁目錄表物理地址所對應的虛擬地址
10 
11     enum intr_status old_status = intr_disable();
12     ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
13     list_append(&thread_ready_list, &thread->general_tag);
14 
15     ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
16     list_append(&thread_all_list, &thread->all_list_tag);
17     intr_set_status(old_status);
18 }

  在該函數中首先使用get_kernel_pages函數在內核物理空間中申請一頁物理記憶體來作為進程的PCB,因為最終調度是由內核來操控的,所以PCB統一都在內核物理空間中申請。隨後依舊調用init_thread()thread_create()函數來初始化進程的PCB。

  下麵開始不一樣了,create_user_vaddr_bitmap()函數的作用是給進程創建初始化點陣圖。這裡科普一下:我們都知道進程有4GB的虛擬空間,其中第1~3GB是分配給用戶空間,第4GB是分配給內核空間,這是Linux下的分配習慣,我們照搬。而用戶空間實際上只用上了0x08048000到0xc0000000這一部分。所以create_user_vaddr_bitmap()函數也就是將這一部分空間劃分到用戶的虛擬地址記憶體池中。

  再來看create_page_dir()函數,我們知道操作系統被所有用戶進程所共用,所以我們將用戶進程頁目錄表中的第768~1023個頁目錄項用內核頁目錄表的第768~1023個頁目錄項代替,其實就是將內核所在的頁目錄項複製到進程頁目錄表中同等位置,這樣就能讓用戶進程的高1GB空間指向內核。最後再將進程添加到全部隊列和就緒隊列中供調度。至此,用戶進程就算創建初始化完畢了。

  我們現在來看看進程的PCB的內容:

   

三、如何進行進程的切換

  因為我們之前一直都是處於內核態下,也就是0特權級下。現在要切換到用戶進程也就是用戶態,3特權級下運行,和之前的切換不太一樣。還是舉例來說明吧。

  假設當前內核線程A時間片用光了,在調度函數schedule()中會從就緒隊列中彈出下一個進程B的PCB,根據PCB我們就知道了進程B的所有信息。不過接下來和之前線程的切換不一樣了,首先調用process_activate()函數激活下一個內核線程或者進程的頁表。對於內核線程來說,內核線程的頁目錄表在之前激活分頁機制的時候就已經設定好了,被存放在0x10000地址處。如果不是內核線程,那麼就需要將進程B的頁目錄表地址賦給CR3寄存器,因為CPU定址是基於CR3寄存器中保存的頁目錄表的地址來定址的。切換到進程B後,需要將進程B的頁目錄表地址賦給了CR3寄存器。

 1 /*激活線程或進程的頁表,更新tss中的esp0為進程的特權級0的棧*/
 2 void process_activate(struct task_struct *p_thread)
 3 {
 4     ASSERT(p_thread != NULL);
 5     //激活該線程或者進程的頁表
 6     page_dir_activate(p_thread);
 7     
 8     if (p_thread->pgdir) {  //如果是進程那麼需要在tss中填入0級特權棧的esp0
 9         update_tss_esp(p_thread);
10     }
11 }
process_activate

  除此之外,還要將tss中的esp0欄位更新為進程B的0級棧。前面已經說過,進程在由例如中斷等操作從3特權級進入0特權級後,也就是進入內核態,使用的會是0特權級下的棧,不再是3特權級的棧。因此在這個地方我們需要給進程B更新0特權級棧。方便以後進程B進入內核態。這裡我們可以看到,進程B的0特權級的棧頂指針指向進程B的PCB最高處

1 /*更新tss中的esp0欄位的值為pthread的0級棧*/
2 void update_tss_esp(struct task_struct *pthread)
3 {
4     tss.esp0 = (uint32_t *)((uint32_t)pthread + PG_SIZE);
5 }
update_tss_esp

  這一系列操作完成後,我們又回到switch_to函數,和前面講線程切換也是一樣,首先通過一系列的push操作,將當前內核線程A的寄存器信息壓入棧中以便下次又被調度上CPU後可以恢復環境。隨後從進程B的PCB中得到新的棧。此時進程B的棧的情況如下:
         

 1 switch_to:
 2     push esi            ;這裡是根據ABI原則保護四個寄存器 放到棧裡面
 3     push edi
 4     push ebx
 5     push ebp
 6     
 7     mov eax, [esp+20]    ;esp+20的位置是cur cur的pcb賦值給eax
 8     mov [eax], esp       ;[eax]為pcb的內核棧指針變數 把當前環境的esp值記錄下來
 9     
10     mov eax, [esp+24]
11     mov esp, [eax]       
12 
13     pop ebp
14     pop ebx
15     pop edi
16     pop esi
17     ret                 

  進程B的還是通過一系列POP操作,最終調用*eip所指向的函數kernel_thread,在該函數中又調用*function所指向的函數start_process(),該函數代碼如下:

 1 void start_process(void *filename)
 2 {
 3     void *function = filename;
 4     struct task_struct *cur = running_thread();
 5     cur->self_kstack += sizeof(struct thread_stack);
 6     struct intr_stack *proc_stack = (struct intr_stack *)cur->self_kstack;
 7     proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
 8     proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
 9     proc_stack->gs = 0;
10     proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;  //數據段選擇子
11     proc_stack->eip = function; //函數地址 ip
12     proc_stack->cs = SELECTOR_U_CODE; //cs ip cs選擇子
13     proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1); //不能夠關閉中斷 ELFAG_IF_1 不然會導致無法調度
14     proc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE); //棧空間在0xc0000000以下一頁的地方 當然物理記憶體是操作系統來分配
15     proc_stack->ss = SELECTOR_U_DATA; //數據段選擇子
16     asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");
17 }

  來細品一下這個函數的內容。還記得前面的那個進程的PCB圖嗎?

   

  首先通過running_thread函數獲取到當前進程的PCB的地址。根據圖中我們可以知道self_kstack一開始是被賦值指向棧頂,也就是線程棧的開始位置。經過cur->self_kstack += sizeof(struct thread_stack)後,現在self_kstack指向中斷棧處了,如圖所示。然後定義一個pro_stack指針指向self_kstack。這個先記住,待會兒會用上。

  隨後便是對一系列寄存器的初始化,重點關註ds、es、fs、cs、ss和gs這幾個段寄存器的初始化,我們將它們初始化為用戶進程下的3特權級的段選擇子。因為在用戶態下,我們是不能訪問0特權級下的代碼段和數據段的。對於gs寄存器,這裡其實不管是否設置為0都無所謂,因為用戶態下的程式是不能直接訪問顯存的,進程在從內核態進入用戶態時會進行特權檢查,如果gs段寄存器中的段選擇子的特權等級高於進程返回後的特權等級,CPU就會自動將段寄存器gs給置0,如果用戶進程一旦訪問顯存,就會報錯。

  再往下就給esp賦值,這個地方是為了當回到用戶態空間後,給用戶程式指定一個棧頂指針。這裡我們將用戶態的棧頂指針設置為用戶態空間下的0xc0000000處。

  最後通過內聯彙編:

  asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");

  將proc_stack所指向的值賦給當前進程的esp,也就是棧頂指針,前面我們知道proc_stack已經被賦好了值,為self_kstack。最後便是跳轉到intr_exit處執行代碼。

  此時棧的情況如下:  
                                
  然後intr_exit的代碼如下所示:

1 intr_exit:
2     add esp, 4
3     popad
4     pop gs
5     pop fs
6     pop es
7     pop ds
8     add esp, 4
9     iretd

  看著代碼就很好理解了,首先add esp, 4跳過棧中的vec_no,隨後popad和pop操作彈出8個32位的通用寄存器和4個段寄存器。又是通過add esp, 4跳過棧中的err_code,最後執行iretd指令,將(*eip)、cs、eflags彈出,而我們事先已經將用戶進程要運行的函數地址存放在eip中。最後,由於我們跳轉後的用戶態,它的特權級不同於當前內核態的特權級,所以需要恢複舊棧,CPU自動將棧中的esp和ss彈出。這些值在我們前面的start_process()函數中已經初始化完畢。至此我們就已經完成了內核態到用戶態的轉換。

四、運行測試

  這裡我貼上本章所有相關代碼:

  1 #include "process.h"
  2 #include "thread.h"
  3 #include "global.h"
  4 #include "memory.h"
  5 #include "debug.h"
  6 #include "console.h"
  7 #include "interrupt.h"
  8 #include "tss.h"
  9 
 10 extern void intr_exit(void);
 11 extern struct list thread_ready_list;           //就緒隊列
 12 extern struct list thread_all_list;  
 13 
 14 void start_process(void *filename)
 15 {
 16     void *function = filename;
 17     struct task_struct *cur = running_thread();
 18     cur->self_kstack += sizeof(struct thread_stack);
 19     struct intr_stack *proc_stack = (struct intr_stack *)cur->self_kstack;
 20     proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
 21     proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
 22     proc_stack->gs = 0;
 23     proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;            //數據段選擇子
 24     proc_stack->eip = function;                                //函數地址 ip
 25     proc_stack->cs = SELECTOR_U_CODE;                                //cs ip cs選擇子
 26     proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);                //不能夠關閉中斷 ELFAG_IF_1 不然會導致無法調度
 27     proc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);    //棧空間在0xc0000000以下一頁的地方 當然物理記憶體是操作系統來分配
 28     proc_stack->ss = SELECTOR_U_DATA;                                //數據段選擇子
 29     asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");
 30 }
 31 
 32 
 33 /*激活頁表*/
 34 void page_dir_activate(struct task_struct *p_thread)
 35 {
 36     //內核線程的頁目錄表的物理地址為0x100000
 37     uint32_t pagedir_phy_addr = 0x100000;
 38     if (p_thread->pgdir != NULL) { //說明下一個調用的是進程,否則是內核線程
 39         pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir);
 40     }
 41 
 42     /*更新頁目錄寄存器CR3,使新頁表生效*/
 43     asm volatile("movl %0, %%cr3" : : "r" (pagedir_phy_addr) : "memory");
 44 }
 45 
 46 /*激活線程或進程的頁表,更新tss中的esp0為進程的特權級0的棧*/
 47 void process_activate(struct task_struct *p_thread)
 48 {
 49     ASSERT(p_thread != NULL);
 50     //激活該線程或者進程的頁表
 51     page_dir_activate(p_thread);
 52     
 53     if (p_thread->pgdir) {  //如果是進程那麼需要在tss中填入0級特權棧的esp0
 54         update_tss_esp(p_thread);
 55     }
 56 }
 57 
 58 uint32_t *create_page_dir(void)
 59 {
 60     //用戶進程的頁表不能讓用戶直接訪問到,所以在內核空間申請
 61     uint32_t *page_dir_vaddr = get_kernel_pages(1);                //得到記憶體
 62     if (page_dir_vaddr == NULL) {
 63         console_put_str("create_page_dir: get_kernel_page failed!\n");
 64         return NULL;
 65     }
 66     
 67     memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300 * 4), (uint32_t*)(0xfffff000 + 0x300 * 4), 1024); // 256項
 68     uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr);                    
 69     page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;                    //最後一項是頁目錄項自己的地址
 70     
 71     return page_dir_vaddr;                                         
 72 }
 73 
 74 
 75 /*創建用戶進程虛擬地址點陣圖*/
 76 void create_user_vaddr_bitmap(struct task_struct *user_prog)
 77 {
 78     user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
 79     
 80     //計算需要多少物理記憶體頁來記錄點陣圖 USER_VADDR_START為0x08048000
 81     uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE); 
 82     user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt);
 83 
 84     user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;
 85     bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap);
 86 }
 87 
 88 /*創建用戶進程*/
 89 void process_execute(void *filename, char *name)
 90 {
 91     /*pcb內核的數據結構,由內核來維護進程信息,因此要在內核記憶體池中申請*/
 92     struct task_struct *thread = get_kernel_pages(1);
 93     init_thread(thread, name, 31);    
 94     thread_create(thread, start_process, filename);
 95     create_user_vaddr_bitmap(thread);    //創建虛擬地址的點陣圖
 96     thread->pgdir = create_page_dir();   //用戶進程的頁目錄表的物理地址,這裡傳進來的是頁目錄表物理地址所對應的虛擬地址
 97 
 98     enum intr_status old_status = intr_disable();
 99     ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
100     list_append(&thread_ready_list, &thread->general_tag);
101 
102     ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
103     list_append(&thread_all_list, &thread->all_list_tag);
104     intr_set_status(old_status);
105 }
process.c
 1 #ifndef  __USERPROG_PROCESS_H
 2 #define  __USERPROG_PROCESS_H
 3 #include "stdint.h"
 4 #include "thread.h"
 5 
 6 #define USER_STACK3_VADDR (0xc0000000 - 0x1000)
 7 #define USER_VADDR_START 0x08048000
 8 
 9 
10 void process_execute(void *filename, char *name);
11 void create_user_vaddr_bitmap(struct task_struct *user_prog);
12 uint32_t *create_page_dir(void);
13 void process_activate(struct task_struct *p_thread);
14 void page_dir_activate(struct task_struct *p_thread);
15 void start_process(void *filename);
16 
17 #endif
process.h
  1 #include "memory.h"
  2 #include "print.h"
  3 #include "stdio.h"
  4 #include "debug.h"
  5 #include "string.h"
  6 #include "thread.h"
  7 #include "sync.h"
  8 
  9 #define PG_SIZE 4096     //頁大小
 10 
 11 /*0xc0000000是內核從虛擬地址3G起,
 12 * 0x100000意指低端記憶體1MB,為了使虛擬地址在邏輯上連續
 13 * 後面申請的虛擬地址都從0xc0100000開始
 14 */
 15 #define K_HEAP_START 0xc0100000 
 16 
 17 #define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
 18 #define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
 19 
 20 struct pool {
 21     struct bitmap pool_bitmap;     //本記憶體池用到的點陣圖結構
 22     uint32_t phy_addr_start;       //本記憶體池管理的物理記憶體的起始地址 
 23     uint32_t pool_size;            //記憶體池的容量
 24     struct lock lock;
 25 };
 26 
 27 struct pool kernel_pool, user_pool;  //生成內核記憶體池和用戶記憶體池
 28 struct virtual_addr kernel_vaddr;    //此結構用來給內核分配虛擬地址
 29 
 30 
 31 /*初始化記憶體池*/
 32 static void mem_pool_init(uint32_t all_mem) 
 33 {
 34     put_str("mem_pool_init start\n");
 35     /*目前頁表和頁目錄表的占用記憶體
 36     * 1頁頁目錄表 + 第0和第768個頁目錄項指向同一個頁表 + 第769~1022個頁目錄項共指向254個頁表 = 256個頁表
 37     */
 38     lock_init(&kernel_pool.lock);
 39     lock_init(&user_pool.lock);
 40 
 41     uint32_t page_table_size = PG_SIZE * 256;
 42     uint32_t used_mem = page_table_size + 0x100000;  //目前總共用掉的記憶體空間
 43     uint32_t free_mem = all_mem - used_mem;          //剩餘記憶體為32MB-used_mem
 44     uint16_t all_free_pages = free_mem / PG_SIZE;    //將剩餘記憶體劃分為頁,餘數捨去,方便計算
 45     
 46     /*內核空間和用戶空間各自分配一半的記憶體頁*/
 47     uint16_t kernel_free_pages = all_free_pages / 2; 
 48     uint16_t user_free_pages = all_free_pages - kernel_free_pages; 
 49 
 50     /*為簡化點陣圖操作,餘數不用做處理,壞處是這樣會丟記憶體,不過只要記憶體沒用到極限就不會出現問題*/
 51     uint32_t kbm_length = kernel_free_pages / 8; //點陣圖的長度單位是位元組
 52     uint32_t ubm_length = user_free_pages / 8;
 53 
 54     uint32_t kp_start = used_mem;                                 //內核記憶體池的起始物理地址
 55     uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;   //用戶記憶體池的起始物理地址
 56 
 57     /*初始化內核用戶池和用戶記憶體池*/
 58     kernel_pool.phy_addr_start = kp_start;
 59     user_pool.phy_addr_start = up_start;
 60 
 61     kernel_pool.pool_size = kernel_free_pages * PG_SIZE; 
 62     user_pool.pool_size = user_free_pages * PG_SIZE;
 63 
 64     kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
 65     user_pool.pool_bitmap.btmp_bytes_len = ubm_length;
 66 
 67     /***********內核記憶體池和用戶記憶體池點陣圖************
 68     *內核的棧底是0xc009f00,減去4KB的PCB大小,便是0xc009e00
 69     *這裡再分配4KB的空間用來存儲點陣圖,那麼點陣圖的起始地址便是
 70     *0xc009a00,4KB的空間可以管理4*1024*8*4KB=512MB的物理記憶體
 71     *這對於我們的系統來說已經綽綽有餘了。
 72     */
 73     /*內核記憶體池點陣圖地址*/
 74     kernel_pool.pool_bitmap.bits = (void *)MEM_BIT_BASE;  //MEM_BIT_BASE(0xc009a00)
 75     /*用戶記憶體池點陣圖地址緊跟其後*/
 76     user_pool.pool_bitmap.bits = (void *)(MEM_BIT_BASE + kbm_length);
 77 
 78     /*輸出記憶體池信息*/
 79     put_str("kernel_pool_bitmap_start:");
 80     put_int((int)kernel_pool.pool_bitmap.bits);
 81     put_str("\n");
 82     put_str("kernel_pool.phy_addr_start:");
 83     put_int(kernel_pool.phy_addr_start);
 84     put_str("\n");
 85 
 86     put_str("user_pool_bitmap_start:");
 87     put_int((int)user_pool.pool_bitmap.bits);
 88     put_str("\n");
 89     put_str("user_pool.phy_addr_start:");
 90     put_int(user_pool.phy_addr_start);
 91     put_str(
              
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • C++ 課堂筆記(一) 說明:此筆記是學習於B站黑馬程式員的C++視頻所作的,感謝黑馬程式員的教學;如有什麼不足之處,望各位賜教。僅供學習。 第一個代碼:書寫hello world #include<iostream> using namespace std; int main() { cout < ...
  • x64dbg 是一款開源的應用層反彙編調試器,旨在對沒有源代碼的可執行文件進行惡意軟體分析和逆向工程,同時 x64dbg 還允許用戶開發插件來擴展功能,插件開發環境的配置非常簡單,如下將簡單介紹x64dbg是如何配置開發環境以及如何開發插件的。 ...
  • 摘要 這就是一個記錄自己進行WinUI項目實踐的博客,項目開源地址如下,覺得有幫助的可以去看看,因為項目都開源了,所以保姆級的講解肯定不如直接看代碼來的實在了。 電子腦殼項目地址 為什麼叫新 因為之前發過一篇講開發上位機應用的博客,所以作為區分就把這篇成為新的一篇了,微軟最新的windows應用開發 ...
  • RPC 遠程過程調用(遠程函數調用) GRPC google開發,跨語言RPC,用來解決微服務通信性能和擴展問題 跨語言:通過Protobuffer文件(通用文件)解決跨語言問題的 高併發:GRPC基於http/2協議,多路復用機制(服務端一個線程可以連接任意數量客戶端請求) webapi缺陷 we ...
  • 前言 在日常工作中,偶爾需要調查一些詭異的問題,而業務代碼經過長時間的演化,很可能已經變得錯綜複雜,流程、分支眾多,如果能在關鍵方法的日誌里添加上調用者的信息,將對定位問題非常有幫助。 介紹 StackTrace, 位於 System.Diagnostics 命名空間下,名字很直觀,它代表一個方法調 ...
  • 一:背景 一直在用 WinDbg 調試用戶態程式,並沒有用它調試過 內核態,畢竟不是做驅動開發,也沒有在分析 dump 中需要接觸用內核態的需求,但未知的事情總覺得很酷,加上最近在看 《深入解析 Windows 操作系統》 一書,書中有不少案例需要深入到 內核態 ,所以這篇準備整理一下如何用 Win ...
  • zabbix的基礎使用 zabbix服務端web界面使用介紹 基於zabbix服務端的部署進行下麵的操作 web界面 在我們登錄的時候預設會進入監控選項欄的儀錶盤界面 (Monitoring)監控選項欄設置 (Dashboard)儀錶盤 這裡我們一般要修改的是儀錶盤的佈局 選擇編輯儀錶盤 將“當前問 ...
  • 說明 之前安裝的brew被折騰壞了,重新安裝出現了一些問題,之前安裝homebrew的方式不好用了,網上資料很多,折騰了一番,最後成功了,整理好這篇記錄,避免一些坑。 系統版本 版本:macOS [email protected] 晶元:Apple M1 安裝 (1)終端輸入 /bin/zsh -c " ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...