首页 > 其他分享 >用零拷贝技术优化adb install的执行时间

用零拷贝技术优化adb install的执行时间

时间:2023-09-09 15:32:46浏览次数:34  
标签:count jobject st adb install 拷贝 NULL out

背景

某项目有对adb install优化的一个需求,项目的平台是Android 10,内核版本是4.19, Data分区是F2FS文件系统。由于adb install是Android一个很标准的流程,网上有很多详细的介绍,本文不涉及这个具体流程。

Adb install简单的流程是这样的,首先把安装包从PC传到设备中,然后再在设备中执行安装操作。本文只涉及安装流程中的第一个环节,即把安装文件从PC通过USB传到设备本地文件系统中的这个拷贝过程,这个过程耗时随着安装包的变大而变长。

文件复制代码

在执行adb install的过程中把安装包从PC通过USB复制到本地文件系统中,这个过程会调用到一个copy函数,这个函数的输入是2个文件的FD,输入FD对应的是从USB传来的源文件,输出文件对应的是要保存的本地临时文件。这个函数在frameworks/base/core/java/android/os/FileUtils.java里面。

/**
     * Copy the contents of one FD to another.
     * <p>
     * Attempts to use several optimization strategies to copy the data in the
     * kernel before falling back to a userspace copy as a last resort.
     *
     * @param count the number of bytes to copy.
     * @param signal to signal if the copy should be cancelled early.
     * @param executor that listener events should be delivered via.
     * @param listener to be periodically notified as the copy progresses.
     * @return number of bytes copied.
     * @hide
     */
    public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out, long count,
            @Nullable CancellationSignal signal, @Nullable Executor executor,
            @Nullable ProgressListener listener) throws IOException {
        if (sEnableCopyOptimizations) {
            try {
                final StructStat st_in = Os.fstat(in);
                final StructStat st_out = Os.fstat(out);
                if (S_ISREG(st_in.st_mode) && S_ISREG(st_out.st_mode)) {
                    return copyInternalSendfile(in, out, count, signal, executor, listener);
                } else if (S_ISFIFO(st_in.st_mode) || S_ISFIFO(st_out.st_mode)) {
                    return copyInternalSplice(in, out, count, signal, executor, listener);
                }
            } catch (ErrnoException e) {
                throw e.rethrowAsIOException();
            }
        }

        // Worse case fallback to userspace
        return copyInternalUserspace(in, out, count, signal, executor, listener);
    }

看这个函数的代码,可以得到以下信息

  1. copy是一个单纯函数,就是根据传入的输入输出FD,完成文件复制标准操作。
  2. Google根据FD的类型,进行了相应优化操作
  1. 如果输入输出都是普通文件,则调用copyInternalSendfile
  2. 如果输入输出有一个是FIFO文件,则调用copyInternalSplice
  3. 其他情况,则调用copyInternalUserspace

下面会对这几种情况进一步分析,在分析之前先了解一下零拷贝技术。

零拷贝技术

零拷贝(Zero-copy)技术指在计算机执行I/O操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域。相比于标准I/O,零拷贝并不是没有拷贝数据,而是减少上下文切换以及CPU拷贝。零拷贝机制的好处:

  • 减少数据在内核缓冲区和用户进程缓冲区之间反复的 I/O 拷贝操作
  • 减少用户进程地址空间和内核地址空间之间的上下文切换
  • 实现 CPU 的零参与,消除 CPU 在这方面的负载

零拷贝的实现主要有以下几种:

  • mmap+write
  • sendfile+DMA scatter/gather
  • splice

零拷贝的应用

有了对零拷贝的基础知识的了解,可以知道零拷贝固然好但是使用起来有各种限制,在adb install这个场景下,输入的FD是一个Socket,输出的FD是一个普通文件,并不能直接采用任何一种零拷贝技术,那如何来优化呢?下面先进一步看一下本例中涉及到的几个函数。

copyInternalSendfile

