Книга: Linux программирование в примерах

10.8.3.2. Снисходительные родители: минимальный надзор

10.8.3.2. Снисходительные родители: минимальный надзор

В качестве альтернативы можно беспокоиться лишь о завершении потомка и не интересоваться простыми изменениями состояния (остановке и возобновлении). В этом случае используйте флаг SA_NOCLDSTOP и установите обработчик сигнала, вызывающий wait() (или родственную ей функцию) для получения данных процесса.

В общем вы не можете ожидать получать по одному сигналу SIGCHLD на каждого завершающегося потомка. Следует считать, что SIGCHLD означает «завершился по крайней мере один потомок» и быть готовым собрать при обработке SIGCHLD сведения о как можно большем числе потомков.

Следующая программа, ch10-reap1.с, блокирует SIGCHLD до тех пор, пока не будет готова восстановить потомков.

1  /* ch10-reap1.с --- демонстрирует управление SIGCHLD с использованием цикла */
2
3  #include <stdio.h>
4  #include <errno.h>
5  #include <signal.h>
6  #include <string.h>
7  #include <sys/types.h>
8  #include <sys/wait.h>
9
10 #define MAX_KIDS 42
11 #define NOT_USED -1
12
13 pid_t kids[MAX_KIDS];
14 size_t nkids = 0;

Массив потомков отслеживает ID порожденных процессов. Если элемент содержит NOT_USED, он не представляет необработанного потомка. (Его инициализируют строки 89–90 внизу) nkids указывает, сколько значений в kids следует проверить.

16 /* format_num --- вспомогательная функция, поскольку нельзя использовать [sf]printf() */
17
18 const char *format_num(int num)
19 {
20 #define NUMSIZ 30
21  static char buf[NUMSIZ];
22  int i;
23
24  if (num <= 0) {
25   strcpy(buf, "0");
26   return buf;
27  }
28
29  i = NUMSIZ - 1;
30  buf[i--] = '';
31
32  /* Преобразует цифры обратно в строку. */
33  do {
34   buf[i--] = (num % 10) + '0';
35   num /= 10;
36  } while (num > 0);
37
38  return &buf[i+1];
39 }

Поскольку обработчики сигналов не должны вызывать функции семейства printf(), мы предусмотрели для преобразования десятичного сигнала или номера PID в строку простую «вспомогательную» функцию format_num(). Это примитивно, но работает.

41 /* childhandler --- перехват SIGCHLD, сбор сведений со всех доступных потомков */
42
43 void childhandler(int sig)
44 {
45  int status, ret;
46  int i;
47  char buf[100];
48  static const char entered[] = "Entered childhandlern" ;
49  static const char exited[] = "Exited childhandlern";
50
51  writed, entered, strlen(entered));
52  for (i =0; i < nkids; i++) {
53   if (kids[i] == NOT_USED)
54    continue;
55
56 retry:
57   if ((ret = waitpid(kids[i], &status, WNOHANG)) == kids[i]) {
58    strcpy(buf, "treaped process ");
59    strcat(buf, format_num(ret));
60    strcat(buf, "n");
61    write(1, buf, strlen(buf));
62    kids[i] = NOT_USED;
63   } else if (ret == 0) {
64    strcpy(buf, "tpid ");
65    strcat(buf, format_num(kids[i]));
66    strcat(buf, " not available yetn");
67    write(1, buf, strlen(buf));
68   } else if (ret == -1 && errno == EINTR) {
69    write(1, "tretryingn", 10);
70    goto retry;
71   } else {
72    strcpy(buf, "twaitpid() failed: ");
73    strcat(buf, strerror(errno));
74    strcat(buf, "n");
75    write(1, buf, strlen(buf));
76   }
77  }
78  write(1, exited, strlen(exited));
79 }

Строки 51 и 58 выводят «входное» и «завершающее» сообщения, так что мы можем ясно видеть, когда вызывается обработчик сигнала. Другие сообщения начинаются с ведущего символа TAB.

Главной частью обработчика сигнала является большой цикл, строки 52–77. Строки 53–54 проверяют на NOT_USED и продолжают цикл, если текущий слот не используется.

Строка 57 вызывает waitpid() с PID текущего элемента kids. Мы предусмотрели опцию WNOHANG, которая заставляет waitpid() возвращаться немедленно, если затребованный потомок недоступен. Этот вызов необходим, так как возможно, что не все потомки завершились.

Основываясь на возвращенном значении, код предпринимает соответствующее действие. Строки 57–62 обрабатывают случай обнаружения потомка, выводя сообщение и помещая в соответствующий слот в kids значение NOT_USED.

Строки 63–67 обрабатывают случай, когда затребованный потомок недоступен. В этом случае возвращается значение 0, поэтому выводится сообщение, и выполнение продолжается.

Строки 68–70 обрабатывают случай, при котором был прерван системный вызов. В этом случае самым подходящим способом обработки является goto обратно на вызов waitpid(). (Поскольку main() блокирует все сигналы при вызове обработчика сигнала [строка 96], это прерывание не должно случиться. Но этот пример показывает, как обработать все случаи.)

Строки 71–76 обрабатывают любую другую ошибку, выводя соответствующее сообщение об ошибке.

81  /* main --- установка связанных с порожденными процессами сведений и сигналов, создание порожденных процессов */
82
83  int main(int argc, char **argv)
84  {
85   struct sigaction sa;
86   sigset_t childset, emptyset;
87   int i;
88
89   for (i = 0; i < nkids; i++)
90    kids[i] = NOT_USED;
91
92   sigemptyset(&emptyset);
93
94   sa.sa_flags =SA_NOCLDSTOP;
95   sa.sa_handler = childhandler;
96   sigfillset(&sa.sa_mask); /* блокировать все при вызове обработчика */
97   sigaction(SIGCHLD, &sa, NULL);
98
99   sigemptyset(&childset);
100  sigaddset(&childset, SIGCHLD);
101
102  sigprocmask(SIG_SETMASK, &childset, NULL); /* блокировать его в коде main */
103
104  for (nkids = 0; nkids < 5; nkids++) {
105   if ((kids[nkids] = fdrk()) == 0) {
106    sleep(3);
107    _exit(0);
108   }
109  }
110
111  sleep(5); /* дать потомкам возможность завершения */
112
113  printf("waiting for signaln");
114  sigsuspend(&emptyset);
115
116  return 0;
117 }

Строки 89–90 инициализируют kids. Строка 92 инициализирует emptyset. Строки 94–97 настраивают и устанавливают обработчик сигнала для SIGCHLD. Обратите внимание на использование в строке 94 SA_NOCLDSTOP, тогда как строка 96 блокирует все сигналы при вызове обработчика.

Строки 99–100 создают набор сигналов, представляющих SIGCHLD, а строка 102 устанавливает их в качестве маски сигналов процесса для программы.

Строки 104–109 создают пять порожденных процессов, каждый из которых засыпает на три секунды. По ходу дела они обновляют массив kids и переменную nkids.

Строка 111 дает затем потомкам шанс завершиться, заснув на еще больший промежуток времени. (Это не гарантирует, что порожденные процессы завершатся, но шансы довольно велики.)

Наконец, строки 113–114 выводят сообщение и приостанавливаются, заменив маску сигналов процесса, блокирующую SIGCHLD, пустой маской. Это дает возможность появиться сигналу SIGCHLD, что в свою очередь вызывает запуск обработчика сигнала. Вот что происходит:

$ ch10-reap1 /* Запуск программы */
waiting for signal
Entered childhandler
  reaped process 23937
  reaped process 23938
  reaped process 23939
  reaped process 23940
  reaped process 23941
Exited childhandler

Обработчик сигнала собирает сведения о потомках за один проход.

Следующая программа, ch10-reap2.c, сходна с ch10-reap1.c. Разница в том, что она допускает появление сигнала SIGCHLD в любое время. Такое поведение увеличивает шанс получения более одного SIGCHLD, но не гарантирует это. В результате обработчик сигнала все равно должен быть готов обработать в цикле несколько потомков.

1  /* ch10-reap2.c — демонстрирует управление SIGCHLD, один сигнал на потомка */
2
   /* ...не изменившийся код пропущен... */
12
13 pid_t kids[MAX_KIDS];
14 size_t nkids = 0;
15 size_t kidsleft = 0; /* <<< Добавлено */
16
 /* ...не изменившийся код пропущен... */
