итм загрузки системы 221 ра. Порожденный нулевым новый процесс, процесс 1, запускается в том же режи- ме и создает свой пользовательский контекст, формируя область данных и при- соединяя ее к своему адресному пространству. Он увеличивает размер области до надлежащей величины и переписывает программу загрузки из адресного прост- ранства ядра в новую область: эта программа теперь будет определять контекст процесса 1. Затем процесс 1 сохраняет регистровый контекст задачи, "возвра- щается" из режима ядра в режим задачи и исполняет только что переписанную программу. В отличие от нулевого процесса, который является процессом сис- темного уровня, выполняющимся в режиме ядра, процесс 1 относится к пользова- тельскому уровню. Код, исполняемый процессом 1, включает в себя вызов сис- темной функции exec, запускающей на выполнение программу из файла "/etc/init". Обычно процесс 1 именуется процессом init, поскольку он отвеча- ет за инициализацию новых процессов. Казалось бы, зачем ядру копировать программу, запускаемую с помощью фун- кции exec, в адресное пространство процесса 1 ? Он мог бы обратиться к внут- реннему варианту функции прямо из ядра, одна- ко, по сравнению с уже описанным алгоритмом это было бы гораздо труднее реа- лизовать, ибо в этом случае функции exec пришлось бы производить анализ имен файлов в пространстве ядра, а не в пространстве задачи. Подобная деталь, требующаяся только для процесса init, усложнила бы программу реализации фун- кции exec и отрицательно отразилась бы на скорости выполнения функции в бо- лее общих случаях. Процесс init (Рисунок 7.31) выступает диспетчером процессов, который по- рождает процессы, среди всего прочего позволяющие пользователю регистриро- ваться в системе. Инструкции о том, какие процессы нужно создать, считывают- ся процессом init из файла "/etc/inittab". Строки файла включают в себя идентификатор состояния "id" (однопользовательский режим, многопользователь- ский и т. д.), предпринимаемое действие (см. упражнение 7.43) и спецификацию программы, реализующей это действие (см. Рисунок 7.32). Процесс init прос- матривает строки файла до тех пор, пока не обнаружит идентификатор состоя- ния, соответствующего тому состоянию, в котором находится процесс, и создает процесс, исполняющий программу с указанной спецификацией. Например, при за- пуске в многопользовательском режиме (состояние 2) процесс init обычно по- рождает getty-процессы, управляющие функционированием терминальных линий, входящих в состав системы. Если регистрация пользователя прошла успешно, getty-процесс, пройдя через процедуру login, запускает на исполнение регист- рационный shell (см. главу 10). Тем временем процесс init находится в состо- янии ожидания (wait), наблюдая за прекращением существования своих потомков, а также "внучатых" процессов, оставшихся "сиротами" после гибели своих роди- телей. Процессы в системе UNIX могут быть либо пользовательскими, либо управля- ющими, либо системными. Большинство из них составляют пользовательские про- цессы, связанные с пользователями через терминалы. Управляющие процессы не связаны с конкретными пользователями, они выполняют широкий спектр системных функций, таких как администрирование и управление сетями, различные периоди- ческие операции, буферизация данных для вывода на устройство построчной пе- чати и т.д. Процесс init может порождать управляющие процессы, которые будут существовать на протяжении всего времени жизни системы, в различных случаях они могут быть созданы самими пользователями. Они похожи на пользовательские процессы тем, что они исполняются в режиме задачи и прибегают к услугам сис- темы путем вызова соответствующих системных функций. Системные процессы выполняются исключительно в режиме ядра. Они могут порождаться нулевым процессом (например, процесс замещения страниц vhand), который затем становится процессом подкачки. Системные процессы похожи на управляющие процессы тем, что они выполняют системные функции, при этом они обладают большими возможностями приоритетного выполнения, поскольку лежащие в их основе программные коды являются составной частью ядра. Они могут обра- щаться к структурам данных и алгоритмам ядра, не прибегая к вызову системных функций, отсюда вытекает их исключительность. Однако, они не обладают такой 222 +------------------------------------------------------------+ | алгоритм init /* процесс init, в системе именуемый | | "процесс 1" */ | | входная информация: отсутствует | | выходная информация: отсутствует | | { | | fd = open("/etc/inittab",O_RDONLY); | | while (line_read(fd,buffer)) | | { | | /* читать каждую строку файлу */ | | if (invoked state != buffer state) | | continue; /* остаться в цикле while */ | | /* найден идентификатор соответствующего состояния | | */ | | if (fork() == 0) | | { | | execl("процесс указан в буфере"); | | exit(); | | } | | /* процесс init не дожидается завершения потомка */ | | /* возврат в цикл while */ | | } | | | | while ((id = wait((int*) 0)) != -1) | | { | | /* проверка существования потомка; | | * если потомок прекратил существование, рассматри- | | * вается возможность его перезапуска */ | | /* в противном случае, основной процесс просто про- | | * должает работу */ | | } | | } | +------------------------------------------------------------+ Рисунок 7.31. Алгоритм выполнения процесса init +------------------------------------------------------------+ | Формат: идентификатор, состояние, действие, спецификация | | процесса | | Поля разделены между собой двоеточиями | | Комментарии в конце строки начинаются с символа '#' | | | | co::respawn:/etc/getty console console #Консоль в машзале| | 46:2:respawn:/etc/getty -t 60 tty46 4800H #комментарии | +------------------------------------------------------------+ Рисунок 7.32. Фрагмент файла inittab же гибкостью, как управляющие процессы, поскольку для того, чтобы внести из- менения в их программы, придется еще раз перекомпилировать ядро. 7.10 ВЫВОДЫ В данной главе были рассмотрены системные функции, предназначенные для работы с контекстом процесса и для управления выполнением процесса. Систем- ная функция fork создает новый процесс, копируя для него содержимое всех об- ластей, подключенных к родительскому процессу. Особенность реализации функ- ции fork состоит в том, что она выполняет инициализацию сохраненного регист- 223 рового контекста порожденного процесса, таким образом этот процесс начинает выполняться, не дожидаясь завершения функции, и уже в теле функции начинает осознавать свою предназначение как потомка. Все процессы завершают свое вы- полнение вызовом функции exit, которая отсоединяет области процесса и посы- лает его родителю сигнал "гибель потомка". Процесс-родитель может совместить момент продолжения своего выполнения с моментом завершения процесса-потомка, используя системную функцию wait. Системная функция exec дает процессу воз- можность запускать на выполнение другие программы, накладывая содержимое ис- полняемого файла на свое адресное пространство. Ядро отсоединяет области, ранее занимаемые процессом, и назначает процессу новые области в соответст- вии с потребностями исполняемого файла. Совместное использование областей команд и наличие режима "sticky-bit" дают возможность более рационально ис- пользовать память и экономить время, затрачиваемое на подготовку к запуску программ. Простым пользователям предоставляется возможность получать приви- легии других пользователей, даже суперпользователя, благодаря обращению к услугам системной функции setuid и setuid-программ. С помощью функции brk процесс может изменять размер своей области данных. Функция signal дает про- цессам возможность управлять своей реакцией на поступающие сигналы. При по- лучении сигнала производится обращение к специальной функции обработки сиг- нала с внесением соответствующих изменений в стек задачи и в сохраненный ре- гистровый контекст задачи. Процессы могут сами посылать сигналы, используя системную функцию kill, они могут также контролировать получение сигналов, предназначенных группе процессов, прибегая к услугам функции setpgrp. Командный процессор shell и процесс начальной загрузки init используют стандартные обращения к системным функциям, производя набор операций, в дру- гих системах обычно выполняемых ядром. Shell интерпретирует команды пользо- вателя, переназначает стандартные файлы ввода-вывода данных и выдачи ошибок, порождает процессы, организует каналы между порожденными процессами, синхро- низирует свое выполнение с этими процессами и формирует коды, возвращаемые командами. Процесс init тоже порождает различные процессы, в частности, уп- равляющие работой пользователя за терминалом. Когда такой процесс завершает- ся, init может породить для выполнения той же самой функции еще один про- цесс, если это вытекает из информации файла "/etc/inittab". 7.11 УПРАЖНЕНИЯ 1. Запустите с терминала программу, приведенную на Рисунке 7.33. Переадре- суйте стандартный вывод данных в файл и сравните результаты между со- бой. +------------------------------------+ | main() | | { | | printf("hello\n"); | | if (fork() == 0) | | printf("world\n"); | | } | +------------------------------------+ Рисунок 7.33. Пример модуля, содержащего вызов функции fork и обра- щение к стандартному выводу 2. Разберитесь в механизме работы программы, приведенной на Рисунке 7.34, и сравните ее результаты с результатами программы на Рисунке 7.4. 3. Еще раз обратимся к программе, приведенной на Рисунке 7.5 и показываю- щей, как два процесса обмениваются сообщениями, используя спаренные ка- налы. Что произойдет, если они попытаются вести обмен сообщениями, ис- пользуя один канал ? 4. Возможна ли потеря информации в случае, когда процесс получает несколь- 224 ко сигналов прежде чем ему предоставляется возможность отреагировать на них надлежащим образом ? (Рассмотрите случай, когда процесс подсчитыва- ет количество полученных сигналов о прерывании.) Есть ли необходимость в решении этой проблемы ? 5. Опишите механизм работы системной функции kill. 6. Процесс в программе на Рисунке 7.35 принимает сигналы типа "гибель по- томка" и устанавливает функцию обработки сигналов в исходное состояние. Что происходит при выполнении программы ? 7. Когда процесс получает сигналы определенного типа и не обрабатывает их, ядро дампирует образ процесса в том виде, который был у него в момент получения сигнала. Ядро создает в текущем каталоге процесса файл с име- нем "core" и копирует в него пространство процесса, области команд, данных и стека. Впоследствии пользователь может тщательно изучить дамп образа процесса с помощью стандартных средств отладки. Опишите алго- ритм, которому на Ваш взгляд должно следовать ядро в процессе создания файла "core". Что нужно предпринять в том случае, если в текущем ката- логе файл с таким именем уже существует ? Как должно вести себя ядро, когда в одном и том же каталоге дампируют свои образы сразу несколько процессов? 8. Еще раз обратимся к программе (Рисунок 7.12), описывающей, как один процесс забрасывает другой процесс сигналами, которые принимаются их адресатом. Подумайте, что произошло бы в том случае, если бы алгоритм обработки сигналов был переработан в любом из следующих направлений: +------------------------------------------------------------+ | #include | | int fdrd,fdwt; | | char c; | | | | main(argc,argv) | | int argc; | | char *argv[]; | | { | | if (argc != 3) | | exit(1); | | fork(); | | | | if ((fdrd = open(argv[1],O_RDONLY)) == -1) | | exit(1); | | if (((fdwt = creat(argv[2],0666)) == -1) && | | ((fdwt = open(argv[2],O_WRONLY)) == -1)) | | exit(1); | | rdwrt(); | | } | | rdwrt() | | { | | for (;;) | | { | | if (read(fdrd,&c,1) != 1) | | return; | | write(fdwt,&c,1); | | } | | } | +------------------------------------------------------------+ Рисунок 7.34. Пример программы, в которой процесс-родитель и процесс-потомок не разделяют доступ к файлу * ядро не заменяет функцию обработки сигналов до тех пор, пока пользо- ватель явно не потребует этого; 225 * ядро заставляет процесс игнорировать сигналы до тех пор, пока пользо- ватель не обратится к функции signal вновь. 9. Переработайте алгоритм обработки сигналов так, чтобы ядро автоматически перенастраивало процесс на игнорирование всех последующих поступлений сигналов по возвращении из функции, обрабатывающей их. Каким образом ядро может узнать о завершении функции обработки сигналов, выполняющей- ся в режиме задачи ? Такого рода перенастройка приблизила бы нас к трактовке сигналов в системе BSD. *10. Если процесс получает сигнал, находясь в состоянии приостанова во время выполнения системной функции с допускающим прерывания приоритетом, он выходит из функции по алгоритму longjump. Ядро производит необходимые установки для запуска функции обработки сигнала; когда процесс выйдет из функции обработки сигнала, в версии V это будет выглядеть так, слов- но он вернулся из системной функции с признаком ошибки (как бы прервав свое выполнение). В системе BSD системная функция в этом случае автома- тически перезапускается. Каким образом можно реализовать этот момент в нашей системе? +------------------------------------------------------------+ | #include | | main() | | { | | extern catcher(); | | | | signal(SIGCLD,catcher); | | if (fork() == 0) | | exit(); | | /* пауза до момента получения сигнала */ | | pause(); | | } | | | | catcher() | | { | | printf("процесс-родитель получил сигнал\n"); | | signal(SIGCLD,catcher); | | } | +------------------------------------------------------------+ Рисунок 7.35. Программа, в которой процесс принимает сигналы типа "гибель потомка" 11. В традиционной реализации команды mkdir для создания новой вершины в дереве каталогов используется системная функция mknod, после чего дваж- ды вызывается системная функция link, привязывающая точки входа в ката- лог с именами "." и ".." к новой вершине и к ее родительскому каталогу. Без этих трех операций каталог не будет иметь надлежащий формат. Что произойдет, если во время исполнения команды mkdir процесс получит сиг- нал ? Что если при этом будет получен сигнал SIGKILL, который процесс не распознает ? Эту же проблему рассмотрите применительно к реализации системной функции mkdir. 12. Процесс проверяет наличие сигналов в моменты перехода в состояние при- останова и выхода из него (если в состоянии приостанова процесс нахо- дился с приоритетом, допускающим прерывания), а также в момент перехода в режим задачи из режима ядра по завершении исполнения системной функ- ции или после обработки прерывания. Почему процесс не проверяет наличие сигналов в момент обращения к системной функции ? *13. Предположим, что после исполнения системной функции процесс готовится к возвращению в режим задачи и не обнаруживает ни одного необработанного сигнала. Сразу после этого ядро обрабатывает прерывание и посылает про- 226 цессу сигнал. (Например, пользователем была нажата клавиша "break".) Что делает процесс после того, как ядро завершает обработку прерывания? *14. Если процессу одновременно посылается несколько сигналов, ядро обраба- тывает их в том порядке, в каком они перечислены в описании. Существуют три способа реагирования на получение сигнала - прием сигналов, завер- шение выполнения со сбросом на внешний носитель (дампированием) образа процесса в памяти и завершение выполнения без дампирования. Можно ли указать наилучший порядок обработки одновременно поступающих сигналов ? Например, если процесс получает сигнал о выходе (вызывающий дампирова- ние образа процесса в памяти) и сигнал о прерывании (выход без дампиро- вания), то какой из этих сигналов имело бы смысл обработать первым ? 15. Запомните новую системную функцию newpgrp(pid,ngrp); которая включает процесс с идентификатором pid в группу процессов с но- мером ngrp (устанавливает для процесса новую группу). Подумайте, для каких целей она может использоваться и какие опасности таит в себе ее вызов. 16. Прокомментируйте следующее утверждение: по алгоритму wait процесс может приостановиться до наступления какого-либо события и это не отразилось бы на работе всей системы. 17. Рассмотрим новую системную функцию nowait(pid); где pid - идентификатор процесса, являющегося потомком того процесса, который вызывает функцию. Вызывая функцию, процесс тем самым сообщает ядру о том, что он не собирается дожидаться завершения выполнения свое- го потомка, поэтому ядро может по окончании существования потомка сразу же очистить занимаемое им место в таблице процессов. Каким образом это реализуется на практике ? Оцените достоинства новой функции и сравните ее использование с использованием сигналов типа "гибель потомка". 18. Загрузчик модулей на Си автоматически подключает к основному модулю на- чальную процедуру (startup), которая вызывает функцию main, принадлежа- щую программе пользователя. Если в пользовательской программе отсутст- вует вызов функции exit, процедура startup сама вызывает эту функцию при выходе из функции main. Что произошло бы в том случае, если бы и в процедуре startup отсутствовал вызов функции exit (из-за ошибки загруз- чика) ? 19. Какую информацию получит процесс, выполняющий функцию wait, если его потомок запустит функцию exit без параметра ? Имеется в виду, что про- цесс-потомок вызовет функцию в формате exit() вместо exit(n). Если программист постоянно использует вызов функции exit без параметра, то насколько предсказуемо значение, ожидаемое функцией wait ? Докажите свой ответ. 20. Объясните, что произойдет, если процесс, исполняющий программу на Ри- сунке 7.36 запустит с помощью функции exec самого себя. Как в таком случае ядро сможет избежать возникновения тупиковых ситуаций, связанных с блокировкой индексов ? +----------------------------------+ | main(argc,argv) | | int argc; | | char *argv[]; | | { | | execl(argv[0],argv[0],0); | | } | +----------------------------------+ Рисунок 7.36 21. По условию первым аргументом функции exec является имя (последняя ком- понента имени пути поиска) исполняемого процессом файла. Что произойдет в результате выполнения программы, приведенной на Рисунке 7.37 ? Каков будет эффект, если в качестве файла "a.out" выступит загрузочный мо- 227 дуль, полученный в результате трансляции программы, приведенной на Ри- сунке 7.36 ? 22. Предположим, что в языке Си поддерживается новый тип данных "read-only" (только для чтения), причем процесс, пытающийся записать информацию в поле с этим типом, получает отказ системы защиты. Опишите реализацию этого момента. (Намек: сравните это понятие с понятием "разделяемая об- ласть команд".) В какие из алгоритмов ядра потребуется внести изменения ? Какие еще объекты могут быть реализованы аналогичным с областью обра- зом ? 23. Какие изменения имеют место в алгоритмах open, chmod, unlink и unmount при работе с файлами, для которых установлен режим "sticky-bit" ? Какие действия, например, следует предпринять в отношении такого файла ядру, когда с файлом разрывается связь ? 24. Суперпользователь является единственным пользователем, имеющим право на запись в файл паролей "/etc/passwd", благодаря чему содержимое файла предохраняется от умышленной или случайной порчи. Программа passwd дает пользователям возможность изменять свой собственный пароль, защищая от изменений чужие записи. Каким образом она работает ? +-----------------------------------------------------+ | main() | | { | | if (fork() == 0) | | { | | execl("a.out",0); | | printf("неудачное завершение функции exec\n");| | } | | } | +-----------------------------------------------------+ Рисунок 7.37 *25. Поясните, какая угроза безопасности хранения данных возникает, если setuid-программа не защищена от записи. 26. Выполните следующую последовательность команд, в которой "a. out" - имя исполняемого файла: +-----------------------------------------------------+ | main() | | { | | char *endpt; | | char *sbrk(); | | int brk(); | | | | endpt = sbrk(0); | | printf("endpt = %ud после sbrk\n", (int) endpt); | | | | while (endpt--) | | { | | if (brk(endpt) == -1) | | { | | printf("brk с параметром %ud завершилась | | неудачно\n",endpt); | | exit(); | | } | | } | | } | +-----------------------------------------------------+ Рисунок 7.38 228 chmod 4777 a.out chown root a.out Команда chmod "включает" бит setuid (4 в 4777); пользователь "root" традиционно является суперпользователем. Может ли в результате выполне- ния этой последовательности произойти нарушение защиты информации ? 27. Что произойдет в процессе выполнения программы, представленной на Ри- сунке 7.38 ? Поясните свой ответ. 28. Библиотечная подпрограмма malloc увеличивает область данных процесса с помощью функции brk, а подпрограмма free освобождает память, выделенную подпрограммой malloc. Синтаксис вызова подпрограмм: ptr = malloc(size); free(ptr); где size - целое число без знака, обозначающее количество выделяемых байт памяти, а ptr - символьная ссылка на вновь выделенное пространст- во. Прежде чем появиться в качестве параметра в вызове подпрограммы free, указатель ptr должен быть возвращен подпрограммой malloc. Выпол- ните эти подпрограммы. 29. Что произойдет в процессе выполнения программы, представленной на Ри- сунке 7.39 ? Сравните результаты выполнения этой программы с результа- тами, предусмотренными в системном описании. +-----------------------------------------------------+ | main() | | { | | int i; | | char *cp; | | extern char *sbrk(); | | | | cp = sbrk(10); | | for (i = 0; i < 10; i++) | | *cp++ = 'a' + i; | | sbrk(-10); | | cp = sbrk(10); | | for (i = 0; i < 10; i++) | | printf("char %d = '%c'\n",i,*cp++); | | } | +-----------------------------------------------------+ Рисунок 7.39. Пример программы, использующей подпрограмму sbrk 30. Каким образом командный процессор shell узнает о том, что файл исполня- емый, когда для выполнения команды создает новый процесс ? Если файл исполняемый, то как узнать, создан ли он в результате трансляции исход- ной программы или же представляет собой набор команд языка shell ? В каком порядке следует выполнять проверку указанных условий ? 31. В командном языке shell символы ">>" используются для направления выво- да данных в файл с указанной спецификацией, например, команда: run >> outfile открывает файл с именем "outfile" (а в случае отсутствия файла с таким именем создает его) и записывает в него данные. Напишите прог- рамму, в которой используется эта команда. 32. Процессор командного языка shell проверяет код, возвращаемый функцией exit, воспринимая нулевое значение как "истину", а любое другое значе- ние как "ложь" (обратите внимание на несогласованность с языком Си). Предположим, что файл, исполняющий программу на Рисунке 7.40, имеет имя "truth". Поясните, что произойдет, когда shell будет исполнять следую- щий набор команд: while truth 229 +------------------+ | main() | | { | | exit(0); | | } | +------------------+ Рисунок 7.40 do truth & done 33. Вопрос по Рисунку 7.29: В связи с чем возникает необходимость в созда- нии процессов для конвейерной обработки двухкомпонентной команды в ука- занном порядке ? 34. Напишите более общую программу работы основного цикла процессора shell в части обработки каналов. Имеется в виду, что программа должна уметь обрабатывать случайное число каналов, указанных в командной строке. 35. Переменная среды PATH описывает порядок, в котором shell'у следует просматривать каталоги в поисках исполняемых файлов. В библиотечных функциях execlp и execvp перечисленные в PATH каталоги присоединяются к именам файлов, кроме тех, которые начинаются с символа "/". Выполните эти функции. *36. Для того, чтобы shell в поисках исполняемых файлов не обращался к теку- щему каталогу, суперпользователь должен задать переменную среды PATH. Какая угроза безопасности хранения данных может возникнуть, если shell попытается исполнить файлы из текущего каталога ? 37. Каким образом shell обрабатывает команду cd (создать каталог) ? Какие действия предпринимает shell в процессе обработки следующей командной строки: cd pathname & ? 38. Когда пользователь нажимает на клавиатуре терминала клавиши "delete" или "break", всем процессам, входящим в группу регистрационного shell'а, терминальный драйвер посылает сигнал о прерывании. Пользова- тель может иметь намерение остановить все процессы, порожденные shell'ом, без выхода из системы. Какие усовершенствования в связи с этим следует произвести в теле основного цикла программы shell (Рисунок 7.28) ? 39. С помощью команды nohup command_line пользователь может отменить действие сигналов о "зависании" и о завер- шении (quit) в отношении процессов, реализующих командную строку (command_line). Как эта команда будет обрабатываться в основном цикле программы shell ? 40. Рассмотрим набор команд языка shell: nroff -mm bigfile1 > big1out & nroff -mm bigfile2 > big2out и вновь обратимся к основному циклу программы shell (Рисунок 7.28). Что произойдет, если выполнение первой команды nroff завершится раньше вто- рой ? Какие изменения следует внести в основной цикл программы shell на этот случай ? 41. Часто во время выполнения из shell'а непротестированных программ появ- ляется сообщение об ошибке следующего вида: "Bus error - core dumped" (Ошибка в магистрали - содержимое памяти сброшено на внешний носитель). Очевидно, что в программе выполняются какие-то недопустимые действия; откуда shell узнает о том, что ему нужно вывести сообщение об ошибке ? 42. Процессом 1 в системе может выступать только процесс init. Тем не ме- нее, запустив процесс init, администратор системы может тем самым изме- нить состояние системы. Например, при загрузке система может войти в однопользовательский режим, означающий, что в системе активен только консольный терминал. Для того, чтобы перевести процесс init в состояние 230 2 (многопользовательский режим), администратор системы вводит с консоли команду init 2 . Консольный shell порождает свое ответвление и запускает init. Что имело бы место в системе в том случае, если бы активен был только один про- цесс init ? 43. Формат записей в файле "/etc/inittab" допускает задание действия, свя- занного с каждым порождаемым процессом. Например, с getty-процессом связано действие "respawn" (возрождение), означающее, что процесс init должен возрождать getty-процесс, если последний прекращает существова- ние. На практике, когда пользователь выходит из системы процесс init порождает новый getty-процесс, чтобы другой пользователь мог получить доступ к временно бездействующей терминальной линии. Каким образом это делает процесс init ? 44. Некоторые из алгоритмов ядра прибегают к просмотру таблицы процессов. Время поиска данных можно сократить, если использовать указатели на: родителя процесса, любого из потомков, другой процесс, имеющий того же родителя. Процесс обнаруживает всех своих потомков, следуя сначала за указателем на любого из потомков, а затем используя указатели на другие процессы, имеющие того же родителя (циклы недопустимы). Какие из алго- ритмов выиграют от этого ? Какие из алгоритмов нужно оставить без изме- 231