并行计算——MPI编程

news2025/2/12 11:16:03

目录

基础知识

进程与线程,并行与并发

奇偶排序

MPI实现 odd-even sort

思路

环境部署

编程实现(C++)

“若干”的问题

参考链接


一个偶然的机会,我接触到了国立清华大学的MPI编程作业,也就接触到了并行计算。这次作业简单描述的话,就是使用MPI的并行计算能力实现一个奇偶排序,并理解他的原理、思想,下面是作业的原题。

In this homework, you are required to write a parallel odd-even transposition sort by using MPI. A a parallel odd-even transposition sort is performed as follows:

/* Initially, numbers are distributed to processes, respectively.*/

  1. For each process with odd rank P, send its number to the process with rank P-1.

  2. For each process with rank P- 1, compare its number with the number sent by the process with rank P and send the larger one back to the process with rank P.

  3. For each process with even rank Q, send its number to the process with rank Q-1.

  4. For each process with rank Q- 1, compare its number with the number sent by the process with rank Q and send the larger one back to the process with rank Q.

本文主要介绍并行计算的概念,以及MPI编程如何实现。

基础知识

进程与线程,并行与并发

进程与线程:

① 进程:可以理解为电脑任务管理器的一个个应用,他是系统资源分配的基本单位,一个进程可以被多个CPU执行。

② 线程:可以理解为进程的一部分,一个进程可以由多个线程组成,他是CPU调度执行的基本单位,一个线程不可以被多个CPU执行。

多任务执行的两种方式:

① 并发:在一段时间内交替去执行多个任务:任务数量大于CPU核心数<切换速度很快>

② 并行:在一段时间内真正同时一起执行多个任务:任务数量小于或等于CPU核心数

在python中,有许多模块,例如threading、multiprocessing都可以实现线程的并发、并行,一下是一段使用threading的代码示例:

import threading
import time
​
​
# 定义任务函数
def task(num):
    print('Thread %d is processing...' % num)
    time.sleep(1)  # 模拟任务执行时间
    print('Thread %d is done.' % num)
​
​
# 并行执行任务
def parallel_processing():
    threads = []
    start_time = time.time()  # 记录开始时间
    # 创建 5 个线程,并启动执行任务
    for i in range(5):
        t = threading.Thread(target=task, args=(i,))
        threads.append(t)
        t.start()
    # 等待所有线程都完成
    for t in threads:
        t.join()
    end_time = time.time()  # 记录结束时间
    print(f"Parallel processing time: {end_time - start_time:.2f}s")  # 输出并行处理所需时间
​
​
# 并发执行任务
def concurrent_processing():
    start_time = time.time()  # 记录开始时间
    # 创建 5 个线程,并启动执行任务
    for i in range(5):
        t = threading.Thread(target=task, args=(i,))
        t.start()
    end_time = time.time()  # 记录结束时间
    print(f"Concurrent processing time: {end_time - start_time:.2f}s")  # 输出并发处理所需时间
​
​
if __name__ == '__main__':
    parallel_processing()  # 并行处理任务
    concurrent_processing()  # 并发处理任务

一般情况下,线程并发比线程并行更快。这是因为线程并发使用的是单一CPU资源进行多个任务的切换执行,而线程并行则需要将任务分配到多个CPU核心上进行并行处理。由于存在线程切换的开销和多个CPU之间的数据交互的通信开销,线程并行可能会消耗更多的系统资源和时间,导致效率较低。但是,对于某些计算密集型或I/O密集型的任务,如果能够利用多核CPU的优势,采用线程并行方式可以取得更好的效果。

那么同样的道理,进程的并行通常比并发更快。但是,采用并发还是并行都必须结合具体情况,如程序所需的资源多少和当前的CPU个数进行具体分析。

奇偶排序

奇偶排序(odd even sort)是一个经典的排序算法,而将这个算法用于体现MPI编程的好处,则是更为经典的做法。

下面为奇偶排序示意动图以及python代码:

def odd_even_sort(arr):
    """
    奇偶排序函数,输入一个列表,返回一个按升序排列的新列表
    """
    n = len(arr)
    sorted_flag = 0
    while sorted_flag == 0:
        sorted_flag = 1
        for odd_even in range(2):
            # 如果是奇数轮,则从下标为1的位置开始,每隔2个元素比较一次
            # 如果是偶数轮,则从下标为0的位置开始,每隔2个元素比较一次
            for i in range(odd_even, n - 1, 2):
                if arr[i] > arr[i + 1]:
                    arr[i], arr[i + 1] = arr[i + 1], arr[i]
                    sorted_flag = 0
    return arr
​
​
if __name__ == '__main__':
    number = [503, 87, 512, 61, 908, 170, 897, 275, 653, 426, 154, 509, 612, 677, 765, 703]
​
    # 输出未排序的关键字
    print("Unsorted Ks:")
    for i in range(len(number)):
        print(i + 1, ":", number[i])
    print()
​
    # 奇偶排序
    odd_even_sort(number)
​
    # 输出已排序的关键字
    print("Sorted Ks:")
    for i in range(len(number)):
        print(i + 1, ":", number[i])

我们不难看出,奇偶排序有很强的可并行性,同一个for i in range(odd_even, n - 1, 2)中,每一次的交换都是没有前置条件的,这当然可以并行批量完成交换。

MPI实现 odd-even sort

MPI(Massage Passing Interface)是消息传递函数库的标准规范,由MPI论坛开发。可以使用他进行进程间的通信,实现进程间的并行计算。

思路

思路描述如下:

  • Step0:和前面说的一样,先根据CPU的个数将整个待排序数组分成若干个子序列(这里的“若干个”,在后面会有更详细的解释)。

  • Step1:在每个子序列中,随意的使用一个排序算法将子序列从小到大排好(真的随便用什么算法,总之排好就可以了)。

  • Step2:以子序列为单位,对整个序列进行奇偶排序,在比较后序号较小的子序列保存前半部分的小数,较大的子序列保留后半部分大数字(这个大小完全是根据排序升降序的要求)

  • Step3:重复Step1、Step2,直到序列完全有序。

你可能存在疑惑,没关系,下面有例子。

假设待排序的数组如下:

arr=[15,11,9,16,3,14,8,7,4,6,12,10,5,2,13,1],即1-16的打乱,子序列分为4个,排序过程如下:

排序过程\子序列P1P2P3P4
初始情况15,11,9,166,3,14,87,4,6,125,2,13,1
1_组内排序9,11,15,163,6,8,144,6,7,121,2,5,13
1_奇偶排序_奇3,7,8,911,14,15,161,2,4,56,10,12,13
1_奇偶排序_偶3,7,8,91,2,4,511,14,15,166,10,12,13
2_奇偶排序_奇1,2,3,45,7,8,96,10,11,1211,12,13,14
2_奇偶排序_偶1,2,3,45,6,7,89,10,11,1211,12,13,14

1_奇偶排序_奇:P1与P2比,P3与P4比,这里的“比”是指将两个子序列合并排好序,序号偏小的留下数值小的部分,序号偏大的留下数值大的部分。

1_奇偶排序_偶:P2与P3比,P1、P4被跳过了。“比”的含义同上。

这个过程固然是很麻烦,把原本简单的排序似乎搞复杂了,但是要明白,联合很多个人做事要比单干麻烦很多,想到这里我也就不觉得奇怪了。

环境部署

由于MPI不是一个语言,而是一种新的库描述,所以可以用很多语言实现,本文讲C++和python的环境配置方法。

C++

# 首先安装gcc和gfortan,这两步应该问题不大
sudo apt‐get install gcc
which gcc
sudo apt‐get install gfortran
whicg gfortan
# 接着安装MPI编程的环境
sudo wget http://www.mpich.org/static/downloads/3.3.2/mpich‐3.3.2.tar.gz
tar ‐zxvf mpich‐3.3.2.tar.gz
cd mpich‐3.3.2
./configure ‐‐prefix=/usr/local/mpich‐3.3.2
# The ‐‐prefix parameter is to set the installation path, just set the appropriate path according to your needs, but you need to remember the installation location
make
# 这一步需要很长时间,请耐心等待
make install
vim ~/.bashrc
# 写入环境变量
    export PATH="/usr/local/mpich‐3.3.2/bin:$PATH"
