كيفية جعل عملية الطفل تموت بعد خروج الوالدين؟

StackOverflow https://stackoverflow.com/questions/284325

  •  08-07-2019
  •  | 
  •  

سؤال

لنفترض أن لدي عملية تولد عملية فرعية واحدة بالضبط.الآن عندما تخرج العملية الأصلية لأي سبب كان (بشكل طبيعي أو غير طبيعي، عن طريق القتل، ^C، تأكيد الفشل أو أي شيء آخر) أريد أن تموت العملية الفرعية.كيف نفعل ذلك بشكل صحيح؟


بعض الأسئلة المماثلة حول تدفق المكدس:


بعض الأسئلة المماثلة حول تدفق المكدس لـ شبابيك:

هل كانت مفيدة؟

المحلول

يمكن للطفل أن يطلب من kernel التسليم SIGHUP (أو إشارة أخرى) عندما يموت الوالد عن طريق تحديد الخيار PR_SET_PDEATHSIG في prctl() استدعاء النظام مثل هذا:

prctl(PR_SET_PDEATHSIG, SIGHUP);

يرى man 2 prctl للتفاصيل.

يحرر:هذا هو نظام التشغيل Linux فقط

نصائح أخرى

أحاول حل نفس المشكلة، وبما أن برنامجي يجب أن يعمل على OS X، فإن الحل الخاص بنظام التشغيل Linux فقط لم ينجح معي.

لقد توصلت إلى نفس النتيجة التي توصل إليها الأشخاص الآخرون في هذه الصفحة - لا توجد طريقة متوافقة مع POSIX لإخطار الطفل عند وفاة أحد الوالدين.لذلك أخطأت في ثاني أفضل شيء -- إجراء استطلاع رأي الأطفال.

عندما تموت العملية الأم (لأي سبب من الأسباب)، تصبح العملية الأم للطفل هي العملية 1.إذا قام الطفل بالاستطلاع بشكل دوري، فيمكنه التحقق مما إذا كان والده هو 1.إذا كان الأمر كذلك، يجب على الطفل الخروج.

هذا ليس رائعًا، ولكنه ناجح، وهو أسهل من حلول استقصاء مقبس TCP/lockfile المقترحة في مكان آخر في هذه الصفحة.

لقد حققت ذلك في الماضي عن طريق تشغيل الكود "الأصلي" في "الطفل" والكود "المنشأ" في "الوالد" (أي:يمكنك عكس المعنى المعتاد للاختبار بعد fork()).ثم قم باحتجاز SIGCHLD في الكود "المنشأ"...

قد لا يكون ذلك ممكنًا في حالتك، ولكنه لطيف عندما يعمل.

إذا لم تتمكن من تعديل العملية الفرعية، فيمكنك تجربة شيء مثل ما يلي:

int pipes[2];
pipe(pipes)
if (fork() == 0) {
    close(pipes[1]); /* Close the writer end in the child*/
    dup2(0, pipes[0]); /* Use reader end as stdin */
    exec("sh -c 'set -o monitor; child_process & read dummy; kill %1'")
}

close(pipes[0]); /* Close the reader end in the parent */

يؤدي هذا إلى تشغيل الطفل من داخل عملية Shell مع تمكين التحكم في الوظيفة.يتم إنشاء العملية الفرعية في الخلفية.تنتظر القذيفة سطرًا جديدًا (أو EOF) ثم تقتل الطفل.

عندما يموت الوالد - بغض النظر عن السبب - فسوف يغلق طرف الأنبوب.ستحصل الصدفة الفرعية على EOF من القراءة وتشرع في قتل العملية الفرعية ذات الخلفية.

من أجل الاكتمال.على نظام التشغيل macOS، يمكنك استخدام kqueue:

