电子科技大学-操作系统实验课-遇到的问题以及查找的知识总结

linux与windows的操作系统内核都是宏内核

内核编程特点:

  1. 不能使用C库函数,如外部头文件,printf,可以使用如printk函数
  2. 可以使用汇编语言,GNU C和GCC编译器
  3. 不能使用FPU
  4. 可能会产生内存故障

编写的内核模块必须有下面两种函数

  1. static int hello_int(void){...}  // static int __init hello_init(void)
    module_init(hello_init);
    

    通过module_init(...);来指定程序开始运行的函数

  2. static void hello_exit(void){...}  // static void __exit hello_exit(void)
    module_exit(hello_exit);
    

    通过module_exit(...);来指定程序终结时运行的函数,一般在这个exit函数,会释放申请的各种系统资源,最后退出,整个模块从内核中卸载

printk的使用

内核模块的两种形态

  1. 静态编译进内核的模块

  2. insmod命令动态加载的模块

    rmmod来卸载模块

    sudo insmod hello.ko var=XXX

    sudo rmmod hello.ko

内核的两种声明

MODULE_LICENSE("GPL");

内核是GNU开源工程的一部分,遵循GPL协议,使用MODULE_LICENSE可以声明你编写的这段代码使用设什么协议发布,一般声明GPL就可以了,不声明的话,内核可能不会接受,运行时的打印可能打不出来

MODULE_AUTHOR("wit@zhaixue.cc");

MODULE_AUTHOR声明代码作者,一般可以留个邮箱或者名字,对这块代码感兴趣的人可以通过该联系方式找到你

需要包含的头文件

#include <linux/init.h>
#include <linux/module.h>

因为我们在内核模块中使用了module_initmodule_exit函数,按照C语言先声明后使用的传统,所以要包含module.h头文件,这个头文件位于include/linux目录下,include是路径起点,所以要给头文件价格前缀:linux/module.hinit.h头文件主要定义了__init__exit宏,用于将这个内核模块在编译时放到指定的section中,统一管理。

一个内核模块的简单示例

#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/moduleparam.h>
#include<linux/slab.h>


MODULE_LICENSE("GPL");

static char *whom = "world";
static int num = 10;

//传递命令参数 S_IRUGO 指明参数可以被所有人读取 
module_param(whom, charp, S_IRUGO
);
module_param(num,
int,S_IRUGO);

typedef struct Node  //typedef方法函数可以对于struct Node进行重命名
{
    int val;
    struct Node *next;

} listnode, *LinkList;  //定义一个结构体指针方便后续的操作


listnode *newNode(int val) {
    listnode *L = (listnode *) kmalloc(sizeof(listnode), GFP_KERNEL);   //为头结点动态分配内存
    L->next = NULL;  //初始化时头结点指向空
    L->val = val;
    return L;
}

int insertNode(int val, listnode *head) {
    listnode *n = newNode(val);
    listnode *tail = head;
    while (tail->next) {
        tail = tail->next;
    }
    tail->next = n;
    head->val += 1;
    return 1;
}

int deleteNode(int val, listnode *head) {
    listnode *b = head->next;
    listnode *a = head;
    int i = 0;
    for (; i < head->val; i++) {
        if (b->val == val) {
            a->next = b->next;
            kfree(b);
            head->val -= 1;
            return 1;
        } else {
            a = b;
            b = b->next;
        }
    }
    return -1;
}

int lookupNode(int val, listnode *head) {
    listnode *b = head->next;
    int i = 0;
    for (; i < head->val; i++) {
        if (b->val == val) {
            return 1;
        } else {
            b = b->next;
        }
    }
    return -1;
}

void printListNode(listnode *head) {
    listnode *b = head->next;
    int i = 0;
    for (; i < head->val - 1; i++) {
        printk(KERN_ALERT
        "%d,", b->val);
        b = b->next;
    }
    printk(KERN_ALERT
    "%d,", b->val);
}