source ~/.bashrc
which mpicc
which mpif90 # 查看是否安装成功

MPI编程的测试代码:

/* hellow.c */
#include <stdio.h>
#include <mpi.h>
int main(int argc, char * argv[])
{
    int myrank, nprocs;
    MPI_Init(&argc, &argv);
    MPI_Comm_size(MPI_COMM_WORLD,&nprocs);
    MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
    printf("rank%dof%d:Hello,world!\n",myrank, nprocs);
    MPI_Finalize();
    return 0;
}
  • MPI_Init(&argc, &argv);初始化MPI编程环境

  • MPI_Comm_size(MPI_COMM_WORLD,&nprocs);MPI_Comm_rank(MPI_COMM_WORLD, &myrank);获得并行环境参数(进程总数,本地进程编号等)。

  • printf("rank%dof%d:Hello,world!\n",myrank, nprocs);执行计算、通信。

  • MPI_Finalize();结束MPI编程。

编译、执行命令:

mpicc hellow.c ‐o hellow
mpirun ‐np 4 ./hello

python

MPI在Python中的实现是通过mpi4py库来完成的,瞬间安装命令如下:

pip install mpi4py --upgrade -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com

当然,还需要安装MPI本身:Download Microsoft MPI v10.0 from Official Microsoft Download Center,直接一路安装到底,也不需要手动配环境变量。

编程实现(C++)

#include<iostream>
#include"mpi.h"
#include<ctime>
#include<cmath>
#include<algorithm>
​
using namespace std;
​
void odd_even_sort(float *p, int num);//顺序执行的奇偶排序
void swap(float *p, int i, int j);//交换算法
void mergeSort(float *data, float *buffer, float *tmp, bool &sort, int part);//两块排好序的元素进行排序合并
void parallelMergeSort(float *data, int part);//奇偶排序式的 融合排序算法
​
int main(int argc, char *argv[]) {
    MPI_Init(&argc, &argv);
    int myid, numprocs;//进程标识,进程总数
    MPI_Comm_rank(MPI_COMM_WORLD, &myid);
    MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
    int masterNode = 0;
    float *memoryPool = nullptr;
​
    if (myid == masterNode) {
​
        const int N = 9900;//待排元素个数
        int padding = numprocs - N % numprocs;//若不能整除则需加上padding个无穷大的元素使能够整除
​
        memoryPool = new float[N + padding] {0};
        srand(time(NULL));
        for (int i = 0; i < N; ++i)
            memoryPool[i] = (rand() % 1000) / 10.0;
​
        double start = MPI_Wtime();//串行开始计时
        if (numprocs == 1) {
            odd_even_sort(memoryPool, N);
            double finish = MPI_Wtime();//串行停止计时
            cout << "串行时间:" << finish - start << endl;
            MPI_Finalize();
            return 0;
        }
        for (int i = 0; i < padding; ++i)
            memoryPool[N + i] = numeric_limits<float>::infinity();//设置一个无穷大值
        int part = (N + padding) / numprocs;
​
        start = MPI_Wtime();//并行开始计时
        MPI_Bcast(&part, 1, MPI_INT, masterNode, MPI_COMM_WORLD);
​
        float *data = new float[part] {0};
​
        //主进程向各进程播撒数据
        MPI_Scatter(memoryPool, part, MPI_FLOAT, data, part, MPI_FLOAT, masterNode, MPI_COMM_WORLD);
​
        //保证部分有序
        odd_even_sort(data, part);
​
        parallelMergeSort(data, part);
​
        //将全局有序的数据收集到主进程
        MPI_Gather(data, part, MPI_FLOAT, memoryPool, part, MPI_FLOAT, masterNode, MPI_COMM_WORLD);
​
        double finish = MPI_Wtime();//并行停止计算
​
        if (memoryPool)delete[]memoryPool;
​
        if (myid == masterNode)
            cout << "调用进程数:" << numprocs << " 计算时间:" << finish - start << endl;
        delete[]data;
    }
​
    MPI_Finalize();
    return 0;
}
​
void odd_even_sort(float *p, int num) {
    bool sort = false;
    while (!sort) { //若一次奇数排序,一次偶数排序,均未发生数据交换,则认为已排好序
        sort = true;
        for (int i = 1; i < num; i += 2) {
            if (p[i] < p[i - 1]) { //此处不能写等号
                swap(p, i, i - 1);
                sort = false;
            }
        }
        for (int i = 2; i < num; i += 2) {
            if (p[i] < p[i - 1]) {
                swap(p, i, i - 1);
                sort = false;
            }
        }
    }
}
inline void swap(float *p, int i, int j) {
    float tmp = p[i];
    p[i] = p[j];
    p[j] = tmp;
}
void mergeSort(float *data, float *buffer, float *tmp, bool &sort, int part) {
    int i = 0, j = 0, k = 0;
    while (i != part && j != part) {
        if (data[i] <= buffer[j]) {
            tmp[k++] = data[i++];    //此处等号一定要加
        } else {
            tmp[k++] = buffer[j++];
        }
    }
    if (j != 0)sort = false;
    if (i != part) {
        while (k != 2 * part)tmp[k++] = data[i++];
    }
    if (j != part) {
        while (k != 2 * part)tmp[k++] = buffer[j++];
    }
    copy_n(tmp, part, data);
    copy_n(tmp + part, part, buffer);
}
​
void parallelMergeSort(float *data, int part) {
    MPI_Status status;
    int myid, numprocs;
    MPI_Comm_rank(MPI_COMM_WORLD, &myid);
    MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
    int left = myid - 1, right = myid + 1;
    if (left < 0)left = MPI_PROC_NULL;//虚拟进程,使所有进程都有左右,边界上无序特殊处理
    if (right > numprocs - 1)right = MPI_PROC_NULL;
​
    float *buffer = new float[part] { 0 };
    float *tmp = new float[2 * part] { 0 };
    bool sort = false;
    bool flag = false;
​
    while (!flag) {
        sort = true;
        if (myid % 2 == 0) {
            MPI_Send(data, part, MPI_FLOAT, left, 0, MPI_COMM_WORLD);
            MPI_Recv(data, part, MPI_FLOAT, left, 0, MPI_COMM_WORLD, &status);
        } else if (right != MPI_PROC_NULL) { //如果接受的是虚拟进程的消息(空),那就不应该进行mergeSort
            MPI_Recv(buffer, part, MPI_FLOAT, right, 0, MPI_COMM_WORLD, &status);
            mergeSort(data, buffer, tmp, sort, part);
            MPI_Send(buffer, part, MPI_FLOAT, right, 0, MPI_COMM_WORLD);
        }
​
        MPI_Barrier(MPI_COMM_WORLD);//屏障函数,显式将奇排序和偶排序分开,不加也可以
​
        if (myid % 2 == 1) {
            MPI_Send(data, part, MPI_FLOAT, left, 0, MPI_COMM_WORLD);
            MPI_Recv(data, part, MPI_FLOAT, left, 0, MPI_COMM_WORLD, &status);
        } else if (right != MPI_PROC_NULL) {
            MPI_Recv(buffer, part, MPI_FLOAT, right, 0, MPI_COMM_WORLD, &status);
            mergeSort(data, buffer, tmp, sort, part);
            MPI_Send(buffer, part, MPI_FLOAT, right, 0, MPI_COMM_WORLD);
        }
​
​
        //全规约函数,对每个进程中的sort标识求逻辑与,结果保存在flag中,flag=1 意味着每个进程的sort都为1,排序完成
        MPI_Allreduce(&sort, &flag, 1, MPI_C_BOOL, MPI_LAND, MPI_COMM_WORLD);
​
    }
​
    delete[]tmp;
    delete[]buffer;
}

“若干”的问题

从下面的实验结果可以看出,在选择分为两个子序列时,排序速度是最快的(使用CPU的核数通常都是偶数,具体原因个人感觉有技术和市场两方面)。那么为什么分的子序列更多,反而排序的速度成倍减慢呢?

因为这个实验是在服务器上做的,代码是C++写的,我的服务器CPU只有两个,如果分的子序列超过了CPU的个数,那么进程之间的切换会耗费大量的时间,这非但不会加快排序的速度,反而会让程序严重变慢。这就有点东施效颦的感觉,明明没有那么多CPU,还非要分那么多子进程

参考链接

两小时入门MPI与并行计算(一):并行计算预备知识

并行计算:MPI总结

MPI并行计算学习笔记4——奇偶排序的优雅实现_奇偶排序并行化

odd-even-sort/odd_even_sort.cpp at main · thkkk/odd-even-sort · GitHub

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

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

相关文章

基于Python3接口自动化测试初探

自动化测试是什么&#xff1f; 自动化测试简单来说就是借助工具的方式来辅助手动测试的行为就可以看做是自动化测试。 自动化测试工具有哪些&#xff1f; 现在常用的自动化测试工具包括&#xff1a; QTP&#xff1a;主要用于回归测试和测试同一软件的新版本 Robot Framewor…

大数据ETL工具Kettle

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言最近公司在搞大数据数字化&#xff0c;有MES,CIM,WorkFlow等等N多的系统&#xff0c;不同的数据源DB&#xff0c;需要将这些不同的数据源DB里的数据进行整治统一…

【算法】模拟,高精度

高精度加法 P1601 AB Problem&#xff08;高精&#xff09; - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 思路就是模拟&#xff0c;值得注意的就是要用字符串类型输入。存进自己的int数组时要倒着存&#xff0c;因为如果是正着存的话&#xff0c;进位会有点trouble。 时间…

Spread.NET v16.0.20222.0 ASP.NET cRACK

关于 Spread.NET 提供类似 Excel 的电子表格体验。 Spread.NET 可帮助您创建电子表格、网格、仪表板和表单。它包括一个强大的计算引擎&#xff0c;具有450 函数以及导入和导出Excel电子表格的能力。利用广泛的 .NET 电子表格 API 和强大的计算引擎来创建分析、预算、仪表板、…

【C++ 基础篇:24】:【重要模板】C++ 输入输出运算符重载【以 Date 日期类为例】

系列文章说明 本系列 C 相关文章 仅为笔者学习笔记记录&#xff0c;用自己的理解记录学习&#xff01;C 学习系列将分为三个阶段&#xff1a;基础篇、STL 篇、高阶数据结构与算法篇&#xff0c;相关重点内容如下&#xff1a; 基础篇&#xff1a;类与对象&#xff08;涉及C的三大…

Mysql Access denied for user ‘root‘@ ‘*.*.*.*‘ (using password: YES)异常处理

目录 一、异常错误二、原因三、解决方法 一、异常错误 PS C:\Users\10568> mysql -u root -p Enter password: **** ERROR 1045 (28000): Access denied for user rootlocalhost (using password: YES)Access denied表示拒绝访问&#xff0c;using password&#xff1a;NO/…

计算机视觉 | 语义分割与Segmentation

前 言 「MMSegmentation」 是一个基于 PyTorch 的语义分割开源工具箱。它是 OpenMMLab 项目的一部分。 MMSegmentation v1.x 在 0.x 版本的基础上有了显著的提升&#xff0c;提供了更加灵活和功能丰富的体验。 主要特性 统一的基准平台 我们将各种各样的语义分割算法集成到了…

Linux权限维持

SSH后门&VIM后门 ssh后门&#xff1a; 创建一个软链接&#xff1a; ln -sf /usr/sbin/sshd /tmp/su 拓展&#xff1a;软链接相当于一个快捷键&#xff0c;硬链接相当于一个指针指向文件地址&#xff0c;也类似于复制 开启后门&#xff1a; /tmp/su -oport12345 开启后…

chatgpt赋能python:Python另存为:如何保存你的程序代码

Python另存为&#xff1a;如何保存你的程序代码 简介 Python是一种高级编程语言&#xff0c;最初由Guido van Rossum于1991年创建。自创建以来&#xff0c;Python已被广泛应用于Web开发、数据分析、人工智能等领域。作为一名有10年Python编程经验的工程师&#xff0c;我发现在…

万物的算法日记|第一天

笔者自述&#xff1a; 一直有一个声音也一直能听到身边的大佬经常说&#xff0c;要把算法学习搞好&#xff0c;一定要重视平时的算法学习&#xff0c;虽然每天也在学算法&#xff0c;但是感觉自己一直在假装努力表面功夫骗了自己&#xff0c;没有规划好自己的算法学习和总结&am…

内部服务器端口映射实验:打通内外网络,实现公网访问内部服务

内部服务器端口映射实验&#xff1a;打通内外网络&#xff0c;实现公网访问内部服务 【实验目的】 部署服务器地址转换。熟悉服务器地址转换的应用方法。验证配置。 【实验拓扑】 实验拓扑如图所示。 实验拓扑 设备参数如表所示。 设备参数表 设备 接口 IP地址 子网掩…

BOM,什么是BOM?

4.BOM 4.1 什么是BOM? BOM&#xff08;Browser Object Model&#xff09;即浏览器对象模型&#xff0c;它提供了独立于内容而与浏览器窗口进行交互的对象&#xff0c;其核心对象是 window。 ​ BOM 由一系列相关的对象构成&#xff0c;并且每个对象都提供了很多方法与属性。 …

react---todoList案例

todoList案例效果图 1.组件拆分 2.操作state数据 state 放在哪个组件&#xff1a; 如果某个组件组件使用: 放在其自身的state中。如果某些组件使用&#xff1a;放在他们共同的父组件state中&#xff08;官方称此操作为状态提升&#xff09; 状态&#xff08;state&#xff09;在…

Cocos creator实现飞机大战空中大战《战击长空》小游戏资源及代码

Cocos creator实现飞机大战空中大战《战击长空》小游戏资源及代码 最近在学习Cocos Creator&#xff0c;作为新手&#xff0c;刚刚开始学习Cocos Creator&#xff0c;刚刚入门&#xff0c;这里记录一下飞机大战小游戏实现。 https://wxaurl.cn/VEgRy2eTMyi 一 安装CocosDashBo…

Netty实战(十四)

WebSocket协议&#xff08;二&#xff09; 一、初始化 ChannelPipeline二、引导三、加密 一、初始化 ChannelPipeline 我们之前说过为了将 ChannelHandler 安装到 ChannelPipeline 中&#xff0c;需要扩展了ChannelInitializer&#xff0c;并实现 initChannel()方法。 下面我…

Nacos架构与原理 - 注册中心的设计原理

文章目录 Pre服务的分级模型 &#xff08;服务-集群-实例三层模型&#xff09;数据⼀致性负载均衡服务端侧负载均衡客户端侧负载均衡 健康检查性能与容量易用性集群扩展性用户扩展性 Pre 目前的网络架构是每个主机都有⼀个独立的 IP 地址&#xff0c;那么服务发现基本上都是通…

行为型设计模式07-命令模式

&#x1f9d1;‍&#x1f4bb;作者&#xff1a;猫十二懿 ❤️‍&#x1f525;账号&#xff1a;CSDN 、掘金 、个人博客 、Github &#x1f389;公众号&#xff1a;猫十二懿 命令模式 1、命令模式介绍 命令模式&#xff08;Command&#xff09;&#xff0c;将一个请求封装为一…

【Linux后端服务器开发】常用开发工具

目录 一、apt / yum 二、gcc / g 三、make / makefile 四、vi / vim 五、gdb 一、apt / yum apt 和 yum 都是在Linux环境下的软件包管理器&#xff0c;负责软件的查找、安装、更新与卸载。 apt 是Ubuntu系统的包管理器&#xff0c;yum是Centos系统的包管理器&#xff0c…

SQL回顾总结,超级全

SELECT&#xff1a;语句用于从数据库中选取数据 从 "Websites" 表中选取 "name" 和 "country" 列 SELECT name,country FROM Websites 从 "Websites" 表中选取所有列 SELECT * FROM Websites; SELECT DISTINCT&#xff1a;用于返…

Android系统视角下对APK的分析(2)- APK安装过程的定性分析

声明 以Android手机用户角度来看&#xff0c;安装各式各样的APP&#xff0c;基本就是从应用市场上 “搜索->下载->安装” 三连。而对Android系统来说&#xff0c;这就是个大工程了&#xff0c;因为对Android系统来说APK是“外来户”&#xff0c;如何安装它、有限制地支持…