找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2152

积分

0

好友

308

主题
发表于 2025-12-30 04:02:02 | 查看: 26| 回复: 0

Stressapptest(Stressful Application Test)是一款用于模拟高负载环境的压力测试工具。它通过执行大量计算、数据处理和内存操作,来测试系统在多线程并发任务下的性能与稳定性。

主要命令行参数

工具通过一系列参数来控制测试行为,以下是一些核心参数:

  • -s:设置测试运行的秒数。
  • -m:指定内存复制线程(Memory Copy)的数量。
  • -i:指定内存反转线程(Invert Copy)的数量。
  • -c:启用CRC校验(Data Check)。
  • -C:指定CPU压力测试线程的数量。
  • -M:设置测试使用的内存大小(单位:兆字节)。注意,若设置值超过系统可用内存,进程可能会被终止。
  • 其他参数可参考源码文件 sat.cc

一个典型的使用示例如下:

./stressapptest -s 86400 -m 4 -i 4 -c 4 -C 4 -M xxx

官网提供的示例如下:

# 分配256MB内存,运行8个“预热复制”线程和8个CPU负载线程,运行20秒后退出。
./stressapptest -s 20 -M 256 -m 8 -C 8 -W
# 运行2个文件IO线程,并自动检测内存大小和核心数以选择分配的内存和内存复制线程数。
./stressapptest -f /tmp/file1 -f /tmp/file2

如何将 stressapptest 加入编译系统

在不同平台的Android系统源码中,可以通过修改编译配置文件来集成此工具。

  • 高通平台:在 device\qcom\common\base.mk 文件中添加:
    PRODUCT_PACKAGES += stressapptest
  • 瑞芯微(Rockchip)平台:例如在 device/rockchip/rk3562/rk3562_t/rk3562_t.mk 文件中进行添加。

RK3562平台Makefile配置截图
图1:在RK3562平台的Makefile中添加stressapptest编译项

APK调用方法

如果希望在Android应用中进行压力测试,可以将stressapptest可执行文件打包进APK并调用。

加载并拷贝stressapptest文件

首先需要将存放在Assets目录下的可执行文件复制到设备的数据目录,并赋予执行权限。以下是一个示例方法:

private boolean copyBin(String name) {
    File desFile = new File("/data/", name);
    Log.i(TAG, "copyBin, desFile: " + desFile.toString());
    // 从源码的assets的bin目录下,复制到/data/目录下,通常需要系统级应用权限
    if (copyAssetFile("bin/" + name ,desFile )) {
        // 授予文件执行权限
        ShellUtils.CommandResult result2 = ShellUtils.execCmd(
            "chmod 777 /data/" + name,
            true
        );
        Log.i(TAG, "copyBin, chmod result: " + result2.toString());
        return result2.result >= 0 && TextUtils.isEmpty(result2.errorMsg);
    }
    return false;
}

/**
 * Execute the command.
 *
 * @param command  The command.
 * @param isRooted True to use root, false otherwise.
 * @return the single {@link CommandResult} instance
 */
public static CommandResult execCmd(final String command, final boolean isRooted) {
    return execCmd(new String[]{command}, isRooted, true);
}

/**
 * Execute the command.
 *
 * @param commands        The commands.
 * @param isRooted        True to use root, false otherwise.
 * @param isNeedResultMsg True to return the message of result, false otherwise.
 * @return the single {@link CommandResult} instance
 */
