Избегание состояния гонки fork()/SIGCHLD

Пожалуйста, рассмотрите следующий псевдокод fork()/SIGCHLD.

  // main program excerpt
    for (;;) {
      if ( is_time_to_make_babies ) {

        pid = fork();
        if (pid == -1) {
          /* fail */
        } else if (pid == 0) {
          /* child stuff */
          print "child started"
          exit
        } else {
          /* parent stuff */
          print "parent forked new child ", pid
          children.add(pid);
        }

      }
    }

  // SIGCHLD handler
  sigchld_handler(signo) {
    while ( (pid = wait(status, WNOHANG)) > 0 ) {
      print "parent caught SIGCHLD from ", pid
      children.remove(pid);
    }
  }

В приведенном выше примере есть условие гонки. Возможно, что "/* child stuff */" заканчивается до начала "/* parent stuff */", что может привести к тому, что дочерний pid будет добавлен в список дочерних элементов после его выхода и никогда не будет удален. Когда придет время закрыть приложение, родитель будет бесконечно ждать, пока закончит законченный ребенок.

Одно из решений, о котором я могу думать, - это иметь два списка: started_children и finished_children. Я бы добавил к started_children в том же месте, что и сейчас, в children. Но в обработчике сигнала вместо удаления из children я бы добавил к finished_children. Когда приложение закрывается, родитель может просто подождать, пока разница между started_children и finished_children равна нулю.

Другим возможным решением, о котором я могу думать, является использование общей памяти, например. поделиться родительским списком детей и позволить детям .add и .remove сами? Но я не слишком много знаю об этом.

EDIT: Еще одно возможное решение, которое первым делом пришло в голову, - просто добавить sleep(1) в начале /* child stuff */, но это пахнет мне смешно, и именно поэтому я его оставил. Я даже не уверен, что это 100% -ное исправление.

Итак, как бы вы исправили это состояние гонки? И если у вас есть хорошо зарекомендовавший себя образец, сообщите мне об этом!

Спасибо.

Ответ 1

Самое простое решение - заблокировать сигнал SIGCHLD до fork() с помощью sigprocmask() и разблокировать его в родительском коде после того, как вы обработали pid.

Если ребенок умер, обработчик сигнала для SIGCHLD будет вызван после того, как вы разблокируете сигнал. Это критическая секция - в вашем случае критический раздел начинается до fork() и заканчивается после children.add().

Ответ 2

Если вы не можете использовать критический фрагмент, возможно, простой счетчик может выполнить эту работу. +1 при добавлении, -1 при удалении, без мата, который происходит первым, вы в конечном итоге можете получить нуль, когда все будет сделано.

Ответ 3

В дополнение к существующим "детям" добавить новую структуру данных "ранние смерти". Это обеспечит чистоту содержимого детей.

  // main program excerpt
    for (;;) {
      if ( is_time_to_make_babies ) {

        pid = fork();
        if (pid == -1) {
          /* fail */
        } else if (pid == 0) {
          /* child stuff */
          print "child started"
          exit
        } else {
          /* parent stuff */
          print "parent forked new child ", pid
          if (!earlyDeaths.contains(pid)) {
              children.add(pid);
          } else {
              earlyDeaths.remove(pid);
          }
        }

      }
    }

  // SIGCHLD handler
  sigchld_handler(signo) {
    while ( (pid = wait(status, WNOHANG)) > 0 ) {
      print "parent caught SIGCHLD from ", pid
      if (children.contains(pid)) {
          children.remove(pid);
      } else {
          earlyDeaths.add(pid);
      }
    }
  }

РЕДАКТИРОВАТЬ: это может быть упрощено, если ваш процесс однопоточен - earlyDeaths не обязательно должен быть контейнером, он просто должен содержать один pid.

Ответ 4

Может быть, оптимистический алгоритм? Попробуйте children.remove(pid), и если это не удается, продолжайте жизнь.

Или проверьте, что pid у детей, прежде чем пытаться удалить его?