從module_init看內核模塊

来源:https://www.cnblogs.com/kfggww/archive/2023/08/02/17598809.html
-Advertisement-
Play Games

module_init是linux內核提供的一個巨集, 可以用來在編寫內核模塊時註冊一個初始化函數, 當模塊被載入的時候, 內核負責執行這個初始化函數. 在編寫設備驅動程式時, 使用這個巨集看起來理所應當, 沒什麼特別的, 但畢竟我還是一個有點追求的程式員嘛:P, 這篇文章是我學習module_init... ...


開篇

module_init是linux內核提供的一個巨集, 可以用來在編寫內核模塊時註冊一個初始化函數, 當模塊被載入的時候, 內核負責執行這個初始化函數. 在編寫設備驅動程式時, 使用這個巨集看起來理所應當, 沒什麼特別的, 但畢竟我還是一個有點追求的程式員嘛:P, 這篇文章是我學習module_init相關源碼的一個記錄, 主要就回答了下麵的3個問題, 篇幅略長, 做好準備.

Q1:

內核模塊是什麼?

Q2:

內核模塊是怎麼被載入的?

Q3:

內核怎麼獲取到module_init註冊的初始化函數?


註: 以下回答是個人學習總結, 僅供參考.

A1:

編譯好內核模塊的代碼, 會得到一個".ko"文件, 這個就是內核模塊了. 實際上, ".ko"就是一個普通的ELF文件, 只不過可以使用insmod讓內核去動態載入它. 查閱ELF格式標準可知, 主要有三種類型的ELF文件, 包括:

  • relocatable file
  • excutable file
  • shared object file

以上三種類型的ELF, 基本上可以簡單對應編譯得到的".o", "a.out", ".so". 這裡討論的".ko"模塊文件, 屬於relocatable file類型, 可以在系統里找一個內核模塊文件驗證一下.

junan@ZEN2:/lib/modules/5.19.0-50-generic/kernel/drivers/char$ ls
agp  applicom.ko  hangcheck-timer.ko  hw_random  ipmi  lp.ko  mwave  nvram.ko  pcmcia  ppdev.ko  tlclk.ko  tpm  uv_mmtimer.ko  xillybus
junan@ZEN2:/lib/modules/5.19.0-50-generic/kernel/drivers/char$ file lp.ko 
lp.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=98c89bd841e31b1140e61559c0bf312eb5128f5c, not stripped

使用gcc編譯一個c文件, 可以得到對應的.o文件, 前面說過.o文件屬於relocatable類型的ELF, 如果有多個.o文件, 可以使用鏈接器把它們"合併"成一個.o, 就像這樣:

junan@ZEN2:~$ ls
a.c  a.o  b.c  b.o  Desktop  Documents  Downloads  Music  Pictures  Public  snap  Templates  Videos
junan@ZEN2:~$ file a.o b.o
a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
b.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
junan@ZEN2:~$ ld -r a.o b.o -o c.o
junan@ZEN2:~$ file c.o
c.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

經過ld鏈接之後, 得到了"c.o", 這個"c.o"相當於合併了"a.o"和"b.o", 這可以從它們各自包含的符號中看出來:

junan@ZEN2:~$ nm a.o b.o c.o

a.o:
                 U add
0000000000000000 T call_add

b.o:
0000000000000000 T add

c.o:
000000000000001a T add
0000000000000000 T call_add

同樣的道理, 一個設備驅動, 可能包含多個源文件, 但是編譯之後最終可以合併成一個".ko"文件. 總結下來, ".ko"也是一種普通的ELF文件, 可以被內核動態載入和卸載.

A2:

一個內核模塊, 除了自己實現一些功能外, 通常還要引用其他人提供的api, 包括:

  • 內核本身提供的api
  • 其他模塊提供的api

內核僅僅把".ko"文件讀到自己的地址空間中, 是遠遠不夠的, 他要像鏈接器一樣, 幫我們的內核模塊正確地處理這些符號引用關係, 並且調用我們使用module_init註冊的模塊初始化函數. 下麵分析一下這部分的內核源碼. 我們的".ko"文件是使用insmod命令才載入進內核的, insmod命令實際上是一個符號鏈接. 和insmod一樣, rmmod也指向/bin/kmod, 當kmod被執行的時候, 可以通過args[0]區分出是執行insmod還是rmmod, 或者其他的功能.

junan@ZEN2:~$ which insmod | xargs ls -l
lrwxrwxrwx 1 root root 9  7月 22 23:20 /usr/sbin/insmod -> /bin/kmod

