Android系统的Ashmem匿名共享内存子系统分析(4)- Ashmem子系统的 Java访问接口

news2024/10/6 4:09:53

声明

  • 其实对于Android系统的Ashmem匿名共享内存系统早就有分析的想法,记得2019年6、7月份Mr.Deng离职期间约定一起对其进行研究的,但因为我个人问题没能实施这个计划,留下些许遗憾…
  • 文中参考了很多书籍及博客内容,可能涉及的比较多先不具体列出来了;
  • 本文使用的代码是LineageOS的cm-14.1,对应Android 7.1.2,可以参考我的另一篇博客:cm-14.1 Android系统启动过程分析(1)-如何下载Nexus5的LineageOS14.1(cm-14.1)系统源码并编译、刷机
  • 若对JNI不熟悉,可参考此专栏:Androd系统的JNI与NDK

1 Ashmem的架构

  Android 系统实现的 Ashmem 匿名共享内存子系统,用来在应用程序之间共享数据。Ashmem 与传统的Linux系统实现的共享内存一样,都是基于内核提供的临时文件系统tmpfs实现的,但是 Ashmem 对内存块进行了更为精细化的管理。应用程序可以动态地将一块匿名共享内存划分为若干个小块,当这些小块内存不再需要使用时,它们就可以被内存管理系统回收。通过这种动态的、分而治之的内存管理方式,Android系统就能够有效地使用系统内存,适应内存较小的移动设备环境。

  匿名共享内存系统是以Ashmem驱动程序为基础的,系统中所有的匿名共享内存都由Ashmem驱动程序负责分配和管理。Android系统在 Native 层提供了 C/C++ 调用接口和 Framework 层提供了 Java 调用接口。

  • 在Framework 层中,提供了两个C++类 MemoryBase 和 MemoryHeapBase,以及一个 Java 类 MemoryFile 来使用匿名共享内存。
  • 在运行时库 cutils 中,主要提供了三个C函数 ashmem_create_region、ashmem_pin_region 和 ashmem_unpin_region 来访问 Ashmem 驱动程序。

  Ashmem驱动程序在启动时,会创建一个 /dev/ashmem 设备文件,这样,运行时库 cutils 中的匿名共享内存接口就可以通过文件操作函数 open 和 ioctl 等来访问 Ashmem 驱动程序。
在这里插入图片描述

  传统的 Linux 系统使用一个整数来标志一块共享内存,而 Android 系统则使用一个文件描述符来标志一块匿名共享内存。使用文件描述符来描述一块匿名共享内存有两个好处:

  1. 可以方便地将它映射到进程的地址空间,从而可以直接访问它的内容;
  2. 可以使用 Binder 进程间通信机制来传输这个文件描述符,从而实现在不同的应用程序之间共享一块匿名内存。

  Binder 进程间通信机制使用一个类型为 BINDER_TYPE_FD 的 Binder 对象来描述一个文件描述符,当 Binder 驱动程序发现进程间通信数据中包含有这种 Binder 对象时,就会将对应的文件描述符复制到目标进程中,从而实现在两个进程中共享同一个文件。

2 Ashmem子系统 Java 访问接口分析

  在Android的 Framework 层中通过使用接口 MemoryFile 来封装匿名共享内存文件的创建和使用接口 MemoryFile 在:frameworks/base/core/java/android/os/MemoryFile.java,具体实现代码如下所示:

public class MemoryFile
{
    private static String TAG = "MemoryFile";

    // mmap(2) protection flags from <sys/mman.h>
    private static final int PROT_READ = 0x1;
    private static final int PROT_WRITE = 0x2;

    private static native FileDescriptor native_open(String name, int length) throws IOException;
    // returns memory address for ashmem region
    private static native long native_mmap(FileDescriptor fd, int length, int mode)
            throws IOException;
    private static native void native_munmap(long addr, int length) throws IOException;
    private static native void native_close(FileDescriptor fd);
    private static native int native_read(FileDescriptor fd, long address, byte[] buffer,
            int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException;
    private static native void native_write(FileDescriptor fd, long address, byte[] buffer,
            int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException;
    private static native void native_pin(FileDescriptor fd, boolean pin) throws IOException;
    private static native int native_get_size(FileDescriptor fd) throws IOException;

    private FileDescriptor mFD;        // ashmem file descriptor
    private long mAddress;   // address of ashmem memory
    private int mLength;    // total length of our ashmem region
    private boolean mAllowPurging = false;  // true if our ashmem region is unpinned

    /**
     * Allocates a new ashmem region. The region is initially not purgable.
     *
     * @param name optional name for the file (can be null).
     * @param length of the memory file in bytes, must be non-negative.
     * @throws IOException if the memory file could not be created.
     */
    public MemoryFile(String name, int length) throws IOException {
        mLength = length;
        if (length >= 0) {
            mFD = native_open(name, length);
        } else {
            throw new IOException("Invalid length: " + length);
        }

        if (length > 0) {
            mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE);
        } else {
            mAddress = 0;
        }
    }

    /**
     * Closes the memory file. If there are no other open references to the memory
     * file, it will be deleted.
     */
    public void close() {
        deactivate();
        if (!isClosed()) {
            native_close(mFD);
        }
    }

    /**
     * Unmaps the memory file from the process's memory space, but does not close it.
     * After this method has been called, read and write operations through this object
     * will fail, but {@link #getFileDescriptor()} will still return a valid file descriptor.
     *
     * @hide
     */
    void deactivate() {
        if (!isDeactivated()) {
            try {
                native_munmap(mAddress, mLength);
                mAddress = 0;
            } catch (IOException ex) {
                Log.e(TAG, ex.toString());
            }
        }
    }

    /**
     * Checks whether the memory file has been deactivated.
     */
    private boolean isDeactivated() {
        return mAddress == 0;
    }

    /**
     * Checks whether the memory file has been closed.
     */
    private boolean isClosed() {
        return !mFD.valid();
    }

    @Override
    protected void finalize() {
        if (!isClosed()) {
            Log.e(TAG, "MemoryFile.finalize() called while ashmem still open");
            close();
        }
    }

    /**
     * Returns the length of the memory file.
     *
     * @return file length.
     */
    public int length() {
        return mLength;
    }

    /**
     * Is memory file purging enabled?
     *
     * @return true if the file may be purged.
     */
    public boolean isPurgingAllowed() {
        return mAllowPurging;
    }

    /**
     * Enables or disables purging of the memory file.
     *
     * @param allowPurging true if the operating system can purge the contents
     * of the file in low memory situations
     * @return previous value of allowPurging
     */
    synchronized public boolean allowPurging(boolean allowPurging) throws IOException {
        boolean oldValue = mAllowPurging;
        if (oldValue != allowPurging) {
            native_pin(mFD, !allowPurging);
            mAllowPurging = allowPurging;
        }
        return oldValue;
    }

    /**
     * Creates a new InputStream for reading from the memory file.
     *
     @return InputStream
     */
    public InputStream getInputStream() {
        return new MemoryInputStream();
    }

    /**
     * Creates a new OutputStream for writing to the memory file.
     *
     @return OutputStream
     */
     public OutputStream getOutputStream() {
        return new MemoryOutputStream();
    }

    /**
     * Reads bytes from the memory file.
     * Will throw an IOException if the file has been purged.
     *
     * @param buffer byte array to read bytes into.
     * @param srcOffset offset into the memory file to read from.
     * @param destOffset offset into the byte array buffer to read into.
     * @param count number of bytes to read.
     * @return number of bytes read.
     * @throws IOException if the memory file has been purged or deactivated.
     */
    public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count)
            throws IOException {
        if (isDeactivated()) {
            throw new IOException("Can't read from deactivated memory file.");
        }
        if (destOffset < 0 || destOffset > buffer.length || count < 0
                || count > buffer.length - destOffset
                || srcOffset < 0 || srcOffset > mLength
                || count > mLength - srcOffset) {
            throw new IndexOutOfBoundsException();
        }
        return native_read(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);
    }

    /**
     * Write bytes to the memory file.
     * Will throw an IOException if the file has been purged.
     *
     * @param buffer byte array to write bytes from.
     * @param srcOffset offset into the byte array buffer to write from.
     * @param destOffset offset  into the memory file to write to.
     * @param count number of bytes to write.
     * @throws IOException if the memory file has been purged or deactivated.
     */
    public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)
            throws IOException {
        if (isDeactivated()) {
            throw new IOException("Can't write to deactivated memory file.");
        }
        if (srcOffset < 0 || srcOffset > buffer.length || count < 0
                || count > buffer.length - srcOffset
                || destOffset < 0 || destOffset > mLength
                || count > mLength - destOffset) {
            throw new IndexOutOfBoundsException();
        }
        native_write(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);
    }

    /**
     * Gets a FileDescriptor for the memory file.
     *
     * The returned file descriptor is not duplicated.
     *
     * @throws IOException If the memory file has been closed.
     *
     * @hide
     */
    public FileDescriptor getFileDescriptor() throws IOException {
        return mFD;
    }

    /**
     * Returns the size of the memory file that the file descriptor refers to,
     * or -1 if the file descriptor does not refer to a memory file.
     *
     * @throws IOException If <code>fd</code> is not a valid file descriptor.
     *
     * @hide
     */
    public static int getSize(FileDescriptor fd) throws IOException {
        return native_get_size(fd);
    }

    private class MemoryInputStream extends InputStream {

        private int mMark = 0;
        private int mOffset = 0;
        private byte[] mSingleByte;

        @Override
        public int available() throws IOException {
            if (mOffset >= mLength) {
                return 0;
            }
            return mLength - mOffset;
        }

        @Override
        public boolean markSupported() {
            return true;
        }

        @Override
        public void mark(int readlimit) {
            mMark = mOffset;
        }

        @Override
        public void reset() throws IOException {
            mOffset = mMark;
        }

        @Override
        public int read() throws IOException {
            if (mSingleByte == null) {
                mSingleByte = new byte[1];
            }
            int result = read(mSingleByte, 0, 1);
            if (result != 1) {
                return -1;
            }
            return mSingleByte[0];
        }

        @Override
        public int read(byte buffer[], int offset, int count) throws IOException {
            if (offset < 0 || count < 0 || offset + count > buffer.length) {
                // readBytes() also does this check, but we need to do it before
                // changing count.
                throw new IndexOutOfBoundsException();
            }
            count = Math.min(count, available());
            if (count < 1) {
                return -1;
            }
            int result = readBytes(buffer, mOffset, offset, count);
            if (result > 0) {
                mOffset += result;
            }
            return result;
        }

        @Override
        public long skip(long n) throws IOException {
            if (mOffset + n > mLength) {
                n = mLength - mOffset;
            }
            mOffset += n;
            return n;
        }
    }

    private class MemoryOutputStream extends OutputStream {

        private int mOffset = 0;
        private byte[] mSingleByte;

        @Override
        public void write(byte buffer[], int offset, int count) throws IOException {
            writeBytes(buffer, offset, mOffset, count);
            mOffset += count;
        }

        @Override
        public void write(int oneByte) throws IOException {
            if (mSingleByte == null) {
                mSingleByte = new byte[1];
            }
            mSingleByte[0] = (byte)oneByte;
            write(mSingleByte, 0, 1);
        }
    }
}

  构造方法 MemoryFile 以指定的字符串调用了JNI方法 native_open,目的是建立一个匿名共享内存文件,这样可以得到一个文件描述符。然后使用这个文件描述符为参数调用JNI方法natvie_mmap,并把匿名共享内存文件映射到进程空间中这样就可以通过映射得到地址空间的方式直接访问内存数据。
  成员函数 readBytes 用于读取某一块匿名共享内存的内容,成员函数 writeBytes 用于写入某一块匿名共享内存的内容,成员函数 isDeactivated 用于保证匿名共享内存已经被映射到进程的地址空间中。

  以上native方法对应实现在源码文件:frameworkslbaselcorejniandroid_os_MemoryFile.cpp 中,具体实现代码如下所示:

#define LOG_TAG "MemoryFile"
#include <utils/Log.h>

#include <cutils/ashmem.h>
#include "core_jni_helpers.h"
#include "JNIHelp.h"
#include <unistd.h>
#include <sys/mman.h>


namespace android {

static jobject android_os_MemoryFile_open(JNIEnv* env, jobject clazz, jstring name, jint length)
{
    const char* namestr = (name ? env->GetStringUTFChars(name, NULL) : NULL);

    int result = ashmem_create_region(namestr, length);

    if (name)
        env->ReleaseStringUTFChars(name, namestr);

    if (result < 0) {
        jniThrowException(env, "java/io/IOException", "ashmem_create_region failed");
        return NULL;
    }

    return jniCreateFileDescriptor(env, result);
}

static jlong android_os_MemoryFile_mmap(JNIEnv* env, jobject clazz, jobject fileDescriptor,
        jint length, jint prot)
{
    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
    void* result = mmap(NULL, length, prot, MAP_SHARED, fd, 0);
    if (result == MAP_FAILED) {
        jniThrowException(env, "java/io/IOException", "mmap failed");
    }
    return reinterpret_cast<jlong>(result);
}

static void android_os_MemoryFile_munmap(JNIEnv* env, jobject clazz, jlong addr, jint length)
{
    int result = munmap(reinterpret_cast<void *>(addr), length);
    if (result < 0)
        jniThrowException(env, "java/io/IOException", "munmap failed");
}

static void android_os_MemoryFile_close(JNIEnv* env, jobject clazz, jobject fileDescriptor)
{
    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
    if (fd >= 0) {
        jniSetFileDescriptorOfFD(env, fileDescriptor, -1);
        close(fd);
    }
}

static jint android_os_MemoryFile_read(JNIEnv* env, jobject clazz,
        jobject fileDescriptor, jlong address, jbyteArray buffer, jint srcOffset, jint destOffset,
        jint count, jboolean unpinned)
{
    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
    if (unpinned && ashmem_pin_region(fd, 0, 0) == ASHMEM_WAS_PURGED) {
        ashmem_unpin_region(fd, 0, 0);
        jniThrowException(env, "java/io/IOException", "ashmem region was purged");
        return -1;
    }

    env->SetByteArrayRegion(buffer, destOffset, count, (const jbyte *)address + srcOffset);

    if (unpinned) {
        ashmem_unpin_region(fd, 0, 0);
    }
    return count;
}

static jint android_os_MemoryFile_write(JNIEnv* env, jobject clazz,
        jobject fileDescriptor, jlong address, jbyteArray buffer, jint srcOffset, jint destOffset,
        jint count, jboolean unpinned)
{
    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
    if (unpinned && ashmem_pin_region(fd, 0, 0) == ASHMEM_WAS_PURGED) {
        ashmem_unpin_region(fd, 0, 0);
        jniThrowException(env, "java/io/IOException", "ashmem region was purged");
        return -1;
    }

    env->GetByteArrayRegion(buffer, srcOffset, count, (jbyte *)address + destOffset);

    if (unpinned) {
        ashmem_unpin_region(fd, 0, 0);
    }
    return count;
}

static void android_os_MemoryFile_pin(JNIEnv* env, jobject clazz, jobject fileDescriptor, jboolean pin)
{
    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
    int result = (pin ? ashmem_pin_region(fd, 0, 0) : ashmem_unpin_region(fd, 0, 0));
    if (result < 0) {
        jniThrowException(env, "java/io/IOException", NULL);
    }
}

static jint android_os_MemoryFile_get_size(JNIEnv* env, jobject clazz,
        jobject fileDescriptor) {
    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
    // Use ASHMEM_GET_SIZE to find out if the fd refers to an ashmem region.
    // ASHMEM_GET_SIZE should succeed for all ashmem regions, and the kernel
    // should return ENOTTY for all other valid file descriptors
    int result = ashmem_get_size_region(fd);
    if (result < 0) {
        if (errno == ENOTTY) {
            // ENOTTY means that the ioctl does not apply to this object,
            // i.e., it is not an ashmem region.
            return (jint) -1;
        }
        // Some other error, throw exception
        jniThrowIOException(env, errno);
        return (jint) -1;
    }
    return (jint) result;
}

static const JNINativeMethod methods[] = {
    {"native_open",  "(Ljava/lang/String;I)Ljava/io/FileDescriptor;", (void*)android_os_MemoryFile_open},
    {"native_mmap",  "(Ljava/io/FileDescriptor;II)J", (void*)android_os_MemoryFile_mmap},
    {"native_munmap", "(JI)V", (void*)android_os_MemoryFile_munmap},
    {"native_close", "(Ljava/io/FileDescriptor;)V", (void*)android_os_MemoryFile_close},
    {"native_read",  "(Ljava/io/FileDescriptor;J[BIIIZ)I", (void*)android_os_MemoryFile_read},
    {"native_write", "(Ljava/io/FileDescriptor;J[BIIIZ)V", (void*)android_os_MemoryFile_write},
    {"native_pin",   "(Ljava/io/FileDescriptor;Z)V", (void*)android_os_MemoryFile_pin},
    {"native_get_size", "(Ljava/io/FileDescriptor;)I",
            (void*)android_os_MemoryFile_get_size}
};

int register_android_os_MemoryFile(JNIEnv* env)
{
    return RegisterMethodsOrDie(env, "android/os/MemoryFile", methods, NELEM(methods));
}

}

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

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