copyInternalSendfile实际上就是采用了零拷贝技术之一,sendfile系统调用。这个函数要求是输入输出都是普通文件,本场景不满足,所有不会进入这个分支。


    /**
     * Requires both input and output to be a regular file.
     *
     * @hide
     */
    @VisibleForTesting
    public static long copyInternalSendfile(FileDescriptor in, FileDescriptor out, long count,
            CancellationSignal signal, Executor executor, ProgressListener listener)
            throws ErrnoException {
        。。。
        while ((t = Os.sendfile(out, in, null, Math.min(count, COPY_CHECKPOINT_BYTES))) != 0) {
            progress += t;
            checkpoint += t;
            count -= t;
           。。。
        }
    }

static jlong Linux_splice(JNIEnv* env, jobject, jobject javaFdIn, jobject javaOffIn, jobject javaFdOut, jobject javaOffOut, jlong len, jint flags) {
    。。。
    ret = splice(fdIn, (javaOffIn == NULL ? NULL : &offIn),
           fdOut, (javaOffOut == NULL ? NULL : &offOut),
           len, flags);
    。。。
}

copyInternalSplice

copyInternalSplice实际上就是采用了零拷贝技术之一,splice系统调用。这个函数要求是输入输出至少有一个是FIFO,本场景不满足,所有不会进入这个分支。

/**
     * Requires one of input or output to be a pipe.
     *
     * @hide
     */
    @VisibleForTesting
    public static long copyInternalSplice(FileDescriptor in, FileDescriptor out, long count,
            CancellationSignal signal, Executor executor, ProgressListener listener)
            throws ErrnoException {
        。。。
        while ((t = Os.splice(in, null, out, null, Math.min(count, COPY_CHECKPOINT_BYTES),
                SPLICE_F_MOVE | SPLICE_F_MORE)) != 0) {
            progress += t;
            checkpoint += t;
            count -= t;
            。。。
        }
    }

static jlong Linux_splice(JNIEnv* env, jobject, jobject javaFdIn, jobject javaOffIn, jobject javaFdOut, jobject javaOffOut, jlong len, jint flags) {
    。。。
    ret = splice(fdIn, (javaOffIn == NULL ? NULL : &offIn),
           fdOut, (javaOffOut == NULL ? NULL : &offOut),
           len, flags);
    。。。
}

copyInternalUserspace

copyInternalUserspace实际上就是采用传统的文件读写方式来实现拷贝的,即从一个FD读出,然后写入另一个FD,直接在Java层就完成了。这是适用性最广的方式,当然性能也是最差的。不满足任何优化限制条件的场景最后都会执行这个分支。


    /** {@hide} */
    @VisibleForTesting
    public static long copyInternalUserspace(InputStream in, OutputStream out,
            CancellationSignal signal, Executor executor, ProgressListener listener)
            throws IOException {
        。。。

        while ((t = in.read(buffer)) != -1) {
            out.write(buffer, 0, t);
            progress += t;
        。。。
        }
    }

优化方案

Google的原生代码里面,针对adb install这样,输入FD是Socket,输出是普通文件的场景不满足任何优化条件,只能执行普通的拷贝流程。那么针对这种场景,是否可以优化呢?答案是肯定的,可以利用splice来实现,因为splice要求输入,输出FD至少有一个是FIFO。这样我们可以用一个FIFO来作为中介,实现从Socket-->FIFO-->File,这样就利用2次零拷贝的系统调用来完成一次实际拷贝,达到了优化的效果。

下面就是实现Socket-->FIFO-->File的整个函数


static jlong Linux_spliceSocket(JNIEnv* env, jobject, jobject javaFdIn, jobject javaOffIn, jobject javaFdOut, jobject javaOffOut, jlong len, jint flags) {
    int fdIn = jniGetFDFromFileDescriptor(env, javaFdIn);
    int fdOut = jniGetFDFromFileDescriptor(env, javaFdOut);
    int spliceErrno;
    int pfd[2];

    jlong offIn = (javaOffIn == NULL ? 0 : env->GetLongField(javaOffIn, int64RefValueFid));
    jlong offOut = (javaOffOut == NULL ? 0 : env->GetLongField(javaOffOut, int64RefValueFid));
    jlong ret = -1;
    long count = len;
    long sum = 0;

    if (count == 0)
        return 0;
   // 创建一个Pipe用来作为传输中介
    pipe(pfd);
    do {
        bool wasSignaled = false;
        {
            AsynchronousCloseMonitor monitorIn(fdIn);
            AsynchronousCloseMonitor monitorOut(fdOut);
            // 从Socket拷贝到Pipe
            ret = splice(fdIn, (javaOffIn == NULL ? NULL : &offIn), pfd[1], NULL, count, flags);
            if (ret < 0) {
                ALOGE("Splice in error, err:%d, errstr:%s\n", errno, strerror(errno));
                break;
            }

            // 从Pipe拷贝到File
            ret = splice(pfd[0], NULL, fdOut, (javaOffOut == NULL ? NULL : &offOut), count, flags);
            if (ret < 0) {
                ALOGE("Splice out error, err:%d, errstr:%s\n", errno, strerror(errno));
                break;
            }
            count -= ret;
            sum += ret;
            if (count <= 0) {
                break;
            }

            spliceErrno = errno;
            wasSignaled = monitorIn.wasSignaled() || monitorOut.wasSignaled();
        }
        if (wasSignaled) {
            jniThrowException(env, "java/io/InterruptedIOException", "splice interrupted");
            ret = -1;
            break;
        }
        if (ret == -1 && spliceErrno != EINTR) {
            throwErrnoException(env, "splice");
            break;
        }
    } while ((ret == -1) || (count > 0));
    if (ret == -1) {
        /* If the syscall failed, re-set errno: throwing an exception might have modified it. */
        errno = spliceErrno;
    } else {
        if (javaOffIn != NULL) {
            env->SetLongField(javaOffIn, int64RefValueFid, offIn);
        }
        if (javaOffOut != NULL) {
            env->SetLongField(javaOffOut, int64RefValueFid, offOut);
        }
    }

    if (ret > 0)
        ret = sum;
    // 关闭使用的Pipe
    close(pfd[0]);
    close(pfd[1]);
    return ret;

}

对应的在Copy函数里面,增加一个优化拷贝的判定条件,在输入,输出FD中至少有一个是Socket的时候,调用我们实现的Linux_spliceSocket来执行。

public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out, long count,
            @Nullable CancellationSignal signal, @Nullable Executor executor,
            @Nullable ProgressListener listener) throws IOException {
        if (sEnableCopyOptimizations) {
            try {
                final StructStat st_in = Os.fstat(in);
                final StructStat st_out = Os.fstat(out);
                if (S_ISREG(st_in.st_mode) && S_ISREG(st_out.st_mode)) {
                    return copyInternalSendfile(in, out, count, signal, executor, listener);
                } else if (S_ISFIFO(st_in.st_mode) || S_ISFIFO(st_out.st_mode)) {
                    return copyInternalSplice(in, out, count, signal, executor, listener);
                } else if (S_ISSOCK(st_in.st_mode) || S_ISSOCK(st_out.st_mode)) {
                    return copyInternalSpliceSocket(in, out, count, signal, executor, listener);
                }
            } catch (ErrnoException e) {
                throw e.rethrowAsIOException();
            }
        }

        // Worse case fallback to userspace
        return copyInternalUserspace(in, out, count, signal, executor, listener);
    }

优化效果

运用Splice零拷贝技术,相比传统的读出再写入方式,减少了2次从内核态到用户态的拷贝消耗,在XXX项目平台上实际测试了优化效果,效果还比较明显。单独文件Copy的耗时减少了30%,整个adb install的耗时降低了5%左右。

总结

本文引入Pipe作为中间媒介的拷贝方法,是一个利用零拷贝技术的通用方法,可以应用在不同的场景,adb install这个场景只是一个实际的应用。当然零拷贝技术也有局限性,就是在此过程中不能对数据进行修改,对于有数据修改要求的场景并不适用。

标签:count,jobject,st,adb,install,拷贝,NULL,out
From: https://blog.51cto.com/u_16175630/7419735

相关文章

  • 原型模式和深拷贝,浅拷贝
    原型模式案例引入克隆羊问题有一只羊,姓名为tom,年龄为1,颜色为白色,编写程序创建和tom羊属性完全相同的羊。传统方式解决代码实现publicclassSheep{privateStringname;privateintage;privateStringcolor;publicSheep(){}publicShe......
  • python的深浅拷贝
    通过id内存地址发生变化print()打印出来的变化,这种现象就是’拷贝‘,’浅拷贝‘,’深拷贝‘拷贝(赋值)lt=[1,2,3]lt=ltlt.append(4)print(lt)#因为列表是可变类型,所以lt的值变化,lt2的值也跟着变化print(lt2)浅拷贝copy.copy()浅拷贝,拷贝出来的值内存地址都一样,但......
  • 使用HeidiSQL工具导出导入MariaDB数据的正确方法
    这个开源工具,用来导数据确实好使,而且可以一次导多个数据库甚至多个连接导出:1.首先在数据库或表上右键--点击"导出数据为sql脚本"2.左边栏就可以选择数据库或者表,甚至连接3.右边是各种参数,重要的:最大insert如果填0是逐条,会很慢,默认即可;文件路径注意:它不会检测是否有......
  • 解构赋值是深拷贝还是浅拷贝?
    letarr=[1,2,3]letnewArr=[...arr]newArr.push(4)console.log(arr)//[1,2,3]console.log(newArr)//[1,2,3,4]letarr2=[[1,2,3],[4,5,6]]letnewArr2=[...arr2]newArr2[0].push(100)console.log(arr2)//[[1,2,3,100],[4,5,6]]console.log(newArr......
  • 深拷贝与浅拷贝
    1.可变对象与不可变对象在Python中,对象可以分为可变对象(MutableObject)和不可变对象(ImmutableObject)两种类型。可变对象指的是能够在原地修改的对象,即对象的值可以被改变而不需要创建新的对象。常见的可变对象包括列表(list)和字典(dict)。不可变对象指的是不能够被修改的对象,......
  • pip install 与conda install区别的个人理解
    当使用condainstall安装包时,会将包下载到虚拟环境公用的文件夹(假设为文件夹A)下。当在虚拟环境中安装包时(假设该虚拟环境下报的下载位置是B),会先搜索公用文件夹(A),如果搜索到了,会将包直接从A复制到B,如果没有找到,先将包下载到A中,再从A复制到B。当使用pipinstall安装包时,会直接将......
  • Android 调试桥 (adb) 使用教程/示例
    sidebar:autoAndroid调试桥(adb)Android调试桥(adb)是一种功能多样的命令行工具,可让您与设备进行通信。adb命令可用于执行各种设备操作,例如安装和调试应用。adb提供对Unixshell(可用来在设备上运行各种命令)的访问权限。它是一种客户端-服务器程序,包括以下三个组件:客......
  • 手撕代码,实现String类的构造函数、拷贝构造函数、赋值构造函数以及析构函数
    #include<bits/stdc++.h>usingnamespacestd;classString{public:String(constchar*str=NULL){//普通构造函数cout<<"普通构造函数被调用"<<endl;if(str==NULL){data=newchar[1];*dat......
  • 浅拷贝和深拷贝实现
    #include<bits/stdc++.h>usingnamespacestd;classstudent{private:char*name;public:student(){name=newchar(20);cout<<"创建student"<<endl;};~student(){cout<<&qu......
  • ios windows下使用altinstaller安装unc0ver进行越狱
    由于时长要重启ios设备,客户又未必有mac设备,寻找一种在windows下方便的越狱解决方案(ios13系统)在证书未过期的情况下,可以通过altinstaller进行续签,就不需要链接电脑了Altstore官方网站:https://altstore.io/项目地址:https://github.com/altstoreio/AltStore安装Altstore使用提......