Ссылка:
1 2 3 |
https://www.opennet.ru/docs/RUS/zlp/ https://www.youtube.com/watch?v=OTXHSdYNLcA&list=PL7KBbsb4oaOn6ekuNGqZxl4-U_Ox81qTx&index=2&ab_channel=FromLAMERtoProgrammer https://www.youtube.com/watch?v=qZsRdlkItWM&list=PL7KBbsb4oaOmyeV840MF_yWaiLkVvAaWC&ab_channel=FromLAMERtoProgrammer |
0. hello world
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
0. Создаем файл vim hello.c ----------- /* hello */ #include <stdio.h> int main (void) { printf ("hello world\n"); } ----------- 1. Компилируем файл !!! Если исходный код написан без синтаксических ошибок, то компилятор завершит свою работу без каких-либо сообщений. Молчание - знак повиновения и согласия. gcc -o hello hello.c 2. Проверяем ./hello |
1. Мульти файловое программирование
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
0. Создадим первый файл с именем main.c: vim main.c ----------- /* main.c */ int main (void) { print_hello (); } ----------- 1. Теперь создадим еще один файл hello.c со следующим содержимым: vim hello.c ----------- /* hello.c */ #include <stdio.h> void print_hello (void) { printf ("Hello World\n"); } ----------- 2. Теперь нужно получить два объектных файла. Компилируем. gcc -c main.c gcc -c hello.c ls 3. Итак, мы получили два объектных файла. Теперь их надо объединить в один бинарник: gcc -o hello main.o hello.o 4.Компилятор "увидел", что вместо исходных файлов (с расширением .c) ему подбросили объектные файлы (с расширением .o) и отреагировал согласно ситуации: вызвал линковщик с нужными опциями. Давайте разберемся, что же все-таки произошло. В этом нам поможет утилита nm. nm hello.o ---------- U printf 00000000 T print_hello ---------- nm main.o --------- 00000000 T main U print_hello --------- Таблицы символов объектных файлов содержат общее имя print_hello. В процессе линковки высчитываются и подставляются в нужные места адреса, соответствующие именам из таблицы. Вот и весь секрет. Объектные файлы содержат таблицу символов. Утилита nm как раз позволяет посмотреть эту таблицу в читаемом виде. Те, кто пробовал программировать на ассемблере знают, что в исполняемом файле буквально все (функции, переменные) стоит на своей позиции: стоит только вставить или убрать из программы один байт, как программа тут же превратиться в груду мусора из-за смещенных позиций (адресов). У объектных файлов особая роль: они хранят в таблице символов имена некоторых позиций (глобально объявленных функций, например). В процессе линковки происходит стыковка имен и пересчет позиций, что позволяет нескольким объектным файлам объединиться в один бинарник. |
1. automake make
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
0. Утилита make, которая работает со своими собственными сценариями. Сценарий записывается в файле с именем Makefile и помещается в репозиторий (рабочий каталог) проекта. Сценарии утилиты make просты и многофункциональны, а формат Makefile используется повсеместно (и не только на Unix-системах). Дошло до того, что стали создавать программы, генерирующие Makefile'ы. Самый яркий пример - набор утилит GNU Autotools. Самое главное преимущество make - это "интеллектуальный" способ рекомпиляции: в процессе отладки make компилирует только измененные файлы. То, что выполняет утилита make, называется сборкой проекта, а сама утилита make относится к разряду сборщиков. Любой Makefile состоит из трех элементов: комментарии, макроопределения и целевые связки (или просто связки). В свою очередь связки состоят тоже из трех элементов: цель, зависимости и правила. Сценарии make используют однострочные комментарии, начинающиеся с литеры # (решетка). О том, что такое комментарии и зачем они нужны, объяснять не буду. Макроопределения позволяют назначить имя практически любой строке, а затем подставлять это имя в любое место сценария, где должна использоваться данная строка. Макросы Makefile схожи с макроконстантами языка C. Связки определяют: 1) что нужно сделать (цель); 2) что для этого нужно (зависимости); 3) как это сделать (правила). В качестве цели выступает имя или макроконстанта. Зависимости - это список файлов и целей, разделенных пробелом. Правила - это команды передаваемые оболочке. 1. Создаем файл с именем Makefile: vim 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) в любых других случаях, когда требуется полная пересборка (напрмиер, для измерения времени полной сборки). 2. Формат запуска утилиты make следующий: make [опции] [цели...] Опции make нам пока не нужны. Если вызвать make без указания целей, то будет выполнена первая попавшаяся связка (со всеми зависимостями) и сборка завершится. Нам это и требуется: $ make В процессе сборки утилита make пишет все выполняемые правила. Проект собран, все работает. Теперь давайте немного модернизируем наш проект. 3. Добавим одну строку в файл hello.c: vim hello.c ----------- /* hello.c */ #include <stdio.h> void print_hello (void) { printf ("Hello World\n"); printf ("Goodbye World\n"); } ----------- 4. Теперь повторим сборку и проверим: $ make $ ./hello Утилита make "пронюхала", что был изменен только hello.c, то есть компилировать нужно только его. Файл main.o остался без изменений. 5. Теперь давайте очистим проект, оставив одни исходники: $ make clean В данном случае мы указали цель непосредственно в командной строке. Так как целевая связка clean содержит пустой список зависимостей, то выполняется только одно правило. Не забывайте "чистить" проект каждый раз, когда изменяется список исходных файлов или когда изменяется сам Makefile. |
3. Модель КИС:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
Любая программа имеет свой репозиторий - рабочий каталог, в котором находятся исходники, сценарии сборки (Makefile) и прочие файлы, относящиеся к проекту. Репозиторий рассмотренного нами проекта мультифайлового Hello World изначально состоит из файлов main.c, hello.c и, собственно, Makefile. После сборки репозиторий дополняется файлами main.o, hello.o и hello. Практика показывает, что правильная организация исходного кода в репозитории не только упрощает модернизацию и отладку, но и предотвращает возможность появления многих ошибок. Модель КИС (Клиент-Интерфейс-Сервер) - это элегантная концепция распределения исходного кода в репозитории, в рамках которой все исходники можно поделить на клиенты, интерфейсы и серверы. Библиотека - это просто коллекция скомпонованных особым образом объектных файлов. Заголовочный файл - это интерфейс. Основная разница между библиотеками и заголовочными файлами в том, что библиотека - это объектный (почти исполняемый) код, а заголовочный файл - это исходный код. Библиотека - это набор объектных файлов, которые подсоединяются к программе на стадии линковки. Так как стандартная библиотека языка C - это часть стандарта языка C, то она подключается автоматически во время линковки программы, но так как компилятор gcc сам вызывает линковщик с нужными параметрами, то мы этого просто не замечаем. С точки зрения модели КИС, библиотека - это сервер. Библиотеки несут в себе одну важную мысль: возможность использовать одни и те же механизмы в разных программах. В Linux библиотеки используются повсеместно, поскольку это очень удобный способ "не изобретать велосипеды". Даже ядро Linux в каком-то смысле представляет собой библиотеку механизмов, называемых системными вызовами. Статическая библиотека - это просто архив объектных файлов, который подключается к программе во время линковки. Эффект такой же, как если бы вы подключали каждый из файлов отдельно. В отличие от статических библиотек, код совместно используемых (динамических) библиотек не включается в бинарник. Вместо этого в бинарник включается только ссылка на библиотеку. В Linux статические библиотеки обычно имеют расширение .a (Archive), а совместно используемые библиотеки имеют расширение .so (Shared Object). Хранятся библиотеки, как правило, в каталогах /lib и /usr/lib. В случае иного расположения (относится только к совместно используемым библиотекам), приходится немного "подшаманить", чтобы программа запустилась. |
4. Пример статической библиотеки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
0. Начнем с интерфейса. Создадим файл world.h: !!! Здесь просто объявлены функции, которые будут использоваться vim world.h ----------- /* world.h */ void h_world (void); void g_world (void); ----------- 1. Теперь надо реализовать серверы. Создадим файл h_world.c: vim h_world.c ------------- /* h_world.c */ #include <stdio.h> #include "world.h" void h_world (void) { printf ("Hello World\n"); } ------------- 2. Теперь создадим файл g_world.c, содержащий реализацию функции g_world(): !!! Можно было бы с таким же успехом уместить обе функции в одном файле (hello.c, например) vim g_world.c ------------- /* g_world.c */ #include <stdio.h> #include "world.h" void g_world (void) { printf ("Goodbye World\n"); } ------------- 3. Теперь создадим файл main.c. Это клиент, который будет пользоваться услугами сервера: vim main.c ---------- /* main.c */ #include "world.h" int main (void) { h_world (); g_world (); } ---------- 4. Теперь напишем сценарий для make. Для этого создаем Makefile: !!! Не забывайте ставить табуляции перед каждым правилом в целевых связках. vim 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 ------------ 5. Итого Итак, в приведенном примере появились три новые вещи: опции -l и -L компилятора, а также команда ar. Начнем с последней ar. Как вы уже догадались, команда ar создает статическую библиотеку (архив). В нашем случае два объектных файла объединяются в один файл libworld.a. В Linux практически все библиотеки имеют префикс lib. Как уже говорилось, компилятор gcc сам вызывает линковщик, когда это нужно. Опция -l, переданная компилятору, обрабатывается и посылается линковщику для того, чтобы тот подключил к бинарнику библиотеку. Как вы уже заметили, у имени библиотеки "обрублены" префикс и суффикс. Это делается для того, чтобы создать "видимое безразличие" между статическими и динамическими библиотеками. Сейчас важно знать лишь то, что и библиотека libfoo.so и библиотека libfoo.a подключаются к проекту опцией -lfoo. В нашем случае libworld.a "урезалось" до -lworld. Опция -L указывает линковщику, где ему искать библиотеку. В случае, если библиотека располагается в каталоге /lib или /usr/lib, то вопрос отпадает сам собой и опция -L не требуется. В нашем случае библиотека находится в репозитории (в текущем каталоге). По умолчанию линковщик не просматривает текущий каталог в поиске библиотеки, поэтому опция -L. (точка означает текущий каталог) необходима. |
Пример совместно используемой библиотеки
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
!!! Для того, чтобы создать и использовать динамическую (совместно используемую) библиотеку, достаточно переделать в нашем проекте Makefile. vim 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 - это использование переменной окружения LD_LIBRARY_PATH. У каждого пользователя есть так называемое окружение (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), который используется в динамических библиотеках. В таком коде используются не фиксированные позиции (адреса), а плавающие, благодаря чему код из библиотеки имеет возможность подключаться к программе в момент запуска. |
Окружение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
Окружение (environment) или среда - это набор пар ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ, доступный каждому пользовательскому процессу. Иными словами, окружение - это набор переменных окружения. Если вы используете оболочку, отличную от bash, то не все примеры этой главы могут быть воспроизведены. Для того, чтобы посмотреть окружение, просто введите команду env без аргументов. В зависимости от конфигурации системы, вывод env может занять несколько экранов, поэтому лучше сделать так: env > myenv.txt env | more env | less Переменные окружения могут формироваться как из заглавных, так и из строчных символов, однако исторически сложилось именовать их в верхнем регистре. Приятно, например, когда программа "угадывает" имя пользователя или домашний каталог пользователя. Чаще всего такая информация "добывается" из переменных окружения USER и HOME соответственно. Значение каждой переменной окружения изначально представляет собой строковую константу (строку). Интерпретация значений переменных полностью возлагается на программу. Иными словами, все переменные окружения имеют тип char*, а само окружение имеет тип char**. Чтобы вывести на экран значение какой-нибудь переменной окружения, достаточно набрать echo $ИМЯ_ПЕРЕМЕННОЙ echo $USER echo $HOME Ниже приведены те переменные окружения, которые есть почти у всех пользователей Linux: USER - имя текущего пользователя HOME - путь к домашнему каталогу текущего пользователя PATH - список каталогов, разделенных двоеточиями, в которых производится "поиск" программ PWD - текущий каталог OLDPWD - предыдущий текущий каталог TERM - тип терминала SHELL - текущая командная оболочка Некоторые переменные окружения имеются не во всех системах, но все-таки требуют упоминания: HOSTNAME - имя машины QTDIR - расположение библиотеки QT MAIL - почтовый ящик LD_LIBRARY_PATH - место "поиска" дополнительных библиотек (см. предыдущую главу) MANPATH - место поиска файлов man-страниц (каталоги, разделенные двоеточием) LANG - язык и кодировка пользователя (иногда LANGUAGE) DISPLAY - текущий дисплей в X11 Помимо переменных окружения, командные оболочки, такие как 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, например). Для самой оболочки новый процесс будет дочерним. Мы будем подробно изучать процессы в последующих главах книги. Сейчас же важно одно: новый процесс получает копию родительского окружения. Из этого правила существует несколько исключений, но мы пока об этом говорить не будем. Важно то, что у каждого процесса своя независимая копия окружения, с которой процесс может делать все что угодно. Если процесс завершается, то копия теряется; если процесс породил другой, дочерний процесс, то этот новый процесс получает копию окружения своего родителя. Мы еще неоднократно столкнемся с окружением при изучении многозадачности. |
Массив environ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
0. vim unistd.h ------------ extern char ** environ; ------------ В этом массиве хранится копия окружения процесса. 1. Велосипед (код программы) vim environ.c ------------- /* 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); } ------------- 2. Makefile для этой программы vim Makefile ------------ # Makefile for environ environ: environ.c gcc -o environ environ.c clean: rm -f environ ------------ 3. Проверяем Проверяем: make ./environ ./environ USER ./environ ABRAKADABRA |
Чтение окружения: getenv()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
0. Функция vim stdlib.h ------------- char * getenv (const char * name); ------------- Функция эта работает очень просто: если в качестве аргумента указано имя существующей переменной окружения, то функция возвращает указатель на строку, содержащую значение этой переменной; если переменная отсутствует, возвращается NULL. 1. Код программы vim getenv.c ------------ /* 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); } ------------ 2. Создадим файл для сборки vim Makefile ------------ # Makefile for getenv environ: getenv.c gcc -o getenv getenv.c clean: rm -f getenv ------------ |
Запись окружения: setenv()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
каждый процесс получает не доступ к окружению, а копию окружения родительского процесса (в нашем случае это командная оболочка). Чтобы добавить в окружение новую переменную или изменить существующую, используется функция setenv, объявленная в файле stdlib.h. Ниже приведен адаптированный прототип этой функции. 0. Функция vim stdlib.h ------------ int setenv (const char * name, const char * value, int overwrite); ------------ Если хотите узнать, что значит "адаптированный прототип", загляните в /usr/include/stdlib.h на объявления функций getenv() и setenv() и больше не спрашивайте ;-) 1. Код программы vim setenv.c ------------ /* 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 можно несколько изменить поведение программы по отношению к существующим переменным окружения. Еще раз напоминаю: у каждого процесса своя копия окружения, которая уничтожается при завершении процесса. Экспериментируйте! 2. Создадим файл для сборки vim Makefile ------------ # Makefile for setenv environ: setenv.c gcc -o setenv setenv.c clean: rm -f setenv ------------ |
Сырая модификация окружения: putenv()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
0. Функция vim putenv.h ------------ int putenv (char * str); ------------ Функция putenv(), объявленная в заголовочном файле stdlib.h вызывается с единственным аргументом - строкой формата ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ или просто ПЕРЕМЕННАЯ. Обычно такие преформатированные строки называют запросами. Если переменная отсутствует, то в окружение добавляется новая запись. Если переменная уже существует, то текущее значение перезаписывается. Если в качестве аргумента фигурирует просто имя переменной, то переменная удаляется из окружения. В случае удачного завершения, putenv() возвращает нуль и -1 - в случае ошибки. У функции putenv() есть одна особенность: указатель на строку, переданный в качестве аргумента, становится частью окружения. Если в дальнейшем строка будет изменена, будет изменено и окружение. Это очень важный момент, о котором не следует забывать. 1. Программа vim putenv.c ------------ /* 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() для пополнения/модификации окружения. 2. Файл сборки vim Makefile ------------ # Makefile for putenv environ: putenv.c gcc -o putenv putenv.c clean: rm -f putenv ------------ |
Удаление переменной окружения: unsetenv()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
0. Функция vim stdlib.h ------------- int unsetenv (const char * name); ------------- Функция unsetenv() использует в качестве аргумента имя переменной окружения. Возвращаемое значение - нуль при удачном завершении и -1 в случае ошибки. Рассмотрим простую программу, которая удаляет переменную окружения USER (!!!). Каждый процесс работает с собственной копией окружения, никак не связанной с копиями окружения других процессов, за исключением дочерних процессов, которых у нас нет. Ниже приведен исходный код программы, учитывающий исторические изменения прототипа функции unsetenv() 1. Программа vim unsetenv.c -------------- /* 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(). 2. Файл сборки vim Makefile ------------ # Makefile for putenv environ: unsetenv.c gcc -o unsetenv unsetenv.c clean: rm -f unsetenv ------------ |
Очистка окружения: clearenv()
1 2 3 4 5 6 7 8 9 10 |
Функция clearenv(), объявленная в заголовочном файле stdlib.h, используется крайне редко для полной очистки окружения. clearenv() поддерживается не всеми версиями Unix. int clearenv (void); При успешном завершении clearenv() возвращает нуль. В случае ошибки возвращается ненулевое значение. В большинстве случаев вместо clearenv() можно использовать следующую инструкцию: environ = NULL; |
Механизмов ввода-вывода в Linux:
1 2 3 4 5 6 7 8 9 10 11 |
В языке C для осуществления файлового ввода-вывода используются механизмы стандартной библиотеки языка, объявленные в заголовочном файле stdio.h. Это не более чем частный случай файлового ввода-вывода. В C++ для ввода-вывода чаще всего используются потоковые типы данных. Однако все эти механизмы являются всего лишь надстройками над низкоуровневыми механизмами ввода-вывода ядра операционной системы. С точки зрения модели КИС (Клиент-Интерфейс-Сервер), сервером стандартных механизмов ввода вывода языка C (printf, scanf, FILE*, fprintf, fputc и т. д.) является библиотека языка. А сервером низкоуровневого ввода-вывода в Linux, которому посвящена эта глава книги, является само ядро операционной системы. Пользовательские программы взаимодействуют с ядром операционной системы посредством специальных механизмов, называемых системными вызовами (system calls, syscalls). Внешне системные вызовы реализованы в виде обычных функций языка C, однако каждый раз вызывая такую функцию, мы обращаемся непосредственно к ядру операционной системы. Рассмотрим основные системные вызовы, осуществляющие ввод-вывод: open(), close(), read(), write(), lseek() и некоторые другие. Список всех системных вызовов Linux можно найти в файле /usr/include/asm/unistd.h. |
Файловые дескрипторы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
В языке 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 Если вы работаете с оболочкой C-shell (csh, tcsh), то в вашем распоряжении команда limit: $ limit descriptors В командной оболочке, в которой вы работаете (bash, например), открыты три файла: стандартный ввод (дескриптор 0), стандартный вывод (дескриптор 1) и стандартный поток ошибок (дескриптор 2). Когда под оболочкой запускается программа, в системе создается новый процесс, который является для этой оболочки дочерним процессом, следовательно, получает копию таблицы дескрипторов своего родителя (то есть все открытые файлы родительского процесса). Таким образом программа может осуществлять консольный ввод-вывод через эти дескрипторы. На протяжении всей книги мы будем часто играть с этими дескрипторами. Таблица дескрипторов, помимо всего прочего, содержит информацию о текущей позиции чтения-записи для каждого дескриптора. При открытии файла, позиция чтения-записи устанавливается в ноль. Каждый прочитанный или записанный байт увеличивает на единицу указатель текущей позиции. |
Открытие файла: системный вызов open()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
Чтобы получить возможность прочитать что-то из файла или записать что-то в файл, его нужно открыть. Это делает системный вызов 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, ...); Первый аргумент - имя файла в файловой системе в обычной форме: полный путь к файлу (если файл не находится в текущем каталоге) или сокращенное имя (если файл в текущем каталоге). Второй аргумент - это режим открытия файла, представляющий собой один или несколько флагов открытия, объединенных оператором побитового ИЛИ. Наиболее часто используют только первые семь флагов. Если вы хотите, например, открыть файл в режиме чтения и записи, и при этом автоматически создать файл, если такового не существует, то второй аргумент open() будет выглядеть примерно так: O_RDWR|O_CREAT. Константы-флаги открытия объявлены в заголовочном файле bits/fcntl.h, однако не стоит включать этот файл в свои программы, поскольку он уже включен в файл fcntl.h. Третий аргумент используется в том случае, если open() создает новый файл. В этом случае файлу нужно задать права доступа (режим), с которыми он появится в файловой системе. Права доступа задаются перечислением флагов, объединенных побитовым ИЛИ. Вместо флагов можно использовать число (как правило восьмиричное), однако первый способ нагляднее и предпочтительнее. Список флагов приведен в Таблице 1 Приложения 2. Чтобы, например, созданный файл был доступен в режиме "чтение-запись" пользователем и группой и "только чтение" остальными пользователями, - в третьем аргументе open() надо указать примерно следующее: S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH или 0664. Флаги режима доступа реально объявлены в заголовочном файле bits/stat.h, но он не предназначен для включения в пользовательские программы, и вместо него мы должны включать файл sys/stat.h. Тип mode_t объявлен в заголовочном файле sys/types.h. Если файл был успешно открыт, open() возвращает файловый дескриптор, по которому мы будем обращаться к файлу. Если произошла ошибка, то open() возвращает -1. |
Закрытие файла: системный вызов close()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
Системный вызов close() закрывает файл. Вообще говоря, по завершении процесса все открытые файлы (кроме файлов с дескрипторами 0, 1 и 2) автоматически закрываются. Тем не менее, это не освобождает нас от самостоятельного вызова close(), когда файл нужно закрыть. К тому же, если файлы не закрывать самостоятельно, то соответствующие дескрипторы не освобождаются, что может привести к превышению лимита открытых файлов. Простой пример: приложение может быть настроено так, чтобы каждую минуту открывать и перечитывать свой файл конфигурации для проверки обновлений. Если каждый раз файл не будет закрываться, то в моей системе, например, приложение может "накрыться медным тазом" примерно через 17 часов. Автоматически! Кроме того, файловая система Linux поддерживает механизм буферизации. Это означает, что данные, которые якобы записываются, реально записываются на носитель (синхронизируются) только через какое-то время, когда система сочтет это правильным и оптимальным. Это повышает производительность системы и даже продлевает ресурс жестких дисков. Системный вызов close() не форсирует запись данных на диск, однако дает больше гарантий того, что данные останутся в целости и сохранности. Системный вызов close() объявлен в файле unistd.h. Ниже приведен его адаптированный прототип. vim unistd.h ------------- int close (int fd); ------------- Очевидно, что единственный аргумент - это файловый дескриптор. Возвращаемое значение - ноль в случае успеха, и -1 - в случае ошибки. Довольно часто close() вызывают без проверки возвращаемого значения. Это не очень грубая ошибка, но, тем не менее, иногда закрытие файла бывает неудачным (в случае неправильного дескриптора, в случае прерывания функции по сигналу или в случае ошибки ввода-вывода, например). В любом случае, если программа сообщит пользователю, что файл невозможно закрыть, это хорошо. Теперь можно написать простую программу, использующую системные вызовы open() и close(). Мы еще не умеем читать из файлов и писать в файлы, поэтому напишем программу, которая создает файл с именем, переданным в качестве аргумента (argv[1]) и с правами доступа 0600 (чтение и запись для пользователя). Ниже приведен исходный код программы. vim openclose.c --------------- /* 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, который "дает добро" только на создание еще не существующих файлов. Наглядности ради, флаги открытия и флаги режима мы занесли в отдельные переменные, однако можно было бы сделать так: 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); Создаем Makefile: vim openclose ------------- # Makefile for openclose environ: openclose.c gcc -o openclose openclose.c clean: rm -f openclose ------------- |
Чтение файла: системный вызов read()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
Системный вызов read(), объявленный в файле unistd.h, позволяет читать данные из файла. В отличие от библиотечных функций файлового ввода-вывода, которые предоставляют возможность интерпретации считываемых данных. Можно, например, записать в файл следующее содержимое: 2006 Теперь, используя библиотечные механизмы, можно читать файл по-разному: fscanf (filep, "%s", buffer); fscanf (filep, "%d", number); Системный вызов read() читает данные в "сыром" виде, то есть как последовательность байт, без какой-либо интерпретации. Ниже представлен адаптированный прототип read(). vim unistd.h ------------ 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]). Ниже приведен исходный код этой программы. vim myread.c ------------ /* 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 байта. vim myread1.c ------------- /* 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 $ time ./myread1 /boot/vmlinuz > /dev/null |
Запись в файл: системный вызов write()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
Для записи данных в файл используется системный вызов write(). Ниже представлен его прототип. vim unistd.h ------------ ssize_t write (int fd, const void * buffer, size_t count); ------------ Как видите, прототип write() отличается от read() только спецификатором const во втором аргументе. В принципе write() выполняет процедуру, обратную read(): записывает count байтов из буфера buffer в файл с дескриптором fd, возвращая количество записанных байтов или -1 в случае ошибки. Так просто, что можно сразу переходить к примеру. За основу возьмем программу myread1 из предыдущего раздела. vim rw.c -------- /* 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. Подумайте, все ли флаги сочетаются между собой по смыслу. vim Makefile ------------ # Makefile for rw.c environ: rw gcc -o rw rw.c clean: rm -f rw ----------- |
Произвольный доступ: системный вызов lseek()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
Как уже говорилось, с каждым открытым файлом связано число, указывающее на текущую позицию чтения-записи. При открытии файла позиция равна нулю. Каждый вызов read() или write() увеличивает текущую позицию на значение, равное числу прочитанных или записанных байт. Благодаря этому механизму, каждый повторный вызов read() читает следующие данные, и каждый повторный write() записывает данные в продолжение предыдущих, а не затирает старые. Такой механизм последовательного доступа очень удобен, однако иногда требуется получить произвольный доступ к содержимому файла, чтобы, например, прочитать или записать файл заново. Для изменения текущей позиции чтения-записи используется системный вызов lseek(). Ниже представлен его прототип. vim unistd.h ------------ 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. Ниже представлен исходный код этой программы. vim draw.c ---------- /* draw.c */ #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> /* memset() */ #define N_ROWS 15 /* Image height */ #define N_COLS 40 /* Image width */ #define FG_CHAR 'O' /* Foreground character */ #define IMG_FN "image" /* Image filename */ #define N_MIN(A,B) ((A)<(B)?(A):(B)) #define N_MAX(A,B) ((A)>(B)?(A):(B)) static char buffer[N_COLS]; void init_draw (int fd) { ssize_t bytes_written = 0; memset (buffer, ' ', N_COLS); buffer [N_COLS] = '\n'; while (bytes_written < (N_ROWS * (N_COLS+1))) bytes_written += write (fd, buffer, N_COLS+1); } void draw_point (int fd, int x, int y) { char ch = FG_CHAR; lseek (fd, y * (N_COLS+1) + x, SEEK_SET); write (fd, &ch, 1); } void draw_hline (int fd, int y, int x1, int x2) { size_t bytes_write = abs (x2-x1) + 1; memset (buffer, FG_CHAR, bytes_write); lseek (fd, y * (N_COLS+1) + N_MIN (x1, x2), SEEK_SET); write (fd, buffer, bytes_write); } void draw_vline (int fd, int x, int y1, int y2) { int i = N_MIN(y1, y2); while (i <= N_MAX(y2, y1)) draw_point (fd, x, i++); } int main (void) { int a, b, c, i = 0; char ch; int fd = open (IMG_FN, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { fprintf (stderr, "Cannot open file\n"); exit (1); } init_draw (fd); char * icode[] = { "v 1 1 11", "v 11 7 11", "v 14 5 11", "v 18 6 11", "v 21 5 10", "v 25 5 10", "v 29 5 6", "v 33 5 6", "v 29 10 11", "v 33 10 11", "h 11 1 8", "h 5 16 17", "h 11 22 24", "p 11 5 0", "p 15 6 0", "p 26 11 0", "p 30 7 0", "p 32 7 0", "p 31 8 0", "p 30 9 0", "p 32 9 0", NULL }; while (icode[i] != NULL) { sscanf (icode[i], "%c %d %d %d", &ch, &a, &b, &c); switch (ch) { case 'v': draw_vline (fd, a, b, c); break; case 'h': draw_hline (fd, a, b, c); break; case 'p': draw_point (fd, a, b); break; default: abort(); } i++; } close (fd); exit (0); } ---------- Теперь разберемся, как работает эта программа. Изначально "полотно" заполняется пробелами. Функция init_draw() построчно записывает в файл пробелы, чтобы получился "холст", размером N_ROWS на N_COLS. Массив строк icode в функции main() - это набор команд рисования. Команда начинается с одной из трех литер: 'v' - нарисовать вертикальную линию, 'h' - нарисовать горизонтальную линию, 'p' - нарисовать точку. После каждой такой литеры следуют три числа. В случае вертикальной линии первое число - фиксированная координата X, а два других числа - это начальная и конечная координаты Y. В случае горизонтальной линии фиксируется координата Y (первое число). Два остальных числа - начальная координата X и конечная координата X. При рисовании точки используются только два первых числа: координата X и координата Y. Итак, функция draw_vline() рисует вертикальную линию, функция draw_hline() рисует горизонтальную линию, а draw_point() рисует точку. Функция init_draw() пишет в файл N_ROWS строк, каждая из которых содержит N_COLS пробелов, заканчивающихся переводом строки. Это процедура подготовки "холста". Функция draw_point() вычисляет позицию (исходя из значений координат), перемещает туда текущую позицию ввода-вывода файла, и записывает в эту позицию символ (FG_CHAR), которым мы рисуем "картину". Функция draw_hline() заполняет часть строки символами FG_CHAR. Так получается горизонтальная линия. Функция draw_vline() работает иначе. Чтобы записать вертикальную линию, нужно записывать по одному символу и каждый раз "перескакивать" на следующую строку. Эта функция работает медленнее, чем draw_hline(), но иначе мы не можем. Полученное изображение записывается в файл image. Будьте внимательны: чтобы разгрузить исходный код, из программы исключены многие проверки (read(), write(), close(), диапазон координат и проч.). Попробуйте включить эти проверки самостоятельно. |
Основы многозадачности в Linux
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
На экран будут выведен список всех работающих в системе процессов: ps -e ps -e --no-headers | nl | tail -n 1 ps -e --forest Первое число - это количество работающих в системе процессов. Пользователи KDE могут воспользоваться программой kpm(на самом деле есть еще "System Monitor/plusma-system-monitor"), а пользователи Gnome - программой gnome-system-monitor для получения информации о процессах. На то он и Linux, чтобы позволять пользователю делать одно и то же разными способами. Возникает вопрос: "Что такое процесс?". Процессы в Linux, как и файлы, являются аксиоматическими понятиями. Иногда процесс отождествляют с запущенной программой, однако это не всегда так. Будем считать, что процесс - это рабочая единица системы, которая выполняет что-то. Многозадачность - это возможность одновременного сосуществования нескольких процессов в одной системе. Linux - многозадачная операционная система. Это означает что процессы в ней работают одновременно. Естественно, это условная формулировка. Ядро Linux постоянно переключает процессы, то есть время от времени дает каждому из них сколько-нибудь процессорного времени. Переключение происходит довольно быстро, поэтому нам кажется, что процессы работают одновременно. Одни процессы могут порождать другие процессы, образовывая древовидную структуру. Порождающие процессы называются родителями или родительскими процессами, а порожденные - потомками или дочерними процессами. На вершине этого "дерева" находится процесс init, который порождается автоматически ядром в процесссе загрузки системы. К каждому процессу в системе привязана пара целых неотрицательных чисел: идентификатор процесса PID (Process IDentifier) и идентификатор родительского процесса PPID (Parent Process IDentifier). Для каждого процесса PID является уникальным (в конкретный момент времени), а PPID равен идентификатору процесса-родителя. Если ввести в оболочку команду ps -ef, то на экран будет выведен список процессов со значениями их PID и PPID (вторая и третья колонки соотв.). Надо отметить, что процесс init (в современных дистрибутивах на 2022 год это будет скорее всего systemd) всегда имеет идентификатор 1 и PPID равный 0. Хотя в реальности процесса с идентификатором 0 не существует. Дерево процессов можно также приставить в наглядном виде при помощи опции --forest программы ps. Если вызвать программу ps без аргументов, то будет выведен список процессов, принадлежащих текущей группе, то есть работающих под текущим терминалом. О том, что такое терминалы и группы процессов, будет рассказано в последующих главах. |
Использование getpid() и getppid()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
Процесс может узнать свой идентификатор (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>. vim getpid.c ------------ /* 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 |
Порождение процесса
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
Как уже говорилось ранее, процесс в 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. Следующий пример демонстрирует то, как это происходит. vim fork01.c ------------ /* 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() программу выполняли уже два независимых процесса, то сообщение родителя вполне могло бы появиться первым. |
Замена образа процесса
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
Итак, теперь мы умеем порождать процессы. Научимся теперь заменять образ текущего процесса другой программой. Для этих целей используется системный вызов execve(), который объявлен в заголовочном файле unistd.h вот так: vim 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. Рассмотрим теперь пример программы, которая заменяет свой образ другой программой. vim execve01.c -------------- /* 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. И, наконец, следующий пример демонстрирует запуск программы в отдельном процессе. vim forkexec01.c ---------------- /* 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() возвратил что-то, то это однозначно ошибка. |
Таблица 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) |