Системное программирование Линукс. Linux. Системное программирование. Вступление
Скачать 0.65 Mb.
|
49 что пользователь может открыть каталог и увидеть список его содержимого. Право записи позволяет добавлять в каталог новые ссылки, а право исполнения разрешает открыть каталог и ввести его название в имя пути. В табл. 1.1 перечислены все девять бит доступа, их восьмеричные значения (распространенный способ представления девяти бит), их текстовые значения (отображаемые командой ls ) и их права. Таблица 1.1. Биты доступа и их значения Бит Восьмеричное значение Текстовое значение Соответствующие права 8 400 r-------- Владелец может читать 7 200 -w------- Владелец может записывать 6 100 --x------ Владелец может исполнять 5 040 ---r----- Члены группы могут читать 4 020 ----w---- Члены группы могут записывать 3 010 -----x--- Члены группы могут исполнять 2 004 ------r-- Любой может читать 1 002 -------w- Любой может записывать 0 001 --------x Любой может исполнять Кроме исторически сложившихся в UNIX прав доступа, Linux также поддержи вает списки контроля доступа (ACL). Они позволяют предоставлять гораздо более детализированные и точные права, соответственно, более полный контроль над безопасностью. Эти преимущества приобретаются за счет общего усложнения прав доступа и увеличения требуемого для этой информации дискового пространства. Сигналы Сигналы — это механизм, обеспечивающий односторонние асинхронные уведом ления. Сигнал может быть отправлен от ядра к процессу, от процесса к другому процессу либо от процесса к самому себе. Обычно сигнал сообщает процессу, что произошло какоелибо событие, например возникла ошибка сегментации или пользователь нажал Ctrl+C. Ядро Linux реализует около 30 разновидностей сигналов (точное количество зависит от конкретной архитектуры). Каждый сигнал представлен числовой кон стантой и текстовым названием. Например, сигнал SIGHUP используется, чтобы сооб щить о зависании терминала. В архитектуре x8664 этот сигнал имеет значение 1 Сигналы прерывают исполнение работающего процесса. В результате процесс откладывает любую текущую задачу и немедленно выполняет заранее определен ное действие. Все сигналы, за исключением SIGKILL (всегда завершает процесс) и SIGSTOP (всегда останавливает процесс), оставляют процессам возможность вы бора того, что должно произойти после получения конкретного сигнала. Так, про цесс может совершить действие, заданное по умолчанию (в частности, завершение процесса, завершение процесса с созданием дампа, остановка процесса или отсут ствие действия), — в зависимости от полученного сигнала. Кроме того, процессы могут явно выбирать, будут они обрабатывать сигнал или проигнорируют его. Концепции программирования в Linux 50 Проигнорированные сигналы бесшумно удаляются. Если сигнал решено обрабо тать, выполняется предоставляемая пользователем функция, которая называется обработчиком сигнала. Программа переходит к выполнению этой функции, как только получит сигнал. Когда обработчик сигнала возвращается, контроль над программой передается обратно инструкции, работа которой была прервана. Сиг налы являются асинхронными, поэтому обработчики сигналов не должны срывать выполнение прерванного кода. Таким образом, речь идет о выполнении только функций, которые безопасны для выполнения в асинхронной среде, они также на зываются сигналобезопасными. Межпроцессное взаимодействие Самые важные задачи операционной системы — это обеспечение обмена информаци ей между процессами и предоставление процессам возможности уведомлять друг друга о событиях. Ядро Linux реализует большинство из исторически сложившихся в UNIX механизмов межпроцессного взаимодействия. В частности, речь идет о меха низмах, определенных и стандартизированных как в SystemV, так и в POSIX. Кроме того, в ядре Linux имеется пара собственных механизмов подобного взаимодействия. К механизмам межпроцессного взаимодействия, поддерживаемым в Linux, от носятся именованные каналы, семафоры, очереди сообщений, разделяемая память и фьютексы. Заголовки Системное программирование в Linux всецело зависит от нескольких заголовков. И само ядро, и glibc предоставляют заголовки, применяемые при системном про граммировании. К ним относятся стандартный джентльменский набор C (например, ) и обычные заголовки UNIX (к примеру, ). Обработка ошибок Само собой разумеется, что проверка наличия ошибок и их обработка — задача пер востепенной важности. В системном программировании ошибка характеризуется возвращаемым значением функции и описывается с помощью специальной пере менной errno glibc явно предоставляет поддержку errno как для библиотечных, так и для системных вызовов. Абсолютное большинство интерфейсов, рассмотренных в данной книге, используют для сообщения об ошибках именно этот механизм. Функции уведомляют вызывающую сторону об ошибках посредством специаль ного возвращаемого значения, которое обычно равно –1 (точное значение, использу емое в данном случае, зависит от конкретной функции). Значение ошибки предупре ждает вызывающую сторону, что возникла ошибка, но никак не объясняет, почему она произошла. Переменная errno используется для выяснения причины ошибки. Эта переменная объявляется в следующим образом: extern int errno; Глава 1. Введение и основополагающие концепции 51 Ее значение является валидным лишь непосредственно после того, как функция, задающая errno , указывает на ошибку (обычно это делается путем возвращения -1 ). Дело в том, что после успешного выполнения функции значение этой переменной вполне может быть изменено. Переменная errno доступна для чтения или записи напрямую; это модифициру емое именуемое выражение. Значение errno соответствует текстовому описанию конкретной ошибки. Препроцессор #define также ассоциирует переменную errno с числовым значением. Например, препроцессор определяет EACCES равным 1 и озна чает «доступ запрещен». В табл. 1.2 перечислены стандартные определения и соответствующие им описания ошибок. Таблица 1.2. Ошибки и их описание Обозначение препроцессора Описание E2BIG Список аргументов слишком велик EACCES Доступ запрещен EAGAIN Повторить попытку (ресурс временно недоступен) EBADF Недопустимый номер файла EBUSY Заняты ресурс или устройство ECHILD Процессы-потомки отсутствуют EDOM Математический аргумент вне области функции EEXIST Файл уже существует EFAULT Недопустимый адрес EFBIG Файл слишком велик EINTR Системный вызов был прерван EINVAL Недействительный аргумент EIO Ошибка ввода-вывода EISDIR Это каталог EMFILE Слишком много файлов открыто EMLINK Слишком много ссылок ENFILE Переполнение таблицы файлов ENODEV Такое устройство отсутствует ENOENT Такой файл или каталог отсутствует ENOEXEC Ошибка формата исполняемого файла ENOMEM Недостаточно памяти ENOSPC Израсходовано все пространство на устройстве ENOTDIR Это не каталог ENOTTY Недопустимая операция управления вводом-выводом ENXIO Такое устройство или адрес не существует EPERM Операция недопустима EPIPE Поврежденный конвейер ERANGE Результат слишком велик EROFS Файловая система доступна только для чтения ESPIPE Недействительная операция позиционирования ESRCH Такой процесс отсутствует ETXTBSY Текстовый файл занят EXDEV Неверная ссылка Концепции программирования в Linux 52 Библиотека C предоставляет набор функций, позволяющих представлять кон кретное значение errno в виде соответствующего ему текстового представления. Эта возможность необходима только для отчетности об ошибках и т. п. Проверка наличия ошибок и их обработка может выполняться на основе одних лишь опре делений препроцессора, а также напрямую через errno Первая подобная функция такова: #include void perror (const char *str); Эта функция печатает на устройстве stderr (standard error — «стандартная ошибка») строковое представление текущей ошибки, взятое из errno , добавляя в качестве префикса строку, на которую указывает параметр str . Далее следует двоеточие. Для большей информативности имя отказавшей функции следует включать в строку. Например: if (close (fd) == –1) perror ("close"); В библиотеке C также предоставляются функции strerror() и strerror_r() , прототипированные как: #include char * strerror (int errnum); и #include int strerror_r (int errnum, char *buf, size_t len); Первая функция возвращает указатель на строку, описывающую ошибку, вы данную errnum . Эта строка не может быть изменена приложением, однако это мож но сделать с помощью последующих вызовов perror() и strerror() . Соответственно, такая функция не является потокобезопасной. Функция strerror_r() , напротив, является потокобезопасной. Она заполняет буфер, имеющий длину len , на который указывает buf . При успешном вызове strerror_r() возвращается 0 , при сбое — –1 . Забавно, но при ошибке она выдает errno Для некоторых функций к допустимым возвращаемым значениям относится вся область возвращаемого типа. В таких случаях перед вызовом функции errno нужно присвоить значение 0 , а по завершении проверить ее новое значение (такие функции могут возвратить ненулевое значение errno , только когда действительно возникла ошибка). Например: errno = 0; arg = strtoul (buf, NULL, 0); if (errno) perror ("strtoul"); Глава 1. Введение и основополагающие концепции 53 При проверке errno мы зачастую забываем, что это значение может быть изме нено любым библиотечным или системным вызовом. Например, в следующем коде есть ошибка: if (fsync (fd) == –1) { fprintf (stderr, "fsyncfailed!\n"); if (errno == EIO) fprintf (stderr, "I/O error on %d!\n", fd); } Если необходимо сохранить значение errno между вызовами нескольких функ ций, делаем это: if (fsync (fd) == –1) { const int err = errno; fprintf (stderr, "fsync failed: %s\n", strerror (errno)); if (err == EIO) { /* если ошибка связана с вводом-выводом — уходим */ fprintf (stderr, "I/O error on %d!\n", fd); exit (EXIT_FAILURE); } } В однопоточных программах errno является глобальной переменной, как было показано выше в этом разделе. Однако в многопоточных программах errno сохра няется отдельно в каждом потоке для обеспечения потокобезопасности. Добро пожаловать в системное программирование В этой главе мы рассмотрели основы системного программирования в Linux и сде лали обзор операционной системы Linux с точки зрения программиста. В следу ющей главе мы обсудим основы файлового вводавывода, в частности поговорим о чтении файлов и записи информации в них. Многие интерфейсы в Linux реали зованы как файлы, поэтому файловый вводвывод имеет большое значение для решения разных задач, а не только для работы с обычными файлами. Итак, все общие моменты изучены, настало время приступить к настоящему системному программированию. В путь! Добро пожаловать в системное программирование 55 Каждый процесс традиционно имеет не менее трех открытых файловых дес крипторов: 0, 1 и 2, если, конечно, процесс явно не закрывает один из них. Файловый дескриптор 0 соответствует стандартному вводу (stdin), дескриптор 1 — стандарт- ному выводу (stdout), дескриптор 2 — стандартной ошибке (stderr). Библиоте ка С не ссылается непосредственно на эти целые числа, а предоставляет препроцес сорные определения STDIN_FILENO , STDOUT_FILENO и STDERR_FILENO для каждого из вышеописанных вариантов соответственно. Как правило, stdin подключен к тер минальному устройству ввода (обычно это пользовательская клавиатура), а stdout и stderr — к дисплею терминала. Пользователи могут переназначать эти стандарт ные файловые дескрипторы и даже направлять по конвейеру вывод одной програм мы во ввод другой. Именно так оболочка реализует переназначения и конвейеры. Дескрипторы могут ссылаться не только на обычные файлы, но и на файлы устройств и конвейеры, каталоги и фьютексы, FIFO и сокеты. Это соответствует парадигме «все есть файл». Практически любая информация, которую можно читать или записывать, доступна по файловому дескриптору. По умолчанию процесспотомок получает копию таблицы файлов своего про цессапредка. Список открытых файлов и режимы доступа к ним, актуальные файловые позиции и другие метаданные не меняются. Однако изменение, связан ное с одним процессом, например закрытие файла процессомпотомком, не затра гивает таблиц файлов других процессов. В гл. 5 будет показано, что такое поведение является типичным, но предок и потомок могут совместно использовать таблицу файлов предка (как потоки обычно и делают). Открытие файлов Наиболее простой способ доступа к файлу связан с использованием системных вызовов read() и write() . Однако прежде, чем к файлу можно будет получить доступ, его требуется открыть с помощью системного вызова open() или creat() . Когда работа с файлом закончена, его нужно закрыть посредством системного вызова close() Системный вызов open() Открытие файла и получение файлового дескриптора осуществляются с помощью системного вызова open() : #include #include #include int open (const char *name, int flags); int open (const char *name, int flags, mode_t mode); Системный вызов open() ассоциирует файл, на который указывает имя пути name с файловым дескриптором, возвращаемым в случае успеха. В качестве файловой Открытие файлов 56 позиции указывается его начало (нуль), и файл открывается для доступа в соответ ствии с заданными флагами (параметр flags ). Флаги для open(). Аргумент flags — это поразрядное ИЛИ, состоящее из од ного или нескольких флагов. Он должен указывать режим доступа, который может иметь одно из следующих значений: O_RDONLY , O_WRONLY или O_RDWR . Эти аргументы соответственно означают, что файл может быть открыт только для чтения, только для записи или одновременно для того и другого. Например, следующий код открывает каталог /home/kidd/madagascar для чтения: int fd; fd = open ("/home/kidd/madagascar", O_RDONLY); if (fd == –1) /* ошибка */ Если файл открыт только для чтения, в него невозможно чтолибо записать, и на оборот. Процесс, осуществляющий системный вызов open() , должен иметь довольно широкие права, чтобы получить запрашиваемый доступ. Например, если файл до ступен определенному пользователю только для чтения, то процесс, запущенный этим пользователем, может открыть этот файл как O_RDONLY , но не как O_WRONLY или O_RDWR Перед указанием режима доступа задается вариант побитового «ИЛИ» для аргумента flags . Вариант описывается одним или несколькими из следующих значений, изменяющих поведение запроса на открытие файла. O_APPEND . Файл будет открыт в режиме дозаписи. Это означает, что перед каждым актом записи файловая позиция будет обновляться и устанавливаться в теку щий конец файла. Это происходит, даже когда другой процесс чтото записал в файл уже после последнего акта записи от процесса, выполнившего вызов (таким образом, процесс, осуществивший вызов, уже изменил позицию конца файла). O_ASYNC . Когда указанный файл станет доступным для чтения или записи, гене рируется специальный сигнал (по умолчанию SIGIO ). Этот флаг может исполь зоваться только при работе с FIFO, каналами, сокетами и терминалами, но не с обычными файлами. O_CLOEXEC . Задает флаг close-on-exec для открытого файла. Как только начнется выполнение нового процесса, этот файл будет автоматически закрыт. Таким образом, очевидна необходимость вызова fcntl() для задания флага и исклю чается возможность возникновения условий гонки. Этот флаг доступен лишь в версии ядра Linux 2.6.23 и выше. O_CREAT . Если файл, обозначаемый именем name , не существует, то ядро создаст его. Если же файл уже есть, то этот флаг не дает никакого эффекта (кроме слу чаев, в которых также задан O_EXCL ). O_DIRECT . Файл будет открыт для непосредственного вводавывода. O_DIRECTORY . Если файл name не является каталогом, то вызов open() не удастся. Этот флаг используется внутрисистемно библиотечным вызовом opendir() Глава 2. Файловый ввод-вывод 57 O_EXCL . При указании вместе с O_CREAT этот флаг предотвратит срабатывание вызова open() , если файл с именем name уже существует. Данный флаг применя ется для предотвращения условий гонки при создании файлов. Если O_CREAT не указан, то данный флаг не имеет значения. O_LARGEFILE . Указанный файл будет открыт с использованием 64битных сме щений. Таким образом, становится возможно манипулировать файлами крупнее 2 Гбайт. Для таких операций требуется 64битная архитектура. O_NOATIME+ . Последнее значение времени доступа к файлу не обновляется при его чтении. Этот флаг удобно применять при индексировании, резервном копи ровании и использовании других подобных программ, считывающих все файлы в системе. Так предотвращается учет значительной записывающей активности, возникающей при обновлении индексных дескрипторов каждого считываемого файла. Этот флаг доступен лишь в версии ядра Linux 2.6.8 и выше. O_NOCTTY . Если файл с именем name указывает на терминальное устройство (на пример, /dev/tty), это устройство не получает контроля над процессом, даже если в настоящий момент этот процесс не имеет контролирующего устройства. Этот флаг используется редко. O_NOFOLLOW . Если name — это символьная ссылка, то вызов open() окончится ошибкой. Как правило, происходит разрешение ссылки, после чего открывает ся целевой файл. Если остальные компоненты заданного пути также являются ссылками, то вызов все равно удастся. Например, при name , равном /etc/ship/ plank.txt , вызов не удастся в случае, если plank.txt является символьной ссыл кой. При этом если plank.txt не является символьной ссылкой, а etc или ship являются, то вызов будет выполнен успешно. O_NONBLOCK . Если это возможно, файл будет открыт в неблокирующем режиме. Ни вызов open() , ни какаялибо иная операция не вызовут блокирования про цесса (перехода в спящий режим) при вводевыводе. Такое поведение может быть определено лишь для FIFO. O_SYNC . Файл будет открыт для синхронного вводавывода. Никакие операции записи не завершатся, пока данные физически не окажутся на диске. Обычные действия по считыванию уже протекают синхронно, поэтому данный флаг никак не влияет на чтение. Стандарт POSIX дополнительно определяет O_DSYNC и O_RSYNC , но в Linux эти флаги эквивалентны O_SYNC O_TRUNC . Если файл уже существует, является обычным файлом и заданные для него флаги допускают запись, то файл будет усечен до нулевой длины. Флаг O_TRUNC для FIFO или терминального устройства игнорируется. Использование с файлами других типов не определено. Указание O_TRUNC с O_RDONLY также явля ется неопределенным, так как для усечения файла вам требуется разрешение на доступ к нему для записи. Например, следующий код открывает для записи файл /home/teach/pearl . Если файл уже существует, то он будет усечен до нулевой длины. Флаг O_CREAT не ука зывается, когда файл еще не существует, поэтому вызов не состоится: Открытие файлов |