void noteProcDeath(
    CFFileDescriptorRef fdref, 
    CFOptionFlags callBackTypes, 
    void* info) 
{
    // LOG_DEBUG(@"noteProcDeath... ");

    struct kevent kev;
    int fd = CFFileDescriptorGetNativeDescriptor(fdref);
    kevent(fd, NULL, 0, &kev, 1, NULL);
    // take action on death of process here
    unsigned int dead_pid = (unsigned int)kev.ident;

    CFFileDescriptorInvalidate(fdref);
    CFRelease(fdref); // the CFFileDescriptorRef is no longer of any use in this example

    int our_pid = getpid();
    // when our parent dies we die as well.. 
    LOG_INFO(@"exit! parent process (pid %u) died. no need for us (pid %i) to stick around", dead_pid, our_pid);
    exit(EXIT_SUCCESS);
}


void suicide_if_we_become_a_zombie(int parent_pid) {
    // int parent_pid = getppid();
    // int our_pid = getpid();
    // LOG_ERROR(@"suicide_if_we_become_a_zombie(). parent process (pid %u) that we monitor. our pid %i", parent_pid, our_pid);

    int fd = kqueue();
    struct kevent kev;
    EV_SET(&kev, parent_pid, EVFILT_PROC, EV_ADD|EV_ENABLE, NOTE_EXIT, 0, NULL);
    kevent(fd, &kev, 1, NULL, 0, NULL);
    CFFileDescriptorRef fdref = CFFileDescriptorCreate(kCFAllocatorDefault, fd, true, noteProcDeath, NULL);
    CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
    CFRunLoopSourceRef source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);
    CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

هل تحتوي العملية الفرعية على توجيه من/إلى العملية الأصلية؟إذا كان الأمر كذلك، فستتلقى SIGPIPE في حالة الكتابة، أو تحصل على EOF عند القراءة - يمكن اكتشاف هذه الحالات.

في نظام Linux، يمكنك تثبيت إشارة وفاة أحد الوالدين في الطفل، على سبيل المثال:

#include <sys/prctl.h> // prctl(), PR_SET_PDEATHSIG
#include <signal.h> // signals
#include <unistd.h> // fork()
#include <stdio.h>  // perror()

// ...

