1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.am; 18 19 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; 20 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; 21 import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS; 22 23 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESSES; 24 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; 25 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; 26 27 import android.app.ApplicationExitInfo.Reason; 28 import android.app.ApplicationExitInfo.SubReason; 29 import android.os.Handler; 30 import android.os.Process; 31 import android.os.StrictMode; 32 import android.util.FeatureFlagUtils; 33 import android.util.Slog; 34 import android.util.SparseArray; 35 36 import com.android.internal.annotations.GuardedBy; 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.internal.os.ProcStatsUtil; 39 import com.android.internal.os.ProcessCpuTracker; 40 41 import libcore.io.IoUtils; 42 43 import java.io.File; 44 import java.io.FileDescriptor; 45 import java.io.FileInputStream; 46 import java.io.FileNotFoundException; 47 import java.io.IOException; 48 import java.io.InputStream; 49 import java.io.PrintWriter; 50 import java.util.ArrayList; 51 import java.util.Collections; 52 import java.util.function.Function; 53 54 /** 55 * Activity manager code dealing with phantom processes. 56 */ 57 public final class PhantomProcessList { 58 static final String TAG = TAG_WITH_CLASS_NAME ? "PhantomProcessList" : TAG_AM; 59 60 final Object mLock = new Object(); 61 62 /** 63 * All of the phantom process record we track, key is the pid of the process. 64 */ 65 @GuardedBy("mLock") 66 final SparseArray<PhantomProcessRecord> mPhantomProcesses = new SparseArray<>(); 67 68 /** 69 * The mapping between app processes and their phantom processess, outer key is the pid of 70 * the app process, while the inner key is the pid of the phantom process. 71 */ 72 @GuardedBy("mLock") 73 final SparseArray<SparseArray<PhantomProcessRecord>> mAppPhantomProcessMap = 74 new SparseArray<>(); 75 76 /** 77 * The mapping of the pidfd to PhantomProcessRecord. 78 */ 79 @GuardedBy("mLock") 80 final SparseArray<PhantomProcessRecord> mPhantomProcessesPidFds = new SparseArray<>(); 81 82 /** 83 * The list of phantom processes tha's being signaled to be killed but still undead yet. 84 */ 85 @GuardedBy("mLock") 86 final SparseArray<PhantomProcessRecord> mZombiePhantomProcesses = new SparseArray<>(); 87 88 @GuardedBy("mLock") 89 private final ArrayList<PhantomProcessRecord> mTempPhantomProcesses = new ArrayList<>(); 90 91 /** 92 * The mapping between a phantom process ID to its parent process (an app process) 93 */ 94 @GuardedBy("mLock") 95 private final SparseArray<ProcessRecord> mPhantomToAppProcessMap = new SparseArray<>(); 96 97 @GuardedBy("mLock") 98 private final SparseArray<InputStream> mCgroupProcsFds = new SparseArray<>(); 99 100 @GuardedBy("mLock") 101 private final byte[] mDataBuffer = new byte[4096]; 102 103 @GuardedBy("mLock") 104 private boolean mTrimPhantomProcessScheduled = false; 105 106 @GuardedBy("mLock") 107 int mUpdateSeq; 108 109 @VisibleForTesting 110 Injector mInjector; 111 112 private final ActivityManagerService mService; 113 private final Handler mKillHandler; 114 PhantomProcessList(final ActivityManagerService service)115 PhantomProcessList(final ActivityManagerService service) { 116 mService = service; 117 mKillHandler = service.mProcessList.sKillHandler; 118 mInjector = new Injector(); 119 } 120 121 @VisibleForTesting 122 @GuardedBy("mLock") lookForPhantomProcessesLocked()123 void lookForPhantomProcessesLocked() { 124 mPhantomToAppProcessMap.clear(); 125 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); 126 try { 127 synchronized (mService.mPidsSelfLocked) { 128 for (int i = mService.mPidsSelfLocked.size() - 1; i >= 0; i--) { 129 final ProcessRecord app = mService.mPidsSelfLocked.valueAt(i); 130 lookForPhantomProcessesLocked(app); 131 } 132 } 133 } finally { 134 StrictMode.setThreadPolicy(oldPolicy); 135 } 136 } 137 138 @GuardedBy({"mLock", "mService.mPidsSelfLocked"}) lookForPhantomProcessesLocked(ProcessRecord app)139 private void lookForPhantomProcessesLocked(ProcessRecord app) { 140 if (app.appZygote || app.isKilled() || app.isKilledByAm()) { 141 // process forked from app zygote doesn't have its own acct entry 142 return; 143 } 144 final int appPid = app.getPid(); 145 InputStream input = mCgroupProcsFds.get(appPid); 146 if (input == null) { 147 String path = null; 148 try { 149 path = getCgroupFilePath(app.info.uid, appPid); 150 input = mInjector.openCgroupProcs(path); 151 } catch (IllegalArgumentException e) { 152 if (DEBUG_PROCESSES) { 153 Slog.w(TAG, "Unable to obtain cgroup.procs path ", e); 154 } 155 return; 156 } catch (FileNotFoundException | SecurityException e) { 157 if (DEBUG_PROCESSES) { 158 Slog.w(TAG, "Unable to open " + path, e); 159 } 160 return; 161 } 162 // Keep the FD open for better performance 163 mCgroupProcsFds.put(appPid, input); 164 } 165 final byte[] buf = mDataBuffer; 166 try { 167 int read = 0; 168 int pid = 0; 169 long totalRead = 0; 170 do { 171 read = mInjector.readCgroupProcs(input, buf, 0, buf.length); 172 if (read == -1) { 173 break; 174 } 175 totalRead += read; 176 for (int i = 0; i < read; i++) { 177 final byte b = buf[i]; 178 if (b == '\n') { 179 addChildPidLocked(app, pid, appPid); 180 pid = 0; 181 } else { 182 pid = pid * 10 + (b - '0'); 183 } 184 } 185 if (read < buf.length) { 186 // we may break from here safely as sysfs reading should return the whole page 187 // if the remaining data is larger than a page 188 break; 189 } 190 } while (true); 191 if (pid != 0) { 192 addChildPidLocked(app, pid, appPid); 193 } 194 // rewind the fd for the next read 195 input.skip(-totalRead); 196 } catch (IOException e) { 197 Slog.e(TAG, "Error in reading cgroup procs from " + app, e); 198 IoUtils.closeQuietly(input); 199 mCgroupProcsFds.delete(appPid); 200 } 201 } 202 203 @VisibleForTesting getCgroupFilePath(int uid, int pid)204 String getCgroupFilePath(int uid, int pid) { 205 return nativeGetCgroupProcsPath(uid, pid); 206 } 207 getProcessName(int pid)208 static String getProcessName(int pid) { 209 String procName = ProcStatsUtil.readTerminatedProcFile( 210 "/proc/" + pid + "/cmdline", (byte) '\0'); 211 if (procName == null) { 212 return null; 213 } 214 int l = procName.lastIndexOf('/'); 215 if (l > 0 && l < procName.length() - 1) { 216 procName = procName.substring(l + 1); 217 } 218 return procName; 219 } 220 221 @GuardedBy({"mLock", "mService.mPidsSelfLocked"}) addChildPidLocked(final ProcessRecord app, final int pid, final int appPid)222 private void addChildPidLocked(final ProcessRecord app, final int pid, final int appPid) { 223 if (appPid != pid) { 224 // That's something else... 225 final ProcessRecord r = mService.mPidsSelfLocked.get(pid); 226 if (r != null) { 227 // Is this a process forked via app zygote? 228 if (!r.appZygote) { 229 // Unexpected... 230 if (DEBUG_PROCESSES) { 231 Slog.w(TAG, "Unexpected: " + r + " appears in the cgroup.procs of " + app); 232 } 233 } else { 234 // Just a child process of app zygote, no worries 235 } 236 } else { 237 final int index = mPhantomToAppProcessMap.indexOfKey(pid); 238 if (index >= 0) { // unlikely since we cleared the map at the beginning 239 final ProcessRecord current = mPhantomToAppProcessMap.valueAt(index); 240 if (app == current) { 241 // Okay it's unchanged 242 return; 243 } 244 mPhantomToAppProcessMap.setValueAt(index, app); 245 } else { 246 mPhantomToAppProcessMap.put(pid, app); 247 } 248 // Its UID isn't necessarily to be the same as the app.info.uid, since it could be 249 // forked from child processes of app zygote 250 final int uid = Process.getUidForPid(pid); 251 String procName = mInjector.getProcessName(pid); 252 if (procName == null || uid < 0) { 253 mPhantomToAppProcessMap.delete(pid); 254 return; 255 } 256 getOrCreatePhantomProcessIfNeededLocked(procName, uid, pid, true); 257 } 258 } 259 } 260 onAppDied(final int pid)261 void onAppDied(final int pid) { 262 synchronized (mLock) { 263 final int index = mCgroupProcsFds.indexOfKey(pid); 264 if (index >= 0) { 265 final InputStream inputStream = mCgroupProcsFds.valueAt(index); 266 mCgroupProcsFds.removeAt(index); 267 IoUtils.closeQuietly(inputStream); 268 } 269 } 270 } 271 272 /** 273 * Get the existing phantom process record, or create if it's not existing yet; 274 * however, before creating it, we'll check if this is really a phantom process 275 * and we'll return null if it's not. 276 */ 277 @GuardedBy("mLock") getOrCreatePhantomProcessIfNeededLocked(final String processName, final int uid, final int pid, boolean createIfNeeded)278 PhantomProcessRecord getOrCreatePhantomProcessIfNeededLocked(final String processName, 279 final int uid, final int pid, boolean createIfNeeded) { 280 // First check if it's actually an app process we know 281 if (isAppProcess(pid)) { 282 return null; 283 } 284 285 // Have we already been aware of this? 286 final int index = mPhantomProcesses.indexOfKey(pid); 287 if (index >= 0) { 288 final PhantomProcessRecord proc = mPhantomProcesses.valueAt(index); 289 if (proc.equals(processName, uid, pid)) { 290 return proc; 291 } 292 // Somehow our record doesn't match, remove it anyway 293 Slog.w(TAG, "Stale " + proc + ", removing"); 294 onPhantomProcessKilledLocked(proc); 295 } else { 296 // Is this one of the zombie processes we've known? 297 final int idx = mZombiePhantomProcesses.indexOfKey(pid); 298 if (idx >= 0) { 299 final PhantomProcessRecord proc = mZombiePhantomProcesses.valueAt(idx); 300 if (proc.equals(processName, uid, pid)) { 301 return proc; 302 } 303 // Our zombie process information is outdated, let's remove this one, it should 304 // have been gone. 305 mZombiePhantomProcesses.removeAt(idx); 306 } 307 } 308 309 if (!createIfNeeded) { 310 return null; 311 } 312 313 final ProcessRecord r = mPhantomToAppProcessMap.get(pid); 314 315 if (r != null) { 316 // It's a phantom process, bookkeep it 317 try { 318 final int appPid = r.getPid(); 319 final PhantomProcessRecord proc = new PhantomProcessRecord( 320 processName, uid, pid, appPid, mService, 321 this::onPhantomProcessKilledLocked); 322 proc.mUpdateSeq = mUpdateSeq; 323 mPhantomProcesses.put(pid, proc); 324 SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.get(appPid); 325 if (array == null) { 326 array = new SparseArray<>(); 327 mAppPhantomProcessMap.put(appPid, array); 328 } 329 array.put(pid, proc); 330 if (proc.mPidFd != null) { 331 mKillHandler.getLooper().getQueue().addOnFileDescriptorEventListener( 332 proc.mPidFd, EVENT_INPUT | EVENT_ERROR, 333 this::onPhantomProcessFdEvent); 334 mPhantomProcessesPidFds.put(proc.mPidFd.getInt$(), proc); 335 } 336 scheduleTrimPhantomProcessesLocked(); 337 return proc; 338 } catch (IllegalStateException e) { 339 return null; 340 } 341 } 342 return null; 343 } 344 isAppProcess(int pid)345 private boolean isAppProcess(int pid) { 346 synchronized (mService.mPidsSelfLocked) { 347 return mService.mPidsSelfLocked.get(pid) != null; 348 } 349 } 350 onPhantomProcessFdEvent(FileDescriptor fd, int events)351 private int onPhantomProcessFdEvent(FileDescriptor fd, int events) { 352 synchronized (mLock) { 353 final PhantomProcessRecord proc = mPhantomProcessesPidFds.get(fd.getInt$()); 354 if (proc == null) { 355 return 0; 356 } 357 if ((events & EVENT_INPUT) != 0) { 358 proc.onProcDied(true); 359 } else { 360 // EVENT_ERROR, kill the process 361 proc.killLocked("Process error", true); 362 } 363 } 364 return 0; 365 } 366 367 @GuardedBy("mLock") onPhantomProcessKilledLocked(final PhantomProcessRecord proc)368 private void onPhantomProcessKilledLocked(final PhantomProcessRecord proc) { 369 if (proc.mPidFd != null && proc.mPidFd.valid()) { 370 mKillHandler.getLooper().getQueue() 371 .removeOnFileDescriptorEventListener(proc.mPidFd); 372 mPhantomProcessesPidFds.remove(proc.mPidFd.getInt$()); 373 IoUtils.closeQuietly(proc.mPidFd); 374 } 375 mPhantomProcesses.remove(proc.mPid); 376 final int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid); 377 if (index < 0) { 378 return; 379 } 380 SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.valueAt(index); 381 array.remove(proc.mPid); 382 if (array.size() == 0) { 383 mAppPhantomProcessMap.removeAt(index); 384 } 385 if (proc.mZombie) { 386 // If it's not really dead, bookkeep it 387 mZombiePhantomProcesses.put(proc.mPid, proc); 388 } else { 389 // In case of race condition, let's try to remove it from zombie list 390 mZombiePhantomProcesses.remove(proc.mPid); 391 } 392 } 393 394 @GuardedBy("mLock") scheduleTrimPhantomProcessesLocked()395 private void scheduleTrimPhantomProcessesLocked() { 396 if (!mTrimPhantomProcessScheduled) { 397 mTrimPhantomProcessScheduled = true; 398 mService.mHandler.post(this::trimPhantomProcessesIfNecessary); 399 } 400 } 401 402 /** 403 * Clamp the number of phantom processes to 404 * {@link ActivityManagerConstants#MAX_PHANTOM_PROCESSE}, kills those surpluses in the 405 * order of the oom adjs of their parent process. 406 */ trimPhantomProcessesIfNecessary()407 void trimPhantomProcessesIfNecessary() { 408 if (!mService.mSystemReady || !FeatureFlagUtils.isEnabled(mService.mContext, 409 SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS)) { 410 return; 411 } 412 synchronized (mService.mProcLock) { 413 synchronized (mLock) { 414 mTrimPhantomProcessScheduled = false; 415 if (mService.mConstants.MAX_PHANTOM_PROCESSES < mPhantomProcesses.size()) { 416 for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) { 417 mTempPhantomProcesses.add(mPhantomProcesses.valueAt(i)); 418 } 419 synchronized (mService.mPidsSelfLocked) { 420 Collections.sort(mTempPhantomProcesses, (a, b) -> { 421 final ProcessRecord ra = mService.mPidsSelfLocked.get(a.mPpid); 422 if (ra == null) { 423 // parent is gone, this process should have been killed too 424 return 1; 425 } 426 final ProcessRecord rb = mService.mPidsSelfLocked.get(b.mPpid); 427 if (rb == null) { 428 // parent is gone, this process should have been killed too 429 return -1; 430 } 431 if (ra.mState.getCurAdj() != rb.mState.getCurAdj()) { 432 return ra.mState.getCurAdj() - rb.mState.getCurAdj(); 433 } 434 if (a.mKnownSince != b.mKnownSince) { 435 // In case of identical oom adj, younger one first 436 return a.mKnownSince < b.mKnownSince ? 1 : -1; 437 } 438 return 0; 439 }); 440 } 441 for (int i = mTempPhantomProcesses.size() - 1; 442 i >= mService.mConstants.MAX_PHANTOM_PROCESSES; i--) { 443 final PhantomProcessRecord proc = mTempPhantomProcesses.get(i); 444 proc.killLocked("Trimming phantom processes", true); 445 } 446 mTempPhantomProcesses.clear(); 447 } 448 } 449 } 450 } 451 452 /** 453 * Remove all entries with outdated seq num. 454 */ 455 @GuardedBy("mLock") pruneStaleProcessesLocked()456 void pruneStaleProcessesLocked() { 457 for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) { 458 final PhantomProcessRecord proc = mPhantomProcesses.valueAt(i); 459 if (proc.mUpdateSeq < mUpdateSeq) { 460 if (DEBUG_PROCESSES) { 461 Slog.v(TAG, "Pruning " + proc + " as it should have been dead."); 462 } 463 proc.killLocked("Stale process", true); 464 } 465 } 466 for (int i = mZombiePhantomProcesses.size() - 1; i >= 0; i--) { 467 final PhantomProcessRecord proc = mZombiePhantomProcesses.valueAt(i); 468 if (proc.mUpdateSeq < mUpdateSeq) { 469 if (DEBUG_PROCESSES) { 470 Slog.v(TAG, "Pruning " + proc + " as it should have been dead."); 471 } 472 } 473 } 474 } 475 476 /** 477 * Kill the given phantom process, all its siblings (if any) and their parent process 478 */ 479 @GuardedBy("mService") killPhantomProcessGroupLocked(ProcessRecord app, PhantomProcessRecord proc, @Reason int reasonCode, @SubReason int subReason, String msg)480 void killPhantomProcessGroupLocked(ProcessRecord app, PhantomProcessRecord proc, 481 @Reason int reasonCode, @SubReason int subReason, String msg) { 482 synchronized (mLock) { 483 int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid); 484 if (index >= 0) { 485 final SparseArray<PhantomProcessRecord> array = 486 mAppPhantomProcessMap.valueAt(index); 487 for (int i = array.size() - 1; i >= 0; i--) { 488 final PhantomProcessRecord r = array.valueAt(i); 489 if (r == proc) { 490 r.killLocked(msg, true); 491 } else { 492 r.killLocked("Caused by siling process: " + msg, false); 493 } 494 } 495 } 496 } 497 // Lastly, kill the parent process too 498 app.killLocked("Caused by child process: " + msg, reasonCode, subReason, true); 499 } 500 501 @GuardedBy("mLock") getPhantomProcessOfAppLocked(ProcessRecord app)502 private SparseArray<PhantomProcessRecord> getPhantomProcessOfAppLocked(ProcessRecord app) { 503 int index = mAppPhantomProcessMap.indexOfKey(app.getPid()); 504 if (index >= 0) { 505 return mAppPhantomProcessMap.valueAt(index); 506 } 507 return null; 508 } 509 510 /** 511 * Iterate all phantom process belonging to the given app, and invokve callback 512 * for each of them. 513 */ forEachPhantomProcessOfApp(final ProcessRecord app, final Function<PhantomProcessRecord, Boolean> callback)514 void forEachPhantomProcessOfApp(final ProcessRecord app, 515 final Function<PhantomProcessRecord, Boolean> callback) { 516 synchronized (mLock) { 517 final SparseArray<PhantomProcessRecord> array = getPhantomProcessOfAppLocked(app); 518 if (array == null) { 519 return; 520 } 521 for (int i = array.size() - 1; i >= 0; i--) { 522 final PhantomProcessRecord r = array.valueAt(i); 523 if (!callback.apply(r)) { 524 break; 525 } 526 } 527 } 528 } 529 530 /** 531 * Set process group of phantom process belonging to the given app. 532 */ setProcessGroupForPhantomProcessOfApp(final ProcessRecord app, final int group)533 void setProcessGroupForPhantomProcessOfApp(final ProcessRecord app, final int group) { 534 synchronized (mLock) { 535 lookForPhantomProcessesLocked(app); 536 final SparseArray<PhantomProcessRecord> array = getPhantomProcessOfAppLocked(app); 537 if (array == null) { 538 return; 539 } 540 for (int i = array.size() - 1; i >= 0; i--) { 541 final PhantomProcessRecord r = array.valueAt(i); 542 mService.mOomAdjuster.setProcessGroup(r.mPid, group, r.mProcessName); 543 } 544 } 545 } 546 547 @GuardedBy("tracker") updateProcessCpuStatesLocked(ProcessCpuTracker tracker)548 void updateProcessCpuStatesLocked(ProcessCpuTracker tracker) { 549 synchronized (mLock) { 550 // refresh the phantom process list with the latest cpu stats results. 551 mUpdateSeq++; 552 553 // Scan app process's accounting procs 554 lookForPhantomProcessesLocked(); 555 556 for (int i = tracker.countStats() - 1; i >= 0; i--) { 557 final ProcessCpuTracker.Stats st = tracker.getStats(i); 558 final PhantomProcessRecord r = 559 getOrCreatePhantomProcessIfNeededLocked(st.name, st.uid, st.pid, false); 560 if (r != null) { 561 r.mUpdateSeq = mUpdateSeq; 562 r.mCurrentCputime += st.rel_utime + st.rel_stime; 563 if (r.mLastCputime == 0) { 564 r.mLastCputime = r.mCurrentCputime; 565 } 566 r.updateAdjLocked(); 567 } 568 } 569 // remove the stale ones 570 pruneStaleProcessesLocked(); 571 } 572 } 573 dump(PrintWriter pw, String prefix)574 void dump(PrintWriter pw, String prefix) { 575 synchronized (mLock) { 576 dumpPhantomeProcessLocked(pw, prefix, "All Active App Child Processes:", 577 mPhantomProcesses); 578 dumpPhantomeProcessLocked(pw, prefix, "All Zombie App Child Processes:", 579 mZombiePhantomProcesses); 580 } 581 } 582 dumpPhantomeProcessLocked(PrintWriter pw, String prefix, String headline, SparseArray<PhantomProcessRecord> list)583 void dumpPhantomeProcessLocked(PrintWriter pw, String prefix, String headline, 584 SparseArray<PhantomProcessRecord> list) { 585 final int size = list.size(); 586 if (size == 0) { 587 return; 588 } 589 pw.println(); 590 pw.print(prefix); 591 pw.println(headline); 592 for (int i = 0; i < size; i++) { 593 final PhantomProcessRecord proc = list.valueAt(i); 594 pw.print(prefix); 595 pw.print(" proc #"); 596 pw.print(i); 597 pw.print(": "); 598 pw.println(proc.toString()); 599 proc.dump(pw, prefix + " "); 600 } 601 } 602 603 @VisibleForTesting 604 static class Injector { openCgroupProcs(String path)605 InputStream openCgroupProcs(String path) throws FileNotFoundException, SecurityException { 606 return new FileInputStream(path); 607 } 608 readCgroupProcs(InputStream input, byte[] buf, int offset, int len)609 int readCgroupProcs(InputStream input, byte[] buf, int offset, int len) throws IOException { 610 return input.read(buf, offset, len); 611 } 612 getProcessName(final int pid)613 String getProcessName(final int pid) { 614 return PhantomProcessList.getProcessName(pid); 615 } 616 } 617 nativeGetCgroupProcsPath(int uid, int pid)618 private static native String nativeGetCgroupProcsPath(int uid, int pid) 619 throws IllegalArgumentException; 620 } 621