netlib.narod.ru | < Назад | Оглавление | Далее > |
Если бы можно было выполнять только программу fork (или __clone), можно было бы только снова и снова создавать копии одного и того же процесса — система Linux могла бы запускать копии первого когда-либо созданного в ней процесса пользователя — init. Этот процесс полезен, но не настолько, желательно иметь возможность делать и что-нибудь еще.
После появления нового процесса он становится тем, что называется exec, (exec — это не одна функция, а скорее общий термин, относящийся к семейству функций, которые все по существу выполняют одно и то же, но принимают несколько различные аргументы.)
Таким образом, создание «действительно» нового процесса, который из родительской программы запускает образ другой программы, состоит из двух этапов: одного для fork и второго — для exec, приводя к следующей знакомой структуре кода С:
/* Возможность возникновения ошибки в следующих строках программы игнорируется. */ if (fork()) { /* Я - родительская программа; продолжаю обычную работу. */ } else { /* Я - дочерняя программа */ /* Становлюсь /some/other/program. */ execl("/some/other/program", "/some/other/program") }
(execl — одна из нескольких функций семейства exec.)
Основополагающей функцией ядра, реализующей все функции в семействе exec является do_execve, определенная в строках с 10079 по 10141. Функция do_execve выполняет три задачи:
Помня об этих задачах, давайте начнем подробное рассмотрение функции do_execve.
10082: Тип, представляющий всю информацию, которая должна отслеживаться преобразуемым в exec процессом, является struct linux_binprm (см. строку 13786) — скорее всего, binprm является аббревиатурой слов «binary parameters» («двоичные параметры»). do_execve выполняет свою задачу и осуществляет обмен данными с другими функциями, которым она делегировала часть своей работы посредством переменной bprm. Обратите внимание, что переменная bprm освобождается, когда функция do_execve выполняет возврат — эта переменная требуется только во время создания exec, а не в течение всего времени существования процесса.
10087: do_execve начинается с инициализации миниатюрной таблицы страниц (см. главу 8), которая отслеживает страницы памяти, выделенные для аргументов и среды нового процесса. Для этого она выделяет MAX_ARG_PAGES страниц (в строке 13780 это значение определятся равным 32); на платформе х86 размер каждой страницы памяти равен 4 Кб, следовательно, общий доступный для аргументов и среды объем памяти составляет 32 х 4 Кб = 128 Кб. Лично я был весьма рад узнать об этом, поскольку иногда превышал этот предел — обычно, когда запускал команду типа cat * >/tmp/joined в каталоге, содержащем тысячи файлов; все эти имена файлов, будучи объединены, вполне могут занять более 128 Кб. Обычно я обхожу эту проблему, используя программу xargs, но, возможно, со временем я перекомпилирую ядро, установив более высокий предел для MAX_ARG_PAGES. По крайней мере, теперь я знаю как повысить этот предел, если он действительно начинает доставлять беспокойство. (Возможно некоторые решительные читатели решат вообще удалить этот жестко запрограммированный предел.) Всегда приятно иметь исходный код.
10091: Следующий шаг — открытие выполняемого файла. Он еще не считывается — в настоящее время следует убедиться, что файл существует, чтобы функция do_execve знала, следует ли продолжать работу. В некоторых случаях функция do_execve была бы значительно более эффективной, если бы этот шаг был первым, вместо заполнения таблицы страниц bprm — если это не удается, время, затраченное на инициализацию таблицы страниц, оказывается потраченным зря. Однако это помогает, только если файл не существует — этот случай встречается слишком редко, чтобы для него стоило выполнять оптимизацию.
10096: Продолжает заполнять bprm, в частности его члены argc и envc. Для заполнения этих членов функция do_execve использует функцию count (строка A HREF="part1_15.htm#l9480">9480), которая пошагово просматривает переданные массивы argv и envp, подсчитывая отличные от NULL указатели. Первый же указатель, имеющий значение NULL, прерывает список, в результате чего возвращается количество найденных до этого момента отличных от NULL указателей. Вначале это кажется еще одним возможным местом некоторого снижения эффективности: иногда программа, вызывающая функцию do_execve, уже знает длину массивов argv и envp. Следовательно, функцию do_execve можно было бы расширить для приема целочисленных аргументов argc и envc, которые, если не являются отрицательными, указывали бы длину соответствующих массивов. Но все не так просто: count также все время выполняет в сканируемом массиве проверку в поисках ошибок при доступе к памяти. Принуждение программы, вызывающей функцию do_execve (или точнее, доверие ей этого), выполнять такую проверку было ошибочным. Лучше оставить все так, как есть.
10115: Копирует аргументы и среду в новый процесс, в основном посредством использования функции copy_strings (строка 9519). Эта функция кажется очень сложной, но ее задача достаточно проста: копировать строки в область памяти нового процесса, при необходимости выделяя страницы. Ее сложность проистекает из необходимости управлять таблицей страниц и пересекать границы областей ядра и пользователя, как подробнее освещено в главе 8.
10126: Если до сих пор все в порядке, последним шагом является отыскание обработчика двоичных файлов для нового выполняемого файла. Если функция search_binary_handler находит такой обработчик и завершается успешно, для указания успеха возвращается неотрицательное значение.
10134: Если достигнута эта точка, значит в одном из предшествующих шагов имела место ошибка. Любые страницы, выделенные для аргументов и среды программы, должны быть освобождены, после чего для сообщения об ошибке вызывающей программе должно быть возвращено отрицательное значение.
9832: prepare_binprm заполняет значительные области структуры bprm функции do_execve.
9839: С этой строки начинается ряд профилактических проверок, таких, как проверка того, что предпринимается попытка выполнить файл, а не каталог, и что разряд выполнения файла установлен.
9858: Учитывает разряды setuid и setgid, если они установлены, отмечая, что новый процесс должен обрабатывать выполняющего пользователя в качестве другого пользователя (если установлен разряд setuid) и/или члена другой группы (если установлен разряд setgid).
9933: И наконец, функция prepare_binprm считывает первые 128 байт файла (а не первые 512 байт, как утверждается в заглавном комментарии функции) в член buf структуры bprm.
Попутно отметим здесь возможное осложнение поддержки: в строке 13787 член buf структуры struct linux_binprm был объявлен имеющим длину 128 байт, а в строке 9933 128 байт были считаны. Но в обоих местах используется литеральная константа 128 — никакое выражение #define не утверждает, что оба числа должны быть одинаковыми; следовательно, одно из них могло бы измениться без соответствующего изменения второго, внося путаницу. Не хочется быть педантом, но это упущение трудно оправдать повышением эффективности — как, впрочем, и как-либо иначе.
Здесь читателям предоставляется возможность внести небольшой, но полезный вклад в исходный код: замените 128 новым значением #define (или чем-либо вроде sizeof(bprm->buf)) везде, где оно используется для этой цели; существует всего несколько таких случаев, но я предоставляю читателям самим их найти. Попытавшись сделать это, читатели поймут, почему в этом случае лучше использовать #define, а не sizeof. (А еще лучше было бы выявить и исправить все подобные повторяющиеся магические числа. Но выполнить такое глобальное исправление не так просто, поскольку точное выявление всех совпадений — весьма трудоемкий процесс; начните с малого, постепенно расширяя задачу.)
Обработчик двоичных файлов — это механизм ядра Linux, предназначенный для единообразной обработки различных двоичных форматов, потребность в котором связана с тем, что не все программы хранятся в одном и том же файловом формате. Хорошим примером служат файлы .class Java. Java определяет независимый от платформы формат двоичных исполняемых файлов — сами файлы остаются неизменными, независимо от платформы, на которой они выполняются — поэтому ясно, что они не могут быть структурированы так же, как собственные исполняемые файлы Linux. Тем не менее, благодаря использованию соответствующего обработчика двоичных файлов Linux может обрабатывать их, как если бы они были собственными исполняемыми файлами.
Обработчики двоичных файлов будут подробно описаны далее, но теперь читатели знают о них достаточно, чтобы понять, как функция do_execve находит подходящий обработчик. Она делегирует эту задачу функции search_binary_handler (строка 9996).
10037: Начинает итерационный просмотр связанного списка обработчиков двоичных файлов ядра, поочередно передавая каждому из них bprm. (В данный момент аргумент regs нас не интересует.) Точнее говоря, каждый элемент связанного списка обработчиков двоичных файлов содержит набор указателей на функции, которые вместе обеспечивают поддержку единого двоичного формата. (Состав функций приведен в определении структуры struct linux_binfmt в строке 13803; интерес представляют следующие компоненты: предназначенный для загрузки двоичных файлов, load_binary; предназначенный для загрузки библиотеки совместного использования, load_shlib; и предназначенный для создания дампа ядра, core_dump.) Функция search_binary_handler просто вызывает каждую из функций load_binary, пока одна из них не вернет неотрицательное значение, показывающее, что она распознала и успешно загрузила файл. Функция search_binary_handler возвращает отрицательное значение для указания ошибки, в том числе невозможности найти подходящий обработчик двоичных файлов.
10070: Если циклу, который был начат в строке 10037, не удается найти подходящий обработчик двоичных файлов, эта строка предпринимает попытку загрузить новый двоичный формат, который должен привести к успеху при второй попытке. Следовательно все это входит в двухпроходный цикл, начинающийся в строке 10036.
netlib.narod.ru | < Назад | Оглавление | Далее > |