pid_t ppid_before_fork = getpid();
pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
    ; // continue parent execution
} else {
    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
    if (r == -1) { perror(0); exit(1); }
    // test in case the original parent exited just
    // before the prctl() call
    if (getppid() != ppid_before_fork)
        exit(1);
    // continue child execution ...

لاحظ أنه يتم تخزين معرف العملية الأصل قبل الشوكة واختباره في الطفل بعد ذلك prctl() يزيل حالة السباق بين prctl() وخروج العملية التي تسمى الطفل.

لاحظ أيضًا أن إشارة وفاة أحد الوالدين للطفل يتم مسحها في الأطفال حديثي الولادة من تلقاء نفسه.ولا يتأثر ب execve().

ويمكن تبسيط ذلك الاختبار إذا تأكدنا من أن النظام هو المسؤول عن اعتماد العملية كلها الأيتام لديه معرف 1:

pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
    ; // continue parent execution
} else {
    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
    if (r == -1) { perror(0); exit(1); }
    // test in case the original parent exited just
    // before the prctl() call
    if (getppid() == 1)
        exit(1);
    // continue child execution ...

الاعتماد على عملية النظام تلك init ومع ذلك، فإن الحصول على PID 1 ليس أمرًا محمولاً. يحدد POSIX.1-2008:

يجب تعيين معرف العملية الأصلية لجميع العمليات الفرعية الحالية وعمليات الزومبي لعملية الاستدعاء على معرف العملية لعملية نظام محددة بالتنفيذ.أي أن هذه العمليات يجب أن تكون موروثة من خلال عملية نظام خاصة.

تقليديا، عملية النظام التي تتبنى جميع الأيتام هي PID 1، أي.init - وهو سلف جميع العمليات.

على الأنظمة الحديثة مثل لينكس أو فري بي إس دي قد يكون لعملية أخرى هذا الدور.على سبيل المثال، في Linux، يمكن استدعاء العملية prctl(PR_SET_CHILD_SUBREAPER, 1) لترسيخ نفسها كعملية نظام ترث جميع الأيتام من أي من نسلها (راجع:ان مثال في فيدورا 25).

مستوحاة من إجابة أخرى هنا، توصلت إلى حل POSIX التالي.الفكرة العامة هي إنشاء عملية وسيطة بين الوالدين والطفل، لها غرض واحد:لاحظ عندما يموت الوالد، وقتل الطفل صراحة.

يكون هذا النوع من الحلول مفيدًا عندما لا يمكن تعديل التعليمات البرمجية الموجودة في الطفل.

int p[2];
pipe(p);
pid_t child = fork();
if (child == 0) {
    close(p[1]); // close write end of pipe
    setpgid(0, 0); // prevent ^C in parent from stopping this process
    child = fork();
    if (child == 0) {
        close(p[0]); // close read end of pipe (don't need it here)
        exec(...child process here...);
        exit(1);
    }
    read(p[0], 1); // returns when parent exits for any reason
    kill(child, 9);
    exit(1);
}

هناك نوعان من التحذيرات الصغيرة مع هذه الطريقة:

  • إذا قمت بقتل العملية الوسيطة عمدًا، فلن يُقتل الطفل عندما يموت الوالد.
  • إذا خرج الطفل قبل الوالد، فستحاول العملية الوسيطة قتل معرف الطفل الأصلي، والذي قد يشير الآن إلى عملية مختلفة.(يمكن إصلاح ذلك باستخدام المزيد من التعليمات البرمجية في العملية المتوسطة.)

بالإضافة إلى ذلك، الكود الفعلي الذي أستخدمه موجود في بايثون.وهنا للإكتمال:

def run(*args):
    (r, w) = os.pipe()
    child = os.fork()
    if child == 0:
        os.close(w)
        os.setpgid(0, 0)
        child = os.fork()
        if child == 0:
            os.close(r)
            os.execl(args[0], *args)
            os._exit(1)
        os.read(r, 1)
        os.kill(child, 9)
        os._exit(1)
    os.close(r)

لا أعتقد أنه من الممكن ضمان استخدام مكالمات POSIX القياسية فقط.مثل الحياة الحقيقية، بمجرد أن يولد الطفل، يصبح لديه حياة خاصة به.

هو - هي يكون من الممكن أن تلتقط العملية الأصلية معظم أحداث الإنهاء المحتملة، وتحاول قتل العملية الفرعية في تلك المرحلة، ولكن هناك دائمًا بعض الأحداث التي لا يمكن اكتشافها.

على سبيل المثال، لا يمكن لأي عملية التقاط ملف SIGKILL.عندما تتعامل النواة مع هذه الإشارة، فإنها ستقتل العملية المحددة دون أي إشعار لهذه العملية على الإطلاق.

ولتوسيع هذا التشبيه، فإن الطريقة القياسية الأخرى الوحيدة للقيام بذلك هي أن ينتحر الطفل عندما يجد أنه لم يعد لديه أحد الوالدين.

هناك طريقة Linux فقط للقيام بذلك prctl(2) - انظر الإجابات الأخرى.

كما أشار أشخاص آخرون، فإن الاعتماد على معرف الهوية الأصلي ليصبح 1 عند خروج الأصل هو أمر غير قابل للنقل.بدلاً من انتظار معرف العملية الأصلية المحدد، ما عليك سوى الانتظار حتى يتغير المعرف:

pit_t pid = getpid();
switch (fork())
{
    case -1:
    {
        abort(); /* or whatever... */
    }
    default:
    {
        /* parent */
        exit(0);
    }
    case 0:
    {
        /* child */
        /* ... */
    }
}

/* Wait for parent to exit */
while (getppid() != pid)
    ;

أضف نومًا صغيرًا حسب الرغبة إذا كنت لا تريد الاستطلاع بأقصى سرعة.

يبدو لي هذا الخيار أسهل من استخدام الأنبوب أو الاعتماد على الإشارات.

تثبيت أ معالج الفخ للقبض على SIGINT، الذي يقتل عملية طفلك إذا كان لا يزال على قيد الحياة، على الرغم من أن الملصقات الأخرى صحيحة بأنها لن تلتقط SIGKILL.

افتح ملف .lockfile مع وصول خاص واطلب من الاستقصاء الفرعي عليه محاولة فتحه - إذا نجحت عملية الفتح، فيجب إنهاء العملية الفرعية

عملت هذا الحل بالنسبة لي:

  • قم بتمرير أنبوب stdin إلى الطفل - ليس عليك كتابة أي بيانات في الدفق.
  • يقرأ الطفل لأجل غير مسمى من stdin حتى EOF.تشير EOF إلى رحيل الوالد.
  • هذه طريقة مضمونة ومحمولة لاكتشاف وقت رحيل أحد الوالدين.حتى لو تعطل الأصل، سيقوم نظام التشغيل بإغلاق الأنبوب.

كان هذا بالنسبة لعملية من النوع العامل والتي كان وجودها منطقيًا فقط عندما كان الوالد على قيد الحياة.

أعتقد أن الطريقة السريعة والقذرة هي إنشاء أنبوب بين الطفل ووالديه.عند خروج الوالدين، سيحصل الأطفال على SIGPIPE.

لقد ذكرت بعض الملصقات بالفعل الأنابيب و kqueue.في الواقع، يمكنك أيضًا إنشاء زوج متصل مآخذ مجال يونكس بواسطة socketpair() يتصل.يجب أن يكون نوع المقبس SOCK_STREAM.

لنفترض أن لديك واصفي ملف المقبس fd1 وfd2.الآن fork() لإنشاء عملية فرعية، والتي سوف ترث fds.في الوالدين تغلق fd2 وفي الطفل تغلق fd1.الآن يمكن لكل عملية poll() ما تبقى من fd المفتوح على نهايته الخاصة لـ POLLIN حدث.ما دام كل جانب لم يفعل ذلك صراحة close() fd أثناء العمر الطبيعي، يمكنك التأكد تمامًا من أن a POLLHUP يجب أن تشير العلامة إلى إنهاء الطرف الآخر (سواء كانت نظيفة أم لا).عند إخطاره بهذا الحدث، يمكن للطفل أن يقرر ما يجب فعله (على سبيل المثال.حتى الموت).

#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <poll.h>
#include <stdio.h>

int main(int argc, char ** argv)
{
    int sv[2];        /* sv[0] for parent, sv[1] for child */
    socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

    pid_t pid = fork();

    if ( pid > 0 ) {  /* parent */
        close(sv[1]);
        fprintf(stderr, "parent: pid = %d\n", getpid());
        sleep(100);
        exit(0);

    } else {          /* child */
        close(sv[0]);
        fprintf(stderr, "child: pid = %d\n", getpid());

        struct pollfd mon;
        mon.fd = sv[1];
        mon.events = POLLIN;

        poll(&mon, 1, -1);
        if ( mon.revents & POLLHUP )
            fprintf(stderr, "child: parent hung up\n");
        exit(0);
    }
}

يمكنك محاولة تجميع كود إثبات المفهوم أعلاه، وتشغيله في محطة طرفية مثل ./a.out &.لديك ما يقرب من 100 ثانية لتجربة قتل PID الأصلي بواسطة إشارات مختلفة، أو سيخرج ببساطة.في كلتا الحالتين، يجب أن تشاهد الرسالة "الطفل:الوالد معلق".

بالمقارنة مع الطريقة المستخدمة SIGPIPE المعالج، هذه الطريقة لا تتطلب تجربة write() يتصل.

هذه الطريقة أيضاً متماثل, ، أي.يمكن للعمليات استخدام نفس القناة لمراقبة وجود بعضها البعض.

يستدعي هذا الحل وظائف POSIX فقط.لقد جربت هذا في Linux و FreeBSD.أعتقد أنه يجب أن يعمل على أنظمة يونكس الأخرى ولكنني لم أختبره حقًا.

أنظر أيضا:

  • unix(7) من صفحات Linux man، unix(4) للفري بي إس دي، poll(2), socketpair(2), socket(7) على لينكس.

تحت بوسيكس, ، ال exit(), _exit() و _Exit() يتم تعريف الوظائف إلى:

  • إذا كانت العملية عبارة عن عملية تحكم، فيجب إرسال إشارة SIGHUP إلى كل عملية في مجموعة العمليات الأمامية لمحطة التحكم التابعة لعملية الاستدعاء.

لذلك، إذا قمت بالترتيب لتكون العملية الأم بمثابة عملية تحكم لمجموعة العمليات الخاصة بها، فيجب أن يحصل الطفل على إشارة SIGHUP عند خروج الأصل.لست متأكدًا تمامًا من حدوث ذلك عندما يتعطل أحد الوالدين، لكنني أعتقد أنه يحدث.بالتأكيد، بالنسبة لحالات عدم التعطل، يجب أن تعمل بشكل جيد.

لاحظ أنه قد يتعين عليك قراءة الكثير من التفاصيل الدقيقة - بما في ذلك قسم التعريفات الأساسية (التعريفات)، بالإضافة إلى معلومات خدمات النظام الخاصة exit() و setsid() و setpgrp() - للحصول على الصورة كاملة.(لذا فإنني!)

إذا قمت بإرسال إشارة إلى الرقم 0، وذلك باستخدام على سبيل المثال

kill(0, 2); /* SIGINT */

يتم إرسال هذه الإشارة إلى مجموعة العملية بأكملها، وبالتالي قتل الطفل فعليًا.

يمكنك اختباره بسهولة باستخدام شيء مثل:

(cat && kill 0) | python

إذا قمت بعد ذلك بالضغط على ^D، فسترى النص "Terminated" كإشارة إلى أن مترجم بايثون قد قُتل بالفعل، بدلاً من الخروج للتو بسبب إغلاق stdin.

في حالة ما إذا كان الأمر ذا صلة بأي شخص آخر، عندما أقوم بإنشاء مثيلات JVM في عمليات فرعية متشعبة من C++، فإن الطريقة الوحيدة التي يمكنني من خلالها إنهاء مثيلات JVM بشكل صحيح بعد اكتمال العملية الأصلية هي القيام بما يلي.نأمل أن يتمكن شخص ما من تقديم تعليقات في التعليقات إذا لم تكن هذه هي أفضل طريقة للقيام بذلك.

1) اتصل prctl(PR_SET_PDEATHSIG, SIGHUP) في العملية الفرعية المتشعبة كما هو مقترح قبل تشغيل تطبيق Java عبر execv, ، و

2) أضف خطاف إيقاف التشغيل إلى تطبيق Java الذي يستقصي حتى يساوي PID الأصلي 1، ثم قم بإجراء عملية صعبة Runtime.getRuntime().halt(0).ويتم الاقتراع عن طريق إطلاق قذيفة منفصلة تدير ps الأمر (انظر: كيف يمكنني العثور على معرف PID الخاص بي في Java أو JRuby على Linux؟).

تحرير 130118:

ويبدو أن هذا لم يكن حلا قويا.ما زلت أجد صعوبة في فهم الفروق الدقيقة لما يحدث، ولكنني كنت لا أزال أحصل في بعض الأحيان على عمليات JVM اليتيمة عند تشغيل هذه التطبيقات في جلسات الشاشة/SSH.

بدلاً من الاستقصاء عن PPID في تطبيق Java، قمت ببساطة بإجراء تنظيف لخطاف إيقاف التشغيل متبوعًا بإيقاف قوي كما هو مذكور أعلاه.ثم تأكدت من الاستدعاء waitpid في التطبيق الأصل C++ في العملية الفرعية التي تم إنشاؤها عندما حان الوقت لإنهاء كل شيء.يبدو أن هذا حل أكثر قوة، حيث تضمن العملية الفرعية إنهاءها، بينما يستخدم الأصل المراجع الموجودة للتأكد من إنهاء العملية الفرعية.قارن هذا بالحل السابق الذي يقضي بإنهاء عملية الوالدين متى شئت، وجعل الأطفال يحاولون معرفة ما إذا كانوا قد تيتموا قبل الإنهاء.

إذا توفي أحد الوالدين، يتغير PPID للأيتام إلى 1 - ما عليك سوى التحقق من PPID الخاص بك.بطريقة ما، هذا هو الاقتراع المذكور أعلاه.هنا قطعة قذيفة لذلك:

check_parent () {
      parent=`ps -f|awk '$2=='$PID'{print $3 }'`
      echo "parent:$parent"
      let parent=$parent+0
      if [[ $parent -eq 1 ]]; then
        echo "parent is dead, exiting"
        exit;
      fi
}


PID=$$
cnt=0
while [[ 1 = 1 ]]; do
  check_parent
  ... something
done

لقد وجدت حلين، كلاهما غير مثالي.

1. اقتل جميع الأطفال عن طريق القتل (-pid) عند تلقي إشارة SIGTERM.
من الواضح أن هذا الحل لا يمكنه التعامل مع "kill -9"، لكنه يعمل في معظم الحالات وهو بسيط جدًا لأنه لا يحتاج إلى تذكر جميع العمليات الفرعية.


    var childProc = require('child_process').spawn('tail', ['-f', '/dev/null'], {stdio:'ignore'});

    var counter=0;
    setInterval(function(){
      console.log('c  '+(++counter));
    },1000);

    if (process.platform.slice(0,3) != 'win') {
      function killMeAndChildren() {
        /*
        * On Linux/Unix(Include Mac OS X), kill (-pid) will kill process group, usually
        * the process itself and children.
        * On Windows, an JOB object has been applied to current process and children,
        * so all children will be terminated if current process dies by anyway.
        */
        console.log('kill process group');
        process.kill(-process.pid, 'SIGKILL');
      }

      /*
      * When you use "kill pid_of_this_process", this callback will be called
      */
      process.on('SIGTERM', function(err){
        console.log('SIGTERM');
        killMeAndChildren();
      });
    }

