• [技术干货] 鲲鹏之开发套件DevKit
    1、关于鲲鹏1.1、鲲鹏介绍鲲鹏计算产业是基于鲲鹏处理器的基础软硬件设施、行业应用及服务,涵盖从底层硬件、基础软件到上层行业应用的全产业链条。华为作为鲲鹏计算产业的成员,聚焦计算架构创新、处理器和开源基础软件的研发,以及华为云服务,致力于推动鲲鹏生态发展。通过战略性、长周期的研发投入,吸纳全球计算产业的优秀人才和先进技术,持续推进全栈计算技术的创新发展,加快构筑面向多样性计算的全球开源体系与产业标准。基于“硬件开放、软件开源、使能伙伴、发展人才”的策略推动鲲鹏计算产业发展。1.2、鲲鹏解决方案鲲鹏全栈解决方案,主要应用在金融、互联网、运营商、政府、电力、交通等行业。其中应用使能套件BoostKit可应用于大数据、分布式存储、数据库、虚拟化ARM原生等方面。基础软件可应用于openGauss企业级开源数据库、openEuler开源操作系统。开发套件DevKit包含鲲鹏代码迁移工具、鲲鹏编译器、鲲鹏性能分析工具、动态二进制翻译工具。2、鲲鹏开发套件DevKit2.1、DevKit介绍鲲鹏开发套件DevKit提供涵盖代码开发、编译调试、云测服务、性能分析及系统诊断等各环节的开发使能工具,方便开发者快速开发出鲲鹏亲和的高性能软件,帮助开发者加速应用迁移和算力升级。同时面向全研发作业流程,提升应用迁移和调优效率,加速原生开发。鲲鹏开发套件DevKit以开发者为中心,并提升全流程开发效率。开发套件DevKit包含鲲鹏代码迁移工具、鲲鹏编译器、性能分析工具、动态二进制翻译工具等。2.2、鲲鹏代码迁移工具2.2.1、工具简介鲲鹏代码迁移工具是一款可以简化客户应用迁移到基于鲲鹏916/920的服务器的过程的工具。工具仅支持x86 Linux到Kunpeng Linux的扫描与分析,不支持Windows软件代码的扫描、分析与迁移。当客户有x86平台上源代码的软件要迁移到基于鲲鹏916/920的服务器上时,既可以使用该工具分析可迁移性和迁移投入,也可以使用该工具自动分析出需修改的代码内容,并指导用户如何修改。鲲鹏代码迁移工具既解决了客户软件迁移评估分析过程中人工分析投入大、准确率低、整体效率低下的痛点,通过该工具能够自动分析并输出指导报告;也解决了用户代码兼容性人工排查困难、迁移经验欠缺、反复依赖编译调错定位等痛点。2.2.2、应用场景软件迁移评估:自动扫描并分析软件包(非源码包)、已安装的软件,提供可迁移性评估报告。源码迁移:当用户有软件要迁移到基于鲲鹏916/920的服务器上时,可先用该工具分析源码并得到迁移修改建议。软件包重构:帮助用户重构适用于鲲鹏平台的软件安装包。专项软件迁移:使用华为提供的软件迁移模板修改、编译并产生指定软件版本的安装包,该软件包适用于鲲鹏平台。增强功能:支持x86和鲲鹏平台GCC 4.8.5~GCC 9.3.0版本32位应用向64位应用迁移的64位运行模式检查,结构体字节对齐检查、缓存行对齐检查和鲲鹏平台上的内存一致性检查。2.2.3、部署方式单机部署,即将鲲鹏代码迁移工具部署在用户的开发、测试的x86服务器或者基于鲲鹏916/920的服务器。2.3、鲲鹏性能分析工具2.3.1、工具简介鲲鹏性能分析工具由四个子工具组成,分别为:系统性能分析、Java性能分析、系统诊断和调优助手。系统性能分析是针对基于鲲鹏的服务器的性能分析工具,能收集服务器的处理器硬件、操作系统、进程/线程、函数等各层次的性能数据,分析系统性能指标,定位到系统瓶颈点及热点函数,并给出优化建议。该工具可以辅助用户快速定位和处理软件性能问题。Java性能分析是针对基于鲲鹏的服务器上运行的Java程序的性能分析和优化工具,能图形化显示Java程序的堆、线程、锁、垃圾回收等信息,收集热点函数、定位程序瓶颈点,帮助用户采取针对性优化。系统诊断是针对基于鲲鹏的服务器的性能分析工具,提供内存泄漏诊断(包括内存未释放和异常释放)、内存越界诊断、内存消耗信息分析展示、OOM诊断能力、网络丢包等,帮助用户识别出源代码中内存使用的问题点,提升程序的可靠性,工具还支持压测系统,如:网络IO诊断,评估系统最大性能。调优助手是针对基于鲲鹏的服务器的调优工具,能系统化组织性能指标,引导用户分析性能瓶颈,实现快速调优。2.3.2、应用场景客户软件在基于鲲鹏的服务器上运行遇到性能问题时,可用系统性能分析来快速分析和定位。系统性能分析工具将采集系统如下数据:系统软硬件配置和运行信息,例如:CPU类型、内存部署槽位、Kernel版本、内核参数、文件系统、系统运行日志参数等。系统的CPU、内存、存储IO、磁盘IO等性能指标。处理器PMU、SPE的性能数据。处理器访问Cache/内存的次数、带宽、吞吐率等。系统内核进行CPU资源调度、IO操作等数据。进程/线程的CPU、内存、存储IO、上下文切换、系统调用等数据;进程命令行信息,包括:进程名、进程参数。系统的热点函数及其调用栈;热点函数归属的程序/动态库(包含绝对路径);热点函数的汇编指令和热点指令;热点函数所对应的源代码(需要用户自行提供)。2.3.3、部署方式当前版本支持灵活部署,即将系统性能分析所有组件部署在一台服务器上、不同服务器上及混合部署,完成性能数据采集和分析。2.4、鲲鹏开发套件插件工具(VSCode)2.4.1、工具简介鲲鹏开发套件插件工具是基于Visual Studio Code提供给开发者面向鲲鹏平台进行应用软件开发、迁移、编译调试、性能调优等一系列端到端工具,即插即用。一体化呈现代码迁移插件、鲲鹏开发框架插件、编译插件及性能分析插件的完整开发套件。鲲鹏开发套件插件工具是一个工具集,由多个插件组成,支持IDE前端界面,支持一键式安装后端,代码编辑体验增强,自动检测安装鲲鹏编译器,编译调试,用例可视化,编码辅助,工程分析扫描。用户可以通过安装Kunpeng DevKit插件直接将四个插件都安装好,也可以单独选择个别插件安装使用。2.4.2、代码迁移插件鲲鹏代码迁移插件作为客户端调用服务端的功能,完成扫描迁移任务,可以对待迁移软件进行快速扫描分析,并提供专业的代码迁移指导,极大简化客户应用迁移到鲲鹏平台的过程。当客户有软件需要迁移到鲲鹏平台上时,可先用该工具分析可迁移性和迁移投入,以解决客户软件迁移评估中分析投入大、准确率低、整体效率低下的痛点。代码迁移工具支持五个功能特性:软件迁移评估:自动扫描并分析软件包(非源码包)、已安装的软件,提供可迁移性评估报告。源码迁移:能够自动检查并分析出用户源码、C/C++/ASM/Fortran/解释型语言/汇编软件构建工程文件、C/C++/ASM/Fortran/解释型语言/汇编软件构建工程文件使用的链接库、x86汇编代码中需要修改的内容,并给出修改指导,以解决用户代码兼容性排查困难、迁移经验欠缺、反复依赖编译调错定位等痛点。软件包重构:通过分析x86平台软件包(RPM格式、DEB格式)的软件构成关系及硬件依赖性,重构适用于鲲鹏平台的软件包。专项软件迁移:基于鲲鹏解决方案的软件迁移模板,进行自动化迁移修改、编译、构建软件包,帮助用户快速迁移软件。2.4.3、性能分析插件鲲鹏开发套件是Visual Studio Code的一款扩展工具,通常将此类工具称作集成开发环境(IDE)插件。鲲鹏性能分析插件是其中一个子工具,作为客户端调用服务端的功能。鲲鹏性能分析工具由四个子工具组成,分别为:系统性能分析、Java性能分析、系统诊断和调优助手。系统性能分析是针对基于鲲鹏的服务器的性能分析工具,能收集服务器的处理器硬件、操作系统、进程/线程、函数等各层次的性能数据,分析系统性能指标,定位到系统瓶颈点及热点函数,并给出优化建议。该工具可以辅助用户快速定位和处理软件性能问题。Java性能分析是针对基于鲲鹏的服务器上运行的Java程序的性能分析和优化工具,能图形化显示Java程序的堆、线程、锁、垃圾回收等信息,收集热点函数、定位程序瓶颈点,帮助用户采取针对性优化。系统诊断是针对基于鲲鹏的服务器的性能分析工具,提供内存泄漏诊断(包括内存未释放和异常释放)、内存越界诊断、内存消耗信息分析展示、OOM诊断能力,帮助用户识别出源代码中内存使用的问题点,提升程序的可靠性;压测网络,获得网络最大能力,为网络IO性能优化提供基础参考数据;诊断网络,定位网络疑难问题,解决因网络配置和异常而导致的网络IO性能问题;压测存储IO,获得存储设备最大能力,包括:吞吐量、IOPS、时延等,并以此评估存储能力,为存储IO性能优化提供基础参考数据。2.5、二进制动态翻译工具2.5.1、相关概念ExaGear是一款二进制指令动态翻译软件,运行在ARM64服务器上,通过将x86的指令在运行时翻译为ARM64指令并执行,使得绝大部分Linux on x86应用无需重新编译就可运行在ARM64服务器上,实现低成本、快速迁移Linux on x86应用到ARM64服务器。2.5.2、关键特性支持多种部署方式:支持在物理机、虚拟机、容器等平台上部署;部署简单:一键式快速安装,x86应用部署和运行与迁移前保持一致;支持多版本Linux OS:目前支持CentOS 7、CentOS 8、Ubuntu18、Ubuntu20、OpenEuler 20.03,并且根据用户需求,未来可定制支持更多Linux OS发行;低损耗: 大多数场景的应用,翻译损耗在20%以内。3、结束语对鲲鹏开发套件有兴趣的同学可参考如下链接进行进一步学习。相关链接:https://support.huaweicloud.com/kunpengdevps/kunpengdevps.html
  • [交流吐槽] 面试三连:什么是死锁?怎么排查死锁?怎么避免死锁?
    01 死锁的概念在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。那么,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。举个例子,小林拿了小美房间的钥匙,而小林在自己的房间里,小美拿了小林房间的钥匙,而小美也在自己的房间里。如果小林要从自己的房间里出去,必须拿到小美手中的钥匙,但是小美要出去,又必须拿到小林手中的钥匙,这就形成了死锁。死锁只有同时满足以下四个条件才会发生:互斥条件;持有并等待条件;不可剥夺条件;环路等待条件;1.1 互斥条件互斥条件是指​多个线程不能同时使用同一个资源​。比如下图,如果线程 A 已经持有的资源,不能再同时被线程 B 持有,如果线程 B 请求获取线程 A 已经占用的资源,那线程 B 只能等待,直到线程 A 释放了资源。1.2 持有并等待条件持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是​线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1​。1.3 不可剥夺条件不可剥夺条件是指,当线程已经持有了资源 ,​在自己使用完之前不能被其他线程获取​,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。1.4 环路等待条件环路等待条件指都是,在死锁发生的时候,​两个线程获取资源的顺序构成了环形链​。比如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环形图02 模拟死锁问题的产生Talk is cheap. Show me the code.下面,我们用代码来模拟死锁问题的产生。首先,我们先创建 2 个线程,分别为线程 A 和 线程 B,然后有两个互斥锁,分别是 mutex_A 和 mutex_B,代码如下:pthread_mutex_t mutex_A = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_t mutex_B = PTHREAD_MUTEX_INITIALIZER;int main(){    pthread_t tidA, tidB;    //创建两个线程    pthread_create(&tidA, NULL, threadA_proc, NULL);    pthread_create(&tidB, NULL, threadB_proc, NULL);    pthread_join(tidA, NULL);    pthread_join(tidB, NULL);    printf("exit\n");    return 0;}接下来,我们看下线程 A 函数做了什么。//线程函数 Avoid *threadA_proc(void *data){    printf("thread A waiting get ResourceA \n");    pthread_mutex_lock(&mutex_A);    printf("thread A got ResourceA \n");    sleep(1);    printf("thread A waiting get ResourceB \n");    pthread_mutex_lock(&mutex_B);    printf("thread A got ResourceB \n");    pthread_mutex_unlock(&mutex_B);    pthread_mutex_unlock(&mutex_A);    return (void *)0;}可以看到,线程 A 函数的过程:先获取互斥锁 A,然后睡眠 1 秒;再获取互斥锁 B,然后释放互斥锁 B;最后释放互斥锁 A;//线程函数 Bvoid *threadB_proc(void *data){    printf("thread B waiting get ResourceB \n");    pthread_mutex_lock(&mutex_B);    printf("thread B got ResourceB \n");    sleep(1);    printf("thread B waiting  get ResourceA \n");    pthread_mutex_lock(&mutex_A);    printf("thread B got ResourceA \n");    pthread_mutex_unlock(&mutex_A);    pthread_mutex_unlock(&mutex_B);    return (void *)0;}可以看到,线程 B 函数的过程:先获取互斥锁 B,然后睡眠 1 秒;再获取互斥锁 A,然后释放互斥锁 A;最后释放互斥锁 B;然后,我们运行这个程序,运行结果如下:thread B waiting get ResourceB thread B got ResourceB thread A waiting get ResourceA thread A got ResourceA thread B waiting get ResourceA thread A waiting get ResourceB // 阻塞中。。。可以看到线程 B 在等待互斥锁 A 的释放,线程 A 在等待互斥锁 B 的释放,双方都在等待对方资源的释放,很明显,产生了死锁问题。03 利用工具排查死锁问题如果你想排查你的 Java 程序是否死锁,则可以使用 jstack 工具,它是 jdk 自带的线程堆栈分析工具。由于小林的死锁代码例子是 C 写的,在 Linux 下,我们可以使用 pstack + gdb 工具来定位死锁问题。pstack 命令可以显示每个线程的栈跟踪信息(函数调用过程),它的使用方式也很简单,只需要 pstack <pid> 就可以了。那么,在定位死锁问题时,我们可以多次执行 pstack 命令查看线程的函数调用过程,多次对比结果,确认哪几个线程一直没有变化,且是因为在等待锁,那么大概率是由于死锁问题导致的。我用 pstack 输出了我前面模拟死锁问题的进程的所有线程的情况,我多次执行命令后,其结果都一样可以看到,Thread 2 和 Thread 3 一直阻塞获取锁(pthread_mutex_lock)的过程,而且 pstack 多次输出信息都没有变化,那么可能大概率发生了死锁。但是,还不能够确认这两个线程是在互相等待对方的锁的释放,因为我们看不到它们是等在哪个锁对象,于是我们可以使用 gdb 工具进一步确认。整个 gdb 调试过程,如下:// gdb 命令$ gdb -p 87746// 打印所有的线程信息(gdb) info thread  3 Thread 0x7f60a610a700 (LWP 87747)  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0  2 Thread 0x7f60a5709700 (LWP 87748)  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0* 1 Thread 0x7f60a610c700 (LWP 87746)  0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0//最左边的 * 表示 gdb 锁定的线程,切换到第二个线程去查看// 切换到第2个线程(gdb) thread 2[Switching to thread 2 (Thread 0x7f60a5709700 (LWP 87748))]#0  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0 // bt 可以打印函数堆栈,却无法看到函数参数,跟 pstack 命令一样 (gdb) bt#0  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0#1  0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0#2  0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0#3  0x0000000000400792 in threadB_proc (data=0x0) at dead_lock.c:25#4  0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0#5  0x00000037206f4bfd in clone () from /lib64/libc.so.6// 打印第三帧信息,每次函数调用都会有压栈的过程,而 frame 则记录栈中的帧信息(gdb) frame 3#3  0x0000000000400792 in threadB_proc (data=0x0) at dead_lock.c:2527    printf("thread B waiting get ResourceA \n");28    pthread_mutex_lock(&mutex_A);// 打印mutex_A的值 ,  __owner表示gdb中标示线程的值,即LWP(gdb) p mutex_A$1 = {__data = {__lock = 2, __count = 0, __owner = 87747, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}},   __size = "\002\000\000\000\000\000\000\000\303V\001\000\001", '\000' <repeats 26 times>, __align = 2}// 打印mutex_B的值 ,  __owner表示gdb中标示线程的值,即LWP(gdb) p mutex_B$2 = {__data = {__lock = 2, __count = 0, __owner = 87748, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}},   __size = "\002\000\000\000\000\000\000\000\304V\001\000\001", '\000' <repeats 26 times>, __align = 2}我来解释下,上面的调试过程:通过 info thread 打印了所有的线程信息,可以看到有 3 个 线程,一个是主线程(LWP 87746),另外两个都是我们自己创建的线程(LWP 87747 和 87748);通过 thread 2,将切换到第2个线程(LWP 87748);通过 bt,打印线程的调用栈信息,可以看到有 threadB_proc 函数,说明这个是线程B函数,也就说 LWP 87748 是线程 B;通过 frame 3,打印调用栈中的第三个帧的信息,可以看到线程 B 函数,在获取互斥锁 A 的时候阻塞了;通过 p mutex_A,打印互斥锁 A 对象信息,可以看到它被 LWP 为 87747(线程 A) 的线程持有者;通过 p mutex_B,打印互斥锁 A 对象信息,可以看到他被 LWP 为 87748 (线程 B) 的线程持有者;因为线程 B 在等待线程 A 所持有的 mutex_A, 而同时线程 A 又在等待线程 B 所拥有的mutex_B, 所以可以断定该程序发生了死锁04 避免死锁问题的发生前面我们提到,产生死锁的四个必要条件是:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。那么避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。那什么是资源有序分配法呢?线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。我们使用资源有序分配法的方式来修改前面发生死锁的代码,我们可以不改动线程 A 的代码。我们先要清楚线程 A 获取资源的顺序,它是先获取互斥锁 A,然后获取互斥锁 B。所以我们只需将线程 B 改成以相同顺序地获取资源,就可以打破死锁了。总结简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。所以要避免死锁问题,就是要破坏其中一个条件即可,最常用的方法就是使用资源有序分配法来破坏环路等待条件。
  • [知识分享] 常遇到读多写少,教你用ReadWriteLock实现一个通用的缓存中心
    >摘要:本文我们就来说说使用ReadWriteLock如何实现一个通用的缓存中心。 本文分享自华为云社区《[【高并发】原来ReadWriteLock也能开发高性能缓存,看完我也能和面试官好好聊聊了!](https://bbs.huaweicloud.com/blogs/357370?utm_source=csdn&utm_medium=bbs-ex&utm_campaign=other&utm_content=content)》,作者: 冰 河。 在实际工作中,有一种非常普遍的并发场景:那就是读多写少的场景。在这种场景下,为了优化程序的性能,我们经常使用缓存来提高应用的访问性能。因为缓存非常适合使用在读多写少的场景中。而在并发场景中,Java SDK中提供了ReadWriteLock来满足读多写少的场景。本文我们就来说说使用ReadWriteLock如何实现一个通用的缓存中心。 本文涉及的知识点有: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20226/2/1654130615460251400.png) # 读写锁 说起读写锁,相信小伙伴们并不陌生。总体来说,读写锁需要遵循以下原则: - 一个共享变量允许同时被多个读线程读取到。 - 一个共享变量在同一时刻只能被一个写线程进行写操作。 - 一个共享变量在被写线程执行写操作时,此时这个共享变量不能被读线程执行读操作。 **这里,需要小伙伴们注意的是:读写锁和互斥锁的一个重要的区别就是:读写锁允许多个线程同时读共享变量,而互斥锁不允许。所以,在高并发场景下,读写锁的性能要高于互斥锁。但是,读写锁的写操作是互斥的,也就是说,使用读写锁时,一个共享变量在被写线程执行写操作时,此时这个共享变量不能被读线程执行读操作。** 读写锁支持公平模式和非公平模式,具体是在ReentrantReadWriteLock的构造方法中传递一个boolean类型的变量来控制。 ``` public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } ``` **另外,需要注意的一点是:在读写锁中,读锁调用newCondition()会抛出UnsupportedOperationException异常,也就是说:读锁不支持条件变量。** # 缓存实现 这里,我们使用ReadWriteLock快速实现一个缓存的通用工具类,总体代码如下所示。 ``` public class ReadWriteLockCache { private final Map m = new HashMap(); private final ReadWriteLock rwl = new ReentrantReadWriteLock(); // 读锁 private final Lock r = rwl.readLock(); // 写锁 private final Lock w = rwl.writeLock(); // 读缓存 public V get(K key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } // 写缓存 public V put(K key, V value) { w.lock(); try { return m.put(key, value); } finally { w.unlock(); } } } ``` 可以看到,在ReadWriteLockCache中,我们定义了两个泛型类型,K代表缓存的Key,V代表缓存的value。在ReadWriteLockCache类的内部,我们使用Map来缓存相应的数据,小伙伴都都知道HashMap并不是线程安全的类,所以,这里使用了读写锁来保证线程的安全性,例如,我们在get()方法中使用了读锁,get()方法可以被多个线程同时执行读操作;put()方法内部使用写锁,也就是说,put()方法在同一时刻只能有一个线程对缓存进行写操作。 **这里需要注意的是**:无论是读锁还是写锁,锁的释放操作都需要放到finally{}代码块中。 在以往的经验中,有两种向缓存中加载数据的方式,**一种是:项目启动时,将数据全量加载到缓存中,一种是在项目运行期间,按需加载所需要的缓存数据。** ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20226/2/1654130679195164652.png) 接下来,我们就分别来看看全量加载缓存和按需加载缓存的方式。 # 全量加载缓存 全量加载缓存相对来说比较简单,就是在项目启动的时候,将数据一次性加载到缓存中,这种情况适用于缓存数据量不大,数据变动不频繁的场景,例如:可以缓存一些系统中的数据字典等信息。整个缓存加载的大体流程如下所示。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20226/2/1654130694366459733.png) 将数据全量加载到缓存后,后续就可以直接从缓存中读取相应的数据了。 全量加载缓存的代码实现比较简单,这里,我就直接使用如下代码进行演示。 ``` public class ReadWriteLockCache { private final Map m = new HashMap(); private final ReadWriteLock rwl = new ReentrantReadWriteLock(); // 读锁 private final Lock r = rwl.readLock(); // 写锁 private final Lock w = rwl.writeLock(); public ReadWriteLockCache(){ //查询数据库 List> list = .....; if(!CollectionUtils.isEmpty(list)){ list.parallelStream().forEach((f) ->{ m.put(f.getK(), f.getV); }); } } // 读缓存 public V get(K key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } // 写缓存 public V put(K key, V value) { w.lock(); try { return m.put(key, value); } finally { w.unlock(); } } } ``` # 按需加载缓存 按需加载缓存也可以叫作懒加载,就是说:需要加载的时候才会将数据加载到缓存。具体来说:就是程序启动的时候,不会将数据加载到缓存,当运行时,需要查询某些数据,首先检测缓存中是否存在需要的数据,如果存在,则直接读取缓存中的数据,如果不存在,则到数据库中查询数据,并将数据写入缓存。后续的读取操作,因为缓存中已经存在了相应的数据,直接返回缓存的数据即可。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20226/2/1654130717998571646.png) 这种查询缓存的方式适用于大多数缓存数据的场景。 我们可以使用如下代码来表示按需查询缓存的业务。 ``` class ReadWriteLockCache { private final Map m = new HashMap(); private final ReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock r = rwl.readLock(); private final Lock w = rwl.writeLock(); V get(K key) { V v = null; //读缓存 r.lock(); try { v = m.get(key); } finally{ r.unlock(); } //缓存中存在,返回 if(v != null) { return v; } //缓存中不存在,查询数据库 w.lock(); try { //再次验证缓存中是否存在数据 v = m.get(key); if(v == null){ //查询数据库 v=从数据库中查询出来的数据 m.put(key, v); } } finally{ w.unlock(); } return v; } } ``` 这里,在get()方法中,首先从缓存中读取数据,此时,我们对查询缓存的操作添加了读锁,查询返回后,进行解锁操作。判断缓存中返回的数据是否为空,不为空,则直接返回数据;如果为空,则获取写锁,之后再次从缓存中读取数据,如果缓存中不存在数据,则查询数据库,将结果数据写入缓存,释放写锁。最终返回结果数据。 **这里,有小伙伴可能会问:为啥程序都已经添加写锁了,在写锁内部为啥还要查询一次缓存呢?** 这是因为在高并发的场景下,可能会存在多个线程来竞争写锁的现象。例如:第一次执行get()方法时,缓存中的数据为空。如果此时有三个线程同时调用get()方法,同时运行到 w.lock()代码处,由于写锁的排他性。此时只有一个线程会获取到写锁,其他两个线程则阻塞在w.lock()处。获取到写锁的线程继续往下执行查询数据库,将数据写入缓存,之后释放写锁。 此时,另外两个线程竞争写锁,某个线程会获取到锁,继续往下执行,如果在w.lock()后没有v = m.get(key); 再次查询缓存的数据,则这个线程会直接查询数据库,将数据写入缓存后释放写锁。最后一个线程同样会按照这个流程执行。 这里,实际上第一个线程已经查询过数据库,并且将数据写入缓存了,其他两个线程就没必要再次查询数据库了,直接从缓存中查询出相应的数据即可。所以,在w.lock()后添加v = m.get(key); 再次查询缓存的数据,能够有效的减少高并发场景下重复查询数据库的问题,提升系统的性能。 # 读写锁的升降级 **关于锁的升降级,小伙伴们需要注意的是:在ReadWriteLock中,锁是不支持升级的,因为读锁还未释放时,此时获取写锁,就会导致写锁永久等待,相应的线程也会被阻塞而无法唤醒。** 虽然不支持锁升级,但是ReadWriteLock支持锁降级,例如,我们来看看官方的ReentrantReadWriteLock示例,如下所示。 ``` class CachedData { Object data; volatile boolean cacheValid; final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); try { // Recheck state because another thread might have // acquired write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); } finally { rwl.writeLock().unlock(); // Unlock write, still hold read } } try { use(data); } finally { rwl.readLock().unlock(); } } }} ``` # 数据同步问题 首先,这里说的数据同步指的是数据源和数据缓存之间的数据同步,说的再直接一点,就是数据库和缓存之间的数据同步。 这里,我们可以采取三种方案来解决数据同步的问题,如下图所示 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20226/2/1654130769716991175.png) ## 超时机制 这个比较好理解,就是在向缓存写入数据的时候,给一个超时时间,当缓存超时后,缓存的数据会自动从缓存中移除,此时程序再次访问缓存时,由于缓存中不存在相应的数据,查询数据库得到数据后,再将数据写入缓存。采用这种方案需要注意缓存的穿透问题。 ## 定时更新缓存 这种方案是超时机制的增强版,在向缓存中写入数据的时候,同样给一个超时时间。与超时机制不同的是,在程序后台单独启动一个线程,定时查询数据库中的数据,然后将数据写入缓存中,这样能够在一定程度上避免缓存的穿透问题。 ## 实时更新缓存 这种方案能够做到数据库中的数据与缓存的数据是实时同步的,可以使用阿里开源的Canal框架实现MySQL数据库与缓存数据的实时同步。
  • [问题求助] 【AscendCL】是否有python版单线程多stream的demo?
    我们有场景需要多模型推理,因此考虑不同模型并行推理。但目前python版的多线程多stream并不能实现并行;而且,acl也不支持多进程调用。查阅文档后发现stream可以并行运行,那么是否有相关的demo?
  • [技术干货] Linux:驱动程序如何发送【信号】给应用程序?
    作  者:道哥,10+年嵌入式开发老兵,专注于:C/C++、嵌入式、Linux。目录kill 命令和信号使用 kill 命令发送信号多线程中的信号信号注册和处理函数驱动程序代码示例:发送信号功能需求驱动程序代码驱动模块 Makefile编译和加载应用程序代码示例:接收信号注册信号处理函数测试验证别人的经验,我们的阶梯!大家好,我是道哥,今天我为大伙儿解说的技术知识点是:【驱动层中,如何发送信号给应用程序】。在上一篇文章中,我们讨论的是:在应用层如何发送指令来控制驱动层的 GPIOLinux驱动实践:如何编写【 GPIO 】设备的驱动程序?。控制的方向是从应用层到驱动层:那么,如果想让程序的执行路径从下往上,也就是从驱动层传递到应用层,应该如何实现呢?最容易、最简单的方式,就是通过发送信号!这篇文章继续以完整的代码实例来演示如何实现这个功能。kill 命令和信号使用 kill 命令发送信号关于 Linux 操作系统的信号,每位程序员都知道这个指令:使用 kill 工具来“杀死”一个进程:$ kill -9 <进程的 PID>这个指令的功能是:向指定的某个进程发送一个信号 9,这个信号的默认功能是:是停止进程。虽然在应用程序中没有主动处理这个信号,但是操作系统默认的处理动作是终止应用程序的执行。除了发送信号 9,kill 命令还可以发送其他的任意信号。在 Linux 系统中,所有的信号都使用一个整型数值来表示,可以打开文件 /usr/include/x86_64-linux-gnu/bits/signum.h(你的系统中可能位于其它的目录) 查看一下,比较常见的几个信号是:Signals.  #define SIGINT 2 Interrupt (ANSI).  #define SIGKILL 9 Kill, unblockable (POSIX).  #define SIGUSR1 10 User-defined signal 1 (POSIX).  #define SIGSEGV 11 Segmentation violation (ANSI).  #define SIGUSR2 12 User-defined signal 2 (POSIX).  ......#define SIGSYS 31 Bad system call.  #define SIGUNUSED 31#define _NSIG 65 Biggest signal number + 1     (including real-time signals).  These are the hard limits of the kernel.  These values should not beused directly at user level.  #define __SIGRTMIN 32#define __SIGRTMAX (_NSIG - 1)信号 9 对应着 SIGKILL,而信号11(SIGSEGV)就是最令人讨厌的Segmentfault!这里还有一个地方需要注意一下:实时信号和非实时信号,它俩的主要区别是:1. 非实时信号:操作系统不确保应用程序一定能接收到(即:信号可能会丢失);2. 实时信号:操作系统确保应用程序一定能接收到;如果我们的程序设计,通过信号机制来完成一些功能,那么为了确保信号不会丢失,肯定是使用实时信号的。从文件 signum.h 中可以看到,实时信号从 __SIGRTMIN(数值:32) 开始。多线程中的信号我们在编写应用程序时,虽然没有接收并处理 SIGKILL 这个信号,但是一旦别人发送了这个信号,我们的程序就被操作系统停止掉了,这是默认的动作。那么,在应用程序中,应该可以主动声明接收并处理指定的信号,下面就来写一个最简单的实例。在一个应用程序中,可能存在多个线程;当有一个信号发送给此进程时,所有的线程都可能接收到,但是只能有一个线程来处理;在这个示例中,只有一个主线程来接收并处理信号;信号注册和处理函数按照惯例,所有应用程序文件都创建在 ~/tmp/App 目录中。这个示例程序接收的信号是 SIGUSR1 和 SIGUSR2,也就是数值 10 和 12。编译、执行:$ gcc app_handle_signal.c -o app_handle_signal$ ./app_handle_signal此时,应用程序开始执行,等待接收信号。在另一个终端中,使用kill指令来发送信号SIGUSR1或者 SIGUSR2。kill 发送信号,需要知道应用程序的 PID,可以通过指令: ps -au | grep app_handle_signal 来查看。其中的15428就是进程的 PID。执行发送信号SIGUSR1指令:$ kill -10 15428此时,在应用程序的终端窗口中,就能看到下面的打印信息:说明应用程序接收到了 SIGUSR1 这个信号!注意:我们是使用kill命令来发送信号的,kill 也是一个独立的进程,程序的执行路径如下:在这个执行路径中,我们可控的部分是应用层,至于操作系统是如何接收kill的操作,然后如何发送信号给 app_handle_signal 进程的,我们不得而知。下面就继续通过示例代码来看一下如何在驱动层主动发送信号。驱动程序代码示例:发送信号功能需求在刚才的简单示例中,可以得出下面这些信息:1. 信号发送方:必须知道向谁[PID]发送信号,发送哪个信号;2. 信号接收方:必须定义信号处理函数,并且向操作系统注册:接收哪些信号;发送方当然就是驱动程序了,在示例代码中,继续使用 SIGUSR1 信号来测试。那么,驱动程序如何才能知道应用程序的PID呢?可以让应用程序通过oictl函数,把自己的PID主动告诉驱动程序:驱动程序这里的示例代码,是在上一篇文章的基础上修改的,改动部分的内容,使用宏定义 MY_SIGNAL_ENABLE 控制起来,方便查看和比较。以下所有操作的工作目录,都是与上一篇文章相同的,即:~/tmp/linux-4.15/drivers/。这里大部分的代码,在上一篇文章中已经描述的比较清楚了,这里把重点关注放在这两个函数上:gpio_ioctl 和 send_signal。(1)函数 gpio_ioctl当应用程序调用 ioctl() 的时候,驱动程序中的 gpio_ioctl 就会被调用。这里定义一个简单的协议:当应用程序调用参数中 cmd 为 100 的时候,就表示用来告诉驱动程序自己的 PID。驱动程序定义了一个全局变量 g_pid,用来保存应用程序传入的参数PID。需要调用函数 copy_from_user(&g_pid, pArg, sizeof(int)),把用户空间的参数复制到内核空间中;成功取得PID之后,就调用函数 send_signal 向应用程序发送信号。这里仅仅是用于演示目的,在实际的项目中,可能会根据接收到硬件触发之后再发送信号。(2)函数 send_signal这个函数主要做了3件事情:构造一个信号结构体变量:struct siginfo info;通过应用程序传入的 PID,获取任务信息:pid_task(find_vpid(g_pid), PIDTYPE_PID);发送信号:send_sig_info(sig_no, &info, my_task);驱动模块 Makefile$ touch Makefile内容如下:编译驱动模块$ make得到驱动程序: my_driver_signal.ko 。加载驱动模块$ sudo insmod my_driver_signal.ko通过 dmesg 指令来查看驱动模块的打印信息:因为示例代码是在上一篇GPIO的基础上修改的,因此创建的设备节点文件,与上篇文章是一样的:应用程序代码示例:接收信号注册信号处理函数应用程序仍然放在 ~/tmp/App/ 目录下。$ mkdir ~/tmp/App/app_mysignal$ cd ~/tmp/App/app_mysignal$ touch mysignal.c文件内容如下:可以看到,应用程序主要做了两件事情:(1)首先通过函数 sigaction() 向操作系统注册了信号 SIGUSR1 和 SIGUSR2,它俩的信号处理函数是同一个:signal_handler()。除了 sigaction 函数,应用程序还可以使用 signal 函数来注册信号处理函数;(2)然后通过 ioctl(fd, 100, &pid); 向驱动程序设置自己的 PID。编译应用程序:$ gcc mysignal.c -o mysignal执行应用程序:$ sudo ./mysignal根据刚才驱动程序的代码,当驱动程序接收到设置PID的命令之后,会立刻发送两个信号:先来看一下 dmesg 中驱动程序的打印信息:可以看到:驱动把这两个信号(10 和 12),发送给了应用程序(PID=6259)。应用程序的输出信息如下:可以看到:应用程序接收到信号 10 和 12,并且正确打印出信号中携带的一些信息!
  • [技术干货] 中断处理中的【工作队列】 workqueue 是什么鬼?
    目录· 工作队列是什么· 驱动程序· 编译、测试别人的经验,我们的阶梯!大家好,我是道哥,今天我为大伙儿解说的技术知识点是:【中断处理中的下半部分机制-工作队列】。在刚开始介绍中断处理的时候,曾经贴出下面这张图:图中描述了中断处理中的下半部分都有哪些机制,以及如何根据实际的业务场景、限制条件来进行选择。可以看出:这些不同的实现之间,有些是重复的,或者是相互取代的关系。也正因为此,它们之间的使用方式几乎是大同小异,至少是在API接口函数的使用方式上,从使用这的角度来看,都是非常类似的。这篇文章,我们就通过实际的代码操作,来演示一下工作队列(workqueue)的使用方式。工作队列是什么工作队列是Linux操作系统中,进行中断下半部分处理的重要方式!从名称上可以猜到:一个工作队列就好像业务层常用的消息队列一样,里面存放着很多的工作项等待着被处理。工作队列中有两个重要的结构体:工作队列(workqueue_struct) 和 工作项(work_struct):struct workqueue_struct {    struct list_head        pwqs;            WR: all pwqs of this wq    struct list_head        list;            PR: list of all workqueues           ...           char                    name[WQ_NAME_LEN];  I: workqueue name           ...           hot fields used during command issue, aligned to cacheline           unsigned int            flags ____cacheline_aligned;  WQ: WQ_* flags           struct pool_workqueue __percpu *cpu_pwqs;  I: per-cpu pwqs           struct pool_workqueue __rcu *numa_pwq_tbl[];  PWR: unbound pwqs indexed by node      };struct work_struct {           atomic_long_t data;           struct list_head entry;           work_func_t func;   // 指向处理函数#ifdef CONFIG_LOCKDEP                                                                                             struct lockdep_map lockdep_map;#endif};在内核中,工作队列中的所有工作项,是通过链表串在一起的,并且等待着操作系统中的某个线程挨个取出来处理。这些线程,可以是由驱动程序通过 kthread_create 创建的线程,也可以是由操作系统预先就创建好的线程。这里就涉及到一个取舍的问题了。如果我们的处理函数很简单,那么就没有必要创建一个单独的线程来处理了。原因有二:1.创建一个内核线程是很耗费资源的,如果函数很简单,很快执行结束之后再关闭线程,太划不来了,得不偿失;2.如果每一个驱动程序编写者都毫无节制地创建内核线程,那么内核中将会存在大量不必要的线程,当然了本质上还是系统资源消耗和执行效率的问题;为了避免这种情况,于是操作系统就为我们预先创建好一些工作队列和内核线程。我们只需要把需要处理的工作项,直接添加到这些预先创建好的工作队列中就可以了,它们就会被相应的内核线程取出来处理。例如下面这些工作队列,就是内核默认创建的(include/linux/workqueue.h):* System-wide workqueues which are always present.** system_wq is the one used by schedule[_delayed]_work[_on]().* Multi-CPU multi-threaded.  There are users which expect relatively* short queue flush time.  Don't queue works which can run for too* long.** system_highpri_wq is similar to system_wq but for work items which* require WQ_HIGHPRI.** system_long_wq is similar to system_wq but may host long running* works.  Queue flushing might take relatively long.** system_unbound_wq is unbound workqueue.  Workers are not bound to* any specific CPU, not concurrency managed, and all queued works are* executed immediately as long as max_active limit is not reached and* resources are available.** system_freezable_wq is equivalent to system_wq except that it's* freezable.** *_power_efficient_wq are inclined towards saving power and converted* into WQ_UNBOUND variants if 'wq_power_efficient' is enabled; otherwise,* they are same as their non-power-efficient counterparts - e.g.* system_power_efficient_wq is identical to system_wq if* 'wq_power_efficient' is disabled.  See WQ_POWER_EFFICIENT for more info.extern struct workqueue_struct *system_wq;extern struct workqueue_struct *system_highpri_wq;extern struct workqueue_struct *system_long_wq;extern struct workqueue_struct *system_unbound_wq;extern struct workqueue_struct *system_freezable_wq;extern struct workqueue_struct *system_power_efficient_wq;extern struct workqueue_struct *system_freezable_power_efficient_wq;以上这些默认工作队列的创建代码是(kernel/workqueue.c):int __init workqueue_init_early(void){    ...        system_wq = alloc_workqueue("events", 0, 0);           system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);                                     system_long_wq = alloc_workqueue("events_long", 0, 0);           system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,                                           WQ_UNBOUND_MAX_ACTIVE);           system_freezable_wq = alloc_workqueue("events_freezable",                                             WQ_FREEZABLE, 0);           system_power_efficient_wq = alloc_workqueue("events_power_efficient",                                             WQ_POWER_EFFICIENT, 0);           system_freezable_power_efficient_wq = alloc_workqueue("events_freezable_power_efficient",                                             WQ_FREEZABLE | WQ_POWER_EFFICIENT,                                             0);     ...}此外,由于工作队列 system_wq 被使用的频率很高,于是内核就封装了一个简单的函数(schedule_work)给我们使用:/*** schedule_work - put work task in global workqueue* @work: job to be done** Returns %false if @work was already on the kernel-global workqueue and* %true otherwise.** This puts a job in the kernel-global workqueue if it was not already* queued and leaves it in the same position on the kernel-global* workqueue otherwise.static inline bool schedule_work(struct work_struct *work){       return queue_work(system_wq, work);}当然了,任何事情有利就有弊!由于内核默认创建的工作队列,是被所有的驱动程序共享的。如果所有的驱动程序都把等待处理的工作项委托给它们来处理,那么就会导致某个工作队列中过于拥挤。根据先来后到的原则,工作队列中后加入的工作项,就可能因为前面工作项的处理函数执行的时间太长,从而导致时效性无法保证。因此,这里存在一个系统平衡的问题。关于工作队列的基本知识点就介绍到这里,下面来实际操作验证一下。驱动程序之前的几篇文章,在驱动程序中测试中断处理的操作流程都是一样的,因此这里就不在操作流程上进行赘述了。这里直接给出驱动程序的全貌代码,然后查看 dmesg 的输出信息。创建驱动程序源文件和 Makefile:$ cd tmp/linux-4.15/drivers$ mkdir my_driver_interrupt_wq$ touch my_driver_interrupt_wq.c$ touch Makefile示例代码全貌测试场景是:加载驱动模块之后,如果监测到键盘上的ESC键被按下,那么就往内核默认的工作队列system_wq中增加一个工作项,然后观察该工作项对应的处理函数是否被调用。#include <linux/kernel.h>#include <linux/module.h>#include <linux/interrupt.h>static int irq;static char * devname;static struct work_struct mywork;// 接收驱动模块加载时传入的参数module_param(irq, int, 0644);module_param(devname, charp, 0644);// 定义驱动程序的 ID,在中断处理函数中用来判断是否需要处理#define MY_DEV_ID   1226// 驱动程序数据结构struct myirq{  int devid;};struct myirq mydev  ={ MY_DEV_ID };#define KBD_DATA_REG        0x60  #define KBD_STATUS_REG      0x64#define KBD_SCANCODE_MASK   0x7f#define KBD_STATUS_MASK     0x80// 工作项绑定的处理函数static void mywork_handler(struct work_struct *work){printk("mywork_handler is called. ");    // do some other things}//中断处理函数static irqreturn_t myirq_handler(int irq, void * dev){  struct myirq mydev;  unsigned char key_code;  mydev = *(struct myirq*)dev;     // 检查设备 id,只有当相等的时候才需要处理     if (MY_DEV_ID == mydev.devid)   {          // 读取键盘扫描码         key_code = inb(KBD_DATA_REG);         if (key_code == 0x01)       {                     printk("ESC key is pressed! ");                   // 初始化工作项                    INIT_WORK(&mywork, mywork_handler);                 // 加入到工作队列 system_wq                    schedule_work(&mywork);          }    }    return IRQ_HANDLED;}// 驱动模块初始化函数static int __init myirq_init(void){       printk("myirq_init is called. ");          // 注册中断处理函数       if(request_irq(irq, myirq_handler, IRQF_SHARED, devname, &mydev)!=0)            {                 printk("register irq[%d] handler failed. ", irq);                 return -1;              }                 printk("register irq[%d] handler success. ", irq);                 return 0;        }// 驱动模块退出函数static void __exit myirq_exit(void){             printk("myirq_exit is called. ");                  // 释放中断处理函数             free_irq(irq, &mydev);}MODULE_LICENSE("GPL");module_init(myirq_init);module_exit(myirq_exit);Makefile 文件ifneq ($(KERNELRELEASE),)          obj-m := my_driver_interrupt_wq.oelse          KERNELDIR ?= /lib/modules/$(shell uname -r)/build          PWD := $(shell pwd)default:         $(MAKE) -C $(KERNELDIR) M=$(PWD) modulesclean:         $(MAKE) -C $(KERNEL_PATH) M=$(PWD) cleanendif编译、测试$ make$ sudo insmod my_driver_interrupt_wq.ko irq=1 devname=mydev检查驱动模块是否加载成功:$ lsmod | grep my_driver_interrupt_wqmy_driver_interrupt_wq    16384  0再看一下 dmesg 的输出信息:$ dmesg...[  188.247636] myirq_init is called.[  188.247642] register irq[1] handler success.说明:驱动程序的初始化函数 myirq_init 被调用了,并且成功注册了 1 号中断的处理程序。此时,按一下键盘上的 ESC 键。操作系统在捕获到键盘中断之后,会依次调用此中断的所有中断处理程序,其中就包括我们注册的 myirq_handler 函数。在这个函数中,当判断出是ESC按键时,就初始化一个工作项(把结构体 work_struct 类型的变量与一个处理函数绑定起来),然后丢给操作系统预先创建好的工作队列(system_wq)去处理,如下所示:if (key_code == 0x01){       printk("ESC key is pressed! ");       INIT_WORK(&mywork, mywork_handler);       schedule_work(&mywork);}因此,当相应的内核线程从这个工作队列(system_wq)中取出工作项(mywork)来处理的时候,函数 mywork_handler 就会被调用。现在来看一下 dmesg 的输出信息:[  305.053155] ESC key is pressed![  305.053177] mywork_handler is called.可以看到:mywork_handler函数被正确调用了。完美!
  • [问题求助] 请问怎么通过IPM采集各个MPI进程之间的通信量
        https://support.huaweicloud.com/tngg-kunpenghpcs/kunpenghpcsolution_05_0026.html   在这里看到了IPM,请问ipm该怎么安装和使用?
  • [技术干货] 在 Linux 中调试程序执行的 Strace 示例[转载]
    链接:https://bbs.huaweicloud.com/blogs/354287Strace 是一个调试工具,可以帮助您解决问题。Strace 监控特定程序的系统调用和信号。当您没有源代码并想调试程序的执行时,它会很有帮助。strace 为您提供二进制文件从头到尾的执行顺序。本文解释了 7 个 strace 示例以帮助您入门。1. 跟踪可执行文件的执行您可以使用 strace 命令来跟踪任何可执行文件的执行。以下示例显示了 Linux ls 命令的 strace 输出。$ strace ls execve("/bin/ls", ["ls"], [/* 21 vars */]) = 0 brk(0) = 0x8c31000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb78c7000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=65354, ...}) = 0 ... ... ...2. 使用选项 -e 跟踪可执行文件中的特定系统调用默认情况下,strace 显示给定可执行文件的所有系统调用。要仅显示特定的系统调用,请使用 strace -e 选项,如下所示。$ strace -e open ls open("/etc/ld.so.cache", O_RDONLY) = 3 open("/lib/libselinux.so.1", O_RDONLY) = 3 open("/lib/librt.so.1", O_RDONLY) = 3 open("/lib/libacl.so.1", O_RDONLY) = 3 open("/lib/libc.so.6", O_RDONLY) = 3 open("/lib/libdl.so.2", O_RDONLY) = 3 open("/lib/libpthread.so.0", O_RDONLY) = 3 open("/lib/libattr.so.1", O_RDONLY) = 3 open("/proc/filesystems", O_RDONLY|O_LARGEFILE) = 3 open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3 open(".", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY|O_CLOEXEC) = 3 Desktop Documents Downloads examples.desktop libflashplayer.so Music Pictures Public Templates Ubuntu_OS Videos上面的输出只显示了 ls 命令的 open 系统调用。在 strace 输出的末尾,它还显示 ls 命令的输出。如果要跟踪多个系统调用,请使用“-e trace=”选项。以下示例显示了 open 和 read 系统调用。$ strace -e trace=open,read ls /home open("/etc/ld.so.cache", O_RDONLY) = 3 open("/lib/libselinux.so.1", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\3\3\1\260G004"..., 512) = 512 open("/lib/librt.so.1", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\3\3\1\300\30004"..., 512) = 512 .. open("/lib/libattr.so.1", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\3\3\1\360\r004"..., 512) = 512 open("/proc/filesystems", O_RDONLY|O_LARGEFILE) = 3 read(3, "nodev\tsysfs\nnodev\trootfs\nnodev\tb"..., 1024) = 315 read(3, "", 1024) = 0 open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3 open("/home", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY|O_CLOEXEC) = 3 bala3. 使用选项 -o 将跟踪执行保存到文件以下示例将 strace 输出存储到 output.txt 文件。$ strace -o output.txt ls Desktop Documents Downloads examples.desktop libflashplayer.so Music output.txt Pictures Public Templates Ubuntu_OS Videos $ cat output.txt execve("/bin/ls", ["ls"], [/* 37 vars */]) = 0 brk(0) = 0x8637000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7860000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=67188, ...}) = 0 ... ...4. 使用选项 -p 在正在运行的 Linux 进程上执行 Strace您可以使用进程 ID 在已经运行的程序上执行 strace。首先,使用ps 命令识别程序的 PID 。例如,如果要对当前正在运行的 firefox 程序执行 strace,请确定 firefox 程序的 PID。$ ps -C firefox-bin PID TTY TIME CMD 1725 ? 00:40:50 firefox-bin使用如下所示的 strace -p 选项来显示给定进程 ID 的 strace。$ sudo strace -p 1725 -o firefox_trace.txt $ tail -f firefox_trace.txt现在 firefox 进程的执行跟踪将被记录到 firefox_trace.txt 文本文件中。您可以跟踪此文本文件以查看 firefox 可执行文件的实时跟踪。当您的用户 ID 与给定进程的用户 ID 不匹配时,Strace 将显示以下错误。$ strace -p 1725 -o output.txt attach: ptrace(PTRACE_ATTACH, ...): Operation not permitted Could not attach to process. If your uid matches the uid of the target process, check the setting of /proc/sys/kernel/yama/ptrace_scope, or try again as the root user. For more details, see /etc/sysctl.d/10-ptrace.conf5. 使用选项 -t 打印每个跟踪输出行的时间戳要打印每个 strace 输出行的时间戳,请使用选项 -t,如下所示。$ strace -t -e open ls /home 20:42:37 open("/etc/ld.so.cache", O_RDONLY) = 3 20:42:37 open("/lib/libselinux.so.1", O_RDONLY) = 3 20:42:37 open("/lib/librt.so.1", O_RDONLY) = 3 20:42:37 open("/lib/libacl.so.1", O_RDONLY) = 3 20:42:37 open("/lib/libc.so.6", O_RDONLY) = 3 20:42:37 open("/lib/libdl.so.2", O_RDONLY) = 3 20:42:37 open("/lib/libpthread.so.0", O_RDONLY) = 3 20:42:37 open("/lib/libattr.so.1", O_RDONLY) = 3 20:42:37 open("/proc/filesystems", O_RDONLY|O_LARGEFILE) = 3 20:42:37 open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3 20:42:37 open("/home", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY|O_CLOEXEC) = 3 bala6. 使用选项 -r 打印系统调用的相对时间Strace 还可以选择打印每个系统调用的执行时间,如下所示。$ strace -r ls 0.000000 execve("/bin/ls", ["ls"], [/* 37 vars */]) = 0 0.000846 brk(0) = 0x8418000 0.000143 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) 0.000163 mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb787b000 0.000119 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) 0.000123 open("/etc/ld.so.cache", O_RDONLY) = 3 0.000099 fstat64(3, {st_mode=S_IFREG|0644, st_size=67188, ...}) = 0 0.000155 mmap2(NULL, 67188, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb786a000 ... ...7.使用选项-c生成系统调用的统计报告使用选项 -c,strace 为执行跟踪提供有用的统计报告。以下输出中的“调用”列指示了该特定系统调用执行了多少次。$ strace -c ls /home bala % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- -nan 0.000000 0 9 read -nan 0.000000 0 1 write -nan 0.000000 0 11 open -nan 0.000000 0 13 close -nan 0.000000 0 1 execve -nan 0.000000 0 9 9 access -nan 0.000000 0 3 brk -nan 0.000000 0 2 ioctl -nan 0.000000 0 3 munmap -nan 0.000000 0 1 uname -nan 0.000000 0 11 mprotect -nan 0.000000 0 2 rt_sigaction -nan 0.000000 0 1 rt_sigprocmask -nan 0.000000 0 1 getrlimit -nan 0.000000 0 25 mmap2 -nan 0.000000 0 1 stat64 -nan 0.000000 0 11 fstat64 -nan 0.000000 0 2 getdents64 -nan 0.000000 0 1 fcntl64 -nan 0.000000 0 2 1 futex -nan 0.000000 0 1 set_thread_area -nan 0.000000 0 1 set_tid_address -nan 0.000000 0 1 statfs64 -nan 0.000000 0 1 set_robust_list ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000000 114 10 total
  • [技术干货] UAP执行MML 命令卡死,无法继续执行其他命令处理
     如上图 UAP 执行mml命令卡死,处理不完成。处理步骤:先判断nmu主备还是单机,确认主机后执行如下步骤(此操作不影响通话)1.       登陆UAP 后台NMU 主机上2.       omu 用户执行 stop_lmt和stop_cm 重启两个对应进程。3.       chk_status 实时查看进程是否被拉起(正常停止进程后UAP会自动拉起进程)4.       重新登陆UAP  CDE 后执行命令正常执行
  • [技术干货] redis 主从复制
    主从复制:主节点负责写数据,从节点负责读数据,从而实现读写分离,提高redis的高可用性。让一个服务器去复制(replicate)另一个服务器,我们称呼被复制的服务器为主节点(master),而对主服务器进行复制的服务器则被称为从节点(slave)主从复制的特点:1、一个master可以有多个slave2、一个slave只能有一个master3、数据流向是单向的,master到slave 主从复制的作用:1、数据副本:多一份或多份数据拷贝,保证redis高可用2、扩展性能:单机redis的性能是有限的,主从复制能横向扩展 如容量、QPS等主从复制实现方式客户端命令:slaveof全量复制和增量复制全量复制过程:1.  向主节点发送psync,有两个参数,第一个参数是runId,第二个参数是偏移量,第一次发送不知道主节点的runId,也不知道偏移量,因此从节点发送 ?  -12.  主节点收到消息,根据? -1 能判断出来是第一次复制,主节点把runId和offset 发送给Slave节点,3.  从节点保存主节点基本信息4-5-6.  Master节点执行bgsave生成快照,在此期间会记录后续执行的数据更改命令所更改的数据,直到主节点将生成的RDB文件传输到从节点为止,            期间Master节点执行的写操作,主节点会将缓冲区中记录的新更改的数据发送给从节点7-8      从节点清空此前的所有数据,加载RDB文件恢复数据并存入新更改的数据说明: 全量复制的性能开销:1. bgsave生成RDB文件需要的时间,2.  RDB文件在网络间的传输时间,3.  从节点的数据清空时间  ,  4.  加载RDB文件的时间  5. 可能的 AOF   重写时间 数据更改命令缓冲区repl_back_buffer用于:当Redis通过Linux中的fork()函数开辟一个子进程处理其他事务(比如主进程执行bgsave生成一个RDB文件时,或者主进程执行bgrewriteaof生成一个AOF文件时), 而主进程(即处理客户端命令的进程)后续执行的一些数据更改命令会被暂时保存在该区域,而且该区域空间有限(配置文件中repl-backlog-size 1mb即可配置该处空间大小)部分复制部分复制解决的问题:在实际环境中,主节点与从节点之间可能会发生一些网络波动等情况,导致从节点与主节点之间的网络连接断开(主从节点的Redis均未关闭),如果重新连接上后,可以使用全量复制来重新进行一次主从节点数据同步,但是全量复制会带来一个性能开销的问题,而且从节点中可能有大量数据是主节点中没有更该过的,也就是不需要进行再次同步的数据,如果使用全量复制肯定是带来了一些不必要的浪费。所以,部分复制功能就是为了解决该问题的。过程:1.  主从节点直接连接断开,2.  此时主节点继续执行的数据更改命令会被记录在一个缓冲区  repl_back_buffer  中3.  当从节点重新连接主节点时,4.   自动发出一条命令(psync offset run_id),将从节点中存储的主节点的Redis运行时id和从节点中保存的偏移量发送给主节点5.  主节点接收从节点发送的偏移量和id,对比此时主节点的偏移量和接收的偏移量,如果两个偏移量之差大于repl_back_buffer中的数据,那么就表示在断开连接期间从节点已经丢失了超出规定数量的数据,此时就需要进行全量复制了,否则就进行部分复制6.  将主节点缓冲区中的数据同步更新到从节点中,这样就实现了部分数据的复制同步,降低了性能开销主从节点的故障处理故障发生时服务自动转移(自动故障转移):即当某个节点发生故障导致停止服务时,该节点提供的服务会有另一个节点自动代替提供,这样就实现了一个高可用的效果从节点故障:即如果某个从节点发生了故障,导致无法向在该节点上的客户端提供读服务,解决办法就是使该客户端转移到另一个可用从节点上,但是在转移时,应该考虑该从节点能承受几个客户端的压力主节点故障:如果主节点发生故障,在使用主节点进行读写操作的客户端就无法使用了,而使用从节点只进行读操作的客户端还是可以继续使用的,解决办法就是从从节点中选一个节点更改为主节点,并且将原主节点的客户端连接到新的主节点上,然后通过该客户端将其他从节点连接到新的主节点中主从复制确实可以解决故障问题,但是主从复制不能实现自动故障转移,其必须要通过一些手动操作,而且非常麻烦,所以要实现自动故障转移还需要另一个功能,Redis中提供了sentinel功能来实现自动故障转移。主从节点的故障处理 1. 读写分离:即客户端发来的读写命令分开,写命令交给主节点执行,读命令交给从节点执行,不仅减少了主节点的压力,而且增强了读操作的能力;但也会造成一些问题但是主从节点之间数据复制造成的阻塞延迟也可能会导致主从不一致的情况,也就是主节点先进行了写操作,但可能因为数据复制造成的阻塞延迟,导致在从节点上进行的读操作获取的数据与主节点不一致读取过期数据:主从复制会将带有过期时间的数据一并复制到从节点中,但是从节点是没有删除数据的能力的,即使是过期数据,所以主节点中的已经删除了过期数据,但是因为主从复制的阻塞延迟问题导致从节点中的过期数据没有删除,此时客户端就会读到一个过期数据    2. 主从配置不一致:造成的问题有比如配置中的maxmemory参数如果配置不一致,比如主节点2Gb,从节点1Gb,那么就可能会导致数据丢失;以及一些其他配置问题    3. 规避全量复制:全量复制的性能开销较大,所以要尽量避免全量复制,在第一次建立主从节点关系式一定会发生全量复制;可以适当减小Redis的maxmemory参数,这样可以使得RDB更快,或者选择在客户端操作低峰期进行,比如深夜从节点中保存的主节点run_id不一致时也一定会发生全量复制(比如主节点的重启);可以通过故障转移来尽量避免,例如Redis Sentinel 与 Redis Cluster 当主从节点的偏移量之差大于命令缓冲区repl_back_buffer中对应数据的偏移差时,也会发生全量复制,也就是上面的部分复制的复制过程中所说的;可以适当增大配置文件中repl-backlog-size即数据缓冲区可尽量避免    4. 规避复制风暴:单主节点导致的复制风暴,即当主节点重启后,要向其所有的从节点都进行一次全量复制,这非常消耗性能;可以更换主从节点的拓扑结构,更换为类似树形的结构,一个主节点只与少量的从节点建立主从关系,而而这些主节点又与其他从节点构成主从关系单主节点机器复制风暴:即如果过一台机器专门用来部署多个主节点,然后其他机器部署从节点,那么一旦主节点机器宕机重启,就会引起所有的主从节点之间的全量复制,造成非常大的性能开销;可以采用多台机器,分散部署主节点,或者使用自动故障转移来将某个从节点变为主节点实现一个高可用
  • [技术干货] Redis两种持久化方式
    对Redis而言,其数据是保存在内存中的,一旦机器宕机,内存中的数据会丢失,因此需要将数据异步持久化到硬盘中保存。这样,即使机器宕机,数据能从硬盘中恢复。常见的数据持久化方式:1.快照:类似拍照记录时光,快照是某时某刻将数据库的数据做拍照记录下其数据信息。如MYSQL的Dump,Redis的RDB模式2.写日志方式:是将数据的操作全部写到日志当中,需要恢复的时候,按照日志记录的操作记录重新再执行一遍。例如MYSQL的Binlog,Redis的AAOF模式、RDB说明:redis默认开启,将redis在内存中保存的数据,以快照的方式持久化到硬盘中保存。触发机制:1.save命令:阻塞方式,需要等redis执行完save后,才能执行其他get、set等操作。同步方式2.bgsave命令:非阻塞,其原理是调用linux 的  fork()函数,创建redis的子进程,子进程进行创建  rdb 文件的操作。异步方式,3.自动方式:在redis.conf文件中配置,如下  save  <指定时间间隔>  <执行指定次数更新操作>  ,save  60  10000 表示 60秒年内有10000次操作会自动生成rdb文件。4.其他方式4.1  执行flushall命令,清空数据,几乎不用4.2  执行shutdown命令,安全关闭redis不丢失数据,几乎用不到。4.3  主从复制,在主从复制的时候,rdb文件作为媒介来关联主节点和从节点的数据一致。RDB优缺点优点:1 适合大规模的数据恢复。2 如果业务对数据完整性和一致性要求不高,RDB是很好的选择。缺点:1 不可控,容易丢失数据:数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。2 耗时耗性能:备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。所以Redis 的持久化和数据的恢复要选择在夜深人静的时候执行是比较合理的。AOF说明: redis默认不开启,采用日志的形式来记录每个写操作,并追加到 .aof 文件中。Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作生成AOF的三种策略:1. always : 每条命令都会刷新到缓冲区,把缓冲区fsync到硬盘,对硬盘IO压力大,一般sata盘只有几百TPS,如果redis的写入量非常大,那对硬盘的压力也横刀。2. everysec:  每秒把缓冲区fsync 到硬盘,如果出现故障,会丢失1s(默认配置是1秒)的数据。一般使用这种。3. no : 由操作系统来定什么时候fsync到硬盘中。 缺点:不可控 AOF重写:把过期的,没有用的,重复的,可优化的命令简化为很小的aof文件。实际上是redis内存中的数据回溯成aof文件。如下图所示:作用:1.减少硬盘占用量2.加快恢复速度 AOF重写的实现方式1.bgrewriteaof   命令 : 从redis的主进程fork一个子进程生成包含当前redis内存数据的最小命令集、2.AOF重写配置:# 1. aof文件增长率 auto-aof-rewrite-percentage 100# 2. aof文件重写需要的尺寸 auto-aof-rewrite-min-size 64mb自动触发时机:(需要同时满足)当前的aof文件大小   >   aof文件重写需要的尺寸        (aof当前文件大小 -  上次aof的文件大小)/  上次aof文件大小  >  aof文件增长率 AOF优缺点优点:1.数据的完整性和一致性更高缺点:1.因为AOF记录的内容多,文件会越来越大,数据恢复也会越来越慢。2. AOF每秒fsync一次指令硬盘,如果硬盘IO慢,会阻塞父进程;风险是会丢失1秒多的数据;在Rewrite过程中,主进程把指令存到mem-buffer中,最后写盘时会阻塞主进程。关于Redis持久化方式RDB和AOF的缺点原因是redis持久化方式的痛点,缺点比较明显。1、RDB需要定时持久化,风险是可能会丢两次持久之间的数据,量可能很大。2、AOF每秒fsync一次指令硬盘,如果硬盘IO慢,会阻塞父进程;风险是会丢失1秒多的数据;在Rewrite过程中,主进程把指令存到mem-buffer中,最后写盘时会阻塞主进程。3、这两个缺点是个很大的痛点。为了解决这些痛点,GitHub的两位工程师 Bryana Knight 和 Miguel Fernández 日前写了一篇 文章 ,讲述了将持久数据从Redis迁出的经验: http://www.open-open.com/lib/view/open1487736984424.html如何选择RDB和AOF建议全都要。1、对于我们应该选择RDB还是AOF,官方的建议是两个同时使用。这样可以提供更可靠的持久化方案。在redis 4.0 之后,官方提供了混合持久化模式,具体如下持久化文件结构上半段RDB格式,后半段是AOF模式。
  • [技术干货] 云管理解密之CloudcampusAPP AP蓝牙串口
    1 扫描周围蓝牙设备扫描周围的蓝牙设备并显示蓝牙设备的强度,名称,蓝牙地址。显示之前的AP蓝牙连接记录(最近2条)。2 连接蓝牙设备点击蓝牙列表的连接按钮,进行绑定连接的动作。3 与AP蓝牙进行通讯成功连接ap后,点击进入进行蓝牙串口通讯,进行命令交互。可以向蓝牙端发送命令,也可以接受到ap返回的信息。4  查看历史记录,导出报告点击图标,进入蓝牙命令历史记录列表 查看蓝牙命令的历史内容,选择对应的报告,可以导出为excel文件。删除对应选择的历史记录。常用命令:display cloud-mng info命令用来查看当前设备配置的CloudCampus@AC-Campus信息。display cloud-mng offline-record命令用来查看设备最近10次下线记录信息。display cloud-mng offline-record diagnostic-information显示设备最近10次下线的原因表1 常见设备重启原因见设备重启原因解释处理建议Reset for others其他原因。联系技术支持人员。Reset by user command执行重启设备的命令。无需处理。Np initialize failedNP启动失败。联系技术支持人员。Startup np overtimeNP启动超时。联系技术支持人员。Reset for memory use out内存耗尽。检查内存占用情况。Reset by ac command resetAC下发命令重启AP成功。无需处理。Reset for power off设备断电。无需处理。Reset for kernel panic内核KP重启。联系技术支持人员。Reset for switch zone after update versionAP升级主备区切换重启。无需处理。Reset for update version successAP升级成功重启。无需处理。Reset for update version failedAP升级失败重启。检查AP升级失败原因。Reset for exceptionVOS异常信号重启。联系技术支持人员。Reset for watchdog看门狗异常重启。联系技术支持人员。Reset for cpld写CPLD寄存器重启。联系技术支持人员。Reset for reset-key按设备的复位按钮重启。无需处理。Reset for the radio type is different from that specified by the configuration file射频类型与配置文件不一致。无需处理。Reset for the radio type is changed射频类型被修改。无需处理。Reset for 11ac target chip abnormalWIFI芯片固件异常。联系技术支持人员。Reset for tx dma stopTX DMA停止,发包异常。联系技术支持人员。Reset for restoration to factory defaults(by command)通过命令恢复出厂配置。无需处理。Reset for MFPI detect MSC module abnormalMFPI监控到MSC模块异常。联系技术支持人员。Reset for MFPI detect MSU module abnormalMFPI监控到MSU模块异常。联系技术支持人员。Reset for MFPI detect KAP module abnormalMFPI监控到KAP模块异常。联系技术支持人员。Reset for configuration synchronization command重启以同步配置。配置同步场景,主备公有配置不一致,通过手动方式同步,根据重启提示确认重启。无需处理。Reset for auto configuration synchronization重启以同步配置。配置同步场景,主备公有配置不一致,自动同步后,根据重启提示确认重启。无需处理。Reset for a country code change变更国家码。无需处理。Reset for an AP MAC or SN change变更AP的MAC或SN。无需处理。Reset for an AP upgradeAP升级重启。无需处理。Reset for the ap-reset command执行重启AP的命令。无需处理。Reset for the undo ap command执行删除AP的命令。无需处理。Reset for license expirationLicense过期。购买并激活License。Reset for the AP added to the blacklistAP被加入黑名单。无需处理。Reset for the CAPWAP link and AP status mismatchAP状态与CAPWAP链路状态不一致。无需处理。Reset for a DTLS configuration change变更DTLS开关。无需处理。Reset for a CAPWAP link faultCAPWAP链路状态不正常。无需处理。Reset for restoration to factory defaults恢复出厂配置。无需处理。Reset for a dual-link backup switch changeAC双链路备份切换。无需处理。Reset for the ap-rename command下发修改AP名称的命令。无需处理。Reset for the ap-regroup command下发修改AP组的命令。无需处理。Reset for an AP conflict state changeAP状态冲突。无需处理。Reset for an AP management VLAN changeAP的管理VLAN变更。无需处理。Reset for commands in the provision-ap view下发AP上线参数的命令。无需处理。Reset for a country code mismatch不支持的国家码。检查国家码配置。Reset for a central AP type change中心AP类型发生变化。无需处理。Reset for AP deleted by controllerAP被AC删除。无需处理。Reset for abnormal network port self-healing网口异常自愈。检查网口。Reset for the radio type mismatch between the AP and ACAC和AP间射频类型不匹配。无需处理。Reset for batch deleteHSB批量删除。无需处理。Reset for the DTLS configuration change of the data link数据链路DTLS配置变更。无需处理。Reset for the AC IP address list change修改AC IP地址列表。无需处理。Reset for the address mode change修改地址模式配置。无需处理。Reset for the IP address change修改IP地址配置。无需处理。Reset for AP self-healing (The AP is not online on the AC for consecutive 24 hours)AP连续24小时未上线自愈重启。检查AP上线失败原因。Reset for a channel set switching室外AP加载室内国家码信道集配置。无需处理。Reset for the online configuration switching误配置导致设备离线,自恢复重启。无需处理。Reset for slow task switching系统运行慢导致的重启。检查内存占用情况。Reset for restoration to factory defaults(by button)通过复位按钮恢复出厂设置。无需处理。Reset for a configuration delivery failure配置下发失败。检查配置下发失败原因。Reset for MFPI detect CAP PBUF use outMFPI监控到转发PBUF耗尽。联系技术支持人员。Reset for abnormal wifi txrx self-healingWIFI驱动侧收发包长时间异常。联系技术支持人员。Reset for exception(redis-server exit)Redis-server进程异常退出。联系技术支持人员。Reset for exception(confd exit)Confd进程异常退出。联系技术支持人员。Reset for exception(callhome exit)Callhome进程异常退出。联系技术支持人员。Reset for an abnormal process进程异常。联系技术支持人员。Reset for a smart upgrade智能升级重启。无需处理。Reset for the branch group of AP change修改AP分支组。无需处理。Reset for the AC license expiresAC上License过期。购买并激活License。Reset for the AC port mode change between 40GE and XGEAC端口模式在40GE和XGE之间切换。无需处理。Reset for AP self-healing (The AP is not online for consecutive 24 hours)AP连续24小时未上线自愈重启。检查AP上线失败原因。Reset for the version rollback because the AP does not go online after the online upgrade在线升级后AP未上线回退版本重启。检查AP上线失败原因。display cloud-mng online-fail-record命令用来查看设备最近5次上线失败记录信息。display cloud-mng online-fail-record diagnostic-information显示设备最近5次上线失败的原因。表1 上线失败原因上线失败原因解释处理建议AP can't obtain address设备无法获取IP请检查DHCP服务器,确保设备能获取IP地址。Connect to confd failedconfd进程异常请联系技术支持人员进行具体定位。DNS failed设备无法通过DNS解析控制器地址请检查DNS服务器,确保设备能正确解析到云管理平台的IP地址。Connect to controller failed连接控制器失败请检查网络连通性和端口,确保设备能正常访问云管理平台的10020端口。Could not load host key证书加载失败请联系技术支持人员进行具体定位。Register Fail: Internal error, the controller is not already控制器未启动完成,或者正在获取当前部署场景请联系云管理平台系统管理员,确认云管理平台运行正常。Register Fail: Get the device's information timeout or the device returns fail控制器获取设备信息失败请联系技术支持人员进行具体定位。Register Fail: Illegal device's information format设备上报的信息格式有误请联系技术支持人员进行具体定位。Register Fail: The device is not added to the controller设备的ESN未添加到控制器请将设备的ESN添加到云管理平台的站点中。Register Fail: License is not authorized or expiredLicense未授权或过期请确保云管理平台上的License资源充足。Register Fail: Distribution controller node failure控制器节点分配失败请联系技术支持人员进行具体定位。Register Fail: The device unreports device models设备未向控制器上报款型信息请联系技术支持人员进行具体定位。Register Fail: The device model do not match with esn设备向控制器上报的款型信息和ESN不匹配请联系技术支持人员进行具体定位。Register Fail: The device unreports MAC设备未向控制器上报MAC或不符合MAC规则请联系技术支持人员进行具体定位。Register Fail: The cloud APs cannot add to AC site云AP不能加入到AC类型的站点请将设备添加到AP类型的站点。Register Fail: The ESN is not in allow rule设备不在白名单中请联系云管理平台的系统管理员检查配置,将当前设备的ESN添加到设备白名单中。Register Fail: Unsupport fail reason未知原因请联系技术支持人员进行具体定位。[Huawei] ap-mode-switch cloudWarning: The system will reboot and start in cloud mode of V200R008C10. All of configurations will restore to factory. Continue? (y/n)[n]:yInfo: system is rebooting ,please wait...===========================AP的模式切换和云管理平台地址的配置如下:首先,在系统视图下执行命令ap-mode-switch cloud,切换到云模式。执行此命令后,设备会提示清除设备的配置并重新启动。重启完毕,即切换到了云模式。然后,执行命令cloud-mng controller ip-address ip-address port port-number或cloud-mng controller url url-string port port-number,配置云管理平台的IP地址或URL。缺省情况下,设备上没有配置云管理平台的地址信息。A面商用环境,可以按这个配置:cloud-mng controller ip-address 43.254.2.230 port 10020或cloud-mng controller url device-naas.huawei.com port 10020B面商用环境,可以按这个配置:cloud-mng controller ip-address 121.36.226.228 port 10020或cloud-mng controller url device-naas1.huaweicloud.com port 10020
  • [新手课堂] 设计模式之观察者模式
    ## 观察者模式 ### 前言 观察者模式是一种对象行为型模式,其主要优点如下: - 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。 - 目标与观察者之间建立了一套触发机制。 它的主要缺点如下: - 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。 - 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。 ### 生活小案列 这就是一个观察者模式的生活案例。当领导有事的时候发布通知到群里,群里的所有人收到通知后做相应的事情。 以上案例中可以分为下面几个角色: - 监听者(也可以说是观察者):群里面每一个人都是一个监听者。 - 管理者(对应其他教程中的主题-subject):也就是群,主要有添加(群成员)监听者,移除(群成员)监听者,还有通知所有监听者的功能。 - 事件(或者说通知):也就是领导发到群里面的消息是一个事件(或者说通知)。 通过上面的例子来整理一下实现一个观察者模式的思路。 看看一个流程:领导创建群将相关人员添加到群,然后向群里面发布一个通知。群里面每个人看到这条消息后,做相应的事情,当这个消息与自己无关时,啥也不做。 领导在这里面可以看做是应用程序的一个线程,只是程序执行的一个单元而已。 接下来就是一般设计模式都有的套路,为了程序的扩展性。上面的几个角色都需要定义成抽象的概念,那么在Java里面定义抽象有两种一个是接口一个是抽象类。具体定义成接口还是抽象类根据实际情况自行选择。 ### 抽象概念 为什么要定义成抽象的呢?我们先了解一下抽象的概念,我理解抽象就是对一类事物公共部分的定义。比如说水果,就是对一类事物的抽象定义,说到水果,大家肯定能联想到,多汁且主要味觉为甜味和酸味,可食用的植物果实,有丰富的营养成分。这个就是水果的公共成分,但是水果又分为多种,火龙果,百香果···。 抽象的好处:比如今天你家里只有一种水果-火龙果。你爹叫你拿一点水果来吃,那你肯定就能直接把家里唯一的水果火龙果拿过来孝敬你老爹。在这个过程中你爹说的水果而不是火龙果,能够少说一个字从而节约能量多活一纳秒。那么我们可以得出一个结论-使用抽象概念可以延年益寿→_→。 开个玩笑,下面言归正传,我说一下我认为抽象的好处: - 当接口只定义一个实现类时,方便功能的替换(换一个实现类,在新实现类新增功能。从而避免了对调用方和原实现类原代码的改动)。 - 方法形参定义为抽象,这时就能实现传入不同的实现类该方法可以实现不同的功能。 - 统一管理,让程序更规范化,当抽象中定义新的非抽象方法,子类可以直接继承使用。 有了上面的铺垫,很容易理解下面的代码示例。 ### 观察者模式代码示例 观察者模式其实也是发布订阅模式。 针对不同的观察者需要有不同的实现方式,所以先创建一个管理者的接口,将其定义为一个抽象概念,方便后续扩展。 这个接口相当于-群(管理者) ```java /** * 观察者的顶层接口 * @param */ public interface ObserverInterface { //注册监听者 public void registerListener(T t); //移除监听者 public void removeListener(T t); //通知监听者 public void notifyListener(DataEvent t); } ``` 定义抽象的监听者接口 这个接口相当于-群成员(监听者) ```java /** * Listener的顶级接口,为了抽象Listener而存在 */ public interface MyListener { void onEvent(DataEvent event); } ``` 定义抽象的事件接口 这个接口相当于群里面发布的通知 ```java @Data public abstract class DataEvent { private String msg; } ``` 创建管理者的实现类,相当于具体的群(如微信群,钉钉群) ```java /** * 循环调用方式的观察者(同步) */ @Component public class LoopObserverImpl implements ObserverInterface { //监听者的注册列表 private List listenerList = new ArrayList(); @Override public void registerListener(MyListener listener) { listenerList.add(listener); } @Override public void removeListener(MyListener listener) { listenerList.remove(listener); } @Override public void notifyListener(DataEvent event) { for (MyListener myListener : listenerList) { myListener.onEvent(event); } } } ``` 创建两个event的实现类,一个是积分事件,一个是短信事件 ```java /** * 积分事件类 */ public class ScoreDataEvent extends DataEvent { private Integer score; } /** * 短信事件类 */ public class SmsDataEvent extends DataEvent { private String phoneNum; } ``` 创建两个listener的实现类,一个是处理积分的,一个是处理短信的 ```java /** * MyListener的实现类,分数监听者 */ @Component public class MyScoreListener implements MyListener { @Override public void onEvent(DataEvent dataEvent) { if (dataEvent instanceof ScoreDataEvent) { //...省略业务逻辑 System.out.println("积分处理:" + dataEvent.getMsg()); } } } /** * MyListener的实现类,短信监听者 */ @Component public class MySmsListener implements MyListener { @Override public void onEvent(DataEvent dataEvent) { if (dataEvent instanceof SmsDataEvent) { //...省略短信处理逻辑 System.out.println("短信处理"); } } } ``` 观察者模式的要素就到齐了,我们在main方法里面跑一下 ```java public class Operator { public static void main(String[] args) { //通过spring的AnnotationConfigApplicationContext将com.example.demo.user.admin.design路径下的所有加了spring注解的类都扫描放入spring容器 AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example.demo.user.admin.design"); //从spring容器中获取对应bean的实例 LoopObserverImpl loopObserver = context.getBean(LoopObserverImpl.class); MyScoreListener scoreL = context.getBean(MyScoreListener.class); MySmsListener smsL = context.getBean(MySmsListener.class); //向观察者中注册listener loopObserver.registerListener(scoreL); loopObserver.registerListener(smsL); ScoreDataEvent scoreData = new ScoreDataEvent(); scoreData.setMsg("循环同步观察者"); //发布积分事件,通知监听者 loopObserver.notifyListener(scoreData); /*******************************************/ //从spring容器获取QueueObserverImpl观察者 QueueObserverImpl queueObserver = context.getBean(QueueObserverImpl.class); //向观察者中注册listener queueObserver.registerListener(scoreL); queueObserver.registerListener(smsL); ScoreDataEvent scoreData1 = new ScoreDataEvent(); scoreData1.setMsg("队列异步观察者"); //发布积分事件,通知监听者 queueObserver.notifyListener(scoreData1); } } ``` 接下来看看下面这个新的观察者实现类和上面示例中的的观察者实现类`LoopObserverImpl`有什么不同吗 ```java /** * 启动一个线程循环阻塞队列的观察者,可以实现解耦异步。 */ @Component public class QueueObserverImpl implements ObserverInterface { //监听者的注册列表 private List listenerList = new ArrayList(); //创建一个大小为10的阻塞队列 private BlockingQueue queue = new LinkedBlockingQueue(10); //创建一个线程池 private ExecutorService executorService = new ScheduledThreadPoolExecutor(1, r -> { Thread t = new Thread(r); t.setName("com.kangarooking.observer.worker"); t.setDaemon(false); return t; }); // private ExecutorService executorService = Executors.newFixedThreadPool(1); @Override public void registerListener(MyListener listener) { listenerList.add(listener); } @Override public void removeListener(MyListener listener) { listenerList.remove(listener); } @Override public void notifyListener(DataEvent event) { System.out.println("向队列放入DataMsg:" + event.getMsg()); queue.offer(event); } @PostConstruct public void initObserver() { System.out.println("初始化时启动一个线程"); executorService.submit(() -> { while (true) { try { System.out.println("循环从阻塞队列里面获取数据,take是阻塞队列没有数据就会阻塞住"); DataEvent dataMsg = queue.take(); System.out.println("从阻塞队列获取到数据:" + dataMsg.getMsg()); eventNotify(dataMsg); } catch (InterruptedException e) { e.printStackTrace(); } } }); } private void eventNotify(DataEvent event) { System.out.println("循环所有的监听者"); for (MyListener myListener : listenerList) { myListener.onEvent(event); } } } ``` 不同之处就是引入了阻塞队列,让通知这个操作变成异步操作,既只需要将event时间放入阻塞队列之后就可以直接返回了。不用像`LoopObserverImpl`要等到listener注册表循环完毕才能返回。这样就实现了通知操作和循环listener注册表的解耦和异步。 举例说明异步实现和同步实现的区别: 同步:还是团建群的例子,假如领导是保姆型领导,通知下来任务之后可能不太放心,要挨个问,小张你准备什么表演阿,大概多久能准备好鸭。小红你呢→_→。。。 异步:假如是甩手掌柜型领导,发布完消息之后他就不管了。 上面就是同步和异步的区别,同步就是领导是个保姆,挨个问挨个了解情况之后这个事情才算完。异步就是领导发布完消息就完事儿。 ### 开源框架的实现 #### 同步方式 spring的发布订阅就是基于同步的观察者模式: 简单来说就是将所有的监听者注册到一个列表里面,然后当发布事件时,通过循环监听者列表,在循环里面调用每个监听者的onEvent方法,每个监听者实现的在onEvent方法里面判断传入的event是否属于当前需要的event,属于就处理该事件,反之不处理。 spring的`ApplicationEventMulticaster`就是示例讲的观察者顶层接口 `ApplicationListener`就是示例代码的监听者顶层接口 在`refresh`方法里面调用的`registerListeners();`方法就是将所有的监听者实现类注册到观察者的注册表中 `ApplicationEventMulticaster`的`multicastEvent`方法就是上面讲的通知方法,这里就是循环监听者注册表,调用每个监听者的onApplicationEvent方法(这里的`invokeListener`方法里面最终会调用到`listener.onApplicationEvent(event);`) 随便看一个`onApplicationEvent`方法的实现,跟上面的例子是不是很相似 #### 异步方式 nacos中有很多地方都使用到了观察者模式,如client端和server端建立连接,发布连接事件,相关监听者做相应的处理,断开连接也是一样。 在server端接收到client端的注册请求后,会发布一个注册事件的通知 在nacos-server启动的时候也是会开启一个线程做死循环,循环的去queue里面take数据,如果没有的话就会阻塞。所以死循环只有在queue里面一直有数据的时候才会一直循环,当queue里面没有数据的时候就会阻塞在`queue.take();`方法处。 我们看看`receiveEvent(event);`方法里面做了什么,这里就体现了框架里面设计的精妙:在上面我们自己的设计中,这里应该是需要循环调用所有的listener的`onApplicationEvent`方法,但是当注册表中listener太多的时候就会出现(有些event可能会有多个listener需要处理)循环调用太慢的问题,这里使用多线程的处理方式,让这些调用并行处理,大大的提高了框架的事件处理效率。 ### 关于业务使用场景 可以说观察者模式能解决的,消息队列也可以解决,并且可以做的更好。主要根据实际情况取舍。 当公司的服务器资源充足,并且用户量大,相关业务逻辑调用频繁,消息要求高可靠性,以及消息要求发布订阅更灵活,就可以考虑使用消息队列。 当服务器资源不充足,或者调用比较少,或者希望使用轻量的通知机制,对于消息可靠性要求不高,可以考虑在项目代码里面使用观察者模式。 当然使用观察者模式比较麻烦的一点就是要自己写一定量的代码,而且功能还不如消息队列的强大,并且不能保证消息的可靠性,当观察者获取消息在自己的处理逻辑里面产生异常时,可能还需要自己先写好发生异常后的降级代码(当然如果对可靠性要求不高的业务场景就不需要)。 为什么框架使用观察者模式而不使用消息队列(个人理解): 1. 消息队列太重; 2. 本身就是开源框架(本省代表原创),不适合再引入另一个很重的消息队列。增加用户的使用和部署成本以及难度,对于自身的推广也不利。 ## 总结 看到这里其实我就想告诉大家,设计模式其实只是一种思维方式,我们学习设计模式只是了解一个基本的编程思维方式,在实际的使用过程中是需要根据实际情况变化的。观察者模式也是如此,只要思想不滑坡,你可以创造出很多种不同实现方式的观察者模式。
  • [新手课堂] JVM原理学习总结
    这篇总结主要是基于我之前JVM系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢#更多详细内容可以查看我的专栏文章:深入理解JVM虚拟机https://blog.csdn.net/column/details/21960.htmlJVM介绍和源码首先JVM是一个虚拟机,当你安装了jre,它就包含了jvm环境。JVM有自己的内存结构,字节码执行引擎,因此class字节码才能在jvm上运行,除了Java以外,Scala,groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。JVM内存模型内存模型老生常谈了,主要就是线程共享的堆区,方法区,本地方法栈。还有线程私有的虚拟机栈和程序计数器。堆区存放所有对象,每个对象有一个地址,Java类jvm初始化时加载到方法区,而后会在堆区中生成一个Class对象,来负责这个类所有实例的实例化。栈区存放的是栈帧结构,栈帧是一段内存空间,包括参数列表,返回地址,局部变量表等,局部变量表由一堆slot组成,slot的大小固定,根据变量的数据类型决定需要用到几个slot。方法区存放类的元数据,将原来的字面量转换成引用,当然,方法区也提供常量池,常量池存放-128到127的数字类型的包装类。字符串常量池则会存放使用intern的字符串变量。JVM OOM和内存泄漏这里指的是oom和内存泄漏这类错误。oom一般分为三种,堆区内存溢出,栈区内存溢出以及方法区内存溢出。堆内存溢出主要原因是创建了太多对象,比如一个集合类死循环添加一个数,此时设置jvm参数使堆内存最大值为10m,一会就会报oom异常。栈内存溢出主要与栈空间和线程有关,因为栈是线程私有的,如果创建太多线程,内存值超过栈空间上限,也会报oom。方法区内存溢出主要是由于动态加载类的数量太多,或者是不断创建一个动态代理,用不了多久方法区内存也会溢出,会报oom,这里在1.7之前会报permgem oom,1.8则会报meta space oom,这是因为1.8中删除了堆中的永久代,转而使用元数据区。内存泄漏一般是因为对象被引用无法回收,比如一个集合中存着很多对象,可能你在外部代码把对象的引用置空了,但是由于对象还被集合给引用着,所以无法被回收,导致内存泄漏。测试也很简单,就在集合里添加对象,添加完以后把引用置空,循环操作,一会就会出现oom异常,原因是内存泄漏太多了,导致没有空间分配新的对象。常见调试工具命令行工具有jstack jstat jmap 等,jstack可以跟踪线程的调用堆栈,以便追踪错误原因。jstat可以检查jvm的内存使用情况,gc情况以及线程状态等。jmap用于把堆栈快照转储到文件系统,然后可以用其他工具去排查。visualvm是一款很不错的gui调试工具,可以远程登录主机以便访问其jvm的状态并进行监控。class文件结构class文件结构比较复杂,首先jvm定义了一个class文件的规则,并且让jvm按照这个规则去验证与读取。开头是一串魔数,然后接下来会有各种不同长度的数据,通过class的规则去读取这些数据,jvm就可以识别其内容,最后将其加载到方法区。JVM的类加载机制jvm的类加载顺序是bootstrap类加载器,extclassloader加载器,最后是appclassloader用户加载器,分别加载的是jdk/bin ,jdk/ext以及用户定义的类目录下的类(一般通过ide指定),一般核心类都由bootstrap和ext加载器来加载,appclassloader用于加载自己写的类。双亲委派模型,加载一个类时,首先获取当前类加载器,先找到最高层的类加载器bootstrap让他尝试加载,他如果加载不了再让ext加载器去加载,如果他也加载不了再让appclassloader去加载。这样的话,确保一个类型只会被加载一次,并且以高层类加载器为准,防止某些类与核心类重复,产生错误。defineclass findclass和loadclass类加载classloader中有两个方法loadclass和findclass,loadclass遵从双亲委派模型,先调用父类加载的loadclass,如果父类和自己都无法加载该类,则会去调用findclass方法,而findclass默认实现为空,如果要自定义类加载方式,则可以重写findclass方法。常见使用defineclass的情况是从网络或者文件读取字节码,然后通过defineclass将其定义成一个类,并且返回一个Class对象,说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代,1.8以后则是元空间了。JVM虚拟机字节码执行引擎jvm通过字节码执行引擎来执行class代码,他是一个栈式执行引擎。这部分内容比较高深,在这里就不献丑了。编译期优化和运行期优化编译期优化主要有几种1 泛型的擦除,使得泛型在编译时变成了实际类型,也叫伪泛型。2 自动拆箱装箱,foreach循环自动变成迭代器实现的for循环。3 条件编译,比如if(true)直接可得。运行期优化主要有几种1 JIT即时编译Java既是编译语言也是解释语言,因为需要编译代码生成字节码,而后通过解释器解释执行。但是,有些代码由于经常被使用而成为热点代码,每次都编译太过费时费力,干脆直接把他编译成本地代码,这种方式叫做JIT即时编译处理,所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。2 公共表达式擦除,就是一个式子在后面如果没有被修改,在后面调用时就会被直接替换成数值。3 数组边界擦除,方法内联,比较偏,意义不大。4 逃逸分析,用于分析一个对象的作用范围,如果只局限在方法中被访问,则说明不会逃逸出方法,这样的话他就是线程安全的,不需要进行并发加锁。1JVM的垃圾回收1 GC算法:停止复制,存活对象少时适用,缺点是需要两倍空间。标记清除,存活对象多时适用,但是容易产生随便。标记整理,存活对象少时适用,需要移动对象较多。2 GC分区,一般GC发生在堆区,堆区可分为年轻代,老年代,以前有永久代,现在没有了。年轻代分为eden和survior,新对象分配在eden,当年轻代满时触发minor gc,存活对象移至survivor区,然后两个区互换,等待下一场gc,当对象存活的阈值达到设定值时进入老年代,大对象也会直接进入老年代。老年代空间较大,当老年代空间不足以存放年轻代过来的对象时,开始进行full gc。同时整理年轻代和老年代。一般年轻代使用停止复制,老年代使用标记清除。3 垃圾收集器serial串行parallel并行它们都有年轻代与老年代的不同实现。然后是scanvage收集器,注重吞吐量,可以自己设置,不过不注重延迟。cms垃圾收集器,注重延迟的缩短和控制,并且收集线程和系统线程可以并发。cms收集步骤主要是,初次标记gc root,然后停顿进行并发标记,而后处理改变后的标记,最后停顿进行并发清除。g1收集器和cms的收集方式类似,但是g1将堆内存划分成了大小相同的小块区域,并且将垃圾集中到一个区域,存活对象集中到另一个区域,然后进行收集,防止产生碎片,同时使分配方式更灵活,它还支持根据对象变化预测停顿时间,从而更好地帮用户解决延迟等问题。
  • [新手课堂] 进程间的通信方式有哪些?
    管道:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。命名管道FIFO:未命名的管道只能在两个相关的进程之间通信,通过命名管道FIFO,不相关的进程也能交换数据。消息队列:消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。消息队列允许一个或多个进程向它写入与读取消息。管道和命名管道的通信数据都是先进先出原则,消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取,比FIFO更有优势。共享内存:共享内存是允许一个或多个进程共享的一块内存区域。信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
总条数:756 到第
上滑加载中