Очень приятно читать отзывы на мои статьи. Интересно узнать о собственной вредности (смею вас уверить, что редактор строго следит за мерой моей «вредности» и немедленно исключает особо «страшные» примеры, отговариваясь нехваткой места) и о том, что занимаюсь отпугиванием новичков от Unix, плохо о нем думаю, выражаюсь и рассказываю исключительно о его ошибках. Это не так, я не думаю, что Unix плохо продуман, безобразно устроен и является одной большой ошибкой.
Очередная заметка нашего гуру Игоря Облакова, ведущего постоянную рубрику, посвященную деталям функционирования ОС Unix и Unix-подобных систем, продолжает начатое в предыдущих номерах обсуждение подводных камней и тонкостей организации ядра операционной системы, а также рассказывает о премудростях командного языка |
Это основной инструмент, который я использую, и всякий раз, после того как мне приходится сталкиваться с другими системами, я с радостью возвращаюсь в Unix. Можно даже сказать, что Unix — моя любимая игрушка. Ну, с этим сами знаете, как бывает: «Уронили мишку на пол, оторвали мишке лапу...» (предположительно из фундаментального труда А. Барто «Воспитание молодых хакеров» - И.О.) - главное же, как вы помните, потом.
Вопрос на самом деле в другом. На нашем книжном рынке появилось достаточно много литературы о Unix и Linux. Часть ее действительно интересна и полезна. Как правило, книги содержат изложение «положительного» опыта работы - как надо делать. Но необходимо знать и некоторые типичные ошибки, подводные камни и т.п., проявляя любопытство в стиле «а что если...». Мне же в определенном плане «повезло»: часто приходилось разбираться с «непонятными» ситуациями, коллекционировать потрясающие ошибки, решения и т.п. Можно считать, что я ориентируюсь в первую очередь на «начинающих администраторов», хотя статьи читают и новички. К сожалению, новички обычно просто не представляют, с чего им начать и не могут корректно сформулировать вопрос. Однако попробуем представить и «положительный», мирный опыт. Что может быть более мирным, чем река...
Можно ли войти в одну реку дважды
Рассмотрим тривиальную задачу: внутри командного файла shell (например, bash в Linux) записать в переменную abc последний из заданных при вызове параметров (в предположении, что он наличествует). Попробуем проделать это различными способами.
Попытка первая. Вспоминаем, что для ссылки на некоторый параметр (например, первый) можно использовать запись вида ${1}, а $# - количество параметров.
abc=${$#}
К сожалению, этот вариант не проходит. Проблема заключается в том, что просмотр для подстановки переменных делается однократно. Хотелось бы, чтобы сначала была выполнена подстановка для $#, а затем результат использовался бы для ${}. Есть ли исключения из этого правила? В принципе, да. Таким исключением является подстановка результата команды в виде $(команда). В собственной практике мне не приходило в голову так уж стараться сэкономить на строках, но подобного рода вопросами я сталкивался. Вот, например, как может выглядеть попытка «красиво» выдать информацию о текущем каталоге вместе с ее именем:
echo «<< « $(ls -ld $(pwd)) « >>»
Попытка вторая. Обращаемся к команде знатоков «Что? Где? Когда?» и получаем такой совет: переменная $_ содержит последний параметр предыдущей команды (будьте бдительны, изучите описание более внимательно). В качестве предыдущей можно использовать команду «:», которая просто подставляет свои аргументы. Итого получаем:
: $* abc=$_
Попробуйте переформулировать вопрос «как получить предпоследний параметр» и обратитесь к тому источнику знаний.
Рекомендую читателям убедиться в том, что данный командный файл, в принципе, работает, но содержит ошибку. Его более правильный вариант требует использования не $*, а «$@».
Третья попытка. Классика. Ну, что поделать? Попробуем цикл.while [ $# -gt 1 ] do shift done abc=$1
В данном случае мы потеряли все параметры. Это, пожалуй, перебор. Однако, если нам надо по пути обработать все остальные параметры..., подобное решение и в самом деле является почти классическим.
Четвертая попытка. Пускаемся во все тяжкие и вспоминаем, что существуют функции и сдвиг на нужное число параметров. Сдвиг на $# несколько больше того, что нам хотелось бы, но мы можем добавить этот параметр в начало.
last_par() { shift $1 echo $1 } abc=`last_par $# $*`
В этом случае у нас нет проблем с потерей параметров; сдвиг выполняется для локальных параметров функции.
Пятая попытка. Решение для гуру. Именно в епархию гуру почему-то была отнесена команда eval в одной из книг. Вам не страшно?
eval abc=$$#
eval требует повторного просмотра данной команды. Например, если у нас было 4 параметра, то перед вторым просмотром знак будет убран и будет выполнена команда
abc=$4Именно этого мы и хотели. Если нас интересует предпоследний параметр, следует воспользоваться комбинацией:
eval abc=$$(($#-1))или
eval abc=$`expr $# - 1`
Какого рода подстановки еще способен проделывать командный интерпретатор, например, bash в Linux? Возможны подстановки для псевдонимов, обработка кавычек («?), обработка групп, подстановка параметров и переменных, команд, арифметических выражений, подстановка имен файлов, перенаправления. Заметим, что существенен и порядок подстановок.
Пример работы с группами (проверьте, может ли это ваш вариант shell): mv abc.{f,c} — переименование файлаЭту процедуру нельзя сделать массовой, скажем, попытаться изменить расширения для всех файлов начинающихся с a:
mv a*.{f,c}
Проблема состоит в том, что сначала произойдет подстановка группы, а затем расширение имен файлов. Приведенная команда эквивалентна следующей:
mv a*.f a*.cПопробуйте представить себе возможные варианты результатов выполнения такой команды в зависимости от наполнения текущего каталога.
Весьма забавен следующий вид подстановки (в некоторых диалектах может не работать):
cat <(echo a) xyz <(echo b) tar -cvf >(...) /usrВ этой команде cat использует временно созданные файлы (или конвейеры) для передачи данных от одной команды к другой (подставьте еще один echo вместо cat и посмотрите на результат - это тоже любопытно). В принципе, эти команды можно переписать и по-другому:
echo a; cat xyz; echo b tar -cvf - /usr | ....
Однако последний вариант существенно отличается структурой дерева процессов, что может отразиться на возможностях работы с текущим контекстом.
«Погружение» в ядро продолжается
Рассмотрим, как ядро Linux работает с различными устройствами. Для упрощения опустим начала маршрутных имен файлов (/usr/src/linux).
Как известно, устройства в Unix бывают двух типов: блочные и «символьные» (или «сырые», не буферизованные). Основное отличие между ними состоит именно в использовании или неиспользовании буфера. Однако, это не вся разница между ними. Что можно делать с блочными устройствами? Буферизуемые устройства похожи на пай-мальчиков, которые никогда не позволят себе каких-либо шалостей (типа изменения размера физического сектора/записи на ходу, перемотки и т.п.), все, что разрешается делать этим устройствам, это - открываться, закрываться, читать/писать блок. Подобные строгости необходимы, чтобы эти шалости не мешали системе буферизовать ввод/вывод к устройствам. В процессе «буферизации» система имеет полное право менять порядок выполнения операции так, как ей покажется более удобным, например, с точки зрения производительности.
Если же какие-либо «шалости» все же необходимы, то вам необходимо отказаться от буферизации и работать с «сырым» устройством напрямую. Что может потребоваться «нормальному» процессу от «сырого» устройства: переустановка скорости последовательного порта, перемотка ленты в конец или начало, установка требуемого диска в библиотеке, перенос конкретного экстента на диске (как часть процесса дефрагментации в некоторых файловых системах) и т.п. Здесь перечислены управляющие операции разных уровней сложности, но все они используют общий вызов для работы с устройством - ioctl. Этот вызов в большинстве систем можно использовать только для небуферизуемых устройств.
Для дисков обычно существуют оба варианта доступа: к диску можно обратиться и как к буферизуемому (что за редкими исключениями и используется при работе с файловыми системами), и как к небуферизуемому. Возможность работы с небуферизуемым устройством позволяет выполнять специальные виды операций, например, опрос характеристик диска (имя, размер физического блока, размер диска и т.п.), форматирование и т.п.
В некоторых системах для создания файловой системы необходимо указывать «сырое» устройство, а для монтирования — блочное, что необходимо просто для того, чтобы запрос создания файловой системы мог узнать размер устройства. (В ряде систем оба задействуемых устройства - блочные.) Наличие двух путей доступа к одному носителю автоматически порождает проблемы. Например, что произойдет, если одновременно открыть оба файла устройства и попытаться выполнить запись? Какая именно запись дойдет до диска последней? Это полностью определяется деталями механизма буферизации.
Но вернемся к Linux. Его создатели поступили достаточно просто и решили, что все файлы вне зависимости от типа (обычный файл/блочное устройство/конвейер/...) описываются набором допустимых операций file_operations из файла include/linux/fs.h. Что входит в этот набор?
Для определения особенностей функционирования файла (а устройства являются разновидностью файлов) необходимо задать операции (не обязательно все - это-то и определяется типом): позиционирования, чтения, записи, выбора (реализация операции select), чтения каталога, управления (ioctl), отображения на память (mmap), открытия, освобождения, синхронизация (sync, то есть сброс буферов), поддержка fcntl, процедуры для проверки и обработки смены носителя.
В других операционных системах этот набор разбивается на два различных, соответственно один для блочных устройств, один для символьных и ограничиваются операциями для инициализации драйвера, открытия/закрытия устройства, чтения/записи данных, выполнения ioctl (для символьных), иногда дополнительный вход для select (обычно через ioctl) и т.д.
Более широкий набор операций для устройства иногда оказывается удобен. Посмотрите, например, drivers/char/fbmem.c - драйвер для кадрового буфера. В нем вы найдете процедуру mmap - это кажется вполне логичным: зачем работать с графическим буфером посредством каких-то там read/write.
Как выбирается процедура, которую надо выполнить? Обычно в системе имеются таблицы для блочных и символьных устройств с набором операций. В Linux описание этих таблиц (они называются chrdevs и blkdevs) находится в fs/devices.c. Обращение к конкретной операции выполняется достаточно просто. В Linux получить список действий по старшему («мажору») и младшему («минору») номерам файла устройства можно при помощи процедуры get_fops. Как правило, это прямое обращение к таблице устройств по значению старшего номера, значение которого выступает за определение типа и непосредственно за набор доступных операций. Младший номер в большинстве случаев указывает на конкретное устройство в рамках заданного типа - это в некотором смысле адрес устройства.
Например, «минор» может быть составлен из номера контроллера, номера носителя и номера раздела (см. include/linux/blk.h). Если же заглянуть в файл drivers/char/console.c, то там «минор «ссылается на номер копии консоли. Минор не обязан быть столь уж «тривиальным». Иногда разработчики навешивают определенные характеристики устройства в виде части бит (как дополнение к адресу). В Linux самый «навернутый» пример подобного вида можно найти в файле include/linux/tpqic02.h; здесь среди бит есть признак перемотки ленты после выполнения операции, биты плотности, отладки и даже специальный «минор» для сброса. В большинстве систем чаще всего это происходит с последовательными портами. Иногда минор одного устройств является мажором другого. Таким образом можно, например, клонировать устройства — в мире Unix запрета на это еще нет.
Не замерзли? Всплываем. Посмотрим еще на некоторые любопытные моменты в поведении программ при работе с устройствами и файлами. Замечали ли вы разницу в выполнении следующих команд:
ls ls | more
Во втором случае пользователь получает возможность управления выдачей информации на экран. Однако самое интересное не в этом. Просто ls выдает список файлов в несколько колонок. ls|more выдает список файлов в одну колонку. В какой момент и кто проделывает этот фокус?
В роли фокусника выступает сам ls (и, в какой-то степени, другие программы). ls анализирует файл, используемый для вывода. Если этот файл оказывается терминалом, ls выдает имена в несколько колонок. Если же это конвейер или файл другого типа, то включается собственная буферизация программы, что приводит к изменению режима вывода. В случае, когда вывод идет на терминал, большинство программ использует минимальную буферизацию для того, чтобы пользователь мог следить за поведением программы (а не копить досаду на системного администратора) и не паниковать, если же вывод идет не на терминал, то используется буфер в несколько килобайт. Если вы занимаетесь отладкой программы (командного файла), возникает желание «остановить мгновение» и посмотреть и/или даже записать диагностику в файл. Например:
.... 2>&1 | tee all.res
если программа слева от конвейера завершается аварийно, то буфер с ошибкой может просто «не добежать» до программы tee и сгинуть в памяти аварийного процесса. Это та самая ситуация, в которой может помочь отказ от услуг tee и заказ большого количества строк хранения в эмуляторе терминала, поскольку в этом случае буфер программы будет меньше. Есть здесь и другая интересная проблема. Что будет, если процесс слева нормально закончился, но в процессе работы запустил какой-то специальный параллельный процесс (например, лицензионный менеджер)? Команда tee не сможет закончиться, так как конвейер не будет закрыт с другой стороны.
Какие материалы вам более интересны - «мирные» или о UNIX-граблях, дабы бледнолицый не наступал на них?