بنفس الطريقة، يمكنك تثبيت معالج "الخروج" كما هو مذكور أعلاه إذا اتصلت بـprocess.exit في مكان ما.ملحوظة:تمت معالجة Ctrl+C والتعطل المفاجئ تلقائيًا بواسطة نظام التشغيل لقتل مجموعة العمليات، لذلك لا يوجد المزيد هنا.

2. الاستخدام chjj/pty.js لتفرخ العملية الخاصة بك مع محطة التحكم المرفقة.
عندما تقتل العملية الحالية بأي حال من الأحوال حتى -9، سيتم قتل جميع العمليات الفرعية تلقائيًا أيضًا (بواسطة نظام التشغيل؟).أعتقد أنه نظرًا لأن العملية الحالية تحمل جانبًا آخر من الجهاز، لذلك إذا ماتت العملية الحالية، فستموت العملية الفرعية SIGPIPE.


    var pty = require('pty.js');

    //var term =
    pty.spawn('any_child_process', [/*any arguments*/], {
      name: 'xterm-color',
      cols: 80,
      rows: 30,
      cwd: process.cwd(),
      env: process.env
    });
    /*optionally you can install data handler
    term.on('data', function(data) {
      process.stdout.write(data);
    });
    term.write(.....);
    */

لقد تمكنت من إنشاء حل محمول وغير قابل للاقتراع من خلال 3 عمليات عن طريق إساءة استخدام التحكم الطرفي والجلسات.هذا هو الاستمناء العقلي، لكنه يعمل.

