Ядро отсоединяет области при выполнении системных функций exec, exit и shmdt (отсоединить разделяемую память). При этом ядро корректирует соответствующую запись и разъединяет связь с физической памятью, делая недействительными связанные с областью регистры управления памятью (алгоритм detachreg, Рисунок 6.26). Механизм преобразования адресов после этого будет относиться уже к процессу, а не к области (как в алгоритме freereg). Ядро уменьшает значение счетчика ссылок на область и значение поля, описывающего размер процесса в записи таблицы процессов, в соответствии с размером области. Если значение счетчика становится равным 0 и если нет причины оставлять область без изменений (область не является областью разделяемой памяти или областью команд с признаками неотъемлемой части процесса, о чем будет идти речь в разделе 7.5), ядро освобождает область по алгоритму freereg. В противном случае ядро снимает с индекса и с области блокировку, установленную для того, чтобы предотвратить конкуренцию между параллельно выполняющимися процессами (см. раздел 7.5), но оставляет область и ее ресурсы без изменений.

6.5.8 Копирование содержимого области

    Рисунок 6.27. Копирование содержимого области
    алгоритм dupreg /* копирование содержимого существующей области */
    входная информация: указатель на точку входа в таблице областей
    выходная информация: указатель на область, являющуюся точной копией существующей области
    {
     if (область разделяемая) /* в вызывающей программе счетчик ссылок на область будет увеличен, после чего будет исполнен алгоритм attachreg */
      return (указатель на исходную область);
     выделить новую область (алгоритм allocreg);
     установить значения вспомогательных структур управления памятью в точном соответствии со значениями существующих структур исходной области;
     выделить для содержимого области физическую память;
     «скопировать» содержимое исходной области во вновь созданную область;
     return (указатель на выделенную область);
    }
    Рисунок 6.28. Алгоритм копирования содержимого существующей области
 
   Системная функция fork требует, чтобы ядро скопировало содержимое областей процесса. Если же область разделяемая (разделяемый текст команд или разделяемая память), ядру нет надобности копировать область физически; вместо этого оно увеличивает значение счетчика ссылок на область, позволяя родительскому и порожденному процессам использовать область совместно. Если область не является разделяемой и ядру нужно физически копировать ее содержимое, оно выделяет новую запись в таблице областей, новую таблицу страниц и отводит под создаваемую область физическую память. В качестве примера рассмотрим Рисунок 6.27, где процесс A порождает с помощью функции fork процесс B и копирует области родительского процесса. Область команд процесса A является разделяемой, поэтому процесс B может использовать эту область совместно с процессом A. Однако области данных и стека родительского процесса являются его личной принадлежностью (имеют частный тип), поэтому процессу B нужно скопировать их содержимое во вновь выделенные области. При этом даже для областей частного типа физическое копирование области не всегда необходимо, в чем мы убедимся позже (глава 9). На Рисунке 6.28 приведен алгоритм копирования содержимого области (dupreg).

6.6 ПРИОСТАНОВКА ВЫПОЛНЕНИЯ

   К настоящему моменту мы рассмотрели все функции работы с внутренними структурами процесса, выполняющиеся на нижнем уровне взаимодействия с процессом и обеспечивающие переход в состояние «выполнения в режиме ядра» и выход из этого состояния в другие состояния, за исключением функций, переводящих процесс в состояние «приостанова выполнения». Теперь перейдем к рассмотрению алгоритмов, с помощью которых процесс переводится из состояния «выполнения в режиме ядра» в состояние «приостанова в памяти» и из состояния приостанова в состояния «готовности к запуску» с выгрузкой и без выгрузки из памяти.
 
    Рисунок 6.29. Стандартные контекстные уровни приостановленного процесса
 
   Выполнение процесса приостанавливается обычно во время исполнения запрошенной им системной функции: процесс переходит в режим ядра (контекстный уровень 1), исполняя внутреннее прерывание операционной системы, и приостанавливается в ожидании ресурсов. При этом процесс переключает контекст, запоминая в стеке свой текущий контекстный уровень и исполняясь далее в рамках системного контекстного уровня 2 (Рисунок 6.29). Выполнение процессов приостанавливается также и в том случае, когда оно наталкивается на отсутствие страницы в результате обращения к виртуальным адресам, не загруженным физически; процессы не будут выполняться, пока ядро не считает содержимое страниц.

6.6.1 События, вызывающие приостанов выполнения, и их адреса

   Как уже говорилось во второй главе, процессы приостанавливаются до наступления определенного события, после которого они «пробуждаются» и переходят в состояние «готовности к выполнению» (с выгрузкой и без выгрузки из памяти). Такого рода абстрактное рассуждение недалеко от истины, ибо в конкретном воплощении совокупность событий отображается на совокупность виртуальных адресов (ядра). Адреса, с которыми связаны события, закодированы в ядре, и их единственное назначение состоит в их использовании в процессе отображения ожидаемого события на конкретный адрес. Как для абстрактного рассмотрения, так и для конкретной реализации события безразлично, сколько процессов одновременно ожидают его наступления. Как результат, возможно возникновение некоторых противоречий. Во-первых, когда событие наступает и процессы, ожидающие его, соответствующим образом оповещаются об этом, все они «пробуждаются» и переходят в состояние «готовности к выполнению». Ядро выводит процессы из состояния приостанова все сразу, а не по одному, несмотря на то, что они в принципе могут конкурировать за одну и ту же заблокированную структуру данных и большинство из них через небольшой промежуток времени опять вернется в состояние приостанова (более подробно об этом шла речь в главах 2 и 3). На Рисунке 6.30 изображены несколько процессов, приостановленных до наступления определенных событий.
 
    Рисунок 6.30. Процессы, приостановленные до наступления событий, и отображение событий на конкретные адреса
 
   Еще одно противоречие связано с тем, что на один и тот же адрес могут отображаться несколько событий. На Рисунке 6.30, например, события «освобождение буфера» и «завершение ввода-вывода» отображаются на адрес буфера («адрес A»). Когда ввод-вывод в буфер завершается, ядро возобновляет выполнение всех процессов, приостановленных в ожидании наступления как того, так и другого события. Поскольку процесс, ожидающий завершения ввода-вывода, удерживает буфер заблокированным, другие процессы, которые ждали освобождения буфера, вновь приостановятся, ибо буфер все еще занят. Функционирование системы было бы более эффективным, если бы отображение событий на адреса было однозначным. Однако на практике такого рода противоречие на производительности системы не отражается, поскольку отображение на один адрес более одного события имеет место довольно редко, а также поскольку выполняющийся процесс обычно освобождает заблокированные ресурсы до того, как начнут выполняться другие процессы. Стилистически, тем не менее, механизм функционирования ядра стал бы более понятен, если бы отображение было однозначным.
 
 

6.6.2 Алгоритмы приостанова и возобновления выполнения

    алгоритм sleep
    входная информация:
     (1) адрес приостанова
     (2) приоритет
    выходная информация:
     1, если процесс возобновляется по сигналу, который ему удалось уловить;
     вызов алгоритма longjump, если процесс возобновляется по сигналу, который ему не удалось уловить;
     0 — во всех остальных случаях;
    {
     поднять приоритет работы процессора таким образом, чтобы заблокировать все прерывания;
     перевести процесс в состояние приостанова;
     включить процесс в хеш-очередь приостановленных процессов, базирующуюся на адресах приостанова;
     сохранить адрес приостанова в таблице процессов;
     сделать ввод для процесса приоритетным;
     if (приостанов процесса НЕ допускает прерываний)  {
      выполнить переключение контекста;
       /* с этого места процесс возобновляет выполнение, когда «пробуждается» */
      снизить приоритет работы процессора так, чтобы вновь разрешить прерывания (как было до приостанова процесса);
      return (0);
     }
     /* приостанов процесса принимает прерывания, вызванные сигналами */
     if (к процессу не имеет отношения ни один из сигналов)  {
      выполнить переключение контекста;
       /* с этого места процесс возобновляет выполнение, когда «пробуждается» */
      if (к процессу не имеет отношения ни один из сигналов)  {
       восстановить приоритет работы процессора таким, каким он был в момент приостанова процесса;
       return (0);
      }
     }
     удалить процесс из хеш-очереди приостановленных процессов, если он все еще находится там;
     восстановить приоритет работы процессора таким, каким он был в момент приостанова процесса;
     if (приоритет приостановленного процесса позволяет принимать сигналы)  return (1);
     запустить алгоритм longjump;
    }
    Рисунок 6.31. Алгоритм приостанова процесса
   На Рисунке 6.31 приведен алгоритм приостанова процесса. Сначала ядро повышает приоритет работы процессора так, чтобы заблокировать все прерывания, которые могли бы (путем создания конкуренции) помешать работе с очередями приостановленных процессов, и запоминает старый приоритет, чтобы восстановить его, когда выполнение процесса будет возобновлено. Процесс получает пометку «приостановленного», адрес приостанова и приоритет запоминаются в таблице процессов, а процесс помещается в хеш-очередь приостановленных процессов. В простейшем случае (когда приостанов не допускает прерываний) процесс выполняет переключение контекста и благополучно «засыпает». Когда приостановленный процесс «пробуждается», ядро начинает планировать его запуск: процесс возвращает сохраненный в алгоритме sleep контекст, восстанавливает старый приоритет работы процессора (который был у него до начала выполнения алгоритма) и возвращает управление ядру.
 
    алгоритм wakeup /* возобновление приостановленного процесса */
    входная информация: адрес приостанова
    выходная информация: отсутствует
    {
     повысить приоритет работы процессора таким образом, чтобы заблокировать все прерывания;
     найти хеш-очередь приостановленных процессов с указанным адресом приостанова;
     for (каждого процесса, приостановленного по указанному адресу)  {
      удалить процесс из хеш-очереди;
      сделать пометку о том, что процесс находится в состоянии «готовности к запуску»;
      включить процесс в список процессов, готовых к запуску (для планировщика процессов);
      очистить поле, содержащее адрес приостанова, в записи таблицы процессов;
      if (процесс не загружен в память)
       возобновить выполнение программы подкачки (нулевой процесс);
      else  if (возобновляемый процесс более подходит для исполнения, чем ныне выполняющийся)
        установить соответствующий флаг для планировщика;
     }
     восстановить первоначальный приоритет работы процессора;
    }
    Рисунок 6.32. Алгоритм возобновления приостановленного процесса
 
   Чтобы возобновить выполнение приостановленных процессов, ядро обращается к алгоритму wakeup (Рисунок 6.32), причем делает это как во время исполнения алгоритмов реализации стандартных системных функций, так и в случае обработки прерываний. Алгоритм iput, например, освобождает заблокированный индекс и возобновляет выполнение всех процессов, ожидающих снятия блокировки. Точно так же и программа обработки прерываний от диска возобновляет выполнение процессов, ожидающих завершения ввода-вывода. В алгоритме wakeup ядро сначала повышает приоритет работы процессора, чтобы заблокировать прерывания. Затем для каждого процесса, приостановленного по указанному адресу, выполняются следующие действия: делается пометка в поле, описывающем состояние процесса, о том, что процесс готов к запуску; процесс удаляется из списка приостановленных процессов и помещается в список процессов, готовых к запуску; поле в записи таблицы процессов, содержащее адрес приостанова, очищается. Если возобновляемый процесс не загружен в память, ядро запускает процесс подкачки, обеспечивающий подкачку возобновляемого процесса в память (подразумевается система, в которой подкачка страниц по обращению не поддерживается); в противном случае, если возобновляемый процесс более подходит для исполнения, чем ныне выполняющийся, ядро устанавливает для планировщика специальный флаг, сообщающий о том, что процессу по возвращении в режим задачи следует пройти через алгоритм планирования (глава 8). Наконец, ядро восстанавливает первоначальный приоритет работы процессора. При этом на ядро не оказывается никакого давления: «пробуждение» (wakeup) процесса не вызывает его немедленного исполнения; благодаря «пробуждению», процесс становится только доступным для запуска.
   Все, о чем говорилось выше, касается простейшего случая выполнения алгоритмов sleep и wakeup, поскольку предполагается, что процесс приостанавливается до наступления соответствующего события. Во многих случаях процессы приостанавливаются в ожидании событий, которые «должны» наступить, например, в ожидании освобождения ресурса (индексов или буферов) или в ожидании завершения ввода-вывода, связанного с диском. Уверенность процесса в неминуемом возобновлении основана на том, что подобные ресурсы могут быть предоставлены только во временное пользование. Тем не менее, иногда процесс может приостановиться в ожидании события, не будучи уверенным в неизбежном наступлении последнего, в таком случае у процесса должна быть возможность в любом случае вернуть себе управление и продолжить выполнение. В подобных ситуациях ядро немедленно нарушает «сон» приостановленного процесса, посылая ему сигнал. Более подробно о сигналах мы поговорим в следующей главе; здесь же примем допущение, что ядро может (выборочно) возобновлять приостановленные процессы по сигналу и что процесс может распознавать получаемые сигналы.
   Например, если процесс обратился к системной функции чтения с терминала, ядро не будет в состоянии выполнить запрос процесса до тех пор, пока пользователь не введет данные с клавиатуры терминала (глава 10). Тем не менее, пользователь, запустивший процесс, может оставить терминал на весь день, при этом процесс останется приостановленным в ожидании ввода, а терминал может понадобиться другому пользователю. Если другой пользователь прибегнет к решительным мерам (таким как выключение терминала), ядро должно иметь возможность восстановить отключенный процесс: в качестве первого шага ядру следует возобновить приостановленный процесс по сигналу. В том, что процессы могут приостановиться на длительное время, нет ничего плохого. Приостановленный процесс занимает позицию в таблице процессов и может поэтому удлинять время поиска (ожидания) путем выполнения определенных алгоритмов, которые не занимают время центрального процессора и поэтому выполняются практически незаметно.
   Чтобы как-то различать между собой состояния приостанова, ядро устанавливает для приостанавливаемого процесса (при входе в это состояние) приоритет планирования на основании соответствующего параметра алгоритма sleep. То есть ядро запускает алгоритм sleep с параметром «приоритет», в котором отражается наличие уверенности в неизбежном наступлении ожидаемого события. Если приоритет превышает пороговое значение, процесс не будет преждевременно выходить из приостанова по получении сигнала, а будет продолжать ожидать наступления события. Если же значение приоритета ниже порогового, процесс будет немедленно возобновлен по получении сигнала [20].
   Проверка того, имеет ли процесс уже сигнал при входе в алгоритм sleep, позволяет выяснить, приостанавливался ли процесс ранее. Например, если значение приоритета в вызове алгоритма sleep превышает пороговое значение, процесс приостанавливается в ожидании выполнения алгоритма wakeup. Если же значение приоритета ниже порогового, выполнение процесса не приостанавливается, но на сигнал процесс реагирует точно так же, как если бы он был приостановлен. Если ядро не проверит наличие сигналов перед приостановом, возможна опасность, что сигнал больше не поступит вновь и в этом случае процесс никогда не возобновится.
   Когда процесс «пробуждается» по сигналу (или когда он не переходит в состояние приостанова из-за наличия сигнала), ядро может выполнить алгоритм longjump (в зависимости от причины, по которой процесс был приостановлен). С помощью алгоритма longjump ядро восстанавливает ранее сохраненный контекст, если нет возможности завершить выполняемую системную функцию. Например, если из-за того, что пользователь отключил терминал, было прервано чтение данных с терминала, функция read не будет завершена, но возвратит признак ошибки. Это касается всех системных функций, которые могут быть прерваны во время приостанова. После выхода из приостанова процесс не сможет нормально продолжаться, поскольку ожидаемое событие не наступило. Перед выполнением большинства системных функций ядро сохраняет контекст процесса, используя алгоритм setjump и вызывая тем самым необходимость в последующем выполнении алгоритма longjump.
   Встречаются ситуации, когда ядро требует, чтобы процесс возобновился по получении сигнала, но не выполняет алгоритм longjump. Ядро запускает алгоритм sleep со специальным значением параметра «приоритет», подавляющим исполнение алгоритма longjump и заставляющим алгоритм sleep возвращать код, равный 1. Такая мера более эффективна по сравнению с немедленным выполнением алгоритма setjump перед вызовом sleep и последующим выполнением алгоритма longjump для восстановления первоначального контекста процесса. Задача заключается в том, чтобы позволить ядру очищать локальные структуры данных. Драйвер устройства, например, может выделить свои частные структуры данных и приостановиться с приоритетом, допускающим прерывания; если по сигналу его работа возобновляется, он освобождает выделенные структуры, а затем выполняет алгоритм longjump, если необходимо. Пользователь не имеет возможности проконтролировать, выполняет ли процесс алгоритм longjump; выполнение этого алгоритма зависит от причины приостановки процесса, а также от того, требуют ли структуры данных ядра внесения изменений перед выходом из системной функции.

6.7 ВЫВОДЫ

   Мы завершили рассмотрение контекста процесса. Процессы в системе UNIX могут находиться в различных логических состояниях и переходить из состояния в состояние в соответствии с установленными правилами перехода, при этом информация о состоянии сохраняется в таблице процессов и в адресном пространстве процесса. Контекст процесса состоит из пользовательского контекста и системного контекста. Пользовательский контекст состоит из программ процесса, данных, стека задачи и областей разделяемой памяти, а системный контекст состоит из статической части (запись в таблице процессов, адресное пространство процесса и информация, необходимая для отображения адресного пространства) и динамической части (стек ядра и сохраненное состояние регистров предыдущего контекстного уровня системы), которые запоминаются в стеке и выбираются из стека при выполнении процессом обращений к системным функциям, при обработке прерываний и при переключениях контекста. Пользовательский контекст процесса распадается на отдельные области, которые представляют собой непрерывные участки виртуального адресного пространства и трактуются как самостоятельные объекты использования и защиты. В модели управления памятью, которая использовалась при описании формата виртуального адресного пространства процесса, предполагалось наличие у каждой области процесса своей таблицы страниц. Ядро располагает целым набором различных алгоритмов для работы с областями. В заключительной части главы были рассмотрены алгоритмы приостанова (sleep) и возобновления (wakeup) процессов. Структуры и алгоритмы, описанные в данной главе, будут использоваться в последующих главах при рассмотрении системных функций управления процессами и планирования их выполнения, а также при объяснении различных методов распределения памяти.

6.8 УПРАЖНЕНИЯ

   1. Составьте алгоритм преобразования виртуальных адресов в физические, на входе которого задаются виртуальный адрес и адрес точки входа в частную таблицу областей.
   2. В машинах AT&T 3B2 и NSC серии 32000 используется двухуровневая схема трансляции виртуальных адресов в физические (с сегментацией). То есть в системе поддерживается указатель на таблицу страниц, каждая запись которой может адресовать фиксированную часть адресного пространства процесса по смещению в таблице. Сравните алгоритм трансляции виртуальных адресов на этих машинах с алгоритмом, изложенным в тексте при обсуждении модели управления памятью. Подумайте над проблемами производительности и потребности в памяти для размещения вспомогательных таблиц.
   3. В архитектуре системы VAX-11 поддерживаются два набора регистров защиты памяти, используемых машиной в процессе трансляции пользовательских адресов. Механизм трансляции используется тот же, что и в предыдущем пункте, за одним исключением: указателей на таблицу страниц здесь два. Если процесс располагает тремя областями — команд, данных и стека — то каким образом, используя два набора регистров, следует производить отображение областей на таблицы страниц? Увеличение стека в архитектуре системы VAX-11 идет в направлении младших виртуальных адресов. Какой тогда вид имела бы область стека? В главе 11 будет рассмотрена область разделяемой памяти: как она может быть реализована в архитектуре системы VAX-11?
   4. Составьте алгоритм выделения и освобождения страниц памяти и таблиц страниц. Какие структуры данных следует использовать, чтобы достичь наивысшей производительности или наибольшей простоты реализации алгоритма?
   5. Устройство управления памятью MC68451 для семейства микропроцессоров Motorola 68000 допускает выделение сегментов памяти размером от 256 байт до 16 мегабайт. Каждое (физическое) устройство управления памятью поддерживает 32 дескриптора сегментов. Опишите эффективный метод выделения памяти для этого случая. Каким образом осуществлялась бы реализация областей?
   6. Рассмотрим отображение виртуальных адресов, представленное на Рисунке 6.5. Предположим, что ядро выгружает процесс (в системе с подкачкой процессов) или откачивает в область стека большое количество страниц (в системе с замещением страниц). Если через какое-то время процесс обратится к виртуальному адресу 68432, будет ли он должен обратиться к соответствующей ячейке физической памяти, из которой он считывал данные до того, как была выполнена операция выгрузки (откачки)? Если нижние уровни системы управления памятью реализуются с использованием таблицы страниц, следует ли эти таблицы располагать в тех же, что и сами страницы, местах физической памяти?
   *7. Можно реализовать систему, в которой стек ядра располагается над вершиной стека задачи. Подумайте о достоинствах и недостатках подобной системы.
   8. Каким образом, присоединяя область к процессу, ядро может проверить то, что эта область не накладывается на виртуальные адреса областей, уже присоединенных к процессу?
   9. Обратимся к алгоритму переключения контекста. Допустим, что в системе готов к выполнению только один процесс. Другими словами, ядро выбирает для выполнения процесс с только что сохраненным контекстом. Объясните, что произойдет при этом.
   10. Предположим, что процесс приостановился, но в системе нет процессов, готовых к выполнению. Что произойдет, когда приостановившийся процесс переключит контекст?
   11. Предположим, что процесс, выполняемый в режиме задачи, израсходовал выделенный ему квант времени и в результате прерывания по таймеру ядро выбирает для выполнения новый процесс. Объясните, почему переключение контекста произойдет на системном контекстном уровне 2.
   12. В системе с замещением страниц процесс, выполняемый в режиме задачи, может столкнуться с отсутствием нужной страницы, которая не была загружена в память. В ходе обработки прерывания ядро считывает страницу из области подкачки и приостанавливается. Объясните, почему переключение контекста (в момент приостанова) произойдет на системном контекстном уровне 2.
   13. Процесс использует системную функцию read с форматом вызова read(fd,buf,1024);
   в системе с замещением страниц памяти. Предположим, что ядро исполняет алгоритм read для считывания данных в системный буфер, однако при попытке копирования данных в адресное пространство задачи сталкивается с отсутствием нужной страницы, содержащей структуру buf, вследствие того, что она была ранее выгружена из памяти. Ядро обрабатывает возникшее прерывание, считывая отсутствующую страницу в память. Что происходит на каждом из системных контекстных уровней? Что произойдет, если программа обработки прерывания приостановится в ожидании завершения считывания страницы?