//程序中必须有下列两个函数
static int hello_init(void) {
    LinkList head = newNode(0);
    int i;
    int result;
    for (i = 0; i < num; i++) {
        insertNode(i, head);
    }
    result = lookupNode(5, head);
    printk(KERN_ALERT
    "find 5 is %d", result);
    deleteNode(5, head);
    result = lookupNode(5, head);
    printk(KERN_ALERT
    "find 5 is %d", result);
    printListNode(head);
    while (head) {
        listnode *n = head;
        head = head->next;
        kfree(n);
    }
    return 0;
}

static void hello_exit(void) {
    printk(KERN_ALERT
    "goodbye,kernel/n");
}

//加载or卸载模块
module_init(hello_init);
module_exit(hello_exit);
// 可选 
MODULE_AUTHOR("zhou-silence");
MODULE_DESCRIPTION("This is a simple example!/n");
MODULE_VERSION("v1.0");
MODULE_ALIAS("A simplest example");
obj-m:=hello.o


CURRENT_PATH :=$(shell pwd)
VERSION_NUM :=$(shell uname -r)
LINUX_PATH :=/usr/src/linux-headers-$(VERSION_NUM)

all :
	make -C $(LINUX_PATH) M=$(CURRENT_PATH) modules
clean :
	make -C $(LINUX_PATH) M=$(CURRENT_PATH) clean
root@duyuhui:~/experiment# ls
hello.c  Makefile
root@duyuhui:~/experiment# make
make -C /usr/src/linux-headers-5.15.0-71-generic M=/root/experiment modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-71-generic'
warning: the compiler differs from the one used to build the kernel
  The kernel was built by: gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0
  You are using:           gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
  CC [M]  /root/experiment/hello.o
  MODPOST /root/experiment/Module.symvers
  CC [M]  /root/experiment/hello.mod.o
  LD [M]  /root/experiment/hello.ko
  BTF [M] /root/experiment/hello.ko
Skipping BTF generation for /root/experiment/hello.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-71-generic'
root@duyuhui:~/experiment# 
root@duyuhui:~/experiment# insmod hello.ko
root@duyuhui:~/experiment# rmmod hello.ko
root@duyuhui:~/experiment# dmesg
......
[  502.824660] find 5 is 1
[  502.824758] find 5 is -1
[  502.824817] 0,
[  502.824869] 1,
[  502.824918] 2,
[  502.824967] 3,
[  502.825019] 4,
[  502.825071] 6,
[  502.825121] 7,
[  502.825171] 8,
[  502.825221] 9,
[  506.827271] goodbye,kernel/n

Makefile文件简介

Makefile文件干啥的

在windows下的各种IDE可以帮助自动运行代码,但是到了linux环境下,需要使用Makefile文件来自动编译代码,一旦整个项目的Makefile都写好了以后,只需要一个简单的make命令,就可以实现自动编译了。当然,准确的说,是make这个命令工具帮助我们实现了我们想要做的事,而Makefile就相当于是一个规则文件,make程序会按照Makefile所指定的规则,去判断哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译

obj-mobj-y

obj-m表示把文件XXX.o作为模块进行编译,不会编译到内核,但是会生成一个独立的XXX.ko文件
obj-y表示把XXX.o文件编译进内核

ifeqandifneqandifdefandifndef

  1. ifeq是判断是否相等,如果相等就执行。

    其格式有4种

    ifeq (ARG1, ARG2)
    如果相等就执行
    ifeq 'ARG1' 'ARG2'
    如果相等就执行
    ifeq "ARG1" "ARG2"
    如果相等就执行
    ifeq "ARG1" 'ARG2'
    如果相等就执行
    
    ifeq ($(strip $(foo)),)
    如果为空就执行
    endif
    

    如果ARG1与ARG2相等就执行

  2. ifneqifeq相反

  3. ifdef判断一个变量是否已经定义了

    格式为ifdef VARIABLE-NAME

    ifdef 只是测试一个变量是否有值,不会对变量进行替换展开来判断变量的值是否为空。

    除开VARIABLE-NAME=这种情况以外,使用其它方式对它的定义都会使ifdef返回真。就是说,即使我们通过其它方式(比如,定义它的值引用了其它的变量)给它赋了一个空值, ifdef也会返回真。

    例子:

    例1: 
    bar = 
    foo = $(bar) 
    ifdef foo 
    frobozz = yes 
    else 
    frobozz = no 
    endif 
    
    例 2: 
    foo = 
    ifdef foo 
    frobozz = yes 
    else 
    frobozz = no 
    endif 
    

    例1的结果是:frobozz = yes

    例2的结果是:frobozz = no

  4. ifndef与ifdef相反

