Readline linux что это

Обновлено: 04.07.2024

Главное достоинство открытого ПО – не в том, что программы распространяются бесплатно, а в том, что мы всегда можем найти примеры хорошего кода, пригодного к использованию в наших собственных проектах (которые, если нам повезет, тоже обогатят сокровищницу Open Source). На протяжении многих лет я копировал интересные фрагменты из исходных текстов разных популярных программ и с форумов, посвященных программированию для Unix. Теперь я делюсь некоторыми рецептами с вами.

Командная строка «как у Bash»

Каждый поклонник Unix знает, что при прочих равных условиях программы с консольным интерфейсом гораздо удобнее, чем все эти «окошечки» и «менюшечки». Шутки – шутками, а командная строка Bash, действительно, очень комфортная. Работу с ней здорово ускоряет завершение имен команд и файлов по нажатию Tab и история ранее введенных команд. Эти две функции Bash настолько удобны и привычны линуксоидам, что их реализуют и многие другие программы: например, завершение по Tab работает в диалогах открытия/сохранения файлов KDE.

Если в вашей программе есть что-то вроде командной строки (или просто строки ввода), есть смысл реализовать указанные возможности – благо, это очень просто. В состав Linux (и многих других систем) входит библиотека GNU Readline, реализующая необходимую функциональность. Собственно, ею пользуется и сам Bash!

Главная функция библиотеки Readline называется readline(). Как нетрудно догадаться, она предназначена для чтения строки текста с терминала. В каче­cтве аргумента функция readline() принимает «приглашение командной строки», отображаемое на экране терминала, а возвращает значение типа char *, указывающее на копию строки, введенной пользователем. Если вы реализуете в своей программе аналог командной строки Bash, пользоваться readline() будет удобнее, чем стандартными функциями библиотеки C. Во-первых, readline() позволяет вам не думать о размере буфера для ввода текста – она будет считывать его до тех пор, пока не будет нажата клавиша Enter, а затем вернет вам строку, содержащую все набранные символы. Стоит, однако, учесть, что алгоритм добавления новых символов заметно замедляет свою работу по мере роста строки, так что я не рекомендовал бы использовать вызов для редактирования цельных текстов. Строка, возвращенная функцией readline(), создана специально для вас, и вы должны высвободить занятую ею память с помощью функции free().

Тексты упомянуты не случайно: readline() предоставляет вам полноценный набор команд редактирования вводимой строки в стиле Emacs. Но и это еще не все. Каждый раз, когда пользователь нажимает клавишу табуляции (или другую спецклавишу, например, Esc), readline() прерывает нормальное выполнение и предоставляет программисту возможность выполнить некоторые действия. В программе shelldemo показано, как можно использовать функцию readline для реализации автозавершения имен команд и имен файлов:

Все функции и переменные, связанные с readline(), объявлены в файле <readline/readline.h> (учтите, что сама библиотека Readline наверняка установлена в вашей системе, а вот заголовочные файлы для нее, скорее всего, придется добавлять).

Функция rl_bind_key() позволяет связать специальное действие с некоторым символом. Ее первый аргумент – символ (‘\t’ для табуляции, ‘\e’ для Esc и так далее), второй – адрес функции, которую следует вызвать в ответ на его ввод. Привязка действий к символам – чрезвычайно мощный механизм, позволяющий существенно расширить возможности функции readline(). В нашем примере мы связываем символ табуляции и функцию rl_complete(). Если теперь в потоке ввода readline() появится ‘\t’, он не будет напечатан, а вместо этого будет вызвана функция rl_complete(), которая выполнит автоматиче­cкое завершение команды и все сопутствующие операции (например, демонстрацию возможных вариантов в случае неоднозначного выбора). Сама rl_complete() работает по довольно сложной схеме, но, в конечном счете, полагается на функцию обратного вызова, адрес которой хранится в переменной rl_completion_entry_function (в нашем примере – centry_func()). Именно она позволяет нам задать свой собственный механизм завершения вводимого текста. Функция centry_func() вызывается несколько раз подряд и возвращает либо очередной вариант завершения переданного текста, либо NULL, если их больше нет. Несколько вызовов нужны потому, что введенный текст в общем случае может быть завершен несколькими способами. В этой ситуации функция readline() не дополняет текст, а показывает все возможные варианты завершения. Далее пользователь должен уточнить свой выбор, чтобы завершение сработало.