الحيلة هي:

  • بدأت العملية أ
  • تقوم العملية A بإنشاء أنبوب P (ولا تقرأ منه أبدًا)
  • العملية "أ" تفرع إلى العملية "ب".
  • تقوم العملية B بإنشاء جلسة جديدة
  • تقوم العملية B بتخصيص محطة افتراضية لتلك الجلسة الجديدة
  • تقوم العملية B بتثبيت معالج SIGCHLD ليموت عند خروج الطفل
  • تقوم العملية B بتعيين معالج SIGPIPE
  • شوكات العملية B إلى العملية C
  • تقوم العملية C بكل ما تحتاجه (على سبيل المثال.exec () هو الملف الثنائي غير المعدل أو يقوم بتشغيل أي منطق)
  • تكتب العملية B إلى الأنبوب P (وتمنع بهذه الطريقة)
  • العملية A تنتظر () في العملية B وتخرج عندما تموت

من ذلك الطريق:

  • إذا ماتت العملية أ:تحصل العملية B على SIGPIPE وتموت
  • إذا ماتت العملية B:تعود عملية الانتظار () الخاصة بالعملية A وتموت، وتحصل العملية C على SIGHUP (لأنه عندما يموت قائد الجلسة لجلسة ذات محطة طرفية متصلة، تحصل جميع العمليات في مجموعة العمليات الأمامية على SIGHUP)
  • إذا ماتت العملية C:تحصل العملية B على SIGCHLD وتموت، لذا تموت العملية A

نقائص:

  • لا تستطيع العملية C التعامل مع SIGHUP
  • سيتم تشغيل العملية C في جلسة مختلفة
  • لا يمكن للعملية C استخدام واجهة برمجة تطبيقات مجموعة الجلسة/العملية لأنها ستؤدي إلى كسر الإعداد الهش
  • إن إنشاء محطة لكل عملية من هذا القبيل ليست أفضل فكرة على الإطلاق

على الرغم من مرور 7 سنوات، إلا أنني واجهت هذه المشكلة للتو لأنني أقوم بتشغيل تطبيق SpringBoot الذي يحتاج إلى بدء تشغيل خادم webpack-dev-server أثناء التطوير ويحتاج إلى إيقافه عندما تتوقف عملية الواجهة الخلفية.

أحاول استخدامها Runtime.getRuntime().addShutdownHook ولكنه كان يعمل على نظام التشغيل Windows 10 ولكن ليس على نظام التشغيل Windows 7.

لقد قمت بتغييره لاستخدام مؤشر ترابط مخصص ينتظر إنهاء العملية أو انتهاءها InterruptedException والذي يبدو أنه يعمل بشكل صحيح على كلا الإصدارين من Windows.

private void startWebpackDevServer() {
    String cmd = isWindows() ? "cmd /c gradlew webPackStart" : "gradlew webPackStart";
    logger.info("webpack dev-server " + cmd);

    Thread thread = new Thread(() -> {

        ProcessBuilder pb = new ProcessBuilder(cmd.split(" "));
        pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
        pb.redirectError(ProcessBuilder.Redirect.INHERIT);
        pb.directory(new File("."));

        Process process = null;
        try {
            // Start the node process
            process = pb.start();

            // Wait for the node process to quit (blocking)
            process.waitFor();

            // Ensure the node process is killed
            process.destroyForcibly();
            System.setProperty(WEBPACK_SERVER_PROPERTY, "true");
        } catch (InterruptedException | IOException e) {
            // Ensure the node process is killed.
            // InterruptedException is thrown when the main process exit.
            logger.info("killing webpack dev-server", e);
            if (process != null) {
                process.destroyForcibly();
            }
        }

    });

    thread.start();
}

تاريخيًا، منذ إصدار UNIX v7، اكتشف نظام العملية عزلة العمليات عن طريق التحقق من المعرف الأصلي للعملية.وكما قلت تاريخيا. init(8) تعتبر عملية النظام عملية خاصة لسبب واحد فقط:لا يمكن أن يموت.لا يمكن أن تموت لأن خوارزمية kernel للتعامل مع تعيين معرف عملية أصل جديد تعتمد على هذه الحقيقة.عندما تنفذ عملية ما exit(2) استدعاء (عن طريق استدعاء نظام العملية أو عن طريق مهمة خارجية كإرسال إشارة أو ما شابه ذلك) تعيد النواة تعيين معرف العملية الأولية لجميع الأطفال في هذه العملية كمعرف العملية الأصلية.يؤدي هذا إلى الاختبار الأسهل والطريقة الأكثر سهولة لمعرفة ما إذا كانت العملية قد أصبحت يتيمة أم لا.فقط تأكد من نتيجة getppid(2) استدعاء النظام وإذا كان هو معرف العملية الخاص بـ init(2) العملية ثم أصبحت العملية معزولة قبل استدعاء النظام.

تبرز مسألتان من هذا النهج يمكن أن تؤدي إلى مشاكل:

  • أولا، لدينا إمكانية تغيير init العملية لأي عملية مستخدم، فكيف يمكننا التأكد من أن عملية init ستكون دائمًا أصل جميع العمليات اليتيمة؟حسنا، في exit رمز استدعاء النظام هناك فحص صريح لمعرفة ما إذا كانت العملية التي تنفذ الاستدعاء هي عملية init (العملية ذات معرف المنتج يساوي 1) وإذا كان الأمر كذلك، فإن النواة ستصاب بالذعر (لا ينبغي أن تكون قادرة بعد الآن على الحفاظ على التسلسل الهرمي للعملية) لذلك لا يجوز لعملية init إجراء عملية exit(2) يتصل.
  • ثانيًا، هناك حالة سباق في الاختبار الأساسي الموضح أعلاه.يُفترض تاريخيًا أن يكون معرف عملية Init 1, ، ولكن هذا لا يضمنه نهج POSIX، الذي ينص (كما هو موضح في ردود أخرى) على أن معرف عملية النظام فقط هو المحجوز لهذا الغرض.لا يوجد تطبيق posix تقريبًا يفعل ذلك، ويمكنك الافتراض في الأنظمة الأصلية المشتقة من نظام التشغيل Unix 1 كرد على getppid(2) استدعاء النظام يكفي لافتراض أن العملية يتيمة.هناك طريقة أخرى للتحقق وهي إجراء getppid(2) مباشرة بعد الشوكة ومقارنة هذه القيمة بنتيجة المكالمة الجديدة.هذا ببساطة لا يعمل في جميع الحالات، حيث أن كلا الاستدعاءين ليسا ذريين معًا، ويمكن أن تموت العملية الأصلية بعد fork(2) وقبل الأول getppid(2) مكالمة النظام.العمليةparent id only changes once, when its parent does anخروج(2)call, so this should be enough to check if theجيتبيد(2)result changed between calls to see that parent process has exit. This test is not valid for the actual children of the init process, because they are always children ofinit(8)`، ولكن يمكنك أن تفترض بأمان أن هذه العمليات ليس لها أصل أيضًا (إلا عندما تستبدل عملية init في النظام)

هناك طريقة أخرى للقيام بذلك خاصة بنظام Linux وهي إنشاء الأصل في مساحة اسم PID جديدة.سيكون بعد ذلك PID 1 في مساحة الاسم هذه، وعندما يخرج منها، سيتم قتل جميع أطفاله على الفور SIGKILL.

لسوء الحظ، لإنشاء مساحة اسم PID جديدة، يجب أن يكون لديك CAP_SYS_ADMIN.لكن هذه الطريقة فعالة جدًا ولا تتطلب أي تغيير حقيقي للوالدين أو الأبناء بعد الإطلاق الأولي للوالد.

يرى استنساخ(2), pid_namespaces(7), ، و إلغاء المشاركة(2).

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top