| /* |
| * 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); |
| } |
| } |
| } |