在內核代碼中, 專門為模塊的載入和卸載提供了兩個系統調用, 準確說是三個, 其中兩個用於載入模塊, 一個用於卸載模塊. linux代碼中使用SYSCALL_DEFINEx這個巨集定義一個系統調用的入口, 其中x代表系統調用的參數個數, 看一下內核代碼就可以找到和模塊的載入以及卸載相關的syscall函數, 使用正則表達式或者其他工具能很快在內核代碼中找到這三個系統調用的定義.

首先是init_module和finit_module:

SYSCALL_DEFINE3(init_module, void __user *, umod,
		unsigned long, len, const char __user *, uargs)
{
    // ...
	return load_module(&info, uargs, 0);
}

SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
{
    // ...
	return load_module(&info, uargs, flags);
}

以上兩個syscall負責模塊的載入, 最終都調用了load_module去真正載入模塊.

然後是delete_module:

SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
		unsigned int, flags)
{
    // ...
}

這三個syscall定義在"kernel/module.c"文件中, 我使用的內核版本是5.4.250, 下麵可以寫一個什麼都不做的內核模塊, 通過gdb調試的方法, 看一下這幾個syscall是怎麼被調用的. 環境的準備包括:

  • qemu: 啟動編譯好的內核鏡像, 以及gdb server, 等待gdb的連接
  • rootfs: 內核正常啟動, 需要一個根文件系統, 使用busybox製作
  • kernel Image: 編譯好的內核鏡像
  • ko文件: 編譯好的沒有實際功能的內核模塊

關於怎麼建立內核的調試環境, 會在其他文章中說明, 這裡僅通過調試內核的方法, 記錄一下以上三個syscall的調用過程. 當你還不知道insmod和rmmod需要使用到這三個系統調用, 怎麼樣才能知道這兩個命令依賴什麼系統調用呢? 答案是可以使用strace去定位一個進程運行過程中用到了哪些syscall, 比如, 我的系統里有一個名字叫做lp的ko模塊:

junan@ZEN:~$ lsmod | grep lp
lp                     28672  0
drm_display_helper    184320  1 i915
cec                    81920  2 drm_display_helper,i915
drm_kms_helper        200704  2 drm_display_helper,i915

先把這個模塊卸載, 看看使用了什麼syscall:

junan@ZEN:~$ sudo strace rmmod lp
...
...
close(3)                                = 0
openat(AT_FDCWD, "/sys/module/lp/refcnt", O_RDONLY|O_CLOEXEC) = 3
read(3, "0\n", 31)                      = 2
read(3, "", 29)                         = 0
close(3)                                = 0
delete_module("lp", O_NONBLOCK)         = 0
exit_group(0)                           = ?
+++ exited with 0 +++

能看到倒數第3行, 調用了delete_module.

再重新載入這個lp模塊:

junan@ZEN:/usr/lib/modules/5.19.0-50-generic/kernel/drivers/char$ sudo strace insmod lp.ko
...
...
getcwd("/usr/lib/modules/5.19.0-50-generic/kernel/drivers/char", 4096) = 55
newfstatat(AT_FDCWD, "/usr/lib/modules/5.19.0-50-generic/kernel/drivers/char/lp.ko", {st_mode=S_IFREG|0644, st_size=72553, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/modules/5.19.0-50-generic/kernel/drivers/char/lp.ko", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1", 6)               = 6
lseek(3, 0, SEEK_SET)                   = 0
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=72553, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 72553, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f4675dac000
finit_module(3, "", 0)                  = 0
munmap(0x7f4675dac000, 72553)           = 0
close(3)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

倒數第5行, finit_module被調用, 用來載入lp模塊. 使用這種方式, 先分析一下執行過程, 能夠幫助你大致確定它是怎麼實現的. 接下來開始調試內核, 看一下syscall的調用過程:

以下是對視頻中調試過程的詳細記錄:

  1. 使用qemu載入了一個編譯好的內核鏡像.

    • 使用arm平臺
    • 掛載本機目錄到qemu虛擬機, 目錄下包含一個等待測試的ko模塊
    • 開啟調試選項, qemu啟動之後等待外部gdb的連接
  2. 啟動vscode遠程調試的配置, 開始調試內核代碼

    • 先關閉所有斷點, 載入和卸載testko模塊, 得到正確的輸出
    • 之後開啟斷點, 分別break在init_module和finit_module兩個syscall上
  3. 重新插入testko模塊

    • init_module系統調用上的斷點命中
    • 調用load_module函數
      • 載入模塊到內核
      • 完成鏈接, 處理符號的引用關係
      • 調用testko註冊的初始化函數, 得到初始化函數的輸出

所以, 內核模塊在載入時, 需要使用init_module/finit_module系統調用, 經syscall進入內核之後, 內核會把我們的模塊載入到自己的地址空間中, 然後完成原本鏈接器需要做的工作, 這時, 模塊中引用的其他符號, 已經得到了真實的地址, 在模塊載入的最後階段, 內核調用do_one_initcall去調用我們註冊的模塊初始化函數. 以上就是內核模塊載入的基本過程, 模塊卸載的過程類似.

A3:

先找到module_init巨集的實現代碼, 在include/linux/module.h文件中, 能找到這個巨集的定義:

#ifndef MODULE
/**
 * module_init() - driver initialization entry point
 * @x: function to be run at kernel boot time or module insertion
 *
 * module_init() will either be called during do_initcalls() (if
 * builtin) or at module insertion time (if a module).  There can only
 * be one per module.
 */
#define module_init(x)	__initcall(x);

/**
 * module_exit() - driver exit entry point
 * @x: function to be run when driver is removed
 *
 * module_exit() will wrap the driver clean-up code
 * with cleanup_module() when used with rmmod when
 * the driver is a module.  If the driver is statically
 * compiled into the kernel, module_exit() has no effect.
 * There can only be one per module.
 */
#define module_exit(x)	__exitcall(x);

#else /* MODULE */

/*
 * In most cases loadable modules do not need custom
 * initcall levels. There are still some valid cases where
 * a driver may be needed early if built in, and does not
 * matter when built as a loadable module. Like bus
 * snooping debug drivers.
 */
#define early_initcall(fn)		module_init(fn)
#define core_initcall(fn)		module_init(fn)
#define core_initcall_sync(fn)		module_init(fn)
#define postcore_initcall(fn)		module_init(fn)
#define postcore_initcall_sync(fn)	module_init(fn)
#define arch_initcall(fn)		module_init(fn)
#define subsys_initcall(fn)		module_init(fn)
#define subsys_initcall_sync(fn)	module_init(fn)
#define fs_initcall(fn)			module_init(fn)
#define fs_initcall_sync(fn)		module_init(fn)
#define rootfs_initcall(fn)		module_init(fn)
#define device_initcall(fn)		module_init(fn)
#define device_initcall_sync(fn)	module_init(fn)
#define late_initcall(fn)		module_init(fn)
#define late_initcall_sync(fn)		module_init(fn)

#define console_initcall(fn)		module_init(fn)

/* Each module must use one module_init(). */
#define module_init(initfn)					\
	static inline initcall_t __maybe_unused __inittest(void)		\
	{ return initfn; }					\
	int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));

/* This is only required if you want to be unloadable. */
#define module_exit(exitfn)					\
	static inline exitcall_t __maybe_unused __exittest(void)		\
	{ return exitfn; }					\
	void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));

#endif

可以看到根據是否定義了MODULE, module_init有兩個不同的實現, 對於驅動程式的內核模塊來說, 走到的是#else分支的定義, 這一點能夠從編譯ko時詳細的編譯命令中確認:

junan@ZEN:~/Documents/github/blogcodes/01-testko$ ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE KDIR=$KDIR make -nB V=1
...
arm-linux-gnueabihf-gcc -Wp,-MD,/home/junan/Documents/github/blogcodes/01-testko/.testko.o.d -nostdinc -isystem /usr/lib/gcc-cross/arm-linux-gnueabihf/11/include -I../arch/arm/include -I./arch/arm/include/generated -I../include -I./include -I../arch/arm/include/uapi -I./arch/arm/include/generated/uapi -I../include/uapi -I./include/generated/uapi -include ../include/linux/kconfig.h -include ../include/linux/compiler_types.h -D__KERNEL__ -mlittle-endian -Wall -Wundef -Werror=strict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common -fshort-wchar -fno-PIE -Werror=implicit-function-declaration -Werror=implicit-int -Werror=return-type -Wno-format-security -std=gnu89 -fno-dwarf2-cfi-asm -fno-ipa-sra -mabi=aapcs-linux -mfpu=vfp -funwind-tables -marm -Wa,-mno-warn-deprecated -D__LINUX_ARM_ARCH__=7 -march=armv7-a -msoft-float -Uarm -fno-delete-null-pointer-checks -Wno-frame-address -Wno-format-truncation -Wno-format-overflow -Wno-address-of-packed-member -O2 -fno-allow-store-data-races -Wframe-larger-than=1024 -fstack-protector-strong -Wimplicit-fallthrough -Wno-unused-but-set-variable -Wno-unused-const-variable -fomit-frame-pointer -fno-var-tracking-assignments -g -gdwarf-4 -Wdeclaration-after-statement -Wvla -Wno-pointer-sign -Wno-stringop-truncation -Wno-zero-length-bounds -Wno-array-bounds -Wno-stringop-overflow -Wno-restrict -Wno-maybe-uninitialized -fno-strict-overflow -fno-merge-all-constants -fmerge-constants -fno-stack-check -fconserve-stack -Werror=date-time -Werror=incompatible-pointer-types -Werror=designated-init -fmacro-prefix-map=../= -Wno-packed-not-aligned  -DMODULE  -DKBUILD_BASENAME='\''"testko"'\'' -DKBUILD_MODNAME='\''"testko"'\'' -c -o /home/junan/Documents/github/blogcodes/01-testko/testko.o /home/junan/Documents/github/blogcodes/01-testko/testko.c
...

從上面截取的編譯命令中, 應該能找到"-DMODULE", 雖然有點多, 但仔細看還是能找到的哈, 實在沒看到可以Ctrl+F搜索一下. gcc的-D參數相當於幫你在代碼里#define了一個巨集, 所以, 在包含module.h頭文件時, module_init確實會走到#else那個分支, 在這個分支里, module_init定義了一個static inline的函數, 並且, 聲明瞭一個名字叫做init_module的函數, 千萬不要把這個函數名和之前視頻里調試的syscall名字搞混了:

int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));

註意, 這個函數帶了一個alias屬性(詳細介紹, 參考官方文檔), 這樣init_module函數就變成了我們傳遞進來的initfn函數的別名, 也就是說如果調用init_module, 實際上會調用initfn. 接下來的問題是, 載入模塊的時候, 內核如何能夠得到模塊初始化函數的地址呢? 你如果自己編譯一個內核模塊代碼, 會發現編譯完成之後, 除了".ko"還會生成很多其他東西, 比如: xxx.mod.c, 內核的構建系統給你生成了一個c文件, 看一下裡面的內容:

junan@ZEN:~/Documents/github/blogcodes/01-testko$ cat testko.mod.c
#include <linux/build-salt.h>
#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>

BUILD_SALT;

MODULE_INFO(vermagic, VERMAGIC_STRING);
MODULE_INFO(name, KBUILD_MODNAME);

__visible struct module __this_module
__section(.gnu.linkonce.this_module) = {
        .name = KBUILD_MODNAME,
        .init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
        .exit = cleanup_module,
#endif
        .arch = MODULE_ARCH_INIT,
};

#ifdef CONFIG_RETPOLINE
MODULE_INFO(retpoline, "Y");
#endif

MODULE_INFO(depends, "");

這個文件中定義了一個struct module類型的變數__this_module, 並且.init成員已經被填上了模塊初始化函數的並名"init_module", .exit成員也是這樣. 這樣當內核把模塊載入自己的地址空間, 完成鏈接器的工作之後, 這個.init欄位就指向真實的模塊初始化函數地址了, 之前的調試視頻里可以看到有這樣的判斷:

...
if(mod->init != NULL)
    ret = do_one_initcall(mod->init);
...

這裡的"mod"是一個struct module*, 它實際上就是xxx.mod.c中的__this_module, 看一下代碼:

static struct module *layout_and_allocate(struct load_info *info, int flags)
{
	struct module *mod;
	unsigned int ndx;
	int err;

	...

    /* Determine total sizes, and put offsets in sh_entsize.  For now
	   this is done generically; there doesn't appear to be any
	   special cases for the architectures. */
	layout_sections(info->mod, info);
	layout_symtab(info->mod, info);

	/* Allocate and move to the final place */
	err = move_module(info->mod, info);
	if (err)
		return ERR_PTR(err);

	/* Module has been copied to its final place now: return it. */
	mod = (void *)info->sechdrs[info->index.mod].sh_addr;
	kmemleak_load_module(mod, info);
	return mod;
}

