7.4 ОЖИДАНИЕ ЗАВЕРШЕНИЯ ВЫПОЛНЕНИЯ ПРОЦЕССА

Процесс может синхронизировать продолжение своего выполнения с моментом завершения потомка, если воспользуется системной функцией wait. Синтаксис вызова функции:

   pid = wait(stat_addr);

где pid - значение кода идентификации (PID) прекратившего свое существование потомка, stat_addr - адрес переменной целого типа, в которую будет помещено возвращаемое функцией exit значение, в пространстве задачи.

     main()
     {
         int child;

         if ((child = fork()) == 0)
         {
             printf("PID потомка %d\n",getpid());
             pause();     /* приостанов выполнения до получения
                             сигнала */
         }
         /* родитель */
         printf("PID потомка %d\n",child);
         exit(child);
     }

Рисунок 7.15. Пример использования функции exit

Алгоритм функции wait приведен на Рисунке 7.16. Ядро ведет поиск потомков процесса, прекративших существование, и в случае их отсутствия возвращает ошибку. Если потомок, прекративший существование, обнаружен, ядро передает его код идентификации и значение, возвращаемое через параметр функции exit, процессу, вызвавшему функцию wait. Таким образом, через параметр функции exit (status) завершающийся процесс может передавать различные значения, в закодированном виде содержащие информацию о причине завершения процесса, однако на практике этот параметр используется по назначению довольно редко. Ядро передает в соответствующие поля, принадлежащие пространству родительского процесса, накопленные значения продолжительности исполнения процесса-потомка в режиме ядра и в режиме задачи и, наконец, освобождает в таблице процессов место, которое в ней занимал прежде прекративший существование процесс. Это место будет предоставлено новому процессу.

Если процесс, выполняющий функцию wait, имеет потомков, продолжающих существование, он приостанавливается до получения ожидаемого сигнала. Ядро не возобновляет по своей инициативе процесс, приостановившийся с помощью функции wait: такой процесс может возобновиться только в случае получения сигнала. На все сигналы, кроме сигнала "гибель потомка", процесс реагирует ранее рассмотренным образом. Реакция процесса на сигнал "гибель потомка" проявляется по-разному в зависимости от обстоятельств:

     алгоритм wait
     входная информация:  адрес переменной для хранения значения
                          status, возвращаемого завершающимся
                          процессом
     выходная информация: идентификатор потомка и код возврата
                          функции exit
     {
        если (процесс, вызвавший функцию wait, не имеет потом-
         ков)
             возвратить (ошибку);

        для (;;)     /* цикл с внутренним циклом */
        {
             если (процесс, вызвавший функцию wait, имеет потом-
              ков, прекративших существование)
             {
                 выбрать произвольного потомка;
                 передать его родителю информацию об использова-
                  нии потомком ресурсов центрального процессора;
                 освободить в таблице процессов место, занимае-
                  мое потомком;
                 возвратить (идентификатор потомка, код возврата
                  функции exit, вызванной потомком);
             }
             если (у процесса нет потомков)
                 возвратить ошибку;
             приостановиться с приоритетом, допускающим прерыва-
              ния (до завершения потомка);
        }
     }

Рисунок 7.16. Алгоритм функции wait

Например, если пользователь запускает программу, приведенную на Рисунке 7.17, с параметром и без параметра, он получит разные результаты. Сначала рассмотрим случай, когда пользователь запускает программу без параметра (единственный параметр - имя программы, то есть argc равно 1). Родительский процесс порождает 15 потомков, которые в конечном итоге завершают свое выполнение с кодом возврата i, номером процесса в порядке очередности создания. Ядро, исполняя функцию wait для родителя, находит потомка, прекратившего существование, и передает родителю его идентификатор и код возврата функции exit. При этом заранее не известно, какой из потомков будет обнаружен. Из текста программы, реализующей системную функцию exit, написанной на языке Си и включенной в библиотеку стандартных подпрограмм, видно, что программа запоминает код возврата функции exit в битах 8-15 поля ret_code и возвращает функции wait идентификатор процесса-потомка. Таким образом, в ret_code хранится значение, равное 256*i, где i - номер потомка, а в ret_val заносится значение идентификатора потомка.

Если пользователь запускает программу с параметром (то есть argc > 1), родительский процесс с помощью функции signal делает распоряжение игнорировать сигналы типа "гибель потомка". Предположим, что родительский процесс, выполняя функцию wait, приостановился еще до того, как его потомок произвел обращение к функции exit: когда процесс-потомок переходит к выполнению функции exit, он посылает своему родителю сигнал "гибель потомка"; родительский процесс возобновляется, поскольку он был приостановлен с приоритетом, допускающим прерывания. Когда так или иначе родительский процесс продолжит свое выполнение, он обнаружит, что сигнал сообщал о "гибели" потомка; однако, поскольку он игнорирует сигналы этого типа и не обрабатывает их, ядро удаляет из таблицы процессов запись, соответствующую прекратившему существование потомку, и продолжает выполнение функции wait так, словно сигнала и не было. Ядро выполняет эти действия всякий раз, когда родительский процесс получает сигнал типа "гибель потомка", до тех пор, пока цикл выполнения функции wait не будет завершен и пока не будет установлено, что у процесса больше потомков нет. Тогда функция wait возвращает значение, равное -1. Разница между двумя способами запуска программы состоит в том, что в первом случае процесс-родитель ждет завершения любого из потомков, в то время как во втором случае он ждет, пока завершатся все его потомки.

     #include <signal.h>
     main(argc,argv)
       int argc;
       char *argv[];
     {
       int i,ret_val,ret_code;

       if (argc >= 1)
           signal(SIGCLD,SIG_IGN);   /* игнорировать гибель
                                        потомков */
       for (i = 0; i < 15; i++)
           if (fork() == 0)
           {
               /* процесс-потомок */
               printf("процесс-потомок %x\n",getpid());
               exit(i);
           }
       ret_val = wait(&ret_code);
       printf("wait ret_val %x ret_code %x\n",ret_val,ret_code);
     }

Рисунок 7.17. Пример использования функции wait и игнорирования сигнала "гибель потомка"

В ранних версиях системы UNIX функции exit и wait не использовали и не рассматривали сигнал типа "гибель потомка". Вместо посылки сигнала функция exit возобновляла выполнение родительского процесса. Если родительский процесс при выполнении функции wait приостановился, он возобновляется, находит потомка, прекратившего существование, и возвращает управление. В противном случае возобновления не происходит; процесс-родитель обнаружит "погибшего" потомка при следующем обращении к функции wait. Точно так же и процесс начальной загрузки (init) может приостановиться, используя функцию wait, и завершающиеся по exit процессы будут возобновлять его, если он имеет усыновленных потомков, прекращающих существование.

В такой реализации функций exit и wait имеется одна нерешенная проблема, связанная с тем, что процессы, прекратившие существование, нельзя убирать из системы до тех пор, пока их родитель не исполнит функцию wait. Если процесс создал множество потомков, но так и не исполнил функцию wait, может произойти переполнение таблицы процессов из-за наличия потомков, прекративших существование с помощью функции exit. В качестве примера рассмотрим текст программы планировщика процессов, приведенный на Рисунке 7.18. Процесс производит считывание данных из файла стандартного ввода до тех пор, пока не будет обнаружен конец файла, создавая при каждом исполнении функции read нового потомка. Однако, процесс-родитель не дожидается завершения каждого потомка, поскольку он стремится запускать процессы на выполнение как можно быстрее, тем более, что может пройти довольно много времени, прежде чем процесс-потомок завершит свое выполнение. Если, обратившись к функции signal, процесс распорядился игнорировать сигналы типа "гибель потомка", ядро будет очищать записи, соответствующие прекратившим существование процессам, автоматически. Иначе в конечном итоге из-за таких процессов может произойти переполнение таблицы.

     #include <signal.h>
     main(argc,argv)
     {
          char buf[256];

          if (argc != 1)
               signal(SIGCLD,SIG_IGN);   /* игнорировать гибель
                                            потомков */
          while (read(0,buf,256))
               if (fork() == 0)
               {
                    /* здесь процесс-потомок обычно выполняет
                       какие-то операции над буфером (buf) */
                    exit(0);
               }
     }

Рисунок 7.18. Пример указания причины появления сигнала "гибель потомков"

Предыдущая глава || Оглавление || Следующая глава