English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
مسار معالجة إشارات عملية المبادئية الأندرويد
في الأندرويد، عند انتهاء عملية (exit())، يتم إرسال إشارة SIGCHLD إلى عملية الأب. عند استقبال عملية الأب لهذه الإشارة، يتم إطلاق موارد النظام المخصصة لهذه الفرعية؛ ويجب على عملية الأب الت调用 wait() أو waitpid() لانتظار انتهاء الفرعية. إذا لم يقم عملية الأب بذلك، أو لم يتم إجراء هذا التحقق عند بدء عملية الأب، بتكليف signal(SIGCHLD, SIG_IGN) لتجاهل معالجة إشارة SIGCHLD، فإن الفرعية ستظل في حالة الانهيار الحالية، ولن تنتهي تمامًا. هذه الفرعية لا يمكن توجيهها، وما تقوم به هو مجرد الحصول على مكان في قائمة العمليات، وتبقيت معلومات حول PID للعملية، حالة الانهيار، وقت استخدام CPU وما إلى ذلك؛ نسمي هذه العملية بـ 'Zombie'، أي عملية ميتة.
في لينكس، الغرض من ضبط عملية الموت هو الحفاظ على بعض معلومات فرعية لفائدة عملية الأب للاستعلام لاحقا. خاصةً، إذا تم إيقاف عملية الأب، فإن عملية الأب للعمليات الميتة لهذه العملية ستكون عملية المبادئية (PID 1)، وستكون عملية المبادئية مسئولة عن استعادة هذه العمليات الميتة (ستنتظر عملية المبادئية wait()/waitpid()، وستزيل معلوماتها من قائمة العمليات).
بسبب أن عملية الموت تستمر في الحصول على مكان في قائمة العمليات، وعدد العمليات التي يدعمها لينكس محدود؛ عند تجاوز هذا الحد، لا يمكننا إنشاء عملية. لذا، من الضروري تنظيف العمليات الميتة، والتأكد من تشغيل النظام بشكل طبيعي.
الآن، دعنا نحلل كيفية معالجة عملية Init لإشارة SIGCHLD.
في ملف Init.cpp، نحن نستخدم signal_handler_init() لتحديد معالجة إشارة SIGCHLD:
void signal_handler_init() { // إنشاء آلية إشارة لـ SIGCHLD. int s[2]; // يخلق socketpair() زوجًا من套دات UNIX المسمى غير المحددة ومرتبطة ببعضها البعض if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s) == -1) { ERROR("socketpair failed: %s\n", strerror(errno)); exit(1); } signal_write_fd = s[0]; signal_read_fd = s[1]; // تكتب إلى signal_write_fd إذا تم التقاطع SIGCHLD. struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = SIGCHLD_handler; ضبط معالج دالة الإشارة، عند ظهور إشارة، يتم كتابة بيانات في socket الذي تم إنشاؤه في الأعلى، ويتم استدعاء الدالة المسجلة لمعالجة الحدث عند ملاحظة fd القابلة للقراءة في socket هذا act.sa_flags = SA_NOCLDSTOP; ضبط العلامة، مما يعني أننا نقبل إشارة SIGCHID فقط عند إنهاء فرع الفرع sigaction(SIGCHLD, &act, 0); تحديد طريقة معالجة إشارة SIGCHLD reap_any_outstanding_children(); معالجة الفرع الذي غادر قبل ذلك register_epoll_handler(signal_read_fd, handle_signal); }
نحن نستخدم دالة sigaction() لتح�始化 الإشارات. في معامل act، يتم تحديد معالج الإشارة: SIGCHLD_handler()؛ إذا وصلت إشارة، يتم استدعاء هذه الدالة للمعالجة؛ بالإضافة إلى ذلك، في معامل act، قمنا بضبط علامة SA_NOCLDSTOP، مما يعني أننا نقبل إشارة SIGCHLD فقط عند إنهاء فرع الفرع.
في Linux، الإشارات هي انقطاعات ناعمة، لذا وصول الإشارة سيؤدي إلى إنهاء العملية التي كانت تتم معالجتها حاليًا. لذا، لا يجب أن يتم استدعاء بعض الدوال غير القابلة للإعادة التشغيل في الدوال المعالجة المسجلة. بالإضافة إلى ذلك، لن يقوم Linux بترتيب الإشارات، حيث سيتم إرسال إشارة واحدة فقط إلى عملية بعد معالجة الإشارة الحالية، حتى لو تم استقبال إشارات أخرى أثناء معالجة الإشارة الحالية؛ لذا يوجد احتمال فقد الإشارة. لتجنب فقد الإشارة، يجب أن تكون عمليات الدالة المعالجة المسجلة فعالة ومتسارعة بأقصى قدر ممكن.}
أما بالنسبة لمعالجة إشارة SIGCHLD، فإن عملية الآباء ستقوم بالانتظار، وهو وقت طويل. لتحقيق هذا، تم إنشاء زوج من sockets المحلية غير المسمى والمتعلقة للتواصل بين الأطراف في الكود المبكر للإشارات. الدالة المعالجة المسجلة هي SIGCHLD_handler():
static void SIGCHLD_handler(int) { if (TEMP_FAILURE_RETRY(write(signal_write_fd, "1", 1)) == -1) { ERROR("فشل كتابة signal_write_fd: %s\n", strerror(errno)); } }
#define TEMP_FAILURE_RETRY(exp) \ ({ \ decltype(exp) _rc; \ do { \ _rc = (exp); \ } while (_rc == -1 && errno == EINTR); \ _rc; \ )
عند وصول إشارة، ما دام يتم كتابة البيانات في socket، فإن هذه العملية سريعة جدًا، ويتم نقل معالجة الإشارة إلى استجابة socket؛ لذا لن يؤثر ذلك على معالجة الإشارة التالية. بالإضافة إلى ذلك، يحتوي دالة write() على دائرة while...do مدمجة، شرط الدائرة هو خطأ في write() وإذا كان الرقم المتعلق بالخطأ EINTR (EINTR: تم قطع هذه الطلبات من قبل الإشارة)، أي إذا كان خطأ write() بسبب وصول الإشارة، فإن العملية ستتم مرة أخرى؛ في جميع الحالات الأخرى، يتم تنفيذ دالة write() مرة واحدة فقط. بعد إعادة تعريف معالجة الإشارات، يتم استدعاء reap_any_outstanding_children() لمعالجة حالات الخروج للعمليات السابقة:
static void reap_any_outstanding_children() { while (wait_for_one_process()) { } }
يستدعي wait_for_one_process() وظيفة waitpid() لانتظار انتهاء عملية الفرع، وعندما يكون هذا العملا represents خدمة تحتاج إلى إعادة التشغيل، يتم القيام به بعض الإعدادات والتنظيفات.
في النهاية، يتم تسجيل socket المحلي باستخدام epoll_ctl() لمراقبة ما إذا كان يمكن قراءته؛ وقد تم تسجيل معالج أحداث epoll:
register_epoll_handler(signal_read_fd, handle_signal);
void register_epoll_handler(int fd, void (*fn)()) { epoll_event ev; ev.events = EPOLLIN; // للصفحة المناسبة قابلة للقراءة ev.data.ptr = reinterpret_cast<void*>(fn); // حفظ استدلال الدالة المحددة، التي سيتم استخدامها في معالجة الأحداث اللاحقة if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) { // إضافة fd الذي سيتم مراقبته إلى epoll_fd، مثل مراقبة أحداث property وkeychord وsignal ERROR("فشل epoll_ctl: %s\n", strerror(errno)); } }
نحن نستخدم انتهاء عملية Zygote كمثال لفهم عملية معالجة إشارة SIGCHLD بشكل دقيق. يتم إعلان عملية Zygote كخدمة في ملف init.rc ويتم إنشاؤها بواسطة عملية Init. عند انتهاء عملية Zygote، يتم إرسال إشارة SIGCHLD إلى عملية Init. قد اكتملت عملية التكوين للإشارة في الكود السابق، لذا يتم استدعاء وظيفة SIGCHLD_handler() عند وصول الإشارة لمعالجة الإشارة، والتي تتكون من كتابة بيانات عبر socket واستعادة العمل على الفور؛ في هذه الحالة، يتم نقل معالجة SIGCHLD إلى استجابة أحداث socket. لقد سجلنا socket المحلي باستخدام epoll_ctl وسمعنا إلى ما إذا كان يمكن قراءته؛ بسبب استدعاء write() السابق، يوجد الآن بيانات يمكن قراءتها في socket، وسيتم استدعاء وظيفة handle_signal() المسجلة لمعالجة هذه البيانات:
static void handle_signal() {}} // تنظيف الطلبات الجارية. char buf[32]; read(signal_read_fd, buf, sizeof(buf)); reap_any_outstanding_children(); }
سيقوم بتحميل بيانات socket إلى buffer، ثم تنفيذ وظيفة reap_any_outstanding_children() لتعامل انتهاء عملية الفرعية وإعادة تشغيل الخدمة:
static void reap_any_outstanding_children() { while (wait_for_one_process()) { } }
static bool wait_for_one_process() { int status; pid_t pid = TEMP_FAILURE_RETRY(waitpid(-1, &status, WNOHANG)); // الانتظار على إنهاء عملية فرعية، والحصول على رقم عملية الفرعية، WNOHANG يعني أنه إذا لم تنتهي أي عملية فرعية، فتقوم بالعودة فورًا. إذا (pid == 0) { return false; } آخره إذا (pid == -1) { ERROR("فشل waitpid: %s\n", strerror(errno)); return false; } service* svc = service_find_by_pid(pid); // البحث عن معلومات الخدمة بناءً على pid في السلسلة. std::string name; إذا (svc) { name = android::base::StringPrintf("خدمة '%s' (pid %d)", svc->name, pid); } name = android::base::StringPrintf("pid غير متتبع %d", pid); } NOTICE("%s %s\n", name.c_str(), DescribeStatus(status).c_str()); إذا (!svc) { إرجاع صحيح; } // TODO: كل الكود من هنا حتى هنا يجب أن يكون وظيفة عضوية على الخدمة. // إذا لم يتم تعيين علامة SVC_ONESHOT في عملية الخدمة، أو تم تعيين علامة SVC_RESTART، قم أولاً بقتل العملية الحالية، ثم قم بإنشاء عملية جديدة; // لتجنب الأخطاء عند إعادة تشغيل العمليات لاحقاً، لأن عملية الخدمة الحالية موجودة بالفعل. if (!(svc->flags & SVC_ONESHOT) || (svc->flags & SVC_RESTART)) { NOTICE("Service '%s' (pid %d) killing any children in process group\n", svc->name, pid); kill(-pid, SIGKILL); } // إزالة أي sockets قد تم إنشاؤها. // إذا تم إنشاء socket لهذه عملية الخدمة من قبل، فيجب علينا إزالة هذا socket الآن for (socketinfo* si = svc->sockets; si; si = si->next) { char tmp[128]; snprintf(tmp, sizeof(tmp), ANDROID_SOCKET_DIR"/%s", si->name); unlink(tmp);// إزالة ملفsocket هذا الجهاز } if (svc->flags & SVC_EXEC) {//// الخدمة خرجت بشكل كامل، قم بإزالة جميع المعلومات، وإزالة الخدمة من svc-slist INFO("SVC_EXEC pid %d completed...\n", svc->pid); waiting_for_exec = false; list_remove(&svc->slist); free(svc->name); free(svc); إرجاع صحيح; } svc->pid = 0; svc->flags &= (~SVC_RUNNING); // عملية Oneshot تدخل في حالة التعطيل عند الخروج, // باستثناء إعادة التشغيل اليدوية. // إذا كان هناك علامة SVC_ONESHOT في عملية الخدمة، وليس هناك علامة SVC_RESTART، فإن ذلك يعني أن الخدمة لا تحتاج إلى إعادة التشغيل إذا ((svc->flags & SVC_ONESHOT) && !(svc->flags & SVC_RESTART)) { svc->flags |= SVC_DISABLED; } // لا يتم إعادة تشغيل العمليات المحطمة والمعدلة تلقائيًا. // إذا كانت الخدمة تحتوي على علامة SVC_RESET، فإن الخدمة لا تحتاج إلى إعادة التشغيل if (svc->flags & (SVC_DISABLED | SVC_RESET)) { // من النتيجة يمكن رؤية أن أولوية التقييم لـ SVC_RESET أعلى svc->NotifyStateChange("stopped"); إرجاع صحيح; } // حتى الآن، يمكننا معرفة أن عملية العملية في init.rc إذا لم يتم تعريف SVC_ONESHOT وSVC_RESET، عند وفاتها، سيتم إعادة تشغيلها // ولكن، إذا كان لدير العملية علامة SVC_CRITICAL ولم يكن لديها علامة SVC_RESTART، عند إعادة التشغيل والانهيار أكثر من 4 مرات، فإن النظام سيقوم بإعادة التشغيل تلقائيًا وسيصل إلى وضع الاسترداد time_t now = gettime(); if ((svc->flags & SVC_CRITICAL) && !(svc->flags & SVC_RESTART)) { if (svc->time_crashed + CRITICAL_CRASH_WINDOW >= now) { if (++svc->nr_crashed > CRITICAL_CRASH_THRESHOLD) { ERROR("عملية حرجة '%s' غادرت %d مرة في %d دقيقة; ") "إعادة التشغيل إلى وضع الاسترداد\n", svc->name, CRITICAL_CRASH_THRESHOLD, CRITICAL_CRASH_WINDOW / 60); android_reboot(ANDROID_RB_RESTART2, 0, "recovery"); إرجاع صحيح; } } svc->time_crashed = now; svc->nr_crashed = 1; } } svc->flags &= (~SVC_RESTART); svc->flags |= SVC_RESTARTING; // لتحديد أن الخدمة تحتاج إلى إعادة التشغيل، يتم إضافة علامة إعادة التشغيل لها؛ العمل اللاحق يجب أن يتم بناءً على ذلك // تنفيذ جميع أوامر onrestart لهذه الخدمة. struct listnode* node; list_for_each(node, &svc->onrestart.commands) {//إذا كانت الخدمة تحتوي على خيار onrestart،فسيتم مرور قائمة الأوامر التي يجب تنفيذها عند إعادة تشغيل العملية،وأداء هذه الأوامر command* cmd = node_to_item(node, struct command, clist); cmd->func(cmd->nargs, cmd->args); } svc->NotifyStateChange("إعادة التشغيل"); إرجاع صحيح; }
يتم التعامل مع هذه النقاط الرئيسية في هذه الوظيفة:
如果这个子进程所代表的服务需要重启,则会为该服务加上SVC_RESTARTING标志。
在之前介绍Init进程初始化流程时,我们分析过,当Init进程处理完,它就会进入一个循环化身为守护进程,处理signal、property和keychord等服务:
while (true) { if (!waiting_for_exec) { execute_one_command();//执行命令列表中的命令 restart_processes();//启动服务列表中的进程 } int timeout = -1; if (process_needs_restart) { timeout = (process_needs_restart - gettime()) * 1000; if (timeout < 0) timeout = 0; } if (!action_queue_empty() || cur_action) { timeout = 0; } bootchart_sample(&timeout);//bootchart是一个用可视化方式对启动过程进行性能分析的工具;需要定时唤醒进程 epoll_event ev; int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout));//开始轮询,epoll_wait()等待事件产生 if (nr == -1) { ERROR("epoll_wait failed: %s\n", strerror(errno)); } else if (nr == 1) { ((void (*)()) ev.data.ptr)();//调用epoll_event事件存储的函数指针处理事件 } }
在里面,它会循环调用restart_processes()去重启在service_list列表中带有所有带有SVC_RESTARTING标志(该标志是在wait_for_one_process()处理中设置的)的服务:
static void restart_processes() { process_needs_restart = 0; service_for_each_flags(SVC_RESTARTING, restart_service_if_needed);}} } void service_for_each_flags(unsigned matchflags, void (*func)(struct service *svc)) { struct listnode *node; struct service *svc; list_for_each(node, &service_list) { svc = node_to_item(node, struct service, slist); if (svc->flags & matchflags) { func(svc); } } }
static void restart_service_if_needed(struct service *svc) { time_t next_start_time = svc->time_started + 5; if (next_start_time <= gettime()) { svc->flags &= (~SVC_RESTARTING); service_start(svc, NULL); return; } if ((next_start_time < process_needs_restart) || (process_needs_restart == 0)) { process_needs_restart = next_start_time; } }
سيتم استدعاء دالة service_start() لاستعادة خدمة مغلقة. عملية معالجة دالة service_start() تمت مناقشتها عند شرح عملية معالجة عملية Init، لذا لن أكرر ذلك.
شكرًا على القراءة، آمل أن تساعدكم، شكرًا لدعمكم لموقعنا!