Linux. Системное программирование.
Данная книга рассказывает о системном программировании в Linux. Системное программирование — это практика написания системного ПО, низкоуровневый код которого взаимодействует непосредственно с ядром и основными системными библиотеками. Иными словами, речь далее пойдет в основном о системных вызовах Linux и низкоуровневых функциях, в частности тех, которые определены в библиотеке C. Есть немало пособий, посвященных системному программированию для UNIX-систем, но вы почти не найдете таких, которые рассматривают данную тему достаточно подробно и фокусируются именно на Linux. Еще меньше подобных книгучитывают новейшие релизы Linux и продвинутые интерфейсы, ориентированные исключительно на Linux. Эта книга не только лишена всех перечисленных недостатков, но и обладает важным достоинством: дело в том, что я написал массу кода для Linux, как для ядра, так и для системных программ, расположенных непосредственно «над ядром». На самом деле я реализовал на практике ряд системных вызовов и других функций, описанных далее. Соответственно книга содержит богатый материал, рассказывая не только о том, как должны работать системные интерфейсы, но и о том, как они действительно работают и как вы сможете использовать их с максимальной эффективностью. Таким образом, данная книга одновременно является и руководством по системному программированию для Linux, и справочным пособием, описывающим системные вызовы Linux, и подробным повествованием о том, как создавать более интеллектуальный и быстрый код. Текст написан простым, доступным языком. Независимо от того, является ли создание системного кода вашей основной работой, эта книга научит полезным приемам, которые помогут вам стать по-настоящему высокопрофессиональным программистом.
Низкоуровневое программирование для linux
Здравствуйте, уважаемые подписчики. В этом выпуске мы начинаем новую очень важную тему: низкоуровневый ввод-вывод в Linux.
Все вопросы, касающиеся рассылки или проекта Lindevel.Ru направляйте на мой новый адрес: nn@lindevel.ru или в форум на сайте Lindevel.Ru (http://www.lindevel.ru).
1. Низкоуровневый ввод/вывод
1.1. Что значит «низкоуровневый»?
До сих пор для чтения/записи файлов мы использовали библиотечный набор функций. Для языка C это были функции fopen(), fclose(), fputc(), fgetc(), fprintf(), printf() и т. д. В языке C++ мы пользовались поточными типами cout, cerr, cin, fstream, ifstream, ofstream и т. п. Все эти механизмы достаточно удобны и переносимы, однако как настоящие Linux-программисты мы должны понять, как же устроен механизм ввода/вывода в Linux (и в других Unix-системах), что скрывается за библиотечными интерфейсами.
За всем этим безобразием (читай: разнообразием) скрывается несколько системных вызовов. Как я уже объяснял, системные вызовы — это не простые функции, а механизмы, реализованные в ядре операционной системы. Мы уже знаем несколько системных вызовов, связанных с многозадачностью — это fork(), семейство exec () и семейство wait(). В этом выпуске мы научимся открывать, закрывать, читать и писать файлы, обращаясь непосредственно к ядру.
Непосредственное использование системных вызовов для работы с файлами в Linux — это и есть низкоуровневый ввод/вывод. Возникает логически обоснованный вопрос: зачем мне все это надо? Хочу заметить, что низкоуровневый ввод-вывод хорош не для всех программ. Есть ряд случаев, когда следует отказаться от системных вызовов в пользу библиотечных функций. Предлагаю рассмотреть преимущества и недостатки низкоуровневого ввода-вывода.
- Высокая скорость ввода-вывода. Полезно, когда программа работает с большими файлами и/или с большим количеством файлов.
- Отвечает стандарту POSIX. Основные системные вызовы Linux, отвечающие за низкоуровневый ввод-вывод отвечают стандарту POSIX. Это значит, что ваша программа не будет намертво привязана к Linux и вы сможете откомпилировать и запустить ее, например, под FreeBSD.
- Полный контроль. Не смотря на то, что библиотечные интерфейсы файлового ввода-вывода достаточно гибкие и многофункциональные, полный контроль за происходящим (права доступа, режимы чтения-записи и проч.) вам обеспечат только низкоуровневые механизмы ввода-вывода.
- Унифицированный подход. На первый взгляд кажется, что библиотечные интерфейсы ввода-вывода удобнее низкоуровневых системных вызовов. Это так, если выполнять всегда однотипные простые операции. Библиотечные интерфейсы для расширения своих возможностей требуют запоминания новой информации и чтения документации. С другой стороны, если вы один раз научитесь пользоваться системными вызовами для файлового ввода-вывода, вам больше не потребуется обращаться к документации для освоения новых возможностей.
- Понимание происходящего Библиотеки ввода-вывода за счет высокоуровневых интерфейсов частично скрывают от нас суть ввода-вывода Linux. Используя системные вызовы вы обращаетесь непосредственно к ядру и понимаете, что происходит на самом деле.
- Возможность комбинации. Никто не мешает вам использовать в рамках одной программы как низкоуровневую так и высокоуровневую схемы ввода-вывода. Это даже приветствуется в большинстве случаев. В приведенных примерах как раз рассматривается такой подход.
- Переносимость Не смотря на то, что низкоуровневые механизмы ввода-вывода легко переносятся между Unix-системами, с другими ОС могут возникнуть проблемы. Не знаю точно, но скорее всего ОС $toomuch(TM) или MacOS(TM) спросят вас: «Что такое open() или write()?», если вы попытаетесь откомпилировать в них программу, использующую низкоуровневый ввод-вывод. Если вы хотите, чтобы программа работала везде, следует помнить о том, что не все операционные системы отвечают стандарту POSIX для системных вызовов.
- Форматирование ввода-вывода Низкоуровневые фукнции файлового ввода-вывода работают с сырыми данными, то есть воспринимают информацию как набор байтов. Если ваша программа использует сложные схемы форматирования ввода-вывода, то использование низкоуровневых интерфейсов может привести к загромождению исходного кода и к появлению скрытых ошибок.
Вывод: выбор способа файлового ввода-вывода должен производиться не на основании личных предпочтений, а на основании здравого смысла и четкого понимания ситуации.
1.2. Основные понятия
Прежде чем двигаться дальше, определимся с основополагающими понятиями низкоуровневого ввода-вывода. Некоторые понятия, для которых нет четких названий и формулировок я придумал сам.
Файловый дескриптор (file descriptor)
Это целое знаковое число (int), используемое как аналог библиотечного указателя FILE*. Каждому открытому файлу операционная система назначает файловый дескриптор. Файловые дескрипторы, соответствующие реально открым файлам, больше или равны нулю. Каждый открытый файл имеет свой уникальный дескриптор. Понятие дескриптора привязано к процессу. Дочерний процесс наследует файловые дескрипторы своего родителя. Операционная система накладывает ограничения на количество открытых файлов для одного пользователя. Команда ulimit -n позволяет узнать это значение.
Флаг открытия файла
Каждый файл в программе открывается с какой-то целью (только для чтения, только для записи и т. д.). Использование флагов открытия файла позволяет явно прописать свои намерения во время открытия файла, чтобы потом оградить вас (или другого программиста, работающего над программой) от нецелевого использования файлового дескриптора. Можно сказать, что флаги открытия файлов определяют локальные права доступа к файлу в пределах процессов, работающих с дескриптором. Для формирования набора флагов открытия файлов используются обычный целочисленный тип данных.
Режим записи файла
В отличие от флагов открытия, режимы записи определяют хорошо знакомые вам глобальные права доступа к файлу в пределах файловой системы (чтение, запись, выполнение, владельцы, пользователи, группы и проч. и проч.). Для установки режима записи файлов используется тип mode_t (заголовочный файл sys/stat.h в каталоге /usr/include или include/linux/stat.h в исходных кодах ядра Linux).
Позиция в файле
Использование позиций позволяет получать произвольный доступ к данным в файле. Для работы с позициями используется тип off_t (заголовочный файл /usr/include/sys/types.h или include/linux/types.h в исходных кодах ядра Linux). Следует заметить, что файлы, находящиеся на диске — это не память (RANDOM Access Memory) с реальным произвольным доступом. Частые перемещения внутри файла могут значительно снизить скорость выполнения вашей программы. Поэтому иногда (например, при многопроходном синтаксическом анализе или при сортировке) бывает выгоднее скопировать файл в память. Про то, как перемещаться по файлу будет рассказано в следующем выпуске рассылки.
1.3. Ввод, вывод и ошибки
В Unix-системах можно на пальцах пересчитать вещи, не являющиеся файлами. Стандартный ввод (stdin), стандартный вывод (stdout) и стандартный вывод для ошибок (stderr) — это тоже файлы. Теперь быстренько вспоминаем, что дочерний процесс наследует ВСЕ ОТКРЫТЫЕ файлы своего родителя. Если вы запускаете программу стандартным образом (например, из командной оболочки), то наследуете три открытых файла: стандартный ввод, стандартный вывод и стандартный поток для ошибок. По общепринятому соглашению эти файлы открыты под дескрипторами 0, 1 и 2 соответственно. То есть, запись в файл с дескриптором 2, например, значит вывод информации в стандартный поток ошибок. А что будет стандартным потоком ошибок — это уже вам решать. Любая нормальная командная оболочка располагает средствами для перенаправления стандартных потоков. Можете поэкспериментировать с принтером 🙂 Вот приблизительный рецепт для bash, если не жалко бумаги, чернил и нервов:
1.4. Обзор низкоуровневых системных вызовов ввода-вывода
- int open (ИМЯ_ФАЙЛА, ФЛАГИ_ОТКРЫТИЯ) — для чтения
int open (ИМЯ_ФАЙЛА, ФЛАГИ_ОТКРЫТИЯ, РЕЖИМ_ЗАПИСИ) — для чтения и/или записи.
Системный вызов open() открывает файл с указанными флагами открытия и указанным режимом записи. В случае успеха возвращается неотрицательный файловый дескриптор. Флаги открытия представлены набором констант, которые можно объединять побитовым ИЛИ. Рассмотрим наиболее используемые флаги:
O_CREAT — создать файл, если не существует
O_RDWR — чтение + запись
O_WRONLY — только запись
O_RDONLY — только чтение
O_APPEND — запись в конец файла
O_TRUNC — перезапись существующего файла
Полный список можно получить вот так:
$ grep O_ /usr/include/bits/fcntl.h
Режимы записи также представляют собой набор констант, объединяемых при необходимости побитовым ИЛИ. Большинство из них можно записать в следующем формате:
S_I + +
S_IRUSR — пользователь может читать. Аналогично chmod u+r или chmod 400
S_IWOTH — остальные могут писАть. Аналогично chmod o+w или chmod 002
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH — аналогично rw-r—r—
Полный список можно получить так:
$ grep S_I /usr/include/sys/stat.h
2. Примеры
2.1. Чтение из файла
/* myread.c */ #include #include #include #define BUF_SIZE 1 /* Size of input buffer */ int main (int argc, char** argv) < if (argc != 2) < fprintf (stderr, "Usage: myread \n"); exit (1); > int fd = open (argv[1], O_RDONLY); if (fd < 0) < fprintf (stderr, "Can't read file %s\n", argv[1]); exit (2); >char buffer[BUF_SIZE] = ""; while (read (fd, buffer, BUF_SIZE)) < write (1, buffer, BUF_SIZE); >; close (fd); exit (0); >
2.2. Запись в файл
/* myio.c */ #include #include #include #include #include int main (int argc, char** argv) < if (argc != 3) < fprintf (stderr, "Usage: myio \n"); exit (1); > /* Permissions: RW- RW- R-- (664) */ mode_t fmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH; /* Write only. Create new file if necessary */ int fflags = O_WRONLY | O_CREAT; int fd = open (argv[1], fflags, fmode); if (fd < 0) < fprintf (stderr, "Can't open file %s\n", argv[1]); exit (2); >write (fd, argv[2], strlen(argv[2])); write (fd, "\n", 1); close (fd); exit (0); >
Заключение
Специально не буду проводить разбор полетов, потому что примеры очень простые. Кому еще не стало дурно от всех этих системных вызовов, советую поэкспериментировать с дескрипторами 0, 1 и 2. Посмотрите, что получится, если закрыть, например, файл с дескриптором 1 и т. д. Вопросы и пожелания направляйте в форум на Lindevel.Ru (http://www.lindevel.ru) или мне (nn@lindevel.ru).
Пожалуйста, если найдете ошибки, опечатки или плохое форматирование, сообщайте мне об этом. Самое лучшее, что умеет делать человек — забывать и ошибаться. Ваши корректировки помогут создать на сайте архив рассылки, не содержащий ошибок. У меня нет времени и терпения самостоятельно проверять каждую строчку по десять раз.
Спасибо за внимание.
С наилучшими пожеланиями, Николай.