總結下來, 關於內核怎麼獲取到我們註冊的初始化函數這個問題:

  • 實現模塊初始化函數
  • 使用module_init巨集傳遞上述初始化函數
    • 這個巨集會聲明一個名字叫做"init_module"的函數, 這個函數是真實的模塊初始化函數的別名
  • 編譯模塊時, 內核的構建系統生成xxx.mod.c文件, 在這個文件里
    • 定義一個struct module類型的變數, __this_module
    • 用"init_module"填充.init成員
    • 順便說一下, THIS_MODULE巨集實際上就會展開成__this_module
  • 內核載入模塊
    • 做一些鏈接器的工作(這個地方實際上有點複雜, 之後有需要可以仔細研究, 暫時就理解為內核幫你把引用的一些符號的地址都算好了, 併在引用的地方填上正確的值)
    • 獲取到__this_module的地址, 判斷.init成員是否為空, 不為空就調用模塊初始化函數

註: 當module_init巨集的定義走到第一個分支時, 之後再寫一篇討論一下.


結尾

這篇文章主要討論了3個問題:

  1. 內核模塊是什麼?
  2. 模塊是怎麼被內核載入的?
  3. 內核是怎麼找到模塊初始化函數的?

讀完之後, 你應該對這3個問題有了自己的體會. 除此之外, 你還應該解到一些的技術, 它們能夠幫你找到一些問題的答案:

  1. strace跟蹤系統調用
  2. 使用qemu+gdb調試內核源碼
  3. gcc的一些擴展語法, 比如給函數或者變數加屬性

第1點, 比較簡單, 就是一個命令的使用問題, 但是背後的原理應該不簡單;

第2點, 是一個環境搭建的問題, 有時間可以發個視頻詳細介紹一下;

第3點, 讀源碼的過程中碰到了, 不懂的話查一下官方手冊, 寫一點代碼驗證一下, 就知道怎麼用了. 實際上, 除了attribute, 還有很多方式能夠告訴編譯器, 你要做什麼.

建了個QQ群: 838923389. 有想法的老鐵可以加一下, 一起交流linux內核的使用和學習經驗. 後續也會在b站發一些技術視頻, 老鐵們覺得有需要可以先關註一下, 視頻和文章肯定會給各位帶來一些啟發和幫助.

本文來自博客園,作者:kfggww,轉載請註明原文鏈接:https://www.cnblogs.com/kfggww/p/17598809.html


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

-Advertisement-
Play Games
更多相關文章
  • EOF,為End Of File的縮寫,通常在文本的最後存在此字元表示資料結束。 在微軟的DOS和Windows中,讀取數據時終端不會產生EOF。此時,應用程式知道數據源是一個終端(或者其它“字元設備”),並將一個已知的保留的字元或序列解釋為文件結束的指明;最普遍地說,它是ASCII碼中的替換字元( ...
  • 本文介紹了在沒有 Spring Boot 和 Starter 之前,開發人員在使用傳統的 Spring XML 開發 Web 應用時需要引用許多依賴,並且需要大量編寫 XML 代碼來描述 Bean 以及它們之間的依賴關係。也瞭解瞭如何利用 SPI 載入自定義標簽來載入 Bean 併進行註入。 ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《quarkus依賴註入》系列的第 ...
  • 我的一位朋友前陣子遇到一個問題,問題的核心就是try……catch……finally中catch和finally代碼塊到底哪個先執。這個問題看起來很簡單,當然是“catch先執行、finally後執行”了?真的是這樣嗎? 有下麵一段C#代碼,請問這段代碼的執行結果是什麼? public static ...
  • # Unity IPreprocessBuildWithReport Unity IPreprocessBuildWithReport是Unity引擎中的一個非常有用的功能,它可以讓開發者在構建項目時自動執行一些操作,並且可以獲取構建報告。這個功能可以幫助開發者提高工作效率,減少手動操作的時間和錯誤 ...
  • # 個人博客-首頁排版優化 # 優化計劃 - [x] 置頂3個且可滾動或切換 - [ ] 推薦改為4個,然後新增歷史文章,將推薦的載入更多放入歷史文章,按文章發佈時間降序排列。 - [ ] 標簽功能,可以為文章貼上標簽 - [ ] 推薦點贊功能 本篇文章優化置頂 # 原先置頂如圖 ![image]( ...
  • # C#委托 太久沒用了,簡單的複習一下 快速過一遍語法使用 ## 使用委托的步驟 1.定義一個委托類型 只需要在聲明的前面加上delegate關鍵字,其他的就跟聲明一個方法(函數)類似 ~~~ public delegate void SayHello(string name); ~~~ 2.使用 ...
  • # Unity BuildPlayerProcessor Unity BuildPlayerProcessor是Unity引擎中的一個非常有用的功能,它可以讓開發者在構建項目時自動執行一些操作。這個功能可以幫助開發者提高工作效率,減少手動操作的時間和錯誤率。在本文中,我們將介紹Unity Build ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...