Функция centry_func() получает два параметра – текст, который необходимо автоматиче­cки завершить, и параметр состояния, который, попросту говоря, показывает, сколько раз уже вызывалась centry_func(). Функция берет список известных команд из массива command_list и перебирает его элементы, используя статиче­cкую переменную command_index и определяя те коман­ды, первые символы которых совпадают с введенным текстом. Найдя подходящую строку, мы создаем ее копию с помощью функции strdup() и возвращаем указатель на нее. Дальнейшую заботу о выделенной области памяти возьмут на себя функции, вызывающие centry_func(). Обработав все элементы массива, можно было бы вернуть NULL, но мы поступаем иначе и вызываем функцию rl_filename_completion_function() из библиотеки Readline: она выполняет стандартное завершение имен файлов. В результате centry_func() сначала проверяет переданный текст на соответствие именам команд, а затем ищет совпадение в именах файлов. Чтобы последние не путались в нашем примере с командами, их следует начинать со специальных символов – /,./,

/. Нажав Tab в пустой командной строке shelldemo, мы увидим полный список команд (все как положено).

Если в какой-то момент вы захотите, чтобы Tab потерял свое специальное значение, измените привязку символа:

Функция rl_insert() просто добавляет связанный с ней символ в строку.

Обогатить нашу программу историей команд в стиле Bash даже проще, чем реализовать автозавершение. После ввода очередной команды мы добавляем ее в список с помощью функции add_history(), объявленной в <readline/history.h>. Можно спросить, почему введенные команды не добавляются в список истории команд автоматиче­cки? Ответ прост – Readline предоставляет программисту самому решать, какие команды и в какой форме следует сохранять в истории. Наверняка вы не захотите, чтобы в нее попадали введенные пользователем пустые строки (а может, и захотите, кто вас знает?).

Как вы, конечно, знаете, Bash умеет сохранять историю команд в перерывах между сеансами. Readline предоставляет нам простые средства для решения и этой задачи. В начале работы программы shelldemo мы загружаем сохраненную ранее историю команд с помощью функции read_history() из файла .history (его имя передается как аргумент). Если файла .history не существует, read_history() не скажет нам ничего плохого. Точно так же в конце работы программы мы сохраняем историю команд с помощью функции write_history().

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

Поведение Readline можно настроить с помощью внешнего файла конфигурации. Он называется .inputrc и должен располагаться в вашей домашней директории. С помощью .inputrc вы настраиваете поведение сразу всех программ, использующих Readline. На первый взгляд может показаться, что это неудобно, но на самом деле такой унифицированный подход имеет смысл. Разные правила обработки одних и тех же команд для разных программ только создают ненужную путаницу. В файле .inputrc можно присваивать значения переменным, управляющим поведением Readline, связывать известные Readline действия с комбинациями клавиш и даже использовать условные переходы, как в сценариях Bash. Мы, однако, рассмотрим только переменные файла .inputrc – самые интересные из них можно найти ниже.

Переменные GNU Readline

Для присваивания переменным новых значений, в файле .inputrc используется команда set:

Ниже перечислены переменные, которые изменяют поведение изученных нами функций библиотеки GNU Readline:

Как развернуть тильду

Оболочка Bash (и библиотека Readline) умеют преобразовывать символы типа «

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

’. Напомню, что сочетание «

/» в начале имени файла обозначает абсолютный путь к домашней директории текущего пользователя.

username/ разворачивается в абсолютный путь к домашней директории пользователя username, которая вовсе не обязательно выглядит как /home/username/. Задача осложняется еще и тем фактом, что тильда может использоваться в именах файлов и директорий как обычный символ. С помощью mkdir вы можете создать директорию с именем, начинающимся с ‘

’. Система не позволит вам создать локальную директорию

user1, если /home/user1 уже существует, но в обратном порядке (сначала локальную

user1, потом /home/user1) это проделать можно. Возникающая в результате неоднозначность способна запутать даже интерпретатор Bash.

Рассмотрим функцию преобразования строк, содержащих тильду, в полные имена файлов:

Как можно видеть, она написана на C++ и использует тип данных std::string. Для получения директории текущего пользователя используется переменная окружения HOME, а если она не задана – сочетание функций getuid() (возвращает идентификатор текущего пользователя) и getpwuid() (возвращает указатель на структуру passwd, хранящую данные учетной записи пользователя). Имя домашней директории пользователя хранится в поле pw этой структуры. Определение домашней директории пользователя, заданного по имени, выполняется с помощью функции pwnam(), которая также возвращает указатель на структуру passwd. Если функция expand_path() не смогла связать тильду с домашней директорией пользователя, она оставляет ее без изменений. Функцию expand_path() можно использовать непосредственно для преобразования имен файлов, и практика показывает, что обмануть ее невозможно!

Как избавиться от терминала

Функция fork() создает новый процесс, который является копией графиче­cкой программы. Мы завершаем родительский процесс, в котором вызов fork() вернул ненулевое значение, и продолжаем работу в дочернем процессе. Дочерний процесс наследует от родителя все открытые дескрипторы, в том числе дескрипторы стандартных потоков ввода-вывода (по умолчанию они имеют номера 0, 1 и 2). Чтобы наша программа не печатала данных на терминал, с которым она «попрощалась», мы закрываем эти дескрипторы. Но оставлять стандартные потоки ввода-вывода закрытыми нельзя, так как многие функции, в том числе и функции X Window, используют их. Поэтому мы открываем пустое устройство /dev/null для чтения и записи и создаем копии открытого дескриптора с номерами 0, 1 и 2. Используемые нами функции и константы объявлены в заголовочных файлах <stdio.h>, <fcntl.h>, <sys/types.h>, <unistd.h>.

На диске вы найдете пример программы X Window (файл events.c), которая отключается от запустившего ее терминала. Чтобы скомпилировать приложение, скомандуйте:

Как перехватить вызов

Иногда нам бывает нужно контролировать выполняемые программой вызовы библиотечных функций. Если исходные тексты приложения доступны, мы можем просто заменить вызов интересующей нас функции на перехватчик, но доступ к исходным кодам есть не всегда и не у всех. В этом случае нам поможет переменная окружения LD_PRELOAD. С ее помощью мы можем указать имя библиотеки, которая должна быть загружена прежде всех остальных библиотек, используемых приложением (даже раньше, чем библиотека libc). В результате, если в библиотеке-перехватчике определена функция или переменная с именем, которое совпадает с одним из имен, экспортируемых другими библиотеками, программа будет использовать объект из библиотеки-перехватчика вместо одноименного объекта из своей «родной» библиотеки. Очень часто переменная LD_PRELOAD используется для внедрения в программу функций, являющихся обертками для стандартных. Эти функции-обертки выполняют требуемые нам дополнительные действия, а затем вызывают стандартные функции для выполнения основной работы. Однако вызвать в библиотеке-перехватчике стандартную функцию не так просто, как кажется, ведь ее имя совпадает с именем функции-обертки, определенной в той же библиотеке (если не предпринять специальных действий, вместо вызова стандартной функции функция-обертка будет рекурсивно вызывать саму себя). Для решения этой проблемы используется функция dlsym() которая позволяет загрузить функцию, заданную именем, из другого модуля. Вот как, например, может выглядеть обертка для стандартной функции fopen():

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

Следует отметить что данный метод перехвата не сработает, если сама программа использует механизм dlopen()/dlsym() для получения адресов библиотечных функций или же если она скомпонована статиче­cки. Если вы хотите использовать этот метод для перехвата функций из модулей, написанных на C++, то вы должны учесть, что C++ применяет преобразование имен экспортируемых объектов (names mangling) с целью исключить неоднозначность, которая может возникнуть при экспорте перегруженных функций (преобразование имен может быть отключено явным образом, и тогда они будут экспортироваться неизменными). Перед тем как перехватывать функцию, следует проверить, как выглядит ее имя в скомпилированном модуле. Это можно сделать с помощью утилиты nm.

Надеемся, что благодаря этим советам ваши программы для Linux (и не только для Linux) станут лучше и надежнее. Будет время – черкните нам пару строк и расскажите, где вы применили то, что узнали на этом уроке (благодарности в исходных текстах всемирно известных проектов тоже приветствуются!). LXF

Например, при вводе строки с использованием readline, нажатие C-b (Ctrl+B) передвигает курсор на одну позицию назад, тогда как Ctrl+F передвигает курсор на одну позицию вперёд; нажатие Ctrl+R позволяет произвести поиск команд среди ранее введённых; использование этих клавиш пришло из одной из старейших и популярнейших программ проекта GNU — текстового редактора Emacs (описаны назначенные по умолчанию клавиши, но это назначение можно изменить, сделав его подобным применяемому в редакторе vi). Кроме того, readline поддерживает буфер обмена и дополнение имени команды по первым символам при нажатии клавиши Tab ↹. Readline является кросс-платформеной библиотекой, а значит позволяет многим программам сохранить одинаковое поведение при вводе строки пользователем даже при переходе на другую платформу. [1]

Содержание

Основные функции

Библиотека GNU Readline предоставляет набор функций для использования приложениями, которые позволяют пользователям редактировать командные строки, как они набираются. Оба режима редактирования Emacs и VI доступны. Библиотека Readline включает в себя дополнительные функции для поддержания списка ранее введенных командных строк, чтобы вспомнить и, возможно, пересоздать эти строки, а также выполнять CSH-как расширение истории предыдущих команд. [2]

В истории Facilites также помещены в отдельную библиотеку, библиотеку истории, как часть процесса сборки. Библиотека История может быть использована без Readline в приложениях.

Readline является свободным программным обеспечением, распространяется в соответствии с условиями GNU General Public License, версия 3. Это означает, что если вы хотите использовать Readline в программе, которая отпущена или распространяющий кому-либо, программа должна быть бесплатное программное обеспечение и иметь GPL- совместимой лицензией.

Виды доступа

Readline поставляется в стандартной библиотеке на большинстве систем GNU / Linux и FreeBSD. Это также часть коллекции NetBSD пакетов и коллекции пакетов OpenBSD. [3]

Проект OpenPKG делает источник РПМ из Readline-6.3 доступна для различных систем Unix и Linux в качестве базовой части текущего выпуска.

Пользователи MacOS X могут получить пакеты MacOS X для Readline-6.3 из MacPorts, Fink или Homebrew.

Пользователи HP-UX может получить Readline-6.3 пакеты и исходный код программного обеспечения Портирование и архива Центра HP-UX. Это даже доступно на Minix. Если вы работаете в Windows, я рекомендую вам использовать Cygwin, который в настоящее время корабль Readline-6.1 и Readline-6.2 или MinGW, которая в настоящее время имеет пакеты для Readline-5.2.

Конфигурация и примеры

После установки readline полезно будет настроить под себя возможности редактирования в командной строке. Для этого нужно отредактировать один из конфигурационных файлов:

/etc/inputrc — глобальный файл конфигурации для всех пользователей;

/.inputrc — файл конфигурации для отдельных пользователей, хранимый в их домашнем каталоге;

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

Формат файла конфигурации

Интересные примеры макросов

В файлах конфигурации помимо простого переназначения функций редактирования новым «клавиатурным сочетаниям» можно создать достаточно нетривиальные макросы для выполнения специальных задач. Вот несколько примеров:

Нетривиальные примеры

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

Документация

