rustdesk編譯修改名字

最近,我用Rust重寫了一個2W+行C代碼的linux內核模塊。在此記錄一點經驗。我此前沒寫過內核模塊,認識比較疏淺,有錯誤歡迎指正。

為什么要重寫?


這個模塊2W+行代碼量看起來不多,卻在線上時常故障,永遠改不完。十多年的老代碼,經手了無數程序員,沒人能解決其中的內存安全問題。拿過來一看,代碼中的確有不少會產生UB的寫法,線上的故障從core來看都飄得太遠,難以定位根本原因在哪里。所以我沒有把握(沒有能力)在原代碼基礎上能將所有線上故障修復。 而Rust是一個現代的、高性能、無GC、內存安全的編程語言,我想它非常適合用來重寫這個內核模塊。

Hello World


首先來介紹下如何用Rust寫linux內核模塊吧。也可以參考這里, 該項目正在嘗試寫一個safe的rust內核框架,目前的狀態還不實用,我沒使用該框架,僅參考了其基本編譯配置。

基本思路就是分別建立一個linux內核c工程和rust的hello world工程,把它們放到一塊兒(不放到一塊兒也行),文件分布如下:

├── Cargo.toml
├── Makefile
├── mydriver.c
└── src└── lib.rs


然后在linux內核模塊的入口和出口函數分別調用rust中實現的入口和出口函數,rust中將入口、出口函數標記為extern "C",所有業務邏輯在Rust中完成。

// mydriver.c
// ... include headersextern int my_drv_init(void); // defined in rust
extern void my_drv_exit(void); // defined in ruststatic int _my_drv_init(void)
{printk("loading my drivern");return my_drv_init();
}static void _my_drv_exit(void)
{printk("exiting my drivern");my_drv_exit();
}module_init(_my_drv_init);
module_exit(_my_drv_exit);
// lib.rs
#[no_mangle]
pub extern "C" fn my_drv_init() -> i32 {KLogger::install();info!("loading my driver in rust");0
}#[no_mangle]
pub extern "C" fn my_drv_exit() {info!("exiting my driver in rust");
}


Cargo.toml中需要配置輸出staticlib:

[lib]
name = "mydriver"
crate-type = ["staticlib", "rlib"]


模塊的Makefile調用cargo編譯rust庫,然后將其和c一塊兒鏈接成ko,大概這個樣子:

MODNAME = mydriverKDIR ?= /lib/modules/$(shell uname -r)/build
BUILD_TYPE = release
LIB_DIR = target/$(ARCH)-linux-kernel/$(BUILD_TYPE)all:$(MAKE) -C $(KDIR) M=$(CURDIR)clean:$(MAKE) -C $(KDIR) M=$(CURDIR) cleanrm -rf targetrlib:# 目前需要nightly才能編譯core和alloc.cargo +nightly build --$(BUILD_TYPE) -Z features=dev_dep,build_dep -Z build-std=core,alloc --target=$(ARCH)-linux-kernelobj-m := $(MODNAME).o$(MODNAME)-objs := mydriver.o mydriver.rust.o.PHONY: $(src)/lib$(MODNAME).a
$(src)/lib$(MODNAME).a:cd $(src); make rlibcd $(src); cp $(LIB_DIR)/lib$(MODNAME).a .%.rust.o: lib%.a$(LD) -r -o $@.tmp --whole-archive $<$(src)/plt2pc.py $@.tmp $@


可行性評估


用Rust寫linux內核模塊還是有些擔憂,目前還沒看到Rust內核模塊相關的嚴肅開源項目,Demo倒是有兩個。動手之前,咱們還是盡可能評估一下可行性。之前有了解到有工具C2Rust可以將C代碼轉換成Rust代碼,所以,我的想法是先用C2Rust將原有C代碼轉成Rust,看能不能編譯跑起來,各功能是否正常,看看有沒有什么硬傷。如果能正常使用,則可以在轉出的代碼的基礎上逐漸將unsafe rust重構為safe rust。

C2Rust工作流

按照C2Rust相關文檔操作下來,遇到幾個問題:

