Merge "Track the app forked processes by using the accounting info"

This commit is contained in:
TreeHugger Robot
2020-10-09 20:27:41 +00:00
committed by Android (Google) Code Review
3 changed files with 314 additions and 58 deletions

View File

@@ -12127,6 +12127,7 @@ public class ActivityManagerService extends IActivityManager.Stub
app.setHasClientActivities(false);
mServices.killServicesLocked(app, allowRestart);
mPhantomProcessList.onAppDied(app.pid);
boolean restart = false;

View File

@@ -27,15 +27,22 @@ import android.app.ApplicationExitInfo.Reason;
import android.app.ApplicationExitInfo.SubReason;
import android.os.Handler;
import android.os.Process;
import android.os.StrictMode;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.ProcStatsUtil;
import com.android.internal.os.ProcessCpuTracker;
import libcore.io.IoUtils;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
@@ -78,18 +85,178 @@ public final class PhantomProcessList {
@GuardedBy("mLock")
private final ArrayList<PhantomProcessRecord> mTempPhantomProcesses = new ArrayList<>();
/**
* The mapping between a phantom process ID to its parent process (an app process)
*/
@GuardedBy("mLock")
private final SparseArray<ProcessRecord> mPhantomToAppProcessMap = new SparseArray<>();
@GuardedBy("mLock")
private final SparseArray<InputStream> mCgroupProcsFds = new SparseArray<>();
@GuardedBy("mLock")
private final byte[] mDataBuffer = new byte[4096];
@GuardedBy("mLock")
private boolean mTrimPhantomProcessScheduled = false;
@GuardedBy("mLock")
int mUpdateSeq;
@VisibleForTesting
Injector mInjector;
private final ActivityManagerService mService;
private final Handler mKillHandler;
PhantomProcessList(final ActivityManagerService service) {
mService = service;
mKillHandler = service.mProcessList.sKillHandler;
mInjector = new Injector();
}
@VisibleForTesting
@GuardedBy("mLock")
void lookForPhantomProcessesLocked() {
mPhantomToAppProcessMap.clear();
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
synchronized (mService.mPidsSelfLocked) {
for (int i = mService.mPidsSelfLocked.size() - 1; i >= 0; i--) {
final ProcessRecord app = mService.mPidsSelfLocked.valueAt(i);
lookForPhantomProcessesLocked(app);
}
}
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
@GuardedBy({"mLock", "mService.mPidsSelfLocked"})
private void lookForPhantomProcessesLocked(ProcessRecord app) {
if (app.appZygote || app.killed || app.killedByAm) {
// process forked from app zygote doesn't have its own acct entry
return;
}
InputStream input = mCgroupProcsFds.get(app.pid);
if (input == null) {
final String path = getCgroupFilePath(app.info.uid, app.pid);
try {
input = mInjector.openCgroupProcs(path);
} catch (FileNotFoundException | SecurityException e) {
if (DEBUG_PROCESSES) {
Slog.w(TAG, "Unable to open " + path, e);
}
return;
}
// Keep the FD open for better performance
mCgroupProcsFds.put(app.pid, input);
}
final byte[] buf = mDataBuffer;
try {
int read = 0;
int pid = 0;
long totalRead = 0;
do {
read = mInjector.readCgroupProcs(input, buf, 0, buf.length);
if (read == -1) {
break;
}
totalRead += read;
for (int i = 0; i < read; i++) {
final byte b = buf[i];
if (b == '\n') {
addChildPidLocked(app, pid);
pid = 0;
} else {
pid = pid * 10 + (b - '0');
}
}
if (read < buf.length) {
// we may break from here safely as sysfs reading should return the whole page
// if the remaining data is larger than a page
break;
}
} while (true);
if (pid != 0) {
addChildPidLocked(app, pid);
}
// rewind the fd for the next read
input.skip(-totalRead);
} catch (IOException e) {
Slog.e(TAG, "Error in reading cgroup procs from " + app, e);
IoUtils.closeQuietly(input);
mCgroupProcsFds.delete(app.pid);
}
}
@VisibleForTesting
static String getCgroupFilePath(int uid, int pid) {
return "/acct/uid_" + uid + "/pid_" + pid + "/cgroup.procs";
}
static String getProcessName(int pid) {
String procName = ProcStatsUtil.readTerminatedProcFile(
"/proc/" + pid + "/cmdline", (byte) '\0');
if (procName == null) {
return null;
}
int l = procName.lastIndexOf('/');
if (l > 0 && l < procName.length() - 1) {
procName = procName.substring(l + 1);
}
return procName;
}
@GuardedBy({"mLock", "mService.mPidsSelfLocked"})
private void addChildPidLocked(final ProcessRecord app, final int pid) {
if (app.pid != pid) {
// That's something else...
final ProcessRecord r = mService.mPidsSelfLocked.get(pid);
if (r != null) {
// Is this a process forked via app zygote?
if (!r.appZygote) {
// Unexpected...
if (DEBUG_PROCESSES) {
Slog.w(TAG, "Unexpected: " + r + " appears in the cgroup.procs of " + app);
}
} else {
// Just a child process of app zygote, no worries
}
} else {
final int index = mPhantomToAppProcessMap.indexOfKey(pid);
if (index >= 0) { // unlikely since we cleared the map at the beginning
final ProcessRecord current = mPhantomToAppProcessMap.valueAt(index);
if (app == current) {
// Okay it's unchanged
return;
}
mPhantomToAppProcessMap.setValueAt(index, app);
} else {
mPhantomToAppProcessMap.put(pid, app);
}
// Its UID isn't necessarily to be the same as the app.info.uid, since it could be
// forked from child processes of app zygote
final int uid = Process.getUidForPid(pid);
String procName = mInjector.getProcessName(pid);
if (procName == null || uid < 0) {
mPhantomToAppProcessMap.delete(pid);
return;
}
getOrCreatePhantomProcessIfNeededLocked(procName, uid, pid, true);
}
}
}
void onAppDied(final int pid) {
synchronized (mLock) {
final int index = mCgroupProcsFds.indexOfKey(pid);
if (index >= 0) {
final InputStream inputStream = mCgroupProcsFds.valueAt(index);
mCgroupProcsFds.removeAt(index);
IoUtils.closeQuietly(inputStream);
}
}
}
/**
@@ -99,7 +266,7 @@ public final class PhantomProcessList {
*/
@GuardedBy("mLock")
PhantomProcessRecord getOrCreatePhantomProcessIfNeededLocked(final String processName,
final int uid, final int pid) {
final int uid, final int pid, boolean createIfNeeded) {
// First check if it's actually an app process we know
if (isAppProcess(pid)) {
return null;
@@ -123,56 +290,47 @@ public final class PhantomProcessList {
if (proc.equals(processName, uid, pid)) {
return proc;
}
// Our zombie process information is outdated, let's remove this one, it shoud
// Our zombie process information is outdated, let's remove this one, it should
// have been gone.
mZombiePhantomProcesses.removeAt(idx);
}
}
int ppid = getParentPid(pid);
if (!createIfNeeded) {
return null;
}
// Walk through its parents and see if it could be traced back to an app process.
while (ppid > 1) {
if (isAppProcess(ppid)) {
// It's a phantom process, bookkeep it
try {
final PhantomProcessRecord proc = new PhantomProcessRecord(
processName, uid, pid, ppid, mService,
this::onPhantomProcessKilledLocked);
proc.mUpdateSeq = mUpdateSeq;
mPhantomProcesses.put(pid, proc);
SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.get(ppid);
if (array == null) {
array = new SparseArray<>();
mAppPhantomProcessMap.put(ppid, array);
}
array.put(pid, proc);
if (proc.mPidFd != null) {
mKillHandler.getLooper().getQueue().addOnFileDescriptorEventListener(
proc.mPidFd, EVENT_INPUT | EVENT_ERROR,
this::onPhantomProcessFdEvent);
mPhantomProcessesPidFds.put(proc.mPidFd.getInt$(), proc);
}
scheduleTrimPhantomProcessesLocked();
return proc;
} catch (IllegalStateException e) {
return null;
final ProcessRecord r = mPhantomToAppProcessMap.get(pid);
if (r != null) {
// It's a phantom process, bookkeep it
try {
final PhantomProcessRecord proc = new PhantomProcessRecord(
processName, uid, pid, r.pid, mService,
this::onPhantomProcessKilledLocked);
proc.mUpdateSeq = mUpdateSeq;
mPhantomProcesses.put(pid, proc);
SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.get(r.pid);
if (array == null) {
array = new SparseArray<>();
mAppPhantomProcessMap.put(r.pid, array);
}
array.put(pid, proc);
if (proc.mPidFd != null) {
mKillHandler.getLooper().getQueue().addOnFileDescriptorEventListener(
proc.mPidFd, EVENT_INPUT | EVENT_ERROR,
this::onPhantomProcessFdEvent);
mPhantomProcessesPidFds.put(proc.mPidFd.getInt$(), proc);
}
scheduleTrimPhantomProcessesLocked();
return proc;
} catch (IllegalStateException e) {
return null;
}
ppid = getParentPid(ppid);
}
return null;
}
private static int getParentPid(int pid) {
try {
return Process.getParentPid(pid);
} catch (Exception e) {
}
return -1;
}
private boolean isAppProcess(int pid) {
synchronized (mService.mPidsSelfLocked) {
return mService.mPidsSelfLocked.get(pid) != null;
@@ -346,10 +504,14 @@ public final class PhantomProcessList {
synchronized (mLock) {
// refresh the phantom process list with the latest cpu stats results.
mUpdateSeq++;
// Scan app process's accounting procs
lookForPhantomProcessesLocked();
for (int i = tracker.countStats() - 1; i >= 0; i--) {
final ProcessCpuTracker.Stats st = tracker.getStats(i);
final PhantomProcessRecord r =
getOrCreatePhantomProcessIfNeededLocked(st.name, st.uid, st.pid);
getOrCreatePhantomProcessIfNeededLocked(st.name, st.uid, st.pid, false);
if (r != null) {
r.mUpdateSeq = mUpdateSeq;
r.mCurrentCputime += st.rel_utime + st.rel_stime;
@@ -392,4 +554,19 @@ public final class PhantomProcessList {
proc.dump(pw, prefix + " ");
}
}
@VisibleForTesting
static class Injector {
InputStream openCgroupProcs(String path) throws FileNotFoundException, SecurityException {
return new FileInputStream(path);
}
int readCgroupProcs(InputStream input, byte[] buf, int offset, int len) throws IOException {
return input.read(buf, offset, len);
}
String getProcessName(final int pid) {
return PhantomProcessList.getProcessName(pid);
}
}
}

View File

@@ -34,12 +34,12 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.platform.test.annotations.Presubmit;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.server.LocalServices;
import com.android.server.ServiceThread;
import com.android.server.am.ActivityManagerService.Injector;
import com.android.server.appop.AppOpsService;
import com.android.server.wm.ActivityTaskManagerService;
@@ -55,7 +55,11 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.quality.Strictness;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@Presubmit
public class AppChildProcessTest {
@@ -68,6 +72,7 @@ public class AppChildProcessTest {
private Context mContext = getInstrumentation().getTargetContext();
private TestInjector mInjector;
private PhantomTestInjector mPhantomInjector;
private ActivityManagerService mAms;
private ProcessList mProcessList;
private PhantomProcessList mPhantomProcessList;
@@ -94,6 +99,7 @@ public class AppChildProcessTest {
mProcessList = spy(pList);
mInjector = new TestInjector(mContext);
mPhantomInjector = new PhantomTestInjector();
mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread());
mAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
mAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
@@ -101,6 +107,7 @@ public class AppChildProcessTest {
mAms.mPackageManagerInt = mPackageManagerInt;
pList.mService = mAms;
mPhantomProcessList = mAms.mPhantomProcessList;
mPhantomProcessList.mInjector = mPhantomInjector;
doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
doReturn(false).when(() -> Process.supportsPidFd());
// Remove stale instance of PackageManagerInternal if there is any
@@ -136,47 +143,60 @@ public class AppChildProcessTest {
final String child2ProcessName = "test1_child1_child2";
final String nativeProcessName = "test_native";
makeParent(zygote64Pid, initPid);
makeParent(zygote32Pid, initPid);
makeProcess(rootUid, zygote64Pid, zygote64ProcessName);
makeParent(rootUid, zygote64Pid, initPid);
makeProcess(rootUid, zygote32Pid, zygote32ProcessName);
makeParent(rootUid, zygote32Pid, initPid);
makeAppProcess(app1Pid, app1Uid, app1ProcessName, app1ProcessName);
makeParent(app1Pid, zygote64Pid);
makeAppProcess(app2Pid, app2Uid, app2ProcessName, app2ProcessName);
makeParent(app2Pid, zygote64Pid);
mPhantomProcessList.lookForPhantomProcessesLocked();
assertEquals(0, mPhantomProcessList.mPhantomProcesses.size());
// Verify zygote itself isn't a phantom process
assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
zygote64ProcessName, rootUid, zygote64Pid));
zygote64ProcessName, rootUid, zygote64Pid, false));
assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
zygote32ProcessName, rootUid, zygote32Pid));
zygote32ProcessName, rootUid, zygote32Pid, false));
// Verify none of the app isn't a phantom process
assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
app1ProcessName, app1Uid, app1Pid));
app1ProcessName, app1Uid, app1Pid, false));
assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
app2ProcessName, app2Uid, app2Pid));
app2ProcessName, app2Uid, app2Pid, false));
// "Fork" an app child process
makeParent(child1Pid, app1Pid);
makeProcess(app1Uid, child1Pid, child1ProcessName);
makeParent(app1Uid, child1Pid, app1Pid);
mPhantomProcessList.lookForPhantomProcessesLocked();
PhantomProcessRecord pr = mPhantomProcessList
.getOrCreatePhantomProcessIfNeededLocked(child1ProcessName, app1Uid, child1Pid);
.getOrCreatePhantomProcessIfNeededLocked(
child1ProcessName, app1Uid, child1Pid, true);
assertTrue(pr != null);
assertEquals(1, mPhantomProcessList.mPhantomProcesses.size());
assertEquals(pr, mPhantomProcessList.mPhantomProcesses.valueAt(0));
verifyPhantomProcessRecord(pr, child1ProcessName, app1Uid, child1Pid);
// Create another native process from init
makeParent(nativePid, initPid);
makeProcess(rootUid, nativePid, nativeProcessName);
makeParent(rootUid, nativePid, initPid);
mPhantomProcessList.lookForPhantomProcessesLocked();
assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
nativeProcessName, rootUid, nativePid));
nativeProcessName, rootUid, nativePid, false));
assertEquals(1, mPhantomProcessList.mPhantomProcesses.size());
assertEquals(pr, mPhantomProcessList.mPhantomProcesses.valueAt(0));
// "Fork" another app child process
makeParent(child2Pid, child1Pid);
makeProcess(app1Uid, child2Pid, child2ProcessName);
makeParent(app1Uid, child2Pid, app1Pid);
mPhantomProcessList.lookForPhantomProcessesLocked();
PhantomProcessRecord pr2 = mPhantomProcessList
.getOrCreatePhantomProcessIfNeededLocked(child2ProcessName, app1Uid, child2Pid);
.getOrCreatePhantomProcessIfNeededLocked(
child2ProcessName, app1Uid, child2Pid, false);
assertTrue(pr2 != null);
assertEquals(2, mPhantomProcessList.mPhantomProcesses.size());
verifyPhantomProcessRecord(pr2, child2ProcessName, app1Uid, child2Pid);
@@ -197,19 +217,27 @@ public class AppChildProcessTest {
assertEquals(pid, pr.mPid);
}
private void makeProcess(int uid, int pid, String processName) {
doReturn(uid).when(() -> Process.getUidForPid(eq(pid)));
mPhantomInjector.mPidToName.put(pid, processName);
}
private void makeAppProcess(int pid, int uid, String packageName, String processName) {
makeProcess(uid, pid, processName);
ApplicationInfo ai = new ApplicationInfo();
ai.packageName = packageName;
ai.uid = uid;
ProcessRecord app = new ProcessRecord(mAms, ai, processName, uid);
app.pid = pid;
mAms.mPidsSelfLocked.doAddInternal(app);
mPhantomInjector.addToProcess(uid, pid, pid);
}
private void makeParent(int pid, int ppid) {
doReturn(ppid).when(() -> Process.getParentPid(eq(pid)));
private void makeParent(int uid, int pid, int ppid) {
mPhantomInjector.addToProcess(uid, ppid, pid);
}
private class TestInjector extends Injector {
private class TestInjector extends ActivityManagerService.Injector {
TestInjector(Context context) {
super(context);
}
@@ -230,6 +258,56 @@ public class AppChildProcessTest {
}
}
private class PhantomTestInjector extends PhantomProcessList.Injector {
ArrayMap<String, InputStream> mPathToInput = new ArrayMap<>();
ArrayMap<String, StringBuffer> mPathToData = new ArrayMap<>();
ArrayMap<InputStream, StringBuffer> mInputToData = new ArrayMap<>();
ArrayMap<Integer, String> mPidToName = new ArrayMap<>();
@Override
InputStream openCgroupProcs(String path) throws FileNotFoundException, SecurityException {
InputStream input = mPathToInput.get(path);
if (input != null) {
return input;
}
input = new ByteArrayInputStream(new byte[8]); // buf size doesn't matter here
mPathToInput.put(path, input);
StringBuffer sb = mPathToData.get(path);
if (sb == null) {
sb = new StringBuffer();
mPathToData.put(path, sb);
}
mInputToData.put(input, sb);
return input;
}
@Override
int readCgroupProcs(InputStream input, byte[] buf, int offset, int len) throws IOException {
StringBuffer sb = mInputToData.get(input);
if (sb == null) {
return -1;
}
byte[] avail = sb.toString().getBytes();
System.arraycopy(avail, 0, buf, offset, Math.min(len, avail.length));
return Math.min(len, avail.length);
}
@Override
String getProcessName(final int pid) {
return mPidToName.get(pid);
}
void addToProcess(int uid, int pid, int newPid) {
final String path = PhantomProcessList.getCgroupFilePath(uid, pid);
StringBuffer sb = mPathToData.get(path);
if (sb == null) {
sb = new StringBuffer();
mPathToData.put(path, sb);
}
sb.append(newPid).append('\n');
}
}
static class ServiceThreadRule implements TestRule {
private ServiceThread mThread;