Linux驱动开发——字符设备驱动开发

news2024/11/15 21:49:30

1 概述

1.1 说明

本文是学习rk3568开发板驱动开发的记录,代码依托于rk3568开发板

1.2 字符设备介绍

字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
Linux应用程序向下调用驱动程序流程如下:
在这里插入图片描述
在Linux中,一切皆是文件,驱动加载成功之后,会在dev目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作
应用程序运行在用户控件,Linux驱动属于内核的一部分,运行在内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用
户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open、close、write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分,调用open函数的流程如下:
在这里插入图片描述
每一个系统调用,在驱动中都有与之对应的一个驱动函数,在Linux内核文件include/linux/fs.h中有个file_operations的接口提,定义了内核驱动操作函数集合。

1.3 file_operations定义

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);

	ANDROID_KABI_RESERVE(1);
	ANDROID_KABI_RESERVE(2);
	ANDROID_KABI_RESERVE(3);
	ANDROID_KABI_RESERVE(4);
} __randomize_layout;

常用的函数有:

  • owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
  • llseek 函数用于修改文件当前的读写位置。
  • read 函数用于读取设备文件。
  • write 函数用于向设备文件写入(发送)数据。
  • poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
  • unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
  • compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
  • mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
  • open 函数用于打开设备文件。
  • release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
  • fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。

2 字符设备驱动开发步骤

2.1 驱动模块的加载和卸载

Linux驱动有两种运行模式,一种是直接编译进Linux内核中,内核启动的时候自动运行驱动程序。第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用“modprobe”或者“insmod”命令加载。通常将其编译成模块进行调试,因为这样不用整编内核代码。当没有问题后可以考虑编译进内核。
模块有加载和卸载两种操作:

module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“modprobe”命令加载驱动的时候,xxx_init 这个函数就会被调用。module_exit函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用
驱动加载和卸载模板代码如下:

 /* 驱动入口函数 */
 static int __init xxx_init(void)
 {
 /* 入口函数具体内容 */
 return 0;
 }

 /* 驱动出口函数 */
 static void __exit xxx_exit(void)
 {
 /* 出口函数具体内容 */
 }

 /* 将上面两个函数指定为驱动的入口和出口函数 */
 module_init(xxx_init);
 module_exit(xxx_exit);

加载和卸载命令会调用以上的驱动函数。
有两种命令可以加载驱动模块:insmod和modprobe,二者的区别在于insmod只加载驱动模块,但是不会解决模块间的依赖关系,而modprobe可以解决模块间的依赖关系。
也有两种命令可以卸载驱动模块:rmmod和modprobe,一样的,rmmod直接卸载对应驱动模块,而modprobe可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则不能使用modprobe来卸载驱动模块。
加载和卸载命令如下:

加载:
insmod drv.ko
modprobe drv.ko
卸载:
rmmod drv.ko
modprobe -r drv.ko

2.2 字符设备注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。

static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}

static inline void unregister_chrdev(unsigned int major, const char *name)
{
	__unregister_chrdev(major, 0, 256, name);
}

以上两个函数就是用于字符设备注册和注销的函数

2.3 实现设备的具体操作函数

file_operations 结构体就是设备的具体操作函数,需要对这个结构体进行初始化,当应用层调用系统调用的时候,能够调用到驱动的对应函数。

1.能够对字符设备进行打开和关闭操作
设备打开和关闭是最基本的需求,需要实现file_operations 中的open和release两个函数
2. 对字符设备进行读写操作
需要重写file_operations 中的write和read两个函数

2.4 添加license和作者信息

最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。LICENSE 和作者信息的添加使用如下两个函数:

MODULE_AUTHOR();
MODULE_LICENSE();

3 Linux设备号

3.1 设备号组成

为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下:

typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

dev_t是unsigned int类型的,是一个32为的数据类型,这32位分为主设备号和次设备号两个部分,高12位为主设备号,低20位为次设备号。因此 Linux 系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文件 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))

3.2 设备号分配

3.2.1 静态分配设备号

本小节讲的设备号分配主要是主设备号的分配。前面讲解字符设备驱动的时候说过了,注册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态指定的一个设备号,比如 200 这个主设备号。有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt。并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号,使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。