В документации для библиотек Readline и история появляется в подкаталоге `док '. Есть три Texinfo файлов и два Unix-стиль руководства страницы, описывающие объекты, доступные в Readline и библиотеках истории. Файлы Texinfo включают в себя как пользователя, так и руководства программиста. Нынешние руководства являются:

  • Readline библиотека GNU
  • История Библиотека GNU
  • GNU Readline интерфейс пользователя

Установка и закачка

Текущая версия Readline является Readline-7.0. (GPG подпись).

Загружаемый файл текущей версии со всеми официальными патчами применяемых доступен из репозитория GNU. [5]

Readline — библиотека GNU Project, которую Bash и другие программы с CLI-интерфейсом используют для взаимодействия с командной строкой. Подробнее см. readline(3) .

Contents

Установка

Вероятнее всего, пакет readline уже установлен как зависимость Bash.

Режим редактирования

По умолчанию Readline использует для взаимодействия с командной строкой комбинации клавиш в стиле Emacs. Чтобы выбрать стиль vi, добавьте следующую строку в файл

Если вы хотите задать этот режим только для Bash, отредактируйте

Индикатор режима в приглашении

Редактирование в стиле vi имеет два режима: командный и вставки. Настроить отображение индикатора режима можно следующей опцией:

Теперь в вашем приглашении будет отображаться строка-индикатор (по умолчанию — (cmd) / (ins) ), которую можно модифицировать переменными vi-ins-mode-string и vi-cmd-mode-string .

Разные формы курсора для режимов

В виртуальной консоли используются другие коды управляющих последовательностей, так что сначала необходимо проверить, какой терминал используется:

Быстрое перемещение по словам

Xterm поддерживает перемещение по словам клавишами Ctrl+Left и Ctrl+Right по умолчанию. Чтобы настроить этот эффект в других эмуляторах, выберите подходящие коды терминала и присвойте их переменным backward-word и forward-word в файле

История команд

Обычно нажатие клавиши "стрелка вверх" выводит последнюю команду вне зависимости от того, что было (частично) введено на данный момент. Более практично будет искать только те прошлые команды, которые совпадают с текущим вводом.

Например, пользователь вводил следующие команды:

  • ls /usr/src/linux-2.6.15-ARCH/kernel/power/Kconfig
  • who
  • mount
  • man mount

Если в этой ситуации набрать ls и нажать стрелку, текущий ввод будет заменён на man mount , последнюю выполненную команду. Если поиск по истории включён, то показываться будут только прошлые команды, начинающиеся с ls (текущий ввод), в данном случае — ls /usr/src/linux-2.6.15-ARCH/kernel/power/Kconfig .

Включить режим поиска по истории можно следующими строками в /etc/inputrc или

Если вы используете режим vi, то добавьте следующие строки в

Если вы настраивали файл

/.inputrc , то рекомендуется добавить в него ещё одну строку, чтобы избежать странных вещей вроде этой:

В качестве альтернативы можно использовать инкрементный поиск по комбинации Ctrl+R . В этом случае поиск будет вестись не по последним командам, а по похожим. Повторное нажатие Ctrl+R при работе в этом режиме отобразит предыдущую строку в буфере, совпадающую с текущей строкой поиска, а Ctrl+G отменит поиск и восстановит текущий ввод. Так, чтобы поиск шёл среди ранее выполненных команд mount , нажмите Ctrl+R , введите 'mount' и продолжайте нажимать Ctrl+R , пока не дойдёте до нужной команды.

Обратный (вперёд) эквивалент этого режима называется forward-search-history и привязан к комбинации Ctrl+S . Правда, в большинстве терминалов комбинация Ctrl+S переопределена на приостановку выполнения програмы до нажатия Ctrl+Q (т.н. управление выполнением XON/XOFF). Для активации forward-search-history либо отключите управление потоком выполнения:

либо назначьте в inputrc другую комбинацию. Например, можно использовать Alt+S :

Быстрое завершение

При автодополнении (tab completion) одиночное нажатие клавиши tab пытается дополнить команду. Если частичное дополнение невозможно, двукратное нажатие покажет все возможные завершения.

Можно изменить двойное нажатие на одиночное:

Либо же можно настроить, чтобы такое одиночное нажатие выполняло оба действия: дополнение слова и показ всех возможных завершений, если полное дополнение невозможно:

Цветное завершение

Включить цветное отображение имён файлов можно опцией colored-stats . При желании можно настроить подсветку одинакового префикса в списке возможных завершений опцией colored-completion-prefix . Например:

Макросы

Readline позволяет привязывать клавиатурные макросы на клавиши. Например, выполните эту команду в Bash:

или добавьте строку внутри одиночных кавычек в inputrc:

Макросы позволяют автоматизировать часто используемые идиомы. Например, добавление "| less" к строке и "нажатие" Ctrl+M (эквивалент Enter ) по комбинации Ctrl+Alt+L :

Добавление префикса 'yes |' по нажатию Ctrl+Alt+Y , в результате чего программа получит утвердительный ответ на любой вопрос "да/нет":

Добавление префикса sudo по нажатию Alt+S . Несколько безопаснее, чем предыдущий пример, потому что не происходит нажатия Enter :

Наконец, быстрый перевод программы в фоновый режим выполнения с удалением всего вывода по комбинации Ctrl+Alt+B .

Отключение отображения клавиши control

Readline выдаёт в терминал строку ^C при нажатия Ctrl+C . Чтобы это отключить, просто добавьте следующие строки в

В языке C для осуществления файлового ввода-вывода используются механизмы стандартной библиотеки языка, объявленные в заголовочном файле stdio.h. Как вы вскоре узнаете консольный ввод-вывод - это не более чем частный случай файлового ввода-вывода. В C++ для ввода-вывода чаще всего используются потоковые типы данных. Однако все эти механизмы являются всего лишь надстройками над низкоуровневыми механизмами ввода-вывода ядра операционной системы.

С точки зрения модели КИС (Клиент-Интерфейс-Сервер), сервером стандартных механизмов ввода вывода языка C (printf, scanf, FILE*, fprintf, fputc и т. д.) является библиотека языка. А сервером низкоуровневого ввода-вывода в Linux, которому посвящена эта глава книги, является само ядро операционной системы.

Пользовательские программы взаимодействуют с ядром операционной системы посредством специальных механизмов, называемых системными вызовами (system calls, syscalls). Внешне системные вызовы реализованы в виде обычных функций языка C, однако каждый раз вызывая такую функцию, мы обращаемся непосредственно к ядру операционной системы. Список всех системных вызовов Linux можно найти в файле /usr/include/asm/unistd.h. В этой главе мы рассмотрим основные системные вызовы, осуществляющие ввод-вывод: open(), close(), read(), write(), lseek() и некоторые другие.

5.2. Файловые дескрипторы

В языке C при осуществлении ввода-вывода мы используем указатель FILE*. Даже функция printf() в итоге сводится к вызову vfprintf(stdout. ), разновидности функции fprintf(); константа stdout имеет тип struct _IO_FILE*, синонимом которого является тип FILE*. Это я к тому, что консольный ввод-вывод - это файловый ввод-вывод. Стандартный поток ввода, стандартный поток вывода и поток ошибок (как в C, так и в C++) - это файлы. В Linux все, куда можно что-то записать или откуда можно что-то прочитать представлено (или может быть представлено) в виде файла. Экран, клавиатура, аппаратные и виртуальные устройства, каналы, сокеты - все это файлы. Это очень удобно, поскольку ко всему можно применять одни и те же механизмы ввода-вывода, с которыми мы и познакомимся в этой главе. Владение механизмами низкоуровневого ввода-вывода дает свободу перемещения данных в Linux. Работа с локальными файловыми системами, межсетевое взаимодействие, работа с аппаратными устройствами, - все это осуществляется в Linux посредством низкоуровневого ввода-вывода.

Вы уже знаете из предыдущей главы, что при запуске программы в системе создается новый процесс (здесь есть свои особенности, о которых пока говорить не будем). У каждого процесса (кроме init) есть свой родительский процесс (parent process или просто parent), для которого новоиспеченный процесс является дочерним (child process, child). Каждый процесс получает копию окружения (environment) родительского процесса. Оказывается, кроме окружения дочерний процесс получает в качестве багажа еще и копию таблицы файловых дескрипторов.

Файловый дескриптор (file descriptor) - это целое число (int), соответствующее открытому файлу. Дескриптор, соответствующий реально открытому файлу всегда больше или равен нулю. Копия таблицы дескрипторов (читай: таблицы открытых файлов внутри процесса) скрыта в ядре. Мы не можем получить прямой доступ к этой таблице, как при работе с окружением через environ. Можно, конечно, кое-что "вытянуть" через дерево /proc, но нам это не надо. Программист должен лишь понимать, что каждый процесс имеет свою копию таблицы дескрипторов. В пределах одного процесса все дескрипторы уникальны (даже если они соответствуют одному и тому же файлу или устройству). В разных процессах дескрипторы могут совпадать или не совпадать - это не имеет никакого значения, поскольку у каждого процесса свой собственный набор открытых файлов.

Возникает вопрос: сколько файлов может открыть процесс? В каждой системе есть свой лимит, зависящий от конфигурации. Если вы используете bash или ksh (Korn Shell), то можете воспользоваться внутренней командой оболочки ulimit, чтобы узнать это значение. Если вы работаете с оболочкой C-shell (csh, tcsh), то в вашем распоряжении команда limit:

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

Таблица дескрипторов, помимо всего прочего, содержит информацию о текущей позиции чтения-записи для каждого дескриптора. При открытии файла, позиция чтения-записи устанавливается в ноль. Каждый прочитанный или записанный байт увеличивает на единицу указатель текущей позиции. Мы вернемся к этой теме в разделе 5.7.

5.3. Открытие файла: системный вызов open()

Чтобы получить возможность прочитать что-то из файла или записать что-то в файл, его нужно открыть. Это делает системный вызов open(). Этот системный вызов не имеет постоянного списка аргументов (за счет использования механизма va_arg); в связи с этим существуют две "разновидности" open(). Не только в С++ есть перегрузка функций ;-) Если интересно, то о механизме va_arg можно прочитать на man-странице stdarg (man 3 stdarg) или в книге Б. Кернигана и Д. Ритчи "Язык программирования Си". Ниже приведены адаптированные прототипы системного вызова open().

Системный вызов open() объявлен в заголовочном файле fcntl.h. Ниже приведен общий адаптированный прототип open().

Начнем по порядку. Первый аргумент - имя файла в файловой системе в обычной форме: полный путь к файлу (если файл не находится в текущем каталоге) или сокращенное имя (если файл в текущем каталоге).

Второй аргумент - это режим открытия файла, представляющий собой один или несколько флагов открытия, объединенных оператором побитового ИЛИ. Список доступных флагов приведен в Таблице 4 Приложения 2.. Наиболее часто используют только первые семь флагов. Если вы хотите, например, открыть файл в режиме чтения и записи, и при этом автоматически создать файл, если такового не существует, то второй аргумент open() будет выглядеть примерно так: O_RDWR|O_CREAT. Константы-флаги открытия объявлены в заголовочном файле bits/fcntl.h, однако не стоит включать этот файл в свои программы, поскольку он уже включен в файл fcntl.h.

Третий аргумент используется в том случае, если open() создает новый файл. В этом случае файлу нужно задать права доступа (режим), с которыми он появится в файловой системе. Права доступа задаются перечислением флагов, объединенных побитовым ИЛИ. Вместо флагов можно использовать число (как правило восьмиричное), однако первый способ нагляднее и предпочтительнее. Список флагов приведен в Таблице 1 Приложения 2. Чтобы, например, созданный файл был доступен в режиме "чтение-запись" пользователем и группой и "только чтение" остальными пользователями, - в третьем аргументе open() надо указать примерно следующее: S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH или 0664. Флаги режима доступа реально объявлены в заголовочном файле bits/stat.h, но он не предназначен для включения в пользовательские программы, и вместо него мы должны включать файл sys/stat.h. Тип mode_t объявлен в заголовочном файле sys/types.h.

Если файл был успешно открыт, open() возвращает файловый дескриптор, по которому мы будем обращаться к файлу. Если произошла ошибка, то open() возвращает -1. Позже, в последующих главах книги мы научимся распознавать ошибки системных вызовов.

5.4. Закрытие файла: системный вызов close()

Системный вызов close() закрывает файл. Вообще говоря, по завершении процесса все открытые файлы (кроме файлов с дескрипторами 0, 1 и 2) автоматически закрываются. Тем не менее, это не освобождает нас от самостоятельного вызова close(), когда файл нужно закрыть. К тому же, если файлы не закрывать самостоятельно, то соответствующие дескрипторы не освобождаются, что может привести к превышению лимита открытых файлов. Простой пример: приложение может быть настроено так, чтобы каждую минуту открывать и перечитывать свой файл конфигурации для проверки обновлений. Если каждый раз файл не будет закрываться, то в моей системе, например, приложение может "накрыться медным тазом" примерно через 17 часов. Автоматически! Кроме того, файловая система Linux поддерживает механизм буферизации. Это означает, что данные, которые якобы записываются, реально записываются на носитель (синхронизируются) только через какое-то время, когда система сочтет это правильным и оптимальным. Это повышает производительность системы и даже продлевает ресурс жестких дисков. Системный вызов close() не форсирует запись данных на диск, однако дает больше гарантий того, что данные останутся в целости и сохранности.

Системный вызов close() объявлен в файле unistd.h. Ниже приведен его адаптированный прототип.

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

Теперь можно написать простенкую программу, использующую системные вызовы open() и close(). Мы еще не умеем читать из файлов и писать в файлы, поэтому напишем программу, которая создает файл с именем, переданным в качестве аргумента (argv[1]) и с правами доступа 0600 (чтение и запись для пользователя). Ниже приведен исходный код программы.

Обратите внимание, если запустить программу дважды с одним и тем же аргументом, то на второй раз open() выдаст ошибку. В этом виноват флаг O_EXCL (см. Таблицу 4 Приложения 2), который "дает добро" только на создание еще не существующих файлов. Наглядности ради, флаги открытия и флаги режима мы занесли в отдельные переменные, однако можно было бы сделать так: Или так:

5.5. Чтение файла: системный вызов read()

Системный вызов read(), объявленный в файле unistd.h, позволяет читать данные из файла. В отличие от библиотечных функций файлового ввода-вывода, которые предоставляют возможность интерпретации считываемых данных. Можно, например, записать в файл следующее содержимое:

Теперь, используя библиотечные механизмы, можно читать файл по-разному:

Системный вызов read() читает данные в "сыром" виде, то есть как последовательность байт, без какой-либо интерпретации. Ниже представлен адаптированный прототип read().

Первый аргумент - это файловый дескриптор. Здесь больше сказать нечего. Второй аргумент - это указатель на область памяти, куда будут помещаться данные. Третий аргумент - количество байт, которые функция read() будет пытаться прочитать из файла. Возвращаемое значение - количество прочитанных байт, если чтение состоялось и -1, если произошла ошибка. Хочу заметить, что если read() возвращает значение меньше count, то это не символизирует об ошибке.

Хочу сказать несколько слов о типах. Тип size_t в Linux используется для хранения размеров блоков памяти. Какой тип реально скрывается за size_t, зависит от архитектуры; как правило это unsigned long int или unsigned int. Тип ssize_t (Signed SIZE Type) - это тот же size_t, только знаковый. Используется, например, в тех случаях, когда нужно сообщить об ошибке, вернув отрицательный размер блока памяти. Системный вызов read() именно так и поступает.

Теперь напишем программу, которая просто читает файл и выводит его содержимое на экран. Имя файла будет передаваться в качестве аргумента (argv[1]). Ниже приведен исходный код этой программы.

В этом примере используется укороченная версия open(), так как файл открывается только для чтения. В качестве буфера (второй аргумент read()) мы передаем адрес переменной типа char. По этому адресу будут считываться данные из файла (по одному байту за раз) и передаваться на стандартный вывод. Цикл чтения файла заканчивается, когда read() возвращает нуль (нечего больше читать) или -1 (ошибка). Системный вызов close() закрывает файл.

Как можно заметить, в нашем примере системный вызов read() вызывается ровно столько раз, сколько байт содержится в файле. Иногда это действительно нужно; но не здесь. Чтение-запись посимвольным методом (как в нашем примере) значительно замедляет процесс ввода-вывода за счет многократных обращений к системным вызовам. По этой же причине возрастает вероятность возникновения ошибки. Если нет действительной необходимости, файлы нужно читать блоками. О том, какой размер блока предпочтительнее, будет рассказано в последующих главах книги. Ниже приведен исходный код программы, которая делает то же самое, что и предыдущий пример, но с использованием блочного чтения файла. Размер блока установлен в 64 байта.

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

5.6. Запись в файл: системный вызов write()

Для записи данных в файл используется системный вызов write(). Ниже представлен его прототип.

Как видите, прототип write() отличается от read() только спецификатором const во втором аргументе. В принципе write() выполняет процедуру, обратную read(): записывает count байтов из буфера buffer в файл с дескриптором fd, возвращая количество записанных байтов или -1 в случае ошибки. Так просто, что можно сразу переходить к примеру. За основу возьмем программу myread1 из предыдущего раздела.

В этом примере нам уже не надо изощеряться в попытках вставить нуль-терминатор в строку для записи, поскольку системный вызов write() не запишет большее количество байт, чем мы ему указали. В данном случае для демонстрации write() мы просто записывали данные в файл с дескриптором 1, то есть в стандартный вывод. Но прежде, чем переходить к чтению следующего раздела, попробуйте самостоятельно записать что-нибудь (при помощи write(), естественно) в обычный файл. Когда будете открывать файл для записи, обратите пожалуйста внимание на флаги O_TRUNC, O_CREAT и O_APPEND. Подумайте, все ли флаги сочетаются между собой по смыслу.

5.7. Произвольный доступ: системный вызов lseek()

Как уже говорилось, с каждым открытым файлом связано число, указывающее на текущую позицию чтения-записи. При открытии файла позиция равна нулю. Каждый вызов read() или write() увеличивает текущую позицию на значение, равное числу прочитанных или записанных байт. Благодаря этому механизму, каждый повторный вызов read() читает следующие данные, и каждый повторный write() записывает данные в продолжение предыдущих, а не затирает старые. Такой механизм последовательного доступа очень удобен, однако иногда требуется получить произвольный доступ к содержимому файла, чтобы, например, прочитать или записать файл заново.

Для изменения текущей позиции чтения-записи используется системный вызов lseek(). Ниже представлен его прототип.

Первый аргумент, как всегда, - файловый дескриптор. Второй аргумент - смещение, как положительное (вперед), так и отрицательное (назад). Третий аргумент обычно передается в виде одной из трех констант SEEK_SET, SEEK_CUR и SEEK_END, которые показывают, от какого места отсчитывается смещение. SEEK_SET - означает начало файла, SEEK_CUR - текущая позиция, SEEK_END - конец файла. Рассмотрим следующие вызовы:

Первый вызов устанавливает текущую позицию в начало файла. Второй вызов смещает позицию вперед на 20 байт. В третьем случае текущая позиция перемещается на 10 байт назад относительно конца файла.

В случае удачного завершения, lseek() возвращает значение установленной "новой" позиции относительно начала файла. В случае ошибки возвращается -1.

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

Теперь разберемся, как работает эта программа. Изначально "полотно" заполняется пробелами. Функция init_draw() построчно записывает в файл пробелы, чтобы получился "холст", размером N_ROWS на N_COLS. Массив строк icode в функции main() - это набор команд рисования. Команда начинается с одной из трех литер: 'v' - нарисовать вертикальную линию, 'h' - нарисовать горизонтальную линию, 'p' - нарисовать точку. После каждой такой литеры следуют три числа. В случае вертикальной линии первое число - фиксированная координата X, а два других числа - это начальная и конечная координаты Y. В случае горизонтальной линии фиксируется координата Y (первое число). Два остальных числа - начальная координата X и конечная координата X. При рисовании точки используются только два первых числа: координата X и координата Y. Итак, функция draw_vline() рисует вертикальную линию, функция draw_hline() рисует горизонтальную линию, а draw_point() рисует точку.

Функция init_draw() пишет в файл N_ROWS строк, каждая из которых содержит N_COLS пробелов, заканчивающихся переводом строки. Это процедура подготовки "холста".

Функция draw_point() вычисляет позицию (исходя из значений координат), перемещает туда текущую позицию ввода-вывода файла, и записывает в эту позицию символ (FG_CHAR), которым мы рисуем "картину".

Функция draw_hline() заполняет часть строки символами FG_CHAR. Так получается горизонтальная линия. Функция draw_vline() работает иначе. Чтобы записать вертикальную линию, нужно записывать по одному символу и каждый раз "перескакивать" на следующую строку. Эта функция работает медленнее, чем draw_hline(), но иначе мы не можем.

Полученное изображение записывается в файл image. Будьте внимательны: чтобы разгрузить исходный код, из программы исключены многие проверки (read(), write(), close(), диапазон координат и проч.). Попробуйте включить эти проверки самостоятельно.

Читайте также: