【Linux】進程地址空間揭秘(初步認識)

10.進程地址空間(初步認識)

文章目錄

  • 10.進程地址空間(初步認識)
      • 一、進程地址空間的實驗現象解析
      • 二、進程地址空間
      • 三、虛擬內存管理
        • 補充:數據的寫時拷貝(淺談)
        • 補充:頁表(淺談)
        • 補充:關于地址空間mm_struct初始化問題(淺談)
      • 四、為什么要有虛擬地址空間
      • 五、總結

一、進程地址空間的實驗現象解析

這里有一個之前我們驗證進程之間的獨立性的代碼,現在在此基礎上做一些改動,得到以下帶代碼:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>int g_value = 100;  // 全局變量定義 
int main() 
{printf("父進程 PID:%d PPID:%d\n", getpid(), getppid());  // 獲取進程信息 pid_t id = fork();  // 創建子進程 if (id < 0) return -1;  // 錯誤處理 if (id == 0) {// 子進程邏輯while(1) {printf("子進程 PID:%d PPID:%d g_value:%d 地址:%p\n",getpid(), getppid(), g_value++, &g_value);  // 修改變量 sleep(1);  // 延時控制 }} else {while(1){// 父進程邏輯printf("父進程 PID:%d PPID:%d g_value:%d 地址:%p\n",getpid(), getppid(), g_value, &g_value);  // 保持原值 sleep(1);}}return 0;
}

運行結果:

[lisihan@hcss-ecs-b735 lession14]$ gcc -o code1 code1.c 
[lisihan@hcss-ecs-b735 lession14]$ ./code1 
父進程 PID:31004 PPID:27811
父進程 PID:31004 PPID:27811 g_value:100 地址:0x601054
子進程 PID:31005 PPID:31004 g_value:100 地址:0x601054
父進程 PID:31004 PPID:27811 g_value:100 地址:0x601054
子進程 PID:31005 PPID:31004 g_value:101 地址:0x601054
父進程 PID:31004 PPID:27811 g_value:100 地址:0x601054
子進程 PID:31005 PPID:31004 g_value:102 地址:0x601054
父進程 PID:31004 PPID:27811 g_value:100 地址:0x601054
子進程 PID:31005 PPID:31004 g_value:103 地址:0x601054
子進程 PID:31005 PPID:31004 g_value:104 地址:0x601054
父進程 PID:31004 PPID:27811 g_value:100 地址:0x601054
子進程 PID:31005 PPID:31004 g_value:105 地址:0x601054
父進程 PID:31004 PPID:27811 g_value:100 地址:0x601054
父進程 PID:31004 PPID:27811 g_value:100 地址:0x601054

實驗結果特征:

  1. 父進程持續輸出原始值:

    父進程 PID:1001 PPID:999 g_value:100 地址:0x601044 
    
  2. 子進程輸出遞減序列:

    子進程 PID:1002 PPID:1001 g_value:101 地址:0x601044 
    
  3. 地址顯示完全相同但值獨立變化

  • 變量內容不?樣,所以??進程輸出的變量絕對不是同?個變量
  • 但地址值是?樣的,說明,該地址絕對不是物理地址!
  • 在Linux地址下,這種地址叫做虛擬地址
  • 我們在?C/C++語?所看到的地址,全部都是虛擬地址!物理地址,???概看不到,由OS統?管理。OS需要把虛擬地址轉化為物理地址

二、進程地址空間

首先要了解一個概念,就是內存布局:

在這里插入圖片描述

如圖所示,如果從低地址到高地址,我們整個程序的內存在布局情況,包括正文部分、初始化數據、未初始化數據、堆區、棧區。我們的程序地址空間布局是依照這張圖展開的。它不叫程序地址空間,它全稱應該叫做進程地址空間,所以它不屬于語言范疇,它屬于系統范疇,這是屬于系統方面的概念。

我們也可以用一個簡單的代碼來驗證上面的結論:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int unval;
int gval = 100;int main()
{printf("code addr: %p\n", main);printf("gval addr: %p\n", &gval);printf("unval addr: %p\n", &unval);int *mem = (int*)malloc(10*sizeof(int));printf("heap add: %p\n", mem);int a,b,c;printf("stack addr : %p\n", &a);printf("stack addr : %p\n", &b);printf("stack addr : %p\n", &c);
}

運行結果:

