Версия: 0.094
Оригинал: lindevel.ru/zlp/
Copyright (c) 2003-2006 Nikolay N. Ivanov.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.2
or any later version published by the Free Software Foundation;
Каждый имеет право воспроизводить, распространять и/или вносить изменения в настоящий Документ в соответствии с условиями GNU Free Documentation License, Версией 1.2 или любой более поздней версией, опубликованной Free Software Foundation;
Предполагается, что читатель этой книги должен иметь хотя бы начальные пользовательские навыки при работе с GNU/Linux (умение вводить команды в оболочку и знать основы операционной системы), а также знать язык C хотя бы на начальном уровне. Я посчитал бессмысленным писать еще одну книгу по языку C. Если вы знаете язык C на уровне книги Кернигана и Ритчи "Язык программирования Си", то у вас не будет проблем с изучением материала этой книги.
Если вы знаете язык C++, но не знаете C, ничего страшного. Эти два языка хотя и разные по своей сути, но имеют одну и ту же синтаксическую основу. C++-программисту не надо обладать феноменальной сообразительностью, чтобы разобраться в программе, написанной на C.
Большая часть материала этой книги относится и к другим современным Unix-подобным операционным системам (FreeBSD, например). Если вы еще не знаете Linux, то больших проблем не будет.
В книге используется достаточно мало условных обозначений. Листинги программ, а также вырезки из листингов и содержимое конфигурационных или иных файлов выделяются синим цветом:
#include <stdio.h> int main (void) { printf ("Hello World\n"); }
Примеры команд, вводимых в оболочку выделяются оранжевым цветом и начинаются с символа $ (приглашение командной строки). Вывод команд выделяется жирным шрифтом:
$ gcc gcc: no input files $
Примеры команд, вводимых от имени суперпользователя (root) выделяются так же, как и обычные команды, только в качестве приглашения командной строки используется символ #
# lilo -t Added Linux * Added BootFromCD The boot sector and the map file have *NOT* been altered. #
Некоторые вещи в основном тексте, на которые я хотел бы обратить ваше внимание, будут выделяться жирным шрифтом. Некоторые термины, которые следует запомнить, будут выделяться курсивом. Кроме того, жирным шрифтом могут быть отделены от основного текста имена файлов, имена функций и т. п.
То, что нам нужно иметь, называют инструментарием. Инструментарий программиста зависит от того, какие программы и на каких языках он будет писать. Наш инструментарий будет состоять из: 1) Операционной системы Linux (GNU/Linux) с текстовым редактором; 2) Компилятора с линковщиком; 3) Дополнительных утилит. Ну и конечно же из терпения и усердия.
Для начала нужно иметь Linux (здесь правильнее будет сказать GNU/Linux). Подойдет практически любой современный дистрибутив. Посмотрите, какая версия ядра Linux у вас используется. Наберите в оболочке следующую команду:
$ uname -r 2.6.10nn1 $У меня ядро версии 2.6.10. У вас, скорее всего, будет что-то другое. Дистрибутивы, построенные на ядрах версий 2.4.x и 2.6.x достаточно современные для того, чтобы воспроизвести все примеры в этой книге. Версии 2.0.x и 2.2.x также могут использоваться, но некоторые возможности, описанные в этой книге в этих ядрах не реализованы.
Следующий шаг - компилятор. Мы будем использовать компилятор gcc (не обсуждается!). Практически все дистрибутивы Linux поставляются вместе с компилятором языка C. Вопросы о том, какие программы установлены, как устанавливать недостающие программы выходят за рамки этой книги. Поэтому не буду объяснять, как проверить присутствие в системе компилятора. На начальном этапе обучения версия компилятора не имеет значения. Если у вас есть компилятор gcc, то и линковщик (о том, что это такое, будет рассказано позже) тоже есть (вероятность 99%).
Многие программисты используют различные оболочки для программирования (KDevelop, Glade и прочие). Мы откажемся от этого подхода по двум причинам: 1) это претит идеологии Unix; 2) за всеми этими средами разработки стоят те самые утилиты, которые мы будем использовать непосредственно. Нам на начальном этапе потребуется лишь утилита make.
И, наконец, немаловажная деталь. Текстовый редактор. Здесь на вкус и цвет товарища нет. В мире Unix-систем есть два лагеря: любители Vi и любители Emacs. В настоящее время написано много клонов редактора Vi (Vim, Elvis, GVim) и много клонов (а точнее - расширений) редактора Emacs. И для Vi и для Emacs существуют как консольные так и графические версии. Нет смысла обсуждать преимущества и недостатки этих двух "гигантов". Вам придется самостоятельно выбрать наиболее удобный для вас текстовый редактор. Возможно вам не понравится ни один из них и вы будете использовать что-нибудь наподобие kate или nedit или pico. Между прочим, редактор Pico открывает свой третий лагерь поклонников. Этот редактор, изначально разработанный в Вашингтонском университете для почтовика pine, а затем ставший самостоятельным редактором, уверенно набирает силу в сообществе Unix. Если вы полный аскет, можете попробовать редактор ed (red), который мало напоминает текстовый редактор в современном понимании. Опять же повторюсь, выбор за вами. Я использую консольную версию редактора Vim, но с большим уважением отношусь и к Emacs. Очень важно попробовать оба редактора "в полную силу", а не просто "взглянуть". Только опытный пользователь сможет за видимой неуклюжестью ощутить мощь и удобство. Это одна из важнейших граней идеологии Unix.
Подавляющую часть работы мы будем проводить в командной строке. "Командная строка" - общий термин, подразумевающий собой сочетание терминальной программы и командной оболочки. Терминальная программа (или программа эмуляции терминала), это то, что вы видите на экране, когда работаете с командной строкой. Терминальная программа позволяет пользователю вводить команды и видеть результаты их выполнения. Программа konsole в KDE, например, является терминальной программой. Под терминальной программой запускается командная оболочка (командный интерпретатор или просто shell). Командная оболочка через интерфейс терминальной программы общается с пользователем и выполняет всю основную работу. Bash, Csh, Ksh - это все командные оболочки или, как их еще называют, командные интерпретаторы. В Linux я рекомендую использовать оболочку Bash (Bourne Again SHell). Однако не произойдет ничего страшного, если вы будете использовать другую командную оболочку, например C-Shell (csh), у которой достаточно своих поклонников. Как вы позже узнаете, любая командная оболочка - это не просто исполнитель команд, но и полноценный интерпретатор собственного языка программирования. Чтобы узнать, какая оболочка запущена под терминальной программой, просто наберите команду ps. В одной из строк (скорее всего - в первой) будет написана ваша командная оболочка:
$ ps PID TTY TIME CMD 5003 pts/0 00:00:00 bash 6025 pts/0 00:00:00 ps $В моем случае это bash.
Если вы заметили ошибки/опечатки. Или если что-то в этой книге плохо описано, сообщайте об этом по адресу zlp@lindevel.ru или форуме на сайте Lindevel.Ru (http://www.lindevel.ru).
Книга распространяется на условиях GNU Free Documentation License. В соответствии с условиями этой лицензии вы можете свободно скачивать и распространять книгу без каких-либо разрешений, но не нарушая и не изменяя условий Лицензии. Если вы хотите поместить копию книги на своем Интернет-ресурсе или просто распечатать - делайте это свободно, как завещал великий Столлман!
Вы также можете вносить самостоятельные изменения в эту книгу. В этом случае я просто прошу сообщать мне об этом.
Чтобы сразу начать программировать, создадим еще один клон известной программы "Hello World". Что делает эта программа, вы знаете. Откройте свой любимый текстовый редактор и наберите в нем следующий текст:
/* hello.c */ #include <stdio.h> int main (void) { printf ("Hello World\n"); }
Я назвал свой файл hello.c. Вы можете назвать как угодно, сохранив суффикс .c. Содержимое файла hello.c - это исходный код программы ('program source', 'source code' или просто 'source'). А hello.c - это исходный файл программы ('source file'). Hello World - очень маленькая программа, исходный код которой помещается в одном файле. В "настоящих" программах, как правило, исходный код разносится по нескольким файлам. В больших программах исходных файлов может быть больше сотни.
Наш исходный код написан на языке программирования C. Языки программирования были придуманы для того, чтобы программист мог объяснить компьютеру, что делать. Но вот беда, компьютер не понимает ни одного языка программирования. У компьютера есть свой язык, который называют машинным кодом или исполняемым кодом ('executable code'). Написать Hello World в машинном коде можно, но серьезные программы на нем не пишутся. Исполняемый код не только сложный по своей сути, но и очень неудобный для человека. Программа, которую можно написать за один день на языке программирования будет писаться целый год в машинном коде. Потом программист сойдет с ума. Чтобы этого не случилось, был придуман компилятор ('compiler'), который переводит исходный код программы в исполняемый код. Процесс перевода исходного кода программы в исполняемый код называют компиляцией.
Чтобы откомпилировать наш Hello World достаточно набрать в командной строке следующее заклинание:
$ gcc -o hello hello.c $Если исходный код написан без синтаксических ошибок, то компилятор завершит свою работу без каких-либо сообщений. Молчание - знак повиновения и согласия. Набрав команду ls вы тут же обнаружите новый файл с именем hello. Этот файл содержит исполняемый код программы. Такие файлы называют исполняемыми файлами ('executable files') или бинарниками ('binary files').
Вы наверняка догадались, что опция -o компилятора gcc указывает на то, каким должно быть имя выходного файла. Как вы позже узнаете, выходным файлом может быть не только бинарник. Если не указать опцию -o, то бинарнику, в нашем случае, будет присвоено имя a.out.
Осталось только запустить полученный бинарник. Для этого набираем в командной строке следующую команду:
$ ./hello Hello World $
Когда мы набираем в командной строке путь к бинарнику, мы, в реальности сообщаем оболочке, что надо выполнить программу. Оболочка "передает" бинарник ядру операционной системе, а ядро системы особым шаманским способом отдает программу на выполнение процессору. Затем, если программа не была запущена в фоновом режиме, то оболочка ждет от ядра сообщения о том, что программа выполнилась. Получив такое сообщение, оболочка выдает приглашение на ввод новой команды. Вы можете еще раз набрать ./hello и процедура повторится. В нашем случае программа выполняется очень быстро, и новое приглашение командной строки "вылетает" практически сразу.
Мы рассмотрели идеальный случай, когда программа написана без синтаксических ошибок. Попробуем намеренно испортить программу таким образом, чтобы она не отвечала канонам языка C. Для этого достаточно убрать точку с запятой в конце вызова функции printf():
printf ("Hello World\n")Теперь, если попытаться откомпилировать программу, то компилятор выругается, указав нам на то, что он считает неправильным:
$ gcc -o hello hello.c hello.c: In function 'main': hello.c:7: error: syntax error before '}' token $В первой строке говорится, что в файле hello.c (у нас он единственный) в теле функции main() что-то произошло. Вторая строка сообщает, что именно произошло: седьмая строка файла hello.c вызвала ошибку (error). Далее идет расшифровка: синтаксическая ошибка перед закрывающейся фигурной скобкой.
Заглянув в файл hello.c мы с удивлением обнаружим, что нахулиганили мы не в седьмой, а в шестой строке. Дело в том, что компилятор обнаружил нелады только в седьмой строке, но написал 'before' (до), что означает "прокручивай назад".
Естественно, пока мы не исправим ошибку, ни о каком бинарнике не может идти и речи. Если мы удалим старый бинарник hello, доставшийся нам от прошлой компиляции, то увидим, что компиляция испорченного файла не даст никакого результата. Однако иногда компилятор может лишь "заподозрить" что-то неладное, потенциально опасное для нормального существования программы. Тогда вместо 'error' пишется 'warning' (предупреждение), и бинарник все-таки появляется на свет (если в другом месте нет явных ошибок). Не следует игнорировать предупреждения, за исключением тех случаев, когда вы на 100% знаете, что делаете.
Парадокс программирования заключается в том, что можно наделать кучу ошибок (уже не синтаксических, как в нашем случае, а смысловых) по всем правилам языка программирования. В таком случае компилятор выдает бинарник, который делает не то, что мы хотели. В таком случае программу приходится отлаживать. Отладка - это обычное дело при написании любой достаточно сложной программы. Не ошибается только тот, кто ничего не делает.
Как я уже говорил, если исходный код сколько-нибудь серьезной программы уместить в одном файле, то такой код станет просто нечитаемым. К тому же если программа компилируется достаточно долго (особенно это относится к языку C++), то после исправления одной ошибки, нужно перекомпилировать весь код.
Куда лучше разбросать исходный код по нескольким файлам (осмысленно, по какому-нибудь критерию), и компилировать каждый такой файл отдельно. Как вы вскоре узнаете, это очень даже просто.
Давайте сначала разберемся, как из исходного файла получается бинарник. Подобно тому как гусеница не сразу превращается в бабочку, так и исходный файл не сразу превращается в бинарник. После компиляции создается объектный код. Это исполняемый код с некоторыми "вкраплениями", из-за которых объектный код еще не способен к выполнению. Сразу в голову приходит стиральная машина: вы ее только что купили и она стоит у вас дома в коробке. В таком состоянии она стирать не будет, но вы все равно рады, потому что осталось только вытащить из коробки и подключить.
Вернемся к объектному коду. Эти самые "вкрапления" (самое главное среди них - таблица символов) позволяют объектному коду "пристыковываться" к другому объектному коду. Такой фокус делает компоновщик (линковщик) - программа, которая объединяет объектный код, полученный из "разных мест", удаляет все лишнее и создает полноценный бинарник. Этот процесс называется компоновкой или линковкой.
Итак, чтобы откомпилировать мультифайловую программу, надо сначала добыть объектный код из каждого исходного файла в отдельности. Каждый такой код будет представлять собой объектный модуль. Каждый объектный модуль записывается в отдельный объектный файл. Затем объектные модули надо скомпоновать в один бинарник.
В Linux в качестве линковщика используется программа ld, обладающая приличным арсеналом опций. К счастью gcc самостоятельно вызывает компоновщик с нужными опциями, избавляя нас от "ручной" линковки.
Попробуем теперь, вооружившись запасом знаний, написать мультифайловый Hello World. Создадим первый файл с именем main.c:
/* main.c */ int main (void) { print_hello (); }Теперь создадим еще один файл hello.c со следующим содержимым:
/* hello.c */ #include <stdio.h> void print_hello (void) { printf ("Hello World\n"); }
Здесь функция main() вызывает функцию print_hello(), находящуюся в другом файле. Функция print_hello() выводит на экран заветное приветствие. Теперь нужно получить два объектных файла. Опция -c компилятора gcc заставляет его отказаться от линковки после компиляции. Если не указывать опцию -o, то в имени объектного файла расширение .c будет заменено на .o (обычные объектные файлы имеют расширение .o):
$ gcc -c main.c $ gcc -c hello.c $ ls hello.c hello.o main.c main.o $Итак, мы получили два объектных файла. Теперь их надо объединить в один бинарник:
$ gcc -o hello main.o hello.o $ ls hello* hello.c hello.o main.c main.o $ ./hello Hello World $Компилятор "увидел", что вместо исходных файлов (с расширением .c) ему подбросили объектные файлы (с расширением .o) и отреагировал согласно ситуации: вызвал линковщик с нужными опциями.
Давайте разберемся, что же все-таки произошло. В этом нам поможет утилита nm. Я уже оговорился, что объектные файлы содержат таблицу символов. Утилита nm как раз позволяет посмотреть эту таблицу в читаемом виде. Те, кто пробовал программировать на ассемблере знают, что в исполняемом файле буквально все (функции, переменные) стоит на своей позиции: стоит только вставить или убрать из программы один байт, как программа тут же превратиться в груду мусора из-за смещенных позиций (адресов). У объектных файлов особая роль: они хранят в таблице символов имена некоторых позиций (глобально объявленных функций, например). В процессе линковки происходит стыковка имен и пересчет позиций, что позволяет нескольким объектным файлам объединиться в один бинарник. Если вызвать nm для файла hello.o, то увидим следующую картину:
$ nm hello.o U printf 00000000 T print_hello $О смысловой нагрузке нулей и литер U,T мы будем говорить при изучении библиотек. Сейчас же важным является то, что в объектном файле сохранилась информация об использованных именах. Своя информация есть и в файле main.o:
$ nm main.o 00000000 T main U print_hello $Таблицы символов объектных файлов содержат общее имя print_hello. В процессе линковки высчитываются и подставляются в нужные места адреса, соответствующие именам из таблицы. Вот и весь секрет.
В предыдущем разделе для создания бинарника из двух исходных файлов нам пришлось набрать три команды. Если бы программу пришлось отлаживать, то каждый раз надо было бы вводить одни и те же три команды. Казалось бы выход есть: написать сценарий оболочки. Но давайте подумаем, какие в этом случае могут быть недостатки. Во-первых, каждый раз сценарий будет компилировать все файлы проекта, даже если мы исправили только один из них. В нашем случае это не страшно. Но если речь идет о десятках файлов! Во-вторых, сценарий "намертво" привязан к конкретной оболочке. Программа тут же становится менее переносимой. И, наконец, простому скрипту не хватает функциональности (задание аргументов сборки и т. п.), а хороший скрипт (с многофункциональными прибамбасами) плохо модернизируется.
Выход из сложившейся ситуации есть. Это утилита make, которая работает со своими собственными сценариями. Сценарий записывается в файле с именем Makefile и помещается в репозиторий (рабочий каталог) проекта. Сценарии утилиты make просты и многофункциональны, а формат Makefile используется повсеместно (и не только на Unix-системах). Дошло до того, что стали создавать программы, генерирующие Makefile'ы. Самый яркий пример - набор утилит GNU Autotools. Самое главное преимущество make - это "интеллектуальный" способ рекомпиляции: в процессе отладки make компилирует только измененные файлы.
То, что выполняет утилита make, называется сборкой проекта, а сама утилита make относится к разряду сборщиков.
Любой Makefile состоит из трех элементов: комментарии, макроопределения и целевые связки (или просто связки). В свою очередь связки состоят тоже из трех элементов: цель, зависимости и правила.
Сценарии make используют однострочные комментарии, начинающиеся с литеры # (решетка). О том, что такое комментарии и зачем они нужны, объяснять не буду.
Макроопределения позволяют назначить имя практически любой строке, а затем подставлять это имя в любое место сценария, где должна использоваться данная строка. Макросы Makefile схожи с макроконстантами языка C.
Связки определяют: 1) что нужно сделать (цель); 2) что для этого нужно (зависимости); 3) как это сделать (правила). В качестве цели выступает имя или макроконстанта. Зависимости - это список файлов и целей, разделенных пробелом. Правила - это команды передаваемые оболочке.
Теперь рассмотрим пример. Попробуем составить сценарий сборки для рассмотренного в предыдущем разделе мультифайлового проекта Hello World. Создайте файл с именем Makefile:
# Makefile for Hello World project hello: main.o hello.o gcc -o hello main.o hello.o main.o: main.c gcc -c main.c hello.o: hello.c gcc -c hello.c clean: rm -f *.o helloОбратите внимание, что в каждой строке перед вызовом gcc, а также в строке перед вызовом rm стоят табуляции. Как вы уже догадались, эти строки являются правилами. Формат Makefile требует, чтобы каждое правило начиналось с табуляции. Теперь рассмотрим все по порядку.
Makefile может начинаться как с заглавной так и со строчной буквы. Но рекомендуется все-таки начинать с заглавной, чтобы он не перемешивался с другими файлами проекта, а стоял "в списке первых".
Первая строка - комментарий. Здесь можно писать все, что угодно. Комментарий начинается с символа # (решетка) и заканчивается символом новой строки. Далее по порядку следуют четыре связки: 1) связка для компоновки объектных файлов main.o и hello.o; 2) связка для компиляции main.c; 3) связка для компиляции hello.c; 4) связка для очистки проекта.
Первая связка имеет цель hello. Цель отделяется от списка зависимостей двоеточием. Список зависимостей отделяется от правил символом новой строки. А каждое правило начинается на новой строке с символа табуляции. В нашем случае каждая связка содержит по одному правилу. В списке зависимостей перечисляются через пробел вещи, необходимые для выполнения правила. В первом случае, чтобы скомпоновать бинарник, нужно иметь два объектных файла, поэтому они оказываются в списке зависимостей. Изначально объектные файлы отсутствуют, поэтому требуется создать целевые связки для их получения. Итак, чтобы получить main.o, нужно откомпилировать main.c. Таким образом файл main.c появляется в списке зависимостей (он там единственный). Аналогичная ситуация с hello.o. Файлы main.c и hello.c изначально существуют (мы их сами создали), поэтому никаких связок для их создания не требуется.
Особую роль играет целевая связка clean с пустым списком зависимостей. Эта связка очищает проект от всех автоматически созданных файлов. В нашем случае удаляются файлы main.o, hello.o и hello. Очистка проекта бывает нужна в нескольких случаях: 1) для очистки готового проекта от всего лишнего; 2) для пересборки проекта (когда в проект добавляются новые файлы или когда изменяется сам Makefile; 3) в любых других случаях, когда требуется полная пересборка (напрмиер, для измерения времени полной сборки).
Теперь осталось запустить сценарий. Формат запуска утилиты make следующий:
make [опции] [цели...]Опции make нам пока не нужны. Если вызвать make без указания целей, то будет выполнена первая попавшаяся связка (со всеми зависимостями) и сборка завершится. Нам это и требуется:
$ make gcc -c main.c gcc -c hello.c gcc -o hello main.o hello.o $ ls hello* hello.c hello.o main.c main.o Makefile $ ./hello Hello World $В процессе сборки утилита make пишет все выполняемые правила. Проект собран, все работает.
Теперь давайте немного модернизируем наш проект. Добавим одну строку в файл hello.c:
/* hello.c */ #include <stdio.h> void print_hello (void) { printf ("Hello World\n"); printf ("Goodbye World\n"); }Теперь повторим сборку:
$ make gcc -c hello.c gcc -o hello main.o hello.o $ ./hello Hello World Goodbye World $Утилита make "пронюхала", что был изменен только hello.c, то есть компилировать нужно только его. Файл main.o остался без изменений. Теперь давайте очистим проект, оставив одни исходники:
$ make clean rm -f *.o hello $ ls hello.c main.c Makefile $В данном случае мы указали цель непосредственно в командной строке. Так как целевая связка clean содержит пустой список зависимостей, то выполняется только одно правило. Не забывайте "чистить" проект каждый раз, когда изменяется список исходных файлов или когда изменяется сам Makefile.
Любая программа имеет свой репозиторий - рабочий каталог, в котором находятся исходники, сценарии сборки (Makefile) и прочие файлы, относящиеся к проекту. Репозиторий рассмотренного нами проекта мультифайлового Hello World изначально состоит из файлов main.c, hello.c и, собственно, Makefile. После сборки репозиторий дополняется файлами main.o, hello.o и hello. Практика показывает, что правильная организация исходного кода в репозитории не только упрощает модернизацию и отладку, но и предотвращает возможность появления многих ошибок.
Модель КИС (Клиент-Интерфейс-Сервер) - это элегантная концепция распределения исходного кода в репозитории, в рамках которой все исходники можно поделить на клиенты, интерфейсы и серверы.
Итак, сервер предоставляет услуги. В нашем случае это могут быть функции, структуры, перечисления, константы, глобальные переменные и проч. В языке C++ это чаще всего классы или иерархии классов. Любой желающий (клиент) может воспользоваться предоставленными услугами, то есть вызвать функцию со своими фактическими параметрами, создать экземпляр структуры, воспользоваться константой и т. п. В C++, как правило, клиент использует класс как тип данных и использует его члены.
Часто бывает, что клиент сам становится сервером, точнее начинает играть роль промежуточного сервера. Хороший пример - наш мультифайловый Hello World. Здесь функция print_hello() (клиент) пользуется услугами стандартной библиотеки языка C (сервер), вызывая функцию printf(). Однако в дальнейшем функция print_hello() сама становится сервером, предоставляя свои услуги функции main(). В языке C++ довольно часто клиент создает производный класс, который наследует некоторые механизмы базового класса сервера. Таким образом клиент сам становится сервером, предоставляя услуги своего производного класса.
Клиент с сервером должны "понимать" друг друга, иначе взаимодействие невозможно. Интерфейс (протокол) - это условный набор правил, согласно которым взаимодействуют клиент и сервер. В нашем случае (мультифайловый Hello World) интерфейсом (протоколом) является общее имя в таблице символов двух объектных файлов. Такой способ взаимодействия может привести к неприятным последствиям. Клиент (функция main()) не знает ничего, кроме имени функции print_hello() и, наугад вызывает ее без аргументов и без присваивания. Иначе говоря, клиент не знает до конца правил игры. В нашем случае прототип функции print_hello() неизвестен.
Обычно для организации интерфейсов используются объявления (прототипы), которые помещаются чаще всего в заголовочные файлы. В языке C это файлы с расширением .h; в языке C++ это файлы с раширением .h, .hpp или без расширения. Некоторые "всезнайки" ошибочно называют заголовочные файлы библиотеками и умудряются учить этому других. Забегая вперед скажу, что библиотека - это просто коллекция скомпонованных особым образом объектных файлов, а заголовочный файл - это интерфейс. Основная разница между библиотеками и заголовочными файлами в том, что библиотека - это объектный (почти исполняемый) код, а заголовочный файл - это исходный код. Включая в программу заголовочный файл директивой #include мы соглашаемся работать с сервером (будь то библиотека или простой объектный файл) по его протоколу: если сервер сказал, что функция вызывается без аргументов, то она и будет вызываться без аргументов, иначе компилятор костьми ляжет, но не даст откомпилировать "незаконный вызов".
Вернемся к Hello World. В таком виде, как он есть сейчас, мы можем, например, вызвать функцию print_hello() с аргументом, и компилятор даже не заподозрит неладное, потому что на уровне исходного кода нет четких правил, регламентирующих взаимодействие клиента и сервера. После того как мы создадим заголовочный файл и включим его в файл main.c, компилятор будет "сматывать удочки" каждый раз, когда мы будем пытаться вызвать функцию print_hello() не по правилам. Таким образом интерфейс (набор объявлений, в данном случае - в заголовочном файле) - это публичная оферта сервера клиенту. Включение заголовочного файла директивой #include - это акцепт или подпись.
Еще хочу сказать пару слов о стандартной библиотеке языка C. Как я уже говорил, библиотека - это набор объектных файлов, которые подсоединяются к программе на стадии линковки. Так как стандартная библиотека языка C - это часть стандарта языка C, то она подключается автоматически во время линковки программы, но так как компилятор gcc сам вызывает линковщик с нужными параметрами, то мы этого просто не замечаем. Включая в исходники заголовочный файл stdio.h, мы автоматически соглашаемся использовать механизмы стандартного ввода-вывода на условиях сервера (стандартной библиотеки языка C).
Теперь попробуем применить модель КИС на практике для нашего проекта Hello World. Создадим файл hello.h:
/* hello.h */ void print_hello (void);Теперь включим этот файл в main.c:
/* main.c */ #include "hello.h" int main (void) { print_hello (); }Так как в проект был добавлен новый файл, надо сделать полную пересборку:
$ make clean rm -f *.o hello $ make gcc -c main.c gcc -c hello.c gcc -o hello main.o hello.o $ ./hello Hello World Goodbye World $С виду ничего не изменилось. Но на самом деле программа стала более правильной, более изящной и, самое главное, более безопасной.
Как уже неоднократно упоминалось в предыдущей главе, библиотека - это набор скомпонованных особым образом объектных файлов. Библиотеки подключаются к основной программе во время линковки. По способу компоновки библиотеки подразделяют на архивы (статические библиотеки, static libraries) и совместно используемые (динамические библиотеки, shared libraries). В Linux, кроме того, есть механизмы динамической подгрузки библиотек. Суть динамической подгрузки состоит в том, что запущенная программа может по собственному усмотрению подключить к себе какую-либо библиотеку. Благодаря этой возможности создаются программы с подключаемыми плагинами, такие как XMMS. В этой главе мы не будем рассматривать динамическую подгрузку, а остановимся на классическом использовании статических и динамических библиотек.
С точки зрения модели КИС, библиотека - это сервер. Библиотеки несут в себе одну важную мысль: возможность использовать одни и те же механизмы в разных программах. В Linux библиотеки используются повсеместно, поскольку это очень удобный способ "не изобретать велосипеды". Даже ядро Linux в каком-то смысле представляет собой библиотеку механизмов, называемых системными вызовами.
Статическая библиотека - это просто архив объектных файлов, который подключается к программе во время линковки. Эффект такой же, как если бы вы подключали каждый из файлов отдельно.
В отличие от статических библиотек, код совместно используемых (динамических) библиотек не включается в бинарник. Вместо этого в бинарник включается только ссылка на библиотеку.
Рассмотрим преимущества и недостатки статических и совместно используемых библиотек. Статические библиотеки делают программу более автономной: программа, скомпонованная со статической библиотекой может запускаться на любом компьютере, не требуя наличия этой библиотеки (она уже "внутри" бинарника). Программа, скомпонованная с динамической библиотекой, требует наличия этой библиотеки на том компьютере, где она запускается, поскольку в бинарнике не код, а ссылка на код библиотеки. Не смотря на такую зависимость, динамические библиотеки обладают двумя существенными преимуществами. Во-первых, бинарник, скомпонованный с совместно используемой библиотекой меньше размером, чем такой же бинарник, с подключенной к нему статической библиотекой (статически скомпонованный бинарник). Во-вторых, любая модернизация динамической библиотеки, отражается на всех программах, использующих ее. Таким образом, если некоторую библиотеку foo используют 10 программ, то исправление какой-нибудь ошибки в foo или любое другое улучшение библиотеки автоматически улучшает все программы, которые используют эту библиотеку. Именно поэтому динамические библиотеки называют совместно используемыми. Чтобы применить изменения, внесенные в статическую библиотеку, нужно пересобрать все 10 программ.
В Linux статические библиотеки обычно имеют расширение .a (Archive), а совместно используемые библиотеки имеют расширение .so (Shared Object). Хранятся библиотеки, как правило, в каталогах /lib и /usr/lib. В случае иного расположения (относится только к совместно используемым библиотекам), приходится немного "подшаманить", чтобы программа запустилась.
Теперь давайте создадим свою собственную библиотеку, располагающую двумя функциями: h_world() и g_world(), которые выводят на экран "Hello World" и "Goodbye World" соответственно. Начнем со статической библиотеки.
Начнем с интерфейса. Создадим файл world.h:
/* world.h */ void h_world (void); void g_world (void);Здесь просто объявлены функции, которые будут использоваться.
Теперь надо реализовать серверы. Создадим файл h_world.c:
/* h_world.c */ #include <stdio.h> #include "world.h" void h_world (void) { printf ("Hello World\n"); }Теперь создадим файл g_world.c, содержащий реализацию функции g_world():
/* g_world.c */ #include <stdio.h> #include "world.h" void g_world (void) { printf ("Goodbye World\n"); }Можно было бы с таким же успехом уместить обе функции в одном файле (hello.c, например), однако для наглядности мы разнесли код на два файла.
Теперь создадим файл main.c. Это клиент, который будет пользоваться услугами сервера:
/* main.c */ #include "world.h" int main (void) { h_world (); g_world (); }
Теперь напишем сценарий для make. Для этого создаем Makefile:
# Makefile for World project binary: main.o libworld.a gcc -o binary main.o -L. -lworld main.o: main.c gcc -c main.c libworld.a: h_world.o g_world.o ar cr libworld.a h_world.o g_world.o h_world.o: h_world.c gcc -c h_world.c g_world.o: g_world.c gcc -c g_world.c clean: rm -f *.o *.a binaryНе забывайте ставить табуляции перед каждым правилом в целевых связках.
Собираем программу:
$ make gcc -c main.c gcc -c h_world.c gcc -c g_world.c ar cr libworld.a h_world.o g_world.o gcc -o binary main.o -L. -lworld $
Осталось только проверить, работает ли программа и разобраться, что же мы такое сделали:
$ ./binary Hello World Goodbye World $
Итак, в приведенном примере появились три новые вещи: опции -l и -L компилятора, а также команда ar. Начнем с последней. Как вы уже догадались, команда ar создает статическую библиотеку (архив). В нашем случае два объектных файла объединяются в один файл libworld.a. В Linux практически все библиотеки имеют префикс lib.
Как уже говорилось, компилятор gcc сам вызывает линковщик, когда это нужно. Опция -l, переданная компилятору, обрабатывается и посылается линковщику для того, чтобы тот подключил к бинарнику библиотеку. Как вы уже заметили, у имени библиотеки "обрублены" префикс и суффикс. Это делается для того, чтобы создать "видимое безразличие" между статическими и динамическими библиотеками. Но об этом речь пойдет в других главах книги. Сейчас важно знать лишь то, что и библиотека libfoo.so и библиотека libfoo.a подключаются к проекту опцией -lfoo. В нашем случае libworld.a "урезалось" до -lworld.
Опция -L указывает линковщику, где ему искать библиотеку. В случае, если библиотека располагается в каталоге /lib или /usr/lib, то вопрос отпадает сам собой и опция -L не требуется. В нашем случае библиотека находится в репозитории (в текущем каталоге). По умолчанию линковщик не просматривает текущий каталог в поиске библиотеки, поэтому опция -L. (точка означает текущий каталог) необходима.
Для того, чтобы создать и использовать динамическую (совместно используемую) библиотеку, достаточно переделать в нашем проекте Makefile.
# Makefile for World project binary: main.o libworld.so gcc -o binary main.o -L. -lworld -Wl,-rpath,. main.o: main.c gcc -c main.c libworld.so: h_world.o g_world.o gcc -shared -o libworld.so h_world.o g_world.o h_world.o: h_world.c gcc -c -fPIC h_world.c g_world.o: g_world.c gcc -c -fPIC g_world.c clean: rm -f *.o *.so binary
Внешне ничего не изменилось: программа компилируется, запускается и выполняет те же самые действия, что и в предыдущем случае. Изменилась внутренняя суть, которая играет для программиста первоочередную роль. Рассмотрим все по порядку.
Правило для сборки binary теперь содержит пугающую опцию -Wl,-rpath,. Ничего страшного тут нет. Как уже неоднократно говорилось, компилятор gcc сам вызывает линковщик ld, когда это надо и передает ему нужные параметры сборки, избавляя нас от ненужной платформенно-зависимой волокиты. Но иногда мы все-таки должны вмешаться в этот процесс и передать линковщику "свою" опцию. Для этого используется опция компилятора -Wl,option,optargs,... Расшифровываю: передать линковщику (-Wl) опцию option с аргументами optargs. В нашем случае мы передаем линковщику опцию -rpath с аргументом . (точка, текущий каталог). Возникает вопрос: что означает опция -rpath? Как уже говорилось, линковщик ищет библиотеки в определенных местах; обычно это каталоги /lib и /usr/lib, иногда /usr/local/lib. Опция -rpath просто добавляет к этому списку еще один каталог. В нашем случае это текущий каталог. Без указания опции -rpath, линковщик "молча" соберет программу, но при запуске нас будет ждать сюрприз: программа не запустится из-за отсутствия библиотеки. Попробуйте убрать опцию -Wl,-rpath,. из Makefile и пересоберите проект. При попытке запуска программа binary завершится с кодом возврата 127 (о кодах возврата будет рассказано в последующих главах). То же самое произойдет, если вызвать программу из другого каталога. Верните обратно -Wl,-rpath,., пересоберите проект, поднимитесь на уровень выше командой cd .. и попробуйте запустить бинарник командой world/binary. Ничего не получится, поскольку в новом текущем каталоге библиотеки нет.
Есть один способ не передавать линковщику дополнительных опций при помощи -Wl - это использование переменной окружения LD_LIBRARY_PATH. В последующих главах мы будем подробно касаться темы окружения (environment). Сейчас лишь скажу, что у каждого пользователя есть так называемое окружение (environment) представляющее собой набор пар ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ, используемых программами. Чтобы посмотреть окружение, достаточно набрать команду env. Чтобы добавить в окружение переменную, достаточно набрать export ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ, а чтобы удалить переменную из окружения, надо набрать export -n ПЕРЕМЕННАЯ. Будьте внимательны: export - это внутреннаяя команда оболочки BASH; в других оболочках (csh, ksh, ...) используются другие команды для работы с окружением. Переменная окружения LD_LIBRARY_PATH содержит список дополнительных "мест", разделенных двоеточиеями, где линковщих должен искать библиотеку.
Не смотря на наличие двух механизмов передачи информации о нестандартном расположении библиотек, лучше помещать библиотеки в конечных проектах в /lib и в /usr/lib. Допускается расположение библиотек в подкаталоги /usr/lib и в /usr/local/lib (с указанем -Wl,-rpath). Но заставлять конечного пользователя устанавливать LD_LIBRARY_PATH почти всегда является плохим стилем программирования.
Следующая немаловажная деталь - это процесс создания самой библиотеки. Статические библиотеки создаются при помощи архиватора ar, а совместно используемые - при помощи gcc с опцией -shared. В данном случае gcc опять же вызывает линковщик, но не для сборки бинарника, а для создания динамической библиотеки.
Последнее отличие - опциии -fPIC (-fpic) при компиляции h_world.c и g_world.c. Эта опция сообщает компилятору, что объектные файлы, полученные в результате компиляции должны содержать позиционно-независимый код (PIC - Position Independent Code), который используется в динамических библиотеках. В таком коде используются не фиксированные позиции (адреса), а плавающие, благодаря чему код из библиотеки имеет возможность подключаться к программе в момент запуска.
Окружение (environment) или среда - это набор пар ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ, доступный каждому пользовательскому процессу. Иными словами, окружение - это набор переменных окружения. Если вы используете оболочку, отличную от bash, то не все примеры этой главы могут быть воспроизведены.
Для того, чтобы посмотреть окружение, просто введите команду env без аргументов. В зависимости от конфигурации системы, вывод env может занять несколько экранов, поэтому лучше сделать так:
$ env > myenv $Или так:
$ env | moreИли так:
$ env | less $
Переменные окружения могут формироваться как из заглавных, так и из строчных символов, однако исторически сложилось именовать их в верхнем регистре. Мы также не будем отступать от этого неписанного правила.
Про полезность окружения можно говорить долго, но основное его назначение - заставить одни и те же программы работать у разных пользователей по-разному. Приятно, например, когда программа "угадывает" имя пользователя или домашний каталог пользователя. Чаще всего такая информация "добывается" из переменных окружения USER и HOME соответственно.
Значение каждой переменной окружения изначально представляет собой строковую константу (строку). Интерпретация значений переменных полностью возлагается на программу. Иными словами, все переменные окружения имеют тип char*, а само окружение имеет тип char**. Чтобы вывести на экран значение какой-нибудь переменной окружения, достаточно набрать echo $ИМЯ_ПЕРЕМЕННОЙ:
$ echo $USER nn $ echo $HOME /home/nn $Вообще говоря, при работе с оболочкой bash, запись $ИМЯ_ПЕРЕМЕННОЙ заменяется на само значение переменной, если только эта запись не встречается в кавычках, апострофах или в комментариях. В моем случае, например, запись $HOME заменяется на /home/nn. То есть команда mkdir $HOME/mynewdir создаст в моем домашнем каталоге подкаталог mynewdir.
В разных системах и у разных пользователей окружение отличается не только значениями переменных, но и наличием/отсутствием этих переменных. Пользователи, использующие универсальные MUA (Mail User Agent), наподобие Mozilla-mail, Kmail или Sylpheed вряд ли будут иметь в своем окружении (по крайней мере с пользой) переменные MAIL или MAILDIR. А пользователям mutt, pine или elm (с довесками в виде fetchmail/getmail, procmail и проч.) эти переменные жизненно необходимы. Пользователь, не использующий графические оболочки, вряд ли будет иметь в своем окружении переменную QTDIR. Ниже приведены те переменные окружения, которые есть почти у всех пользователей Linux:
Помимо переменных окружения, командные оболочки, такие как bash располагают собственным набором пар ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ. Это переменные оболочки. Набор таких переменных называют окружением (или средой) оболочки. Эти переменные чем-то напоминают локальные (стековые) переменные в языке C. Они недоступны для других программ (в том числе и для env) и используются в основном в сценариях оболочки. Чтобы задать переменную оболочки, достаточно написать в командной строке ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ.
$ MYVAR=Hello $ echo $MYVAR Hello $ env | grep MYVAR $Однако, при желании, можно включить локальную переменную оболочки в основное окружение. Для этого используется команда export:
$ export MYVAR $ env | grep MYVAR MYVAR=Hello $Можно сделать сразу так:
$ export MYNEWVAR=Goodbye $ echo $MYNEWVAR Goodbye $ env | grep MYNEWVAR MYNEWVAR=Goodbye $Прежде, чем продолжать дальше, попробуйте поиграться с переменными окружения, чтобы лучше все понять. Выясните экспериментальным путем, чувствительны ли к регистру символов переменные окружения; можно ли использовать в качестве значений переменных окружения строки, содержащие пробелы; если можно, то как?
Теперь разберемся с тем, откуда берется окружение. Любая запущенная и работающая в Linux программа - это процесс. Запуская дважды одну и ту же программу, вы получаете два процесса. У каждого процесса (кроме init) есть свой процесс-родитель. Когда вы набираете в командной строке vim, в системе появляется новый процесс, соотвествующий текстовому редактору vim; родительским процессом здесь будет оболочка (bash, например). Для самой оболочки новый процесс будет дочерним. Мы будем подробно изучать процессы в последующих главах книги. Сейчас же важно одно: новый процесс получает копию родительского окружения. Из этого правила существует несколько исключений, но мы пока об этом говорить не будем. Важно то, что у кажного процесса своя независимая копия окружения, с которой процесс может делать все что угодно. Если процесс завершается, то копия теряется; если процесс породил другой, дочерний процесс, то этот новый процесс получает копию окружения своего родителя. Мы еще неоднократно столкнемся с окружением при изучении многозадачности.
Теперь, когда мы разобрались, что такое окружение, самое время написать программу для взаимодействия с окружением. Чтобы показать, как это все работает, сначала изобретем велосипед.
В заголовочном файле unistd.h объявлен внешний двумерный массив environ:
extern char ** environ;В этом массиве хранится копия окружения процесса. Точка.
Массив не константный, но я не рекомендую вам изменять его - это опасно (для программы) и является плохим стилем программирования. Для изменения environ есть специальные механизмы, которые мы рассмотрим чуть позже. Уверен, что настоящие будущие хакеры прочитают это и сделают с точностью до "наоборот".
А читать environ нам никто не запрещал. Напишем одноименную программу (environ), которой в качестве аргумента передается имя переменной. Программа будет проверять, существует ли эта переменная в окружении; и если существует, то каково ее значение. Как мы позже узнаем, это можно было бы сделать значительно проще. Но я предупредил: мы изобретаем велосипед. Вот эта программа:
/* environ.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> extern char ** environ; /* Environment itself */ int main (int argc, char ** argv) { int i; if (argc < 2) { fprintf (stderr, "environ: Too few arguments\n"); fprintf (stderr, "Usage: environ <variable>\n"); exit (1); } for (i = 0; environ[i] != NULL; i++) { if (!strncmp (environ[i], argv[1], strlen (argv[1]))) { printf ("'%s' found\n", environ[i]); exit (0); } } printf ("'%s' not found\n", argv[1]); exit (0); }А вот Makefile для этой программы (если нужен):
# Makefile for environ environ: environ.c gcc -o environ environ.c clean: rm -f environПроверяем:
$ make gcc -o environ environ.c $ ./environ environ: Too few arguments Usage: environ <variable> $ ./environ USER 'USER=nn' found $ ./environ ABRAKADABRA 'ABRAKADABRA' not found $
В приведенном примере мы осуществили простой синтаксический анализ массива environ, так как переменные и значения представлены в нем в обычном виде (ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ). К счастью нам больше не придется осуществлять синтаксический разбор массива environ. О настоящем предназначении этого массива будет рассказано в главе, посвященной многозадачности.
В заголовочном файле stdlib.h объявлена функция getenv , которая доказывает, что в предыдущем примере мы изобрели велосипед. Ниже приведен адаптированный прототип этой функции.
char * getenv (const char * name);
Функция эта работает очень просто: если в качестве аргумента указано имя существующей переменной окружения, то функция возвращает указатель на строку, содержащую значение этой переменной; если переменная отсутствует, возвращается NULL.
Как видим, функция getenv() позволяет не осуществлять синтаксический разбор environ. Напишем новую программу, которая делает то же, что и предыдущая, только более простым способом. Назовем ее getenv по имени функции - виновника торжества.
/* getenv.c */ #include <stdio.h> #include <stdlib.h> int main (int argc, char ** argv) { if (argc < 2) { fprintf (stderr, "getenv: Too few arguments\n"); fprintf (stderr, "Usage: getenv <variable>\n"); exit (1); } char * var = getenv (argv[1]); if (var == NULL) { printf ("'%s' not found\n", argv[1]); exit (0); } printf ("'%s=%s' found\n", argv[1], var); exit (0); }
Пришла пора модифицировать окружение! Еще раз напоминаю: каждый процесс получает не доступ к окружению, а копию окружения родительского процесса (в нашем случае это командная оболочка). Чтобы добавить в окружение новую переменную или изменить существующую, используется функция setenv, объявленная в файле stdlib.h. Ниже приведен адаптированный прототип этой функции.
int setenv (const char * name, const char * value, int overwrite);
Если хотите узнать, что значит "адаптированный прототип", загляните в /usr/include/stdlib.h на объявления функций getenv() и setenv() и больше не спрашивайте ;-)
Функция setenv() устанавливает значение (второй аргумент, value) для переменной окружения (первый аргумент, name). Третий аргумент - это флаг перезаписи. При ненулевом флаге уже существующая переменная перезаписывается, при нулевом флаге переменная, если уже существует, - не перезаписывается. В случае успешного завершения setenv() возвращает нуль (даже если существующая переменная не перезаписалась при overwrite==0). Если в окружении нет места для новой переменной, то setenv() возвращает -1.
Наша новая программа setenv читает из командной строки два аргумента: имя переменной и значение этой переменной. Если переменная не может быть установлена, выводится ошибка, если ошибки не произошло, выводится результат в формате ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ. Вот эта программа:
/* setenv.c */ #include <stdio.h> #include <stdlib.h> #define FL_OVWR 0 /* Overwrite flag. You may change it. */ int main (int argc, char ** argv) { if (argc < 3) { fprintf (stderr, "setenv: Too few arguments\n"); fprintf (stderr, "Usage: setenv <variable> <value>\n"); exit (1); } if (setenv (argv[1], argv[2], FL_OVWR) != 0) { fprintf (stderr, "setenv: Cannot set '%s'\n", argv[1]); exit (1); } printf ("%s=%s\n", argv[1], getenv (argv[1])); exit (0); }
Изменяя константу FL_OVWR можно несколько изменить поведение программы по отношению к существующим переменным окружения. Еще раз напоминаю: у каждого процесса своя копия окружения, которая уничтожается при завершении процесса. Экспериментируйте!
Функция putenv(), объявленная в заголовочном файле stdlib.h вызывается с единственным аргументом - строкой формата ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ или просто ПЕРЕМЕННАЯ. Обычно такие преформатированные строки называют запросами. Если переменная отсутствует, то в окружение добавляется новая запись. Если переменная уже существует, то текущее значение перезаписывается. Если в качестве аргумента фигурирует просто имя переменной, то переменная удаляется из окружения. В случае удачного завершения, putenv() возвращает нуль и -1 - в случае ошибки.
У функции putenv() есть одна особенность: указатель на строку, переданный в качестве аргумента, становится частью окружения. Если в дальнейшем строка будет изменена, будет изменено и окружение. Это очень важный момент, о котором не следует забывать. Ниже приведен адаптированный прототип функции putenv:
int putenv (char * str);
Теперь напишем программу, использующую putenv(). Вот она:
/* putenv.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #define QUERY_MAX_SIZE 32 char * query_str; void print_evar (const char * var) { char * tmp = getenv (var); if (tmp == NULL) { printf ("%s is not set\n", var); return; } printf ("%s=%s\n", var, tmp); } int main (void) { int ret; query_str = (char *) calloc (QUERY_MAX_SIZE, sizeof(char)); if (query_str == NULL) abort (); strncpy (query_str, "FOO=foo_value1", QUERY_MAX_SIZE-1); ret = putenv (query_str); if (ret != 0) abort (); print_evar ("FOO"); strncpy (query_str, "FOO=foo_value2", QUERY_MAX_SIZE-1); print_evar ("FOO"); strncpy (query_str, "FOO", QUERY_MAX_SIZE-1); ret = putenv (query_str); if (ret != 0) abort (); print_evar ("FOO"); free (query_str); exit (0); }
Программа немного сложнее тех, что приводились ранее, поэтому разберем все по порядку. Сначала создаем для удобства функцию print_evar (PRINT Environment VARiable), которая будет отражать текущее состояние переменной окружения, переданной в качестве аргумента. В функции main() перво-наперво выделяем в куче (heap) память для буфера, в который будут помещаться запросы; заносим адрес буфера в query_str. Теперь формируем строку, и посылаем запрос в функцию putenv(). Здесь нет ничего необычного. Дальше идет демонстрация того, на чем я акцентировал внимание: простое изменение содержимого памяти по адресу, хранящемуся в query_str приводит к изменению окружения; это видно из вывода функции print_evar(). Наконец, вызываем putenv() со строкой, не содержащей символа '=' (равно). Это запрос на удаление переменной из окружения. Функция print_evar() подтверждает это.
Хочу заметить, что putenv() поддерживается не всеми версиями Unix. Если нет крайней необходимости, лучше использовать setenv() для пополнения/модификации окружения.
Функция unsetenv(), объявленная в stdlib.h, удаляет переменную из окружения. Ниже приведен адаптированный прототип этой функции.
int unsetenv (const char * name);
Прежде всего хочу обратить внимание на то, что раньше функция unsetenv() ничего не возращала (void). С выходом версии 2.2.2 библиотеки glibc (январь 2001 года) функция стала возвращать int.
Функция unsetenv() использует в качестве аргумента имя переменной окружения. Возвращаемое значение - нуль при удачном завершении и -1 в случае ошибки. Рассмотрим простую программу, которая удаляет переменную окружения USER (!!!). Для тех, кто испугался, напоминаю еще один раз: каждый процесс работает с собственной копией окружения, никак не связанной с копиями окружения других процессов, за исключением дочерних процессов, которых у нас нет. Ниже приведен исходный код программы, учитывающий исторические изменения прототипа функции unsetenv().
/* unsetenv.c */ #include <stdlib.h> #include <stdio.h> #include <string.h> #include <gnu/libc-version.h> #define OLD_LIBC_VERSION 0 #define NEW_LIBC_VERSION 1 #define E_VAR "USER" int libc_cur_version (void) { int ret = strcmp (gnu_get_libc_version (), "2.2.2"); if (ret < 0) return OLD_LIBC_VERSION; return NEW_LIBC_VERSION; } int main (void) { int ret; char * str; if (libc_cur_version () == OLD_LIBC_VERSION) { unsetenv (E_VAR); } else { ret = unsetenv (E_VAR); if (ret != 0) { fprintf (stderr, "Cannot unset '%s'\n", E_VAR); exit (1); } } str = getenv (E_VAR); if (str == NULL) { printf ("'%s' has removed from environment\n", E_VAR); } else { printf ("'%s' hasn't removed\n", E_VAR); } exit (0); }
В программе показан один из самых варварских способов подстроить код под версию библиотеки. Это сделано исключительно для демонстрации двух вариантов unsetenv(). Никогда не делайте так в реальных программах. Намного проще и дешевле (в плане времени), не получая ничего от unsetenv() проверить факт удаления переменной при помощи getenv().
Функция clearenv(), объявленная в заголовочном файле stdlib.h, используется крайне редко для полной очистки окружения. clearenv() поддерживается не всеми версиями Unix. Ниже приведен ее прототип.
int clearenv (void);
При успешном завершении clearenv() возвращает нуль. В случае ошибки возвращается ненулевое значение.
В большинстве случаев вместо clearenv() можно использовать следующую инструкцию:
environ = NULL;
В языке 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() и некоторые другие.
В языке 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, чтобы узнать это значение.
$ ulimit -n 1024 $Если вы работаете с оболочкой C-shell (csh, tcsh), то в вашем распоряжении команда limit:
$ limit descriptors descriptors 1024 $
В командной оболочке, в которой вы работаете (bash, например), открыты три файла: стандартный ввод (дескриптор 0), стандартный вывод (дескриптор 1) и стандартный поток ошибок (дескриптор 2). Когда под оболочкой запускается программа, в системе создается новый процесс, который является для этой оболочки дочерним процессом, следовательно, получает копию таблицы дескрипторов своего родителя (то есть все открытые файлы родительского процесса). Таким образом программа может осуществлять консольный ввод-вывод через эти дескрипторы. На протяжении всей книги мы будем часто играть с этими дескрипторами.
Таблица дескрипторов, помимо всего прочего, содержит информацию о текущей позиции чтения-записи для каждого дескриптора. При открытии файла, позиция чтения-записи устанавливается в ноль. Каждый прочитанный или записанный байт увеличивает на единицу указатель текущей позиции. Мы вернемся к этой теме в разделе 5.7.
Чтобы получить возможность прочитать что-то из файла или записать что-то в файл, его нужно открыть. Это делает системный вызов open(). Этот системный вызов не имеет постоянного списка аргументов (за счет использования механизма va_arg); в связи с этим существуют две "разновидности" open(). Не только в С++ есть перегрузка функций ;-) Если интересно, то о механизме va_arg можно прочитать на man-странице stdarg (man 3 stdarg) или в книге Б. Кернигана и Д. Ритчи "Язык программирования Си". Ниже приведены адаптированные прототипы системного вызова open().
int open (const char * filename, int flags, mode_t mode); int open (const char * filename, int flags);
Системный вызов open() объявлен в заголовочном файле fcntl.h. Ниже приведен общий адаптированный прототип open().
int open (const char * filename, int flags, ...);
Начнем по порядку. Первый аргумент - имя файла в файловой системе в обычной форме: полный путь к файлу (если файл не находится в текущем каталоге) или сокращенное имя (если файл в текущем каталоге).
Второй аргумент - это режим открытия файла, представляющий собой один или несколько флагов открытия, объединенных оператором побитового ИЛИ. Список доступных флагов приведен в Таблице 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. Позже, в последующих главах книги мы научимся распознавать ошибки системных вызовов.
Системный вызов close() закрывает файл. Вообще говоря, по завершении процесса все открытые файлы (кроме файлов с дескрипторами 0, 1 и 2) автоматически закрываются. Тем не менее, это не освобождает нас от самостоятельного вызова close(), когда файл нужно закрыть. К тому же, если файлы не закрывать самостоятельно, то соответствующие дескрипторы не освобождаются, что может привести к превышению лимита открытых файлов. Простой пример: приложение может быть настроено так, чтобы каждую минуту открывать и перечитывать свой файл конфигурации для проверки обновлений. Если каждый раз файл не будет закрываться, то в моей системе, например, приложение может "накрыться медным тазом" примерно через 17 часов. Автоматически! Кроме того, файловая система Linux поддерживает механизм буферизации. Это означает, что данные, которые якобы записываются, реально записываются на носитель (синхронизируются) только через какое-то время, когда система сочтет это правильным и оптимальным. Это повышает производительность системы и даже продлевает ресурс жестких дисков. Системный вызов close() не форсирует запись данных на диск, однако дает больше гарантий того, что данные останутся в целости и сохранности.
Системный вызов close() объявлен в файле unistd.h. Ниже приведен его адаптированный прототип.
int close (int fd);
Очевидно, что единственный аргумент - это файловый дескриптор. Возвращаемое значение - ноль в случае успеха, и -1 - в случае ошибки. Довольно часто close() вызывают без проверки возвращаемого значения. Это не очень грубая ошибка, но, тем не менее, иногда закрытие файла бывает неудачным (в случае неправильного дескриптора, в случае прерывания функции по сигналу или в случае ошибки ввода-вывода, например). В любом случае, если программа сообщит пользователю, что файл невозможно закрыть, это хорошо.
Теперь можно написать простенкую программу, использующую системные вызовы open() и close(). Мы еще не умеем читать из файлов и писать в файлы, поэтому напишем программу, которая создает файл с именем, переданным в качестве аргумента (argv[1]) и с правами доступа 0600 (чтение и запись для пользователя). Ниже приведен исходный код программы.
/* openclose.c */ #include <fcntl.h> /* open() and O_XXX flags */ #include <sys/stat.h> /* S_IXXX flags */ #include <sys/types.h> /* mode_t */ #include <unistd.h> /* close() */ #include <stdlib.h> #include <stdio.h> int main (int argc, char ** argv) { int fd; mode_t mode = S_IRUSR | S_IWUSR; int flags = O_WRONLY | O_CREAT | O_EXCL; if (argc < 2) { fprintf (stderr, "openclose: Too few arguments\n"); fprintf (stderr, "Usage: openclose <filename>\n"); exit (1); } fd = open (argv[1], flags, mode); if (fd < 0) { fprintf (stderr, "openclose: Cannot open file '%s'\n", argv[1]); exit (1); } if (close (fd) != 0) { fprintf (stderr, "Cannot close file (descriptor=%d)\n", fd); exit (1); } exit (0); }
Обратите внимание, если запустить программу дважды с одним и тем же аргументом, то на второй раз open() выдаст ошибку. В этом виноват флаг O_EXCL (см. Таблицу 4 Приложения 2), который "дает добро" только на создание еще не существующих файлов. Наглядности ради, флаги открытия и флаги режима мы занесли в отдельные переменные, однако можно было бы сделать так:
fd = open (argv[1], O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);Или так:
fd = open (argv[1], O_WRONLY | O_CREAT | O_EXCL, 0600);
Системный вызов read(), объявленный в файле unistd.h, позволяет читать данные из файла. В отличие от библиотечных функций файлового ввода-вывода, которые предоставляют возможность интерпретации считываемых данных. Можно, например, записать в файл следующее содержимое:
2006
Теперь, используя библиотечные механизмы, можно читать файл по-разному:
fscanf (filep, "%s", buffer); fscanf (filep, "%d", number);
Системный вызов read() читает данные в "сыром" виде, то есть как последовательность байт, без какой-либо интерпретации. Ниже представлен адаптированный прототип read().
ssize_t read (int fd, void * buffer, size_t count);
Первый аргумент - это файловый дескриптор. Здесь больше сказать нечего. Второй аргумент - это указатель на область памяти, куда будут помещаться данные. Третий аргумент - количество байт, которые функция read() будет пытаться прочитать из файла. Возвращаемое значение - количество прочитанных байт, если чтение состоялось и -1, если произошла ошибка. Хочу заметить, что если read() возвращает значение меньше count, то это не символизирует об ошибке.
Хочу сказать несколько слов о типах. Тип size_t в Linux используется для хранения размеров блоков памяти. Какой тип реально скрывается за size_t, зависит от архитектуры; как правило это unsigned long int или unsigned int. Тип ssize_t (Signed SIZE Type) - это тот же size_t, только знаковый. Используется, например, в тех случаях, когда нужно сообщить об ошибке, вернув отрицательный размер блока памяти. Системный вызов read() именно так и поступает.
Теперь напишем программу, которая просто читает файл и выводит его содержимое на экран. Имя файла будет передаваться в качестве аргумента (argv[1]). Ниже приведен исходный код этой программы.
/* myread.c */ #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> int main (int argc, char ** argv) { int fd; ssize_t ret; char ch; if (argc < 2) { fprintf (stderr, "Too few arguments\n"); exit (1); } fd = open (argv[1], O_RDONLY); if (fd < 0) { fprintf (stderr, "Cannot open file\n"); exit (1); } while ((ret = read (fd, &ch, 1)) > 0) { putchar (ch); } if (ret < 0) { fprintf (stderr, "myread: Cannot read file\n"); exit (1); } close (fd); exit (0); }
В этом примере используется укороченная версия open(), так как файл открывается только для чтения. В качестве буфера (второй аргумент read()) мы передаем адрес переменной типа char. По этому адресу будут считываться данные из файла (по одному байту за раз) и передаваться на стандартный вывод. Цикл чтения файла заканчивается, когда read() возвращает нуль (нечего больше читать) или -1 (ошибка). Системный вызов close() закрывает файл.
Как можно заметить, в нашем примере системный вызов read() вызывается ровно столько раз, сколько байт содержится в файле. Иногда это действительно нужно; но не здесь. Чтение-запись посимвольным методом (как в нашем примере) значительно замедляет процесс ввода-вывода за счет многократных обращений к системным вызовам. По этой же причине возрастает вероятность возникновения ошибки. Если нет действительной необходимости, файлы нужно читать блоками. О том, какой размер блока предпочтительнее, будет рассказано в последующих главах книги. Ниже приведен исходный код программы, которая делает то же самое, что и предыдущий пример, но с использованием блочного чтения файла. Размер блока установлен в 64 байта.
/* myread1.c */ #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #define BUFFER_SIZE 64 int main (int argc, char ** argv) { int fd; ssize_t read_bytes; char buffer[BUFFER_SIZE+1]; if (argc < 2) { fprintf (stderr, "Too few arguments\n"); exit (1); } fd = open (argv[1], O_RDONLY); if (fd < 0) { fprintf (stderr, "Cannot open file\n"); exit (1); } while ((read_bytes = read (fd, buffer, BUFFER_SIZE)) > 0) { buffer[read_bytes] = 0; /* Null-terminator for C-string */ fputs (buffer, stdout); } if (read_bytes < 0) { fprintf (stderr, "myread: Cannot read file\n"); exit (1); } close (fd); exit (0); }
Теперь можно примерно оценить и сравнить скорость работы двух примеров. Для этого надо выбрать в системе достаточно большой файл (бинарник ядра или видеофильм, например) и посмотреть на то, как быстро читаются эти файлы:
$ time ./myread /boot/vmlinuz > /dev/null real 0m1.443s user 0m0.383s sys 0m1.039s $ time ./myread1 /boot/vmlinuz > /dev/null real 0m0.055s user 0m0.010s sys 0m0.023s $
Для записи данных в файл используется системный вызов write(). Ниже представлен его прототип.
ssize_t write (int fd, const void * buffer, size_t count);
Как видите, прототип write() отличается от read() только спецификатором const во втором аргументе. В принципе write() выполняет процедуру, обратную read(): записывает count байтов из буфера buffer в файл с дескриптором fd, возвращая количество записанных байтов или -1 в случае ошибки. Так просто, что можно сразу переходить к примеру. За основу возьмем программу myread1 из предыдущего раздела.
/* rw.c */ #include <stdlib.h> #include <stdio.h> #include <unistd.h> /* read(), write(), close() */ #include <fcntl.h> /* open(), O_RDONLY */ #include <sys/stat.h> /* S_IRUSR */ #include <sys/types.h> /* mode_t */ #define BUFFER_SIZE 64 int main (int argc, char ** argv) { int fd; ssize_t read_bytes; ssize_t written_bytes; char buffer[BUFFER_SIZE]; if (argc < 2) { fprintf (stderr, "Too few arguments\n"); exit (1); } fd = open (argv[1], O_RDONLY); if (fd < 0) { fprintf (stderr, "Cannot open file\n"); exit (1); } while ((read_bytes = read (fd, buffer, BUFFER_SIZE)) > 0) { /* 1 == stdout */ written_bytes = write (1, buffer, read_bytes); if (written_bytes != read_bytes) { fprintf (stderr, "Cannot write\n"); exit (1); } } if (read_bytes < 0) { fprintf (stderr, "myread: Cannot read file\n"); exit (1); } close (fd); exit (0); }
В этом примере нам уже не надо изощеряться в попытках вставить нуль-терминатор в строку для записи, поскольку системный вызов write() не запишет большее количество байт, чем мы ему указали. В данном случае для демонстрации write() мы просто записывали данные в файл с дескриптором 1, то есть в стандартный вывод. Но прежде, чем переходить к чтению следующего раздела, попробуйте самостоятельно записать что-нибудь (при помощи write(), естественно) в обычный файл. Когда будете открывать файл для записи, обратите пожалуйста внимание на флаги O_TRUNC, O_CREAT и O_APPEND. Подумайте, все ли флаги сочетаются между собой по смыслу.
Как уже говорилось, с каждым открытым файлом связано число, указывающее на текущую позицию чтения-записи. При открытии файла позиция равна нулю. Каждый вызов read() или write() увеличивает текущую позицию на значение, равное числу прочитанных или записанных байт. Благодаря этому механизму, каждый повторный вызов read() читает следующие данные, и каждый повторный write() записывает данные в продолжение предыдущих, а не затирает старые. Такой механизм последовательного доступа очень удобен, однако иногда требуется получить произвольный доступ к содержимому файла, чтобы, например, прочитать или записать файл заново.
Для изменения текущей позиции чтения-записи используется системный вызов lseek(). Ниже представлен его прототип.
off_t lseek (int fd, ott_t offset, int against);
Первый аргумент, как всегда, - файловый дескриптор. Второй аргумент - смещение, как положительное (вперед), так и отрицательное (назад). Третий аргумент обычно передается в виде одной из трех констант SEEK_SET, SEEK_CUR и SEEK_END, которые показывают, от какого места отсчитывается смещение. SEEK_SET - означает начало файла, SEEK_CUR - текущая позиция, SEEK_END - конец файла. Рассмотрим следующие вызовы:
lseek (fd, 0, SEEK_SET); lseek (fd, 20, SEEK_CUR); lseek (fd, -10, SEEK_END);
Первый вызов устанавливает текущую позицию в начало файла. Второй вызов смещает позицию вперед на 20 байт. В третьем случае текущая позиция перемещается на 10 байт назад относительно конца файла.
В случае удачного завершения, lseek() возвращает значение установленной "новой" позиции относительно начала файла. В случае ошибки возвращается -1.
Я долго думал, какой бы пример придумать, чтобы продемонстрировать работу lseek() наглядным образом. Наиболее подходящим примером мне показалась идея создания программы рисования символами. Программа оказалась не слишком простой, однако если вы сможете разобраться в ней, то можете считать, что успешно овладели азами низкоуровневого ввода-вывода Linux. Ниже представлен исходный код этой программы.
/* draw.c */ #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> /* memset() */ #define N_ROWS 15 /* Image height */ #define N_COLS 40 /* Image width */ #define FG_CHAR 'O' /* Foreground character */ #define IMG_FN "image" /* Image filename */ #define N_MIN(A,B) ((A)<(B)?(A):(B)) #define N_MAX(A,B) ((A)>(B)?(A):(B)) static char buffer[N_COLS]; void init_draw (int fd) { ssize_t bytes_written = 0; memset (buffer, ' ', N_COLS); buffer [N_COLS] = '\n'; while (bytes_written < (N_ROWS * (N_COLS+1))) bytes_written += write (fd, buffer, N_COLS+1); } void draw_point (int fd, int x, int y) { char ch = FG_CHAR; lseek (fd, y * (N_COLS+1) + x, SEEK_SET); write (fd, &ch, 1); } void draw_hline (int fd, int y, int x1, int x2) { size_t bytes_write = abs (x2-x1) + 1; memset (buffer, FG_CHAR, bytes_write); lseek (fd, y * (N_COLS+1) + N_MIN (x1, x2), SEEK_SET); write (fd, buffer, bytes_write); } void draw_vline (int fd, int x, int y1, int y2) { int i = N_MIN(y1, y2); while (i <= N_MAX(y2, y1)) draw_point (fd, x, i++); } int main (void) { int a, b, c, i = 0; char ch; int fd = open (IMG_FN, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { fprintf (stderr, "Cannot open file\n"); exit (1); } init_draw (fd); char * icode[] = { "v 1 1 11", "v 11 7 11", "v 14 5 11", "v 18 6 11", "v 21 5 10", "v 25 5 10", "v 29 5 6", "v 33 5 6", "v 29 10 11", "v 33 10 11", "h 11 1 8", "h 5 16 17", "h 11 22 24", "p 11 5 0", "p 15 6 0", "p 26 11 0", "p 30 7 0", "p 32 7 0", "p 31 8 0", "p 30 9 0", "p 32 9 0", NULL }; while (icode[i] != NULL) { sscanf (icode[i], "%c %d %d %d", &ch, &a, &b, &c); switch (ch) { case 'v': draw_vline (fd, a, b, c); break; case 'h': draw_hline (fd, a, b, c); break; case 'p': draw_point (fd, a, b); break; default: abort(); } i++; } close (fd); exit (0); }
Теперь разберемся, как работает эта программа. Изначально "полотно" заполняется пробелами. Функция 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(), диапазон координат и проч.). Попробуйте включить эти проверки самостоятельно.
Эта глава открывает большую и очень важную для Linux-программиста тему многозадачности. Описать все сразу не получится, поэтому мы будем неоднократно возвращаться к многозадачности в последующих главах книги. Пристегните ремни покрепче!
Наберите в своей оболочке следующую команду:
$ ps -e
На экран будут выведен список всех работающих в системе процессов. Если хотите посчитать количество процессов, наберите что-нибудь, набодобие этого:
$ ps -e --no-headers | nl | tail -n 1 74 4650 pts/0 00:00:00 tail $
Первое число - это количество работающих в системе процессов. Пользователи KDE могут воспользоваться программой kpm, а пользователи Gnome - программой gnome-system-monitor для получения информации о процессах. На то он и Linux, чтобы позволять пользователю делать одно и то же разными способами.
Возникает вопрос: "Что такое процесс?". Процессы в Linux, как и файлы, являются аксиоматическими понятиями. Иногда процесс отождествляют с запущенной программой, однако это не всегда так. Будем считать, что процесс - это рабочая единица системы, которая выполняет что-то. Многозадачность - это возможность одновременного сосуществования нескольких процессов в одной системе.
Linux - многозадачная операционная система. Это означает что процессы в ней работают одновременно. Естественно, это условная формулировка. Ядро Linux постоянно переключает процессы, то есть время от времени дает каждому из них сколько-нибудь процессорного времени. Переключение происходит довольно быстро, поэтому нам кажется, что процессы работают одновременно.
Одни процессы могут порождать другие процессы, образовывая древовидную структуру. Порождающие процессы называются родителями или родительскими процессами, а порожденные - потомками или дочерними процессами. На вершине этого "дерева" находится процесс init, который порождается автоматически ядром в процесссе загрузки системы.
К каждому процессу в системе привязана пара целых неотрицательных чисел: идентификатор процесса PID (Process IDentifier) и идентификатор родительского процесса PPID (Parent Process IDentifier). Для каждого процесса PID является уникальным (в конкретный момент времени), а PPID равен идентификатору процесса-родителя. Если ввести в оболочку команду ps -ef, то на экран будет выведен список процессов со значениями их PID и PPID (вторая и третья колонки соотв.). Вот, например, что творится у меня в системе:
$ ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 06:16 ? 00:00:01 init [3] root 2 1 0 06:16 ? 00:00:00 [migration/0] root 3 1 0 06:16 ? 00:00:00 [ksoftirqd/0] root 4 1 0 06:16 ? 00:00:00 [watchdog/0] root 5 1 0 06:16 ? 00:00:00 [migration/1] root 6 1 0 06:16 ? 00:00:00 [ksoftirqd/1] root 7 1 0 06:16 ? 00:00:00 [watchdog/1] root 8 1 0 06:16 ? 00:00:00 [events/0] root 9 1 0 06:16 ? 00:00:00 [events/1] root 10 1 0 06:16 ? 00:00:00 [khelper] root 11 1 0 06:16 ? 00:00:00 [kthread] root 35 11 0 06:16 ? 00:00:00 [kblockd/0] root 36 11 0 06:16 ? 00:00:00 [kblockd/1] root 37 11 0 06:16 ? 00:00:00 [kacpid] root 216 11 0 06:16 ? 00:00:00 [kseriod] root 244 11 0 06:16 ? 00:00:00 [pdflush] root 245 11 0 06:16 ? 00:00:00 [pdflush] root 246 11 0 06:16 ? 00:00:00 [kswapd0] root 247 11 0 06:16 ? 00:00:00 [aio/0] root 248 11 0 06:16 ? 00:00:00 [aio/1] root 395 11 0 06:16 ? 00:00:00 [ata/0] root 396 11 0 06:16 ? 00:00:00 [ata/1] root 397 11 0 06:16 ? 00:00:00 [ata_aux] root 407 11 0 06:16 ? 00:00:00 [scsi_eh_0] root 408 11 0 06:16 ? 00:00:00 [scsi_eh_1] root 409 11 0 06:16 ? 00:00:00 [scsi_eh_2] root 410 11 0 06:16 ? 00:00:00 [scsi_eh_3] root 422 11 0 06:17 ? 00:00:00 [scsi_eh_4] root 423 11 0 06:17 ? 00:00:00 [scsi_eh_5] root 1406 11 0 06:17 ? 00:00:00 [kjournald] root 1443 11 0 06:17 ? 00:00:00 [ksuspend_usbd] root 1446 11 0 06:17 ? 00:00:00 [khubd] root 1462 1 0 06:17 ? 00:00:00 /sbin/udevd --daemon root 3230 11 0 06:17 ? 00:00:00 [kpsmoused] nn 3498 11591 0 12:06 pts/1 00:00:08 kate 006.html nn 3984 11591 0 12:08 pts/1 00:00:03 kate 007.html nn 4026 1 0 12:09 ? 00:00:00 kio_uiserver [kdeinit] nobody 4563 6054 0 09:15 ? 00:00:00 /usr/sbin/httpd -k start root 4652 11 0 06:17 ? 00:00:00 [khpsbpkt] root 4785 11 0 06:17 ? 00:00:00 [pccardd] root 4786 11 0 06:17 ? 00:00:00 [tifm/0] root 4840 11 0 06:17 ? 00:00:00 [knodemgrd_0] root 4849 11 0 06:17 ? 00:00:00 [kmmcd] nn 5504 6133 0 12:17 ? 00:00:00 konsole [kdeinit] nn 5505 5504 0 12:17 pts/0 00:00:00 /bin/bash root 5807 11 0 06:17 ? 00:00:00 [kjournald] root 5810 11 0 06:17 ? 00:00:00 [kjournald] root 5970 1 0 06:17 ? 00:00:00 /usr/sbin/syslog-ng root 5973 1 0 06:17 ? 00:00:00 /usr/sbin/crond root 5981 1 0 06:17 ? 00:00:00 /bin/sh /usr/bin/mysqld_safe root 5983 11 0 06:17 ? 00:00:00 [loop0] root 5984 1 0 06:17 tty1 00:00:00 /bin/login -- root 5985 1 0 06:17 tty2 00:00:00 /sbin/agetty 38400 vc/2 linux root 5986 1 0 06:17 tty3 00:00:00 /sbin/agetty 38400 vc/3 linux root 5987 1 0 06:17 tty4 00:00:00 /sbin/agetty 38400 vc/4 linux root 5988 1 0 06:17 tty5 00:00:00 /sbin/agetty 38400 vc/5 linux root 5989 1 0 06:17 tty6 00:00:00 /sbin/agetty 38400 vc/6 linux mysql 6026 5981 0 06:17 ? 00:00:00 /usr/sbin/mysqld --basedir=/usr root 6029 1 0 06:17 ? 00:00:00 /usr/sbin/cupsd postgres 6033 1 0 06:17 ? 00:00:00 /usr/bin/postmaster -D /var/lib/ root 6046 1 0 06:17 ? 00:00:00 /usr/bin/mpd /etc/mpd.conf root 6048 6046 0 06:17 ? 00:00:04 /usr/bin/mpd /etc/mpd.conf root 6049 6048 0 06:17 ? 00:00:01 /usr/bin/mpd /etc/mpd.conf root 6054 1 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start postgres 6059 6033 0 06:17 ? 00:00:00 postgres: writer process postgres 6060 6033 0 06:17 ? 00:00:00 postgres: stats buffer process postgres 6061 6060 0 06:17 ? 00:00:00 postgres: stats collector proces nobody 6062 6054 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start nobody 6063 6054 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start nobody 6064 6054 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start nobody 6065 6054 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start nobody 6066 6054 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start nn 6067 5984 0 06:28 tty1 00:00:00 -bash nn 6071 6067 0 06:28 tty1 00:00:00 xinit root 6072 6071 1 06:28 tty7 00:04:28 X :0 root 6090 6072 0 06:28 tty7 00:00:00 X :0 nn 6092 6071 0 06:28 tty1 00:00:00 /bin/sh /opt/kde/bin/startkde nn 6113 1 0 06:28 ? 00:00:00 /usr/bin/gpg-agent --daemon nn 6116 1 0 06:28 ? 00:00:00 /usr/bin/ssh-agent -s root 6132 1 0 06:28 tty1 00:00:00 start_kdeinit --new-startup +kcm nn 6133 1 0 06:28 ? 00:00:00 kdeinit Running... nn 6136 1 0 06:28 ? 00:00:00 dcopserver [kdeinit] --nosid nn 6138 6133 0 06:28 ? 00:00:00 klauncher [kdeinit] --new-startu nn 6140 1 0 06:28 ? 00:00:21 kded [kdeinit] --new-startup nn 6145 6092 0 06:28 tty1 00:00:00 kwrapper ksmserver nn 6147 1 0 06:28 ? 00:00:00 ksmserver [kdeinit] nn 6148 6133 0 06:28 ? 00:00:05 kwin [kdeinit] -session 106e6e64 nn 6150 1 0 06:28 ? 00:00:00 knotify [kdeinit] nn 6152 1 0 06:28 ? 00:00:02 kdesktop [kdeinit] nn 6154 1 0 06:28 ? 00:00:11 kicker [kdeinit] nn 6160 1 0 06:28 ? 00:00:00 kaccess [kdeinit] nn 6163 1 0 06:28 ? 00:00:00 kmix [kdeinit] -session 106e6e64 nn 6164 6133 0 06:28 ? 00:00:00 konqueror [kdeinit] --preload nn 6166 6133 0 06:28 ? 00:00:00 konqueror [kdeinit] --preload nn 6242 6133 0 06:29 ? 00:00:04 /usr/bin/python /usr/bin/sonata nn 6251 6133 0 09:24 ? 00:00:00 kio_file [kdeinit] file /tmp/kso nn 6256 1 0 06:29 ? 00:00:00 dbus-launch --autolaunch 27b9194 nn 6257 1 0 06:29 ? 00:00:00 /usr/bin/dbus-daemon --fork --pr nn 8952 1 0 06:43 ? 00:00:40 kopete nn 10460 6133 0 12:43 ? 00:00:01 konsole [kdeinit] nn 10461 10460 0 12:43 pts/2 00:00:00 /bin/bash nn 11454 10461 0 12:49 pts/2 00:00:00 ps -ef nn 11590 6133 0 06:58 ? 00:00:03 konsole [kdeinit] nn 11591 11590 0 06:58 pts/1 00:00:00 /bin/bash nn 13609 6133 0 07:07 ? 00:00:00 /bin/sh /usr/bin/firefox nn 13620 13609 0 07:07 ? 00:00:00 /bin/sh /opt/firefox/run-mozilla nn 13625 13620 1 07:07 ? 00:05:52 /opt/firefox/firefox-bin nn 13632 1 0 07:07 ? 00:00:00 /opt/gnome/libexec/gconfd-2 12 nobody 24957 6054 0 08:09 ? 00:00:00 /usr/sbin/httpd -k start
Надо отметить, что процесс init всегда имеет идентификатор 1 и PPID равный 0. Хотя в реальности процесса с идентификатором 0 не существует. Дерево процессов можно также пресставить в наглядном виде при помощи опции --forest программы ps:
$ ps -e --forest PID TTY TIME CMD 1 ? 00:00:01 init 2 ? 00:00:00 migration/0 3 ? 00:00:00 ksoftirqd/0 4 ? 00:00:00 watchdog/0 5 ? 00:00:00 migration/1 6 ? 00:00:00 ksoftirqd/1 7 ? 00:00:00 watchdog/1 8 ? 00:00:00 events/0 9 ? 00:00:00 events/1 10 ? 00:00:00 khelper 11 ? 00:00:00 kthread 35 ? 00:00:00 \_ kblockd/0 36 ? 00:00:00 \_ kblockd/1 37 ? 00:00:00 \_ kacpid 216 ? 00:00:00 \_ kseriod 244 ? 00:00:00 \_ pdflush 245 ? 00:00:00 \_ pdflush 246 ? 00:00:00 \_ kswapd0 247 ? 00:00:00 \_ aio/0 248 ? 00:00:00 \_ aio/1 395 ? 00:00:00 \_ ata/0 396 ? 00:00:00 \_ ata/1 397 ? 00:00:00 \_ ata_aux 407 ? 00:00:00 \_ scsi_eh_0 408 ? 00:00:00 \_ scsi_eh_1 409 ? 00:00:00 \_ scsi_eh_2 410 ? 00:00:00 \_ scsi_eh_3 422 ? 00:00:00 \_ scsi_eh_4 423 ? 00:00:00 \_ scsi_eh_5 1406 ? 00:00:00 \_ kjournald 1443 ? 00:00:00 \_ ksuspend_usbd 1446 ? 00:00:00 \_ khubd 3230 ? 00:00:00 \_ kpsmoused 4652 ? 00:00:00 \_ khpsbpkt 4785 ? 00:00:00 \_ pccardd 4786 ? 00:00:00 \_ tifm/0 4840 ? 00:00:00 \_ knodemgrd_0 4849 ? 00:00:00 \_ kmmcd 5807 ? 00:00:00 \_ kjournald 5810 ? 00:00:00 \_ kjournald 5983 ? 00:00:00 \_ loop0 1462 ? 00:00:00 udevd 5970 ? 00:00:00 syslog-ng 5973 ? 00:00:00 crond 5981 ? 00:00:00 mysqld_safe 6026 ? 00:00:00 \_ mysqld 5984 tty1 00:00:00 login 6067 tty1 00:00:00 \_ bash 6071 tty1 00:00:00 \_ xinit 6072 tty7 00:04:40 \_ X 6090 tty7 00:00:00 | \_ X 6092 tty1 00:00:00 \_ startkde 6145 tty1 00:00:00 \_ kwrapper 5985 tty2 00:00:00 agetty 5986 tty3 00:00:00 agetty 5987 tty4 00:00:00 agetty 5988 tty5 00:00:00 agetty 5989 tty6 00:00:00 agetty 6029 ? 00:00:00 cupsd 6033 ? 00:00:00 postmaster 6059 ? 00:00:00 \_ postmaster 6060 ? 00:00:00 \_ postmaster 6061 ? 00:00:00 \_ postmaster 6046 ? 00:00:00 mpd 6048 ? 00:00:04 \_ mpd 6049 ? 00:00:01 \_ mpd 6054 ? 00:00:00 httpd 6062 ? 00:00:00 \_ httpd 6063 ? 00:00:00 \_ httpd 6064 ? 00:00:00 \_ httpd 6065 ? 00:00:00 \_ httpd 6066 ? 00:00:00 \_ httpd 24957 ? 00:00:00 \_ httpd 4563 ? 00:00:00 \_ httpd 6113 ? 00:00:00 gpg-agent 6116 ? 00:00:00 ssh-agent 6132 tty1 00:00:00 start_kdeinit 6133 ? 00:00:00 kdeinit 6138 ? 00:00:00 \_ klauncher 6148 ? 00:00:05 \_ kwin 6164 ? 00:00:00 \_ konqueror 6166 ? 00:00:00 \_ konqueror 6242 ? 00:00:04 \_ sonata 11590 ? 00:00:03 \_ konsole 11591 pts/1 00:00:00 | \_ bash 3498 pts/1 00:00:09 | \_ kate 3984 pts/1 00:00:03 | \_ kate 13609 ? 00:00:00 \_ firefox 13620 ? 00:00:00 | \_ run-mozilla.sh 13625 ? 00:05:56 | \_ firefox-bin 6251 ? 00:00:00 \_ kio_file 5504 ? 00:00:00 \_ konsole 5505 pts/0 00:00:00 | \_ bash 10460 ? 00:00:01 \_ konsole 10461 pts/2 00:00:00 \_ bash 12140 pts/2 00:00:00 \_ ps 6136 ? 00:00:00 dcopserver 6140 ? 00:00:21 kded 6147 ? 00:00:00 ksmserver 6150 ? 00:00:00 knotify 6152 ? 00:00:02 kdesktop 6154 ? 00:00:11 kicker 6160 ? 00:00:00 kaccess 6163 ? 00:00:00 kmix 6256 ? 00:00:00 dbus-launch 6257 ? 00:00:00 dbus-daemon 8952 ? 00:00:40 kopete 13632 ? 00:00:00 gconfd-2 4026 ? 00:00:00 kio_uiserver
Если вызвать программу ps без аргументов, то будет выведен список процессов, принадлежащих текущей группе, то есть работающих под текущим терминалом. О том, что такое терминалы и группы процессов, будет рассказано в последующих главах.
Процесс может узнать свой идентификатор (PID), а также родительский идентификатор (PPID) при помощи системных вызовов getpid() и getppid().
Системные вызовы getpid() и getppid() имеют следующие прототипы:
pid_t getpid (void); pid_t getppid (void);
Для использования getpid() и getppid() в программу должны быть включены директивой #include заголовочные файлы unistd.h и sys/types.h (для типа pid_t). Вызов getpid() возвращает идентификатор текущего процесса (PID), а getppid() возвращает идентификатор родителя (PPID). pid_t - это целый тип, размерность которого зависит от конкретной системы. Значениями этого типа можно оперировать как обычными целыми числами типа int.
Рассмотрим теперь простую программу, которая выводит на экран PID и PPID, а затем "замирает" до тех пор, пока пользователь не нажмет <Enter>.
/* getpid.c */ #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main (void) { pid_t pid, ppid; pid = getpid (); ppid = getppid (); printf ("PID: %d\n", pid); printf ("PPID: %d\n", ppid); fprintf (stderr, "Press <Enter> to exit..."); getchar (); return 0; }
Проверим теперь, как работает эта программа. Для этого откомпилируем и запустим ее:
$ gcc -o getpid getpid.c $ ./getpid PID: 27784 PPID: 6814 Press <Enter> to exit...
Теперь, не нажимая <Enter>, откроем другое терминальное окно и проверим, правильность работы системных вызовов getpid() и getppid():
$ ps -ef | grep getpid nn 27784 6814 0 01:05 pts/0 00:00:00 ./getpid nn 28249 28212 0 01:07 pts/1 00:00:00 grep getpid
Как уже говорилось ранее, процесс в Linux - это нечто, выполняющее программный код. Этот код называют образом процесса (process image). Рассмотрим простой пример, когда вы находитесь в оболочке bash и выполняете команду ls. В этом случае происходит следующее. Образ программы-оболочки bash выполняется в процессе #1. Затем вы вводите команду ls, и оболочка определяет, что нужно запустить внешнюю программу (/bin/ls). Тогда процесс #1 создает свою почти точную копию, процесс #2, который выполняет тот же самый программный код. После этого процесс #2 заменяет свой текущий образ (оболочку) другим образом (программой /bin/ls). В итоге получаем отдельный процесс, выполняющий отдельную программу.
"К чему такая путаница?" - спросите вы. Зачем сначала "клонировать" процесс, а затем заменять в нем образ? Не проще ли все делать одной-единственной операцией? Ответы на подобные вопросы дать тяжело, но, как правило, с опытом прихоидит понимание того, что подобная схема является одной из граней красоты Unix-систем.
Попробуем все-таки разобраться, почему в Unix-системах порождение процесса отделено от запуска программы. Для этого выясним, что же происходит с данными при "клонировании" процесса. Итак, каждый процесс хранит в своей памяти различные данные (переменные, файловые дескрипторы и проч.). При порождении нового процесса, потомок получает точную копию данных родителя. Но как только новый процесс создан, родитель и потомок уже распоряжаются своими копиями по своему усмотрению. Это позволяет распараллелить программу, заставив ее выполнять какой-нибудь трудоемкий алгоритм в отдельном процессе.
Может быть кто-то из вас слышал про то, что в Linux есть потоки, которые позволяют в одной программе реализовывать параллельное выполнение нескольких функций. Опять же возникает вопрос: "Если есть потоки, зачем вся эта головомойка с клонированиями и заменой образов?". А дело в том, что потоки работают с общими данными и выполняются в одной программе. Если в потоке произошло что-то страшное, то это, как правило, отражается на всей программе в целом. Хотя технически потоки реализованы в Linux на базе процессов, но процесс все же является более независимой единицей. Крах дочернего процесса никак не отражается на работе родителя, если сам родитель этого не пожелает.
По правде сказать, программисты редко прибегают к методике распараллеливания одной программы при помощи процессов. Но суть в том, что в Unix-системах программист обладает полной свободой выбора стратегии многозадачности. И это здорово!
Разберемся теперь с тем, как на практике происходит "клонирование" процессов. Для этого используется простой системный вызов fork(), прототип которого находится в файле unistd.h:
pid_t fork (void);
Если fork() завершается с ошибкой, то возвращается -1. Это редкий случай, связанный с нехваткой памяти или превышением лимита на количество процессов. Но если разделение произошло, то программе нужно позаботиться об идентификации своего "Я", то есть определении того, где родитель, а где потомок. Это делается очень просто: в родительский процесс fork() возвращает идентификатор потомка, а потомок получает 0. Следующий пример демонстрирует то, как это происходит.
/* fork01.c */ #include <stdio.h> #include <sys/types.h> #include <unistd.h> int main (void) { pid_t pid = fork (); if (pid == 0) { printf ("child (pid=%d)\n", getpid()); } else { printf ("parent (pid=%d, child's pid=%d)\n", getpid(), pid); } return 0; }
Проверяем, что получилось:
$ gcc -o fork01 fork01.c $ ./fork01 child (pid=21026) parent (pid=21025, child's pid=21026)
Обратите внимание, что поскольку после вызова fork() программу выполняли уже два независимых процесса, то сообщение родителя вполне могло бы появиться первым.
Итак, теперь мы умеем порождать процессы. Научимся теперь заменять образ текущего процесса другой программой. Для этих целей используется системный вызов execve(), который объявлен в заголовочном файле unistd.h вот так:
int execve (const char * path, char const * argv[], char * const envp[]);
Все очень просто: системный вызов execve() заменяет текущий образ процесса программой из файла с именем path, набором аргументов argv и окружением envp. Здесь следует только учитывать, что path - это не просто имя программы, а путь к ней. Иными словами, чтобы запустить ls, нужно в первом аргументе указать "/bin/ls".
Массивы строк argv и envp обязательно должны заканчиваться элементом NULL. Кроме того, следует помнить, что первый элемент массива argv (argv[0]) - это имя программы или что-либо иное. Непосредственные аргументы программы отсчитываются от элемента с номером 1.
В случае успешного завершения execve() ничего не возвращает, поскольку новая программа получает полное и безвозвратное управление текущим процессом. Если произошла ошибка, то по традиции возвращается -1.
Рассмотрим теперь пример программы, которая заменяет свой образ другой программой.
/* execve01.c */ #include <unistd.h> #include <stdio.h> int main (void) { printf ("pid=%d\n", getpid ()); execve ("/bin/cat", NULL, NULL); return 0; }
Итак, данная программа выводит свой PID и передает безвозвратное управление программе cat без аргументов и без окружения. Проверяем:
$ gcc -o execve01 execve01.c $ ./execve01 pid=30150
Программа вывела идентификатор процесса и замерла в смиренном ожидании. Откроем теперь другое терминальное окно и проверим, что же творится с нашим процессом:
$ ps -e | grep 30150 30150 pts/3 00:00:00 cat
Итак, мы убедились, что теперь процесс 30150 выполняет программа cat. Теперь можно вернуться в исходное окно и нажатием Ctrl+D завершить работу cat.
И, наконец, следующий пример демонстрирует запуск программы в отдельном процессе.
/* forkexec01.c */ #include <unistd.h> #include <stdio.h> extern char ** environ; int main (void) { char * echo_args[] = { "echo", "child", NULL }; if (!fork ()) { execve ("/bin/echo", echo_args, environ); fprintf (stderr, "an error occured\n"); return 1; } printf ("parent"); return 0; }
Проверяем:
$ gcc -o forkexec01 forkexec01.c $ ./forkexec01 parent child
Обратите внимание, что поскольку execve() не может возвращать ничего кроме -1, то для обработки возможной ошибки вовсе не обязательно создавать ветвление. Иными словами, если вызов execve() возвратил что-то, то это однозначно ошибка.
GNU Free Documentation License Version 1.2, November 2002 Copyright (C) 2000,2001,2002 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 0. PREAMBLE The purpose of this License is to make a manual, textbook, or other functional and useful document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others. This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software. We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference. 1. APPLICABILITY AND DEFINITIONS This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you". You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law. A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language. A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them. The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none. The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words. A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not "Transparent" is called "Opaque". Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only. The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text. A section "Entitled XYZ" means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as "Acknowledgements", "Dedications", "Endorsements", or "History".) To "Preserve the Title" of such a section when you modify the Document means that it remains a section "Entitled XYZ" according to this definition. The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License. 2. VERBATIM COPYING You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3. You may also lend copies, under the same conditions stated above, and you may publicly display copies. 3. COPYING IN QUANTITY If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects. If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages. If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public. It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document. 4. MODIFICATIONS You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version: A. Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission. B. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has fewer than five), unless they release you from this requirement. C. State on the Title page the name of the publisher of the Modified Version, as the publisher. D. Preserve all the copyright notices of the Document. E. Add an appropriate copyright notice for your modifications adjacent to the other copyright notices. F. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below. G. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice. H. Include an unaltered copy of this License. I. Preserve the section Entitled "History", Preserve its Title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section Entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence. J. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission. K. For any section Entitled "Acknowledgements" or "Dedications", Preserve the Title of the section, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein. L. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles. M. Delete any section Entitled "Endorsements". Such a section may not be included in the Modified Version. N. Do not retitle any existing section to be Entitled "Endorsements" or to conflict in title with any Invariant Section. O. Preserve any Warranty Disclaimers. If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles. You may add a section Entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties--for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard. You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one. The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version. 5. COMBINING DOCUMENTS You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers. The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work. In the combination, you must combine any sections Entitled "History" in the various original documents, forming one section Entitled "History"; likewise combine any sections Entitled "Acknowledgements", and any sections Entitled "Dedications". You must delete all sections Entitled "Endorsements". 6. COLLECTIONS OF DOCUMENTS You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects. You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document. 7. AGGREGATION WITH INDEPENDENT WORKS A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an "aggregate" if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document. If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate. 8. TRANSLATION Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail. If a section in the Document is Entitled "Acknowledgements", "Dedications", or "History", the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title. 9. TERMINATION You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 10. FUTURE REVISIONS OF THIS LICENSE The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/. Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation. ADDENDUM: How to use this License for your documents To use this License in a document you have written, include a copy of the License in the document and put the following copyright and license notices just after the title page: Copyright (c) YEAR YOUR NAME. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License". If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, replace the "with...Texts." line with this: with the Invariant Sections being LIST THEIR TITLES, with the Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. If you have Invariant Sections without Cover Texts, or some other combination of the three, merge those two alternatives to suit the situation. If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software.
Флаг | Восьмиричное представление | RWX-представление |
S_IRWXU | 00700 | rwx --- --- |
S_IRUSR | 00400 | r-- --- --- |
S_IREAD | 00400 | r-- --- --- |
S_IWUSR | 00200 | -w- --- --- |
S_IWRITE | 00200 | -w- --- --- |
S_IXUSR | 00100 | --x --- --- |
S_IEXEC | 00100 | --x --- --- |
S_IRWXG | 00070 | --- rwx --- |
S_IRGRP | 00040 | --- r-- --- |
S_IWGRP | 00020 | --- -w- --- |
S_IXGRP | 00010 | --- --x --- |
S_IRWXO | 00007 | --- --- rwx |
S_IROTH | 00004 | --- --- r-- |
S_IWOTH | 00002 | --- --- -w- |
S_IXOTH | 00001 | --- --- --x |
Флаг | Восьмиричное представление | Описание |
S_IFMT | 0170000 | Двоичная маска определения типа файла (побитовое ИЛИ всех следующих ниже флагов) |
S_IFDIR | 0040000 | Каталог |
S_IFCHR | 0020000 | Символьное устройство |
S_IFBLK | 0060000 | Блочное устройство |
S_IFREG | 0100000 | Обычный файл |
S_IFIFO | 0010000 | Канал FIFO |
S_IFLNK | 0120000 | Символическая ссылка |
Флаг | Восьмиричное представление | Описание |
S_ISUID | 0004000 | Бит SETUID |
S_ISGID | 0002000 | Бит SETGID |
S_ISVTX | 0001000 | Липкий (sticky) бит |
Флаг | Описание |
O_RDONLY | Только чтение (0) |
O_WRONLY | Только запись (1) |
O_RDWR | Чтение и запись (2) |
O_CREAT | Создать файл, если не существует |
O_TRUNC | Стереть файл, если существует |
O_APPEND | Дописывать в конец |
O_EXCL | Выдать ошибку, если файл существует при использовании O_CREAT |
O_DSYNC | Принудительная синхронизация записи |
O_RSYNC | Принудительная синхронизация перед чтением |
O_SYNC | Принудительная полная синхронизация записи |
O_NONBLOCK | Открыть файл в неблокируемом режиме, если это возможно |
O_NDELAY | То же, что и O_NONBLOCK |
O_NOCTTY | Если открываемый файл - терминальное устройство, не делать его управляющим терминалом процесса |
O_NOFOLLOW | Выдать ошибку, если открываемый файл является символической ссылкой |
O_DIRECTORY | Выдать ошибку, если открываемый файл не является каталогом |
O_DIRECT | Попытаться минимизировать кэширование чтения/записи файла |
O_ASYNC | Генерировать сигнал, когда появляется возможность чтения или записи в файл |
O_LARGEFILE | Разрешить большие файлы (размер которых не может быть представлен в 31 бите (для систем с поддержкой LFS) |
Эта книга находится в стадии написания и постоянного обновления. Целью создания такой "дополняемой" книги стало осознание многогранности феномена программирования в Linux. Нельзя написать книгу по программированию в Linux и сказать: "Здесь я поместил все, что можно было поместить". Очень надеюсь, что вы присоединитесь к моему эксперименту и будете читать книгу и делиться своими впечатлениями.
Последняя версия книги "Программирование в Linux с нуля" всегда доступна на сайте Lindevel.Ru по адресу http://www.lindevel.ru или на FTP-архиве Lindevel.Ru по адресу ftp://ftp.lindevel.ru. Если есть какие-то замечания или дополнения, направляйте их по адресу zlp@lindevel.ru.
Книга распространяется на условиях лицензии GNU Free Documentation License. Текст лицензии находится в разделе Приложение1: GNU Free Documentation License этой книги. Дополнительную информацию можно получить на сайте проектаGNU по адресу htpp://www.gnu.org.
Nikolay N. Ivanov.