相关文章

【云原生】Docker镜像的创建

1.Dokcer镜像的创建 创建镜像有三种方法&#xff0c;分别为【基于已有镜像创建】、【基于本地模板创建】以及【基于Dockerfile创建】。 1.1 基于现有镜像创建 &#xff08;1&#xff09;首先启动一个镜像&#xff0c;在容器里做修改 docker run -it --name web centos:7 /…

2023年6月 国内大语言模型对比【国内模型正在崛起】

先说一下这个文章怎么来的。因为朋友问我大语言模型可以生成公务员面试回答不&#xff0c;我说可以啊。之前看文心有这个服务。我想最近好几个模型也没用了测一把&#xff01;结果&#xff01;大吃一惊&#xff01;我觉得我的三个傻孩子长大了&#xff01;&#xff08;chatglm1…

chatgpt赋能python:Python如何降低版本:提升代码兼容性与SEO效果

Python如何降低版本&#xff1a;提升代码兼容性与SEO效果 在大多数情况下&#xff0c;使用Python的最新版本是最好的选择。新版本通常提供更好的性能和更多的功能&#xff0c;同时也有更好的安全性和稳定性。然而&#xff0c;有些情况下&#xff0c;我们需要运行旧版本的Pytho…

chatgpt赋能python:Python字体如何调大?一篇全面的教程

Python字体如何调大&#xff1f;一篇全面的教程 什么是Python&#xff1f; Python是一种高级编程语言&#xff0c;一般被用于Web开发、数据分析和人工智能等领域。其编写简单、易读易学易维护&#xff0c;因此被广泛使用。 为什么要调大Python字体&#xff1f; 在Python编程…

imagine 关键词绘图( Midjourney )

前几天从网上看到的imagine 关键词绘图( Midjourney )&#xff0c;感觉挺好用&#xff0c;分享给大家&#xff1a; 一、基本关键词: 一个基本的提示可以简单到一个单词、短语或表情符号。 二、高级关键词: 可以包括一个或多个图像链接、多个文本短语或单词&#xff0c;以及…

内网安全:隧道技术详解

目录 隧道技术 反向连接技术 反向连接实验所用网络拓扑图及说明 网络说明 防火墙限制说明 实验前提说明 实战一&#xff1a;CS反向连接上线 - 拿下Win2008 一. 使用转发代理上线创建监听器 二. 上传后门执行上线 隧道技术 - SMB协议 SMB协议介绍 实战二&#xff1a…

chatgpt赋能python:Python安装到C盘有什么方便之处?

Python安装到C盘有什么方便之处&#xff1f; 在进行Python编程时&#xff0c;安装Python到C盘是一个非常常见的做法。那么&#xff0c;将Python安装到C盘有哪些好处呢&#xff1f;下面&#xff0c;让我们来一一介绍。 1. 方便快捷 安装Python到C盘的好处之一就是非常方便&am…

chatgpt赋能python:Python安装etree:提高XML处理效率的关键

Python安装etree&#xff1a;提高XML处理效率的关键 在Python开发中&#xff0c;XML是一种常用的数据格式。然而&#xff0c;Python标准库自带的ElementTree在大规模数据处理时可能会出现效率低下的情况。因此&#xff0c;我们可以使用第三方模块etree来提高XML处理的效率。 …

案例37:基于Springboot旅游网站系统开题报告设计

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

Linux内核文件读取流程

本文代码基于Linux5.10。 当上层调用read函数读取一个文件时&#xff0c; Linux 内核究竟如何处理&#xff1f; 本文主要介绍这个问题 数据结构 address_space linux 的文件在磁盘上可能是不连续的&#xff0c; 但文件读取又需要将文件当成一个连续的字节流&#xff0c; 为…

大学物理(上)-期末知识点结合习题复习(1)——运动的描述(两类运动学知识点、圆周运动知识点、相对运动知识点)

目录 运动的描述 两类运动知识点 题1 题目描述 题解 题2&#xff08;第一类问题&#xff09; 题目描述 题解 题3&#xff08;第二类问题&#xff09; 题目描述 题解 题4 题目描述 题解 圆周运动知识点 题5 题目描述 题解 相对运动知识点 题6 题目描述…

【通知】关于SRRC认证无线电发射设备型号核准的通知

关于SRRC认证 无线电发射设备型号核准简称型号核准或SRRC&#xff0c;由工业和信息化部无线电管理局受理&#xff0c;中华人民共和国工业和信息化部审批发证。型号核准是指在无线电发射设备投入使用前的研制、生产、进口、购置等环节&#xff0c;对其频谱参数技术指标依法进行…

图文直播功能(互动功能接收端JS-SDK)

图文直播 功能概述 图文直播&#xff0c;即用直播画面与说明文字的形式&#xff0c;以时间轴方式完整详细地记录整场活动/课堂。 当用户错过直播的开始时间&#xff0c;中途进入直播&#xff0c;可通过图文直播的记录对错过的直播进行回顾了解&#xff0c;实现直播信息的有效…

【SpringBoot】SpringBoot Controller接收参数的常用方式

在Controller中加入RestController&#xff0c;效果等于Controller ResponseBody。 1 请求路径参数 直接把表单里面的参数写进 Controller 相应方法的形参中去&#xff0c;这个获取参数的方法适合get提交&#xff0c;而不适合post提交。 PathVariable获取(这个当然不分get和…

Flask学习笔记(1)创建RESTful API

从本文起&#xff0c;笔者将会更新一系列Flask学习笔记。   本文将会讲述在Flask中如何创建RESTful API。在此之前&#xff0c;我们有必要了解下什么是RESTful架构。 RESTful架构 RESTful架构自从2000年被Roy Feilding提出后就受到广泛关注&#xff0c;并被成功地应用于成千…

美国同事的那些离职类型

这次公司的裁员是教科书基本的真的是没错。 但是美国同事的心态和离职类型也让人打开眼界。 后来在公司同事建立的被裁群中才了解到&#xff0c;PM 组也是重灾区&#xff0c;也超过了 50%&#xff0c;不少在公司工作超过 5 年的 PM 都被毫不留情的裁掉了。 就算这次勉勉强强被…

怎么实现常用网络接口自动化测试框架应用?

一、RESTful&#xff08;resource representational state transfer)类型接口测试 (一&#xff09;GUI界面测试工具&#xff1a;jmeter 1、添加线程组 2、添加http请求 3、为线程组添加察看结果树 4、写入接口参数并运行 5、在查看结果树窗口查看结果 6、多组数据可增加CSVDat…

达梦主备守护集群介绍

DM数据守护一主一备或一主多备是一种集成化的高可用、高性能数据库解决方案&#xff0c;是数据库异地容灾的首选方案。通过部署 DM 数据守护&#xff0c;可以在硬件故障&#xff08;如磁盘损坏&#xff09;、自然灾害&#xff08;地震、火灾&#xff09;等极端情况下&#xff0…

javaScript蓝桥杯----收集帛书碎⽚

目录 一、介绍二、准备三、目标四、代码五、完成 一、介绍 三叔在外出考古途中⽆意发现了⼀份战国帛书&#xff0c;帛书边缘有被明显裁剪过的痕迹&#xff0c;单从帛书⽚段&#xff0c;提到记录了神秘⽂物的地点&#xff0c;⽆奈帛书不完整&#xff0c;为了早⽇将⽂物带回博物…

chatgpt赋能python:PythonGUI入门指南:如何安装各种GUI工具包

Python GUI入门指南&#xff1a;如何安装各种GUI工具包 如果你是一名Python开发人员&#xff0c;那么你肯定知道GUI是不可或缺的。它是让用户和程序之间进行交互的一种方法。在Python中&#xff0c;有很多GUI工具包可以选择。但是&#xff0c;安装它们可能会让人感到困惑。在这…