blob: 2ec1aedd18f9ff9347c53d335da9b85390ab5613 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.am;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESSES;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
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.FeatureFlagUtils;
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.File;
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;
import java.util.function.Function;
/**
* Activity manager code dealing with phantom processes.
*/
public final class PhantomProcessList {
static final String TAG = TAG_WITH_CLASS_NAME ? "PhantomProcessList" : TAG_AM;
final Object mLock = new Object();
/**
* All of the phantom process record we track, key is the pid of the process.
*/
@GuardedBy("mLock")
final SparseArray<PhantomProcessRecord> mPhantomProcesses = new SparseArray<>();
/**
* The mapping between app processes and their phantom processess, outer key is the pid of
* the app process, while the inner key is the pid of the phantom process.
*/
@GuardedBy("mLock")
final SparseArray<SparseArray<PhantomProcessRecord>> mAppPhantomProcessMap =
new SparseArray<>();
/**
* The mapping of the pidfd to PhantomProcessRecord.
*/
@GuardedBy("mLock")
final SparseArray<PhantomProcessRecord> mPhantomProcessesPidFds = new SparseArray<>();
/**
* The list of phantom processes tha's being signaled to be killed but still undead yet.
*/
@GuardedBy("mLock")
final SparseArray<PhantomProcessRecord> mZombiePhantomProcesses = new SparseArray<>();
@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;
private static final int CGROUP_V1 = 0;
private static final int CGROUP_V2 = 1;
private static final String[] CGROUP_PATH_PREFIXES = {
"/acct/uid_" /* cgroup v1 */,
"/sys/fs/cgroup/uid_" /* cgroup v2 */
};
private static final String CGROUP_PID_PREFIX = "/pid_";
private static final String CGROUP_PROCS = "/cgroup.procs";
@VisibleForTesting
int mCgroupVersion = CGROUP_V1;
PhantomProcessList(final ActivityManagerService service) {
mService = service;
mKillHandler = service.mProcessList.sKillHandler;
mInjector = new Injector();
probeCgroupVersion();
}
@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.isKilled() || app.isKilledByAm()) {
// process forked from app zygote doesn't have its own acct entry
return;
}
final int appPid = app.getPid();
InputStream input = mCgroupProcsFds.get(appPid);
if (input == null) {
final String path = getCgroupFilePath(app.info.uid, appPid);
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(appPid, 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, appPid);
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, appPid);
}
// 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(appPid);
}
}
private void probeCgroupVersion() {
for (int i = CGROUP_PATH_PREFIXES.length - 1; i >= 0; i--) {
if ((new File(CGROUP_PATH_PREFIXES[i] + Process.SYSTEM_UID)).exists()) {
mCgroupVersion = i;
break;
}
}
}
@VisibleForTesting
String getCgroupFilePath(int uid, int pid) {
return CGROUP_PATH_PREFIXES[mCgroupVersion] + uid + CGROUP_PID_PREFIX + 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, final int appPid) {
if (appPid != 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);
}
}
}
/**
* Get the existing phantom process record, or create if it's not existing yet;
* however, before creating it, we'll check if this is really a phantom process
* and we'll return null if it's not.
*/
@GuardedBy("mLock")
PhantomProcessRecord getOrCreatePhantomProcessIfNeededLocked(final String processName,
final int uid, final int pid, boolean createIfNeeded) {
// First check if it's actually an app process we know
if (isAppProcess(pid)) {
return null;
}
// Have we already been aware of this?
final int index = mPhantomProcesses.indexOfKey(pid);
if (index >= 0) {
final PhantomProcessRecord proc = mPhantomProcesses.valueAt(index);
if (proc.equals(processName, uid, pid)) {
return proc;
}
// Somehow our record doesn't match, remove it anyway
Slog.w(TAG, "Stale " + proc + ", removing");
onPhantomProcessKilledLocked(proc);
} else {
// Is this one of the zombie processes we've known?
final int idx = mZombiePhantomProcesses.indexOfKey(pid);
if (idx >= 0) {
final PhantomProcessRecord proc = mZombiePhantomProcesses.valueAt(idx);
if (proc.equals(processName, uid, pid)) {
return proc;
}
// Our zombie process information is outdated, let's remove this one, it should
// have been gone.
mZombiePhantomProcesses.removeAt(idx);
}
}
if (!createIfNeeded) {
return null;
}
final ProcessRecord r = mPhantomToAppProcessMap.get(pid);
if (r != null) {
// It's a phantom process, bookkeep it
try {
final int appPid = r.getPid();
final PhantomProcessRecord proc = new PhantomProcessRecord(
processName, uid, pid, appPid, mService,
this::onPhantomProcessKilledLocked);
proc.mUpdateSeq = mUpdateSeq;
mPhantomProcesses.put(pid, proc);
SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.get(appPid);
if (array == null) {
array = new SparseArray<>();
mAppPhantomProcessMap.put(appPid, 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;
}
}
return null;
}
private boolean isAppProcess(int pid) {
synchronized (mService.mPidsSelfLocked) {
return mService.mPidsSelfLocked.get(pid) != null;
}
}
private int onPhantomProcessFdEvent(FileDescriptor fd, int events) {
synchronized (mLock) {
final PhantomProcessRecord proc = mPhantomProcessesPidFds.get(fd.getInt$());
if (proc == null) {
return 0;
}
if ((events & EVENT_INPUT) != 0) {
proc.onProcDied(true);
} else {
// EVENT_ERROR, kill the process
proc.killLocked("Process error", true);
}
}
return 0;
}
@GuardedBy("mLock")
private void onPhantomProcessKilledLocked(final PhantomProcessRecord proc) {
if (proc.mPidFd != null && proc.mPidFd.valid()) {
mKillHandler.getLooper().getQueue()
.removeOnFileDescriptorEventListener(proc.mPidFd);
mPhantomProcessesPidFds.remove(proc.mPidFd.getInt$());
IoUtils.closeQuietly(proc.mPidFd);
}
mPhantomProcesses.remove(proc.mPid);
final int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid);
if (index < 0) {
return;
}
SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.valueAt(index);
array.remove(proc.mPid);
if (array.size() == 0) {
mAppPhantomProcessMap.removeAt(index);
}
if (proc.mZombie) {
// If it's not really dead, bookkeep it
mZombiePhantomProcesses.put(proc.mPid, proc);
} else {
// In case of race condition, let's try to remove it from zombie list
mZombiePhantomProcesses.remove(proc.mPid);
}
}
@GuardedBy("mLock")
private void scheduleTrimPhantomProcessesLocked() {
if (!mTrimPhantomProcessScheduled) {
mTrimPhantomProcessScheduled = true;
mService.mHandler.post(this::trimPhantomProcessesIfNecessary);
}
}
/**
* Clamp the number of phantom processes to
* {@link ActivityManagerConstants#MAX_PHANTOM_PROCESSE}, kills those surpluses in the
* order of the oom adjs of their parent process.
*/
void trimPhantomProcessesIfNecessary() {
if (!mService.mSystemReady || !FeatureFlagUtils.isEnabled(mService.mContext,
SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS)) {
return;
}
synchronized (mService.mProcLock) {
synchronized (mLock) {
mTrimPhantomProcessScheduled = false;
if (mService.mConstants.MAX_PHANTOM_PROCESSES < mPhantomProcesses.size()) {
for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) {
mTempPhantomProcesses.add(mPhantomProcesses.valueAt(i));
}
synchronized (mService.mPidsSelfLocked) {
Collections.sort(mTempPhantomProcesses, (a, b) -> {
final ProcessRecord ra = mService.mPidsSelfLocked.get(a.mPpid);
if (ra == null) {
// parent is gone, this process should have been killed too
return 1;
}
final ProcessRecord rb = mService.mPidsSelfLocked.get(b.mPpid);
if (rb == null) {
// parent is gone, this process should have been killed too
return -1;
}
if (ra.mState.getCurAdj() != rb.mState.getCurAdj()) {
return ra.mState.getCurAdj() - rb.mState.getCurAdj();
}
if (a.mKnownSince != b.mKnownSince) {
// In case of identical oom adj, younger one first
return a.mKnownSince < b.mKnownSince ? 1 : -1;
}
return 0;
});
}
for (int i = mTempPhantomProcesses.size() - 1;
i >= mService.mConstants.MAX_PHANTOM_PROCESSES; i--) {
final PhantomProcessRecord proc = mTempPhantomProcesses.get(i);
proc.killLocked("Trimming phantom processes", true);
}
mTempPhantomProcesses.clear();
}
}
}
}
/**
* Remove all entries with outdated seq num.
*/
@GuardedBy("mLock")
void pruneStaleProcessesLocked() {
for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) {
final PhantomProcessRecord proc = mPhantomProcesses.valueAt(i);
if (proc.mUpdateSeq < mUpdateSeq) {
if (DEBUG_PROCESSES) {
Slog.v(TAG, "Pruning " + proc + " as it should have been dead.");
}
proc.killLocked("Stale process", true);
}
}
for (int i = mZombiePhantomProcesses.size() - 1; i >= 0; i--) {
final PhantomProcessRecord proc = mZombiePhantomProcesses.valueAt(i);
if (proc.mUpdateSeq < mUpdateSeq) {
if (DEBUG_PROCESSES) {
Slog.v(TAG, "Pruning " + proc + " as it should have been dead.");
}
}
}
}
/**
* Kill the given phantom process, all its siblings (if any) and their parent process
*/
@GuardedBy("mService")
void killPhantomProcessGroupLocked(ProcessRecord app, PhantomProcessRecord proc,
@Reason int reasonCode, @SubReason int subReason, String msg) {
synchronized (mLock) {
int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid);
if (index >= 0) {
final SparseArray<PhantomProcessRecord> array =
mAppPhantomProcessMap.valueAt(index);
for (int i = array.size() - 1; i >= 0; i--) {
final PhantomProcessRecord r = array.valueAt(i);
if (r == proc) {
r.killLocked(msg, true);
} else {
r.killLocked("Caused by siling process: " + msg, false);
}
}
}
}
// Lastly, kill the parent process too
app.killLocked("Caused by child process: " + msg, reasonCode, subReason, true);
}
/**
* Iterate all phantom process belonging to the given app, and invokve callback
* for each of them.
*/
void forEachPhantomProcessOfApp(final ProcessRecord app,
final Function<PhantomProcessRecord, Boolean> callback) {
synchronized (mLock) {
int index = mAppPhantomProcessMap.indexOfKey(app.getPid());
if (index >= 0) {
final SparseArray<PhantomProcessRecord> array =
mAppPhantomProcessMap.valueAt(index);
for (int i = array.size() - 1; i >= 0; i--) {
final PhantomProcessRecord r = array.valueAt(i);
if (!callback.apply(r)) {
break;
}
}
}
}
}
@GuardedBy("tracker")
void updateProcessCpuStatesLocked(ProcessCpuTracker tracker) {
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, false);
if (r != null) {
r.mUpdateSeq = mUpdateSeq;
r.mCurrentCputime += st.rel_utime + st.rel_stime;
if (r.mLastCputime == 0) {
r.mLastCputime = r.mCurrentCputime;
}
r.updateAdjLocked();
}
}
// remove the stale ones
pruneStaleProcessesLocked();
}
}
void dump(PrintWriter pw, String prefix) {
synchronized (mLock) {
dumpPhantomeProcessLocked(pw, prefix, "All Active App Child Processes:",
mPhantomProcesses);
dumpPhantomeProcessLocked(pw, prefix, "All Zombie App Child Processes:",
mZombiePhantomProcesses);
}
}
void dumpPhantomeProcessLocked(PrintWriter pw, String prefix, String headline,
SparseArray<PhantomProcessRecord> list) {
final int size = list.size();
if (size == 0) {
return;
}
pw.println();
pw.print(prefix);
pw.println(headline);
for (int i = 0; i < size; i++) {
final PhantomProcessRecord proc = list.valueAt(i);
pw.print(prefix);
pw.print(" proc #");
pw.print(i);
pw.print(": ");
pw.println(proc.toString());
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);
}
}
}