[lisihan@hcss-ecs-b735 lession14]$ gcc -o code2 code2.c 
[lisihan@hcss-ecs-b735 lession14]$ ./code2
code addr: 0x40057d
gval addr: 0x60103c
unval addr: 0x601044
heap add: 0x20bc010
stack addr : 0x7ffeeb9704b4
stack addr : 0x7ffeeb9704b0
stack addr : 0x7ffeeb9704ac

可以看到他們的地址數,整個地址是依次增大的。很明顯,堆和棧之間,中間有一大塊的鏤空。所以,我們的程序地址空間布局是依照這張圖展開的。

這幅圖,遵循《深入理解計算機系統》等權威教材,自底向上進行繪制,認為低地址在下方,高地址在上方。以前學到的進程地址空間,可能只考慮了部分區域,甚至像共享環境變量這樣的部分也可能沒見過。

所以之前說‘程序的地址空間’是不準確的,準確的應該說成 進程地址空間 ,那該如何理解呢?看圖:

在這里插入圖片描述

? 圖二

  • 上?的圖就?矣說明問題,同?個變量,地址相同,其實是虛擬地址相同,內容不同其實是被映射到了不同的物理地址

  • 對虛擬地址的理解:

    • 當一個程序在用戶態運行時,操作系統會為其創建一個進程(process)實例,并為此進程“畫一張大餅”——即分配一個虛擬地址空間,使其認為自己獨占整個物理內存。多個進程間彼此隔離,互不干擾;即便父子進程共享同一份代碼和數據,其后續修改也會觸發寫時拷貝,確保各自獨立。

    • 用戶態代碼訪問虛擬地址時,CPU 先查詢當前進程的頁表,將虛擬地址轉換為物理地址,然后讀寫實際內存。用戶和應用程序感知到的永遠是虛擬空間,物理空間由操作系統統一管理。

三、虛擬內存管理

在操作系統中,管理進程的虛擬地址空間是其核心職責。具體的管理方式始于對該空間本身的精確刻畫,即操作系統需要為每個運行中的程序定義一個專屬的描述結構。這種描述隨后被整合進程序的核心控制信息塊中。

本質上,這個虛擬地址空間是操作系統內核維護的一種關鍵數據結構。當創建一個程序時,其核心控制信息塊內會包含一個指向其物理內存使用情況的引用。為了避免程序直接操作物理內存地址,在一個進程的task_struct中,操作系統在程序與物理內存之間引入了一個中間數據結構,其類型通常命名為 mm_struct。該結構雖然內部復雜,但宏觀上承擔此角色。程序的核心控制信息塊內部維護著一個特定指針,該指針直接關聯到當前程序對應的專屬地址空間描述結構。這意味著每個程序都擁有自己獨立的、由操作系統定義的頁表。

struct task_struct
{
/*...*///對于普通的??進程來說該字段指向他的虛擬地址空間的??空間部分,對于內核線程來說這部分為NULL。struct mm_struct *mm; // 該字段是內核線程使?的。當該進程是內核線程時,它的mm字段為NULL,表?沒有內存地址空間,可也并不是真正的沒有,這是因為所有進程關于內核的映射都是?樣的,內核線程可以使?任意進程的地址空間。struct mm_struct *active_mm; 
/*...*/
}

操作系統為每個程序構建好這個專屬的頁表(如圖2所示)后,程序在運行過程中始終通過它來訪問內存。當程序需要讀寫內存時,最終都必須經由自身這個頁表結構來完成訪問操作。

struct mm_struct
{/*...*/struct vm_area_struct *mmap; /* 指向虛擬區間(VMA)鏈表 */struct rb_root mm_rb; /* red_black樹 */unsigned long task_size; /*具有該結構體的進程的虛擬地址空間的??*//*...*/// 代碼段、數據段、堆棧段、參數段及環境段的起始和結束地址。unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack;unsigned long arg_start, arg_end, env_start, env_end;/*...*/
}

可以說,mm_struct結構是對整個??空間的描述。每?個進程都會有??獨?的mm_struct,這樣每?個進程都會有??獨?的地址空間才能互不?擾。

進程的地址空間分布情況:

在這里插入圖片描述

雖然操作系統通過這個結構體讓程序感覺自己獨占了巨大的連續內存范圍,但程序實際能使用的物理內存量通常遠小于此視圖范圍,并且過量申請會被系統限制或終止。操作系統提供這個頁表的承諾,但并不保證其全部空間都必然映射到物理資源。

補充:數據的寫時拷貝(淺談)

fork 創建子進程后,父子進程共享同一份物理頁,頁表條目被標記為只讀。只有在任一進程對共享頁執行寫入操作時,觸發頁錯誤中斷,內核才會分配新物理頁并復制原內容,修改對應進程的頁表映射,以實現各自獨立。

通俗來講:之前提到過,進程具有獨立性即使是父子關系的進程也是如此,又因為進程 = PCB + 代碼和數據,當創建子進程時,OS會拷貝一份內核進程控制塊(PCB),代碼子進程和父進程共享,只有當子進程或父進程對數據進行寫入操作的時候,數據才會從新拷貝一份給兩個進程,換言之如果一份數據對于父子進程都是只讀的,就不會拷貝,父子進程公用一套數據。(這里只是簡單說個大概,后面會詳談)

補充:頁表(淺談)

頁表不只有虛擬地址與物理地址的映射關系,還有一些標志位,比如說讀寫權限的標志位。

  1. 如果有一些物理地址的內容是只讀的,當進程以寫的權限去訪問這塊物理地址的時候發現不匹配,操作系統會直接清除掉這個進程。

    在學C語言的時候有一個經典例子:

    char *str = "hello linux";
    *str = 'H'
    

    這段代碼在編譯的時候不會有任何報錯,但是運行的時候就會直接崩潰。原因在與str指向的是一個字符串,這個字符串儲存在字符常量區,這個區域是只讀的,不可以進行寫入操作,但在編譯的時候編譯器是無法判斷進程運行時的錯誤的,所以編譯沒有報錯。我們寫好的程序運行之后都是一個一個的進程,根據上面所學的頁表的讀寫權限的標志位也就不難理解出現這種情況的原因了。

    野指針也會觸發這種情況:野指針指向的地址的訪問權限與頁表中對應物理地址的權限不匹配或者野指針指向的地址空間在頁表中根本不存在,都會使得程序無法正常運行。

    查找資料:

    權限位(RWX):標記內存區域的可讀、可寫、可執行屬性。例如代碼區設為只讀(R-X),嘗試寫入會觸發操作系統干預,直接終止進程。這解釋了C語言中修改字符串常量導致崩潰的底層原因——字符常量區被映射為只讀,const關鍵字本質是編譯器層面的輔助檢查,而真正的保護由頁表硬件機制實現。

  2. 當一個進程創建的時候,先加載的是內核中的PCB,然后才開始慢慢加載代碼和其他數據,這也符合我們之前對進程的理解“先描述,再組織”。所以就可能會存在內核數據結構已經加載完成了,但是代碼和數據還沒有加載到內存中的情況。還有一種情況,就是之前在進程狀態中提到的阻塞掛起狀態,此時若當前進程處于阻塞狀態,其代碼和數據占用內存但無實際意義,操作系統會將該進程的部分代碼和數據換出至磁盤。此時也會出現代碼和數據還沒有加載到內存中的情況。

    頁表中還有一個標志位可以檢查目標內容是否在內存中,方便在進程運行之前把內容加載進來

    查找資料:

    存在位(Present Bit):指示目標數據是否在物理內存中。若為0,表示數據尚未加載或已被換出到磁盤(如Swap分區)。此時訪問會觸發“缺頁異常”,操作系統負責將所需數據從磁盤調入內存,更新頁表后恢復進程執行。此機制支撐了按需加載(Demand Paging):大型程序(如游戲)啟動時并非全部載入內存,僅加載必要部分,后續訪問時動態調入,極大提高了物理內存利用率,使小內存運行大程序成為可能。

補充:關于地址空間mm_struct初始化問題(淺談)

進程數據結構中的mm_struct結構體主要用于管理進程的內存空間。每個進程都有一個對應的 mm_struct 實例,它包含了關于該進程虛擬地址空間的所有必要信息。所以本質上這個結構體仍然需要初始化,以代碼區為例,比如初始化一個地址空間,代碼區有自己的數據結構。在mm_struct中,有start codeend code表示代碼區域的開始和結束,從而劃分出區域。進程加載時需要創建PCB和地址空間,地址空間和PCB由操作系統初始化。地址空間內有多個屬性,這些屬性如何初始化?

//完整定義包含關鍵字段:
struct mm_struct {struct vm_area_struct *mmap;    // 內存區域鏈表pgd_t *pgd;                    // 頁全局目錄atomic_t mm_users;            // 用戶計數atomic_t mm_count;            // 引用計數unsigned long start_code;     // 代碼段起始unsigned long end_code;       // 代碼段結束unsigned long start_data;     // 數據段起始unsigned long end_data;       // 數據段結束unsigned long start_brk;      // 堆區起始unsigned long brk;            // 堆區當前邊界unsigned long start_stack;    // 棧區起始unsigned long arg_start;      // 參數區起始unsigned long arg_end;        // 參數區結束unsigned long env_start;      // 環境變量起始unsigned long env_end;        // 環境變量結束
};

首先介紹一個命令(工具)readelf:

readelf 是一個強大的工具,用于顯示 ELF(Executable and Linkable Format)文件的信息。ELF 文件是一種常見的二進制文件格式,在 Linux 系統中廣泛使用,包括可執行文件、共享庫和目標文件。readelf 可以幫助開發者和系統管理員檢查這些文件的內容和結構。

簡單來說,readelf可以讀取一個可執行文件的每一個分段信息,其他的功能這里暫時不說,從這里可以知道:

  • 虛擬地址空間的初始化依賴于可執行程序的ELF格式特性:編譯器在生成可執行文件時,已按功能將程序劃分為多個邏輯段(Section),包括存儲機器指令的代碼段(.text)、存放字符串常量的只讀數據段(.rodata)、保存已初始化全局變量的數據段(.data)以及未初始化數據段(.bss)。
  • 通過readelf -S命令可解析ELF文件頭信息,獲取各段關鍵屬性:1) Size字段定義段大小(如代碼段16字節);2) Addr字段標識段在虛擬地址空間的預期起始位置;3) Flags權限標記(R/W/X組合)聲明段的內存訪問規則。

四、為什么要有虛擬地址空間

  • 地址空間和?表是OS創建并維護的!也就意味著凡是想使?地址空間和?表進?映射,也?定要在OS的監管之下來進?訪問!也順便保護了物理內存中的所有的合法數據,包括各個進程以及內核的相關有效數據!
  • 因為有地址空間的存在和?表的映射的存在,我們的物理內存中可以對未來的數據進?任意位置的加載!物理內存的分配 和 進程的管理就可以做到沒有關系,進程管理模塊和內存管理模塊就完成了解耦合
  • 因為有地址空間的存在,所以我們在C、C++語?上new, malloc空間的時候,其實是在地址空間上申請的,物理內存可以甚??個字節都不給你。?當你真正進?對物理地址空間訪問的時候,才執?內存的相關管理算法,幫你申請內存,構建?表映射關系(延遲分配),這是由操作系統?動完成,??包括進程完全0感知!!
  • 因為?表的映射的存在,程序在物理內存中理論上就可以任意位置加載。它可以將地址空間上的虛擬地址和物理地址進?映射,在進程視?所有的內存分布都可以是有序的。

簡單表述:

這種設計實現三項關鍵優勢:

  1. 空間效率:避免一次性加載整個程序;

  2. 安全隔離:頁表基于mm_struct配置的權限攔截非法訪問(如向代碼段寫入);

  3. 動態擴展:BSS段等未初始化區域僅預留虛擬地址范圍,實際物理頁在首次訪問時分配。最終,進程通過專屬的"內存視圖"(mm_struct)訪問物理內存,而操作系統通過維護編譯器預設的段屬性與硬件協作,實現了虛擬地址空間的動態管理與安全控制。

了解上述內容,我們可以回答一下問題:

Q:為什么在程序中的全局變量、字符常量具有全局性,在程序運行期間都會有效?

A:全局變量、字符常量一般存放在已初始化數據區、未初始化數據區,它不像棧和堆一樣會在進程運行期間創建和銷毀,在之前講的進程地址空間可知,進程地址空間與物理地址的映射關系在進程存在期間一直存在,所以這部分地址的映射關系不會改變,隨著進程一直存在,且全局變量的地址可以被整個程序使用。

Q:為什么父進程的環境變量能夠被子進程繼承?

A:因為在進程內存空間中命令行參數與環境變量也有一塊區域用于存放他們的數據,父進程創建子進程的時候會寫時拷貝到子進程,因此子進程可以繼承父進程的環境變量。

五、總結

AI生成
本文介紹了進程地址空間的基本概念,通過實驗展示了父子進程共享相同虛擬地址但實際物理地址不同的現象,解釋了虛擬地址與物理地址的區別。文章詳細分析了進程地址空間的內存布局結構(包括代碼段、數據段、堆棧等區域),并通過代碼示例驗證了各段地址的分布規律。重點闡述了操作系統如何通過mm_struct結構體管理進程的虛擬地址空間,以及頁表機制在虛擬地址到物理地址轉換中的作用。最后指出操作系統的內存管理機制使每個進程都擁有獨立的地址空間視圖,保證了進程間的隔離性。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/83040.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/83040.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/83040.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

深入探討redis:主從復制

前言 如果某個服務器程序&#xff0c;只部署在一個物理服務器上就可能會面臨一下問題(單點問題) 可用性問題&#xff0c;如果這個機器掛了&#xff0c;那么對應的客戶端服務也相繼斷開性能/支持的并發量有限 所以為了解決這些問題&#xff0c;就要引入分布式系統&#xff0c…

MacOS安裝Docker Desktop并漢化

1. 安裝Docker Desktop 到Docker Desktop For Mac下載對應系統的Docker Desktop 安裝包&#xff0c;下載后安裝&#xff0c;沒有賬號需要注冊&#xff0c;然后登陸即可。 2. 漢化 前往漢化包下載鏈接下載對應系統的.asar文件 然后將安裝好的文件覆蓋原先的文件app.asar文件…

索引的選擇與Change Buffer

1. 索引選擇與Change Buffer 問題引出&#xff1a;普通索引 vs 唯一索引 ——如何選擇&#xff1f; 在實際業務中&#xff0c;如果一個字段的值天然具有唯一性&#xff08;如身份證號&#xff09;&#xff0c;并且業務代碼已確保無重復寫入&#xff0c;那就存在兩種選擇&…

lua注意事項

感覺是lua的一大坑啊&#xff0c;它還不如函數內部就局部變量呢 注意函數等內部&#xff0c;全部給加上local得了

【多線程初階】死鎖的產生 如何避免死鎖

文章目錄 關于死鎖一.死鎖的三種情況1.一個線程,一把鎖,連續多次加鎖2.兩個線程,兩把鎖3.N個線程,M把鎖 --哲學家就餐問題 二.如何避免死鎖死鎖是如何構成的(四個必要條件)打破死鎖 三.死鎖小結 關于死鎖 一.死鎖的三種情況 1.一個線程,一把鎖,連續多次加鎖 -->由synchroni…

【NLP基礎知識系列課程-Tokenizer的前世今生第二課】NLP 中的 Tokenizer 技術發展史

從詞表到子詞&#xff1a;Tokenizer 的“進化樹” 我們常說“語言模型是理解人類語言的工具”&#xff0c;但事實上&#xff0c;模型能不能“理解”&#xff0c;關鍵要看它接收到了什么樣的輸入。而 Tokenizer&#xff0c;就是這一輸入階段的設計者。 在 NLP 的發展歷程中&am…

Rust 學習筆記:循環和迭代器的性能比較

Rust 學習筆記&#xff1a;循環和迭代器的性能比較 Rust 學習筆記&#xff1a;循環和迭代器的性能比較示例 1示例 2總結 Rust 學習筆記&#xff1a;循環和迭代器的性能比較 示例 1 我們運行一個基準測試&#xff0c;將《福爾摩斯探案集》的全部內容加載到一個字符串中&#x…

pod創建和控制

一、引言 ?主題?&#xff1a;pod以及控制器模式中的Deployment作用。?控制器模式&#xff1a;使用一種API對象&#xff08;如Deployment&#xff09;管理另一種API對象&#xff08;如Pod&#xff09;的方式。 二、容器鏡像與配置文件 ?容器鏡像?&#xff1a;應用開發者…

HTML實戰:愛心圖的實現

設計思路 使用純CSS創建多種風格的愛心 添加平滑的動畫效果 實現交互式愛心生成器 響應式設計適應不同設備 優雅的UI布局和色彩方案 <!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8"> <meta nam…

2022年 中國商務年鑒(excel電子表格版)

