经典RCU锁原理及Linux内核实现

news2024/12/24 11:28:46

经典RCU锁原理及Linux内核实现

RCU锁原理

  • RCU锁第一个特点就是适用于读很多写很少的场景,那它和读写锁有什么区别呢?区别就是RCU锁读者完全不用加锁(多个写者之间仍需要竞争锁),而读写锁(不管是读优先、写优先或者读写公平)读者和写者之间是需要竞争锁的。由于RCU锁读者完全不用竞争锁,这也带来了其第二个特点,RCU锁需要读者能够忍受旧数据(即在写者开始修改数据到写着完成修改这段时间,读者读取到的还是旧数据)。

  • 在这里插入图片描述

  • 为详细阐述RCU锁原理,如上图所示,有多个读者和一个写者先后参与进来。在t0时刻写者开始进来,在此之前已经有读者1和读者2读取数据M,且读者1已经完成数据M的访问。在t1时刻写者完成数据的修改,C对象不再指向数据M而指向新数据N。

  • RCU锁操作的是指针地址。写者修改数据时,首先将Read原数据Copy一份,然后进行Update,修改完成后将指针地址修改为新数据内存地址(而这一步是通过内存屏障保证修改前后数据一致性)。

  • 在t0至t1期间,由于写者还未完成数据的修改,所以这段期间进入的读者(读者3和再次进入的读者1)将会读到旧数据M。这就是为什么RCU锁读者需要忍受旧数据。t1时刻以后再进入的读者(读者4)将会读取到新数据N。虽然t1时刻已经完成数据的修改,但是旧数据N此时还不能删除,因为需要等待所有访问旧数据N的读者结束访问。在t2时刻,最后一个读者(读者2)完成旧数据N的访问,此时就可以删除旧数据了。如果后续再有新的读者和写者进来,继续上述的过程即可。

  • RCU锁的基本原理就是这样,是不是感觉不过如此,但是实现起来就不是这个feel了,咱们接着往下看。

RCU锁Linux内核实现(基于2.6.11.1版本)

  • RCU锁的其中一个关键点在于如何知道最后一个读者完成了对旧数据访问的这一时机。下面就来看看为了做到这一点,内核RCU锁实现都干了啥:

  • 读者只能在局部范围内访问数据

  • OUT_TYPE Func(IN_TYPE* p)
    {
        // ...
        rcu_read_lock();
    
        IN_TYPE* pLocal = rcu_dereference(p);
        // pLocal->...
    
        rcu_read_unlock();
        // ...
    }
    
  • 如上述代码所示,对数据的访问(pLocal->…)只能在rcu_read_lock()和rcu_read_unlock()之间(至于这两个函数做了啥,咱们后续分析)。另外还有一点就是这里为什么不直接通过指针p访问,而是通过rcu_dereference§将指针地址读取到局部变量pLocal来访问。先看看内核里这个函数做了啥.

  • /**
     * rcu_dereference - fetch an RCU-protected pointer in an
     * RCU read-side critical section.  This pointer may later
     * be safely dereferenced.
     *
     * Inserts memory barriers on architectures that require them
     * (currently only the Alpha), and, more importantly, documents
     * exactly which pointers are protected by RCU.
     */
    
    #define rcu_dereference(p)     ({ \
    				typeof(p) _________p1 = p; \
    				smp_read_barrier_depends(); \
    				(_________p1); \
    				})
    
  • 简而言之,就是除了Alpha架构处理器,直接访问指针p没有问题。但是在Alpha架构处理器下,编译器优化可能会进行指令重排,导致直接访问指针p的指令可能被重排到rcu_read_lock()之前。因此rcu_dereference宏里面增加了一个优化屏障(smp_read_barrier_depends()函数),从而使得后续对局部变量pLocal的访问不会被重排。

  • 读者数据访问结束的判断

  • 为了做到这一点,RCU锁划分出来2种以分别用于内核进程和软中断。这样做的原因在于判断读者数据访问结束的时机是通过进程调度(进程切换和中断)来判断的。先来看看用于内核进程版本的RCU锁实现,软中断版本只在几个点稍有区别。

  • 先来看看读者相关的两个接口

  • #define rcu_read_lock()		preempt_disable()
    #define rcu_read_unlock()	preempt_enable()
    
  • 顾名思义,在访问RCU锁保护数据前禁止抢占,结束访问后重新启用抢占。我们深入看下preempt_disable()和preempt_enable()这两个宏的实现。

  • #ifdef CONFIG_PREEMPT
    
    asmlinkage void preempt_schedule(void);
    
    #define preempt_disable() \
    do { \
    	inc_preempt_count(); \
    	barrier(); \
    } while (0)
    
    #define preempt_enable_no_resched() \
    do { \
    	barrier(); \
    	dec_preempt_count(); \
    } while (0)
    
    #define preempt_check_resched() \
    do { \
    	if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \
    		preempt_schedule(); \
    } while (0)
    
    #define preempt_enable() \
    do { \
    	preempt_enable_no_resched(); \
    	preempt_check_resched(); \
    } while (0)
    
    #else
    
    #define preempt_disable()		do { } while (0)
    #define preempt_enable_no_resched()	do { } while (0)
    #define preempt_enable()		do { } while (0)
    #define preempt_check_resched()		do { } while (0)
    
    #endif
    
  • 如果操作系统不支持**内核抢占**,则这两个宏啥都不需要做,因为该情况下如果执行到RCU锁保护区间的读者代码,则一定不会被其他进程抢占(中断除外),从而保证了如果发生了进程调度,则RCU锁保护区间的读者代码一定执行完成了。相反,如果操作系统支持内核抢占(定义宏CONFIG_PREEMPT),则在访问读者代码前关闭内核抢占,在访问读者代码结束后启用内核抢占。

  • 禁止内核抢占就是将当前线程信息的preempt_count加1,如果preempt_count>0表示不可被抢占。启用内核抢占就是将当前线程信息的preempt_count减1,如果preempt_count=0表示可被抢占。另外在preempt_enable()时,会看当前线程信息的flags是否置位TIF_NEED_RESCHED,如果置位的话会主动触发进程调度从而被抢占(当然此时RCU锁读者代码已执行完毕)。

  • 读者相关的接口比较简单,下面来看看写者相关的接口

  • /**
     * rcu_assign_pointer - assign (publicize) a pointer to a newly
     * initialized structure that will be dereferenced by RCU read-side
     * critical sections.  Returns the value assigned.
     *
     * Inserts memory barriers on architectures that require them
     * (pretty much all of them other than x86), and also prevents
     * the compiler from reordering the code that initializes the
     * structure after the pointer assignment.  More importantly, this
     * call documents which pointers will be dereferenced by RCU read-side
     * code.
     */
    #define rcu_assign_pointer(p, v)	({ \
    						smp_wmb(); \
    						(p) = (v); \
    					})
    
  • rcu_assign_pointer(p, v)宏用于更新指针值(参考RCU锁原理Updater分析)。一方面,加入了写内存屏障,保证多次写操作的顺序正确,另一方面,调用者要加锁保证同时只有一个写者去更新数据。用法例如(摘自内核net/core/netfilter.c):

  • int nf_log_register(int pf, nf_logfn *logfn)
    {
    	int ret = -EBUSY;
    
    	/* Any setup of logging members must be done before
    	 * substituting pointer. */
    	spin_lock(&nf_log_lock);
    	if (!nf_logging[pf]) {
    		rcu_assign_pointer(nf_logging[pf], logfn);
    		ret = 0;
    	}
    	spin_unlock(&nf_log_lock);
    	return ret;
    }
    
  • 最后,就是最不好拿捏的几个接口了,先列出来

  • extern void FASTCALL(call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *head)));
    extern void synchronize_kernel(void);
    
  • synchronize_kernel函数就是调用call_rcu函数,只不过增加了一个同步对象,从而成为一个同步接口。所以我们就只需要分析下call_rcu函数是干嘛的

  • /**
     * call_rcu - Queue an RCU callback for invocation after a grace period.
     * @head: structure to be used for queueing the RCU updates.
     * @func: actual update function to be invoked after the grace period
     *
     * The update function will be invoked some time after a full grace
     * period elapses, in other words after all currently executing RCU
     * read-side critical sections have completed.  RCU read-side critical
     * sections are delimited by rcu_read_lock() and rcu_read_unlock(),
     * and may be nested.
     */
    void fastcall call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu))
    {
    	unsigned long flags;
    	struct rcu_data *rdp;
    
    	head->func = func;
    	head->next = NULL;
    	local_irq_save(flags);
    	rdp = &__get_cpu_var(rcu_data);
    	*rdp->nxttail = head;
    	rdp->nxttail = &head->next;
    	local_irq_restore(flags);
    }
    
  • 代码中rdp的数据结构类型为struct rcu_data

  • /*
     * Per-CPU data for Read-Copy UPdate.
     * nxtlist - new callbacks are added here
     * curlist - current batch for which quiescent cycle started if any
     */
    struct rcu_data {
    	/* 1) quiescent state handling : */
    	long		quiescbatch;     /* Batch # for grace period */
    	int		passed_quiesc;	 /* User-mode/idle loop etc. */
    	int		qs_pending;	 /* core waits for quiesc state */
    
    	/* 2) batch handling */
    	long  	       	batch;           /* Batch # for current RCU batch */
    	struct rcu_head *nxtlist;
    	struct rcu_head **nxttail;
    	struct rcu_head *curlist;
    	struct rcu_head **curtail;
    	struct rcu_head *donelist;
    	struct rcu_head **donetail;
    	int cpu;
    };
    
    DECLARE_PER_CPU(struct rcu_data, rcu_data);
    
  • 它是per-cpu的,所以call_rcu函数修改它前,先保存中断状态并关闭当前cpu的中断,修改完成后再恢复原来的中断状态,这样就保证了修改的串行化。call_rcu的主要逻辑就是把一个新的rcu_head对象添加到rdp的nxttail链表的尾部。这个rcu_head对象持有了一个函数指针,以便在所有读者完成访问后,调用这个函数完成相关资源的清理工作。下面咱们就从初始状态构造一个案例来分析内核是如何判断所有读者完成访问并清理资源的。

  • 在这里插入图片描述

  • 好了,万事俱备,可以进行案例分析了。如上图所示,操作系统启动将rcu_ctrlblk、rcu_state以及各个cpu的rcu_data进行初始化(对应于t0时刻)。接下来各个cpu可以读取RCU锁保护的相关资源,t1时刻cpu2调用call_rcu函数使得cpu2的rcu_data的nxt_list不为空(图中假定指向A)。后面cpu2再次触发时钟中断,调用函数rcu_pending函数判断是否需要RCU相关处理,相关函数实现如下:

  • // file: timer.c
    // 时钟中断判断是否需要进行RCU相关处理
    void update_process_times(int user_tick)
    {
    	// ...
    	if (rcu_pending(cpu))
    		rcu_check_callbacks(cpu, user_tick);
    	scheduler_tick();
    }
    
    // file: rcupdate.h
    // 判断是否需要进行RCU处理的逻辑判断
    static inline int __rcu_pending(struct rcu_ctrlblk *rcp,
    						struct rcu_data *rdp)
    {
    	/* This cpu has pending rcu entries and the grace period
    	 * for them has completed.
    	 */
    	if (rdp->curlist && !rcu_batch_before(rcp->completed, rdp->batch))
    		return 1;
    
    	/* This cpu has no pending entries, but there are new entries */
    	if (!rdp->curlist && rdp->nxtlist)
    		return 1;
    
    	/* This cpu has finished callbacks to invoke */
    	if (rdp->donelist)
    		return 1;
    
    	/* The rcu core waits for a quiescent state from the cpu */
    	if (rdp->quiescbatch != rcp->cur || rdp->qs_pending)
    		return 1;
    
    	/* nothing to do */
    	return 0;
    }
    
    static inline int rcu_pending(int cpu)
    {
    	return __rcu_pending(&rcu_ctrlblk, &per_cpu(rcu_data, cpu)) ||
    		__rcu_pending(&rcu_bh_ctrlblk, &per_cpu(rcu_bh_data, cpu));
    }
    
  • 可以看到由于cpu2的rcu_data的curlist为空,而nxtlist不为空,所以返回1表示需要进行RCU处理,于是调用rcu_check_callbacks函数,其函数实现如下图所示。

  • void rcu_check_callbacks(int cpu, int user)
    {
    	if (user || 
    	    (idle_cpu(cpu) && !in_softirq() && 
    				hardirq_count() <= (1 << HARDIRQ_SHIFT))) {
    		rcu_qsctr_inc(cpu);
    		rcu_bh_qsctr_inc(cpu);
    	} else if (!in_softirq())
    		rcu_bh_qsctr_inc(cpu);
    	tasklet_schedule(&per_cpu(rcu_tasklet, cpu));
    }
    
  • 前面两个判断语句的意思是,如果当前cpu处于用户态或者idle模式且非中断处理过程,那么当前cpu肯定结束了RCU保护资源的访问(内核进程版本RCU和软中断版本RCU都是)。否则如果不是软中断处理过程,则软中断版本的RCU也可以认为结束了资源访问。通过判断的处理就是将rcu_data(软中断版本RCU是rcu_bh_data)的passed_quiesc字段赋值为1。

  • 不过案例当前处理不用关注这两个判断,因为还未开启一个批次,后续开启一个批次时会将passed_quiesc重置为0。最后调用tasklet_schedule会调度执行rcu_process_callbacks函数,其函数实现如下:

  • static void __rcu_process_callbacks(struct rcu_ctrlblk *rcp,
    			struct rcu_state *rsp, struct rcu_data *rdp)
    {
    	if (rdp->curlist && !rcu_batch_before(rcp->completed, rdp->batch)) {
    		*rdp->donetail = rdp->curlist;
    		rdp->donetail = rdp->curtail;
    		rdp->curlist = NULL;
    		rdp->curtail = &rdp->curlist;
    	}
    
    	local_irq_disable();
    	if (rdp->nxtlist && !rdp->curlist) {
    		rdp->curlist = rdp->nxtlist;
    		rdp->curtail = rdp->nxttail;
    		rdp->nxtlist = NULL;
    		rdp->nxttail = &rdp->nxtlist;
    		local_irq_enable();
    
    		/*
    		 * start the next batch of callbacks
    		 */
    
    		/* determine batch number */
    		rdp->batch = rcp->cur + 1;
    		/* see the comment and corresponding wmb() in
    		 * the rcu_start_batch()
    		 */
    		smp_rmb();
    
    		if (!rcp->next_pending) {
    			/* and start it/schedule start if it's a new batch */
    			spin_lock(&rsp->lock);
    			rcu_start_batch(rcp, rsp, 1);
    			spin_unlock(&rsp->lock);
    		}
    	} else {
    		local_irq_enable();
    	}
    	rcu_check_quiescent_state(rcp, rsp, rdp);
    	if (rdp->donelist)
    		rcu_do_batch(rdp);
    }
    
    static void rcu_process_callbacks(unsigned long unused)
    {
    	__rcu_process_callbacks(&rcu_ctrlblk, &rcu_state,
    				&__get_cpu_var(rcu_data));
    	__rcu_process_callbacks(&rcu_bh_ctrlblk, &rcu_bh_state,
    				&__get_cpu_var(rcu_bh_data));
    }
    
  • 假设t2时刻cpu2执行rcu_process_callbacks函数,此函数会将cpu2的rcu_data的nxtlist转移到curlist,并且batch字段增1至-299。接着该函数调用rcu_start_batch函数修改全局rcu_ctrlblk的cur字段增1至-299,rcu_state的cpumask字段各个cpu对应位置1。最后,

  • static void rcu_start_batch(struct rcu_ctrlblk *rcp, struct rcu_state *rsp,
    				int next_pending)
    {
    	if (next_pending)
    		rcp->next_pending = 1;
    
    	if (rcp->next_pending &&
    			rcp->completed == rcp->cur) {
    		/* Can't change, since spin lock held. */
    		cpus_andnot(rsp->cpumask, cpu_online_map, nohz_cpu_mask);
    
    		rcp->next_pending = 0;
    		/* next_pending == 0 must be visible in __rcu_process_callbacks()
    		 * before it can see new value of cur.
    		 */
    		smp_wmb();
    		rcp->cur++;
    	}
    }
    
  • 调用rcu_check_quiescent_state函数,将cpu2的rcu_data的quiescbatch字段也设置未-299,passed_quiesc重置为0,qs_pending设置为1。这样一个就开启了一个新批次的处理。

  • static void rcu_check_quiescent_state(struct rcu_ctrlblk *rcp,
    			struct rcu_state *rsp, struct rcu_data *rdp)
    {
    	if (rdp->quiescbatch != rcp->cur) {
    		/* start new grace period: */
    		rdp->qs_pending = 1;
    		rdp->passed_quiesc = 0;
    		rdp->quiescbatch = rcp->cur;
    		return;
    	}
    
    	/* Grace period already completed for this cpu?
    	 * qs_pending is checked instead of the actual bitmap to avoid
    	 * cacheline trashing.
    	 */
    	if (!rdp->qs_pending)
    		return;
    
    	/* 
    	 * Was there a quiescent state since the beginning of the grace
    	 * period? If no, then exit and wait for the next call.
    	 */
    	if (!rdp->passed_quiesc)
    		return;
    	rdp->qs_pending = 0;
    
    	spin_lock(&rsp->lock);
    	/*
    	 * rdp->quiescbatch/rcp->cur and the cpu bitmap can come out of sync
    	 * during cpu startup. Ignore the quiescent state.
    	 */
    	if (likely(rdp->quiescbatch == rcp->cur))
    		cpu_quiet(rdp->cpu, rcp, rsp);
    
    	spin_unlock(&rsp->lock);
    }
    
  • 接着cpu0和cpu1再次触发时钟中断,由于它两rcu_data的quiescbatch还是-300,而全局正在处理的批次(rcu_ctrlblk的cur字段)为-299,所以rcu_pending函数返回1,表示需要进行RCU相关处理。与上面的调用栈一样,最后会在rcu_check_quiescent_state函数,将两者的rcu_data的相关字段进行设置(qs_pending设置为1,passed_quiesc设置为0,quiescbatch设置为-299),假设分别于t3和t4时刻,cpu0和cpu1完成该过程。

  • t5时刻cpu2再次触发时钟中断,由于其rcu_data的qs_pending字段为1,所以rcu_pending函数返回1继续调用函数rcu_check_callbacks。假设处于用户态,则cpu2完成RCU资源访问(也叫做通过quiescent state,所以quiescent state意思就是结束RCU资源访问的这段时间),passed_quiesc字段设置为1。随后调用至rcu_check_quiescent_state函数,qs_pending设置为0,cpumask对应cpu位置0,表明cpu2已经渡过quiescent state。

  • 同样的流程,t6时刻cpu0因再次触发时钟中断,由于其rcu_data的qs_pending字段为1,所以rcu_pending函数返回1继续调用函数rcu_check_callbacks。假设处于用户态,则cpu0完成RCU资源访问,passed_quiesc字段设置为1。随后调用至rcu_check_quiescent_state函数,qs_pending设置为0,cpumask对应cpu位置0,表明cpu0已经渡过quiescent state。

  • t7时刻,cpu1也因再次时钟中断,经过与cpu0同样的过程。但是由于cpu1是最后结束quiescent state,cpumask被全部置0,此时rcu_ctrlblk的completed字段赋值为-299(赋值为其cur字段,意味着当前批次已完成)。由于cpu1自身rcu_data并没有需要处理的callback,所以后续就返回了。

  • 但是,对于cpu2而言,当再次时钟中断,调用rcu_pending函数时,由于其rcu_data的curlist不为空,且rcu_ctrlblk的completed字段不小于其rcu_data的batch字段,所以还是返回1从而继续处理callback。如图假设此时为t8时刻,则在rcu_process_callbacks函数中会把rcu_data的nxtlist移动至donelist。最后由于donelist不为空,从而调用rcu_do_batch函数处理donelist的callback,假设为t9时刻处理完donelist,则donelist再次为空。

  • 最后,软中断RCU实现只有些微的差别,首先是其读者RCU锁接口不同,内部实现会关闭软中断(当然也就无法抢占)。

  • #define rcu_read_lock_bh()	local_bh_disable()
    #define rcu_read_unlock_bh()	local_bh_enable()
    
  • 再就是前面以及提到的rcu_check_callbacks校验结束quiescent state的判断不同。

总结

  • 本文通过示意图讲解了RCU的基本原理,它与读写锁的不同之处。然后结合2.6.11.1版本linux内核,通过构造一个案例,讲解RCU是如何实现的。其中包含全局的数据结构rcu_ctrlblk和rcu_state,以及各个cpu独有的数据结构rcu_data,案例从数据结构初始化,再到各个CPU的事件处理导致数据结构的变化,最后结束该批次的quiescent state,从而调用callback回收资源。当然还有很多其他复杂的情形,这里并没有涉及,但是经过这样一个简单的案例分析,相信再进行更复杂的案例分析、算法剖析乃至新版本RCU实现的演化更新,咱们也能更加有迹可循。

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

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

相关文章

https://www.aitoolpath.com/ 一个工具数据库,目前储存了有2000+各种工具。每日更新

AI 工具爆炸&#xff1f;别怕&#xff0c;这个网站帮你整理好了&#xff01; 哇塞&#xff0c;兄弟们&#xff01;AI 时代真的来了&#xff01;现在各种 AI 工具跟雨后春笋似的&#xff0c;噌噌噌地往外冒。AI 写作、AI 绘画、AI 代码生成……简直是要逆天啊&#xff01; 可是…

XSS | XSS 常用语句以及绕过思路

关注这个漏洞的其他相关笔记&#xff1a;XSS 漏洞 - 学习手册-CSDN博客 0x01&#xff1a;干货 - XSS 测试常用标签语句 0x0101&#xff1a;<a> 标签 <!-- 点击链接触发 - JavaScript 伪协议 --><a hrefjavascript:console.log(1)>XSS1</a> <!-- 字…

智能制造--EAP设备自动化程序

EAP是设备自动化程序&#xff08;Equipment Automation Program&#xff09;的缩写&#xff0c;他是一种用于控制制造设备进行自动化生产的系统。EAP系统与MES系统整合&#xff0c;校验产品信息&#xff0c;自动做账&#xff0c;同时收集产品生产过程中的制程数据和设备参数数据…

Spring MVC__入门

目录 一、SpringMVC简介1、什么是MVC2、什么是SpringMVC 二、Spring MVC实现原理2.1核心组件2.2工作流程 三、helloworld1、开发环境2、创建maven工程3、配置web.xml4、创建请求控制器5、创建springMVC的配置文件6、测试HelloWorld7、总结 一、SpringMVC简介 1、什么是MVC MV…

git 报错git: ‘remote-https‘ is not a git command. See ‘git --help‘.

报错内容 原因与解决方案 第一种情况&#xff1a;git路径错误 第一种很好解决&#xff0c;在环境变量中配置正确的git路径即可&#xff1b; 第二种情况 git缺少依赖 这个情况&#xff0c;网上提供了多种解决方案。但如果比较懒&#xff0c;可以直接把仓库地址的https改成ht…

【Kotlin基于selenium实现自动化测试】初识selenium以及搭建项目基本骨架(1)

导读大纲 1.1 Java: Selenium 首选语言1.2 配置一个强大的开发环境 1.1 Java: Selenium 首选语言 Java 是开发人员和测试人员进行自动化 Web 测试的首选 Java 和 Selenium 之间的协同作用受到各种因素的驱动,从而提高它们的有效性 为什么Java经常被认为是Selenium的首选语言 广…

记录一次出现循环依赖问题

具体的结构设计&#xff1a; 在上面的图片中&#xff1a; UnboundBlackVerifyChain类中继承了UnboundChain类。但是UnboundChain类中注入了下面三个类。 Scope(“prototype”) UnboundLinkFlowCheck类 Scope(“prototype”) UnboundUserNameCheck类 Scope(“prototype”) Un…

[linux 驱动]input输入子系统详解与实战

目录 1 描述 2 结构体 2.1 input_class 2.2 input_dev 2.4 input_event 2.4 input_dev_type 3 input接口 3.1 input_allocate_device 3.2 input_free_device 3.3 input_register_device 3.4 input_unregister_device 3.5 input_event 3.6 input_sync 3.7 input_se…

用网络分析仪测试功分器驻波的5个步骤

在射频系统中&#xff0c;功分器的驻波比直接关系到信号的稳定性和传输效率。本文将带您深入了解驻波比的测试方法和影响其结果的因素。 一、功分器驻波比 驻波(Voltage Standing Wave Ratio)&#xff0c;简称SWR或VSWR&#xff0c;是指频率相同、传输方向相反的两种波&#xf…

