Версия для печати

Архив документации на OpenNet.ru / Раздел "Программирование, языки" (Многостраничная версия)

Программирование в Linux с нуля

Версия: 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;


Оглавление

Предисловие

Глава 1. ВВЕДЕНИЕ
    1.1. Что нужно знать
    1.2. Условные обозначения
    1.3. Что нужно иметь
    1.4. Обратная связь

Глава 2. ПЕРВЫЙ БЛИН
    2.1. Hello World
    2.2. Мультифайловое программирование
    2.3. Автоматическая сборка
    2.4. Модель КИС

Глава 3. БИБЛИОТЕКИ
    3.1. Введение в библиотеки
    3.2. Пример статической библиотеки
    3.3. Пример совместно используемой библиотеки

Глава 4. ОКРУЖЕНИЕ
    4.1. Введение в окружение
    4.2. Массив environ
    4.3. Чтение окружения: getenv()
    4.4. Запись окружения: setenv()
    4.5. Сырая модификация окружения: putenv()
    4.6. Удаление переменной окружения: unsetenv()
    4.7. Очистка окружения: clearenv()

Глава 5. НИЗКОУРОВНЕВЫЙ ВВОД-ВЫВОД
    5.1. Обзор механизмов ввода-вывода в Linux
    5.2. Файловые дескрипторы
    5.3. Открытие файла: системный вызов open()
    5.4. Закрытие файла: системный вызов close()
    5.5. Чтение файла: системный вызов read()
    5.6. Запись в файл: системный вызов write()
    5.7. Произвольный доступ: системный вызов lseek()

Глава 6. МНОГОЗАДАЧНОСТЬ
    6.1. Основы многозадачности в Linux
    6.2. Использование getpid() и getppid()
    6.3. Порождение процесса
    6.4. Замена образа процесса

Приложение 1: GNU Free Documentation License

Приложение 2: Флаги режима доступа к файлу
    Таблица 1. Флаги общего режима
    Таблица 2. Флаги расширенного режима
    Таблица 3. Дополнительные флаги
    Таблица 4. Флаги режима открытия файла


Copyright © 2003-2006, 2007 Nikolay N. Ivanov
Distributed under the GNU Free Documentaton License

Оглавление

Глава1. ВВЕДЕНИЕ

1.1. Что нужно знать
1.2. Условные обозначения
1.3. Что нужно иметь
1.4. Обратная связь

1.1. Что нужно знать

Предполагается, что читатель этой книги должен иметь хотя бы начальные пользовательские навыки при работе с GNU/Linux (умение вводить команды в оболочку и знать основы операционной системы), а также знать язык C хотя бы на начальном уровне. Я посчитал бессмысленным писать еще одну книгу по языку C. Если вы знаете язык C на уровне книги Кернигана и Ритчи "Язык программирования Си", то у вас не будет проблем с изучением материала этой книги.

Если вы знаете язык C++, но не знаете C, ничего страшного. Эти два языка хотя и разные по своей сути, но имеют одну и ту же синтаксическую основу. C++-программисту не надо обладать феноменальной сообразительностью, чтобы разобраться в программе, написанной на C.

Большая часть материала этой книги относится и к другим современным Unix-подобным операционным системам (FreeBSD, например). Если вы еще не знаете Linux, то больших проблем не будет.

1.2. Условные обозначения

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


#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.3. Что нужно иметь

То, что нам нужно иметь, называют инструментарием. Инструментарий программиста зависит от того, какие программы и на каких языках он будет писать. Наш инструментарий будет состоять из: 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.

1.4. Обратная связь

Если вы заметили ошибки/опечатки. Или если что-то в этой книге плохо описано, сообщайте об этом по адресу zlp@lindevel.ru или форуме на сайте Lindevel.Ru (http://www.lindevel.ru).

Книга распространяется на условиях GNU Free Documentation License. В соответствии с условиями этой лицензии вы можете свободно скачивать и распространять книгу без каких-либо разрешений, но не нарушая и не изменяя условий Лицензии. Если вы хотите поместить копию книги на своем Интернет-ресурсе или просто распечатать - делайте это свободно, как завещал великий Столлман!

Вы также можете вносить самостоятельные изменения в эту книгу. В этом случае я просто прошу сообщать мне об этом.


Copyright © 2003-2006, 2007 Nikolay N. Ivanov
Distributed under the GNU Free Documentaton License

Оглавление

Глава 2. ПЕРВЫЙ БЛИН

2.1. Hello World
2.2. Мультифайловое программирование
2.3. Автоматическая сборка
2.4. Модель КИС

2.1. Hello World

Чтобы сразу начать программировать, создадим еще один клон известной программы "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% знаете, что делаете.

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

2.2. Мультифайловое программирование

Как я уже говорил, если исходный код сколько-нибудь серьезной программы уместить в одном файле, то такой код станет просто нечитаемым. К тому же если программа компилируется достаточно долго (особенно это относится к языку 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. В процессе линковки высчитываются и подставляются в нужные места адреса, соответствующие именам из таблицы. Вот и весь секрет.

2.3. Автоматическая сборка

В предыдущем разделе для создания бинарника из двух исходных файлов нам пришлось набрать три команды. Если бы программу пришлось отлаживать, то каждый раз надо было бы вводить одни и те же три команды. Казалось бы выход есть: написать сценарий оболочки. Но давайте подумаем, какие в этом случае могут быть недостатки. Во-первых, каждый раз сценарий будет компилировать все файлы проекта, даже если мы исправили только один из них. В нашем случае это не страшно. Но если речь идет о десятках файлов! Во-вторых, сценарий "намертво" привязан к конкретной оболочке. Программа тут же становится менее переносимой. И, наконец, простому скрипту не хватает функциональности (задание аргументов сборки и т. п.), а хороший скрипт (с многофункциональными прибамбасами) плохо модернизируется.

Выход из сложившейся ситуации есть. Это утилита 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.

2.4. Модель КИС

Любая программа имеет свой репозиторий - рабочий каталог, в котором находятся исходники, сценарии сборки (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
$

С виду ничего не изменилось. Но на самом деле программа стала более правильной, более изящной и, самое главное, более безопасной.


Copyright © 2003-2006, 2007 Nikolay N. Ivanov
Distributed under the GNU Free Documentaton License

Оглавление

Глава 3. БИБЛИОТЕКИ

3.1. Введение в библиотеки
3.2. Пример статической библиотеки
3.3. Пример совместно используемой библиотеки

3.1. Введение в библиотеки

Как уже неоднократно упоминалось в предыдущей главе, библиотека - это набор скомпонованных особым образом объектных файлов. Библиотеки подключаются к основной программе во время линковки. По способу компоновки библиотеки подразделяют на архивы (статические библиотеки, static libraries) и совместно используемые (динамические библиотеки, shared libraries). В Linux, кроме того, есть механизмы динамической подгрузки библиотек. Суть динамической подгрузки состоит в том, что запущенная программа может по собственному усмотрению подключить к себе какую-либо библиотеку. Благодаря этой возможности создаются программы с подключаемыми плагинами, такие как XMMS. В этой главе мы не будем рассматривать динамическую подгрузку, а остановимся на классическом использовании статических и динамических библиотек.

С точки зрения модели КИС, библиотека - это сервер. Библиотеки несут в себе одну важную мысль: возможность использовать одни и те же механизмы в разных программах. В Linux библиотеки используются повсеместно, поскольку это очень удобный способ "не изобретать велосипеды". Даже ядро Linux в каком-то смысле представляет собой библиотеку механизмов, называемых системными вызовами.

Статическая библиотека - это просто архив объектных файлов, который подключается к программе во время линковки. Эффект такой же, как если бы вы подключали каждый из файлов отдельно.

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

Рассмотрим преимущества и недостатки статических и совместно используемых библиотек. Статические библиотеки делают программу более автономной: программа, скомпонованная со статической библиотекой может запускаться на любом компьютере, не требуя наличия этой библиотеки (она уже "внутри" бинарника). Программа, скомпонованная с динамической библиотекой, требует наличия этой библиотеки на том компьютере, где она запускается, поскольку в бинарнике не код, а ссылка на код библиотеки. Не смотря на такую зависимость, динамические библиотеки обладают двумя существенными преимуществами. Во-первых, бинарник, скомпонованный с совместно используемой библиотекой меньше размером, чем такой же бинарник, с подключенной к нему статической библиотекой (статически скомпонованный бинарник). Во-вторых, любая модернизация динамической библиотеки, отражается на всех программах, использующих ее. Таким образом, если некоторую библиотеку foo используют 10 программ, то исправление какой-нибудь ошибки в foo или любое другое улучшение библиотеки автоматически улучшает все программы, которые используют эту библиотеку. Именно поэтому динамические библиотеки называют совместно используемыми. Чтобы применить изменения, внесенные в статическую библиотеку, нужно пересобрать все 10 программ.

В Linux статические библиотеки обычно имеют расширение .a (Archive), а совместно используемые библиотеки имеют расширение .so (Shared Object). Хранятся библиотеки, как правило, в каталогах /lib и /usr/lib. В случае иного расположения (относится только к совместно используемым библиотекам), приходится немного "подшаманить", чтобы программа запустилась.

3.2. Пример статической библиотеки

Теперь давайте создадим свою собственную библиотеку, располагающую двумя функциями: 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. (точка означает текущий каталог) необходима.

3.3. Пример совместно используемой библиотеки

Для того, чтобы создать и использовать динамическую (совместно используемую) библиотеку, достаточно переделать в нашем проекте 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), который используется в динамических библиотеках. В таком коде используются не фиксированные позиции (адреса), а плавающие, благодаря чему код из библиотеки имеет возможность подключаться к программе в момент запуска.


Copyright © 2003-2006, 2007 Nikolay N. Ivanov
Distributed under the GNU Free Documentaton License

Оглавление

Глава 4. ОКРУЖЕНИЕ

4.1. Введение в окружение
4.2. Массив environ
4.3. Чтение окружения: getenv()
4.4. Запись окружения: setenv()
4.5. Сырая модификация окружения: putenv()
4.6. Удаление переменной окружения: unsetenv()
4.7. Очистка окружения: clearenv()

4.1. Введение в окружение

Окружение (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, например). Для самой оболочки новый процесс будет дочерним. Мы будем подробно изучать процессы в последующих главах книги. Сейчас же важно одно: новый процесс получает копию родительского окружения. Из этого правила существует несколько исключений, но мы пока об этом говорить не будем. Важно то, что у кажного процесса своя независимая копия окружения, с которой процесс может делать все что угодно. Если процесс завершается, то копия теряется; если процесс породил другой, дочерний процесс, то этот новый процесс получает копию окружения своего родителя. Мы еще неоднократно столкнемся с окружением при изучении многозадачности.

4.2. Массив environ

Теперь, когда мы разобрались, что такое окружение, самое время написать программу для взаимодействия с окружением. Чтобы показать, как это все работает, сначала изобретем велосипед.

В заголовочном файле 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. О настоящем предназначении этого массива будет рассказано в главе, посвященной многозадачности.

4.3. Чтение окружения: getenv()

В заголовочном файле 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);
}

4.4. Запись окружения: setenv()

Пришла пора модифицировать окружение! Еще раз напоминаю: каждый процесс получает не доступ к окружению, а копию окружения родительского процесса (в нашем случае это командная оболочка). Чтобы добавить в окружение новую переменную или изменить существующую, используется функция 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 можно несколько изменить поведение программы по отношению к существующим переменным окружения. Еще раз напоминаю: у каждого процесса своя копия окружения, которая уничтожается при завершении процесса. Экспериментируйте!

4.5. Сырая модификация окружения: putenv()

Функция 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() для пополнения/модификации окружения.

4.6. Удаление переменной окружения: unsetenv()

Функция 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().

4.7. Очистка окружения: clearenv()

Функция clearenv(), объявленная в заголовочном файле stdlib.h, используется крайне редко для полной очистки окружения. clearenv() поддерживается не всеми версиями Unix. Ниже приведен ее прототип.


int clearenv (void);

При успешном завершении clearenv() возвращает нуль. В случае ошибки возвращается ненулевое значение.

В большинстве случаев вместо clearenv() можно использовать следующую инструкцию:


environ = NULL;


Copyright © 2003-2006, 2007 Nikolay N. Ivanov
Distributed under the GNU Free Documentaton License

Оглавление

Глава 5. НИЗКОУРОВНЕВЫЙ ВВОД-ВЫВОД

5.1. Обзор механизмов ввода-вывода в Linux
5.2. Файловые дескрипторы
5.3. Открытие файла: системный вызов open()
5.4. Закрытие файла: системный вызов close()
5.5. Чтение файла: системный вызов read()
5.6. Запись в файл: системный вызов write()
5.7. Произвольный доступ: системный вызов lseek()

5.1. Обзор механизмов ввода-вывода в Linux

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

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

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

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

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

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

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

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


$ ulimit -n
1024
$

Если вы работаете с оболочкой C-shell (csh, tcsh), то в вашем распоряжении команда limit:

$ limit descriptors
descriptors 1024
$

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

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

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

Чтобы получить возможность прочитать что-то из файла или записать что-то в файл, его нужно открыть. Это делает системный вызов 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. Позже, в последующих главах книги мы научимся распознавать ошибки системных вызовов.

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

Системный вызов 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);

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

Системный вызов 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
$              

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

Для записи данных в файл используется системный вызов 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. Подумайте, все ли флаги сочетаются между собой по смыслу.

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

Как уже говорилось, с каждым открытым файлом связано число, указывающее на текущую позицию чтения-записи. При открытии файла позиция равна нулю. Каждый вызов 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(), диапазон координат и проч.). Попробуйте включить эти проверки самостоятельно.


Copyright © 2003-2006, 2007 Nikolay N. Ivanov
Distributed under the GNU Free Documentaton License

Оглавление

Глава 6. МНОГОЗАДАЧНОСТЬ

6.1. Основы многозадачности в Linux
6.2. Использование getpid() и getppid()
6.3. Порождение процесса
6.4. Замена образа процесса

6.1. Основы многозадачности в Linux

Эта глава открывает большую и очень важную для 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 без аргументов, то будет выведен список процессов, принадлежащих текущей группе, то есть работающих под текущим терминалом. О том, что такое терминалы и группы процессов, будет рассказано в последующих главах.

6.2. Использование getpid() и getppid()

Процесс может узнать свой идентификатор (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

6.3. Порождение процесса

Как уже говорилось ранее, процесс в 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() программу выполняли уже два независимых процесса, то сообщение родителя вполне могло бы появиться первым.

6.4. Замена образа процесса

Итак, теперь мы умеем порождать процессы. Научимся теперь заменять образ текущего процесса другой программой. Для этих целей используется системный вызов 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() возвратил что-то, то это однозначно ошибка.


Copyright © 2003-2006, 2007 Nikolay N. Ivanov
Distributed under the GNU Free Documentaton License

Оглавление

Приложение 1: GNU Free Documentation License

		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.

Copyright © 2003-2006, 2007 Nikolay N. Ivanov
Distributed under the GNU Free Documentaton License

Оглавление

Приложение2. Флаги режима доступа к файлу

Таблица 1. Флаги общего режима
Таблица 2. Флаги расширенного режима
Таблица 3. Дополнительные флаги
Таблица 4. Флаги режима открытия файла

Таблица 1. Флаги общего режима

Флаг Восьмиричное представление 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

Таблица 2. Флаги расширенного режима

Флаг Восьмиричное представление Описание
S_IFMT 0170000 Двоичная маска определения типа файла (побитовое ИЛИ всех следующих ниже флагов)
S_IFDIR 0040000 Каталог
S_IFCHR 0020000 Символьное устройство
S_IFBLK 0060000 Блочное устройство
S_IFREG 0100000 Обычный файл
S_IFIFO 0010000 Канал FIFO
S_IFLNK 0120000 Символическая ссылка

Таблица 3. Дополнительные флаги

Флаг Восьмиричное представление Описание
S_ISUID 0004000 Бит SETUID
S_ISGID 0002000 Бит SETGID
S_ISVTX 0001000 Липкий (sticky) бит

Таблица 4. Флаги режима открытия файла

Флаг Описание
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)


Copyright © 2003-2006, 2007 Nikolay N. Ivanov
Distributed under the GNU Free Documentaton License

Оглавление

Предисловие

Эта книга находится в стадии написания и постоянного обновления. Целью создания такой "дополняемой" книги стало осознание многогранности феномена программирования в 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.


Copyright © 2003-2006, 2007 Nikolay N. Ivanov
Distributed under the GNU Free Documentaton License