3.2.2 动态分配设备号

静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题,Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

参数依次为:保存申请到的设备号;次设备号其实地址(这个函数申请一连串多个设备号,其中主设备号相同,次设备号不同,次设备号以baseminor为起始地址开始递增,一般为0);要申请的设备号数量;设备名字
注销字符设备之后要释放掉设备号,设备号释放函数如下:

void unregister_chrdev_region(dev_t from, unsigned count)

参数依次为:要释放的设备号;从from开始,要释放的设备号数量

4 字符设备驱动实验

4.1 实验概述

创建一个chrdevbase虚拟设备,设备有两个缓冲区,一个读缓冲区,一个写缓冲区,两个缓冲区的大小都是100字节。应用程序可以向chrdevbase设备的写缓冲区中写入数据,从读缓冲区中读取数据。

4.2 代码编写

4.2.1 配置依赖头文件

首先,需要配置依赖的内核头文件的位置,VSCode中按下“Crtl+Shift+P”打开的控制台,然后输入
“C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件
在这里插入图片描述
打开之后会自动在.vscode目录下生成一个名为 c_cpp_properties.json 的文件,需要在这个文件中添加依赖的头文件

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/alientek/code/rk3568_linux/kernel/arch/arm64/include/",
                "/home/alientek/code/rk3568_linux/kernel/include",
                "/home/alientek/code/rk3568_linux/kernel/arch/arm64/include/generated"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

generated 文件夹必须先编译内核成功才会生成,并且需要确认自己的 SDK 路径

4.2.2 编写并编译驱动程序

以下是字符设备的驱动部分代码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>

// 定义主设备号
#define CHRDEVBASE_MAJOR 200
// 定义设备名称
#define CHRDEVBASE_NAME "chrdevbase"

// 读写缓冲
static char readbuf[100];
static char writebuf[100];
// 内核设备返回数据
static char kernelData[] = {"kernel data!"};

// 设备open时callback,应用侧调用open打开设备时内核回调
static int chrdevbase_open(struct inode *inode, struct file *filp) {
    printk("chrdevbase open!\r\n");
    return 0;
}

// 设备read时callback,应用侧调用read读取设备时内核回调
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {
    int retValue = 0;
    memcpy(readbuf, kernelData, sizeof(kernelData));
    // 将数据从内核空间拷贝到用户空间
    retValue = copy_to_user(buf, readbuf, cnt);
    if (retValue == 0) {
        printk("kernel senddata ok!\r\n");
    } else {
        printk("kernel senddata failed!\r\n");
    }
    return 0;
}

// 设备write时callback,应用侧调用write写入数据时内核回调
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {
    int retValue = 0;
    // 将数据从用户空间拷贝到内核空间
    retValue = copy_from_user(writebuf, buf, cnt);
    if (retValue == 0) {
        printk("kernel recevdata:%s\r\n", writebuf);
    } else {
        printk("kernel recevdata failed!\r\n");
    }
    return 0;
}

// 设备close时callback,应用侧调用close关闭设备时内核回调
static int chrdevbase_release(struct inode *inode, struct file *filp) {
    printk("chrbasedev release!\r\n");
    return 0;
}

// 文件操作函数映射结构体
static struct file_operations chrdevbase_fops = {
    .owner = THIS_MODULE,
    .open = chrdevbase_open,
    .read = chrdevbase_read,
    .write = chrdevbase_write,
    .release = chrdevbase_release,
};

// 内核模块初始化回调
static int __init chrdevbase_init(void) {
    int retValue = 0;
    // 注册字符设备
    retValue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
    if (retValue < 0) {
        printk("chrdevbase driver register failed\r\n");
    }
    printk("chrdevbase_init()\r\n");
    return 0;
}

// 内核模块注销回调
static void __exit chrdevbase_exit(void) {
    unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    printk("chrdevbase_exit()\r\n");
}

// 注册内核模块的初始化回调函数和注销回调函数
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

// 添加license、作者和其他模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

