浅析linux 内核 高精度定时器(hrtimer)实现机制(一)

news2025/1/17 21:50:57

1 hrtimer 概述

在Linux内核中已经存在了一个管理定时器的通用框架。不过它也有很多不足,最大的问题是其精度不是很高。哪怕底层的定时事件设备精度再高,定时器层的分辨率只能达到Tick级别,按照内核配置选项的不同,在100Hz到1000Hz之间。但是,原有的定时器层由于实现教早,应用广泛,如果完全替换掉会引入大量代码改动。因此,Linux内核又独立设计出了一个叫高精度定时器层(High Resolution Timer)的框架,可以为我们提供纳秒级的定时精度,以满足对精确时间有迫切需求的应用程序或内核驱动程序。

高分辨率定时器是建立在每CPU私有独占的本地时钟事件设备上的,对于一个多处理器系统,如果只有全局的时钟事件设备,高分辨率定时器是无法工作的。因为如果没有每CPU私有独占的时钟事件设备,当到期中断发生时系统必须产生夸处理器中断来通知其它CPU完成相应的工作,而过多的夸处理器中断会带来很大的系统开销,这样会令使用高分辨率定时器的代价大大增加,还不如不用。为了让内核支持高分辨率定时器,必须要在编译的时候打开编译选项CONFIG_HIGH_RES_TIMERS。

高分辨率定时器层有两种工作模式:低精度模式与高精度模式。虽然高分辨率定时器子系统是为高精度定时器准备的,但是系统可能在运行过程中动态切换到不同精度和模式的定时事件设备,因此高精度定时器层必须能够在低精度模式与高精度模式下自由切换。

高分辨率定时器层使用红黑树来组织各个高分辨率定时器。随着系统的运行,高分辨率定时器不停地被创建和销毁,新的高分辨率定时器按顺序被插入到红黑树中,树的最左边的节点就是最快到期的定时器。

2 相关数据结构

2.1 hrtimer

高分辨率定时器由hrtimer结构体表示(代码位于include/linux/hrtimer.h中):

struct hrtimer {
	struct timerqueue_node		node;
	ktime_t				_softexpires;
	enum hrtimer_restart		(*function)(struct hrtimer *);
	struct hrtimer_clock_base	*base;
	u8				state;
	u8				is_rel;
	u8				is_soft;
	u8				is_hard;
};

node:是一个timerqueue_node结构体变量。这个结构体中有两个成员,node是红黑树的节点,expires:表示该定时器的硬超时时间:

struct timerqueue_node {
    struct rb_node node;
    ktime_t expires;
};

_softexpires:表示该定时器的软超时时间。高精度定时器一般都有一个到期的时间范围,而不像(低精度)定时器那样就是一个时间点。这个时间范围的前时间点就是软超时时间,而后一个时间点就是硬超时时间。达到软超时时间后,还可以再拖一会再调用超时回调函数,而到达硬超时时间后就不能再拖了。

function:定时器到期后的回调函数。

base:指向包含该高分辨率定时器的的hrtimer_clock_base结构体。

state:用来表示该高分辨率定时器当前所处的状态,目前共有两种状态:

/* 表示定时器还未激活 */
#define HRTIMER_STATE_INACTIVE	0x00
/* 表示定时器已激活(入列) */
#define HRTIMER_STATE_ENQUEUED	0x01

is_rel:表示该定时器的到期时间是否是相对时间。is_soft:表示该定时器是否是“软”定时器。
is_hard:表示该定时器是否是“硬”定时器。

2.2 hrtimer_clock_base

struct hrtimer_clock_base {
	struct hrtimer_cpu_base	*cpu_base;
	unsigned int		index;
	clockid_t		clockid;
	seqcount_t		seq;
	struct hrtimer		*running;
	struct timerqueue_head	active;
	ktime_t			(*get_time)(void);
	ktime_t			offset;
} __hrtimer_clock_base_align;

cpu_base:指向所属CPU的hrtimer_cpu_base结构体。
index:表示该结构体在当前CPU的hrtimer_cpu_base结构体中clock_base数组中所处的下标。
clockid:表示当前时钟类型的ID值。
seq:顺序锁,在处理到期定时器的函数__run_hrtimer中会用到。
running:指向当前正在处理的那个定时器。
active:红黑树,包含了所有使用该时间类型的定时器。
get_time:是一个函数指针,指定了如何获取该时间类型的当前时间的函数。由于不同类型的时间在Linux中都是由时间维护层来统一管理的,因此这些函数都是在时间维护层里面定义好的。
offset:表示当前时间类型和单调时间之间的差值。

2.3 hrtimer_cpu_base

每个CPU单独管理属于自己的高分辨率定时器,为了方便管理,专门定义了一个结构体hrtimer_cpu_base:

struct hrtimer_cpu_base {
	raw_spinlock_t			lock;
	unsigned int			cpu;
	unsigned int			active_bases;
	unsigned int			clock_was_set_seq;
	unsigned int			hres_active		: 1,
					in_hrtirq		: 1,
					hang_detected		: 1,
					softirq_activated       : 1;
#ifdef CONFIG_HIGH_RES_TIMERS
	unsigned int			nr_events;
	unsigned short			nr_retries;
	unsigned short			nr_hangs;
	unsigned int			max_hang_time;
#endif
#ifdef CONFIG_PREEMPT_RT
	......
#endif
	ktime_t				expires_next;
	struct hrtimer			*next_timer;
	ktime_t				softirq_expires_next;
	struct hrtimer			*softirq_next_timer;
	struct hrtimer_clock_base	clock_base[HRTIMER_MAX_CLOCK_BASES];
} ____cacheline_aligned;

lock:用来保护该结构体的自旋锁。

cpu:绑定到的CPU编号。

active_bases:表示clock_base数组中哪些元素下的红黑树中含有定时器。

clock_was_set_seq:表示时钟被设置的序数。

hres_active:表示是否已经处在了高精度模式下。

in_hrtirq:是否正在执行hrtimer_interrupt中断处理程序中。

hang_detected:表明在前一次执行hrtimer_interrupt中断处理程序的时候发生了错误。

softirq_activated:是否正在执行hrtimer_run_softirq软中断处理程序。

nr_events:表明一共执行了多少次hrtimer_interrupt中断处理程序。

nr_retries:表明在执行hrtimer_interrupt中断处理程序的时候对定时事件设备编程错误后重试的次数。

nr_hangs:表明在执行hrtimer_interrupt中断处理程序的时候发生错误的次数。

max_hang_time:表明在碰到错误后,在hrtimer_interrupt中断处理程序中停留的最长时间。

expires_next:该CPU上即将要到期定时器的到期时间。

next_timer:该CPU上即将要到期的定时器。

softirq_expires_next:该CPU上即将要到期的“软”定时器的到期时间。

softirq_next_timer:该CPU上即将要到期的“软”定时器。

clock_base:高分辨率定时器的到期时间可以基于以下几种时间类型,clock_base数组为每种时间基准系统都定义了一个hrtimer_clock_base结构体:

enum  hrtimer_base_type {
	HRTIMER_BASE_MONOTONIC,
	HRTIMER_BASE_REALTIME,
	HRTIMER_BASE_BOOTTIME,
	HRTIMER_BASE_TAI,
	HRTIMER_BASE_MONOTONIC_SOFT,
	HRTIMER_BASE_REALTIME_SOFT,
	HRTIMER_BASE_BOOTTIME_SOFT,
	HRTIMER_BASE_TAI_SOFT,
	HRTIMER_MAX_CLOCK_BASES,
};

没有加 SOFT 后缀的,表示是“硬”定时器,将直接在中断处理程序中处理;而加了SOFT后缀的,表示是“软”定时器,将在软中断(HRTIMER_SOFTIRQ)中处理。而且前面的一半都定义成“硬”定时器类型,后面一半都定义成“软”定时器类型,硬的一半和软的一半申明的次序也是对应的。这样设计就方便根据“硬”“软”特性和时间类型很快查出对应 hrtimer_clock_base 结构体的下标。

 【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份 价值699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

所以,综上所述,高分辨率定时器层的组织相对来说还是比较简单的,甚至比(低分辨率)定时器层还要简单。每个CPU对应有一个 hrtimer_cpu_base 的 Per CPU 结构体变量,其定义如下:

DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) =
{
	.lock = __RAW_SPIN_LOCK_UNLOCKED(hrtimer_bases.lock),
	.clock_base =
	{
		{
			.index = HRTIMER_BASE_MONOTONIC,
			.clockid = CLOCK_MONOTONIC,
			.get_time = &ktime_get,
		},
		{
			.index = HRTIMER_BASE_REALTIME,
			.clockid = CLOCK_REALTIME,
			.get_time = &ktime_get_real,
		},
		{
			.index = HRTIMER_BASE_BOOTTIME,
			.clockid = CLOCK_BOOTTIME,
			.get_time = &ktime_get_boottime,
		},
		{
			.index = HRTIMER_BASE_TAI,
			.clockid = CLOCK_TAI,
			.get_time = &ktime_get_clocktai,
		},
		{
			.index = HRTIMER_BASE_MONOTONIC_SOFT,
			.clockid = CLOCK_MONOTONIC,
			.get_time = &ktime_get,
		},
		{
			.index = HRTIMER_BASE_REALTIME_SOFT,
			.clockid = CLOCK_REALTIME,
			.get_time = &ktime_get_real,
		},
		{
			.index = HRTIMER_BASE_BOOTTIME_SOFT,
			.clockid = CLOCK_BOOTTIME,
			.get_time = &ktime_get_boottime,
		},
		{
			.index = HRTIMER_BASE_TAI_SOFT,
			.clockid = CLOCK_TAI,
			.get_time = &ktime_get_clocktai,
		},
	}
};

每个 hrtimer_cpu_base 结构体中有一个 hrtimer_clock_base 类型的数组变量 clock_base,目前数组元素是8个,分别用来存放8种到期时间类型的高分辨率定时器。而每种到期时间类型下,又是以红黑数来组织所有的高分辨率定时器。因此,高分辨率定时器层的数据结构如下图所示:

3 高精度定时器相关API

3.1 高精度定时器层初始化 hrtimers_init

在linux内核启动初始化阶段,start_kernel 会调用 hrtimers_init 函数对高精度定时器层初始化:

void __init hrtimers_init(void)
{
        /* 初始化属于当前CPU的hrtimer_cpu_base结构体 */
	hrtimers_prepare_cpu(smp_processor_id());
        /* 打开HRTIMER_SOFTIRQ软中断 */
	open_softirq(HRTIMER_SOFTIRQ, hrtimer_run_softirq);
}

该函数主要功能就是初始化属于当前 CPU 的 hrtimer_cpu_base 结构体,然后打开HRTIMER_SOFTIRQ软中断。

int hrtimers_prepare_cpu(unsigned int cpu)
{
        /* 获得属于当前CPU的hrtimer_cpu_base结构体 */
	struct hrtimer_cpu_base *cpu_base = &per_cpu(hrtimer_bases, cpu);
	int i;
 
        /* 初始化所有的hrtimer_clock_base结构体 */
	for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {
                /* 初始化cpu_base */
		cpu_base->clock_base[i].cpu_base = cpu_base;
                /* 初始化红黑树 */
		timerqueue_init_head(&cpu_base->clock_base[i].active);
	}
 
	cpu_base->cpu = cpu;
	cpu_base->active_bases = 0;
	cpu_base->hres_active = 0;
	cpu_base->hang_detected = 0;
	cpu_base->next_timer = NULL;
	cpu_base->softirq_next_timer = NULL;
	cpu_base->expires_next = KTIME_MAX;
	cpu_base->softirq_expires_next = KTIME_MAX;
	hrtimer_cpu_base_init_expiry_lock(cpu_base);
	return 0;
}

3.2 定时器初始化 hrtimer_init

在将一个高分辨率定时器插入并激活之前,首先需要调用hrtimer_init函数对其进行初始化:

void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
		  enum hrtimer_mode mode)
{
	debug_init(timer, clock_id, mode);
	__hrtimer_init(timer, clock_id, mode);
}
EXPORT_SYMBOL_GPL(hrtimer_init);

其直接调用了__hrtimer_init函数:

static void __hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
			   enum hrtimer_mode mode)
{
	bool softtimer = !!(mode & HRTIMER_MODE_SOFT);
	struct hrtimer_cpu_base *cpu_base;
	int base;
 
	if (IS_ENABLED(CONFIG_PREEMPT_RT) && !(mode & HRTIMER_MODE_HARD))
		softtimer = true;
 
        /* 将hrtimer结构体清0 */
	memset(timer, 0, sizeof(struct hrtimer));
 
        /* 获得当前CPU的hrtimer_cpu_base结构体变量 */
	cpu_base = raw_cpu_ptr(&hrtimer_bases);
 
	/* 相对的实时时间调整为单调时间 */
	if (clock_id == CLOCK_REALTIME && mode & HRTIMER_MODE_REL)
		clock_id = CLOCK_MONOTONIC;
 
        /* 根据定时器是不是“软”的找到hrtimer_clock_base的起始下标 */
	base = softtimer ? HRTIMER_MAX_CLOCK_BASES / 2 : 0;
        /* 通过clock_id找hrtimer_clock_base的偏移下标 */
	base += hrtimer_clockid_to_base(clock_id);
	timer->is_soft = softtimer;
	timer->is_hard = !softtimer;
	timer->base = &cpu_base->clock_base[base];
        /* 初始化定时器的红黑树节点结构体 */
	timerqueue_init(&timer->node);
}

所以,可以看出任何一个高分辨率定时器在初始化的时候都是默认被分配到当前处理器上的。

对于 clock_base 下标的计算,由于“硬”定时器模式和“软”定时器模式各占一半,上下对称。所以,如果是“软”的,那起始下标就是最大值的一半,否则就是0。然后,在根据 clock_id 找到对应模式的偏移下标,两者相加就可以了。

static inline int hrtimer_clockid_to_base(clockid_t clock_id)
{
	if (likely(clock_id < MAX_CLOCKS)) {
                /* 将clock_id转换成下标 */
		int base = hrtimer_clock_to_base_table[clock_id];
 
		if (likely(base != HRTIMER_MAX_CLOCK_BASES))
			return base;
	}
	WARN(1, "Invalid clockid %d. Using MONOTONIC\n", clock_id);
        /* 如果出错默认选择单调时间 */
	return HRTIMER_BASE_MONOTONIC;
}

static const int hrtimer_clock_to_base_table[MAX_CLOCKS] = {
	[0 ... MAX_CLOCKS - 1]	= HRTIMER_MAX_CLOCK_BASES,
 
	[CLOCK_REALTIME]	= HRTIMER_BASE_REALTIME,
	[CLOCK_MONOTONIC]	= HRTIMER_BASE_MONOTONIC,
	[CLOCK_BOOTTIME]	= HRTIMER_BASE_BOOTTIME,
	[CLOCK_TAI]		= HRTIMER_BASE_TAI,
};

数组下标是 clock_id,而数组的值是要返回的高分辨率定时器的时间类型。由于实际有意义的只有4项,因此多出来的项全部填上 HRTIMER_MAX_CLOCK_BASES,表示出错了。

高分辨率定时器共有以下几种模式:

enum hrtimer_mode {
	HRTIMER_MODE_ABS	= 0x00,
	HRTIMER_MODE_REL	= 0x01,
	HRTIMER_MODE_PINNED	= 0x02,
	HRTIMER_MODE_SOFT	= 0x04,
	HRTIMER_MODE_HARD	= 0x08,
 
	HRTIMER_MODE_ABS_PINNED = HRTIMER_MODE_ABS | HRTIMER_MODE_PINNED,
	HRTIMER_MODE_REL_PINNED = HRTIMER_MODE_REL | HRTIMER_MODE_PINNED,
 
	HRTIMER_MODE_ABS_SOFT	= HRTIMER_MODE_ABS | HRTIMER_MODE_SOFT,
	HRTIMER_MODE_REL_SOFT	= HRTIMER_MODE_REL | HRTIMER_MODE_SOFT,
 
	HRTIMER_MODE_ABS_PINNED_SOFT = HRTIMER_MODE_ABS_PINNED | HRTIMER_MODE_SOFT,
	HRTIMER_MODE_REL_PINNED_SOFT = HRTIMER_MODE_REL_PINNED | HRTIMER_MODE_SOFT,
 
	HRTIMER_MODE_ABS_HARD	= HRTIMER_MODE_ABS | HRTIMER_MODE_HARD,
	HRTIMER_MODE_REL_HARD	= HRTIMER_MODE_REL | HRTIMER_MODE_HARD,
 
	HRTIMER_MODE_ABS_PINNED_HARD = HRTIMER_MODE_ABS_PINNED | HRTIMER_MODE_HARD,
	HRTIMER_MODE_REL_PINNED_HARD = HRTIMER_MODE_REL_PINNED | HRTIMER_MODE_HARD,
};

共有五种基本模式,其它模式都是由这五种组合而成:

  • HRTIMER_MODE_ABS:表示定时器到期时间是一个绝对值。
  • HRTIMER_MODE_REL:表示定时器到期时间是一个相对于当前时间之后的值。
  • HRTIMER_MODE_PINNED:表示定时器是否需要绑定到某个CPU上。
  • HRTIMER_MODE_SOFT:表示该定时器是否是“软”的,也就是定时器到期回调函数是在软中断下被执行的。
  • HRTIMER_MODE_HARD:表示该定时器是否是“硬”的,也就是定时器到期回调函数是在中断处理程序中被执行的。

3.3 定时器移除 remove_hrtimer

将一个高分辨率定时器从系统中移除是通过 remove_hrtimer 函数实现的:

static inline int
remove_hrtimer(struct hrtimer *timer, struct hrtimer_clock_base *base, bool restart)
{
	u8 state = timer->state;
 
        /* 如果定时器没有激活则直接返回 */
	if (state & HRTIMER_STATE_ENQUEUED) {
		int reprogram;
		
		debug_deactivate(timer);
                /* 只有要删除的定时器激活在当前处理器上时才需要重编程 */
		reprogram = base->cpu_base == this_cpu_ptr(&hrtimer_bases);
 
                /* 如果要删除的定时器不需要重新再激活则将其状态改为HRTIMER_STATE_INACTIVE */
		if (!restart)
			state = HRTIMER_STATE_INACTIVE;
 
		__remove_hrtimer(timer, base, state, reprogram);
		return 1;
	}
	return 0;
}

参数 base 指向的就是那个包含要删除定时器的 hrtimer_clock_base 结构体。参数 restart 表示该定时器删除后是不是还会马上被激活,如果是的话就没必要修改状态为HRTIMER_STATE_INACTIVE(未激活)了。该函数判断一些状态后,主要是调用__remove_hrtimer函数进行删除:

static void __remove_hrtimer(struct hrtimer *timer,
			     struct hrtimer_clock_base *base,
			     u8 newstate, int reprogram)
{
	struct hrtimer_cpu_base *cpu_base = base->cpu_base;
	u8 state = timer->state;
 
	/* 修改当前定时器的状态为参数指定的状态 */
	WRITE_ONCE(timer->state, newstate);
        /* 如果该定时器还没有被添加到任何红黑树中则直接返回 */
	if (!(state & HRTIMER_STATE_ENQUEUED))
		return;
 
        /* 将要删除的定时器从红黑树中移除 */
	if (!timerqueue_del(&base->active, &timer->node))
                /* 如果删除后红黑树为空则清除active_bases中对应的位 */
		cpu_base->active_bases &= ~(1 << base->index);
 
	/* 如果需要重编程并且要删除的定时器是其激活CPU上马上就要到期的定时器则重编程 */
	if (reprogram && timer == cpu_base->next_timer)
		hrtimer_force_reprogram(cpu_base, 1);
}

如果要删除的高分辨率定时器是其激活CPU上马上就要到期的那个定时器,则需要对底层的定时事件设备进行重新编程,让其在下一个定时器的到期时间上到期。关于重编程的部分,后面会介绍。

3.4 定时器激活 hrtimer_start_range_ns

要激活一个高分辨率定时器需要调用 hrtimer_start_range_ns 函数:

void hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
                u64 delta_ns, const enum hrtimer_mode mode)
{
    struct hrtimer_clock_base *base;
    unsigned long flags;
 
    if (!IS_ENABLED(CONFIG_PREEMPT_RT))
        WARN_ON_ONCE(!(mode & HRTIMER_MODE_SOFT) ^ !timer->is_soft);
    else
        WARN_ON_ONCE(!(mode & HRTIMER_MODE_HARD) ^ !timer->is_hard);
 
        /* 获得定时器对应CPU的hrtimer_cpu_base结构体内的自旋锁 */
    base = lock_hrtimer_base(timer, &flags);
 
        /* 激活定时器 */
    if (__hrtimer_start_range_ns(timer, tim, delta_ns, mode, base))
                /* 如果成功则尝试对定时事件设备重编程 */
        hrtimer_reprogram(timer, true);
 
        /* 释放自旋锁 */
    unlock_hrtimer_base(timer, &flags);
}
EXPORT_SYMBOL_GPL(hrtimer_start_range_ns);

参数 tim 保存了定时器的“软”到期时间。参数 delta_ns 是到期时间的范围,所以硬到期时间就是tim+delta_ns。参数 mode 指定了到期时间的类型。在获得了定时器对应 CPU 的hrtimer_cpu_base 结构体内的自旋锁后,其接着调用了 __hrtimer_start_range_ns 函数:

static int __hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
                    u64 delta_ns, const enum hrtimer_mode mode,
                    struct hrtimer_clock_base *base)
{
    struct hrtimer_clock_base *new_base;
 
    /* 先将该定时器从现有红黑树中移除 */
    remove_hrtimer(timer, base, true);
 
        /* 如果是相对时间则获得当前时间并累加得到绝对时间 */
    if (mode & HRTIMER_MODE_REL)
        tim = ktime_add_safe(tim, base->get_time());
 
    tim = hrtimer_update_lowres(timer, tim, mode);
 
        /* 设置定时器的“软”“硬”到期时间 */
    hrtimer_set_expires_range_ns(timer, tim, delta_ns);
 
    /* 尝试迁移该高分辨率定时器 */
    new_base = switch_hrtimer_base(timer, base, mode & HRTIMER_MODE_PINNED);
 
        /* 将定时器插入红黑树 */
    return enqueue_hrtimer(timer, new_base, mode);
}

该函数首先调用 remove_hrtimer 函数,如果要激活的定时器已经被激活过了的话,会将其先删除掉。由于后面还会再激活,所以对应的 restart 参数传的是 true。接着调用了hrtimer_set_expires_range_ns 函数,根据参数设置定时器的“软”“硬”到期时间:

static inline void hrtimer_set_expires_range_ns(struct hrtimer *timer, ktime_t time, u64 delta)
{
	timer->_softexpires = time;
	timer->node.expires = ktime_add_safe(time, ns_to_ktime(delta));
}

所以,“硬”到期时间就是“软”到期时间加上delta。

switch_hrtimer_base 函数会尝试迁移该要激活的高分辨率定时器,这个后面会分析。如果不需要迁移,则返回的 hrtimer_clock_base 结构体和调用参数是一样的,否则会返回一个新的要迁移到的 hrtimer_clock_base 结构体。

最后,函数调用 enqueue_hrtimer 函数,将定时器插入红黑树中,从而完成定时器的激活:

static int enqueue_hrtimer(struct hrtimer *timer,
			   struct hrtimer_clock_base *base,
			   enum hrtimer_mode mode)
{
	debug_activate(timer, mode);
 
        /* 设置active_bases中对应的位 */
	base->cpu_base->active_bases |= 1 << base->index;
 
	/* 更新定时器的状态为HRTIMER_STATE_ENQUEUED */
	WRITE_ONCE(timer->state, HRTIMER_STATE_ENQUEUED);
 
        /* 将该定时器加入对应hrtimer_clock_base结构体内的红黑树中 */
	return timerqueue_add(&base->active, &timer->node);
}

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

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

相关文章

灵界的科学丨一、灵界在哪里?

摘自李嗣涔教授《灵界的科学》 在国内物理学界近十位学者的见证下&#xff0c; 发现我们所处四度时空的物质世界之外&#xff0c; 似乎还有一个世界的存在&#xff0c; 当年我把这个世界称作信息场&#xff0c; 也就是俗称的灵界。 二十世纪末宇宙大尺度谜团的重大发现──…

设计模式学习记录

设计模式 UML图&#xff1a; ------> 依赖 ——>关联 -------▲ 实现 —–—▲ 继承 &#x1f53a;———> 聚合 ▲———> 组合&#xff08;关联性更强&#xff09; 一、策略模式&#xff08;行为型&#xff09; 策略模式&#xff1a;是一种定义一系列算法的方法…

Java --- Spring6项目创建及注意事项

目录 一、Spring框架解决的问题 二、Spring介绍 三、Spring八大模块 四、Spring特点 五、第一个Spring6入门程序 六、spring的细节 6.1、配置文件的bean的id不能重复 6.2、spring底层是通过反射调用无参构造方法创建对象 6.3、spring会把创建好的对象存储在Map集合中 6.4…

【数据结构初阶】树+二叉树+堆的实现

真正的勇士&#xff0c;就是在看清生活的真相后&#xff0c;依旧慷慨面对他所遭受的苦难与挫折。 大学究竟教会了我们什么呢&#xff1f;或许答案只有一个&#xff0c;看清自己&#xff0c;与自己和解&#xff0c;和自己坐下来谈一谈。 人生或许本就没有什么意义&#xff0c;…

MySQL的缓冲池(buffer pool)及 LRU算法

1.什么是缓冲池&#xff08;buffer pool&#xff09; buffer pool 是数据库的一个内存组件&#xff0c;里面缓存了磁盘上的真实数据&#xff0c;Java系统对数据库的增删改操作&#xff0c;主要是这个内存数据结构中的缓存数据执行的。 控制块 存的是 数据页所属的表 空间号&a…

MATLAB break语句

MATLAB中 break 语句用于终止 for 或 while 循环的执行&#xff0c;当在循环体内执行到该语句的时候&#xff0c;程序将会跳出循环&#xff0c;继续执行循环语句的下一语句。 注意&#xff1a;在嵌套循环中&#xff0c;break 退出只能在循环发生&#xff0c;后通过的声明控制循…

git关于创建/删除分支常用命令

主要用来介绍git中如何操作分支的命令&#xff1a; 1.git查看所有的分支: git branch -a 2.创建本地分支&#xff1a; git checkout -b <name> 3.有了本地分支之后推送到远程分支; git push origin <name> 4.切换分支&#xff1a; git checkout <name> 5.删除…

黑*头条_第5章_文章发布粉丝管理成形记

黑*头条_第5章_文章发布&粉丝管理成形记 文章目录黑*头条_第5章_文章发布&粉丝管理成形记文章发布&粉丝管理1 需求分析1.1 功能需求1.2 前端需求1.2.1 图文数据需求1.2.2 素材管理需求1.2.3 发布文章需求1.2.4 内容列表需求1.2.5 粉丝概况需求2 定义2.1 后端定义2.…

MacBook磁盘内存空间清理软件CleanMyMac2023

Mac电脑用的时间久了&#xff0c;Mac用户尤其是MacBook用户会经常收到“磁盘几乎已满”的提示&#xff0c;如何解决这个问题&#xff0c;当我们使用苹果MAC一段时间后&#xff0c;就会有大量的垃圾文件占用磁盘空间&#xff0c;例如系统缓存文件、应用程序缓存文件、备份和重复…

2022/11/18[指针] 关于指针的初步理解

先看一段利用指针交换a和b值得代码&#xff1a; #include<stdio.h> void swap(int* p1, int* p2); int main() {int a, b;int* pa, * pb;printf("enter:");scanf_s("%d%d", &a, &b);pa &a;pb &b;printf("%d,%d\n", pa,…

面试:线程池核心线程如何保持一直运行的

对于Java中 Thread 对象&#xff0c;同一个线程对象调用 start 方法后&#xff0c;会在执行完run 后走向终止&#xff08;TERMINATED&#xff09;状态&#xff0c;也就是说一个线程对象是不可以通过多次调用 start 方法重复执行 run 方法内容的。Java的线程是不允许启动两次的&…

frp内网穿透服务

参考博客&#xff1a; https://www.jianshu.com/p/19ea81efffc4 https://blog.csdn.net/yj222333/article/details/124752420 依赖于&#xff1a;Github开源软件FRP 下载地址&#xff1a;https://github.com/fatedier/frp/releases frp 主要由 客户端(frpc) 和 服务端(frps…

基于JAVA景区售票系统设计与实现 开题报告

本科生毕业论文 基于java框架springboot旅游景区景点购票系统 开题报告 学 院&#xff1a; 专 业&#xff1a; 计算机科学与技术 年 级&#xff1a; 学生姓名&#xff1a; 指导教师…

三维模型文件以及obj、ply格式文件生成pcd点云文件

方法一、三维模型文件生成obj文件 要想生成点云文件&#xff0c;要先将三维模型文件保存为obj文件格式&#xff0c;步骤如下&#xff1a; 通过SolidWorks将模型保存为stl文件格式打开SolidWorks的插件选择&#xff0c;在ScanTo3D前面打勾 在solidworks中以网格文件的形式打开…

通信算法之九十六:电力通信系统-HRF多载波通信系统-物理层收发信道开发

目录1.HRF通信系统-物理层收发信道开发1.1 SIG发射机算法功能模块1.2 SIG接收机算法功能模块1.3 PHR发射机算法功能模块1.4 PHR接收机算法功能模块1.5 PSDU发射接收机处理流程1.6前导LTF/STF序列发射接收处理流程1.7PPDU帧&#xff08;前导SIGPHRPSDU&#xff09;发射接收处理流…

阿里云服务器(Ubuntu)配置nextcloud个人网盘

tags: Ubuntu Server Linux 写在前面 最近迷恋上了云服务器的配置, 感觉云服务器能做的事情太多了, 不管是docker还是直接部署, 都是相当方便快捷的, 下面来看看在阿里云服务器配置nextcloud网盘的基本配置方法, 这里参考了一篇文章1, 写的相当详细了, 我这里只是做一些补充.…

【5G MAC】Msg1 TX开环功控介绍

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

使用python编写mqtt客户端向EMQX服务器发送数据

摘要&#xff1a;本文介绍如何用python编写一个mqtt客户端向EMQX服务器发送数据&#xff0c;实现一个简易的本地物联网服务器。 上一篇文章讲到使用mqtt.fx软件来发布消息。 (1条消息) 使用mqtt.fx向EMQX服务器发送消息_TMS320VC5257H的博客-CSDN博客https://blog.csdn.net/yo…

【树莓派不吃灰】命令篇⑦ 学习awk命令

目录awk 命令1. 基本语法2. 工作原理3. 基础实例操作3.1 把用户名和Shell打印出来3.2 找到以ssh关键字开头的所有行&#xff0c;并输出用户名和shell&#xff0c;中间以“-”分割3.3 给用户 ID加上一个常量3.4 输出第一个字段为sshd所在的行4. BEGIN END 操作4.1 基于3.2&#…

智慧仓储解决方案-最新全套文件

智慧仓储解决方案-最新全套文件一、建设背景二、思路架构三、建设方案四、获取 - 智慧仓储全套最新解决方案合集一、建设背景 仓储行业目前存在的问题 仓管员需要手动录单&#xff0c;工作量大&#xff0c;易出错&#xff0c;无法保证数据的准确率和及时性。 批次属性复杂、保…