KERNELRELEASE

ifeq ($(KERNELRELEASE),)它的由来是指在Linux源码根目录下的Makefile编译内核时,KERNELRELEASE宏会被定义,这个时候非空

make -C $(KDIR) M=$(PWD)

其中-C指明到$(KDIR)目录下读取Makefile,M=$(PWD)表明返回到当前目录继续读入,执行当前的Makefile。

Makefile中的$@$^$<$? %、` +$*`

$@ 表示目标文件

$^ 表示所有的依赖文件

$< 表示第一个依赖文件

$? 表示比目标还要新的依赖文件列表

C语言编写问题

[Error] ‘for‘ loop initial declarations are only allowed in C99 or C11 mode 解决方法

这是因为在 GCC 中直接在 for 循环中初始化了增量, 这种写法在 GCC 中是错误的,必须先定义变量i,然后可以成功编译运行。

[Error] ‘for‘ loop initial declarations are only allowed in C99 or C11 mode 解决方法-腾讯云开发者社区-腾讯云 (tencent.com)

initializer element is not constant 问题

C语言初始化一个全局变量或static变量时,只能用常量赋值,不能用变量赋值

(30条消息) initializer element is not constant 问题_沈万三gz的博客-CSDN博客

kmalloc函数使用

kmalloc函数详解 - 森码世界 - 博客园 (cnblogs.com)

在linux内核中不能使用malloc

kfree函数的使用

直接kfree(对象)即可,值得注意的是你需要手动回收资源,否则系统不会帮你回收

一些其他问题

printk打印不出来的解决方法

linux内核打印数据到串口控制台,printk数据不打印问题_广州建站小戴BOTAO博客 (yii666.com)

这里死活打印不出来,就直接使用dmesg了

insmod第一次killed,第二次直接卡死的原因

(32条消息) 关于模块insmod和rmmod出错的解决方案_rmmod卡死_Victor–的博客-CSDN博客

系统调用

  1. 系统调用都具有一种明确的操作

    如:getpid()系统调用,根据定义它会返回当前进程的PID

    实现如下:

    asmlinkage long sys_getpid(void){
    	return current->tgid;
    }
    

    asmlinkage限定词,通知编译器仅从栈中提取该函数的参数,系统调用都需要这个限定词,系统调用get_pid()在内核中被定义为sys_getpid()

  2. 在Linux系统中,每个系统调用被赋予一个系统调用号,用户空间中的进程执行一个系统调用的时候,这个系统调用号就被用来指明哪个系统调用,系统调用号存储在sys_call_table中,它与体系结构有关,一般在entry.s中定义

  3. 用户如何使用系统调用

    image-20230528102017976

  4. 必须保证系统调用是可重入的

内核下载

apt-cache search linux-sourceapt-get install linux-source 可以搜索并下载当前内核版本的源码,并自动解压到 /usr/src/ 目录下。

使用tar xvf linux-source-4.4.0.tar.gz来解压。

linux 5.15.0等一系列内核的系统调用需要修改的目录为

kernel/sys.c是用于编写具体函数的,如下所示setuid就是一个声明后在这里编写具体函数

image-20230529101600683

include/linux/syscalls.h是用于声明函数的,即asmlinage开头的,如下所示,setuid即为一个系统调用的声明

image-20230529101458045

arch/x86/entry/syscalls/syscall_64.tbl是系统调用号表

image-20230529101745374

编写系统调用

下面的操作都是在下载好的内核里面操作,然后编译内核,安装内核,最终启动内核,然后写个程序测试是否可以使用

  1. 修改系统调用表号,如下,设置了调用表449号为我们的系统调用

image-20230531100410560

  1. 增加系统调用声明,如下,增加了系统调用声明asmlinkage开头的

    image-20230531100521174

  2. 增加系统调用实际内容,如下系统调用的内容是打印一个Hello eternal robot

    image-20230531100616311

  3. 编译内核

    Linux内核添加新的系统调用 教程 - 付杰博客 (fujieace.com)

    Linux重新编译内核_51CTO博客_ubuntu编译linux内核(这篇很详细)

    简单来说就是在下载内核解压根目录下运行下面内容

    make clean
    make defconfig
    make
    make modules_install
    make install
    

    在虚拟机里面,输入reboot加回车后,长按esc键,才能进入选内核界面

  4. 测试内核

    #include <unistd.h>
    #include <sys/syscall.h>
    #include <sys/types.h>
    #include <stdio.h>
    #define __NR_hello_eternal_robot 448
    int main(int argc, char *argv[])
    {
     syscall(__NR_hello_eternal_robot);
     printf("ok! run dmesg | grep Hello in terminal!\n");
     return 0;
    }
    

    使用如下编译运行

    root@duyuhui:~/experiment/2# nano hello.c 
    root@duyuhui:~/experiment/2# gcc -o myhello hello.c
    root@duyuhui:~/experiment/2# ./myhello 
    ok! run dmesg | grep Hello in terminal!
    root@duyuhui:~/experiment/2# dmesg | grep Hello
    [  707.558821] Hello eternal robot 
    [ 1413.170153] Hello eternal robot 
    [ 1462.344935] Hello eternal robot 
    root@duyuhui:~/experiment/2# 
    

    主要运行了三次,所以抓出了三个Hello内容

遇到的一些问题
  1. 什么是SYSCALL_DEFINE

    (34条消息) Linux系统调用之SYSCALL_DEFINE_麦兜布达拉的博客-CSDN博客

  2. 安装内核的时候遇到E:You must put some 'deb-src' URIs in your sources.list怎么办?

  3. 内核编译的时候在输入make -j 5后出现gelf.h: No such file or directory问题

    (34条消息) Ubuntu编译出现:gelf.h: No such file or directory_木可木可❀的博客-CSDN博客

  4. 自定义内核的时候在输入make defconfig后出现lexer.lex.c Error 127问题

    (34条消息) make menuconfig出现错误lexer.lex.c Error 127_scripts/kconfig/lexer.lex.c_qt码农C的博客-CSDN博客

linux内核模块调用其他模块导出的函数

遇到的一些问题

  1. ERROR: modpost: “Func” [xxxxx.ko]undefined!问题,下面给出了三种方式解决,这个里面的第二点解决方案有点问题,会导致下面的第二点错误no symbol version for fptr_Operation

    (34条消息) Linux内核模块间函数调用正确方法_内核模块调用外部函数_少林达摩祖师的博客-CSDN博客

  2. no symbol version for fptr_Operation

    关于内核模块挂载出现“no symbol version for”问题的研究-tekkamanninja-ChinaUnix博客

  3. 如何查看导出的函数,或查看函数是否导出了

    cat /proc/kallsyms | grep hello

  4. 如何计算内核模块时间

    (35条消息) 统计内核代码运行时间_k_time_Li-Yongjun的博客-CSDN博客

linux字符设备驱动程序编写方式

Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev目录 下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。

为了管理这些设备,系统为设备编了号,每 个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编 号,如硬盘的主设备号是3。

Linux为所有的设备文件都提供了统一的操作函数接口,方法是使用数据结构struct file_operations。这个数据结构中包括许多操作函数的指针,如open()、close()、read()和write()等,但由于外设 的种类较多,操作方式各不相同。Struct file_operations结构体中的成员为一系列的接口函数,如用于读/写的read/write函数和用于控制的ioctl等。

打开一个文件就是调用这个文件file_operations中的open操作。不同类型的文件有不同的file_operations成员函数,如普通的磁盘数据文件, 接口函数完成磁盘数据块读写操作;而对于各种设备文件,则最终调用各自驱动程序中的I/O函数进行具体设备的操作。这样,应用程序根本不必考虑操作的是设 备还是普通文件,可一律当作文件处理,具有非常清晰统一的I/O接口。所以file_operations是文件层次的I/O接口。

mknod命令用于创建字符设备文件和块设备文件

mknod (选项) (参数)

选项有:

-Z:设置安全的上下文; -m:设置权限模式; -help:显示帮助信息; --version:显示版本信息。

参数有:

  • 文件名:要创建的设备文件名;
  • 类型:指定要创建的设备文件的类型;
  • 主设备号:指定设备文件的主设备号;
  • 次设备号:指定设备文件的次设备号。

例如:

mknod /dev/ttyUSB32 c 188 32

/dev/ttyUSB32,表示要创建的节点文件
c,表示是一个字符设备
188,主设备号,表示某一个具体的驱动
32,次设备号,表示使用这个驱动的各个设备

Linux中/proc目录下文件详解

/proc文件系统下的多种文件提供的系统信息不是针对某个特定进程的,而是能够在整个系统范围的上下文中使用。可以使用的文件随系统配置的变化而变化。

/proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。

它以文件系统的方式为访问系统内核数据的操作提供接口。

由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统是动态从系统内核读出所需信息并提交的

下面列举一些类型

/proc/cmdline

内核启动的命令行

image-20230604152936222

/proc/cpuinfo

image-20230604153113890

/proc/devices

列出字符和块设备的主设备号,以及分配到这些设备号的设备名称

image-20230604153221943

/proc/filesystems

列出可供使用的文件系统类型,一种类型一行。虽然它们通常是编入内核的文件系统类型,但该文件还可以包含可加载的内核模块加入的其它文件系统类型

image-20230604153955626

/proc/meminfo

image-20230604154139589

遇到的一些问题

struct file_operations的头文件是#include <linux/fs.h>

copy_to_user的头文件是#include <linux/uaccess.h>

步骤

  1. 注册字符设备

    Snipaste_2023-06-04_15-45-11

  2. 编写字符设备模块,以及载入内核,查看一下是否成果

    编写my_char_device.c代码

    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/types.h>
    #include <linux/uaccess.h>
    #include <linux/fs.h>
    
    // 主设备号
    #define CHAR_DEVICE_MAJOR 255
    // 设备名
    #define CHAR_DEVICE_NAME "my_char_devices"
    // 保存数据的最大长度
    #define MSG_LENGTH 1024
    
    // 设备open次数
    static int device_open_counts=0;
    // 保存从用户空间来的数据
    static char message[MSG_LENGTH]={0};
    // 记录用户空间的数据的长度
    static short message_length=0;
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("dyh");
    MODULE_DESCRIPTION("自建字符设备/n");
    
    static int device_open(struct inode *inode, struct file *f)
    {
        device_open_counts++;
        printk("%s 已经打开了 %d 次\n",CHAR_DEVICE_NAME,device_open_counts);
        return 0;
    }
    
    static int device_release(struct inode *inode, struct file *f)
    {
        printk("%s 关闭\n",CHAR_DEVICE_NAME);
        return 0;
    }
    
    static ssize_t device_read(struct file *f, char __user *buffer, size_t count, loff_t *pOffset)
    {
        int ret=0;
        ret=copy_to_user(buffer,message,message_length);
        if(ret==0)
        {
            printk("%s 发送 %d 数量的字符给用户\n",CHAR_DEVICE_NAME,message_length);
            return 0;
        }
        else
        {
            printk("%s 读取数据失败\n",CHAR_DEVICE_NAME);
            return -1;
        }
    }
    
    static ssize_t device_write(struct file *f, const char __user *buffer, size_t count, loff_t *pOffset)
    {
        int ret=0;
        ret=copy_from_user(message,buffer,count);
        message_length=count;
        if(ret==0)
        {
            printk("%s 接收来自用户的数据数量是 %d\n",CHAR_DEVICE_NAME,count);
            return 0;
        }
        else
        {
            printk("%s 接收数据失败\n",CHAR_DEVICE_NAME);
            return -1;
        }
    }
    
    // 设备操作函数结构体
    static struct file_operations device_operations={
        .owner=THIS_MODULE,
        .open=device_open,
        .release=device_release,
        .read=device_read,
        .write=device_write,
    };
    
    static int __init char_device_driver_init(void)
    {
        printk("my_char_device 初始化\n");
        int ret=0;
        // 注册字符设备驱动
        ret=register_chrdev(CHAR_DEVICE_MAJOR,CHAR_DEVICE_NAME,&device_operations);
        if(ret<0)
        {
            printk("%s 设备注册失败\n",CHAR_DEVICE_NAME);
    
        }
        return 0;
    }
    
    static void __exit char_device_driver_exit(void)
    {
        // 注销字符设备驱动
        unregister_chrdev(CHAR_DEVICE_MAJOR,CHAR_DEVICE_NAME);
        printk("my_char_device 退出\n");
    }
    
    module_init(char_device_driver_init);
    module_exit(char_device_driver_exit);
    
    

    编写makefile文件

    obj-m:=my_char_device.o
    
    
    CURRENT_PATH :=$(shell pwd)
    VERSION_NUM :=$(shell uname -r)
    LINUX_PATH :=/usr/src/linux-headers-$(VERSION_NUM)
    
    all :
    	make -C $(LINUX_PATH) M=$(CURRENT_PATH) modules
    clean :
    	make -C $(LINUX_PATH) M=$(CURRENT_PATH) clean
    
    
    make
    insmod my_char_device.ko
    lsmod grep | my_char_device
    ls /dev
    cat /proc/devices
    

    第一行是编译,第二行是载入,第三行是查看载入是否成功,第四行是列出设备,第五行是列出设备

  3. 编写测试程序

    #include <stdio.h>
    #include <stdlib.h>
    #include <fcntl.h>
    #include <string.h>
    
    #define LENGTH 1024
    
    static char message[LENGTH];
    
    int main() {
        int result = 0, fd;
        char to_send[LENGTH] = {0};
        fd = open("/dev/my_char_device", O_RDWR);
        if (fd < 0) {
            printf("无法打开 my_char_device\n");
            return 0;
        }
        printf("给内核模块输入一些东西:\n");
        scanf("%[^\n]%*c", to_send);
        printf("正在写入...\n");
        result = write(fd, to_send, strlen(to_send));
        if (result < 0) {
            printf("写入失败\n");
            return -1;
        }
        printf("按Enter键获取从内核发回的消息\n");
        getchar();
        printf("正在读取...\n");
        result = read(fd, message, LENGTH);
        if (result < 0) {
            printf("从设备中读取失败\n");
            return -1;
        }
        printf("信息是: [%s]\n", message);
        close(fd);
        return 0;
    }
    
    root@duyuhui:~/experiment/4# gcc -o test test.c
    test.c: In function ‘main’:
    test.c:21:14: warning: implicit declaration of function ‘write’; did you mean ‘fwrite’? [-Wimplicit-function-declaration]
       21 |     result = write(fd, to_send, strlen(to_send));
          |              ^~~~~
          |              fwrite
    test.c:29:14: warning: implicit declaration of function ‘read’; did you mean ‘fread’? [-Wimplicit-function-declaration]
       29 |     result = read(fd, message, LENGTH);
          |              ^~~~
          |              fread
    test.c:35:5: warning: implicit declaration of function ‘close’; did you mean ‘pclose’? [-Wimplicit-function-declaration]
       35 |     close(fd);
          |     ^~~~~
          |     pclose
    root@duyuhui:~/experiment/4# ls
    device  test  test.c
    root@duyuhui:~/experiment/4# ./test
    给内核模块输入一些东西:
    sx
    正在写入...
    按Enter键获取从内核发回的消息
    
    正在读取...
    信息是: [sx]
    
    
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大侠月牙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值