.NET Core 高性能并发编程

一、高性能大并发架构设计 .NET Core 是一个高性能、可扩展的开发框架&#xff0c;可以用于构建各种类型的应用程序&#xff0c;包括高性能大并发应用程序。为了设计和开发高性能大并发 .NET Core 应用程序&#xff0c;需要考虑以下几个方面&#xff1a; 1. 异步编程 异步编程…

最大正方形 Python题解

最大正方形 题目描述 在一个 n m n\times m nm 的只包含 0 0 0 和 1 1 1 的矩阵里找出一个不包含 0 0 0 的最大正方形&#xff0c;输出边长。 输入格式 输入文件第一行为两个整数 n , m ( 1 ≤ n , m ≤ 100 ) n,m(1\leq n,m\leq 100) n,m(1≤n,m≤100)&#xff0c;接…

养猪场饲料加工机械设备有哪些

养猪场饲料加工机械设备主要包括以下几类&#xff1a;1‌、粉碎机‌&#xff1a;主要用于将原料进行粉碎&#xff0c;以便与其他饲料原料混合均匀。常见的粉碎机有水滴式粉碎机和立式粉碎机两种&#xff0c;用户可以根据原料的特性选择适合的机型。2‌、搅拌机‌&#xff1a;用…

ONVIF、GB28181技术特点和使用场景分析

技术背景 好多开发者希望搞明白ONVIF和GB28181的区别和各自适合的场景&#xff0c;为什么大牛直播SDK只做了GB28181接入端&#xff0c;没有做ONVIF&#xff1f;本文就二者差别&#xff0c;做个大概的介绍。 ONVIF ONVIF&#xff08;Open Network Video Interface Forum&…

【Linux 23】线程池

文章目录 &#x1f308; 一、线程池的概念&#x1f308; 二、线程池的应用场景&#x1f308; 三、线程池的实现 &#x1f308; 一、线程池的概念 线程池 (thread pool) 是一种利用池化技术的线程使用模式。 虽然创建线程的代价比创建进程的要小很多&#xff0c;但小并不意味着…

Mysql高级篇(下)——日志

日志 一、日志概述二、日志弊端二、日志分类三、 各日志详情介绍1、慢查询日志&#xff08;Slow Query Log&#xff09;2、通用查询日志&#xff08;General Query Log&#xff09;3、错误日志&#xff08;Error Log&#xff09;4、二进制日志&#xff08;Binary Log&#xff0…

初识Linux · 进程等待

目录 前言&#xff1a; 进程等待是什么 为什么需要进程等待 进程等待都在做什么 前言&#xff1a; 通过上文的学习&#xff0c;我们了解了进程终止&#xff0c;知道终止是在干什么&#xff0c;终止的三种情况&#xff0c;以及有了退出码&#xff0c;错误码的概念&#xff…

基于大数据的学生体质健康信息系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

图像数据增强albumentations之自然景色

一 背景 最近在做关于图像数据增强方面&#xff0c;发现albumentations这个包比较好用&#xff0c;在此学习一下如何使用API二 albumentations 安装 注意&#xff0c;注意&#xff0c;注意 python版本3.8 pip install -U albumentations三 API学习 1 模拟雨水 import os i…

慢病中医药膳养生食疗管理微信小程序、基于微信小程序的慢病中医药膳养生食疗管理系统设计与实现、中医药膳养生食疗管理微信小程序的开发与应用(源码+文档+定制)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

【SpringCloud】注册中⼼的其他实现-Nacos

Nacos 1. Nacos简介 2. Nacos安装2.1 下载安装包2.2 Windows2.2.1 解压2.2.2 修改单机模式2.2.3 启动Nacos2.2.4 常⻅问题集群模式启动端⼝号冲突 2.3 Linux2.3.1 准备安装包2.3.2 单机模式启动 1. Nacos简介 2018年6⽉, Eureka 2.0宣布闭源(但是1.X版本仍然为活跃项⽬), 同年…