背景
某项目有对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);
}
看这个函数的代码,能够得到以下信息
-
copy是一个单纯函数,便是依据传入的输入输出FD,完结文件仿制规范操作。
-
Google依据FD的类型,进行了相应优化操作
-
假如输入输出都是一般文件,则调用copyInternalSendfile
-
假如输入输出有一个是FIFO文件,则调用copyInternalSplice
-
其他状况,则调用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 零仿制 –