理解殺進程的實現原理(轉)


基於Android 6.0的源碼剖析, 分析kill進程的實現原理,以及講講系統調用(syscall)過程,涉及源碼:

/framework/base/core/java/android/os/Process.java /framework/base/core/jni/android_util_Process.cpp /system/core/libprocessgroup/processgroup.cpp /frameworks/base/core/jni/com_android_internal_os_Zygote.cpp /kernel/kernel/signal.c /Kernel/include/linux/syscalls.h /kernel/include/uapi/asm-generic/unistd.h /bionic/libc/kernel/uapi/asm-generic/unistd.h /art/runtime/Runtime.cc /art/runtime/signal_catcher.h /art/runtime/signal_catcher.cc 

概述

文章理解Android進程創建流程,介紹了Android進程創建過程是如何從framework一步步走到虛擬機。本文正好相反則是說說進程是如何被kill的過程。簡單說,kill進程其實是通過發送signal信號的方式來完成的。創建進程從Process.start開始說起,那么殺進程則相應從Process.killProcess開始講起。

一、用戶態Kill

Process.java文件有3個方法用於殺進程,下面說說這3個方法的具體工作

 Process.killProcess(int pid) Process.killProcessQuiet(int pid) Process.killProcessGroup(int uid, int pid) 

1.1 killProcess

1.1.1 killProcess

[-> Process.java]

public static final void killProcess(int pid) { sendSignal(pid, SIGNAL_KILL); //【見小節1.1.2】 } 

其中SIGNAL_KILL = 9,這里的sendSignal是一個Native方法。在Android系統啟動過程中,虛擬機會注冊各種framework所需的JNI方法,很多時候查詢Java層的native方法所對應的native方法,可在路徑/framework/base/core/jni中找到,在Zygote篇有介紹JNI方法查看方法。

這里的sendSignal所對應的JNI方法在android_util_Process.cpp文件的android_os_Process_SendSignal方法,接下來進入見流程2.

1.1.2 android_os_Process_sendSignal

[- >android_util_Process.cpp]

void android_os_Process_sendSignal(JNIEnv* env, jobject clazz, jint pid, jint sig) { if (pid > 0) { //打印Signal信息 ALOGI("Sending signal. PID: %" PRId32 " SIG: %" PRId32, pid, sig); kill(pid, sig); } } 

sendSignalsendSignalQuiet的唯一區別就是在於是否有ALOGI()這一行代碼。最終殺進程的實現方法都是調用kill(pid, sig)方法。

1.2 killProcessQuiet

1.2.1 killProcessQuiet

[-> Process.java]

public static final void killProcessQuiet(int pid) { sendSignalQuiet(pid, SIGNAL_KILL); //【見小節1.2.2】 } 

1.2.2 android_os_Process_sendSignalQuiet

[- >android_util_Process.cpp]

void android_os_Process_sendSignalQuiet(JNIEnv* env, jobject clazz, jint pid, jint sig) { if (pid > 0) { kill(pid, sig); } } 

可見killProcesskillProcessQuiet的唯一區別在於是否輸出log。最終殺進程的實現方法都是調用kill(pid, sig)方法。

1.3 killProcessGroup

1.3.1 killProcessGroup

[-> Process.java]

public static final native int killProcessGroup(int uid, int pid); 

該Native方法所對應的Jni方法如下:

1.3.2 android_os_Process_killProcessGroup

[-> android_util_Process.cpp]

jint android_os_Process_killProcessGroup(JNIEnv* env, jobject clazz, jint uid, jint pid) { return killProcessGroup(uid, pid, SIGKILL); //【見小節1.3.3】 } 

1.3.3 killProcessGroup

[-> processgroup.cpp]

int killProcessGroup(uid_t uid, int initialPid, int signal) { int processes; const int sleep_us = 5 * 1000; // 5ms int64_t startTime = android::uptimeMillis(); int retry = 40; // 【見Step 1-3-3-1】 while ((processes = killProcessGroupOnce(uid, initialPid, signal)) > 0) { //當還有進程未被殺死,則重試,最多40次 if (retry > 0) { usleep(sleep_us); --retry; } else { break; //重試40次,仍然沒有殺死進程,代表殺進程失敗 } } if (processes == 0) { //移除進程組相應的目錄 【見Step 1-3-3-2】 return removeProcessGroup(uid, initialPid); } else { return -1; } } 

1.3.3.1 killProcessGroupOnce

[-> processgroup.cpp]

static int killProcessGroupOnce(uid_t uid, int initialPid, int signal) { int processes = 0; struct ctx ctx; pid_t pid; ctx.initialized = false; while ((pid = getOneAppProcess(uid, initialPid, &ctx)) >= 0) { processes++; if (pid == 0) { continue; //不應該進入該分支 } int ret = kill(pid, signal); //殺進程組中的進程pid } if (ctx.initialized) { close(ctx.fd); } //processes代表總共殺死了進程組中的進程個數 return processes; } 

其中getOneAppProcess方法的作用是從節點/acct/uid_<uid>/pid_<pid>/cgroup.procs中獲取相應pid,這里是進程,而非線程。

killProcessGroupOnce的功能是殺掉uid下,跟initialPid同一個進程組的所有進程。也就意味着通過kill <pid> ,當pid是某個進程的子線程時,那么最終殺的仍是進程。

最終殺進程的實現方法都是調用kill(pid, sig)方法。

1.3.3.2 removeProcessGroup

[-> processgroup.cpp]

static int removeProcessGroup(uid_t uid, int pid) { int ret; char path[PROCESSGROUP_MAX_PATH_LEN] = {0}; //刪除目錄 /acct/uid_<uid>/pid_<pid>/ convertUidPidToPath(path, sizeof(path), uid, pid); ret = rmdir(path); //刪除目錄 /acct/uid_<uid>/ convertUidToPath(path, sizeof(path), uid); rmdir(path); return ret; } 

1.4 小結

流程圖:

process-kill-quiet

process-kill-group

說明:

  • Process.killProcess(int pid): 殺pid進程
  • Process.killProcessQuiet(int pid):殺pid進程,且不輸出log信息
  • Process.killProcessGroup(int uid, int pid):殺同一個uid下同一進程組下的所有進程

以上3個方法,最終殺進程的實現方法都是調用kill(pid, sig)方法,該方法位於用戶空間的Native層,經過系統調用進入到Linux內核的sys_kill方法。對於殺進程此處的sig=9,其實與大家平時在adb里輸入的 kill -9 <pid> 效果基本一致。

接下來,進入內核態,看看殺進程的過程。

二、內核態Kill

2.1. sys_kill

[-> syscalls.h]

asmlinkage long sys_kill(int pid, int sig); 

sys_kill()方法在linux內核中沒有直接定義,而是通過宏定義SYSCALL_DEFINE2的方式來實現的。Android內核(Linux)會為每個syscall分配唯一的系統調用號,當執行系統調用時會根據系統調用號從系統調用表中來查看目標函數的入口地址,在calls.S文件中聲明了入口地址信息(這里已經追溯到匯編語言了,就不再介紹)。另外,其中asmlinkage是gcc標簽,表明該函數讀取的參數位於棧中,而不是寄存器。

[-> signal.c]

SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) { struct siginfo info; info.si_signo = sig; info.si_errno = 0; info.si_code = SI_USER; info.si_pid = task_tgid_vnr(current); info.si_uid = from_kuid_munged(current_user_ns(), current_uid()); return kill_something_info(sig, &info, pid); //【見流程2.2】 } 

SYSCALL_DEFINE2是系統調用的宏定義,方法在此處經層層展開,等價於asmlinkage long sys_kill(int pid, int sig)。關於宏展開細節就不多說了,就說一點SYSCALL_DEFINE2中的2是指sys_kill方法有兩個參數。

關於系統調用流程比較復雜,還涉及匯編語言,只需要知道 用戶空間的kill()最終調用到內核空間signal.c的kill_something_info()方法就可以。如果有興趣,想進一步了解,可查看Linux系統調用原理

2.2 kill_something_info

[-> signal.c]

static int kill_something_info(int sig, struct siginfo *info, pid_t pid) { int ret; if (pid > 0) { rcu_read_lock(); //當pid>0時,則發送給pid所對應的進程【見流程2.3】 ret = kill_pid_info(sig, info, find_vpid(pid)); rcu_read_unlock(); return ret; } read_lock(&tasklist_lock); if (pid != -1) { //當pid=0時,則發送給當前進程組; //當pid<-1時,則發送給-pid所對應的進程。 ret = __kill_pgrp_info(sig, info, pid ? find_vpid(-pid) : task_pgrp(current)); } else { //當pid=-1時,則發送給所有進程 int retval = 0, count = 0; struct task_struct * p; for_each_process(p) { if (task_pid_vnr(p) > 1 && !same_thread_group(p, current)) { int err = group_send_sig_info(sig, info, p); ++count; if (err != -EPERM) retval = err; } } ret = count ? retval : -ESRCH; } read_unlock(&tasklist_lock); return ret; } 

功能:

  • 當pid>0 時,則發送給pid所對應的進程;
  • 當pid=0 時,則發送給當前進程組;
  • 當pid=-1時,則發送給所有進程;
  • 當pid<-1時,則發送給-pid所對應的進程。

2.3 kill_pid_info

[-> signal.c]

int kill_pid_info(int sig, struct siginfo *info, struct pid *pid) { int error = -ESRCH; struct task_struct *p; rcu_read_lock(); retry: //根據pid查詢到task結構體 p = pid_task(pid, PIDTYPE_PID); if (p) { error = group_send_sig_info(sig, info, p); //【見流程2.4】 if (unlikely(error == -ESRCH)) goto retry; } rcu_read_unlock(); return error; } 

2.4 group_send_sig_info

[-> signal.c]

int group_send_sig_info(int sig, struct siginfo *info, struct task_struct *p) { int ret; rcu_read_lock(); //檢查sig是否合法以及隱私等權限問題 ret = check_kill_permission(sig, info, p); rcu_read_unlock(); if (!ret && sig) ret = do_send_sig_info(sig, info, p, true); //【見流程2.5】 return ret; } 

2.5 do_send_sig_info

[-> signal.c]

int do_send_sig_info(int sig, struct siginfo *info, struct task_struct *p, bool group) { unsigned long flags; int ret = -ESRCH; if (lock_task_sighand(p, &flags)) { ret = send_signal(sig, info, p, group); //【見流程2.6】 unlock_task_sighand(p, &flags); } return ret; } 

2.6 send_signal

[-> signal.c]

static int send_signal(int sig, struct siginfo *info, struct task_struct *t, int group) { int from_ancestor_ns = 0; #ifdef CONFIG_PID_NS from_ancestor_ns = si_fromuser(info) && !task_pid_nr_ns(current, task_active_pid_ns(t)); #endif return __send_signal(sig, info, t, group, from_ancestor_ns); //【見流程2.7】 } 

2.7 __send_signal

[-> signal.c]

static int __send_signal(int sig, struct siginfo *info, struct task_struct *t, int group, int from_ancestor_ns) { struct sigpending *pending; struct sigqueue *q; int override_rlimit; int ret = 0, result; assert_spin_locked(&t->sighand->siglock); result = TRACE_SIGNAL_IGNORED; if (!prepare_signal(sig, t, from_ancestor_ns || (info == SEND_SIG_FORCED))) goto ret; pending = group ? &t->signal->shared_pending : &t->pending; result = TRACE_SIGNAL_ALREADY_PENDING; if (legacy_queue(pending, sig)) goto ret; result = TRACE_SIGNAL_DELIVERED; if (info == SEND_SIG_FORCED) goto out_set; if (sig < SIGRTMIN) override_rlimit = (is_si_special(info) || info->si_code >= 0); else override_rlimit = 0; q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE, override_rlimit); if (q) { list_add_tail(&q->list, &pending->list); switch ((unsigned long) info) { case (unsigned long) SEND_SIG_NOINFO: q->info.si_signo = sig; q->info.si_errno = 0; q->info.si_code = SI_USER; q->info.si_pid = task_tgid_nr_ns(current, task_active_pid_ns(t)); q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid()); break; case (unsigned long) SEND_SIG_PRIV: q->info.si_signo = sig; q->info.si_errno = 0; q->info.si_code = SI_KERNEL; q->info.si_pid = 0; q->info.si_uid = 0; break; default: copy_siginfo(&q->info, info); if (from_ancestor_ns) q->info.si_pid = 0; break; } userns_fixup_signal_uid(&q->info, t); } else if (!is_si_special(info)) { if (sig >= SIGRTMIN && info->si_code != SI_USER) { result = TRACE_SIGNAL_OVERFLOW_FAIL; ret = -EAGAIN; goto ret; } else { result = TRACE_SIGNAL_LOSE_INFO; } } out_set: //將信號sig傳遞給正處於監聽狀態的signalfd signalfd_notify(t, sig); //向信號集中加入信號sig sigaddset(&pending->signal, sig); //完成信號過程,【見流程2.8】 complete_signal(sig, t, group); ret: trace_signal_generate(sig, info, t, group, result); return ret; } 

2.8 complete_signal

[-> signal.c]

static void complete_signal(int sig, struct task_struct *p, int group) { struct signal_struct *signal = p->signal; struct task_struct *t; //查找能處理該信號的線程 if (wants_signal(sig, p)) t = p; else if (!group || thread_group_empty(p)) return; else { // 遞歸查找適合的線程 t = signal->curr_target; while (!wants_signal(sig, t)) { t = next_thread(t); if (t == signal->curr_target) return; } signal->curr_target = t; } //找到一個能被殺掉的線程,如果這個信號是SIGKILL,則立刻干掉整個線程組 if (sig_fatal(p, sig) && !(signal->flags & (SIGNAL_UNKILLABLE | SIGNAL_GROUP_EXIT)) && !sigismember(&t->real_blocked, sig) && (sig == SIGKILL || !t->ptrace)) { //信號將終結整個線程組 if (!sig_kernel_coredump(sig)) { signal->flags = SIGNAL_GROUP_EXIT; signal->group_exit_code = sig; signal->group_stop_count = 0; t = p; //遍歷整個線程組,全部結束 do { task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK); //向信號集中加入信號SIGKILL sigaddset(&t->pending.signal, SIGKILL); signal_wake_up(t, 1); } while_each_thread(p, t); return; } } //該信號處於共享隊列里(即將要處理的)。喚醒已選中的目標線程,並將該信號移出隊列。 signal_wake_up(t, sig == SIGKILL); return; } 

2.9 小結

到此Signal信號已發送給目標線程,先用一副圖來小結一下上述流程:

process-kill

圖解:

流程分為用戶空間(User Space)和內核空間(Kernel Space)。從用戶空間進入內核空間需要向內核發出syscall,用戶空間的程序通過各種syscall來調用用內核空間相應的服務。系統調用是為了讓用戶空間的程序陷入內核,該陷入動作是由軟中斷來完成的。用戶態的進程進行系統調用后,CPU切換到內核態,開始執行內核函數。unistd.h文件中定義了所有的系統中斷號,用戶態程序通過不同的系統調用號來調用不同的內核服務,通過系統調用號從系統調用表中查看到相應的內核服務。

再回到信號,在Process.java中定義了如下3個信號:

public static final int SIGNAL_QUIT = 3; //用於輸出線程trace public static final int SIGNAL_KILL = 9; //用於殺進程/線程 public static final int SIGNAL_USR1 = 10; //用於強制執行GC 

對於kill -9,信號SIGKILL的處理過程,這是因為SIGKILL是不能被忽略同時也不能被捕獲,故不會由目標線程的signal Catcher線程來處理,而是由內核直接處理,到此便完成。

但對於信號3和10,則是交由目標進程(art虛擬機)的SignalCatcher線程來捕獲完成相應操作的,接下來進入目標線程來處理相應的信號。

三、Signal Catcher

實例:

  • kill -3 <pid>:該pid所在進程的SignalCatcher接收到信號SIGNAL_QUIT,則掛起進程中的所有線程並dump所有線程的狀態。
  • kill -10 <pid>: 該pid所在進程的SignalCatcher接收到信號SIGNAL_USR1,則觸發進程強制執行GC操作。

信號SIGNAL_QUIT、SIGNAL_USR1的發送流程由上一節已介紹,對於信號捕獲則是由SignalCatcher線程來捕獲完成相應操作的。在上一篇文章理解Android進程創建流程的【Step 6-2-1】中的ForkAndSpecializeCommon有涉及到signal相關的操作,接下來說說應用進程在創建過程為信號處理做了哪些准備呢?

3.1 ForkAndSpecializeCommon

[-> com_android_internal_os_Zygote.cpp]

static pid_t ForkAndSpecializeCommon(...) { //設置子進程的signal信號處理函數 //【見流程3.2】 SetSigChldHandler(); pid_t pid = fork(); //fork子進程 if (pid == 0) { //設置子進程的signal信號處理函數為默認函數 //【見流程3.3】 UnsetSigChldHandler(); //進入虛擬機,執行相關操作【見流程3.4】 env->CallStaticVoidMethod(gZygoteClass, gCallPostForkChildHooks, debug_flags, is_system_server ? NULL : instructionSet); ... } else if (pid > 0) { //進入父進程Zygote } return pid; } 

3.2 SetSigChldHandler

[-> com_android_internal_os_Zygote.cpp]

static void SetSigChldHandler() { struct sigaction sa; memset(&sa, 0, sizeof(sa)); //對sa地址內容進行清零操作 sa.sa_handler = SigChldHandler; //安裝信號 int err = sigaction(SIGCHLD, &sa, NULL); if (err < 0) { ALOGW("Error setting SIGCHLD handler: %s", strerror(errno)); } } 

進程處理某個信號前,需要先在進程中安裝此信號,安裝過程主要是建立信號值和進程對相應信息值的動作。此處 SIGCHLD=17,代表子進程退出時所相應的操作動作為SigChldHandler

3.3 UnsetSigChldHandler

[-> com_android_internal_os_Zygote.cpp]

static void UnsetSigChldHandler() { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_DFL; //在Zygote子進程中,設置信號SIGCHLD的處理器恢復為默認行為 int err = sigaction(SIGCHLD, &sa, NULL); if (err < 0) { ALOGW("Error unsetting SIGCHLD handler: %s", strerror(errno)); } } 

3.4 DidForkFromZygote

在文章理解Android進程創建流程已詳細地說明了此過程,並在小節【Step 6-2-2-1-1-1】中說過后續會單獨講講信號處理過程,那本文便是補充這個過程

[-> Runtime.cc]

void Runtime::DidForkFromZygote(JNIEnv* env, NativeBridgeAction action, const char* isa) { ... //創建Java堆處理的線程池 heap_->CreateThreadPool(); //重置gc性能數據,以保證進程在創建之前的GCs不會計算到當前app上。 heap_->ResetGcPerformanceInfo(); ... //啟動信號捕獲 【見流程3.5】 StartSignalCatcher(); //啟動JDWP線程,當命令debuger的flags指定"suspend=y"時,則暫停runtime Dbg::StartJdwp(); } 

3.5 StartSignalCatcher

[-> Runtime.cc]

void Runtime::StartSignalCatcher() {
  if (!is_zygote_) { //【見流程3.6】 signal_catcher_ = new SignalCatcher(stack_trace_file_); } } 

對於非Zygote進程才會啟動SignalCatcher線程。

3.6 SignalCatcher

[-> signal_catcher.cc]

創建SignalCatcher對象

SignalCatcher::SignalCatcher(const std::string& stack_trace_file) : stack_trace_file_(stack_trace_file), lock_("SignalCatcher lock"), cond_("SignalCatcher::cond_", lock_), thread_(nullptr) { SetHaltFlag(false); //通過pthread_create創建一個線程,線程名為"signal catcher thread"; 該線程的啟動將關聯到Android runtime。 CHECK_PTHREAD_CALL(pthread_create, (&pthread_, nullptr, &Run, this), "signal catcher thread"); Thread* self = Thread::Current(); MutexLock mu(self, lock_); while (thread_ == nullptr) { cond_.Wait(self); } } 

SignalCatcher是一個守護線程,用於捕獲SIGQUIT、SIGUSR1信號,並采取相應的行為。

Android系統中,由Zygote孵化而來的子進程,包含system_server進程和各種App進程都存在一個SignalCatcher線程,但是Zygote進程本身是沒有這個線程的。

signal_catcher_thread

上圖是systemui所在進程的部分線程信息,可以看到其中有一個SignalCatcher線程,該線程具體是如何處理信號的呢,請往下繼續看。

3.7 Run

[-> signal_catcher.cc]

void* SignalCatcher::Run(void* arg) {
  SignalCatcher* signal_catcher = reinterpret_cast<SignalCatcher*>(arg);
  CHECK(signal_catcher != nullptr);
  Runtime* runtime = Runtime::Current();
  //檢查當前線程是否依附到Android Runtime CHECK(runtime->AttachCurrentThread("Signal Catcher", true, runtime->GetSystemThreadGroup(), !runtime->IsAotCompiler())); Thread* self = Thread::Current(); DCHECK_NE(self->GetState(), kRunnable); { MutexLock mu(self, signal_catcher->lock_); signal_catcher->thread_ = self; signal_catcher->cond_.Broadcast(self); } SignalSet signals; signals.Add(SIGQUIT); //添加對信號SIGQUIT的處理 signals.Add(SIGUSR1); //添加對信號SIGUSR1的處理 while (true) { //等待信號到來,這是個阻塞操作 int signal_number = signal_catcher->WaitForSignal(self, signals); //當信號捕獲需要停止時,則取消當前線程跟Android Runtime的關聯。 if (signal_catcher->ShouldHalt()) { runtime->DetachCurrentThread(); return nullptr; } switch (signal_number) { case SIGQUIT: signal_catcher->HandleSigQuit(); //輸出線程trace break; case SIGUSR1: signal_catcher->HandleSigUsr1(); //強制GC break; default: LOG(ERROR) << "Unexpected signal %d" << signal_number; break; } } } 

這個方法中,只有信號SIGQUIT和SIGUSR1的處理過程,並沒有信號SIGKILL的處理過程,這是因為SIGKILL是不能被忽略同時也不能被捕獲,所以不會出現在Signal Catcher線程。

3.8 小結

調用流程:

ForkAndSpecializeCommon SetSigChldHandler UnsetSigChldHandler DidForkFromZygote StartSignalCatcher SignalCatcher 

另外,進程被殺后,對於binder的C/S架構,Binder的Server端掛了,Client會收到死亡通告,還會執行各種清理工作。下一篇文章會進一步說明。

四、實例分析

注:下面涉及的signal信號log是Gityuan在kernel.c中自行添加的,原生是沒有的,僅用於查看和調試使用。

4.1 kill -3

當adb終端輸入:adb -3 10562,則signal信號傳遞過程如下:

kill_3

功能:dump桌面App的進程信息。

流程:

  1. 由shell進程(9365)向桌面App的子線程SignalCatcher(10562),發送signal=3信號;該過程需要Art虛擬機參與。
  2. SignalCatcher線程收到信號3后,再向桌面App進程的子線程分別發送signal=33信號(大於31的signal都是實時信號),用於dump各個子線程的信息。

其中:

  • 9365:adb的終端sh所在進程pid;
  • 10562:桌面App的進程pid;
  • 10568:10562進程的子線程(SignalCatcher線程);
  • 上圖由紅框圈起來的線程都是進程10562的子線程;

4.2 kill -10

當adb終端輸入:adb -10 10562,則signal信號傳遞過程如下:

kill_10

功能:強制桌面App執行gc操作。

流程:由shell進程(9365)向進程桌面App的進程(10562),發送signal=10信號; 該過程需要Art虛擬機參與。

其中

  • 9365:adb的終端sh所在進程pid;
  • 10562:桌面App的進程pid;

4.3 kill -9

當adb終端輸入:adb -9 8707,則signal信號傳遞過程如下:

 

功能:殺掉手機瀏覽器進程。

流程:由shell進程(7115)向瀏覽器進程(8707),發送signal=9信號,判斷是SIGKILL信號,則由內核直接處理,殺掉該進程下的所有子線程。

其中

  • 7115:adb的終端sh所在進程pid;
  • 8707:瀏覽器的進程pid;
  • 上圖由紅框圈起來的線程都是進程8707的子線程;

4.4 小結

  • 對於kill -3kill -10流程由前面介紹的信號發送和信號處理兩個過程,過程中由Art虛擬機來對信號進行相應的處理。
  • 對於 kill -9則不同,是由linux底層來完成殺進程的過程,也就是執行到前面講到第一節中的complete_signal()方法后,判斷是SIGKILL信號,則由內核直接處理,Art虛擬機壓根沒機會來處理。

轉自:http://gityuan.com/2016/04/16/kill-signal/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM