背景

某项目有对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

本文不再触及零仿制的更多原理介绍,想了解更多信息,能够参阅IO 零仿制 –

零仿制的运用

有了对零仿制的基础知识的了解,能够知道零仿制固然好但是运用起来有各种限制,在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这个场景仅仅一个实际的运用。当然零仿制技能也有局限性,便是在此进程中不能对数据进行修改,对于有数据修改要求的场景并不适用。

参阅文献

运用程序装置流程

IO 零仿制 –