这里定义主设备号为200,设备名称为chrdevbase,然后是根据file_operations来定义内核操作相关的函数,这里定义了open、read、write和realse四个函数,分别对应应用空间的open、read、write和close四个处理函数。
定义完成之后,定义了模块的初始化和退出函数,这两个函数在模块加载和移除时调用。
然后将模块初始化和退出函数注册到内核中。
最后定义了模块的一些通用信息,如license、作者和模块信息等。
解析来编写makefile文件

KERNELDIR := /home/alientek/code/rk3568_linux/kernel
CURRENT_PATH :=	$(shell pwd)
obj-m := chrdevbase.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

然后执行编译命令

make ARCH=arm64

编译完成之后,会生成文件chrdevbase.ko

4.2.3 编写并编译驱动测试程序

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

static char userdata[] = {"user data!"};

int main(int argc, int *argv[]) {
    int fd, retValue;
    char *fileName;
    char readBuf[100], writeBuf[100];

    if (argc != 3) {
        printf("Error Usage!\r\n");
        return -1;
    }

    fileName = argv[1];
    fd = open(fileName, O_RDWR);
    if (fd < 0) {
        printf("Can't open file %s\r\n", fileName);
        return -1;
    }

    if (atoi(argv[2]) == 1) {
        retValue = read(fd, readBuf, 50);
        if (retValue < 0) {
            printf("read file %s failed!\r\n", fileName);
        } else {
            printf("read data:%s\r\n", readBuf);
        }
    }

    if (atoi(argv[2]) == 2){
        memcpy(writeBuf, userdata, sizeof(userdata));
        retValue = write(fd, writeBuf, 50);
        if (retValue < 0) {
            printf("write file %s failed!\r\n", fileName);
        }
    }

    retValue = close(fd);
    if (retValue < 0) {
        printf("Can't close file %s\r\n", fileName);
        return -1;
    }

    return 0;
}

这里没有多少逻辑,主要就是打开设备,读写设备,最后关闭设备。
编译测试应用程序

/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc chrdevbaseApp.c -o chrdevbaseApp

使用atk提供的buildroot的编译工具链编译chrdevbaseApp.c成测试可执行程序chrdevbaseApp

4.3 测试驱动实验

4.3.1 push测试文件到设备

首先,将生成的内核模块chrdevbase.ko和驱动测试程序chrdevbaseApp推到设备中的/lib/modules/4.19.232目录。
其中4.19.232是内核版本号。

4.3.2 加载驱动模块

有两种方式加载驱动模块:insmod和modprobe,两者的区别就是modprobe可以加载依赖的模块,而insmod不会。
使用modprobe加载chrdevbase.ko的时候会有以下提示:

root@ATK-DLRK356X:/lib/modules/4.19.232# modprobe chrdevbase.ko
modprobe: FATAL: Module chrdevbase.ko not found in directory /lib/modules/4.19.232

modprobe 命令会在“/lib/modules/4.19.232”目录下解析 modules.dep 文件,modules.dep 文
件里面保存了要加载的.ko 模块,我们不用手动创建 modules.dep 这个文件,直接输入 depmod
命令即可自动生成 modules.dep,有些根文件系统可能没有 depmod 这个命令,如果没有这个命
令就只能重新配置 busybox,使能此命令,然后重新编译 busybox。输入“depmod”命令以后会
自动生成 modules.alias、modules.symbols 和 modules.dep 等等一些 modprobe 所需的文件,如下图所示:
在这里插入图片描述
然后重新加载驱动:

modprobe chrdevbase.ko

加载完成之后,通过串口可以看到内核打印
在这里插入图片描述
然后在设备中lsmod查看驱动模块,可以看到驱动已经加载成功
在这里插入图片描述

cat /proc/devices

查看系统中当前的设备,可以看到设备已经存在,设备号为200
在这里插入图片描述

4.3.3 测试驱动模块

此时,驱动模块已经加载,但是驱动模块还没有与特定的设备节点进行绑定,测试应用程序还不能与之通信。首先我们需要创建设备节点并绑定设备。

mknod /dev/chrdevbase c 200 0

在这里插入图片描述
这里,mknod命令用于创建设备节点,/dev/chrdevbase就是创建的设备节点文件,c表示创建字符型设备,200是主设备号,0是次设备号。
创建设备节点完成之后,就可以与字符设备进行通信了。这里的设备节点相当于内核模块在用户空间的呈现,用于应用程序读写这个设备节点,就是操作对应内核模块注册的字符设备。
测试:
执行读操作

./chrdevbaseApp /dev/chrdevbase 1

在这里插入图片描述
应用层读取到了内核的数据,并打印
在这里插入图片描述
这是内核的打印,分别对应应用层的open,read,close操作
执行写操作

./chrdevbaseApp /dev/chrdevbase 2

在这里插入图片描述
内核接收到了应用层传下去的数据。

4.3.3 卸载驱动程序

卸载驱动程序是用rmmod命令

rmmod chrdevbase

在这里插入图片描述
在这里插入图片描述
驱动卸载成功,lsmod查看没有chrdevbase这个模块了。同时内核打印显示也调用了exit函数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1956420.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第05课 Scratch入门篇:海底世界-多彩的鱼

海底世界-多彩的鱼 入门篇适合新手&#xff0c;如您已经学过&#xff0c;可以忽略本节课&#xff01; 故事背景&#xff1a; 蔚蓝的海洋底部有一群凶猛的鲨鱼和一群色彩斑斓的小鱼&#xff0c;还有变色的水母&#xff0c;敲打乐器的章鱼&#xff0c;还有一些能够变色的小鱼畅…

在Ollama运行HuggingFace下载的模型

本地运行模型我之前都直接使用LM-studio&#xff0c;好用、无脑。本地用足够了。但是放在服务器上才是正道&#xff0c;本地运行无法长时间开启保持运行&#xff0c;而且Ollama推出了并行GPU计算之后可用性大幅提升&#xff0c;可用性很高。 今天研究下如何用Ollama如何在本地来…

Python 教程(六):函数式编程

目录 专栏列表前言函数定义参数返回值 示例函数类型普通函数空函数匿名函数&#xff08;Lambda 函数&#xff09;嵌套函数函数装饰器高阶函数 函数参数位置参数默认参数可变位置参数可变关键字参数 函数属性和方法__name____doc__func.__dict__func.__defaults__func.__annotat…

如何为 5G 小型基站部署选择振荡器

5G 网络频谱频率更高、覆盖范围更短&#xff0c;因此比前几代网络密度更高。超高速 5G 回程 (mmWave) 在很大程度上依赖于小型基站&#xff0c;不仅是为了覆盖范围&#xff0c;也是为了速度。除此之外&#xff0c;O-RAN 联盟等举措为 RAN 生态系统提供了更多选择&#xff0c;但…

html+css 实现多选按钮动画(input checkbox按钮)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享htmlcss 绚丽效果&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 文…

Vue3(二):computed、watch、生命周期、hooks

一、computed计算属性 <template><div class"person"> <!-- <input type"text" v-model"{{ firstName }}"> <input type"text" v-model"{{ lastName }}"> --><h1>一个人的信息</h1…

爬虫-通过几个例子来说明并发以及多线程

并发 什么是并发&#xff1f;并发&#xff0c;在操作系统中&#xff0c;是指一个时间段中有几个程序都处于已启动运行到运行完毕之间&#xff0c;且这几个程序都是在同一个处理机上运行&#xff0c;但任一个时刻点上只有一个程序在处理机上运行。 嗯&#xff0c;字认识&#…

vulntarget-b

实际部署之后centos7 的ip有所变动分别是 :192.168.127.130以及10.0.20.30 Centos7 老规矩还是先用fscan扫一下服务和端口&#xff0c;找漏洞打 直接爆出来一个SSH弱口令…&#xff0c;上来就不用打了&#xff0c;什么意思&#xff1f;&#xff1f;&#xff1f; 直接xshell…

快递员送包裹与一致性哈希的关系

一致性哈希&#xff08;Consistent Hashing&#xff09;是一种用于分布式系统中数据分布和负载均衡的哈希技术。它通过减少数据迁移、支持动态扩展和高容错等特点&#xff0c;在分布式缓存、存储、负载均衡等系统中有广泛应用。以下是对一致性哈希的详细介绍&#xff1a; 一致…

跨境电商平台评论管理:如何避免评论被删及提高留评率

在跨境电商领域&#xff0c;评论对于产品的销售和品牌形象至关重要。然而&#xff0c;卖家常常面临评论被删除的问题&#xff0c;这不仅影响了产品的曝光和销售&#xff0c;还可能对店铺声誉造成损害。本文将探讨亚马逊、Ozon、速卖通、Lazada等跨境电商平台评论被删除的原因&a…

财务分析,奥威BI行计算助力财务解放报表工作

【财务分析&#xff0c;奥威BI行计算助力财务解放报表工作】 在企业的财务管理体系中&#xff0c;财务报表的编制与分析是至关重要的一环。然而&#xff0c;传统的手工编制报表方式不仅耗时耗力&#xff0c;还难以应对日益复杂多变的财务数据需求。奥威BI&#xff08;Business…

2024最火的知识付费系统小程序+PC+H5三端数据互通支持采集资源开源版

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 系统含带 裂变模式 可以助力好友来获取资源共享 分站功能 独立后台 会员功能 卡密功能 二级分销功能等 自行研究看 后期有更新新版会在持续发布 目前版本是3.5 是我花三天时间修复的 …

数据开发/数仓工程师上手指南(三)数仓构建流程

前言 此系列的上篇文章通过拆解电商业务数仓系统&#xff0c;通过数仓分层概念对整个业务进行拆解分层&#xff0c;那么本章节将沿着上一篇的数仓概念分层切割电商业务&#xff0c;去具体构建电商业务的数据仓库&#xff0c;我们将按照行业认可标准的流程去构建较为完整的数据…

【C++的剃刀】我不允许你还不会AVL树

​ 学习编程就得循环渐进&#xff0c;扎实基础&#xff0c;勿在浮沙筑高台 循环渐进Forward-CSDN博客 Hello,这里是kiki&#xff0c;今天继续更新C部分&#xff0c;我们继续来扩充我们的知识面&#xff0c;我希望能努力把抽象繁多的知识讲的生动又通俗易懂&#xff0c;今天要…

springboot电影院线上购票系统-计算机毕业设计源码68220

目录 摘要 1 绪论 1.1 选题背景与意义 1.2国内外研究现状 1.3论文结构与章节安排 2系统分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 法律可行性分析 2.2 系统流程分析 2.2.1 添加信息流程 2.2.2 修改信息流程 2.2.3 删除信息流程 2.3 系统功能分析 2.…

暑期审稿慢,第三轮审稿人拒绝复审,怎么办?

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 暑期到了&#xff0c;国内的审稿人又慢了。近期不少学员问我&#xff1a;“娜姐&#xff0c;审稿一直没动静&#xff0c;可以催吗&#xff1f;真是着急啊 &#xff01;” …

如何在 VitePress 中自定义logo,打造精美首页 #home-hero-image

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storm…

C语言 | Leetcode C语言题解之第282题给表达式添加运算符

题目&#xff1a; 题解&#xff1a; #define MAX_COUNT 10000 // 解的个数足够大 #define NUM_COUNT 100 // 操作数的个数足够大 long long num[NUM_COUNT] {0};long long calc(char *a) { // 计算表达式a的值// 将数字和符号&#xff0c;入栈memset(num, 0, sizeof(num));in…

差分法求解 Burgers 方程(附完整MATLAB 及 Python代码)

Burgers 方程的数值解及误差分析 引言 Burgers 方程是一个非线性偏微分方程&#xff0c;在流体力学、非线性声学和交通流理论中有广泛应用。本文将通过数值方法求解带粘性的 Burgers 方程&#xff0c;并分析其误差。 方程模型 Burgers 方程的形式为&#xff1a; u t u u …

在react中如何计算本地存储体积

1.定义useLocalStorageSize钩子函数 // 计算localStorage大小 function useLocalStorageSize() {const [size, setSize] useState(0);useEffect(() > {const calculateSize () > {let totalSize 0;for (let key in localStorage) {//过滤掉继承自原型链的属性if (loc…