2022年 中國商務年鑒&#xff08;excel電子表格版&#xff09;.ziphttps://download.csdn.net/download/2401_84585615/89772883 https://download.csdn.net/download/2401_84585615/89772883 《中國商務年鑒2022》是由商務部國際貿易經濟合作研究院主辦的年度統計資料&#xf…

Redis核心數據結構操作指南:字符串、哈希、列表詳解

注&#xff1a;此為蒼穹外賣學習筆記 Redis作為高性能的鍵值數據庫&#xff0c;其核心價值來自于豐富的數據結構支持。本文將深入解析字符串&#xff08;String&#xff09;、哈希&#xff08;Hash&#xff09;、**列表&#xff08;List&#xff09;**三大基礎結構的操作命令&…

如何以 9 種方式將照片從 iPhone 傳輸到筆記本電腦

您的 iPhone 可能充滿了以照片和視頻形式捕捉的珍貴回憶。無論您是想備份它們、在更大的屏幕上編輯它們&#xff0c;還是只是釋放設備上的空間&#xff0c;您都需要將照片從 iPhone 傳輸到筆記本電腦。幸運的是&#xff0c;有 9 種方便的方法可供使用&#xff0c;同時滿足 Wind…

如何使用Python從MySQL數據庫導出表結構到Word文檔

在開發和維護數據庫的過程中&#xff0c;能夠快速且準確地獲取表結構信息是至關重要的。本文將向您展示一種簡單而有效的方法&#xff0c;利用Python腳本從MySQL數據庫中提取指定表的結構信息&#xff0c;并將其導出為格式化的Word文檔。此方法不僅提高了工作效率&#xff0c;還…

寫作-- 復合句練習

文章目錄 練習 11. 家庭的支持和老師的指導對學生的學術成功有積極影響。2. 缺乏準備和未能適應通常會導致在挑戰性情境中的糟糕表現。3. 吃垃圾食品和忽視鍛煉可能導致嚴重的健康問題,因此人們應注重保持均衡的生活方式。4. 昨天的大雨導致街道洪水泛濫,因此居民們遷往高地以…

QT使用說明

QT環境準備 推薦Ubuntu平臺上使用&#xff0c;配置簡單&#xff0c;坑少。 Ubuntu 20.04 安裝 sudo apt-get install qt5-default -y sudo apt-get install qtcreator -y sudo apt-get install -y libclang-common-8-dev啟動 qtcreatorHelloWorld 打開 Qt Creator。選擇 …

React 第四十九節 Router中useNavigation的具體使用詳解及注意事項

前言 useNavigation 是 React Router 中一個強大的鉤子&#xff0c;用于獲取當前頁面導航的狀態信息。 它可以幫助開發者根據導航狀態優化用戶體驗&#xff0c;如顯示加載指示器、防止重復提交等。 一、useNavigation核心用途 檢測導航狀態&#xff1a;判斷當前是否正在進行…

列表單獨展開收起同時關閉其余子項的問題優化

如圖所示&#xff0c;當在列表中&#xff0c;需要分別單獨點開子選項時&#xff0c;直接這樣用一個index參數判斷即可&#xff0c;非常簡單方便&#xff0c;只需要滿足點開當前index,然后想同index用null值自動關閉即可

WPF【11_5】WPF實戰-重構與美化(MVVM 實戰)

11-10 【重構】創建視圖模型&#xff0c;顯示客戶列表 正式進入 MVVM 架構的代碼實戰。在之前的課程中&#xff0c; Model 和 View 這部分的代碼重構實際上已經完成了。 Model 就是在 Models 文件夾中看到的兩個文件&#xff0c; Customer 和 Appointment。 而 View 則是所有與…

LangChain-結合魔塔社區modelscope的embeddings實現搜索

首先要安裝modelscope pip install modelscope 安裝完成后測試 from langchain_community.embeddings import ModelScopeEmbeddingsembeddings ModelScopeEmbeddings(model_id"iic/nlp_gte_sentence-embedding_chinese-base")text "這是一個測試句子"…

可定制化貨代管理系統,適應不同業務模式需求!

在全球化貿易的浪潮下&#xff0c;貨運代理行業扮演著至關重要的角色。然而&#xff0c;隨著市場競爭的日益激烈&#xff0c;貨代企業面臨著越來越多的挑戰&#xff1a;客戶需求多樣化、業務流程復雜化、運營成本上升、利潤空間壓縮……這些挑戰迫使貨代企業不斷尋求創新和突破…