All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.server.am.PhantomProcessList Maven / Gradle / Ivy

/*
 * 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 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> mAppPhantomProcessMap =
            new SparseArray<>();

    /**
     * The mapping of the pidfd to PhantomProcessRecord.
     */
    @GuardedBy("mLock")
    final SparseArray mPhantomProcessesPidFds = new SparseArray<>();

    /**
     * The list of phantom processes tha's being signaled to be killed but still undead yet.
     */
    @GuardedBy("mLock")
    final SparseArray mZombiePhantomProcesses = new SparseArray<>();

    @GuardedBy("mLock")
    private final ArrayList mTempPhantomProcesses = new ArrayList<>();

    /**
     * The mapping between a phantom process ID to its parent process (an app process)
     */
    @GuardedBy("mLock")
    private final SparseArray mPhantomToAppProcessMap = new SparseArray<>();

    @GuardedBy("mLock")
    private final SparseArray 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 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 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 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 callback) {
        synchronized (mLock) {
            int index = mAppPhantomProcessMap.indexOfKey(app.getPid());
            if (index >= 0) {
                final SparseArray 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 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);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy