linux進階:字元設備

来源:https://www.cnblogs.com/couvrir/archive/2023/08/13/17626647.html
-Advertisement-
Play Games

設計字元設備 文件系統調用系統IO的內核處理過程 inode索引節點是文件系統中的一種數據結構,用於存儲文件的元數據信息,包括文件的大小、訪問許可權、創建時間、修改時間等。每個文件在文件系統中都對應著一個唯一的inode節點,通過inode節點可以查找到文件的實際數據塊的位置。inode節點通常存儲在 ...


設計字元設備

文件系統調用系統IO的內核處理過程

inode索引節點是文件系統中的一種數據結構,用於存儲文件的元數據信息,包括文件的大小、訪問許可權、創建時間、修改時間等。每個文件在文件系統中都對應著一個唯一的inode節點,通過inode節點可以查找到文件的實際數據塊的位置。inode節點通常存儲在磁碟的inode表中,文件系統通過inode號來訪問和管理文件。

file_operation結構體是函數指針表,用於定義文件的操作方法。當應用程式通過文件描述符打開文件時,內核會根據文件描述符找到對應的inode節點,並獲取與inode節點關聯的file_operation表。通過file_operation表中的函數指針,內核可以調用相應的函數來執行文件操作,如open、read、write、close等。不同內核可以有不同的file_operation表,因為不同的內核可能有不同的文件操作方法和特性。

task_struct結構體用於描述和管理進程,內容很多很複雜。裡面有個成員變數是struct files_struct *files,用於存儲與進程相關的文件描述符表的信息(文件描述符表記錄了進程打開的文件以及相應的操作許可權等信息)。要想獲取進程的文件描述符相關信息,需要通過訪問task_struct結構體的files指針來獲取files_struct結構體,進而訪問文件描述符表的信息。

files_struct結構體用於跟蹤和管理進程打開的文件。fd_array[]為指針數組,用於存儲進程打開的文件描述符的信息,即每個文件描述符都對應一個files_struct。通過fd_array數組可以快速訪問和操作這些文件描述符,數組索引值對應著文件描述符的值。

 

硬體層原理

思路:把底層寄存器配置操作放在文件操作介面里,新建一個文件綁定該文件操作介面,應用程式通過操作指定文件來配置底層寄存器。

基本介面實現:查原理圖,數據手冊,確定底層需要配置的寄存器。類似於裸機開發。實現一個文件的底層操作介面,這是文件的基本特征。

struct file_operations存放在ebf-buster-linux/include/linux/fs.h

struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
        ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
        int (*iterate) (struct file *, struct dir_context *);
        int (*iterate_shared) (struct file *, struct dir_context *);
        __poll_t (*poll) (struct file *, struct poll_table_struct *);
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
        int (*mmap) (struct file *, struct vm_area_struct *);
        unsigned long mmap_supported_flags;
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, loff_t, loff_t, int datasync);
        int (*fasync) (int, struct file *, int);
        int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
        int (*check_flags)(int);
        int (*flock) (struct file *, int, struct file_lock *);
        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
        int (*setlease)(struct file *, long, struct file_lock **, void **);
        long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
        void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
        unsigned (*mmap_capabilities)(struct file *);
#endif
        ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);
        int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64);
        int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t, u64);
        int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

幾乎所有成員都是函數指針,用來實現文件的具體操作。 

 

驅動層原理

設備號(dev_t)是uint32_t類型,主設備號(高12位)+次設備號(低20位)。主設備號用於標識該驅動所管理的設備類型,次設備號用於標識同一類型設備的具體設備實例。

 

把file_operations文件操作介面註冊到內核,內核通過主次設備號來記錄它(cdev_init存放在ebf-buster-linux/fs/char_dev.c)

cdev_init()  //把用戶構建的file_operations結構體記錄在內核驅動的基本對象cdev

cdev_init()用於初始化字元設備驅動中的struct cdev結構體。struct cdev結構體是字元設備驅動的核心數據結構,描述了字元設備驅動的屬性和操作(file_operations)。

當開發者編寫字元設備驅動時,需要先調用cdev_init()函數來初始化struct cdev結構體。這個函數會將struct cdev的各個成員初始化為合適的值,並建立與字元設備驅動相關的關聯。

cdev_init()函數通常在字元設備驅動載入時init函數中調用,確保驅動模塊的正確初始化。

 

cdev_add(存放在ebf-buster-linux/fs/char_dev.c),作用:根據哈希函數保存cdev到probes哈希表中,方便內核查找file_operation使用。

int cdev_add(struct cdev *p, dev_t dev, unsigned int count);
參數:
    p:指定的字元設備對象
    dev:設備號
    count:設備號數量

cdev_add()函數用於向字元設備驅動註冊字元設備,即將一個已初始化的字元設備對象(struct cdev)和一個設備號(dev_t)進行關聯,並將其註冊到內核中,使其能夠被用戶程式訪問和使用。

cdev_add()函數通常在字元設備驅動初始化階段調用來註冊字元設備,在此之前,需要先通過cdev_init()初始化。註意:需要正確初始化之後才調用,否則可能導致註冊失敗或出現意外的行為。

 

兩個Hash表(幫助找到cdev結構體)

chrdevs:登記設備號。

cdev_map->probe:保存驅動基本對象struct cdev。

 

文件系統層原理

mknod + 主次設備號

構建一個新的設備文件,通過主次設備號在cdev_map中找到cdev->file_operations,把cdev->file_operations綁定到新的設備文件中。

到這一步,應用程式就可以使用open()、write()、read()等函數來控制設備文件了。

 

 

設備號的組成與哈希表

ebf-buster-linux/include/linux/kdev_t.h描述了設備號的具體構成。

/* 截取部分代碼,關於設備號的描述 */
#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) -1)
 
#define MAJOR(dev)   ((unsigned int)((dev) >> MINORBITS))
#define MINOR(dev)   ((unsigned int)((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

理論取值範圍:

主設備號:2^12=4K

次設備號:2^20=1M

cat /proc/devices查看已註冊的設備號

內核是希望一個設備驅動(file_operation)可以獨自占有一個主設備號和多個次設備號,而通常一個設備文件綁定一個主設備號和一個次設備號,所以設備驅動與設備文件是一對一或者一對多的關係。

 

Hash Table(哈希表、散列表,數組和鏈表的混合使用)

以主設備號為編號,使用哈希函數f(major)= major % 255 來計算主設備號的對應數組下標。

主設備號衝突(如0、255,都掛載在數組0下標),則以次設備號為比較值來排序鏈表節點。

哈希函數的設計目標:鏈表節點儘量平均分佈在各個數組元素中,提高查詢效率。

 

 

設備號管理

關鍵的數據結構:char_device_struct(存放在ebf-buster-linux/fs/char_dev.c)

static struct char_device_struct {
        struct char_device_struct *next;    //指向下一個鏈表節點
        unsigned int major;                 //主設備號  
        unsigned int baseminor;             //次設備號 
        int minorct;                        //次設備號的數量
        char name[64];                      //設備名稱
        struct cdev *cdev;                  //內核字元對象(已丟棄)
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

 

關鍵的函數:__register_chrdev_region(存放在ebf-buster-linux/fs/char_dev.c)

static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name)
{
        struct char_device_struct *cd, **cp;
        int ret = 0;
        int i;

     /* 動態申請記憶體 */ cd
= kzalloc(sizeof(struct char_device_struct), GFP_KERNEL); if (cd == NULL)  return ERR_PTR(-ENOMEM);      /* 加互斥鎖保護資源 */ mutex_lock(&chrdevs_lock); if (major == 0) {  
          /* 主設備號為0,從chadevs哈希表中查找一個空閑位置 */ ret
= find_dynamic_major(); if (ret < 0) { pr_err("CHRDEV \"%s\" dynamic allocation region is full\n", name); goto out; }
          /* 返回主設備號 */ major
= ret;        } if (major >= CHRDEV_MAJOR_MAX) { pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n", name, major, CHRDEV_MAJOR_MAX-1); ret = -EINVAL; goto out; }
     /* 保存參數 */ cd
->major = major; cd->baseminor = baseminor; cd->minorct = minorct; strlcpy(cd->name, name, sizeof(cd->name));
    /* 哈希函數,計算哈希表的位置 */ i
= major_to_index(major);
     /* 鏈表排序,按主設備號從小到大排序。如果主設備號相等,按次設備號從小到大排序,要考慮次設備號的最大值 */
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) if ( (*cp)->major > major || ( (*cp)->major == major && ( ( (*cp)->baseminor >= baseminor ) || ( (*cp)->baseminor + (*cp)->minorct > baseminor) ) ) )  break; /* 如果主設備號相等,檢查次設備號是否存在衝突 */ if (*cp && (*cp)->major == major) {
          /* 獲取鏈表節點的次設備號範圍 */
int old_min = (*cp)->baseminor; int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
          /* 獲取新設備的次設備號範圍 */
int new_min = baseminor; int new_max = baseminor + minorct - 1; /* 判斷新設備的次設備號最大值是否位於鏈表節點的次設備號範圍 */ if (new_max >= old_min && new_max <= old_max) {
               /* 確定衝突,返回錯誤 */ ret
= -EBUSY; goto out; } /* 判斷新設備的次設備號最小值是否位於鏈表節點的次設備號範圍 */ if (new_min <= old_max && new_min >= old_min) {
               /* 確定衝突,返回錯誤 */ ret
= -EBUSY; goto out; }
          /* 判斷新設備的次設備號是否跨越鏈表節點的次設備號範圍 */
if (new_min < old_min && new_max > old_max) {
               /* 確定衝突,返回錯誤 */ ret
= -EBUSY; goto out; } }
     /* 插入新設備的鏈表節點 */ cd
->next = *cp; *cp = cd; mutex_unlock(&chrdevs_lock); return cd; out: mutex_unlock(&chrdevs_lock); kfree(cd); return ERR_PTR(ret); }

上訴函數主設備號相等,判斷新舊次設備號三種錯誤圖如下。

該函數用於註冊字元設備驅動,保存新註冊的設備號到chrdevs哈希表中,防止設備號衝突。

主設備為0時,需要動態分配設備號(優先使用255~234,其次使用511~384),函數會從字元設備哈希表中找到一個空閑的位置,分配主設備號,並將該主設備號保存到字元設備的數據結構中。主設備號最大為512。

然後函數將傳入的參數保存到字元設備結構體cd中,並計算出在字元設備哈希表中的位置。如果遍歷字元設備哈希表的鏈表,按主設備號從小到大排序。

如果遍歷過程中,主設備號和傳入的主設備號衝突或次設備號範圍與傳入的次設備號範圍有重疊,函數會返回錯誤。否則函數會將新字元設備的節點插入到鏈表中,並返回字元設備結構體cd。

該函數一般在字元設備驅動載入時調用,用於註冊字元設備。通過註冊字元設備,內核可以識別和管理相應的設備,並提供相應的介面供用戶空間程式進行讀寫操作。

 

 

保存file_operation結構體

關鍵數據結構:字元設備管理對象cdev(存放在ebf-buster-linux/include/linux/cdev.h)

struct cdev {
        struct kobject kobj;                 //內核對象
        struct module *owner;                //擁有該設備的模塊的指針
        const struct file_operations *ops;   //指向設備操作函數的指針
        struct list_head list;               //用於將設備對象添加到一個鏈表中,方便管理多個設備
        dev_t dev;                           //表示32位設備號
        unsigned int count;                  //用於跟蹤該設備對象的引用計數
} __randomize_layout;                        //告訴編譯器隨機化結構體的佈局,以提高安全性

 

關鍵數據結構:kobj_map(與哈希表有關,存放在ebf-buster-linux/drivers/base/map.c)

struct kobj_map {
        struct probe {
                struct probe *next;
                dev_t dev;
                unsigned long range;
                struct module *owner;
                kobj_probe_t *get;
                int (*lock)(dev_t, void *);
                void *data;
        } *probes[255];
        struct mutex *lock;
};

 

 

創建一個設備文件

mknod命令:創建指定類型的特殊文件

 用法:mknod [選項]... 名稱 類型 [主設備號 次設備號]

以指定的 <名稱> 創建指定 <類型> 的特殊文件。

當 <類型> 為 b、c 或 u 時必須指定 <主設備號> 和 <次設備號>,當 <類型>為 p 時不得指定 <主設備號> 和 <次設備號>。

如果 <主設備號> 或 <次設備號>以 0x 或 0X 開頭,則將它們視為十六進位數進行解析;如果以 0 開頭,則視為八進位數;其餘情況下,視為十進位數。<類型> 可以是:

    b          創建一個(帶緩衝的)塊特殊文件
    c, u      創建一個(不帶緩衝的)字元特殊文件
    p          創建一個 FIFO 文件

舉例:mkmod /dev/test c 2 0

原理分析

init_special_inode函數定義在ebf-buster-linux/fs/inode.c。主要內容是判斷文件的inode類型,如果是字元設備類型,則把def_chr_fops作為該文件的操作介面,並把設備號記錄在inode->i_rdev。

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
        inode->i_mode = mode;

        if (S_ISCHR(mode)) {
                inode->i_fop = &def_chr_fops;
                inode->i_rdev = rdev;
        } else if (S_ISBLK(mode)) {
                inode->i_fop = &def_blk_fops;
                inode->i_rdev = rdev;
        } else if (S_ISFIFO(mode))
                inode->i_fop = &pipefifo_fops;
        else if (S_ISSOCK(mode))
                ;       /* leave it no_open_fops */
        else
                printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for inode %s:%lu\n", mode, inode->i_sb->s_id, inode->i_ino);
}

inode上的file_operation並不是自己構造的file_operation(保存在內核的字元設備驅動哈希表裡),而是字元設備通用的def_chr_fops。

自己構造的file_operation會在chrdev_open函數那裡才會綁定到我們的文件上。所以自己構建的file_operation等在應用程式調用open函數後才會綁定在文件上。

 

 

LED字元設備驅動實驗

驅動模塊 = 內核模塊(.ko)+ 驅動介面(file_operations) 

實驗步驟

  1. 在內核模塊入口函數里獲取GPIO相關寄存器並初始化。
  2. 構造file_operations介面,並註冊到內核。
  3. 創建設備文件,綁定自定義file_operations介面。
  4. 應用程式echo通過寫設備文件控制硬體led。

 

驅動模塊初始化

虛擬地址映射ioremap函數

GPIO寄存器物理地址和虛擬地址映射。函數ioremap存放於ebf-buster-linux/arch/arm/include/asm/io.h。

void __iomem *ioremap(resource_size_t res_cookie, size_t size);
參數:
    res_cookie:物理地址
    size:映射長度
返回值:
    void *類型的指針,指向被映射的虛擬地址
    __iomem 主要是用於編譯器的檢查地址在內核空間的有效性

 

虛擬地址讀寫

舊機制:

readl() / writel()

新機制:

unsigned int ioread32(void __iomem *addr);  //讀取一個雙字

void iowrite32(u32 b, void __iomem *addr);   //寫入一個雙字

檢查CPU大小端,調整位元組序,以提高驅動的可移植性。

 

自定義led的file_operation介面

static struct file_operation led_fops = {
        .owner = THIS_MODULE,
        .open = led_open,
        .read = led_read,
        .write = led_write,
        .release = led_release
};

owner:設置驅動介面關聯的內核模塊,防止驅動程式運行時內核模塊被卸載。

release:文件引用數為0時調用。

 

拷貝數據函數copy_from_user(存放於ebf-buster-linux/include/linux/uaccess.h)

static __always_inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)

參數:
    *to:將數據拷貝到內核的地址
    *from:需要拷貝數據的用戶空間地址
    n:拷貝數據的長度(位元組)
返回值:
    成功:0
    失敗:沒有被拷貝的位元組數

 

註冊字元設備驅動 

層次由高到低:register_chadev -> __register_chadev -> __register_chadev_region(該功能實現了cdev_init函數和cdev_add函數功能,但是在__register_chadev函數中規定了次設備號從0開始,有256個),即一個file_operations對應256個設備文件。

 

chrdev.c文件

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>

#define DEV_MAJOR       0               /* 動態申請主設備號 */
#define DEV_NAME        "red_led"       /* led設備名字 */

/* GPIO虛擬地址指針 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO04;
static void __iomem *SW_PAD_GPIO1_IO04;
static void __iomem *GPIO1_GDIR;
static void __iomem *GPIO1_DR;

static int led_open(struct inode *inode, struct file *filp)
{
        return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
        return -EFAULT;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
        unsigned char databuf[10];

        if(cnt > 10)    cnt = 10;
    
        /* 從用戶空間拷貝數據到內核空間 */
        if(copy_from_user(databuf, buf, cnt)){
                return -EIO;
        }

        if(!memcmp(databuf, "on", 2)){
                iowrite32(0<<4, GPIO1_DR);
        }else if(!memcmp(databuf, "off", 3)){
                iowrite32(1<<4, GPIO1_DR);
        }

        /* 寫成功後,返回寫入的位元組數 */
        return cnt;
}

static int led_release(struct inode *inode, struct file *filp)
{
        return 0;
}

/* 自定義led的file_operation介面 */
static struct file_operations led_fops = {
        .owner = THIS_MODULE,
        .open = led_open,
        .read = led_read,
        .write = led_write,
        .release = led_release
};

int major = 0;
static int __init led_init(void)
{
        /* GPIO相關寄存器操作 */
        IMX6U_CCM_CCGR1 = ioremap(0x20c406c, 4);
        SW_MUX_GPIO1_IO04 = ioremap(0x20e006c, 4);
        SW_PAD_GPIO1_IO04 = ioremap(0x20e02f8, 4);
        GPIO1_GDIR = ioremap(0x0209C004, 4);
        GPIO1_DR = ioremap(0x0209C000, 4);

        /* 使能GPIO1時鐘 */
        iowrite32(0xffffffff, IMX6U_CCM_CCGR1);

        /* 設置GPIO1_IO04復用為普通GPIO */
        iowrite32(5, SW_MUX_GPIO1_IO04);

        /* 設置GPIO屬性 */
        iowrite32(0x10B0, SW_PAD_GPIO1_IO04);

        /* 設置GPIO1_IO04為輸出功能 */
        iowrite32(1<<4, GPIO1_GDIR);

        /* LED輸出高電平 */
        iowrite32(1<<4, GPIO1_DR);

        /* 註冊字元設備驅動 */
        major = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops);
        printk(KERN_ALERT "led major:%d\n", major);

        return 0;
}

static void __exit led_exit(void)
{
   /* 取消映射 */
   iounmap(IMX6U_CCM_CCGR1);
   iounmap(SW_MUX_GPIO1_IO04);
   iounmap(SW_PAD_GPIO1_IO04);
   iounmap(GPIO1_GDIR);
   iounmap(GPIO1_DR);

   /* 註銷字元設備驅動 */
   unregister_chrdev(major, DEV_NAME);
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL2");
MODULE_AUTHOR("couvrir");
MODULE_DESCRIPTION("led module");
MODULE_ALIAS("led module");

make。

 

make copy。

 


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

-Advertisement-
Play Games
更多相關文章
  • ## Eureka 簡介 Eureka 是一個基於 REST 的服務發現組件,SpringCloud 將它集成在其子項目 spring-cloud-netflix 中,以實現 SpringCloud 的服務註冊與發現,同時提供了負載均衡、故障轉移等能力,目前 Eureka2.0 已經不再維護,故不推 ...
  • Quartz由Java編寫的功能豐富的開源作業調度框架,可以集成到幾乎任何Java應用程式中,並且能夠創建多個作業調度; ...
  • ## 引言 深拷貝是指創建一個新對象,該對象的值與原始對象完全相同,但在記憶體中具有不同的地址。這意味著如果您對原始對象進行更改,則不會影響到複製的對象 常見的C#常見的深拷貝方式有以下4類: 1. 各種形式的序列化及反序列化。 2. 通過反射機制獲取該對象的所有欄位和屬性信息。遍歷所有欄位和屬性,遞 ...
  • 鴿了好久的內容,終於補上了。這篇文章對該合集前幾篇文章的內容做了簡要的總結和應用,同時按照MVVM設計模式完成了一個學習小Demo,希望可以幫到正在學習的友友們。有什麼問題可以評論區留言討論。 ...
  • # Unity AssetPostprocessor的Model的動畫相關的函數修改實際應用 在Unity中,AssetPostprocessor是一個非常有用的工具,它可以在導入資源時自動執行一些操作。其中,Model的動畫相關的函數修改可以幫助我們在導入模型時自動修改動畫相關的函數,從而提高我們 ...
  • 最近一段時間沒有看 docker desktop,忽然想起來打開看看,結果死活啟動不了。以前卸載之後,重新安裝就好了,同樣的方法嘗試了很多次還是不太行,重啟也不行... 後來想想是不是 wsl 出了問題,運行 WSA ,WSAClient 啟動後又自動退出了。在命令行下運行 wsl,等待了很久,出現 ...
  • WindowsServer伺服器管理技巧:對於使用WindowsServer伺服器開發人員或者運維人員初學者來說,可能會遇到很多問題,比如:如何設置允許多用戶同時登錄伺服器?如何開啟伺服器防火牆?Windows如何配置SSH遠程登錄?等等,如果遇到了這些問題,來看看這篇文章就能解決啦! ...
  • 國產銀河麒麟系統也是生產環境上經常遇到的(官網簡介:銀河麒麟高級伺服器操作系統V10 - 國產操作系統、銀河麒麟、中標麒麟、開放麒麟、星光麒麟——麒麟軟體官方網站 (kylinos.cn)) 這版系統分為伺服器版和個人桌面版;其中伺服器版命令估計是基於紅帽體系;而桌面版命令估計是基於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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...