轉換時內核頭文件的時候報錯。

/usr/src/kernels/.../arch/x86/include/asm/jump_label.h:16:2: error: 'asm goto' constructs are not supported yetasm_volatile_goto("1:"^
include/linux/compiler-gcc4.h:79:43: note: expanded from macro 'asm_volatile_goto'
# define asm_volatile_goto(x...) ? ? ? ?do { asm goto(x); asm (""); } while (0)


據C2Rust文檔介紹,需要最新的libclang才能支持此語法。

2. 轉換后的代碼編譯報錯。

編譯錯誤大致分為memcpy宏、內聯匯編錯誤、依賴libc crate幾類。

以上錯誤中,libc的依賴僅僅使用了libc中定義的一些C語言基本類型,因此,可以寫一個簡單的libc crate替代。其它錯誤均通過臨時修改內核頭文件,將不支持的語法define成其他替代品規避。

3. 編譯成功后的ko文件加載報錯。

加載ko報如下錯誤:

insmod: ERROR: could not insert module mp.ko: Invalid module format


dmesg顯示:

Unknown rela relocation: 4


這是由于Rust編譯器(LLVM)生成的二進制中對于extern “C”函數的訪問,采用的是R_X86_64_PLT32標記重定位,Linux4.15內核開始支持此標記,而我們使用的3.x內核僅支持R_X86_64_PC32標記。內核中相應提交可以看出內核對這兩個標記是無區別對待的:

"PLT32 relocation is used as marker for PC-relative branches. Becauseof EBX, it looks odd to generate PLT32 relocation on i386 when EBXdoesn't have GOT.As for symbol resolution, PLT32 and PC32 relocations are almostinterchangeable. But when linker sees PLT32 relocation against aprotected symbol, it can resolved locally at link-time since it isused on a branch instruction. Linker can't do that for PC32relocation"but for the kernel use, the two are basically the same, and thiscommit gets things building and working with the current binutilsmaster ? - Linus


因此,我們可以簡單地將編譯出的二進制文件中的PLT32標記替換為PC32就能解決此問題。readelf命令可以幫我們找出這些標記都在什么位置,故甚至都不需要了解elf文件結構,可以寫腳本完成替換:

#!/usr/bin/env pythonimport sys
import os
import repy3 = sys.version_info.major >= 3def get_relocs(filename):"""readelf output:Relocation section '.rela.text' at offset 0x1e8 contains 1 entry:Offset ? ? ? ? ?Info ? ? ? ? ? Type ? ? ? ? ? Sym. Value ? ?Sym. Name + Addend
00000000000a ?000a00000002 R_X86_64_PC32 ? ? 0000000000000000 hello - 4Relocation section '.rela.eh_frame' at offset 0x200 contains 1 entry:Offset ? ? ? ? ?Info ? ? ? ? ? Type ? ? ? ? ? Sym. Value ? ?Sym. Name + Addend
000000000020 ?000200000002 R_X86_64_PC32 ? ? 0000000000000000 .text + 0"""relocs = []sec = ''idx = 0os.environ["LANG"] = ''f = os.popen('readelf -r "%s"' % filename)while True:line = f.readline()if not line:breakif line.startswith('Relocation section'):arr = re.findall(r'0x[0-9a-f]*', line)sec = int(arr[0], base=16)idx = 0f.readline()continueoff = idx * 24 + 8idx += 1arr = line.strip().split()[:4]if len(arr) != 4:continueoffset, info, typ, val = arrif typ != 'R_X86_64_PLT32':continuerelocs.append((sec, off, val))return relocsdef main():PLT32 = 4 if py3 else 'x04'PC32 = 2 if py3 else 'x02'infile = sys.argv[1]outfile = sys.argv[2] if len(sys.argv) == 3 else infileobj = list(open(infile, 'rb').read())for sec, offset, val in get_relocs(infile):goff = sec + offsetassert obj[goff] == PLT32obj[goff] = PC32out_bin = bytes(obj) if py3 else ''.join(obj)open(outfile, 'wb').write(out_bin)if __name__ == '__main__':main()


解決了reloc問題后模塊就能正常加載了,且經測試,各項功能均和原版相同,連bug都一樣。至此,我們用C2Rust完成了一個和原模塊等效的Rust版本。如此順利且真的等效有些出乎意料,相比其他語言中類似的工具(往往需要大量修改轉換后源代碼才能編譯且很難做到等效),C2Rust還是很給力的(用C2Rust轉換的代碼包含2W+行模塊主體代碼和8W行的第三方庫)。

用Rust重寫


重構unsafe的痛
正如預期,用C2Rust轉出來rust沒有safe代碼,一律unsafe。我們需要將其重構為safe代碼。簡短地實踐下來,發現重構轉換出的代碼非常痛苦。

例1,C中的宏調用會被展開,大部分宏展開的結果非常難看,這也直接導致生成的代碼行數膨脹為原版的3-4倍。如,原版代碼是這樣:

do_something(ntohl(info->port), ntohl(info->event));


轉換后變成這樣:

do_something(if 0 != 0 {(((*info).port &0xff as libc::c_ulong as __u32) <<24 as libc::c_int |((*info).port &0xff00 as libc::c_ulong as __u32)<< 8 as libc::c_int |((*info).port &0xff0000 as libc::c_ulong as__u32) >> 8 as libc::c_int) |((*info).port &0xff000000 as libc::c_ulong as__u32) >> 24 as libc::c_int} else { __fswab32((*info).port) },if 0 != 0 {(((*info).event &0xff as libc::c_ulong as __u32) <<24 as libc::c_int |((*info).event &0xff00 as libc::c_ulong as __u32)<< 8 as libc::c_int |((*info).event &0xff0000 as libc::c_ulong as__u32) >> 8 as libc::c_int) |((*info).event &0xff000000 as libc::c_ulong as__u32) >> 24 as libc::c_int} else { __fswab32((*info).event) });


例2, 大量的類型強轉,讓人看不清代碼邏輯。如:

Temp0 = ?do_something(Koeff0, Vk1_0 << 1 as libc::c_int) - Vk2_0 +*arraySamples.offset(ii as isize) as libc::c_int;
Temp1 = Temp1 as __s16 as libc::c_int * Vk2_1 as __s16 as libc::c_int;


每去除一個強轉,都要去斟酌一下是不是和原版等效的(c2rust之所以這么寫,是為了和C中默認的類型提升規則等效)。

例3,每個c文件對應轉換出一個獨立的rs文件,包括C中引用的頭文件中的各種聲明和類型定義,都獨立地在每個rs文件中重復、亂序地定義一份,難以整合。
例4,Rust不支持goto語句,于是c2rust用許多的if/else來模擬c中goto語句,我是比較佩服這么機智的處理方法,但是要重構它就難以看清了。
......
當然,c2rust有個refactor命令,里面許多實驗性的工具來幫助減輕重構的負擔,包括上面遇到的問題,不過使用下來感覺這些工具都不成熟,比較難用。于是,還是決定參照原版功能邏輯,重寫一個吧。

墊腳層


rust程序要在內核工作少不了要和內核交互,這就需要ffi調用內核的一些“API”來完成特定工作。內核的API都聲明在內核頭文件中,理論上我們可以用rust-bindgen直接輸出kernel-bindings.rs來使用這些API。

實踐中,一方面,有少部分的類型bind后無法編譯;另一方面,由于內核頭文件有大量的參數宏和static inline函數,這些API目前無法通過rust-bindgen完成綁定,使得rust-bindgen的意義大大縮減。c2rust倒是可以處理static inline函數,但是c2rust目前綁死到了特定nightly版本上才能用。因此,我還是決定對要用到的內核函數封裝一個墊腳層ksys.c中轉一下,使用rust-bindgen綁定ksys.h,這樣會比較簡單穩定。例如,memcpy的綁定:

原始定義:

#define memcpy(dst, src, len) ? ? ? ? ? ? ? ? ??
({ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?size_t __len = (len); ? ? ? ? ? ? ? ? ??void *__ret; ? ? ? ? ? ? ? ? ? ? ? ?if (__builtin_constant_p(len) && __len >= 64) ? ? ??__ret = __memcpy((dst), (src), __len); ? ? ?else ? ? ? ? ? ? ? ? ? ? ? ? ? ?__ret = __builtin_memcpy((dst), (src), __len); ?__ret; ? ? ? ? ? ?


ksys.h中:

void *ksys_memcpy(void *dest, const void *src, size_t n);


ksys.c中:

void *ksys_memcpy(void *dst, const void *src, size_t n) {return memcpy(dst, src, n);
}


binding結果:

extern "C" {pub fn ksys_memcpy(dest: *mut c_types::c_void,src: *const c_types::c_void,n: usize,) -> *mut c_types::c_void;


這樣實現會導致Rust編譯器不能inline這些函數,從而對性能有一定影響,后續等rust-bindgen完善了再切換過去。

造輪子


內核態寫rust沒有標準庫可用,因此,需要造一些基礎設施的輪子,以及內核API函數的安全封裝。包括lock、channel、fs、net、thread、timer、logger等。當然,不造這些輪子也能實現功能,需要的地方直接調用內核API來完成相關功能就好了...這樣的話,干嘛還用Rust呢?造輪子是常規操作,有大量crate可參考,就不細說了,channel部分遇到一個小坑,后文講述。

棧溢出


程序寫完運行起來遇到的第一個坑是棧溢出,Linux內核線程的棧很小(x86上16KB),容易溢出。debug編譯模式就不說了,一句帶格式的log就能把棧爆掉。我就只講一下release模式,release編譯的程序編譯器會盡可能地優化棧空間的使用,也正是因為編譯器的優化的存在,我們要從代碼中肉眼找出棧空間使用的最深路徑變得困難。幸運的是嵌入式工作組的老大@japaric開發了一個不起眼的工具cargo-call-stack專門用來分析棧空間的使用情況,效果如下圖:

cargo call-stack 輸出

利用該工具,我們可以一瞬間找出棧使用最深點和量,然后順騰摸瓜在代碼中逐個優化掉。

至于哪些寫法會影響編譯器對棧的優化,我沒有太細致的總結,就簡短寫一點吧。不用cargo-call-stack我們可以按照類似下面這樣寫來分析各種寫法對編譯優化的影響:

#![feature(test)]
#![feature(box_syntax)]use std::hint::black_box;static mut BOTTOM: usize = 0;#[inline(never)]
fn anchor_bottom() {let mut v = 0;unsafe { BOTTOM = (&mut v) as *mut i32 as _ };
}#[inline(never)]
fn depth() -> usize {let mut v = 0;unsafe { BOTTOM ?- ((&mut v) as *mut i32 as usize) }
}fn main() {anchor_bottom(); // 標定棧底test_entry();
}#[inline(never)]
fn test_entry() {// 在這里測試各種寫法的影響let mut msg = Message::new();println!("stack size = {}", depth());black_box(&msg); // 防止編譯器認為msg無用而整體優化掉了。
}struct Message {id: usize,data: [u8; 1000],
}impl Message {// inline影響探針的功能,禁掉#[inline(never)]fn new() -> Self {let mut msg = Self::default();println!("stack size in new = {}", depth());msg}
}


執行上面的代碼執行結果:

// debug編譯:
stack size in new = 2320
stack size = 1152
// release編譯:
stack size in new = 1200
stack size = 1104


說明release下new里面的msg變量棧使用被優化了,Self::default()的返回值直接放到了test_entry這幀的msg里面。

這里主要想說兩點:

Box::new(value)會先把value放到棧上,然后copy進堆里面,使用unstable的box關鍵字可以解決。
fn test_entry() {let mut v = Box::new(Message::new());println!("stack size = {}", depth());black_box(&v);
}
// output:
// ?? ?stack size = 1056


換成box:

fn test_entry() {let mut v = box Message::new();println!("stack size = {}", depth());black_box(&v);
}
// output:
// ?? ?stack size = 96


把棧變量的地址傳給ffi函數會阻止編譯器優化該變量,例如,上面的new改成:

fn new() -> Self {let mut msg = Self::default();black_box(&msg);println!("stack size in new = {}", depth());msg}


則會變成:

stack size in new = 2224

cargo-call-stack番外


cargo-call-stack并不能拿來即用,安裝一執行便報一行30MB的錯誤(沒錯,一行,30M):

Failure(("define internal fastcc void @_ZN3std10sys_common9backtrace28__rust_begin_short_backtrace17ha028a22ae68de0a6E(i8* ......


這是由于call-stack通過分析llvm IR來獲得所有函數的調用關系,從而構圖計算評估棧空間。而有些IR語法它并不能識別(工具太小眾了照顧不全),只好自己動手添加不識別的語法支持,對于我遇到的幾個不支持的語法,我已添加并提交了PR。

修完語法問題后就能輸出call-stack圖了,然而并沒有得到其主頁介紹的那美美的圖片,得到的是這樣:

實踐中cargo call-stack的輸出

節點太多,根本無法動彈,換了幾個軟件均沒有理想的查看效果。那就自己動手吧,給call-stack添加一個tui前端,這樣瀏覽起來就方便多了:

添加的cargo call-stack的tui前端

Rust的函數沒有顏色


在支持類協程(如Rust的async/await)編程語言中存在這樣一個問題:協程(async)函數中要避免調用阻塞函數,否則會影響協程的調度。而實踐中編譯器往往沒有做到編譯時檢查出協程中調用阻塞而給出提示,完全依靠人小心避免。Rust社區有嘗試從各種角度解決此問題,比如這里,這里,還有這里,目前沒有什么進展。有人用函數的顏色來描述討論此問題。

而到了內核里,類似的問題就更加凸顯出來。

例如,在內核態,在中斷上下文、獲得spinlock等場景下不允許程序休眠(放棄CPU),否則會導致死鎖或影響系統性能。和用戶態的區別是用戶態用錯了影響一個服務的性能,而內核里用錯了會整個系統垮掉。中斷和spinlock都是寫內核態程序常常要面對的,而內核的API中會sleep的函數里遍地都是,并且不會像用戶態的libc有清晰規范的文檔,這就導致完全依靠人為小心避免變得更困難。如果rust有某種機制,在編譯時禁止或提示這類危險上下文調用某種顏色的函數是不是會更好呢?

又例如,這次我踩到的一個坑:我一開始便使用spin這個crate實現了一個channel用于線程間通信,使用前還專門看了issue,安全審計團隊對這個crate的安全性審計過了,因此比較放心。我把這個channel用在了定時器中給一個服務線程發消息,程序跑起來后就發現時而卡死(死鎖)。看內核文檔得知spinlock用于中斷上下文是有文章的,道理很簡單,內核態一個線程隨時可能被中斷服務程序中斷了,去處理更緊急的事情,但如果被中斷的線程正拿著一個鎖,而此時中斷服務也試圖去獲取同一個鎖就會導致死鎖。內核文檔的描述:

The reasons you mustn't use these versions if you have interrupts that
play with the spinlock is that you can get deadlocks:spin_lock(&lock);...<- interrupt comes in:spin_lock(&lock);



解決辦法就是如果中斷程序里面要獲取一個鎖,則所有獲取該鎖的代碼都要先屏蔽中斷,然后再去拿鎖。內核中因此將spinlock的api分為了幾組:

void spin_lock(spinlock_t *lock);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);


其中后兩個是會屏蔽中斷的,而Rust的spin crate并不會屏蔽中斷,因此導致死鎖。因此,放棄spin crate,封裝一個內核版本spin解決了此問題。如果rust有某種機制,在編譯時禁止或提示中斷上下文中獲取沒有屏蔽中斷的鎖是不是會更好呢?

有些語言中有Effect System來解決這類問題,例如nim語言允許我們對函數標記額外的副作用:

type IO = object # 定義IO副作用
proc readLine(): string {.tags: [IO].} = discard ?# 標記readLine函數具有IO副作用proc no_IO_please() {.tags: [].} = # 標記此函數不允許IO副作用# 編譯器將拒絕此行代碼let x = readLine()


?


避免感覺語法怪異,我將其翻譯為rust風格的偽代碼:

struct IO; // 定義IO副作用

#[tags([IO])] // 標記readline函數具有IO副作用
fn readline() -> String {todo!()
} ?#[tags([])] // 標記此函數不允許IO副作用
fn no_IO_please() {let x = readline(); //編譯器將拒絕此行代碼...
}


?


目前Rust里面函數只有safe/unsafe兩種顏色,沒有更多色深,感覺有些單調。Rust大佬們的討論中也提到了此特性,但目前的情況看,應該短期不會有進展。

不過好在實踐(我的)過程中,無論是中斷還是spinlock上下文,代碼都會非常簡短,影響沒那么大。只要腦子里知道這個知識點,一般就不會再出差錯了。

多姿的內存分配函數
內核中為了提高效率,有各式各樣堆內存分配函數選擇,大塊的/小塊的、是否保證物理連續、是否會sleep、是否觸碰文件系統......。不同的場景需要使用不同的API來分配堆內存。來瞧一瞧:

void *kmalloc(size_t size, gfp_t flags);
void *kcalloc(size_t n, size_t size, unsigned int __nocast gfp_flags);
void *kzalloc(size_t size, unsigned int __nocast gfp_flags);
void *vmalloc(unsigned long size);
void *kvmalloc(size_t size, gfp_t flags);
void *kvzalloc(size_t size, gfp_t flags);
void *kvmalloc_node(size_t size, gfp_t flags, int node);
void *kvzalloc_node(size_t size, gfp_t flags, int node);


其中flags又有這些選擇:

#define GFP_ATOMIC ?(__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL ?(__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
#define GFP_NOWAIT ?(__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO ? ?(__GFP_RECLAIM)
#define GFP_NOFS ? ?(__GFP_RECLAIM | __GFP_IO)
#define GFP_USER ? ?(__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA ? ? __GFP_DMA
#define GFP_DMA32 ? __GFP_DMA32
#define GFP_HIGHUSER ? ?(GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE ? ?(GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_TRANSHUGE_LIGHT ((GFP_HIGHUSER_MOVABLE | __GFP_COMP |?__GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM)
#define GFP_TRANSHUGE ? (GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM)
/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3


?


突然明白了為什么zig語言設計成處處調用需要手動傳入一個allocator。

而Rust的alloc crate只有一個自定義接口,這就導致只能選擇一種,并且需要人為避免在不合適的場景觸發Rust的alloc導致的堆內存分配,其它場景的分配恐怕就要繞過alloc crate另外實現了。目前,為兼容大部分場景,暫且這樣實現分配器:

use crate::ffi;
use core::alloc::{GlobalAlloc, Layout};pub struct KernelAllocator;unsafe impl GlobalAlloc for KernelAllocator {unsafe fn alloc(&self, layout: Layout) -> *mut u8 {// FIXME: kernel does not support custom alignment。// ? ?kmalloc has some sort of guarantee.// ? ?See: https://lwn.net/Articles/787740/let size = layout.size();if size <= PAGE_SIZE {return ffi::kmalloc(size, GFP_KERNEL);} else {return ffi::vmalloc(size);}}unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {if layout.size() <= PAGE_SIZE {return ffi::kfree(ptr);} else {return ffi::vfree(ptr);}}
}


這就需要人為避免在中斷、spinlock等場景觸發Rust的alloc crate中的內存分配。好在實踐過程中沒有遇到這些場景下需要分配堆內存的情況。

結語


雖然遇到一些小坑,但瑕不掩瑜,使用Rust最大的好處就是內存安全,寫完這種安心的感覺會讓人覺得上述那些過程中的坑、額外的工作都是小事兒。只要把好ffi這關,今后因為各隊友的疏忽而引入各種難查的UB將難以再發生。
?

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

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

相關文章

在linux系統中安裝Anaconda,并使用conda

系統 : ubuntu20.04 顯卡&#xff1a;NVIDIA GTX1650 目錄 安裝Anaconda第一步&#xff1a;下載合適版本的Anconda1. 查看自己Linux的操作系統及架構命令&#xff1a;uname -a2. 下載合適版本的Anconda 第二步&#xff1a;安裝Aanconda1. 為.sh文件設置權限2. 執行.sh文件2.1 .…

(前端基礎)HTML(一)

前提 W3C:World Wide Web Consortium&#xff08;萬維網聯盟&#xff09; Web技術領域最權威和具有影響力的國際中立性技術標準機構 其中標準包括&#xff1a;機構化標準語言&#xff08;HTML、XML&#xff09; 表現標準語言&#xff08;CSS&#xff09; 行為標準&#xf…

【ISO 14229-1:2023 UDS診斷(會話控制0x10服務)測試用例CAPL代碼全解析③】

ISO 14229-1:2023 UDS診斷【會話控制0x10服務】_TestCase03 作者&#xff1a;車端域控測試工程師 更新日期&#xff1a;2025年02月15日 關鍵詞&#xff1a;UDS診斷、0x10服務、診斷會話控制、ECU測試、ISO 14229-1:2023 TC10-003測試用例 用例ID測試場景驗證要點參考條款預期…

TDengine 客戶端連接工具 taos-Cli

簡介工具獲取運行命令行參數 基礎參數高級參數 數據導出/導入 數據導出數據導入 執行 SQL 腳本使用小技巧 TAB 鍵自動補全設置字符列顯示寬度其它 錯誤代碼表 簡介 TDengine 命令行工具&#xff08;以下簡稱 TDengine CLI&#xff09;是用戶操作 TDengine 實例并與之交互最簡…

Git高級用法

GIT高級用法及實戰案例解析 前言 作為現代開發者的必備工具&#xff0c;Git的基礎操作&#xff08;add/commit/push&#xff09;早已深入人心。但當面對復雜場景時&#xff0c;掌握Git的高級用法將極大提升開發效率。本文將深入解析Git的高級功能&#xff0c;并配合真實場景案…

9個用于測試自動化的最佳AI測試工具(2024)

選擇一款優質的基于生成式AI人工智能的測試工具能夠確保測試過程的準確性和效率&#xff0c;從而加速整個軟件測試周期。相反&#xff0c;設計不佳的測試工具可能無法發現錯誤&#xff0c;并可能存在安全問題。它們可能產生誤報或漏報&#xff0c;誤導開發與測試團隊&#xff0…

vue-model如何自定義指令,及批量注冊自定義指令

一、在Vue.js中&#xff0c;v-model是一個用于在表單輸入和應用狀態之間創建雙向綁定的指令。要編寫自定義的v-model指令&#xff0c;你需要使用Vue的自定義指令API。以下是編寫自定義v-model指令的步驟&#xff1a; 定義一個自定義指令對象。在指令對象的bind鉤子函數中&…

簡單認識一下-Redis

一、什么是Redis Redis&#xff08;Remote Dictionary Server&#xff09;是一個開源的、基于內存的數據結構存儲系統&#xff0c;它既可以用作數據庫、緩存&#xff0c;也可以作為消息中間件使用。以下為你詳細介紹 Redis&#xff1a; 基本特點 高性能&#xff1a;Redis 將數…

LabVIEW的吞雨測控系統

本案例介紹了一種基于LabVIEW開發的吞雨測控系統&#xff0c;該系統通過建模仿真分析不同控制器模式下的階躍信號響應&#xff0c;從而選擇了最適合的控制器。為了有效解決在控制流量過程中出現的振蕩收斂和流量信號大擾動問題&#xff0c;系統采用了改進的積分分離PID算法&…

C++中的順序容器(一)

文章目錄 順序容器概述所有容器類型都支持的操作迭代器容器定義與初始化將一個容器初始化為另一個容器的拷貝標準庫array具有固定大小 賦值和swap關系運算符 順序容器的特有操作向順序容器添加元素訪問元素刪除元素特殊的forward_list操作改變容器的大小容器操作可能是迭代器失…

Javaweb中,使用Servlet編寫簡單的接口

案例&#xff1a;網頁提交用戶名和密碼信息&#xff0c;后端校驗密碼長度需在6-12位之間 后端部分 WebServlet("/valid") public class SimpleServlet extends HttpServlet{public void service(HttpServletRequest req, HttpServletResponse resp) throws IOExcepti…

C語言實現的常見排序算法

排序是計算機科學中非常重要的基礎算法之一。無論是在數據分析、數據庫查詢還是圖形界面中&#xff0c;我們都可能會遇到排序問題。本文將介紹幾種常見的排序算法&#xff0c;并提供其C語言實現代碼。排序算法的效率和應用場景有很大關系&#xff0c;不同的算法有不同的時間復雜…

對于簡單的HTML、CSS、JavaScript前端,我們可以通過幾種方式連接后端

1. 使用Fetch API發送HTTP請求&#xff08;最簡單的方式&#xff09;&#xff1a; //home.html // 示例&#xff1a;提交表單數據到后端 const submitForm async (formData) > {try {const response await fetch(http://your-backend-url/api/submit, {method: POST,head…

[論文閱讀] SeeSR: Towards Semantics-Aware Real-World Image Super-Resolution

文章目錄 一、前言二、主要貢獻三、Introduction四、Methodology4.1 Motivation &#xff1a;4.2Framework Overview.** 一、前言 通信作者是香港理工大學 & OPPO研究所的張磊教授&#xff0c;也是圖像超分ISR的一個大牛了。 論文如下 SeeSR: Towards Semantics-Aware Rea…

案例-04.部門管理-刪除

一.功能演示 二.需求說明 三.接口文檔 四.思路 既然是通過id刪除對應的部門&#xff0c;那么必然要獲取到前端請求的要刪除部門的id。id作為請求路徑傳遞過來&#xff0c;那么要從請求路徑中獲取&#xff0c;id是一個路徑參數。因此使用注解PathVariable獲取路徑參數。 請求方…

Blazor-父子組件傳遞任意參數

在我們從父組件傳參數給子組件時&#xff0c;可以通過子組件定義的[Parameter]特性的公開屬性進行傳值&#xff0c;但是當我們需要傳遞多個值的時候&#xff0c;就需要通過[Parameter]特性定義多個屬性&#xff0c;有沒有更簡便的方式&#xff1f; 我們可以使用定義 IDictionar…

DeepSeek 的創新融合:多行業應用實踐探索

引言 在數字化轉型的浪潮中&#xff0c;技術的融合與創新成為推動各行業發展的關鍵力量。藍耘平臺作為行業內備受矚目的創新平臺&#xff0c;以其強大的資源整合能力和靈活的架構&#xff0c;為企業提供了高效的服務支持。而 DeepSeek 憑借先進的人工智能技術&#xff0c;在自然…

STM32創建靜態庫lib

創建靜態庫lib 1. 新建工程1.1 創建工程文件夾1.2 編寫用戶相關代碼1.2.1 stm32f4xx_it.h1.2.2 stm32f4xx_it.c1.2.3 標準庫配置&#xff1a;stm32f4xx_conf.h1.2.4 HAL庫的配置&#xff1a;stm32f4xx_hal_conf.h1.2.5 LL庫配置&#xff1a;stm32f4xx_ll_conf.h 1.3 移植通用文…

elabradio入門第二講——BPSK數字調制與解調(插值、升余弦濾波、速率匹配、符號同步)

數字信號可以通過數字基帶傳輸系統進行傳輸&#xff0c;而基帶傳輸系統僅僅適用于低頻信道下的數字信號傳輸。然而&#xff0c;在實際的通信系統中信道通常具有帶通特性&#xff0c;因而需要將基帶信號搬移到適合信道傳輸的高頻載波上&#xff0c;使得信號與信道相匹配&#xf…

汽車 OTA 升級:提升下載與升級速度,優化用戶體驗

摘要&#xff1a; 隨著汽車智能化的飛速發展&#xff0c;OTA&#xff08;Over - the - Air&#xff09;升級已成為汽車行業的重要技術&#xff0c;它能為車輛持續帶來功能更新與性能優化。然而&#xff0c;下載及升級速度較慢的問題常常影響用戶體驗。本文深入探討在汽車 OTA …