Linux kprobe调试技术是内核开发者专门为了编译跟踪内核函数执行状态所涉及的一种轻量级内核调试技术,利用kprobe技术,内核开发人员可以在内核的绝大多数指定函数中动态插入探测点来收集所需的调试状态信息而基本不影响内核原有的执行流程。本章的是基于5.15内核来学习kprobe的相关内容,主要包括以下内容 kprobe技术产生的背景 主要针对ARM64 kprobes的技术实现原理,实现方式 对于ftrace中的kprobe是如何实现的 kpobe可以做什么,可以解决哪些问题 1 kprobe技术背景 对于开发者,我们在内核或者模块的调试过程中,往往需要知道一些函数的执行流程,何时被调用,执行过程中的入参和返回值是什么等等,比较简单的做法就是在内核代码对应的位置添加日志信息,但是这种方式往往需要重新编译内核或者模块,烧写或者替换模块,操作较为复杂甚至可能会破坏原有的代码执行过程。 所以针对这种情况,内核提供了一种调试机制kprobe,提供了一种方法,能够在不修改现有代码的基础上,灵活的跟踪内核函数的执行。 它的基本工作原理是:用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。 kprobe 是一种动态调试机制,用于debugging,动态跟踪,性能分析,动态修改内核行为等,2004年由IBM发布,是名为Dprobes工具集的底层实现机制,2005年合入Linux kernel。probe的含义是像一个探针,可以不修改分析对象源码的情况下,获取Kernel的运行时信息。kprobe一直在X86系统上使用,ARM64的平台支持在2015年合入kernel ,kprobe提供了三种形式的探测点 一种最基本的kprobe:能够在指定代码执行前,执行后进行探测,但此时不能访问被探测函数内的相关变量信息,内核代码的任何指令处 一种是jprobe:用于探测某一个函数的入口,并且能够访问对应的函数参数,这个目前已经不再使用 一种是kretprobe:用于完成指定函数返回值的探测功能,内核函数的退出点 其中最基本的就是kprobe机制,jprobe以及kretprobe的实现都依赖于kprobe,kprobe是linux内核的一个重要的特性,是其他内核调试工具(perf,systemtap)的基础设施,同时内核BPF也是依赖于kprobe,它是利用指令插桩原理,截获指令流,并在指令执行前后插入hook函数,其如下: 所以kprobe的实现原理是把制定地址(探测点)的指令替换成一个可以让cpu进入debug模式的指令,使执行路径暂停,跳转到probe处理函数后收集,修改信息,然后再跳转回来继续执行的过程。 如果需要知道内核函数是否被调用、被调用上下文、入参以及返回值,比较简单的方式是加printk,但是效率低,利用kprobe技术,用户可以自定义自己的回调函数,可以再几乎所有的函数中动态插入探测点。 首先kprobe是最基本的探测方式,是实现后两种的基础,它可以再任意的位置放置探测点(就连函数内部的某条指令处也可以),提供了探测点的调用前,调用后和内存访问出错3种回调方式,分别是- - per_handler:将在被探测指令执行前回调 post_handler:将在被探测指令执行完毕后回调(注意不是被探测函数) 对于kretprobe从名字就可以看出,它同样是基于kprobe实现,用于获取被探测函数的返回值 2 ARM64 kprobe的工作原理 实现kprobes 接口的数据结构和函数已在文件中定义。下面的数据结构描述了一个 kprobe struct kprobe { struct hlist_node hlist; /* 所有注册的kprobe都会添加到kprobe_table哈希表中,hlist成员用来链接到某个槽位中 */ /* list of kprobes for multi-handler support */ struct list_head list; /* 链接一个地址上注册的多个kprobe */ /*count the number of times this probe was temporarily disarmed */ unsigned long nmissed; /* 记录当前的probe没有被处理的次数 */ /* 一个是用户在注册前指定探测点的基地址(加上偏移得到真实的地址), * 另一个是在注册后保存探测点的实际地址, 如果没有指定,必须指定探测的位置的符号信息 */ /* location of the probe point */ kprobe_opcode_t *addr; /* 探测点地址 */ /* 名称和地址不能同时指定,否则注册时会返回EINVAL错误 */ /* Allow user to indicate symbol name of the probe point */ const char *symbol_name; /* 探测点函数名 */ /* Offset into the symbol */ unsigned int offset; /* 探测点在函数内的偏移 */ /* 断点异常触发之后,开始单步执行原始的指令之前被调用 */ /* Called before addr is executed. */ kprobe_pre_handler_t pre_handler; /* 在单步执行原始的指令后会被调用 */ /* Called after addr is executed, unless... */ kprobe_post_handler_t post_handler; /* 后处理函数 */ /* 原始指令,在被替换为断点指令(X86下是int 3指令)前保存。*/ /* Saved opcode (which has been replaced with breakpoint) */ kprobe_opcode_t opcode; /* copy of the original instruction */ struct arch_specific_insn ainsn; /* 保存平台相关的被探测指令和下一条指令 */ /* * Indicates various status flags. * Protected by kprobe_mutex after this kprobe is registered. */ u32 flags; /* 状态标记 */}; 所以对于kprobe的使用比较简单,只需要指定探测点地址,或者使用符号名+偏移的方式,定义xxx_handler,注册即可,注册后,探测指令被替换,可以使用kprobe_enable/disable函数动态开关 2.1 kprobe初始化 下面我们来看看 kprobe 的初始化过程,kprobe 的初始化由 init_kprobes() 函数kernel/kprobes.c实现: static int __init init_kprobes(void){ int i, err = 0; /* 初始化用于存储 kprobe 模块的哈希表 */ /* FIXME allocate the probe table, currently defined statically */ /* initialize all list heads */ for (i = 0; i < KPROBE_TABLE_SIZE; i++) INIT_HLIST_HEAD(&kprobe_table[i]); /* 初始化 kprobe 的黑名单函数列表(不能被 kprobe 跟踪的函数列表) */ err = populate_kprobe_blacklist(__start_kprobe_blacklist, __stop_kprobe_blacklist); if (err) { pr_err("kprobes: failed to populate blacklist: %d\n", err); pr_err("Please take care of using kprobes.\n"); } if (kretprobe_blacklist_size) { /* lookup the function address from its name */ for (i = 0; kretprobe_blacklist[i].name != NULL; i++) { kretprobe_blacklist[i].addr = kprobe_lookup_name(kretprobe_blacklist[i].name, 0); if (!kretprobe_blacklist[i].addr) printk("kretprobe: lookup failed: %s\n", kretprobe_blacklist[i].name); } } /* By default, kprobes are armed */ kprobes_all_disarmed = false; #if defined(CONFIG_OPTPROBES) && defined(__ARCH_WANT_KPROBES_INSN_SLOT) /* Init kprobe_optinsn_slots for allocation */ kprobe_optinsn_slots.insn_size = MAX_OPTINSN_SIZE;#endif /* 初始化CPU架构相关的环境(x86架构的实现为空) */ err = arch_init_kprobes(); if (!err) err = register_die_notifier(&kprobe_exceptions_nb); /* 注册die通知链*/ if (!err) err = register_module_notifier(&kprobe_module_nb); /* 注册模块通知链 */ kprobes_initialized = (err == 0); if (!err) init_test_probes(); return err;}early_initcall(init_kprobes); 2.2 注册一个kprobe实例 内核是通过register_kprobe完成一个kprobe实例的注册,其详细实现过程在kernel/kprobes.c,如下所示 /* struct kprobe结构体,里面包含指令地址或者函数名地址和函数内偏移 */int register_kprobe(struct kprobe *p){ int ret; struct kprobe *old_p; struct module *probed_mod; kprobe_opcode_t *addr; /* 获取被探测点的地址,指定了sysmbol name,则kprobe_lookup_name从kallsyms中获取; * 指定了offsete + address,则返回address + offset */ /* Adjust probe address from symbol */ addr = kprobe_addr(p); if (IS_ERR(addr)) return PTR_ERR(addr); p->addr = addr; /* 判断同一个kprobe是否被重复注册 */ ret = warn_kprobe_rereg(p); if (ret) return ret; /* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */ p->flags &= KPROBE_FLAG_DISABLED; p->nmissed = 0; INIT_LIST_HEAD(&p->list); /* 1. 判断被注册的函数是否位于内核的代码段内,或位于不能探测的kprobe实现路径中 * 2. 判断被探测的地址是否属于某一个模块,并且位于模块的text section内 * 3. 如果被探测的地址位于模块的init地址段内,但该段代码区间已被释放,则直接退出 */ ret = check_kprobe_address_safe(p, &probed_mod); if (ret) return ret; mutex_lock(&kprobe_mutex); /* 判断在同一个探测点是否已经注册了其他的探测函数 */ old_p = get_kprobe(p->addr); if (old_p) { /* Since this may unoptimize old_p, locking text_mutex. */ /* 如果已经存在注册过的kprobe,则将探测点的函数修改为aggr_pre_handler * 将所有的handler挂载到其链表上,由其负责所有handler函数的执行 */ ret = register_aggr_kprobe(old_p, p); goto out; } cpus_read_lock(); /* Prevent text modification */ mutex_lock(&text_mutex); /* 分配特定的内存地址用于保存原有的指令 */ ret = prepare_kprobe(p); mutex_unlock(&text_mutex); cpus_read_unlock(); if (ret) goto out; /* 将kprobe加入到相应的hash表内 */ INIT_HLIST_NODE(&p->hlist); hlist_add_head_rcu(&p->hlist, &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]); /* 将探测点的指令码修改为arm_kprobe */ if (!kprobes_all_disarmed && !kprobe_disabled(p)) { ret = arm_kprobe(p); if (ret) { hlist_del_rcu(&p->hlist); synchronize_rcu(); goto out; } } /* Try to optimize kprobe */ try_to_optimize_kprobe(p);out: mutex_unlock(&kprobe_mutex); if (probed_mod) module_put(probed_mod); return ret;} 其主要包括以下几个步骤: 探测点地址的计算:该函数主要用来指定位置注册探测点,首先使用kprobe_addr计算需要插入探测点的地址,这个会设置到kprobe的addr成员,注册后通过这个成员和offset就可以拿到探测位置的地址。利用这个特性,你可以通过kprobe来获取内核中某一个函数的运行时地址 如果没有指定探测地址,而是指定了符号信息,则调用kprobe_lookup_name在内核符号表中查找符号对应的地址,在找到对应的符号地址后,加上偏移就得到探测点的实际位置 如果只是指定了探测点的地址,则会将这个地址直接加上偏移返回 检测探测点地址:计算探测点的地址后,接下来就需要检查这个地址是否可以被探测 跟踪点是否已经被 ftrace 跟踪,如果是就返回错误(kprobe 与 ftrace 不能同时跟踪同一个地址) kprobe只能用作内核函数的探测,所以在注册前必须检查探测点的地址是否是在内核地址空间中,探测点的地址要么在内核影响中(_stext 和 etext之间,如果是在相同启动阶段(sinittext 和_einittext之间),具体实现在kernel_text_address代码中 跟踪点是否在 kprobe 的黑名单中,如果是就返回错误 如果探测点的地址在一个内核模块中,需要增加对该模板的引用,以防止模块提前卸载,如果模块已经开始卸载,此时也是不能注册探测点 保存被跟踪指令的值: 内核通过调用prepare_kprobe函数来保持被跟踪的指令,而 prepare_kprobe() 最终会调用 CPU 架构相关的 arch_prepare_kprobe() 函数来完成任务 注册kprobe:系统中所有的kprobe实例都保存在kprobe_table这个哈希表中 如果调用get_kprobe()能找到一个kprobe实例,说明已经在当前的探测点注册了一个kprobe,这种情况下会调用register_aggr_kprobe()来处理。 如果当前的探测点没有注册过kprobe,则调用arm_kprobe将被探测位置的指令保持到kprobe的ainsn成员中,并且被探测位置的第一条指令保存到opcode成员中 对于arch_prepare_kprobe,看指令是否是一些分支等特殊指令,需要特别处理。如果是正常可以probe的指令,调用arch_prepare_ss_slot把探测点的指令备份到slot page里,把下一条指令存入struct arch_probe_insn结构的restore成员里,在post_handler之后恢复执行。 arch_prepare_krpobe无误后把kprobe加入kprobe_table哈希链表。 然后调用arch_arm_kprobe替换探测点指令为BRK64_OPCODE_KPROBES指令。 int __kprobes arch_prepare_kprobe(struct kprobe *p){ unsigned long probe_addr = (unsigned long)p->addr; /* 地址应该为4的整数倍 */ if (probe_addr & 0x3) return -EINVAL; /* copy instruction */ p->opcode = le32_to_cpu(*p->addr); /* 大端小端转换,将地址进行转换成PC能识别的地址 */ /* 检测地址是否在异常代码段中 */ if (search_exception_tables(probe_addr)) return -EINVAL; /* 取出探测点的汇编指令 */ /* decode instruction */ switch (arm_kprobe_decode_insn(p->addr, &p->ainsn)) { case INSN_REJECTED: /* insn not supported */ return -EINVAL; /* 异常处理 */ case INSN_GOOD_NO_SLOT: /* insn need simulation */ p->ainsn.api.insn = NULL; break; case INSN_GOOD: /* instruction uses slot */ p->ainsn.api.insn = get_insn_slot(); if (!p->ainsn.api.insn) return -ENOMEM; break; } /* prepare the instruction */ if (p->ainsn.api.insn) arch_prepare_ss_slot(p); /* 将指令存放到slot中,记录吓一条指令到p->ainsn.api.insn */ else arch_prepare_simulate(p); /* 异常处理,如分支指令特殊处理 */ return 0;} 整个过程如下图所示: 最终会调用arm_kprobe,将指令3替换成一条BRK64异常处理指令,当CPU执行到这个跟踪点的时候,将会触发断点中断,这时候就会走到异常处理函数中,对于x86,这个是一条int 3指令,我们来看看针对ARM64,是如何处理的,其最终会调到arch_arm_kprobe,最终会替换成BRK64_OPCODE_KPROBES指令。 /* arm kprobe: install breakpoint in text */void __kprobes arch_arm_kprobe(struct kprobe *p){ void *addr = p->addr; /* 原地址 */ u32 insn = BRK64_OPCODE_KPROBES; /* 替换后的指令 */ aarch64_insn_patch_text(&addr, &insn, 1);} 2.3 触发kprobe探测和回调 kprobe的触发和处理是通过brk exception和single step单步exception执行的,每次的处理函数中会修改被异常中断的上下文(struct pt_regs)的指令寄存器,实现执行流的跳转。ARM64对于异常处理的注册在arch/arm64/kernel/debug-monitors.c, 是arm64的通用debug模块,kgdb也基于这个模块。 void __init debug_traps_init(void){ /* 单步异常处理 */ hook_debug_fault_code(DBG_ESR_EVT_HWSS, single_step_handler, SIGTRAP, TRAP_TRACE, "single-step handler"); /* 断点异常处理 */ hook_debug_fault_code(DBG_ESR_EVT_BRK, brk_handler, SIGTRAP, TRAP_BRKPT, "BRK handler");} 通过hook_debug_fault_code动态定义了异常处理的钩子函数brk_handler,它将在断点异常处理函数中被调用 hook_debug_fault_code是替换arch/arm64/mm/fault.c 中的debug_fault_info异常表项: 对于ARM64的异常处理,当brk断点异常触发后悔执行不同的回调处理,进入异常会跳转到arch/arm64/kernel/entry.S的sync异常处理,此处会跳转到el1_sync 将 entry_handler 1, t, 64, sync宏展开得到调用el1t_64_sync_handler的处理函数,在arch/arm64/kernel/entry-common.c中处理,是通过read_sysreg(esr_el1)来处理对应的异常 最终会调用do_debug_exception处理debug异常 sr_el1的bit27~bit29指示了debug异常类型,对应debug_fault_info数组的索引,此处可知debug异常类型为0x6,对应DBG_ESR_EVT_BRK,由初始化函数debug_traps_init可知inf->fn为brk_handler brk_handler会调用call_break_hook,它实际是根据具体的某种断点异常类型来回调不同的hook,主要是根据ESR_EL1.ISS.Comment进行区分,也就是不同的ESR_EL1.ISS.Comment对应不同的hook。 在初始化时register_kernel_break_hook会向kernel_break_hook链表注册不同的hook,这包括kprobes_break_hook和kprobes_break_ss_hook。list_for_each_entry_rcu(hook, list, node)主要通过遍历kernel_break_hook链表,根据debug断点异常类型找到匹配的hook。 可以看出kprobe_handler里先是进入pre_handler,然后通过setup_singlestep设置single-step相关寄存器,为下一步执行原指令时发生single-step异常做准备 2.4 单步执行 进入异常态后,首先执行pre_handler,然后利用CPU提供的单步调试(single-step)功能,设置好相应的寄存器,将下一条指令设置为插入点处本来的指令,从异常态返回 这个里面使用reenter检查机制,对于SMP,中断等可能有kprobe的重入,允许kpobe发生嵌套 setup_singlestep() 执行完毕后,程序继续执行保存的被探测点的指令,由于开启了单步调试模式,执行完指令后会继续触发异常,单步执行探测点的指令后,会触发单步异常,进入single_step_handler,调用kprobe_breakpoint_ss_handler,主要任务是恢复执行路径,调用用户注册的post_handler kprobe的实现原理是把指定地址(探测点)的指令替换成一个可以让cpu进入debug模式的指令,使执行路径暂停,跳转到probe 处理函数后收集、修改信息,再跳转回来继续执行。 X86中使用的是int3指令,ARM64中使用的是BRK指令进入debug monitor模式。 3 kprobe event实现原理 首先我们跟function一样,从我们的配置开始,krpobe event和功能一样,那么大部分的实现是一样的,最关键的不同就是怎么使用新的插桩方法来创建event。使用向“/sys/kernel/debug/tracing/kprobe_events”文件中echo命令的形式来创建krpobe event。来查看具体的代码实现: 经过层层调用,最终到__trace_kprobe_create函数,其主要的实现如下: 对于alloc_trace_kproe,可以看到kretprobe模式下的桩函数:kretprobe_dispatcher(),而kprobe模式下的插桩函数为kprobe_dispatcher 其最终也会通过__register_trace_kprobe注册kprobe和kpretprobe,其最终的原理也是基本类似 4 kprobe的使用方法 最早的时候,使用kprobe一般都是编写内核驱动,在模块中定义pre-handler和post-handler函数,然后调用kprobe的API(register_kprobe)来进行注册kprobe。加载模块后,pre-handler和post-handler中的printk就会打印定制的信息到系统日志中,目前有三种使用kporbe的接口 kprobe API:使用register_kprobe 基于Ftrace的/sys/kernel/debug/tracing/kprobe_events接口,通过写特定的配置文件 perf_event_open:通过perf工具,perf 的probe命令提供了添加动态探测点的功能, 参看 kernel/tools/perf/Documentation/perf-probe.txt, 在最新的内核上,BPF tracing也是通过这种方式,后面再学习这种方法 kprobes的最大使用者都是一些tracer的前端工具,比如perf、systemtap、BPF 跟踪(BCC和bpftrace) 由于第一种方式灵活而且功能更为强大,对于方法一,大家请参考示例 kprobe:请参考samples/kprobes/kprobe_example.c kretprobe:请参考sample/kprobe/kretprobe_example.c 要编写一个 kprobe 内核模块,可以按照以下步骤完成: 第一步:根据需要来编写探测函数,如 pre_handler 和 post_handler 回调函数。 第二步:定义 struct kprobe 结构并且填充其各个字段,如要探测的内核函数名和各个探测回调函数。 第三步:通过调用 register_kprobe 函数注册一个探测点。 第四步:编写 Makefile 文件。 第五步:编译并安装内核模块。 对于方式二,用户通过/sys/kernel/debug/tracing/目录下的trace等属性文件来探测用户指定的函数,用户可添加kprobe支持的任意函数并设置探测格式与过滤条件,无需再编写内核模块,使用更为简便,但需要内核的debugfs和ftrace功能的支持,详细的请参考内核文档kprobetrace 使用前确定内核CONFIG打开:CONFIG_KPROBE_EVENT=y /sys/kernel/tracing/kprobe_events:添加断点接口 /sys/kernel/tracing/events/kprobes/enabled:断点使能开关 /sys/kernel/tracing/trace:查看trace日志接口 4.1 查看"vfs_open"当前打开文件名 如果你使用了“‘p:’ or ‘r:’+event name” > kprobe_events命令,新的kprobe event将会被添加,可以看到新events对应的文件夹tracing/events/kprobes/,包含‘id’, ‘enabled’, ‘format’ and ‘filter’文件。 enable:使能 filter:过滤想要的信息 trigger:事件发生时触发其他功能,例如function功能 format:环形队列缓冲区的格式 id: event对应的id echo 1 > /sys/kernel/tracing/events/kprobes/myprobe/enable echo 1 > /sys/kernel/tracing/tracing_on 要查看哪些进程触发了这些kprobe,可以通过trace、trace_pipe接口查看,输出格式如下,最左边是进程名,如果是<…>,可能是因为cat的时候,那个进程号对应的进程已经不存在了,第二个是进程PID,触发kprobe的时候记录的。FUNCTION就是触发的那个kprobe的名字,后面括号里是触发的时候代码位置,如果是“r”类型的kprobe,会显示返回到了什么代码位置。代码位置中的行号是反汇编对应的行号。 # 设置kprobe规则,获取vfs_open函数第一个参数path中的文件name cd /sys/kernel/tracingecho 'p vfs_open name=+0x38(+0x8($arg1)):string namep=+0(+0x28(+0x8($arg1))):string' > ./kprobe_events # 使能上述的kprobe echo 1 > ./events/kprobes/p_vfs_open_0/enable # 使能数据写入到 Ring 缓冲区 echo 1 > tracing_on 通过offset和类型打印,实现结构体内部成员的打印,但是需要知道寄存器和参数的对应关系和结构体成员的偏移。 https://www.notion.so/Kprobe-on-ARM64-d6d43e398a5e42b48e752f0f06aa0053#27fdb6f9b3c94d398f9220b495e04107 提到了新的function_event机制,可以直接传递参数名。例如我们想获取net_device的stats信息,获取数据结构偏移的例子:打印ip_rcv的网络设备名和收发包数 $ aarch64-linux-gnu-gdb vmlinux (gdb) ptype/o struct net_device gdb) print (int)&((struct net_device *)0)->stats$7 = 296 cd /sys/kernel/debug/tracing/echo 'p:net ip_rcv name=+0(%x1):string rx_pkts=+296(%x1):u64 tx_pkts=+280(%x1):u64 ' > kprobe_eventsecho 1 > events/kprobes/enable 4.2 设置了一个kretprobe,用来记录返回值 root@rlk:/sys/kernel/tracing# echo 0 > tracing_onroot@rlk:/sys/kernel/tracing# echo 0 > ./events/kprobes/p_vfs_open_0/enableroot@rlk:/sys/kernel/tracing# echo 'p vfs_open name=+0x38(+0x8($arg1)):string namep=+0(+0x28(+0x8($arg1))):string' > ./kprobe_eventsroot@rlk:/sys/kernel/tracing# echo 'r vfs_open ret_val=$retval' >> kprobe_eventsroot@rlk:/sys/kernel/tracing# echo 1 > events/kprobes/p_vfs_open_0/enableroot@rlk:/sys/kernel/tracing# echo 1 > events/kprobes/r_vfs_open_0/enableroot@rlk:/sys/kernel/tracing# echo 1 > tracing_onroot@rlk:/sys/kernel/tracing# echo 0 > tracing_onroot@rlk:/sys/kernel/tracing# cat trace_pipe 4.3 filter:捕获"vfs_open"查看指定文件的信息的事件 # 设置过滤条件,name中包含test字段 echo 'name ~ "*test*"' > ./events/kprobes/p_vfs_open_0/filter 4.4 trigger:包含"test"字段的文件的事件会触发"stacktrace"堆栈打印 # 包含"test"字段的文件的事件会触发"stacktrace"堆栈打印echo 'stacktrace if name ~ "*test*"' > ./events/kprobes/p_vfs_open_0/trigger 5 总结 至此,我们知道Kprobe实现的本质是breakpoint和single-step的结合,这一点和大多数调试工具一样,比如kgdb/gdb。实现动态内核的注入,其主要流程如下: 当 kprobe 被注册后,内核会将对应地址的指令进行拷贝并替换为断点指令(比如 X86 中的 int 3) 随后当内核执行到对应地址时,中断会被触发从而执行流程会被重定向到我们注册的 pre_handler 函数 当对应地址的原始指令执行完后,内核会再次执行 post_handler从而实现指令级别的内核动态监控。也就是说,kprobe 不仅可以跟踪任意带有符号的内核函数,也可以跟踪函数中间的任意指令。
TVS管全称:瞬态电压抑制器(Transient Voltage Suppressor),是一种用于保护电子设备的关键元件。作为保护器件,TVS管能有效的吸收和抑制电路中瞬态过电压。当电路正常工作时(也就是没有出现瞬态过电压时),TVS管的阻抗非常高(漏电流非常小)。当电路中出现瞬态过电压时,TVS管阻抗会迅速下降,从而让瞬态过电压电流通过TVS管导入GND,以热能的形式迅速消耗掉过电压,使过电压抑制在安全电压内,如下图所示:那在实际的项目中,我们该怎么选型?以下面这两张TVS管特性图展开:单向TVS管特性图双向TVS管特性图可以很明显看得出来,单向TVS和双向TVS只要区别在于双向TVS多了负半轴的特性!如果上面的两张图看不懂,没关系,我们接着往下讲,后面会对图中的各个参数进行简述。(1)首先是极性需要提前知道需要保护的接口信号或者电压到底是直流还是交流,对于直流来说,使用单向TVS管即可,如果对于交变的信号或者是电压就需要使用双向的TVS。(2)封装需要根据板子的尺寸来进行选型,如果板子的面积有限,可以选择小封装系列的TVS管。(3)Vrwm:反向工作电压Vrwm也称最大工作电压,指的是加在TVS两端的最大电压,所加电压如果超过Vrwm就可能会对TVS造成永久性损坏。也就是说,需要确定板子的最大工作电压,板子的最大工作电压不能大于Vrwm。直流供电:Vrwm≥最大工作电压交流供电:Vrwm≥最大工作电压*1.414(4)VCmax:最大钳位电压VCmax直白的讲就是当瞬态电压超过TVS的最大钳位电压时,TVS管就会从高阻态变成低阻态,从而把瞬态电压钳至到后级保护电路所能承受的最大耐压值,所以在选型时,需要确定被保护电路的器件最大耐压值是多少,才能选定VCmax。 (5)VBR:击穿电压VBR指在TVS在进入雪崩状态时(被击穿的转态下)的两端电压值,很多TVS管的VBR都是1.1~1.3倍Vrwm。(6)IR:最大反向漏电流IR,这个就是比较好理解了,就是指TVS管在正常工作下的漏电流,漏电流当然是越小越好,但是也要综合考虑成本问题。(7)Ipp:最大反向浪涌峰值电流Ipp,指的就是TVS在遇到瞬态电压时,TVS开始起保护作用时,能吸收的最大电流值或者说能流过TVS管的最大电流值。Ipp和钳位电压VBR对应起来,Ipp和VBR的乘积就是Ppp(最大脉冲峰值功率)理论上来讲,Ipp越大越好,但还是得根据项目的需求,做好权衡。(8)Cj:结电容Cj是TVS管的结电容,这是无法避免的,在选型时,需要注意结电容对正常工作信号的影响,特别是用在高速接口处,需要清楚接口的速率来选择合适的TVS管。图片来源网络
一、氧传感工作原理及其作用氧传感器是排气系统中重要的零部件之一,它一共有两个,分别位于三元催化器的前方和后方。位于前方的叫前氧传感器,后方的叫后氧传感器。 前氧传感器主要是用来检测发动机燃烧后的废气中氧含量多少,然后将检测的信号传送给ECU(发动机电脑)用于修正喷油量,从而实现对空燃比的闭环控制,保证发动机实际的空燃比接近理论的空燃比(空燃比就是空气和燃油的比例)。比如此时发动机混合气过稀,也就是空气多汽油少(空燃比失衡),混合气燃烧后就会还剩余部分空气被当作废气排出,而氧传感器就会检测到排出空气中的氧含量,然后将信号反馈给ECU,让它通过加大喷油量或者减少进气量的方式来调整空燃比,从而使发动机的混合气燃烧保持最佳状态。 后氧传感器的作用就相对简单一些,它主要是用来检测经三元催化净化后的废气氧浓度,如果前氧传感器与后氧传感器所检测出的数据相同,则说明三元催化已经失效。 二、氧传感器损坏的影响 氧传感器在正常状态下是呈晦暗色,且表面没有杂质残留。如果氧传感器表面被黑色或灰白色等沉积物覆盖,则表明氧传感器被机油污染,车辆可能存在烧机油的现象。如果氧传感器表面是呈亮白色的锈迹,则表明氧传感器中毒,主要是由于加注含铅汽油所导致的。如果氧传感器表面有一层类似水垢的沉积物,则表明车辆可能存在防冻液内漏的现象,也就是说防冻液漏入气缸内被燃烧。 氧传感器损坏最明显的特征是油耗增高、怠速不稳、加速无力等故障。因为ECU会根据前氧传感器的信号来修正喷油量,如果前氧传感器损坏,就会导致空燃比时大时小,混合气也就会时浓时稀,或者混合气一直加浓、一直减稀,进而造成车辆怠速不稳、油耗突然增高等现象。一般在解决此类故障时,更换前氧传感器即可。 三、氧传感器损坏后,有必要两个一起更换吗? 前面已经提到过,氧传感器分为前氧和后氧,它们是两个独立的零部件。所以,如果前氧传感器损坏就更换前氧,后氧传感器损坏就更换后氧,不需要两个氧传感器一起更换,除非它们两个同时坏掉。另外,在用车的过程中加注高质量无铅汽油,不仅可以有效延长氧传感器的使用寿命,还可以延长三元催化器的使用时间。 本文来源:汽车电子库
1、综合读写模块(无FIFO)\x26amp;nbsp; \x26amp;nbsp; \x26amp;nbsp;在前六篇SDRAM系列博文中,我们对S
我们知道电感磁芯是很多电子产品中都会用到的产品,比如:手机,变压器等等,电子产品在使用过程中都会产生一定的损耗,而电感磁芯也不例外。如果电感磁芯的损耗过大,就会影响电感磁芯的使用寿命。 电感磁芯损耗(主要包括磁滞损耗和涡流损耗两部分)的特性是功率材料的一个最主要的指标,它影响甚至决定了整机的工作效率、温升、可靠性。 什么是电感? 电感是把电能转化为磁能而存储起来的元件,它只阻碍电流的变化,有通电与未通电两种状态,如果电感器在没有电流通过的状态下,电路接通时它将试图阻碍电流流过它;如果电感器在有电流通过的状态下,电路断开时它将试图维持电流不变。 电感磁芯是由线圈和磁芯以及封装材料组成的,线圈主要起导电作用,即磁芯是由磁导率高的材料组成,把磁场紧密地约束在电感元件周围增大电感。磁芯是由传统的硅钢片,到铁粉, 铁氧体, 铁硅等变化。 电感磁芯损耗 1、磁滞损耗 通俗来说,电感的磁滞损耗可以理解为一种“反复摩擦”引起的能量损失。 电感的核心通常是由磁性材料(比如铁芯)制成的。当电流流过电感时,会产生一个磁场,这个磁场在磁性材料内部来回变化(比如交流电的情况)。然而,磁性材料不是“完美的”,它的内部磁化过程需要克服一定的阻力,就像推动一块橡皮泥需要用力。 这种磁化和去磁的“来回拉扯”过程会让磁性材料内部产生热量,消耗一部分电能。这种能量损耗被称为磁滞损耗,因为它与磁滞回线(表示磁场变化时材料响应的延迟和不可逆性)有关。 简单总结:磁滞损耗就像内部有很多可以旋转的指南针(磁畴),虽然它最终还是能回到原来的状态,但每次旋转都会消耗一点能量,最终会表现为热量释放出来。这就是电感磁滞损耗的基本原理! 磁芯材料磁化时,送到磁场的能量有2部分,一部分转化为势能,即去掉外磁化电流时,磁场能量可以返回电路;而另一部分变为克服摩擦使磁芯发热消耗掉,这就是磁滞损耗。 磁滞回线,如下图: 磁化曲线中阴影部分的面积代表了在一个工作周期内,磁芯在磁化过程中由磁滞现象引起的能量损耗。如上图可知,影响损耗面积大小几个参数是:最大工作磁通密度B、最大磁场强度H、剩磁Br、矫顽力Hc,其中B和H取决于外部的电场条件和磁芯的尺寸参数,而Br和Hc取决于材料特性。电感磁芯每磁化一周期,就要损耗与磁滞回线包围面积成正比的能量,频率越高,损耗功率越大,磁感应摆幅越大,包围面积越大,磁滞损耗越大。 2、涡流损耗 电感的涡流损耗可以通俗地理解为磁场中“感应的小漩涡”带来的能量损耗。 当电感中有交流电流流过时,会产生变化的磁场。如果电感的铁芯是由导电材料制成(比如铁或其他金属),那么这个变化的磁场就会在铁芯内部“切割”导电材料,感应出一些小环形电流,这些环形电流就像水面上出现的小漩涡一样,我们称它们为涡流。 这些涡流在导电的铁芯内部流动时,会遇到电阻,电阻会把这些涡流的能量转化为热量,从而造成能量损失。这就是涡流损耗。 每个电感都可以看成是一个发热不咋地的“电磁炉”: 电磁炉的工作原理可以通俗地总结为:通过磁场产生涡流,利用涡流发热。 详细解释: 高频交流电生成磁场: 电磁炉内部有一个线圈,通上高频交流电(通常在20kHz到100kHz之间),这个电流会在线圈周围产生一个快速变化的磁场。 磁场切割锅底,产生涡流: 当把导磁性和导电性良好的锅(比如铁锅、不锈钢锅)放在电磁炉上时,变化的磁场会在锅底感应出“涡流”(也就是环形电流)。 涡流发热: 涡流在锅底流动时,由于锅底的电阻,电流会转化为热量,锅底开始加热。这个加热过程直接在锅底内部完成,所以锅会很快升温。 锅传热给食物: 锅底加热后,通过传导把热量传递给食物,从而实现烹饪。 每个电感即使发热比例再低,他也是个电磁炉,磁性也是导体,如同锅一样,有部分能量因为电磁场变化,切割磁芯的导体,在磁芯内部形成电流耗电转化为热量,这个就是涡流。 在磁芯线圈中加上交流电压时﹐线圈中流过激励电流﹐激磁安匝产生的全部磁通Φi在磁芯中通过﹐如下图。磁芯本身是导体﹐磁芯截面周围将链合全部磁通Φi而构成单匝的副边线圈。 磁芯中的涡流 根据电磁感应定律可知:U= NdΦ/d t;每一匝的感应电势﹐即磁芯截面最大周边等效一匝感应电势为 因为磁芯材料的电阻率不是无限大﹐绕着磁芯周边有一定的电阻值﹐感应电压产生电流ie即涡流,流过这个电阻,引起ie2R损耗﹐即涡流损耗。 3、剩余损耗 剩余损耗是由于磁化弛豫效应或磁性滞后效应引起的损耗。所谓弛豫是指在磁化或反磁化的过程中,磁化状态并不是随磁化强度的变化而立即变化到它的最终状态,而是需要一个过程,这个‘时间效应’便是引起剩余损耗的原因。它主要是在高频1MHz以上一些驰豫损耗和旋磁共振等,在开关电源几百KHz的电力电子场合剩余损耗比例非常低,可以近似忽略。 选择合适的磁芯,要考虑不同的B-H曲线和频率特性,因为B-H曲线决定了电感的高频损耗,饱和曲线及电感量。因为涡流一方面引起电阻损耗,导致磁材料发热,并引起激磁电流加大,另一方面减少磁芯有效导磁面积。所以尽量选择电阻率高的磁性材料或采用碾轧成带料的形式以减少涡流损耗。因此,铂科新材料NPH-L适用于更高频率、高功率器件的低损耗金属粉芯。如图所示: 磁芯损耗是磁芯材料内交替磁场引致的结果。某一种材料所产生的损耗,是操作频率与总磁通摆幅(ΔB)的函数,从而降低了有效传导损耗。磁芯损耗是由磁芯材料的磁滞、涡流和剩余损耗引起的。所以,磁芯损耗是磁滞损耗、涡流损耗和剩磁损耗的总和。公式如下: 磁滞损耗为磁滞现象产生的功率损耗,正比于磁滞回线包围的面积。当穿过磁芯的磁场发生变化时磁芯内产生涡流,涡流产生的损耗叫做涡流损耗。剩余损耗是除了磁滞损耗和涡流损耗以外其他所有损耗。 这一公式是用于测定磁通密度的峰值,与磁芯损耗曲线并用,应用在正弦波上,在这状态下,磁芯产生—种磁通密度峰与峰之间的摆幅(ΔB),这一摆幅是上述公式所计算出的磁芯损耗磁通密度峰值的两倍,如下图所示: 总结:在总损耗主要是由磁芯损耗而不是铜损耗引起的电感器用途上,可用磁导率较低的磁芯材料改进。
引言:可使用频谱的增加会推动无线设备逐步改进,但增加这些额外的频谱有时候会影响到某些区域,导致其中的频段相互重叠,此外,由于RF路径增加,会导致系统发热量随之增加,而发热反过来又会影响滤波器的性能。
在Linux中经常发现空闲内存很少,似乎所有的内存都被系统占用了,感觉是内存不够用了。其实不然,这是Linux内存管理的一个优秀的特征,主要特点是,物理物理内存有多大,Linux都将其充分利用,将一些程序调用过的硬盘数据读入内存(buffer/Cache),利用内存读写的高速特性来提供Linux系统的数据访问性能高。 1. 什么是Page Cache 当程序去读文件,可以通过read也可以通过mmap去读,当你通过任何一种方式从磁盘读取文件时,内核都会给你申请一个Page cache,用来缓存磁盘上的内容。这样读过一次的数据,下次读取的时候就直接从Page cache里去读,提升了系统的整体性能。 对于Linux可以怎么来观察Page Cache呢?其实,在Linux上直接可以通过命令来看,他们的内容是一致的。 首先最简单的是free命令来看一下 首先我们来看看buffers和cached的定义 Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。 Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘 buffer cache和page cache在处理上是保持一致的,但是存在概念上的差别,page cache是针对文件的cache,buffer是针对磁盘块数据的cache,仅此而已。 2. 为什么需要page cache 通过上图,我们可以直观的看到,标准的I/O和内存映射会先将数据写到Page Cache,这样做是通过减小I/O次数来提升读写效率。我们来实际的例子,我们先来生成一个1G的文件,然后通过把Page cache清空,确保文件内容不在内存中,一次来比较第一次和第二次读文件的差异。 # 1. 生成一个1G的文件dd if=/dev/zero of=/home/dd.out bs=4096 count=1048576# 2.第一次读取文件的耗时如下:root@root-PC:~# time cat /home/dd.out &> /dev/nullreal 0m1.109suser 0m0.016ssys 0m1.093s# 23.清空Page Cache,先执行一下sync来将脏页同步到磁盘,在执行drop cachesync && echo 3 > /proc/sys/vm/drop_caches# 4. 第二次读取文件的耗时如下: root@root-PC:~# time cat /home/dd.out &> /dev/nullreal 0m36.018suser 0m0.069ssys 0m4.839s 通过这两次详细的过程,可以看出第一次读取文件的耗时远小于第二次耗时 因为第一次读取的时候,由于文件内容已经在生成文件的时候已经存在,所以直接从内存读取的数据 第二次会将缓存数据清掉,会从磁盘上读取内容,磁盘I/O比较耗时,内存相比磁盘会快很多 所以Page Cache存在的意义,减小I/O,提升应用的I/O速度。对于Page Cache方案,我们采用原则如下 如果不想增加应用的复杂度,我们优先使用内核管理的Page Cache 如果应用程序需要做精确控制,就需要不走Cache,因为Page Cache有它自身的局限性,就是对于应用程序太过于透明了,以至于很难有好的控制方法。 3. Page Cache是如何“诞生的” Page Cache的产生有两种不同的方式 Buffered I/O(标准I/O) Memory-Mapped I/O(储存映射IO) 这两种方式分别都是如何产生Page Cache的呢? 从图中可以看到,二者是都能产生Page Cache,但是二者还是有差异的 标准I/O是写的话用户缓存区(User page对应的内存),然后再将用户缓存区里的数据拷贝到内核缓存区(Pagecahe Page对应的内存);如果是读的话则是内核缓存区拷贝到用户缓存区,再从用户缓存区去读数据,也就是Buffer和文件内容不存在映射关系 储存映射IO,则是直接将Page Cache的Page给映射到用户空间,用户直接读写PageCache Page里的数据 从原理来说储存映射I/O要比标准的I/O效率高一些,少了“用户空间到内核空间互相拷贝”的过程。下图是一张简图描述这个过程: 首先,往用户缓冲区Buffer(用户空间)写入数据,然后,Buffer中的数据拷贝到内核的缓冲区(这个是PageCache Page) 如果内核缓冲区还没有这个page,就会发生Page Fault会去分配一个Page;如果有,就直接用这个PageCache的Page 拷贝结束后,该PageCache的Page是一个Dirty Page脏页,然后该Dirty Page中的内容会同步到磁盘,同步到磁盘后,该PageCache Page变味Clean Page并且继续存在系统中 我们可以通过手段来测试脏页,如下图所示 $ cat /proc/vmstat | egrep "dirty|writeback"nr_dirty 44nr_writeback 0nr_writeback_temp 0nr_dirty_threshold 1538253nr_dirty_background_threshold 768187 nr_dirty:表示系统中积压了多少脏页(单位为Page 4KB) nr_writeback则表示有多少脏页正在回写到磁盘中(单位为Page 4KB) 总结 读过程,当内核发起一个读请求时候 先检查请求的数据是否缓存到page Cache中,如果有则直接从内存中读取,不访问磁盘 如果Cache中没有请求数据,就必须从磁盘中读取数据,然后内核将数据缓存到Cache中 这样后续请求就可以命中cache,page可以只缓存一个文件的部分内容,不需要把整个文件都缓存 写过程,当内核发起一个写请求时候 直接写到Cache中,内核会将被写入的Page标记为dirty,并将其加入到dirty list中 内核会周期性的将dirty list中的page回写到磁盘上,从而使磁盘上的数据和内存中缓存的数据一致 4. page cache是如何“死亡” free命令中的buffer/cache中的是“活着”的Page Cache,那他们是什么时候被回收的呢? 回收的主要方式有两种 直接回收: 后台回收: 观察Page cache直接回收和后台回收最简单方便的方式,借助这个工具,可以明确观察内存回收行为 pgscank/s: kswapd(后台回收线程)每秒扫面的Page个数 pgscand/s: Application在内存申请过程中每秒直接扫描的Page个数 pgsteal/s: 扫面的page中每秒被回收的个数 %vmeff: pgsteal/(pgscank+pgscand),回收效率,越接近100说明系统越安全,越接近0,说明系统内存压力越大 pgpgin/s 表示每秒从磁盘或SWAP置换到内存的字节数(KB) pgpgout/s: 表示每秒从内存置换到磁盘或SWAP的字节数(KB) fault/s: 每秒钟系统产生的缺页数,即主缺页与次缺页之和(major + minor) majflt/s: 每秒钟产生的主缺页数. pgfree/s: 每秒被放入空闲队列中的页个数 需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享 5.Page Cache性能优化 通过前文我们知道了Linux是用Cache/Buffer缓存数据,提高系统的I/O性能,且有一个回刷任务在适当时候把脏数据回刷到储存介质中。那么接下来我们重点学习优化机制。 包括以下内容 什么时候触发回刷? 脏数据达到多少阈值还是定时触发呢? 内核是如何做到回写机制的 (1) 配置概述 Linux内核在/proc/sys/vm中有透出数个配置文件,可以对触发回刷的时机进行调整。内核的回刷进程是怎么运作的呢?这数个配置文件有什么作用呢? root@public-VirtualBox:~# sysctl -a | grep dirtyvm.dirty_background_bytes = 0vm.dirty_background_ratio = 10vm.dirty_bytes = 0vm.dirty_expire_centisecs = 3000vm.dirty_ratio = 20vm.dirty_writeback_centisecs = 500vm.dirtytime_expire_seconds = 43200 在/proc/sys/vm中有以下文件与回刷脏数据密切相关: vm.dirty_background_ratio: 内存可以填充脏数据的百分比,这些脏数据稍后会写入磁盘。pdflush/flush/kdmflush这些后台进程会稍后清理脏数据。比如,我有32G内存,那么有3.2G(10%的比例)的脏数据可以待着内存里,超过3.2G的话就会有后台进程来清理。 vm.dirty_ratio 可以用脏数据填充的绝对最大系统内存量,当系统到达此点时,必须将所有脏数据提交到磁盘,同时所有新的I/O块都会被阻塞,直到脏数据被写入磁盘。这通常是长I/O卡顿的原因,但这也是保证内存中不会存在过量脏数据的保护机制。 vm.dirty_background_bytes 和 vm.dirty_bytes 另一种指定这些参数的方法。如果设置 xxx_bytes版本,则 xxx_ratio版本将变为0,反之亦然。 vm.dirty_expire_centisecs 指定脏数据能存活的时间。在这里它的值是30秒。当 pdflush/flush/kdmflush 在运行的时候,他们会检查是否有数据超过这个时限,如果有则会把它异步地写到磁盘中。毕竟数据在内存里待太久也会有丢失风险。 vm.dirty_writeback_centisecs 指定多长时间 pdflush/flush/kdmflush 这些进程会唤醒一次,然后检查是否有缓存需要清理。 实际上dirty_ratio的数字大于dirty_background_ratio,是不是就不会达到dirty_ratio呢? 首先达到dirty_background_ratio的条件后触发flush进程进行异步的回写操作,但是这一过程中应用进程仍然可以进行写操作,如果多个应用写入的量大于flush进程刷出的量,那自然就会达到vm.dirty_ratio这个参数所设定的阙值,此时操作系统会转入同步地进行脏页的过程,阻塞应用进程。 (2)配置实例 单纯的配置说明毕竟太抽象。结合网上的分享,我们看看在不同场景下,该如何配置? 场景1:尽可能不丢数据 有些产品形态的数据非常重要,例如行车记录仪。在满足性能要求的情况下,要做到尽可能不丢失数据。 /* 此配置不一定适合您的产品,请根据您的实际情况配置 */dirty_background_ratio = 5dirty_ratio = 10dirty_writeback_centisecs = 50dirty_expire_centisecs = 100 这样的配置有以下特点: 当脏数据达到可用内存的5%时唤醒回刷进程 脏数据达到可用内存的10%时,应用每一笔数据都必须同步等待 每隔500ms唤醒一次回刷进程 当脏数据达到可用内存的5%时唤醒回刷进程 由于发生交通事故时,行车记录仪随时可能断电,事故前1~2s的数据尤为关键。因此在保证性能满足不丢帧的情况下,尽可能回刷数据。 此配置通过减少Cache,更加频繁唤醒回刷进程的方式,尽可能让数据回刷。 此时的性能理论上会比每笔数据都O_SYNC略高,比默认配置性能低,相当于用性能换数据安全。 场景2:追求更高性能 有些产品形态不太可能会掉电,例如服务器。此时不需要考虑数据安全问题,要做到尽可能高的IO性能。 /* 此配置不一定适合您的产品,请根据您的实际情况配置 */dirty_background_ratio = 50dirty_ratio = 80dirty_writeback_centisecs = 2000dirty_expire_centisecs = 12000 这样的配置有以下特点: 当脏数据达到可用内存的50%时唤醒回刷进程 当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待 每隔20s唤醒一次回刷进程 内存中脏数据存在时间超过120s则在下一次唤醒时回刷 与场景1相比,场景2的配置通过 增大Cache,延迟回刷唤醒时间来尽可能缓存更多数据,进而实现提高性能 场景3:突然的IO峰值拖慢整体性能 什么是IO峰值?突然间大量的数据写入,导致瞬间IO压力飙升,导致瞬间IO性能狂跌,对行车记录仪而言,有可能触发视频丢帧。 /* 此配置不一定适合您的产品,请根据您的实际情况配置 */dirty_background_ratio = 5dirty_ratio = 80dirty_writeback_centisecs = 500dirty_expire_centisecs = 3000 这样的配置有以下特点: 当脏数据达到可用内存的5%时唤醒回刷进程 当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待 每隔5s唤醒一次回刷进程 内存中脏数据存在时间超过30s则在下一次唤醒时回刷 这样的配置,通过增大Cache总容量,更加频繁唤醒回刷的方式,解决IO峰值的问题,此时能保证脏数据比例保持在一个比较低的水平,当突然出现峰值,也有足够的Cache来缓存数据。 (3)内核演变 对于回写方式在之前的2.4内核中,使用 bdflush的线程专门负责writeback的操作,因为磁盘I/O操作很慢,而现代操作系统通常具有多个块设备,如果bdflush在其中一个块设备上等待I/O操作的完成,可能需要很长的时间,此时其他块设备还处于空闲状态,这时候,单线程模式的bdflush就称为了影响性能的瓶颈。而此时bdflush是没有周期扫描功能,因此需要配合kupdate线程一起使用。 bdflush 存在的问题: 整个系统仅仅只有一个 bdflush 线程,当系统回写任务较重时,bdflush 线程可能会阻塞在某个磁盘的I/O上, 导致其他磁盘的I/O回写操作不能及时执行 于是在2.6内核中,bdflush机制就被pdflush取代,pdflush是一组线程,根据块设备I/O负载情况,数量从最少的2个到最多的8个不等,如果1S内都没有空闲的pdflush线程可用,内核将创建一个新的pdflush线程,反之某个pdflush线程空闲超过1S,则该线程将会被销毁。pdflush 线程数目是动态的,取决于系统的I/O负载。它是面向系统中所有磁盘的全局任务的。 pdflush 存在的问题: pdflush的数目是动态的,一定程度上缓解了 bdflush 的问题。但是由于 pdflush 是面向所有磁盘的,所以有可能出现多个 pdflush 线程全部阻塞在某个拥塞的磁盘上,同样导致其他磁盘的I/O回写不能及时执行。 于是在内最新的内核中,直接将一个块设备对应一个thread,这种内核线程被称为flusher threads,线程名为“Writeback",执行体为"wb_workfn",通过workqueue机制实现调度。 (4)内核实现 由于内核page cache的作用,写操作实际被延迟写入。当page cache里的数据被用户写入但是没有刷新到磁盘时,则该page为脏页(块设备page cache机制因为以前机械磁盘以扇区为单位读写,引入了buffer_head,每个4K的page进一步划分成8个buffer,通过buffer_head管理,因此可能只设置了部分buffer head为脏)。 脏页在以下情况下将被回写(write back)到磁盘上: 脏页在内存里的时间超过了阈值。 系统的内存紧张,低于某个阈值时,必须将所有脏页回写。 用户强制要求刷盘,如调用sync()、fsync()、close()等系统调用。 以前的Linux通过pbflush机制管理脏页的回写,但因为其管理了所有的磁盘的page/buffer_head,存在严重的性能瓶颈,因此从Linux 2.6.32开始,脏页回写的工作由bdi_writeback机制负责。bdi_writeback机制为每个磁盘都创建一个线程,专门负责这个磁盘的page cache或者buffer cache的数据刷新工作,以提高I/O性能。 在 kernel/sysctl.c中列出了所有的配置文件的信息 static struct ctl_table vm_table[] = { ... { .procname = "dirty_background_ratio", .data = &dirty_background_ratio, .maxlen = sizeof(dirty_background_ratio), .mode = 0644, .proc_handler = dirty_background_ratio_handler, .extra1 = &zero, .extra2 = &one_hundred, }, { .procname = "dirty_ratio", .data = &vm_dirty_ratio, .maxlen = sizeof(vm_dirty_ratio), .mode = 0644, .proc_handler = dirty_ratio_handler, .extra1 = &zero, .extra2 = &one_hundred, }, { .procname = "dirty_writeback_centisecs", .data = &dirty_writeback_interval, .maxlen = sizeof(dirty_writeback_interval), .mode = 0644, .proc_handler = dirty_writeback_centisecs_handler, }, ...} 这些值在mm/page-writeback.c中有全局变量定义 int dirty_background_ratio = 10;int vm_dirty_ratio = 20;unsigned int dirty_writeback_interval = 5 * 100; /* centiseconds */ 通过ps -aux,我们可以看到writeback的内核进程 这实际上是一个工作队列对应的进程,在default_bdi_init()中创建(mm/backing-dev.c) static int __init default_bdi_init(void){ int err; bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_FREEZABLE | WQ_UNBOUND | WQ_SYSFS, 0); if (!bdi_wq) return -ENOMEM; err = bdi_init(&noop_backing_dev_info); return err;} 回刷进程的核心是函数wb_workfn(),通过函数wb_init()绑定。 static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi int blkcg_id, gfp_t gfp){ ... INIT_DELAYED_WORK(&wb->dwork, wb_workfn); ...} 唤醒回刷进程的操作是这样的 static void wb_wakeup(struct bdi_writeback *wb){ spin_lock_bh(&wb->work_lock); if (test_bit(WB_registered, &wb->state)) mod_delayed_work(bdi_wq, &wb->dwork, 0); spin_unlock_bh(&wb->work_lock);} 表示唤醒的回刷任务在工作队列writeback中执行,这样,就把工作队列和回刷工作绑定了,重点看看这个接口做了些什么工作 void wb_workfn(struct work_struct *work){ struct bdi_writeback *wb = container_of(to_delayed_work(work), struct bdi_writeback, dwork); long pages_written; set_worker_desc("flush-%s", dev_name(wb->bdi->dev)); current->flags |= PF_SWAPWRITE; //如果当前不是一个救援工作队列,或者当前bdi设备已注册,这是一般路径 if (likely(!current_is_workqueue_rescuer() || !test_bit(WB_registered, &wb->state))) { ------------------------(1) /* * The normal path. Keep writing back @wb until its * work_list is empty. Note that this path is also taken * if @wb is shutting down even when we're running off the * rescuer as work_list needs to be drained. */ do {从bdi的work_list取出队列里的任务,执行脏页回写 pages_written = wb_do_writeback(wb); trace_writeback_pages_written(pages_written); } while (!list_empty(&wb->work_list)); } else { -----------------------(2) /* * bdi_wq can't get enough workers and we're running off * the emergency worker. Don't hog it. Hopefully, 1024 is * enough for efficient IO. */ pages_written = writeback_inodes_wb(wb, 1024,//只提交一个work并限制写入1024个pages WB_REASON_FORKER_THREAD); trace_writeback_pages_written(pages_written); } //如果上面处理完到现在这段间隔又有了work,再次立马启动回写进程 if (!list_empty(&wb->work_list)) -----------------------(3) mod_delayed_work(bdi_wq, &wb->dwork, 0); else if (wb_has_dirty_io(wb) && dirty_writeback_interval) wb_wakeup_delayed(wb);//如果所有bdi设备上挂的dirty inode回写完,那么就重置定制器, //再过dirty_writeback_interval,即5s后再唤醒回写进程 current->flags &= ~PF_SWAPWRITE;} 正常路径,rescue workerrescue内核线程,内存紧张时创建新的工作线程可能会失败,如果内核中有会需要回收的内存,就调用wb_do_writeback进行回收 如果当前workqueue不能获得足够的worker进行处理,只提交一个work并限制写入1024个pages 这也过程代码较多,暂不去深入分析,重点关注相关的配置是如何起作用的。 (5) 触发回写方式 触发writeback的地方主要有以下几处: 5.1 主动发起 手动执行sysn命令 sync-> SYSCALL_DEFINE0(sync)-> sync_inodes_one_sb-> sync_inodes_sb-> bdi_queue_work syncfs系统调用 SYSCALL_DEFINE1(syncfs, int, fd)-> sync_filesystem-> __sync_filesystem-> sync_inodes_sb-> bdi_queue_work 直接内存回收,内存不足时调用 free_more_memory-> wakeup_flusher_threads-> __bdi_start_writeback-> bdi_queue_work 分配内存空间不足,触发回写脏页腾出内存空间 __alloc_pages_nodemask-> __alloc_pages_slowpath-> __alloc_pages_direct_reclaim-> __perform_reclaim-> try_to_free_pages-> do_try_to_free_pages-> wakeup_flusher_threads-> __bdi_start_writeback-> bdi_queue_work remount/umount操作,需要先将脏页写回 5.2 空间层面 当系统的“dirty”的内存大于某个阈值,该阈值是在总共的“可用内存”(包括free pages 和reclaimable pages)中的占比。 参数“dirty_background_ratio”(默认值10%),或者是绝对字节数“dirty_background_bytes”(默认值为0,表示生效)。两个参数只要谁先达到即可执行,此时就会交给专门负责writeback的background线程去处理。 参数“dirty_ratio”(默认值30%)和“dirty_bates”(默认值为0,表示生效),当“dirty”的内存达到这个比例或数量,进程则会停下write操作(被阻塞),先把“dirty”进行writeback。 5.3 时间层面 周期性的扫描,扫描间隔用参数:dirty_writeback_interval表示,以毫秒为单位。发现存在最近一次更新时间超过某个阈值(参数:dirty_expire_interval,单位毫秒)的pages。如果每个page都维护最近更新时间,开销会很大且扫描会很耗时,因此具体实现不会以page为粒度,而是按inode中记录的dirtying-time来计算。 (6)总结 文件缓存是一项重要的性能改进,在大多数情况下,读缓存在绝大多数情况下是有益无害的(程序可以直接从RAM中读取数据)。写缓存比较复杂,Linux内核将磁盘写入缓存,过段时间再异步将它们刷新到磁盘。这对加速磁盘I/O有很好的效果,但是当数据未写入磁盘时,丢失数据的可能性会增加。
1 正激变换器(Forward Converter)拓扑结构 正激变换器拓扑结构,如图所示: 拓扑结构分析: 输入电压 Vi 输出电压 Vo 开关组件 S 变压器 T 原边线圈圈数 Np 副边线圈圈数 Ns 整流理想二极管 D1,D2 滤波电容 C 2 正激变换器(Forward Converter)原理 正激变换器(Forward Converter)拓扑结构,如图所示: S导通(开关管导通)时: 电流由输入电压端流经变压器原边线圈与开关形成电流回路,此时变压器原边线圈两端压降为Vi 变压器原边线圈因电流流过而产生磁力线,其透过铁芯传到副边线圈,副边线圈产生感应电势 副边线圈两端感应电压Vi/n,使得理想整流二极管D1导通,电流形成回路,通过D1、输出储能电感与输出电容 副边储能电感两端固定压降VL,使得电感线圈上产生电流,此电流于电感铁芯内累积磁力线,直到开关关闭为止 S关断(开关管关断)时: 原边线圈因开关关断,原边无电流回路产生。原边线圈上产生反电动势,该反电动势与占空比D相关 变压器副边线圈因产生的感应电势极性,使得整流二极管D1关断,此时变压器能量传输截止 电感产生反电势,使得续流二极管导通,储能电感于开关关断时续流,电感上压降与输出相同 储存电感将导通时间储存于铁芯内的磁力线,透过电感上的感应线圈,由电流形式进行释放 正激变换器电压转换公式: D = 占空比 T = 周期 n = 变压器匝比 电压波形如图所示: 3 正激变换器(Forward Converter)应用举例 已知:输入电压值为 Vi 给定:变压器匝比为 n 调制:占空比为 D 得出:输出电压值为 Vo 应用举例: 应用于输入电压为100V,隔离输出电压需求为5V,隔离变压器圈比为5。求占空比需控制在多少才能使得输出电压稳定在5V? 4 正激变换器(Forward Converter)应用环境 正激变换器特点: 正激 拓扑形式 隔离降压型 压比(Vo/Vi) D/n 变压器利用率 低 功率应用范围 <300W 功率密度 低 开关管 一个(原边主动开关) 成本 低 调制方式 脉冲宽度调制(PWM) 控制芯片 UCC38C42 应用环境: 控制模块简易稳定 一般范围功率输出需求 低价格产品应用
在PLC(可编程逻辑控制器)编程领域,结构化文本和梯形图是两种最为广泛使用的编程语言。对于初学者或缺乏相关背景的技术人员来说,从梯形图入手学习PLC编程无疑是一条捷径。这是因为,无论PLC的品牌如何,其梯形图的结构都与实际电气控制回路有着惊人的相似性。接下来,我们将介绍几种常见的控制电路,帮助大家回顾并深化对这一领域的理解。 1.启动、保持与停止电路 启动、保持与停止是PLC编程中的基础功能。通过四种不同的梯形图实现方式(如图a、b、c、d所示),我们可以清晰地看到如何实现这一功能。在这些梯形图中,x0代表启动信号,X1代表停止信号。图a和图c利用Y10的常开触点实现自锁保持,而图b和图d则通过SET和RST指令实现同样的效果。 2.多地控制电路 多地控制电路允许在不同位置对同一继电器线圈进行控制。在图例中,X0和X1是一个地方的启动和停止按钮,而X2和X3则是另一个地方的启动和停止按钮。这种设计在大型设备或需要远程控制的场景中尤为实用。 3.互锁控制电路 互锁控制电路确保在多个输出线圈中,每次只能有一个接通。在图例中,X0、X1和X2是启动按钮,X3是停止按钮。通过将Y0、Y1和Y2的常闭触点分别串联到其他两个线圈的控制电路中,实现了互锁功能。 4.顺序启动控制电路 顺序启动控制电路允许按照预定的顺序启动多个设备。在图例中,Y0的常开触点串联在Y1的控制回路中,这意味着Y1的接通必须以Y0的接通为前提。这种设计在需要按照特定顺序启动设备的场景中非常有用。 5.电机正反转电路 电机正反转电路是PLC编程中的一个重要应用。虽然具体的梯形图没有在此展示,但这一电路通常涉及对电机正转和反转的控制,以及必要的互锁和保护措施。 6.集中与分散控制电路 在由多台单机组成的自动线上,集中与分散控制是一种常见的需求。在图例中,x2为选择开关,其触点作为集中控制与分散控制的联锁触点。当X2为ON时,系统处于单机分散启动控制模式;当X2为OFF时,则切换为集中总启动控制模式。在两种模式下,单机和总操作台都可以发出停止命令。 通过了解这些常见的控制电路,我们可以更好地掌握PLC编程的基础知识和实际应用技巧。希望这篇文章能为初学者提供有益的指导和帮助。