Linux x86_64 dump_stack()函数基于FP栈回溯

文章目录

  • 前言
  • 一、dump_stack函数使用
  • 二、dump_stack函数源码解析
    • 2.1 show_stack
    • 2.2 show_stack_log_lvl
    • 2.3 show_trace_log_lvl
    • 2.4 dump_trace
    • 2.5 print_context_stack
  • 参考资料

前言

Linux x86_64
centos7
Linux:3.10.0

一、dump_stack函数使用

dump_stack函数用于打印当前任务的信息以及其堆栈跟踪,能够用来回溯打印调用栈信息。

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

void noinline func_c(void)
{
	dump_stack();	
}

void noinline func_b(void)
{
	func_c();	
}

void noinline func_a(void)
{
	func_b();
}

//内核模块初始化函数
static int __init lkm_init(void)
{
	func_a();
	return 0;
}

//内核模块退出函数
static void __exit lkm_exit(void)
{
	printk("Goodbye\n");
}

module_init(lkm_init);
module_exit(lkm_exit);

MODULE_LICENSE("GPL");

这里加了noinline修饰,否则会被优化成 inline 函数。

[1109990.858938] Call Trace:
[1109990.858952]  [<ffffffff8e781340>] dump_stack+0x19/0x1b
[1109990.858960]  [<ffffffffc0a3700e>] func_c+0xe/0x10 [helloworld]
[1109990.858968]  [<ffffffffc0a3701e>] func_b+0xe/0x10 [helloworld]
[1109990.858974]  [<ffffffffc0a3702e>] func_a+0xe/0x10 [helloworld]
[1109990.858981]  [<ffffffffc0153009>] lkm_init+0x9/0x1000 [helloworld]
[1109990.858990]  [<ffffffff8e00210a>] do_one_initcall+0xba/0x240
[1109990.858999]  [<ffffffff8e11e45a>] load_module+0x271a/0x2bb0
[1109990.859007]  [<ffffffff8e3b4290>] ? ddebug_proc_write+0x100/0x100
[1109990.859016]  [<ffffffff8e119fe3>] ? copy_module_from_fd.isra.44+0x53/0x150
[1109990.859024]  [<ffffffff8e11ead6>] SyS_finit_module+0xa6/0xd0
[1109990.859033]  [<ffffffff8e793f92>] system_call_fastpath+0x25/0x2a

Linux dump_stack 函数原理:
栈帧如下如图所示:callee的RBP寄存器的值保存caller的RBP寄存器地址,可以看作每个栈帧用单链表连接。

// linux-3.10/arch/x86/include/asm/stacktrace.h

/* The form of the top of the frame on the stack */
struct stack_frame {
	struct stack_frame *next_frame;
	unsigned long return_address;
};

在这里插入图片描述

帧指针起到了历史上的作用。帧指针是一个寄存器,它始终包含着上一个堆栈指针的值。在 x86_64 架构中,通常使用的寄存器是 RBP。

由于帧指针寄存器的存在,堆栈现在成为了一个“堆栈帧”的链表,我们可以一直沿着链表向前遍历到开头。在任何时刻,我们只需查看当前帧指针寄存器的值,就可以获得先前的 RSP 值。由于先前的 RSP 值恰好是存储先前帧指针的位置,因此这就是一系列指针沿着堆栈向上爬行的过程。

通过遍历堆栈帧链表,我们可以逐个获取每个函数的返回地址、参数和局部变量等信息。这样,我们就可以按顺序打印每个函数的名称,实现堆栈跟踪。

帧指针寄存器的存在使得堆栈帧之间形成了链式结构,使得在堆栈跟踪过程中可以方便地从当前帧指针寄存器获取前一个堆栈帧的位置。通过这种方式,我们可以沿着堆栈链表一直向上遍历,获取所有函数的信息。

帧指针寄存器的使用使得堆栈跟踪变得更加直观和可靠,因为它提供了一种可靠的方式来遍历堆栈帧链表。但是需要注意的是,某些情况下,编译器可能会对帧指针进行优化或省略,因此在特定的编译器优化设置下,帧指针可能不可用或不准确。

centos 7 配置了CONFIG_FRAME_POINTER选项:

# cat /boot/config-3.10.0-1160.el7.x86_64 | grep CONFIG_FRAME_POINTER
CONFIG_FRAME_POINTER=y

(1)基于Frame Pointer - fp寄存器的栈回溯:
优点:栈回溯比较快,理解简单。相对较简单:基于Frame Pointer寄存器的栈回溯通常比解析unwind节更简单直接。
缺点:gcc添加了优化选项 -O 就会省略掉省略基指针。这样就不能都通过这种形式进行栈回溯了。
-fomit-frame-pointer编译标志进行优化:避免将%rbp用作栈帧指针,把FP当作一个通用寄存器,这样就提供了一个额外的通用寄存器,提高程序运行效率。

(1)func_c RBP寄存器的值存放了父函数func_b的RBP寄存器的地址。其返回地址 = func_cRBP寄存器地址+8。
(2)对函数func_b的RBP寄存器的地址取值获取func_b RBP寄存器的值,func_b RBP寄存器的值存放了父函数func_a的RBP寄存器的地址。其返回地址 = func_bRBP寄存器地址+8。
(3)对函数func_a的RBP寄存器的地址取值获取func_a RBP寄存器的值,func_a RBP寄存器的值存放了父函数lkm_init的RBP寄存器的地址。其返回地址 = func_aRBP寄存器地址+8。
这样一步步回溯就可以获取整个调用栈。

在 x86_64 架构中 rbp 指向当前栈帧的起始位置,这个位置保存着旧的 rbp的值。我们可以看到在旧的 rbp 保存的位置上方保存着返回地址(rbp + 8)。这个返回地址是调用者函数中 call 指令的下一条指令的地址,子函数执行完成后会返回,旧的 rbp 首先出栈并赋值给 rbp 寄存器,同时返回地址也要出栈并赋值给 pc。

上面的过程可以递归的用于多层函数调用上。

我们可以将 dump_stack 函数的栈帧看做 Current frame,当前 pc 的值保存的是 dump_stack 中的某条指令的地址,内核先根据这个地址查询 符号表 获取到 dump_stack 函数的名称与当前指令先相对于 dump_stack 函数起始位置的偏移量,然后通过访问 rbp 寄存器指向的旧 rbp 的值来获取到调用 dump_stack 函数的栈帧指针的值,有了这个值就可以不断的回溯上方的栈帧,一个栈帧就是一个调用层次。

同时返回地址的位置就在旧的 rbp 存储位置的上方,根据这样的特点 dump_stack 也就能回溯不同调用层次中返回地址的值。根据返回地址就可以获取到返回地址的上一条调用语句的地址,对该地址进行寻址,获取到指令的编码,就能够获取到调用函数的入口地址。这里可以使用如下公式:

call 指令调用函数的地址 = call 指令码后面的偏移量 + 返回地址

这之后使用入口地址查询 System-map 获取到函数的名称,同时计算出返回地址相对于函数入口的偏移量就准备好了打印的内容,调用打印函数打印信息,每个栈帧用单链表连接,然后继续重复这一过程直到找不到一个合法的栈帧为止。

二、dump_stack函数源码解析

centos 7 配置了CONFIG_FRAME_POINTER选项:

# cat /boot/config-3.10.0-1160.el7.x86_64 | grep CONFIG_FRAME_POINTER
CONFIG_FRAME_POINTER=y
// linux-3.10/lib/dump_stack.c

/**
 * dump_stack - dump the current task information and its stack trace
 *
 * Architectures can override this implementation by implementing its own.
 */
void dump_stack(void)
{
	dump_stack_print_info(KERN_DEFAULT);
	show_stack(NULL, NULL);
}
EXPORT_SYMBOL(dump_stack);
dump_stack()
	-->show_stack()
		-->show_stack_log_lvl()
			-->show_trace_log_lvl()
				-->dump_trace()
					-->print_context_stack()

2.1 show_stack

// linux-3.10/arch/x86/include/asm/stacktrace.h

#define STACKSLOTS_PER_LINE 4
#define get_bp(bp) asm("movq %%rbp, %0" : "=r" (bp) :)