public static CommandResult execCmd(final String[] commands,
                                    final boolean isRooted,
                                    final boolean isNeedResultMsg) {
    int result = -1;
    if (commands == null || commands.length == 0) {
        return new CommandResult(result, "", "");
    }
    Process process = null;
    BufferedReader successResult = null;
    BufferedReader errorResult = null;
    StringBuilder successMsg = null;
    StringBuilder errorMsg = null;
    DataOutputStream os = null;
    try {
        process = Runtime.getRuntime().exec(isRooted ? "su" : "sh");
        os = new DataOutputStream(process.getOutputStream());
        for (String command : commands) {
            if (command == null) continue;
            os.write(command.getBytes());
            os.writeBytes(LINE_SEP);
            os.flush();
        }
        os.writeBytes("exit" + LINE_SEP);
        os.flush();
        result = process.waitFor();
        if (isNeedResultMsg) {
            successMsg = new StringBuilder();
            errorMsg = new StringBuilder();
            successResult = new BufferedReader(
                new InputStreamReader(process.getInputStream(), "UTF-8")
            );
            errorResult = new BufferedReader(
                new InputStreamReader(process.getErrorStream(), "UTF-8")
            );
            String line;
            if ((line = successResult.readLine()) != null) {
                successMsg.append(line);
                while ((line = successResult.readLine()) != null) {
                    successMsg.append(LINE_SEP).append(line);
                }
            }
            if ((line = errorResult.readLine()) != null) {
                errorMsg.append(line);
                while ((line = errorResult.readLine()) != null) {
                    errorMsg.append(LINE_SEP).append(line);
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (os != null) {
                os.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (successResult != null) {
                successResult.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (errorResult != null) {
                errorResult.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (process != null) {
            process.destroy();
        }
    }
    return new CommandResult(
        result,
        successMsg == null ? "" : successMsg.toString(),
        errorMsg == null ? "" : errorMsg.toString()
    );
}

/**
 * The result of command.
 */
public static class CommandResult {
    public int result;
    public String successMsg;
    public String errorMsg;

    public CommandResult(final int result, final String successMsg, final String errorMsg) {
        this.result = result;
        this.successMsg = successMsg;
        this.errorMsg = errorMsg;
    }

    @Override
    public String toString() {
        return "result: " + result + "\n" +
               "successMsg: " + successMsg + "\n" +
               "errorMsg: " + errorMsg;
    }
}

调用执行

文件准备就绪后,可以在子线程中执行stressapptest命令。

private Runnable mStressAppTestRunnable = new Runnable() {
    @Override
    public void run() {
        Log.i(TAG, "stressapptest run...");
        DataOutputStream dos = null;
        try {
            Process process = Runtime.getRuntime().exec("su");
            dos = new DataOutputStream(process.getOutputStream());
            // 构造命令,例如运行指定时间,使用4个反转线程和4个CPU压力线程等
            String command = "/data/" + STRESS_APP_TEST + "  -s " + (mTargetTime - 10)
                    + " -i 4 -C 4 -W --stop_on_errors -M " + sMemory + "\n";
            dos.write(command.getBytes(Charset.forName("utf-8")));
            dos.flush();
            // 读取并输出测试日志
            String line;
            BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
            while (isRunning() && (line = bufferedReader.readLine()) != null) {
                Log.i(TAG, "stressapptest: " + line);
                Message msg = new Message();
                msg.what = COMMAND_UPDATE_LOG;
                msg.obj = line;
                mHandler.sendMessage(msg); // 通过handler将数据发送出去
            }
            dos.writeBytes("exit\n");
            dos.flush();
            Log.w(TAG, "stressapptest exit.");
        } catch (IOException e) {
            Log.e(TAG, e.getMessage(), e);
        } finally {
            try {
                if (dos != null) {
                    dos.close();
                }
            } catch (IOException e) {
                Log.e(TAG, e.getMessage(), e);
            }
        }
    }
};

stressapptest 源码分析

stressapptest的代码结构清晰,是学习系统级C++压力测试实现的良好材料。

stressapptest源码在IDE中的结构
图2:stressapptest项目在IDE中的源码文件结构

入口函数

程序入口main函数逻辑清晰,遵循典型的初始化-运行-清理流程:

int main(int argc, char **argv) {
    Sat *sat = SatFactory(); // 核心类,通过工厂函数创建
    if (sat == NULL) {
        logprintf(0, "Process Error: failed to allocate Sat object\n");
        return 255;
    }
    // 解析命令行参数
    if (!sat->ParseArgs(argc, argv)) {
        logprintf(0, "Process Error: Sat::ParseArgs() failed\n");
        sat->bad_status();
    } else if (!sat->Initialize()) { // 执行初始化
        logprintf(0, "Process Error: Sat::Initialize() failed\n");
        sat->bad_status();
    } else if (!sat->Run()) { // 运行压力测试
        logprintf(0, "Process Error: Sat::Run() failed\n");
        sat->bad_status();
    }
    // 打印测试结果
    sat->PrintResults();
    // 释放资源
    if (!sat->Cleanup()) {
        logprintf(0, "Process Error: Sat::Cleanup() failed\n");
        sat->bad_status();
    }

    int retval;
    // 根据状态返回结果
    if (sat->status() != 0) {
        logprintf(0, "Process Error: Fatal issue encountered. See above logs for "
                     "details.\n");
        retval = 1;
    } else if (sat->errors() != 0) {
        retval = 1;
    } else {
        retval = 0;
    }
    // 释放Sat对象内存
    delete sat;
    return retval;
}

Sat 类

main函数中初始化的Sat类是工具的核心,其创建通过简单的工厂函数完成:

Sat *SatFactory() {
    return new Sat();
}

该类主要职责包括:

  • 流程控制:解析参数、初始化、运行测试、打印结果、清理资源。
  • 线程管理:包含初始化线程、创建线程、等待线程结束和分析报告的函数。
  • 资源管理:数据成员用于保存配置参数、控制标志、内存和测试配置、资源及结果。
  • 多线程压力测试:利用pthreads库,包含针对内存、文件IO、网络IO、磁盘IO、CPU压力及缓存一致性等不同类型测试的特定方法。
  • 页面队列管理:使用队列结构管理内存页,支持单锁(SAT_ONELOCK)和细粒度锁(SAT_FINELOCK)两种并发模式,以平衡性能与线程安全。
    • 单锁模式:使用全局唯一锁保护整个队列,简单但高并发下可能成为性能瓶颈。
    • 细锁模式:对队列中的元素或子结构单独加锁,允许多线程并发访问不同部分,提高性能。
// 用于页面队列实现模式切换的枚举
enum PageQueueType { SAT_ONELOCK, SAT_FINELOCK };

Sat类功能模块思维导图
图3:Sat类的主要功能模块与线程类型思维导图

CPU 压测原理

CPU压力测试的核心是通过多线程并发执行计算密集型任务。用户通过-C参数指定线程数,工具使用pthread_create()创建对应数量的线程。每个线程执行的任务旨在最大化CPU利用率,例如进行浮点数数组的滑动平均计算:

// 通用的CPU压力工作负载,适用于任何CPU/平台。
// 浮点数组滑动平均计算。
bool OsLayer::CpuStressWorkload() {
    double float_arr[100];
    double sum = 0;
#ifdef HAVE_RAND_R
    unsigned int seed = 12345;
#endif

    // 用随机数初始化数组。
    for (int i = 0; i < 100; i++) {
#ifdef HAVE_RAND_R
        float_arr[i] = rand_r(&seed); //生成随机数
        if (rand_r(&seed) % 2)
            float_arr[i] *= -1.0;
#else
        srand(time(NULL));
        float_arr[i] = rand();  // NOLINT
        if (rand() % 2)         // NOLINT
            float_arr[i] *= -1.0;
#endif
    }

    // 计算滑动平均。通过频繁修改数组元素来加大计算量。
    for (int i = 0; i < 100000000; i++) {
        float_arr[i % 100] =
            (float_arr[i % 100] + float_arr[(i + 1) % 100] +
             float_arr[(i + 99) % 100]) / 3;
        sum += float_arr[i % 100];
    }

    // 防止循环被编译器优化掉的打印语句。
    if (sum == 0.0)
        logprintf(12, "Log: I'm Feeling Lucky!\n");
    return true;
}

DDR 压测原理

DDR内存压力测试主要通过对内存进行高强度的分配、访问和读写操作来实现,旨在测试内存带宽、延迟和稳定性。这类运维 & 测试对于确保服务器和嵌入式设备长期运行的可靠性至关重要。

  • 内存分配:通过-M参数分配指定大小的内存,由AllocateMemory()函数调用系统接口完成。
    // 分配测试所需的内存
    bool Sat::AllocateMemory() {
        bool result = os_->AllocateTestMem(size_, paddr_base_);
        if (!result) {
            logprintf(0, "Process Error: failed to allocate memory\n");
            bad_status();
            return false;
        }
        return true;
    }
  • 内存读写操作:在分配的内存区域进行密集访问,模式包括:
    • 顺序读写
    • 随机读写
    • 内存屏障与同步操作(增加多线程访问的复杂性)
      // 简单示例,具体实现请参阅源码
      void stress_memory(void* ptr, size_t size) {
      volatile char* data = (volatile char*)ptr;
      for (size_t i = 0; i < size; i++) {
          data[i] = (char)(i % 256); // 写入数据
          char temp = data[i];       // 读取数据
      }
      }

开源地址与参考资料

想要了解更多关于系统稳定性测试、性能调优的实践与讨论,欢迎访问 云栈社区 的对应板块。

参考资料

  1. CPU-BPU-DDR 压力测试: _https://developer.d-robotics.cc/rdk_doc/en/rdk_s/Advanced_development/linux_development/hardware_unit_test/bpu_cpu_ddr_stress_



上一篇:实战 RK3568 Ubuntu 22.04 根文件系统rootfs制作与定制
下一篇:Rockchip NPU实战:基于RKNN/RKLLM部署DeepSeek-R1与YOLOv5模型指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 18:36 , Processed in 0.248114 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表