android 如何分析应用的内存(六)

news2024/11/15 23:23:06

android 如何分析应用的内存(六)

接上文,本系列文章,最重要的部分——————对native堆内存的分析,即将上演

分成六大板块:

  1. 手动实现,new和delete,以及malloc和freee,并统计内存分配情况
  2. 使用malloc hook
  3. 使用Malloc和libc回调,搭配使用DDMS
  4. 使用AddressSanitizer
  5. 使用HWASan/Asan工具,查找内存错误
  6. 使用perfetto工具,他也可以分析java部分

本篇文章,实现第一个板块,并作为后续内存分析的基础知识。

先对,用到的理论知识,做说明,然后再进行实现。

理论篇

我们先简单介绍操作系统的系统调用,然后简单说明android libc对其的封装,最后是我们自己对libc的分配函数的封装,以达到内存统计分析的目的

系统调用

对于内存而言,它属于系统资源,它的管理者是操作系统。而为了让用户能够使用系统的资源。有三种系统调用,提供给用户。分别是:

  1. brk系统调用:用于扩展和收缩进程的数据段
  2. sbrk系统调用:同brk
  3. mmap/munmap系统调用:用于在进程的地址空间,创建/取消内存映射
    除了上面提供的以外,还有一些更高级的系统调用,此处不表

其中brk系统调用和sbrk系统调用的区别是:brk用于直接设置break地址。即修改进程数据段的结束地址。
sbrk用于在原有的break地址上,进行增加或减少

mmap和munmap则是,直接进行进程地址空间的映射。

libc库

从上面可以知道,操作系统只提供了一些非常简单的内存操作接口,
如果一个进程中,有多个线程,应该怎么管理这部分堆空间呢?
如果一个线程要反反复复频繁的分配空间怎么办呢?是否要频繁的调用系统调用呢?
如果分配的空间,一会大的要死,一会儿小的要死,应该怎么办呢?

此时,Android的libc库,对上面的内存做了进一步的管理。它使用了一种被称为jemalloc的分配器策略。

jemalloc,维护了大小不同的内存池。当应用程序请求分配内存时,jemalloc会选择合适的内存满足应用的请求。
jemalloc,还为每个线程维护本地缓存,当线程请求内存时,则尽可能返回本地缓存中的内存块。

jemalloc的所有这些内存管理,都是基于上面介绍的三种系统调用。

同时Android的libc库,也是一种轻量级的c库,因此对jemalloc的分配器策略,进行了一种标准接口的封装
即我们熟悉的:malloc,remalloc,calloc标准接口

当应用程序,调用malloc,remalloc,或者calloc函数请求分配内存时.libc库则根据jemalloc的分配器策略分配不同的内存快

自定义内存分配函数

那么我们还可以对,libc库的内存分配函数,做进一步的封装。在应用每次调用内存分配的时候,记录下调用分配的堆栈,和分配的大小。
在合适的时候,将其打印出来,以观察内存的分配情况,从而达到分析内存泄漏的问题。

malloc函数,free函数

malloc,free函数位于stdlib.h文件中,定义如下

void* malloc (size_t size);
void free (void* ptr);

下面是我自定义的malloc函数,我将其放在一个单独的命名为Memory.cpp的文件中

void* malloc(size_t size) {
    void* ptr = NULL;

    // 在这里,你可以添加自定义的逻辑,例如记录内存分配的信息,或者改变内存分配的行为。

    // 调用系统的malloc函数进行实际的内存分配。
    // 注意,这里需要使用函数指针来调用系统的malloc函数,以避免无限递归。
    void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
    //查找下一个叫做malloc的符号
    if (libc_malloc) {
        ptr = libc_malloc(size);
        
        //将ptr保存,以便于后续分析
        addToAllptr(ptr,size);
    }

    return ptr;
}

为了保存,分配的地址和,大小,新增下面的函数

//保存所有指针的地方
AllPtr ptrBuffer;

static std::recursive_mutex mutex;

//为了在ptrBuffer.push的时候,防止循环调用
static bool * isPush = nullptr;

void addToAllptr(void *ptr,std::size_t sz){
    std::unique_lock<std::recursive_mutex> _l(mutex);
    //为什么不直接用一个bool变量?
    //因为发现,在部分版本中,会被优化掉,因此通过使用单独的内存来存储这个bool值
    if(!isPush){
        void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
        if (libc_malloc) {
            isPush =(bool *) libc_malloc(sizeof(bool));
            *isPush = false;
        }
    }
    if(isPush){
        if(*isPush){

        }else{
            *isPush = true;
            ptrBuffer.push(ptr,sz);
            *isPush = false;
        }
    }
}

void popFromAllptr(void *ptr) {
    std::unique_lock<std::recursive_mutex> _l(mutex);
    ptrBuffer.pop(ptr);
}

其中。prtBuffer是类型为Allptr的一个自定义类,这个类负责管理,分配的内存。代码如下:

struct AllPtr{

    //每次内存的分配,就会有一个PtrItem与之对应
    struct PtrItem{
        bool isEmpty = true;//表示该PtrItem是否为空
        void * ptr = nullptr; //存储分配的指针
        std::size_t size = 0;//存储分配的内存大小
        struct timespec now;//存储分配内存的时间
        char * stack = nullptr;//存储分配内存的调用栈
    };

    PtrItem * allptr = nullptr;//PtrItem数组头指针
    std::size_t size = 0;//PtrItem数组大小
    int line = 20;//辅助打印的行数

    //判断PtrItem数组是否满
    bool isFull() const{
        bool full = true;
        for(auto i =0;i<size;i++){
            if(allptr[i].isEmpty){
                full = false;
                break;
            }
        }
        return full;
    }

    //创建一个新的PtrItem数组,它的大小是老数组大小的2倍,并将老数组内容,复制到新数组中
    void createAndCopy(){
        std::size_t newSize = 0;
        if(size > 0 ){
            newSize = size * 2;
            ALOG(LOG_INFO, __FUNCTION__ , "create and copy %lu",newSize);
        }else{
            newSize = 1;
        }
        void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
        if (!libc_malloc) {
            ALOG(LOG_ERROR, __FUNCTION__ , "find lib malloc error");
            return ;
        }
        auto newPtr = (PtrItem *)libc_malloc(sizeof(PtrItem) * newSize);
        for(int i= 0;i<newSize;i++){
            newPtr[i].isEmpty = true;
            newPtr[i].ptr = nullptr;
            newPtr[i].size = 0;
            newPtr[i].now.tv_sec = 0;
            newPtr[i].now.tv_nsec = 0;
            newPtr[i].stack = nullptr;
        }
        //复制老数组内容
        for(int i = 0; i < size;i++){
            newPtr[i].isEmpty = allptr[i].isEmpty;
            newPtr[i].ptr = allptr[i].ptr;
            newPtr[i].size = allptr[i].size;
            newPtr[i].now.tv_sec= allptr[i].now.tv_sec;
            newPtr[i].now.tv_nsec= allptr[i].now.tv_nsec;
            newPtr[i].stack = allptr[i].stack;
        }
        if(allptr){
            void (*lib_free)(void*) = (void  (*)(void*))dlsym(RTLD_NEXT, "free");
            if (lib_free) {
                lib_free(allptr);
            }
        }
        allptr = newPtr;
        size = newSize;
    }

    //保存ptr,新建一个PtrItem
    void push(void * ptr,std::size_t size) {
        //是否为空//是否满
        if (!allptr || isFull()) {
            createAndCopy();
        }
        for (auto i = 0; i < size; i++) {
            if (allptr[i].isEmpty) {
                allptr[i].isEmpty = false;
                allptr[i].ptr = ptr;
                allptr[i].size = size;
                if (clock_gettime(CLOCK_REALTIME, &(allptr[i].now)) == -1) {
                    perror("clock_gettime");
                }
                //initialize
                void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
                if (libc_malloc) {
                    allptr[i].stack = (char * )libc_malloc( 1024*line);
                    //获取对应的调用栈
                    Find::Debug().printStackTrace(allptr[i].stack,1024*line);
                }
                return;
            }
        }
    }

    //弹出Ptr
    void pop(void *ptr) const {
        for(auto i=0;i<size;i++) {
            if(allptr[i].ptr == ptr) {
                allptr[i].isEmpty = true;
                if (allptr[i].stack){
                    void (*lib_free)(void*) = (void  (*)(void*))dlsym(RTLD_NEXT, "free");
                    if (lib_free) {
                        lib_free(allptr[i].stack);
                    }
                }
                return;
            }
        }
    }

    ~AllPtr(){

        if(allptr){
            for(auto i =0;i<size;i++){
                if(!allptr[i].isEmpty){
                    if (allptr[i].stack){
                        void (*lib_free)(void*) = (void  (*)(void*))dlsym(RTLD_NEXT, "free");
                        if (lib_free) {
                            lib_free(allptr[i].stack);
                        }
                    }
                    break;
                }
            }
            void (*lib_free)(void*) = (void  (*)(void*))dlsym(RTLD_NEXT, "free");
            if (lib_free) {
                lib_free(allptr);
            }
        }
    }

    //在合适的时候,dump出PtrItem中的内容,便于分析
    //在这里,只是简单的将其输出到Log系统
    //事实上,应该按照时间进行采样,然后输出到文件中,通过其他工具将其图形化
    //这里为了简化远离,仅仅将其输出到log系统中
    void dump(){
        for(auto i=0;i<size;i++){
            if(allptr[i].isEmpty == false){
                time_t t = allptr[i].now.tv_sec;
                struct tm *local;
                local = localtime(&t);
                if (local == NULL) {
                    perror("localtime");
                }
                ALOG(LOG_INFO, __FUNCTION__ , "%02d:%02d:%02d:%04d,ptr %p,size %lu",
                    local->tm_hour,local->tm_min,local->tm_sec,allptr[i].now.tv_nsec/1000000,
                     allptr[i].ptr,allptr[i].size);
                for(int i=0;i<line;i++){
                    ALOG(LOG_INFO, __FUNCTION__ ,"%s",allptr[i].stack+(i*1024));
                }


            }
        }
    }
};

在上面的例子中,使用Find::Debug().printStackTrace来获取android的native调用栈
它的实现如下:

namespace Find {
  
    //将调用栈信息打印到buffer中
    void Debug::printStackTrace(char * buffer,int size){
        const auto maxStackDeep = 10;
        intptr_t stackBuf[maxStackDeep];
        for(int i = 0; i < maxStackDeep; ++i){
            stackBuf[i] = 0;
        }
        memset(buffer, 0, size);
        dumpBacktraceIndex(buffer, stackBuf, captureBacktrace(stackBuf, maxStackDeep));
    }

    //捕获调用栈信息
    size_t Debug::captureBacktrace(intptr_t *buffer, size_t maxStackDeep) {
        BacktraceState state = {buffer, buffer + maxStackDeep};
        _Unwind_Backtrace(unwindCallback, &state);
        return state.current - buffer;
    }

    //解析调用栈信息
    void Debug::dumpBacktraceIndex(char *out, intptr_t *buffer, size_t count) {
        for (size_t idx = 0; idx < count; ++idx) {
            intptr_t addr = buffer[idx];
            const char *symbol = "      ";
            const char *dlfile = "      ";
            void * baseA = nullptr;

            Dl_info info;
            info.dli_fbase = nullptr;
            if (dladdr((void *) addr, &info)) {
                if (info.dli_sname) {
                    symbol = info.dli_sname;
                }
                if (info.dli_fname) {
                    dlfile = info.dli_fname;
                }
                if(info.dli_fbase){
                    baseA = info.dli_fbase;
                }

            } else {
                strcat(out, "#                               \n");
                continue;
            }
            char temp[50];
            memset(temp, 0, sizeof(temp));
            sprintf(temp, "%zu", idx);
            strcat(out, "#");
            strcat(out, temp);
            strcat(out, ": ");
            memset(temp, 0, sizeof(temp));
            sprintf(temp, "%p", (void *) addr);
            strcat(out, temp);
            strcat(out, "  ");
            memset(temp, 0, sizeof(temp));
            //尤其要注意,这里的两地址相减,这是为了让addr2line工具能够正常解析
            sprintf(temp, "%p", (void *) ((long)addr -  (long )baseA));
            strcat(out, temp);
            strcat(out, "  ");
            strcat(out, symbol);
            strcat(out, "      ");
            strcat(out, dlfile);
            strcat(out, "\n");
        }
    }

    _Unwind_Reason_Code Debug::unwindCallback(struct _Unwind_Context *context, void *arg) {
        BacktraceState *state = static_cast<BacktraceState *>(arg);
        intptr_t ip = (intptr_t) _Unwind_GetIP(context);
        if (ip) {
            if (state->current == state->end) {
                return _URC_END_OF_STACK;
            } else {
                state->current[0] = ip;
                state->current++;
            }
        }
        return _URC_NO_REASON;


    }
}

注意:在Debug中,并没有使用backtrace函数,这是因为,backtrace函数是由GNUC库提供的。而android的c库,是一个被称为bionic的Google单独编写的c库,并没有提供backtrace函数。因此我们选择了libunwind库中的_Unwind_Backtrace。

然后使用dladdr函数进行地址的解析。

需要再次注意的是:addr地址为内存中的地址,并不能直接使用
addr2line进行直接转换。需要将addr和共享库的基址相减,得到相对地址,才能够被add2line进行转化。

有了上面的malloc,还需要有free函数,如下

//释放对应的ptr
void free (void* ptr){
    //删除Allptr中的ptr
    popFromAllptr(ptr);
    void (*lib_free)(void*) = (void  (*)(void*))dlsym(RTLD_NEXT, "free");
    if (lib_free) {
        lib_free(ptr);
    }
}

增加一个广播,用于测试

为了能够直观的观测到记录下来的内存,现在增加一个广播,如下:

application.registerReceiver(object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                Timber.d(intent.action)
                native_dumpCpluplusObjecs()
            }
        }, IntentFilter("find.dump.objects"))

当收到"find.dump.objects"时,会调用native_dumpCpluplusObjecs。它的实现如下:


extern AllPtr ptrBuffer;

extern "C"
JNIEXPORT void JNICALL
Java_cn_findpiano_romsdk_hardware_midi_MidiManager_native_1dumpCpluplusObjecs(JNIEnv *env,
                                                                              jobject thiz) {
    ptrBuffer.dump();
}

我们调用ptrBuffer的dump函数,将记录下来的内存信息输出到log系统中。

现在进入adb shell ,输入如下的命令

am broadcast -a find.dump.objects

那么将会在log系统看到如下的输出
在这里插入图片描述

注意:自定义malloc函数,并不会影响realloc和calloc,因为我目前的项目并没有使用这两函数,因此也没有做演示,但他们的核心思想同malloc一样

c++中的new和delete实现

对于c++这门语言而言,它的new和delete以及new[] delete[]都是直接调用对应的运算符函数,如void* operator new(std::size_t sz);

因此我们可以重载这些运算符函数,以达到监控内存的目的。事实上android的c++库,不管是来自LLVM项目的libc++库。还是,来自android自定义的libstdc++库,都会间接调用,android的libc库的分配函数和释放函数。

注意注意:Android的libstdc++.so和来自GNU项目的libstdc++.so是不一样的。具体原因可以参看
ndk的问题744:https://github.com/android/ndk/issues/744

即然会间接调用libc的分配函数,那么上面关于malloc和free的实现,也已经覆盖了c++的使用。

但是这里为了演示说明,将重新实现void* operator new(std::size_t sz),然后它使用系统提供的malloc,而不是我们自己实现的malloc,下面的代码来自c++中的例子,并增加了部分,前面介绍的内容,如使用libc中的malloc

void* operator new(std::size_t sz)
{
    if (sz == 0)
        ++sz; // avoid std::malloc(0) which may return nullptr on success
    void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
    if (libc_malloc) {
        if (void *ptr = libc_malloc(sz)){
            ALOGD("new an object %lu,ptr %p",sz,ptr);
            addToAllptr(ptr,sz);
            return ptr;
        }
    }

    throw std::bad_alloc{}; 
}

// no inline, required by [replacement.functions]/3
void* operator new[](std::size_t sz)
{
    if (sz == 0)
        ++sz; // avoid std::malloc(0) which may return nullptr on success

    void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
    if (libc_malloc) {
        if (void *ptr = libc_malloc(sz)){
            ALOGD("new an array object %lu,ptr %p",sz,ptr);
            addToAllptr(ptr,sz);
            return ptr;
        }
    }
    throw std::bad_alloc{}; 
}

void operator delete(void* ptr) noexcept
{
    ALOGD("delete an object %p",ptr);
    popFromAllptr(ptr);
    void (*lib_free)(void*) = (void  (*)(void*))dlsym(RTLD_NEXT, "free");
    if (lib_free) {
        lib_free(ptr);
    }
}


void operator delete[](void* ptr) noexcept
{
    ALOGD("delete an array object %p",ptr);
    popFromAllptr(ptr);
    void (*lib_free)(void*) = (void  (*)(void*))dlsym(RTLD_NEXT, "free");
    if (lib_free) {
        lib_free(ptr);
    }
}

上面关于c++的new和delete仅仅是演示使用。c++提供了更加丰富的自定义分配函数的操作。

注意:在上面的代码中,我们使用了dlsym(RTLD_NEXT, “malloc”);来查找下一个叫做malloc的函数,如果有多个so库且都含有malloc,这样的行为,将是根据搜索路径中出现的先后顺序决定。

通过在一段时间中,多次dump,然后使用shell脚本,按照时间排序,可以非常轻松的观测到长期占据且没有释放的内存。然后使用对应的调用栈,找到源码进行分析。

至此,关于手动实现内存的分配和释放,并保存调用栈。已完成。

使用此种方法,基本上只会影响本so库中内容,这取决于编译链接的机制,那么怎样能够监控所有的内存分配呢,下一节molloc_hooks将会介绍这部分内容。敬请期待

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

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

相关文章

LVS+Keepalived负载均衡高可用群集(往事清零,万事顺意)

一、Keepalived高可用详解 1.应用场景 在企业应用中&#xff0c;单台服务器承担应用存在单点故障的危险。单点故障一旦发生&#xff0c;企业服务将发生中断&#xff0c;造成极大的危害。所以需要群集实现高可用性&#xff0c;保证服务稳定。 2.介绍和原理简介 Keepalived是…

【JAVA开发环境配置】 卸载JDK很简单, 一分钟帮你搞定!

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;web开发者、设计师、技术分享博主 &#x1f40b; 希望大家多多支持一下, 我们一起进步&#xff01;&#x1f604; &#x1f3c5; 如果文章对你有帮助的话&#xff0c;欢迎评论 &#x1f4ac;点赞&#x1…

中科三方:DNS云解析技术浅析

智能DNS云解析通过其智能解析&#xff0c;健康监测&#xff0c;负载均衡&#xff0c;宕机切换等高可用性的功能特性&#xff0c;给客户带来快捷&#xff0c;安全&#xff0c;流畅的上网体验。传统的DNS因为其解析时间冗长&#xff0c;易被劫持&#xff0c;无法精准调配用户的流…

【jupyter notebook】Anaconda prompt查询版本包(已安装的包列表、可以安装的包列表)

目录 0.环境介绍 1.查询当前已安装的某包信息 2.查询某包的所有版本 3.查看已安装的各个包的版本 0.环境介绍 windows Anaconda 1.查询当前已安装的某包信息 信息包含包名和版本&#xff0c;以包【matplotlib】为例 conda list matplotlib 2.查询某包的所有版本 conda s…

playwright-自动化测试

这里写目录标题 安装运行记录操作执行脚本 安装 &#xff08;1&#xff09;安装Playwright依赖库&#xff08;Playwright支持Async\Await语法&#xff0c;故需要Python3.7&#xff09; pip install playwright &#xff08;2&#xff09;安装Chromium、Firefox、WebKit等浏览…

【Unity Shader】平面投影实现阴影

介绍 球体和立方体挂载下面这个shader&#xff0c;就是多渲染一个阴影投影到y0的平面上 // shader&#xff0c;放在需要显示阴影的对象上 Shader "Custom/PlanarShadow1" {Properties{_Instensity("Shininess", Range(2, 4)) 2.0 //光照强度_Diffuse(&…

Mac下自定义快捷键

1、系统偏好设置-键盘-快捷键-App快捷键 2、确定要对什么进行自定义 比如 我要对DBeaver的运行自定义&#xff0c;我个人喜欢用cmd r 3、添加App快捷键 注意&#xff0c;菜单标题就是应用里的&#xff0c;必须要一模一样&#xff0c;哪怕是空格都不能少。也不能多。 4、对…

在Excel中如何筛选五笔码表?

你可以按照以下步骤筛选五笔码表中的简词&#xff1a; 1. 在 Excel 中打开五笔码表&#xff0c;选中需要筛选的数据列。 2. 点击“数据”选项卡中的“筛选”按钮&#xff0c;在下拉菜单中选择“高级筛选”。 3. 在弹出的“高级筛选”对话框中&#xff0c;选择“复制到其他位置…

什么是MBR/UEFI/GPT?MBR与UEFI具体区别是什么?

一、什么是MBR MBR是Master Boot Record&#xff08;主引导记录&#xff09;的缩写&#xff0c;是存储在计算机硬盘的第一个扇区&#xff08;通常是第0个扇区&#xff09;中的一个特殊的引导扇区。 MBR包含了启动计算机所需的信息&#xff0c;例如分区表、引导程序等。当计算机…

Python的接口自动化-unittest测试框架和ddt数据驱动

目录 引言 一、unittest测试框架 二、ddt数据驱动 引言 在编写接口自动化用例时&#xff0c;我们一般针对一个接口建立一个.py文件&#xff0c;一条接口测试用例封装为一个函数&#xff08;方法&#xff09;&#xff0c;但是在批量执行的过程中&#xff0c;如果其中一条出错…

#include < > 和#include ” ”有什么区别?

C语言头文件 C语言中头文件的扩展名为 .h的头文件&#xff0c;头文件中包含了函数的声明和宏定义&#xff0c;头文件可以被多个源文件引用。在C语言中有两种类型的头文件&#xff0c;一种是编译器自带的头文件&#xff0c;另一种就是程序员自己编写的头文件。 在C程序中如果要使…

package和import关键字的使用

package(包) package&#xff0c;称为包&#xff0c;用于指明该文件中定义的类、接口等结构所在的包。 8.1.1 语法格式 package 顶层包名.子包名 ; 包的作用 • 包可以包含类和子包&#xff0c;划分项目层次&#xff0c;便于管理 • 控制访问权限 import(导入) 为了使用定…

python基础知识(十三):numpy库的基本用法

目录 1. numpy的介绍2. numpy库产生矩阵2.1 numpy将列表转换成矩阵2.2 numpy创建矩阵 3. numpy的基础运算4. numpy的基础运算25. 索引 1. numpy的介绍 numpy库是numpy是python中基于数组对象的科学计算库。 2. numpy库产生矩阵 2.1 numpy将列表转换成矩阵 import numpy as …

中国人民大学与加拿大女王大学金融硕士——不断充实自己的知识库

在职场我们总是悄无声息的崩溃又悄无声息的自愈。别人拥有的&#xff0c;只要努力&#xff0c;你也会拥有。真正的成功源于生命里的学习&#xff0c;成长和热爱&#xff0c;以及不停步。一直前向走就是巅峰&#xff0c;中国人民大学与加拿大女王大学金融硕士助力我们在职提升。…

锂电池移动电源等电信终端产品配套用电源适配器/充电器产品纳入CCC范围

2023年8月1日起&#xff0c;以下三类产品纳入CCC认证范围&#xff1a; 1、便携式产品用锂离子电池和电池组(0915) 2、移动电源(0914) 3、电信终端产品配套用电源适配器/充电器(0807, 0907) 以上文章来源网络&#xff0c;如有涉及侵权&#xff0c;请联系删除&#xff01;我们一直…

部署Alertmanager对prometheus监控检测飞书报警通知

告警效果 一、编写alertmanager.yml 创建个目录存放alertmanager.yml文件 mkdir -p /data/alertmanager vi alertmanager.ymlroute:group_by: [alertname]group_wait: 30sgroup_interval: 30srepeat_interval: 1mreceiver: web.hook receivers:- name: web.hookwebhook_confi…

独立站买家必知的支付方式优缺点_FP独立站卖家必知的安全收款方式

做独立站该怎么收款是卖家首要考虑&#xff0c;除去衡量费用是否有潜在费用&#xff0c;更重要的能否符合消费者的付款习惯&#xff0c;销售的产品不同市场不同&#xff0c;所用到的收款方式也不相同。那么独立站的支付方式都有哪些呢&#xff1f;他们各有什么优缺点&#xff1…

HarmonyOS学习路之开发篇—Java UI框架(StackLayout)

StackLayout StackLayout直接在屏幕上开辟出一块空白的区域&#xff0c;添加到这个布局中的视图都是以层叠的方式显示&#xff0c;而它会把这些视图默认放到这块区域的左上角&#xff0c;第一个添加到布局中的视图显示在最底层&#xff0c;最后一个被放在最顶层。上一层的视图…

硅晶片的清洗技术

引言 高质量的晶圆在晶体精度、成型质量和表面质量方面都很优越&#xff0c;所以增加LSIs&#xff08;大规模集成电路&#xff09;的集成密度需要更高质量的硅晶片&#xff0c;但我们必须考虑芯片尺寸和制造成本增加的问题。它们会导致互连故障和晶体缺陷&#xff0c;退化设备…

可视化分析碳化硅产业,我国2022年碳化硅功率器件应用规模达近百亿元

碳化硅&#xff08;SiC&#xff09;&#xff0c;又叫金刚砂&#xff0c;它是第三代化合物的半导体原材料。在新能源市场行业发展的推动下&#xff0c;能源的高效率利用转化&#xff0c;带动了碳化硅&#xff08;SiC&#xff09;产业市场的快速发展。 下面我们来利用可视化图表…