#ifdef CONFIG_FRAME_POINTER
static inline unsigned long
stack_frame(struct task_struct *task, struct pt_regs *regs)
{
	unsigned long bp;

	if (regs)
		return regs->bp;

	if (task == current) {
		/* Grab bp right from our regs */
		get_bp(bp);
		return bp;
	}

	/* bp is the last reg pushed by switch_to */
	return *(unsigned long *)task->thread.sp;
}

get_bp(bp)是一个宏定义,使用汇编语句获取当前函数的基址寄存器(rbp)的值,并将其保存在bp变量中。

stack_frame是一个内联函数,用于获取给定任务的栈帧指针。

(1)如果传入的regs参数非空,说明已经提供了寄存器上下文(pt_regs结构),则直接返回其中的基址寄存器(bp)的值。

(2)如果给定的任务结构体指针与当前任务相同(current表示当前任务),则直接使用get_bp宏获取当前函数的基址寄存器的值(rbp),并将其作为栈帧指针返回。

(3)如果以上条件都不满足,则假设bp是由switch_to函数推入的最后一个寄存器,从给定任务的线程结构体中获取栈指针(sp)所指向的地址,并将其解释为unsigned long类型的指针,以获取栈帧指针。

void show_stack(struct task_struct *task, unsigned long *sp)
{
	unsigned long bp = 0;
	unsigned long stack;

	/*
	 * Stack frames below this one aren't interesting.  Don't show them
	 * if we're printing for %current.
	 */
	if (!sp && (!task || task == current)) {
		sp = &stack;
		bp = stack_frame(current, NULL);
	}

	show_stack_log_lvl(task, NULL, sp, bp, "");
}

该函数用于打印给定任务的堆栈跟踪信息。

2.2 show_stack_log_lvl

// linux-3.10/arch/x86/kernel/dumpstack_64.c

void
show_stack_log_lvl(struct task_struct *task, struct pt_regs *regs,
		   unsigned long *sp, unsigned long bp, char *log_lvl)
{
	unsigned long *irq_stack_end;
	unsigned long *irq_stack;
	unsigned long *stack;
	int cpu;
	int i;

	preempt_disable();
	cpu = smp_processor_id();

	irq_stack_end	= (unsigned long *)(per_cpu(irq_stack_ptr, cpu));
	irq_stack	= (unsigned long *)(per_cpu(irq_stack_ptr, cpu) - IRQ_STACK_SIZE);

	/*
	 * Debugging aid: "show_stack(NULL, NULL);" prints the
	 * back trace for this cpu:
	 */
	if (sp == NULL) {
		if (task)
			sp = (unsigned long *)task->thread.sp;
		else
			sp = (unsigned long *)&sp;
	}

	stack = sp;
	for (i = 0; i < kstack_depth_to_print; i++) {
		if (stack >= irq_stack && stack <= irq_stack_end) {
			if (stack == irq_stack_end) {
				stack = (unsigned long *) (irq_stack_end[-1]);
				pr_cont(" <EOI> ");
			}
		} else {
		if (((long) stack & (THREAD_SIZE-1)) == 0)
			break;
		}
		if (i && ((i % STACKSLOTS_PER_LINE) == 0))
			pr_cont("\n");
		pr_cont(" %016lx", *stack++);
		touch_nmi_watchdog();
	}
	preempt_enable();

	pr_cont("\n");
	show_trace_log_lvl(task, regs, sp, bp, log_lvl);
}

show_stack_log_lvl函数用于打印给定任务的堆栈跟踪信息,并在日志级别上进行控制。

函数首先定义了一些局部变量,包括irq_stack_end、irq_stack、stack、cpu和i。

然后,禁用抢占(preempt_disable)并获取当前处理器的 ID(smp_processor_id)。

irq_stack_end表示中断堆栈的结束地址,irq_stack表示中断堆栈的起始地址(通过per_cpu宏和irq_stack_ptr变量计算得到)。

接下来,通过一系列条件判断,确定要打印的堆栈跟踪信息。

如果给定的sp参数为空,表示需要打印当前任务的堆栈跟踪信息。根据是否提供了任务结构体指针(task),确定要使用的栈指针(sp)。如果提供了任务结构体指针,则使用任务的线程结构体中的栈指针;否则,使用当前函数的栈指针。

接下来,通过循环遍历堆栈,打印堆栈上的地址。在遍历过程中,通过一系列条件判断确定是否处于中断堆栈范围内,并在特定情况下打印(End of Interrupt)标记。如果堆栈地址与线程栈的大小(THREAD_SIZE)对齐,则表示已经遍历到了栈的底部,循环结束。

在每次打印堆栈地址后,调用touch_nmi_watchdog函数,用于触发非屏蔽中断(NMI)看门狗,以确保系统不会因为长时间占用CPU而被认为是死锁。

最后,启用抢占(preempt_enable),打印换行符,然后调用show_trace_log_lvl函数,将任务结构体指针、寄存器上下文、栈指针、栈帧指针和日志级别作为参数传递,继续打印堆栈跟踪信息。

2.3 show_trace_log_lvl

// linux-3.10/arch/x86/kernel/dumpstack.c

void
show_trace_log_lvl(struct task_struct *task, struct pt_regs *regs,
		unsigned long *stack, unsigned long bp, char *log_lvl)
{
	printk("%sCall Trace:\n", log_lvl);
	dump_trace(task, regs, stack, bp, &print_trace_ops, log_lvl);
}
Call Trace:
[1119269.645012]  [<ffffffff8e781340>] dump_stack+0x19/0x1b
[1119269.645021]  [<ffffffffc0a5000e>] func_c+0xe/0x10 [helloworld]
[1119269.645028]  [<ffffffffc0a5001e>] func_b+0xe/0x10 [helloworld]
[1119269.645034]  [<ffffffffc0a5002e>] func_a+0xe/0x10 [helloworld]
[1119269.645041]  [<ffffffffc0153009>] lkm_init+0x9/0x1000 [helloworld]
[1119269.645049]  [<ffffffff8e00210a>] do_one_initcall+0xba/0x240
[1119269.645059]  [<ffffffff8e11e45a>] load_module+0x271a/0x2bb0
[1119269.645066]  [<ffffffff8e3b4290>] ? ddebug_proc_write+0x100/0x100
[1119269.645075]  [<ffffffff8e119fe3>] ? copy_module_from_fd.isra.44+0x53/0x150
[1119269.645083]  [<ffffffff8e11ead6>] SyS_finit_module+0xa6/0xd0
[1119269.645093]  [<ffffffff8e793f92>] system_call_fastpath+0x25/0x2a

2.4 dump_trace

(1)

// linux-3.10/arch/x86/kernel/dumpstack_64.c

/*
 * x86-64 can have up to three kernel stacks:
 * process stack
 * interrupt stack
 * severe exception (double fault, nmi, stack fault, debug, mce) hardware stack
 */

void dump_trace(struct task_struct *task, struct pt_regs *regs,
		unsigned long *stack, unsigned long bp,
		const struct stacktrace_ops *ops, void *data)
{
	const unsigned cpu = get_cpu();
	unsigned long *irq_stack_end =
		(unsigned long *)per_cpu(irq_stack_ptr, cpu);
	unsigned used = 0;
	struct thread_info *tinfo;
	int graph = 0;
	unsigned long dummy;

	if (!task)
		task = current;

	if (!stack) {
		if (regs)
			stack = (unsigned long *)regs->sp;
		else if (task != current)
			stack = (unsigned long *)task->thread.sp;
		else
			stack = &dummy;
	}

	if (!bp)
		bp = stack_frame(task, regs);
	/*
	 * Print function call entries in all stacks, starting at the
	 * current stack address. If the stacks consist of nested
	 * exceptions
	 */
	tinfo = task_thread_info(task);
	for (;;) {
		char *id;
		unsigned long *estack_end;
		estack_end = in_exception_stack(cpu, (unsigned long)stack,
						&used, &id);

		if (estack_end) {
			if (ops->stack(data, id) < 0)
				break;

			bp = ops->walk_stack(tinfo, stack, bp, ops,
					     data, estack_end, &graph);
			ops->stack(data, "<EOE>");
			/*
			 * We link to the next stack via the
			 * second-to-last pointer (index -2 to end) in the
			 * exception stack:
			 */
			stack = (unsigned long *) estack_end[-2];
			continue;
		}
		if (irq_stack_end) {
			unsigned long *irq_stack;
			irq_stack = irq_stack_end -
				(IRQ_STACK_SIZE - 64) / sizeof(*irq_stack);

			if (in_irq_stack(stack, irq_stack, irq_stack_end)) {
				if (ops->stack(data, "IRQ") < 0)
					break;
				bp = ops->walk_stack(tinfo, stack, bp,
					ops, data, irq_stack_end, &graph);
				/*
				 * We link to the next stack (which would be
				 * the process stack normally) the last
				 * pointer (index -1 to end) in the IRQ stack:
				 */
				stack = (unsigned long *) (irq_stack_end[-1]);
				irq_stack_end = NULL;
				ops->stack(data, "EOI");
				continue;
			}
		}
		break;
	}

	/*
	 * This handles the process stack:
	 */
	bp = ops->walk_stack(tinfo, stack, bp, ops, data, NULL, &graph);
	put_cpu();
}
EXPORT_SYMBOL(dump_trace);

dump_trace函数用于在给定任务的堆栈上进行跟踪,并通过提供的回调函数执行相应的操作。

函数首先定义了一些局部变量,包括cpu、irq_stack_end、used、tinfo和graph,以及一个dummy变量。

然后,根据情况,确定要跟踪的任务和堆栈的起始地址。如果没有给定任务,则默认使用当前任务。如果没有给定堆栈地址,则根据情况选择使用寄存器上下文的栈指针、任务的线程结构体中的栈指针,或者一个临时变量作为栈指针。

接下来,如果没有给定基指针(bp),则通过调用stack_frame函数计算基指针。

在一个无限循环中,函数根据堆栈的类型进行处理。首先,通过调用in_exception_stack函数检查堆栈是否属于异常堆栈(如双重故障、NMI、堆栈故障、调试、MCE等),并获取异常堆栈的结束地址(estack_end)以及用于标识堆栈的字符串(id)。

如果堆栈属于异常堆栈(接上文)

如果堆栈属于异常堆栈,将调用回调函数ops->stack(data, id)打印堆栈标识符,并通过调用ops->walk_stack函数执行堆栈的遍历操作。然后,再次调用ops->stack(data, “”)打印异常堆栈的结束标识符。之后,通过异常堆栈的倒数第二个指针(索引为-2)获取下一个堆栈的起始地址,并继续下一轮循环。

如果堆栈不属于异常堆栈,将检查是否存在中断堆栈(IRQ stack)。如果存在中断堆栈,将通过调用in_irq_stack函数判断当前堆栈是否属于中断堆栈,并获取中断堆栈的起始地址。如果当前堆栈属于中断堆栈,则与异常堆栈类似,调用回调函数打印中断标识符,并通过ops->walk_stack函数执行中断堆栈的遍历操作。然后,通过中断堆栈的最后一个指针(索引为-1)获取下一个堆栈的起始地址,并继续下一轮循环。

如果既不是异常堆栈也不是中断堆栈,表示已经遍历完所有堆栈,退出循环。

最后,通过调用ops->walk_stack函数处理进程堆栈,并完成整个跟踪过程。最后,调用put_cpu()释放当前CPU的引用计数。

该函数使用了一些其他函数和数据结构,例如task_thread_info函数用于获取线程信息,stack_frame函数用于计算基指针,in_exception_stack和in_irq_stack函数用于判断堆栈类型。回调函数ops->stack用于打印堆栈标识符,回调函数ops->walk_stack用于执行堆栈的遍历操作。

(2)

// linux-3.10/arch/x86/include/asm/stacktrace.h

/* Generic stack tracer with callbacks */

struct stacktrace_ops {
	void (*address)(void *data, unsigned long address, int reliable);
	/* On negative return stop dumping */
	int (*stack)(void *data, char *name);
	walk_stack_t	walk_stack;
};
/*
 * x86-64 can have up to three kernel stacks:
 * process stack
 * interrupt stack
 * severe exception (double fault, nmi, stack fault, debug, mce) hardware stack
 */

static inline int valid_stack_ptr(struct thread_info *tinfo,
			void *p, unsigned int size, void *end)
{
	void *t = tinfo;
	if (end) {
		if (p < end && p >= (end-THREAD_SIZE))
			return 1;
		else
			return 0;
	}
	return p > t && p < t + THREAD_SIZE - size;
}

unsigned long
print_context_stack(struct thread_info *tinfo,
		unsigned long *stack, unsigned long bp,
		const struct stacktrace_ops *ops, void *data,
		unsigned long *end, int *graph)
{
	struct stack_frame *frame = (struct stack_frame *)bp;

	while (valid_stack_ptr(tinfo, stack, sizeof(*stack), end)) {
		unsigned long addr;

		addr = *stack;
		if (__kernel_text_address(addr)) {
			if ((unsigned long) stack == bp + sizeof(long)) {
				ops->address(data, addr, 1);
				frame = frame->next_frame;
				bp = (unsigned long) frame;
			} else {
				ops->address(data, addr, 0);
			}
			print_ftrace_graph_addr(addr, data, ops, tinfo, graph);
		}
		stack++;
	}
	return bp;
}
EXPORT_SYMBOL_GPL(print_context_stack);

static int print_trace_stack(void *data, char *name)
{
	printk("%s <%s> ", (char *)data, name);
	return 0;
}

void printk_address(unsigned long address, int reliable)
{
	pr_cont(" [<%p>] %s%pB\n",
		(void *)address, reliable ? "" : "? ", (void *)address);
}

/*
 * Print one address/symbol entries per line.
 */
static void print_trace_address(void *data, unsigned long addr, int reliable)
{
	touch_nmi_watchdog();
	printk(data);
	printk_address(addr, reliable);
}

static const struct stacktrace_ops print_trace_ops = {
	.stack			= print_trace_stack,
	.address		= print_trace_address,
	.walk_stack		= print_context_stack,
};

2.5 print_context_stack

/* The form of the top of the frame on the stack */
struct stack_frame {
	struct stack_frame *next_frame;
	unsigned long return_address;
};
/*
 * x86-64 can have up to three kernel stacks:
 * process stack
 * interrupt stack
 * severe exception (double fault, nmi, stack fault, debug, mce) hardware stack
 */

static inline int valid_stack_ptr(struct thread_info *tinfo,
			void *p, unsigned int size, void *end)
{
	void *t = tinfo;
	if (end) {
		if (p < end && p >= (end-THREAD_SIZE))
			return 1;
		else
			return 0;
	}
	return p > t && p < t + THREAD_SIZE - size;
}

unsigned long
print_context_stack(struct thread_info *tinfo,
		unsigned long *stack, unsigned long bp,
		const struct stacktrace_ops *ops, void *data,
		unsigned long *end, int *graph)
{
	struct stack_frame *frame = (struct stack_frame *)bp;

	while (valid_stack_ptr(tinfo, stack, sizeof(*stack), end)) {
		unsigned long addr;

		addr = *stack;
		if (__kernel_text_address(addr)) {
			if ((unsigned long) stack == bp + sizeof(long)) {
				ops->address(data, addr, 1);
				frame = frame->next_frame;
				bp = (unsigned long) frame;
			} else {
				ops->address(data, addr, 0);
			}
			print_ftrace_graph_addr(addr, data, ops, tinfo, graph);
		}
		stack++;
	}
	return bp;
}
EXPORT_SYMBOL_GPL(print_context_stack);

print_context_stack函数用于在给定线程的堆栈上打印函数调用的地址,并通过提供的回调函数执行相应的操作。

函数首先定义了局部变量frame,它是一个指向struct stack_frame类型的指针,用于表示帧结构。

然后,使用一个循环遍历堆栈中的每个地址。在每次循环迭代中,函数检查堆栈指针是否有效,并获取当前堆栈指针处的地址。

如果地址属于内核文本空间(通过__kernel_text_address函数判断),则进行以下操作:

  1. 如果当前堆栈指针等于基指针加上一个long大小,表示该地址是当前函数调用的返回地址。在这种情况下,将调用回调函数ops->address(data, addr, 1)打印地址,并更新帧结构和基指针,使其指向上一帧的基指针。
  2. 如果当前堆栈指针不等于基指针加上一个long大小,表示该地址是普通的函数调用地址。在这种情况下,将调用回调函数ops->address(data, addr, 0)打印地址。
  3. 最后,调用print_ftrace_graph_addr函数打印与地址相关的ftrace图形信息。

在每次循环迭代后,将堆栈指针指向下一个地址。

最后,函数返回更新后的基指针。

参考资料

Linux 3.10.0

https://blogs.oracle.com/linux/post/unwinding-stack-frame-pointers-and-orc
https://blog.csdn.net/Longyu_wlz/article/details/103327538

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode 力扣题目:买卖股票的最佳时机 III

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

Autosar架构

蓝框那种叫component&#xff0c;绿框的叫function cluster。 接口 有三种接口&#xff0c;RTE跟SWC之间链接的叫Autosar Interface&#xff0c;RTE跟BSW的Components链接是Standardized Interface&#xff0c;RTE跟BSW的services链接的是Standardized Autosar Interface。 St…

C语言 8 函数递归

目录 1. 递归是什么&#xff1f; 2.递归的限制条件 3. 递归举例1 4. 递归举例2 5.迭代 6. 递归举例3 拓展学习&#xff1a; 1. 递归是什么&#xff1f; 递归是学习C语⾔函数绕不开的⼀个话题&#xff0c;那什么是递归呢&#xff1f; 递归其实是⼀种解决问题的⽅法&#xff0c…

【Spring】Springmvc学习Ⅲ

# Spring&#xff4d;vc学习Ⅲ 文章目录 一、图书管理系统1. 功能1.1 登录前端接口前端代码后端接口后端代码 1.2 图书列表展示步骤:图书类代码mock数据代码控制层调用代码服务层代码&#xff08;存储除数据库中需要存储的数据&#xff09; 2. 分层控制2.1 三层架构2.2 代码重…

Softing dataFEED OPC Suite通过OPC UA标准加速数字化转型

数字化转型的关键在于成功将信息技术&#xff08;IT&#xff09;与运营技术&#xff08;OT&#xff09;相融合&#xff0c;例如将商业应用程序和服务器与可编程逻辑控制器&#xff08;PLC&#xff09;和设备传感器相融合&#xff0c;为此&#xff0c;各种设备和系统必须能够相互…

【Day1:JAVA导学】

目录 1、path环境变量2、Java背景介绍2.1 Java SE&#xff1a;2.2 Java ME&#xff1a;2.3 Java EE&#xff1a; 3、Java的跨平台性3.1 Java跨平台的原理&#xff1a; 4、Java开发程序的三个步骤5、JDK的组成和配置5.1 JDK的组成&#xff1a; 6、IDEA项目结构介绍7、Java关键字…

01 | 为什么需要消息队列?

哪些问题适合使用消息队列来解决&#xff1f; 1. 异步处理 2. 流量控制 使用消息队列隔离网关和后端服务&#xff0c;以达到流量控制和保护后端服务的目的。 3. 服务解耦 无论增加、减少下游系统或是下游系统需求如何变化&#xff0c;订单服务都无需做任何更改&#xff0c…

秋招算法——AcWing101——拦截导弹

文章目录 题目描述思路分析实现源码分析总结 题目描述 思路分析 目前是有一个笨办法&#xff0c;就是创建链表记录每一个最长下降子序列所对应的节点的链接&#xff0c;然后逐个记录所有结点的访问情况&#xff0c;直接所有节点都被访问过。这个方法不是很好&#xff0c;因为需…

工作玩手机监测识别摄像机

工作场所的员工玩手机已经成为了一种常见的现象&#xff0c;特别是在办公室、生产车间等地方。而这种现象不仅仅影响了员工的工作效率&#xff0c;还可能会对工作安全造成一定的隐患。为了监测和识别员工玩手机的情况&#xff0c;工作玩手机监测识别摄像机应运而生。工作玩手机…

不知摄像机网段IP地址?别担心,这里有解决之道

在数字化、智能化的今天&#xff0c;摄像机作为安全监控和日常记录的重要工具&#xff0c;其应用越来越广泛。然而&#xff0c;在实际使用中&#xff0c;我们可能会遇到一些问题&#xff0c;比如忘记了摄像机的网段IP地址&#xff0c;这往往会让我们感到头疼。那么&#xff0c;…

Hashmap详细解析,原理及使用方法分析

hashmap基本原理 根据的hashCode值存储数据。由数组链表组成的&#xff0c;Entnr数组是HashMap的主体&#xff0c;数组中每个元素是一个单向链表。链表则是1/1解哈希冲突而存在的。在lava8中&#xff0c;使用红黑树优化。当链表长度大于8并且元素个数大于64&#xff0c;转为红…

官宣!招商工作全面启动“2024南京智博会”众多企业踊跃报名

2024南京智博会&#xff0c;作为一场盛大的科技盛宴&#xff0c;经过多年的发展与沉淀&#xff0c;已经成功跻身国内顶尖的高新技术产品及解决方案的展示平台之列&#xff0c;成为了引领行业趋势的风向标。本届智博会不仅汇聚了众多知名科技企业&#xff0c;更展现了国内外最前…

Java扫盲

1.常见的代码结构&#xff1a; 转自知乎天马行空的程序猿

##19 序列与时间序列预测:运用RNN和LSTM在PyTorch中的实践

文章目录 前言时间序列预测的基本概念关键概念 RNN及其局限性LSTM网络的崛起用PyTorch进行时间序列预测准备数据集数据预处理创建数据加载器构建LSTM模型训练模型测试和评估模型结语 前言 随着数据的爆炸式增长&#xff0c;时间序列预测在多个领域内变得越来越重要。它能帮助我…

jenkins+docker实现前后端项目的自动化构建和容器部署

1、安装环境 centos 2、docker安装 yum install docker# 启动docker systemctl start docker 3、docker 安装jenkins 3.1 拉取jenkins镜像 docker pull jenkins/jenkins:latest-jdk8 3.2 启动jenkins容器 docker run -d --name jenkins -u root --privilegedtrue --res…

界面组件DevExpress Reporting v24.1预览版 - 拥有原生Angular报表查看器

DevExpress Reporting是.NET Framework下功能完善的报表平台&#xff0c;它附带了易于使用的Visual Studio报表设计器和丰富的报表控件集&#xff0c;包括数据透视表、图表&#xff0c;因此您可以构建无与伦比、信息清晰的报表。 下一个主要更新(v24.1)将于6月初发布&#xff…

JWT -- 复盘

1、前言 1.1、Token流程 先来回顾一下利用 token 进行用户身份验证的流程&#xff1a; 客户端使用用户名和密码请求登录服务端收到请求&#xff0c;验证用户名和密码验证成功后&#xff0c;服务端会签发一个 token&#xff0c;再把这个 token 返回给客户端客户端收到 token 后…

Linux进程(一) -- 介绍进程

计算机的系统架构 用户部分 这是用户直接与计算机交互的部分&#xff0c;包括以下三种操作&#xff1a; 指令操作&#xff1a;用户通过命令行界面&#xff08;CLI&#xff09;输入指令来操作计算机。开发操作&#xff1a;开发人员编写和调试程序代码&#xff0c;与计算机系统…

[AWS] stepfunctions-local

本质是本地docker,只支持异步调用 run aws-stepfunctions-localdocker run -p 8083:8083 \ --mount type=bind,readonly,source=/path/MockConfigFile.json,destination=/home/StepFunctionsLocal/MockConfigFile.json \ -e SFN_MOCK_CONFIG="/home/StepFunctionsLocal/…

照明灯具十大排名都有哪些?市面上比较流行的十大护眼台灯品牌推荐

照明灯具十大排名都有哪些&#xff1f;护眼台灯排名当中靠前的主要有书客、飞利浦、松下等品牌。照明灯具作为家居与办公环境中不可或缺的元素&#xff0c;其品质与选择直接关系到人们的视觉健康与舒适度。本文将为大家揭示照明灯具的十大排名&#xff0c;让大家了解市场上最受…