41
42 /* childhandler --- перехват SIGCHLD, опрос всех доступных потомков */
43
44 void childhandler(int sig)
45 {
46  int status, ret;
47  int i;
48  char buf[100];
49  static const char entered[] = "Entered childhandlern";
50  static const char exited[] = "Exited childhandlern";
51
52  write(1, entered, strlen(entered));
53  for (i = 0; i < nkids; i++) {
54   if (kids[i] == NOT_USED)
55    continue;
56
57 retry:
58  if ((ret = waitpid(kids[i], &status, WNOHANG)) == kids[i]) {
59   strcpy(buf, "treaped process ");
60   strcat(buf, format_num(ret));
61   strcat(buf, "n");
62   write(1, buf, strlen(buf));
63   kids[i] = NOT_USED;
64   kidsleft--; /* <<< Добавлено */
65  } else if (ret == 0) {
    /* ...не изменившийся код пропущен... */
80  write(1, exited, strlen(exited));
81 }

Это идентично предыдущей версии за тем исключением, что у нас есть новая переменная, kidsleft, указывающая, сколько имеется не опрошенных потомков. Строки 15 и 64 помечают новый код.

83  /* main --- установка относящейся к порожденным процессам сведений
       и сигналов, создание порожденных процессов */
84
85  int main(int argc, char **argv)
86  {
     /* ...не изменившийся код пропущен... */
100
101  sigemptyset(&childset);
102  sigaddset(&childset, SIGCHLD);
103
104  /* sigprocmask(SIG_SETMASK, &childset, NULL); /* блокирование в коде main */
105
106  for (nkids = 0; nkids < 5; nkids++) {
107   if ((kids[nkids] = fork()) == 0) {
108    sleep(3);
109    _exit(0);
110   }
111   kidsleft++; /* <<< Added */
112  }
113
114  /* sleep(5); /* дать потомкам шанс завершиться */
115
116  while (kidsleft > 0) { /* <<< Добавлено */
117   printf("waiting for signalsn");
118   sigsuspend(&emptyset);
119  } /* <<< Добавлено */
120
121  return 0;
122 }

Здесь код также почти идентичен. Строки 104 и 114 закомментированы из предыдущей версии, а строки 111, 116 и 119 добавлены. Удивительно, при запуске поведение меняется в зависимости от версии ядра!

$ uname -a /* Отобразить версию системы */
Linux example1 2.4.20-8 #1 Thu Mar 13 17:54:28 EST 2003 i686 i686 i386 GNU/Linux
$ ch10-reap2 /* Запустить программу */
waiting for signals
Entered childhandler /* Опрос одного потомка */
  reaped process 2702
  pid 2703 not available yet
  pid 2704 not available yet
  pid 2705 not available yet
  pid 27 06 not available yet
Exited childhandler
waiting for signals
Entered childhandler /* И следующего */
  reaped process 2703
  pid 2704 not available yet
  pid 2705 not available yet
  pid 2706 not available yet
Exited childhandler
waiting for signals
Entered childhandler /* И так далее */
  reaped process 2704
  pid 2705 not available yet
  pid 2706 not available yet
Exited childhandler
waiting for signals
Entered childhandler
  reaped process 2705
  pid 2706 not available yet
Exited childhandler
waiting for signals
Entered childhandler
  reaped process 2706
Exited childhandler

В данном примере на каждый процесс поступает ровно один SIGCHLD! Хотя это прекрасно и полностью воспроизводимо на этой системе, это также необычно. Как на более раннем, так и на более позднем ядре и на Solaris программа получает один сигнал для более чем одного потомка:

$ uname -a /* Отобразить версию системы */
Linux example2 2.4.22-1.2115.npt1 #1 Wed Oct 29 15:42:51 EST 2003 i686 i686 i386 GNU/Linux
$ ch10-reap2 /* Запуск программы */
waiting for signals
Entered childhandler /* Обработчик сигнала вызван лишь однажды */
  reaped process 9564
  reaped process 9565
  reaped process 9566
  reaped process 9567
  reaped process 9568
Exited childhandler

ЗАМЕЧАНИЕ. В коде для ch10-reap2.c есть один важный дефект — состояние гонки. Взгляните еще раз на строки 106–112 в ch10-reap2.c. Что случится, если SIGCHLD появится при исполнении этого кода? Массив kids и переменные nkids и kidsleft могут оказаться разрушенными: код в main добавляет новый процесс, но обработчик сигнала вычитает один.

Этот пример кода является отличным примером критического раздела; он не должен прерываться при исполнении. Правильным способом работы с этим кодом является заключение его между вызовами, которые сначала блокируют, а затем разблокируют SIGCHLD.

Оглавление книги


Генерация: 1.305. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз