Главная страница  Межпроцессное взаимодействие (состязание) 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 [ 131 ] 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187

Останавливать выполнение fork на полпути сложно и неудобно, поэтому менеджер памяти, чтобы всегда знать, есть ли свободные ячейки в таблице процессов, поддерживает счетчик существующих процессов. Если таблица еще не заполнена, делается попытка выделить память под данные и стек дочернего процесса. Для процессов с разделенными пространствами кода и данных запрашивается только память, достаточная для размещения стека и данных. Если этот шаг пройден успешно, fork гарантированно выполнится. Затем выделенная область памяти заполняется, в таблице процессов находится и заполняется ячейка нового процесса, для него выбирается PID, и другие части системы информируются о создании нового процесса.

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

Проблема возникает в том случае, если родитель завершающегося процесса сам уже мертв. Если не предпринять никаких действий, потомок останется уже полноправным зомби. Поэтому таблицы загодя меняются так, чтобы процесс стал потомком процесса init. При запуске системы init считывает файл с информацией о всех терминалах /etc/ttytab и для обслуживания каждого терминала ответвляет от себя дочерний процесс. Затем он входит в состояние блокировки, ожидая уведомлений о завершении. Таким образом, осиротевшие зомби быстро добиваются.

4.7.5. Системный вызов exec

Когда с терминала поступает команда, оболочка ответвляет новый процесс, который выполняет запрошенную команду. Этим мог бы заняться один системный вызов, который бы одновременно решал задачи fork и exec, но они разделены по одной очень веской причине: чтобы упростить реализацию перенаправления ввода/ вывода. Если стандартный ввод перенаправлен и оболочка выполняет ветвление, то потомок, перед тем как выполнить команду, закрывает стандартный ввод, а затем открывает новый. Таким образом, запущенный процесс наследует перенаправление стандартного ввода. То же относится и к стандартному выводу.

Системный вызов exec является самым сложным в MINIX. Он должен заместить текущий образ процесса новым и, в том числе, установить новый стек. Этапы выполнения этого системного вызова:

1. Проверить, является ли файл исполняемым.



2. Считать из заголовка файла размеры сегментов и общий требуемый объем памяти.

3. Узнать у выполнившего запуск процесса аргументы и переменные окружения.

4. Выделить участок памяти и освободить старую память.

5. Копировать в новый образ стек.

6. Записать в новый образ в памяти данные (и, возможно, код).

7. Проверить и, если они установлены, обработать биты setuid, setgid.

8. Подправить запись в таблице процессов.

9. Сообщить ядру, что процесс готов к запуску.

Каждый из шагов сам, в свою очередь, состоит из серии более мелких шагов, некоторые из них могут завершиться неудачей. Например, доступной памяти может не хватить для запуска процесса. Порядок, в котором производятся проверки, продуман так, чтобы гарантировать, что старый образ процесса в памяти остается до тех пор, пока не будет уверенности, что вызов exec завершится успехом. Это делается во избежание ситуации, когда новый образ сформировать невозможно, а к старому уже не вернуться. Как правило, вызов exec не передает управление обратно, но если вызов провалился , процесс вновь получает управление и уведомляется об ошибке.

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

Тем не менее проверка чересчур строга. Иногда отклоняется вызов exec, который, фактически, мог бы быть выполнен. Например, предположим, что выполняющий exec процесс занимает 20 Кбайт и его текст не разделяют другие процессы. Далее, представим, что есть свободный блок объемом 30 Кбайт, а новый образ требует 50 Кбайт памяти. Выполняя проверку до освобождения памяти, мы обнаружим, что доступно только 30 Кбайт памяти и вызов не будет выполнен. Если бы память сначала высвобождалась, то вызов мог бы быть выполнен, в зависимости от того, сольются ли при освобождении памяти имеющийся свободный блок размером 30 Кбайт с новым, размером 20 Кбайт. Обрабатывать подобные ситуации лучше мог бы более сложный алгоритм.

Еще одно потенциальное улучшение - искать два свободных блока, один для сегмента кода и один для сегмента данных, если адресные пространства запускаемого процесса разделены. Сегменты не должны быть расположены непрерывно.

Более тонкий нюанс - умещается ли новый процесс в виртуальном адресном пространстве. Суть проблемы в том, что память выделяется не байтами, а 256-байтовыми кликами. Каждый клик должен целиком принадлежать одному сег-



менту и не может, например, быть наполовину занят данными, наполовину стеком, так как все управление памятью производится в кликах.

Чтобы стало очевиднее, как такое поведение приводит к проблемам, заметим, что на 16-битных системах (8088 и 80286) адресное пространство ограничено 64 Кбайт, что составляет 256 кликов. Представим теперь, что программе с раздельными адресными пространствами кода и данных требуется 40 ООО байт под код, 32 770 байт под данные и 32 760 байт под стек. Тогда сегмент данных займет 129 кликов, из которых последний будет использоваться только частично (при этом он все равно целиком будет принадлежать сегменту). Под стек потребуется 128 кликов. Для данных и стека вместе нужно более 256 кликов, таким образом, они не смогут сосуществовать, хотя необходимое количество байтов умещается в виртуальной памяти (едва-едва). В теории, эта проблема относится ко всем машинам, у которых размер клика более одного байта, но на практике для процессоров класса Pentium она возникает исключительно редко, так как на таких машинах допустимы большие сегменты (более 4 Гбайт).

Другой важный вопрос - начальная установка стека. Библиотечный вызов, обычно используемый для выполнения exec, выглядит так:

execve(name. argv. envp):

Здесь name - указатель на имя загружаемого файла, argv ссылается на массив указателей на аргументы, а указатель envp содержит адрес массива указателей, ссылающихся на строки переменных окружения.

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

Чтобы уменьшить накладные расходы, связанные со считыванием всех этих кусочков, была выбрана совершенно иная стратегия. Библиотечная процедура execve сама строит новый стек и передает менеджеру памяти его базовый адрес и размер. Создавать новый стек в пользовательском пространстве гораздо эффективнее, поскольку ссылки на аргументы будут локальными указателями, а не ссылками на другое адресное пространство.

Чтобы яснее понять этот механизм, рассмотрим пример. Когда пользователь вводит в оболочке команду

Is -1 f.c g.c

оболочка интерпретирует ее и делает вызов библиотечной процедуры: execve( /bin/1S . argv. envp):

Содержимое двух массивов указателей показано на рис. 4.34, а. Затем процедура execve, работая в пространстве пользователя, строит новый стек, как показано на рис. 4.34, б. В конечном итоге, стек при выполнении менеджером памяти системного вызова exec копируется без изменений.



1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 [ 131 ] 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187

© 2000 - 2018 ULTRASONEX-AMFODENT.RU.
Копирование материалов разрешено исключительно при условии цититирования.