#include ‹fcntl.h›
    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. Пример программы, в которой процесс-родитель и процесс-потомок не разделяют доступ к файлу
 
   3. Еще раз обратимся к программе, приведенной на Рисунке 7.5 и показывающей, как два процесса обмениваются сообщениями, используя спаренные каналы. Что произойдет, если они попытаются вести обмен сообщениями, используя один канал?
   4. Возможна ли потеря информации в случае, когда процесс получает несколько сигналов прежде чем ему предоставляется возможность отреагировать на них надлежащим образом? (Рассмотрите случай, когда процесс подсчитывает количество полученных сигналов о прерывании.) Есть ли необходимость в решении этой проблемы?
   5. Опишите механизм работы системной функции kill.
   6. Процесс в программе на Рисунке 7.35 принимает сигналы типа „гибель потомка“ и устанавливает функцию обработки сигналов в исходное состояние. Что происходит при выполнении программы?
 
    #include ‹signal.h›
    main() {
     extern catcher();
     signal(SIGCLD, catcher);
     if (fork() == 0) exit();
     /* пауза до момента получения сигнала */
     pause();
    }
 
    catcher() {
     printf("процесс-родитель получил сигнал\n");
     signal(SIGCLD, catcher);
    }
    Рисунок 7.35. Программа, в которой процесс принимает сигналы типа „гибель потомка“
 
   7. Когда процесс получает сигналы определенного типа и не обрабатывает их, ядро дампирует образ процесса в том виде, который был у него в момент получения сигнала. Ядро создает в текущем каталоге процесса файл с именем „core“ и копирует в него пространство процесса, области команд, данных и стека. Впоследствии пользователь может тщательно изучить дамп образа процесса с помощью стандартных средств отладки. Опишите алгоритм, которому на Ваш взгляд должно следовать ядро в процессе создания файла „core“. Что нужно предпринять в том случае, если в текущем каталоге файл с таким именем уже существует? Как должно вести себя ядро, когда в одном и том же каталоге дампируют свои образы сразу несколько процессов?
   8. Еще раз обратимся к программе (Рисунок 7.12), описывающей, как один процесс забрасывает другой процесс сигналами, которые принимаются их адресатом. Подумайте, что произошло бы в том случае, если бы алгоритм обработки сигналов был переработан в любом из следующих направлений:
   • ядро не заменяет функцию обработки сигналов до тех пор, пока пользователь явно не потребует этого;
   • ядро заставляет процесс игнорировать сигналы до тех пор, пока пользователь не обратится к функции signal вновь.
   9. Переработайте алгоритм обработки сигналов так, чтобы ядро автоматически перенастраивало процесс на игнорирование всех последующих поступлений сигналов по возвращении из функции, обрабатывающей их. Каким образом ядро может узнать о завершении функции обработки сигналов, выполняющейся в режиме задачи? Такого рода перенастройка приблизила бы нас к трактовке сигналов в системе BSD.
   *10. Если процесс получает сигнал, находясь в состоянии приостанова во время выполнения системной функции с допускающим прерывания приоритетом, он выходит из функции по алгоритму longjump. Ядро производит необходимые установки для запуска функции обработки сигнала; когда процесс выйдет из функции обработки сигнала, в версии V это будет выглядеть так, словно он вернулся из системной функции с признаком ошибки (как бы прервав свое выполнение). В системе BSD системная функция в этом случае автоматически перезапускается. Каким образом можно реализовать этот момент в нашей системе?
   11. В традиционной реализации команды mkdir для создания новой вершины в дереве каталогов используется системная функция mknod, после чего дважды вызывается системная функция link, привязывающая точки входа в каталог с именами "." и ".." к новой вершине и к ее родительскому каталогу. Без этих трех операций каталог не будет иметь надлежащий формат. Что произойдет, если во время исполнения команды mkdir процесс получит сигнал? Что если при этом будет получен сигнал SIGKILL, который процесс не распознает? Эту же проблему рассмотрите применительно к реализации системной функции mkdir.
   12. Процесс проверяет наличие сигналов в моменты перехода в состояние приостанова и выхода из него (если в состоянии приостанова процесс находился с приоритетом, допускающим прерывания), а также в момент перехода в режим задачи из режима ядра по завершении исполнения системной функции или после обработки прерывания. Почему процесс не проверяет наличие сигналов в момент обращения к системной функции?
   *13. Предположим, что после исполнения системной функции процесс готовится к возвращению в режим задачи и не обнаруживает ни одного необработанного сигнала. Сразу после этого ядро обрабатывает прерывание и посылает процессу сигнал. (Например, пользователем была нажата клавиша "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" выступит загрузочный модуль, полученный в результате трансляции программы, приведенной на Рисунке 7.36?
 
    main() {
     if (fork() == 0) {
      execl("a.out", 0);
      printf("неудачное завершение функции exec\n");
     }
    }
    Рисунок 7.37
 
   22. Предположим, что в языке Си поддерживается новый тип данных "read-only" (только для чтения), причем процесс, пытающийся записать информацию в поле с этим типом, получает отказ системы защиты. Опишите реализацию этого момента. (Намек: сравните это понятие с понятием "разделяемая область команд".) В какие из алгоритмов ядра потребуется внести изменения? Какие еще объекты могут быть реализованы аналогичным с областью образом?
   23. Какие изменения имеют место в алгоритмах open, chmod, unlink и unmount при работе с файлами, для которых установлен режим "sticky-bit"? Какие действия, например, следует предпринять в отношении такого файла ядру, когда с файлом разрывается связь?
   24. Суперпользователь является единственным пользователем, имеющим право на запись в файл паролей "/etc/passwd", благодаря чему содержимое файла предохраняется от умышленной или случайной порчи. Программа passwd дает пользователям возможность изменять свой собственный пароль, защищая от изменений чужие записи. Каким образом она работает?
   *25. Поясните, какая угроза безопасности хранения данных возникает, если setuid-программа не защищена от записи.
   26. Выполните следующую последовательность команд, в которой "a.out" — имя исполняемого файла:
 
   chmod 4777 a.out
   chown root a.out
 
   Команда chmod "включает" бит setuid (4 в 4777); пользователь "root" традиционно является суперпользователем. Может ли в результате выполнения этой последовательности произойти нарушение защиты информации?
   27. Что произойдет в процессе выполнения программы, представленной на Рисунке 7.38? Поясните свой ответ.
 
    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
   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" (а в случае отсутствия файла с таким именем создает его) и записывает в него данные. Напишите программу, в которой используется эта команда.
 
    main() { exit(0); }
    Рисунок 7.40
   32. Процессор командного языка shell проверяет код, возвращаемый функцией exit, воспринимая нулевое значение как "истину", а любое другое значение как "ложь" (обратите внимание на несогласованность с языком Си). Предположим, что файл, исполняющий программу на Рисунке 7.40, имеет имя "truth". Поясните, что произойдет, когда shell будет исполнять следующий набор команд:
 
   while truth
   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 в состояние 2 (многопользовательский режим), администратор системы вводит с консоли команду init 2. Консольный shell порождает свое ответвление и запускает init. Что имело бы место в системе в том случае, если бы активен был только один процесс init?
   43. Формат записей в файле "/etc/inittab" допускает задание действия, связанного с каждым порождаемым процессом. Например, с getty-процессом связано действие "respawn" (возрождение), означающее, что процесс init должен возрождать getty-процесс, если последний прекращает существование. На практике, когда пользователь выходит из системы процесс init порождает новый getty-процесс, чтобы другой пользователь мог получить доступ к временно бездействующей терминальной линии. Каким образом это делает процесс init?
   44. Некоторые из алгоритмов ядра прибегают к просмотру таблицы процессов. Время поиска данных можно сократить, если использовать указатели на: родителя процесса, любого из потомков, другой процесс, имеющий того же родителя. Процесс обнаруживает всех своих потомков, следуя сначала за указателем на любого из потомков, а затем используя указатели на другие процессы, имеющие того же родителя (циклы недопустимы). Какие из алгоритмов выиграют от этого? Какие из алгоритмов нужно оставить без изменений?

ГЛАВА 8. ДИСПЕТЧЕРИЗАЦИЯ ПРОЦЕССОВ И ЕЕ ВРЕМЕННЫЕ ХАРАКТЕРИСТИКИ

   В системе разделения времени ядро предоставляет процессу ресурсы центрального процессора (ЦП) на интервал времени, называемый квантом, по истечении которого выгружает этот процесс и запускает другой, периодически переупорядочивая очередь процессов. Алгоритм планирования процессов в системе UNIX использует время выполнения в качестве параметра. Каждый активный процесс имеет приоритет планирования; ядро переключает контекст на процесс с наивысшим приоритетом. При переходе выполняющегося процесса из режима ядра в режим задачи ядро пересчитывает его приоритет, периодически и в режиме задачи переустанавливая приоритет каждого процесса, готового к выполнению.
   Информация о времени, связанном с выполнением, нужна также и некоторым из пользовательских процессов: используемая ими, например, команда time позволяет узнать, сколько времени занимает выполнение другой команды, команда date выводит текущую дату и время суток. С помощью различных системных функций процессы могут устанавливать или получать временные характеристики выполнения в режиме ядра, а также степень загруженности центрального процессора. Время в системе поддерживается с помощью аппаратных часов, которые посылают ЦП прерывания с фиксированной, аппаратно-зависимой частотой, обычно 50-100 раз в секунду. Каждое поступление прерывания по таймеру (часам) именуется таймерным тиком. В настоящей главе рассматриваются особенности реализации процессов во времени, включая планирование процессов в системе UNIX, описание связанных со временем системных функций, а также функций, выполняемых программой обработки прерываний по таймеру.

8.1 ПЛАНИРОВАНИЕ ВЫПОЛНЕНИЯ ПРОЦЕССОВ

   Планировщик процессов в системе UNIX принадлежит к общему классу планировщиков, работающих по принципу "карусели с многоуровневой обратной связью". В соответствии с этим принципом ядро предоставляет процессу ресурсы ЦП на квант времени, по истечении которого выгружает этот процесс и возвращает его в одну из нескольких очередей, регулируемых приоритетами. Прежде чем процесс завершится, ему может потребоваться множество раз пройти через цикл с обратной связью. Когда ядро выполняет переключение контекста и восстанавливает контекст процесса, процесс возобновляет выполнение с точки приостанова.

8.1.1 Алгоритм

   Сразу после переключения контекста ядро запускает алгоритм планирования выполнения процессов (Рисунок 8.1), выбирая на выполнение процесс с наивысшим приоритетом среди процессов, находящихся в состояниях "резервирования" и "готовности к выполнению, будучи загруженным в память". Рассматривать процессы, не загруженные в память, не имеет смысла, поскольку не будучи загружен, процесс не может выполняться. Если наивысший приоритет имеют сразу несколько процессов, ядро, используя принцип кольцевого списка (карусели), выбирает среди них тот процесс, который находится в состоянии "готовности к выполнению" дольше остальных. Если ни один из процессов не может быть выбран для выполнения, ЦП простаивает до момента получения следующего прерывания, которое произойдет не позже чем через один таймерный тик; после обработки этого прерывания ядро снова запустит алгоритм планирования.
 
    алгоритм schedule_process
    входная информация: отсутствует
    выходная информация: отсутствует
    {
     выполнять пока (для запуска не будет выбран один из процессов)  {
      for (каждого процесса в очереди готовых к выполнению)
       выбрать процесс с наивысшим приоритетом из загруженных в память;
      if (ни один из процессов не может быть избран для выполнения)
       приостановить машину;
     /* машина выходит из состояния простоя по прерыванию */
     }
     удалить выбранный процесс из очереди готовых к выполнению;
     переключиться на контекст выбранного процесса, возобновить его выполнение;
    }
    Рисунок 8.1. Алгоритм планирования выполнения процессов

8.1.2 Параметры диспетчеризации

   В каждой записи таблицы процессов есть поле приоритета, используемое планировщиком процессов. Приоритет процесса в режиме задачи зависит от того, как этот процесс перед этим использовал ресурсы ЦП. Можно выделить два класса приоритетов процесса (Рисунок 8.2): приоритеты выполнения в режиме ядра и приоритеты выполнения в режиме задачи. Каждый класс включает в себя ряд значений, с каждым значением логически ассоциирована некоторая очередь процессов. Приоритеты выполнения в режиме задачи оцениваются для процессов, выгруженных по возвращении из режима ядра в режим задачи, приоритеты выполнения в режиме ядра имеют смысл только в контексте алгоритма sleep. Приоритеты выполнения в режиме задачи имеют верхнее пороговое значение, приоритеты выполнения в режиме ядра имеют нижнее пороговое значение. Среди приоритетов выполнения в режиме ядра далее можно выделить высокие и низкие приоритеты: процессы с низким приоритетом возобновляются по получении сигнала, а процессы с высоким приоритетом продолжают оставаться в состоянии приостанова (см. раздел 7.2.1).
   Пороговое значение между приоритетами выполнения в режимах ядра и задачи на Рисунке 8.2 отмечено двойной линией, проходящей между приоритетом ожидания завершения потомка (в режиме ядра) и нулевым приоритетом выполнения в режиме задачи. Приоритеты процесса подкачки, ожидания ввода-вывода, связанного с диском, ожидания буфера и индекса являются высокими, не допускающими прерывания системными приоритетами, с каждым из которых связана очередь из 1, 3, 2 и 1 процесса, соответственно, в то время как приоритеты ожидания ввода с терминала, вывода на терминал и завершения потомка являются низкими, допускающими прерывания системными приоритетами, с каждым из которых связана очередь из 4, 0 и 2 процессов, соответственно. На рисунке представлены также уровни приоритетов выполнения в режиме задачи [24].
   Ядро вычисляет приоритет процесса в следующих случаях:
   • Непосредственно перед переходом процесса в состояние приостанова ядро назначает ему приоритет исходя из причины приостанова. Приоритет не зависит от динамических характеристик процесса (продолжительности ввода-вывода или времени счета), напротив, это постоянная величина, жестко устанавливаемая в момент приостанова и зависящая только от причины перехода процесса в данное состояние. Процессы, приостановленные алгоритмами низкого уровня, имеют тенденцию порождать тем больше узких мест в системе, чем дольше они находятся в этом состоянии; поэтому им назначается более высокий приоритет по сравнению с остальными процессами. Например, процесс, приостановленный в ожидании завершения ввода-вывода, связанного с диском, имеет более высокий приоритет по сравнению с процессом, ожидающим освобождения буфера, по нескольким причинам. Прежде всего, у первого процесса уже есть буфер, поэтому не исключена возможность, что когда он возобновится, он успеет освободить и буфер, и другие ресурсы. Чем больше ресурсов свободно, тем меньше шансов для возникновения взаимной блокировки процессов. Системе не придется часто переключать контекст, благодаря чему сократится время реакции процесса и увеличится производительность системы. Во-вторых, буфер, освобождения которого ожидает процесс, может быть занят процессом, ожидающим в свою очередь завершения ввода-вывода. По завершении ввода-вывода будут возобновлены оба процесса, поскольку они были приостановлены по одному и тому же адресу. Если первым запустить на выполнение процесс, ожидающий освобождения буфера, он в любом случае снова приостановится до тех пор, пока буфер не будет освобожден; следовательно, его приоритет должен быть ниже.
   • По возвращении процесса из режима ядра в режим задачи ядро вновь вычисляет приоритет процесса. Процесс мог до этого находиться в состоянии приостанова, изменив свой приоритет на приоритет выполнения в режиме ядра, поэтому при переходе процесса из режима ядра в режим задачи ему должен быть возвращен приоритет выполнения в режиме задачи. Кроме того, ядро "штрафует" выполняющийся процесс в пользу остальных процессов, отбирая используемые им ценные системные ресурсы.
   • Приоритеты всех процессов в режиме задачи с интервалом в 1 секунду (в версии V) пересчитывает программа обработки прерываний по таймеру, побуждая тем самым ядро выполнять алгоритм планирования, чтобы не допустить монопольного использования ресурсов ЦП одним процессом.
    Рисунок 8.2. Диапазон приоритетов процесса
   В течение кванта времени таймер может послать процессу несколько прерываний; при каждом прерывании программа обработки прерываний по таймеру увеличивает значение, хранящееся в поле таблицы процессов, которое описывает продолжительность использования ресурсов центрального процессора (ИЦП). В версии V каждую секунду программа обработки прерываний переустанавливает значение этого поля, используя функцию полураспада (decay):