Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 6 (всего у книги 55 страниц)
• Программы на С получают аргументы своей командной строки через параметры argc
и argv
. Функция getopt()
предоставляет стандартный способ для последовательного разбора опций и их аргументов GNU версия getopt()
предоставляет некоторые расширения, a getopt_long()
и getopt_long_only()
дает возможность легкого разбора длинных опций.
• Окружение представляет собой набор пар 'имя=значение
', который каждая программа наследует от своего родителя. Программы могут по прихоти своего автора использовать для изменения своего поведения переменные окружения, в дополнение к любым аргументам командной строки. Для получения значений переменных окружения, изменения их значений или удаления существуют стандартные процедуры (getenv()
, setenv()
, putenv()
и unsetenv()
). При необходимости можно получить доступ ко всему окружению через внешнюю переменную environ
или через третий аргумент char **envp
функции main()
. Последний способ не рекомендуется.
1. Предположим, что программа принимает опции -a
, -b
и -с
, и что -b
требует наличия аргумента. Напишите для этой программы код ручного разбора аргументов без использования getopt()
или getopt_long()
. Для завершения обработки опций принимается –
. Убедитесь, что -ас работает, также, как -bYANKEES
, -b YANKEES
и -abYANKEES
. Протестируйте программу.
2. Реализуйте getopt()
. Для первой версии вы можете не беспокоиться насчет случая 'optstring[0] == ':'
'. Можете также игнорировать opterr
.
3. Добавьте код для 'optstring[0] == ':'
' и opterr
к своей версии getopt()
.
4. Распечатайте и прочтите файлы GNU getopt.h
, getopt.с
и getopt1.с
.
5. Напишите программу, которая объявляет как environ
, так и envp
, и сравните их значения.
6. Разбор аргументов командной строки и опций является тем колесом, которое многие люди не могут не изобретать вновь. Вы можете захотеть познакомиться с различными анализирующими аргументы пакетами, помимо getopt()
и getopt_long()
, такими, как:
• библиотека анализа аргументов Plan 9 From Bell Labs arg(2)[31]31
http://plan9.bell-labs.com/magic/man2html/2/arg
– Примеч. автора.
[Закрыть],
• Argp[32]32
http://www.gnu.org/manual/glibc/html_node/Argp.html
– Примеч. автора.
[Закрыть],
• Argv[33]33
http://256.com/sources/argv
– Примеч. автора.
[Закрыть],
• Autoopts[34]34
http://autogen.sourceforge.net/autoopts.html
– Примеч. автора.
[Закрыть],
• GNU Gengetopt[35]35
ftp://ftp.gnu.org/gnu/gengetopt/
– Примеч. автора.
[Закрыть],
• Opt[36]36
http://nis-www.lanl.gov/~jt/Software/opt/opt-3.19.tar.gz
– Примеч. автора.
[Закрыть],
• Popt[37]37
http://freshmeat.net/projects/popt/?topic_id=809
– Примеч. автора.
[Закрыть]. См. также справочную страницу popt(3) системы GNU/Linux.
7. Дополнительный балл, почему компилятор С не может полностью игнорировать ключевое слово register? Подсказка: какие действия невозможно совершать с регистровой переменной?
Глава 3
Управление памятью на уровне пользователя
Без памяти для хранения данных программа не может выполнить никакую работу (Или, скорее, невозможно выполнить никакую полезную работу.) Реальные программы не могут позволить себе полагаться на буферы и массивы структур данных фиксированного размера. Они должны быть способны обрабатывать вводимые данные различных размеров, от незначительных до больших. Это, в свою очередь, ведет к использованию динамически выделяемой памяти – памяти, выделяемой в ходе исполнения, а не при компиляции. Вот как вводится в действие принцип GNU «никаких произвольных ограничений».
Поскольку динамически выделяемая память является основным строительным блоком для реальных программ, мы рассмотрим этот вопрос в начале, до рассмотрения всего остального. Наше обсуждение фокусируется на рассмотрении процесса и его памяти исключительно на уровне пользователя; оно не имеет ничего общего с архитектурой процессора.
3.1. Адресное пространство Linux/UnixВ качестве рабочего определения мы приняли, что процесс является запушенной программой. Это означает, что операционная система загрузила исполняемый файл для этой программы в память, сделала доступными аргументы командной строки и переменные окружения и запустила ее. Процесс имеет пять выделенных для него концептуально различных областей памяти:
Код
Часто называемая сегментом текста область, в которой находятся исполняемые инструкции. Linux и Unix организуют вещи таким образом, что несколько запушенных экземпляров одной программы по возможности разделяют свой код; в любое время в памяти находится лишь одна копия инструкций одной и той же программы (Это прозрачно для работающих программ.) Часть исполняемого файла, содержащая сегмент текста, называется секцией текста.
Инициализированные данные
Статически выделенные и глобальные данные, которые инициализированы ненулевыми значениями, находятся в сегменте данных. У каждого процесса с одной и той же запущенной программой свой собственный сегмент данных. Часть исполняемого файла, содержащая сегмент данных, является секцией данных.
Инициализированные нулями данные [38]38
Существует также другое название для этой области данных – Неинициализированные данные – Примеч. науч. ред.
[Закрыть]
Глобальные и статически выделенные данные, которые по умолчанию инициализированы нулями, хранятся в области процесса, который называют областью BSS[39]39
BSS означает 'Block Started by Symbol', мнемоника из ассемблера IBM 7094 – Примеч. автора.
[Закрыть]. У каждого процесса, в котором запущена одна и та же программа, своя область BSS. При запуске данные BSS помещаются в сегмент данных. В исполняемом файле они хранятся в секции BSS.
Формат исполняемого файла Linux/Unix таков, что пространство исполняемого файла на диске занимают лишь переменные, инициализированные ненулевыми значениями. Поэтому большой массив, объявленный как 'static char somebuf[2048];
', который автоматически заполняется нулями, не занимает 2 Кб пространства на диске. (Некоторые компиляторы имеют опции, позволяющие вам помещать инициализированные нулями данные в сегмент данных.)
Куча (heap)
Куча является местом, откуда выделяется динамическая память (получаемая с помощью функции malloc()
и подобными ей). Когда из кучи выделяется память, адресное пространство процесса растет, что вы можете заметить, отслеживая запущенный процесс с помощью команды ps
.
Хотя память можно вернуть обратно системе и сократить адресное пространство процесса, этого почти никогда не происходит. (Мы различаем освобождение больше не использующейся динамической памяти и сокращение адресного пространства; подробнее это обсуждается далее в этой главе.)
Для кучи характерен «рост вверх». Это означает, что последовательные элементы, добавляемые к куче, добавляются по адресам, численно превосходящим предыдущие. Куча обычно начинается сразу после области BSS сегмента данных.
Стек
Сегмент стека – это область, в которой выделяются локальные переменные. Локальными являются все переменные, объявленные внутри левой открывающей фигурной скобки тела функции (или другой левой фигурной скобки) и не имеющие ключевого слова static
.
В большинстве архитектур параметры функций также помещаются в стек наряду с «невидимой» учетной информацией, генерируемой компилятором, такой, как возвращаемое функцией значение и адрес возврата для перехода из функции к месту, откуда произошел вызов. (В некоторых архитектурах для этого используются регистры.) Именно использование стека для параметров функций и возвращаемых ими значений делает удобным написание рекурсивных функций (тех, которые вызывают сами себя) Переменные, хранящиеся в стеке, «исчезают», когда функция, их содержащая, возвращается, пространство стека используется повторно для последующих вызовов функций. В большинстве современных архитектур стек «растет вниз», это означает, что элементы, находящиеся глубже в цепи вызова, находятся по численно меньшим адресам. В работающей программе области инициализированных данных, BSS и кучи обычно размещаются в единой протяженной области: сегменте данных. Сегменты стека и кода отделены от сегмента данных и друг от друга. Это показано на рис. 3.1.
Рис. 3.1. Адресное пространство Linux/Unix
Хотя перекрывание стека и кучи теоретически возможно, операционная система предотвращает этот случай, и любая программа, пытающаяся это сделать, напрашивается на неприятности. Это особенно верно для современных систем, в которых адресные пространства большие и интервал между верхушкой стека и концом кучи значителен. Различные области памяти могут иметь различную установленную на память аппаратную защиту. Например, сегмент текста может быть помечен «только для исполнения», тогда как у сегментов данных и стека разрешение на исполнение может отсутствовать. Такая практика может предотвратить различные виды атак на безопасность. Подробности, конечно, специфичны для оборудования и операционной системы, и они могут со временем меняться. Стоит заметить, что стандартные как С, так и C++ позволяют размещать элементы с атрибутом const
в памяти только для чтения. Сводка взаимоотношений различных сегментов приведена в табл. 3.1.
Таблица 3.1. Сегменты исполняемой программы и их размещение
Код | Text | Text |
Инициализированные данные | Data | Data |
BSS | Data | BSS |
Куча | Data | |
Стек | Stack |
Программа size
распечатывает размеры в байтах каждой из секций text, data и BSS вместе с общим размером в десятичном и шестнадцатеричном виде. (Программа ch03-memaddr.с
показана далее в этой главе; см. раздел 3.2.5 «Исследование адресного пространства».)
$ cc -o ch03-memaddr.с -о ch03-memaddr /* Компилировать программу */
$ ls -l ch03-memaddr /* Показать общий размер */
-rwxr-xr-x 1 arnold devel 12320 Nov 24 16:45 ch03-memaddr
$ size ch03-memaddr /* Показать размеры компонентов */
text data bss dec hex filename
1458 276 8 1742 6ce ch03-memaddr
$ strip ch03-memaddr /* Удалить символы */
$ ls -l ch03-memaddr /* Снова показать общий размер */
-rwxr-xr-x 1 arnold devel 3480 Nov 24 16:45 ch03-memaddr
$ size ch03-memaddr /* Размеры компонентов не изменились */
text data bss dec hex filename
1458 276 8 1742 6ce ch03-memaddr
Общий размер загруженного в память из файла в 12 320 байтов всего лишь 1742 байта. Большую часть этого места занимают символы (symbols), список имен переменных и функций программы. (Символы не загружаются в память при запуске программы.) Программа strip
удаляет символы из объектного файла. Для большой программы это может сохранить значительное дисковое пространство ценой невозможности отладки дампа ядра[40]40
Дамп ядра (core dump) является образом запущенного процесса в памяти, который создаётся при неожиданном завершении процесса. Позже этот дамп может быть использован для отладки Unix-системы, называют это файл core
, а системы GNU/Linux – core.pid
, где pid
– ID потерпевшего крушения процесса – Примеч. автора.
[Закрыть], если таковой появится (На современных системах об этом не стоит беспокоиться, не используйте strip
.) Даже после удаления символов файл все еще больше, чем загруженный в память образ, поскольку формат объектного файла содержат дополнительные данные о программе, такие, как использованные разделяемые библиотеки, если они есть.[41]41
Описание здесь намеренно упрощено. Запущенные программы занимают значительно больше места, чем указывает программа size
, поскольку разделяемые библиотеки включены в адресное пространство. Также сегмент данных будет расти по мере выделения программной памяти – Примеч. автора.
[Закрыть]
Наконец, упомянем потоки (threads), которые представляют несколько цепочек исполнения в рамках единственного адресного пространства. Обычно у каждого потока имеется свой собственный стек, а также способ получения локальных данных потока, т.е. динамически выделяемых данных для персонального использования этим потоком. Мы больше не будем рассматривать в данной книге потоки, поскольку это является продвинутой темой.
3.2. Выделение памятиЧетыре библиотечные функции образуют основу управления динамической памятью С Мы опишем сначала их, затем последуют описания двух системных вызовов, поверх которых построены эти библиотечные функции. Библиотечные функции С, в свою очередь, обычно используются для реализации других выделяющих память библиотечных функций и операторов C++ new
и delete
.
Наконец, мы обсудим функцию, которую часто используют, но которую мы не рекомендуем использовать.
malloc()
, calloc()
, realloc()
, free()
Динамическую память выделяют с помощью функций malloc()
или calloc()
. Эти функции возвращают указатели на выделенную память. Когда у вас есть блок памяти определенного первоначального размера, вы можете изменить его размер с помощью функции realloc()
. Динамическая память освобождается функцией free()
.
Отладка использования динамической памяти сама по себе является важной темой. Инструменты для этой цели мы обсудим в разделе 15.5.2 «Отладчики выделения памяти».
Вот объявления функций из темы справки GNU/Linux malloc(3):
#include
void *calloc(size_t nmemb, size_t size);
/* Выделить и инициализировать нулями */
void *malloc(size_t size);
/* Выделить без инициализации */
void free(void *ptr);
/* Освободить память */
void *realloc(void *ptr, size_t size);
/* Изменить размер выделенной памяти */
Функции выделения памяти возвращают тип void*
. Это бестиповый или общий указатель, все, что с ним можно делать – это привести его к другому типу и назначить типизированному указателю. Примеры впереди.
Тип size_t
является беззнаковым целым типом, который представляет размер памяти. Он используется для динамического выделения памяти, и далее в книге мы увидим множество примеров его использования. На большинстве современных систем size
_t является unsigned long
, но лучше явно использовать size_t
вместо простого целого типа unsigned
.
Тип ptrdiff_t
используется для вычисления адреса в арифметике указателей, как в случае вычисления указателя в массиве:
#define MAXBUF ...
char *p;
char buf[MAXBUF];
ptrdiff_t where;
p = buf;
while (/* некоторое условие */) {
...
p += something;
...
where = p – buf; /* какой у нас индекс? */
}
Заголовочный файл
объявляет множество стандартных библиотечных функций С и типов (таких, как size_t
), он определяет также константу препроцессора NULL
, которая представляет «нуль» или недействительный указатель. (Это нулевое значение, такое, как 0 или '((void*)0)
'. Явное использование 0 относится к стилю С++; в С, однако, NULL
является предпочтительным, мы находим его гораздо более читабельным для кода С.)
malloc()
Сначала память выделяется с помощью malloc()
. Передаваемое функции значение является общим числом затребованных байтов. Возвращаемое значение является указателем на вновь выделенную область памяти или NULL
, если память выделить невозможно. В последнем случае для обозначения ошибки будет установлен errno
. (errno является специальной переменной, которую системные вызовы и библиотечные функции устанавливают для указания произошедшей ошибки. Она описывается в разделе 4.3 «Определение ошибок».) Например, предположим, что мы хотим выделить переменное число некоторых структур. Код выглядит примерно так:
struct coord { /* 3D координаты */
int x, y, z;
} *coordinates;
unsigned int count; /* сколько нам нужно */
size_t amount; /* общий размер памяти */
/* ... как-нибудь определить нужное число... */
amount = count * sizeof(struct coord); /* сколько байт выделить */
coordinates = (struct coord*)malloc(amount); /* выделить память */
if (coordinates == NULL) {
/* сообщить об ошибке, восстановить или прервать */
}
/* ... использовать координаты... */
Представленные здесь шаги являются стереотипными. Порядок следующий:
1. Объявить указатель соответствующего типа для выделенной памяти.
2. Вычислить размер выделяемой памяти в байтах. Для этого нужно умножить число нужных объектов на размер каждого из них. Последний получается с помощью оператора С sizeof
, который для этой цели и существует (наряду с другими). Таким образом, хотя размер определенной структуры среди различных компиляторов и архитектур может различаться, sizeof
всегда возвращает верное значение, а исходный код остается правильным и переносимым.
При выделении массивов для строк символов или других данных типа char
нет необходимости умножения на sizeof(char)
, поскольку последнее по определению всегда равно 1. Но в любом случае это не повредит.
3. Выделить память с помощью malloc()
, присвоив возвращаемое функцией значение переменной указателя. Хорошей практикой является приведение возвращаемого malloc()
значения к типу переменной, которой это значение присваивается. В С этого не требуется (хотя компилятор может выдать предупреждение). Мы настоятельно рекомендуем всегда приводить возвращаемое значение.
Обратите внимание, что на C++ присвоение знамения указателя одного типа указателю другого типа требует приведения типов, какой бы ни был контекст. Для управления динамической памятью программы C++ должны использовать new
и delete
, а не malloc()
и free()
, чтобы избежать проблем с типами.
4. Проверить возвращенное значение. Никогда не предполагайте, что выделение памяти было успешным. Если выделение памяти завершилось неудачей, malloc()
возвращает NULL
. Если вы используете значение без проверки, ваша программа может быть немедленно завершена из-за нарушения сегментации (segmentation violation), которое является попыткой использования памяти за пределами своего адресного пространства.
Если вы проверите возвращенное значение, вы можете по крайней мере выдать диагностическое сообщение и корректно завершить программу. Или можете попытаться использовать какой-нибудь другой способ восстановления.
Выделив блок памяти и установив в coordinates
указатель на него, мы можем затем интерпретировать coordinates
как массив, хотя он в действительности указатель:
int cur_x, cur_y, cur_z;
size_t an_index;
an_index = something;
cur_x = coordinates[an_index].x;
cur_y = coordinates[an_index].y;
cur_z = coordinates[an_index].z;
Компилятор создает корректный код для индексирования через указатель при получении доступа к членам структуры coordinates[an_index]
.
ЗАМЕЧАНИЕ. Блок памяти, возвращенный
malloc()
, не инициализирован. Он может содержать любой случайный мусор. Необходимо сразу же инициализировать память нужными значениями или хотя бы нулями. В последнем случае используйте функциюmemset()
(которая обсуждается в разделе 12.2 «Низкоуровневая память, функцииmemXXX()
):
memset(coordinates, ' ', amount);
Другой возможностью является использование
calloc()
, которая вскоре будет описана.
Джефф Колье (Geoff Collyer) рекомендует следующую методику для выделения памяти:
some_type *pointer;
pointer = malloc(count * sizeof(*pointer));
Этот подход гарантирует, что malloc()
выделит правильное количество памяти без необходимости смотреть объявление pointer. Если тип pointer
впоследствии изменится, оператор sizeof
автоматически гарантирует, что выделяемое число байтов остается правильным. (Методика Джеффа опускает приведение типов, которое мы только что обсуждали. Наличие там приведения типов также гарантирует диагностику, если тип pointer
изменится, а вызов malloc()
не будет обновлен.)
free()
Когда вы завершили использование памяти, «верните ее обратно», используя функцию free()
. Единственный аргумент является указателем, предварительно полученным с использованием другой функции выделения. Можно (хотя это бесполезно) передать функции free()
пустой указатель:
free(coordinates);
coordinates = NULL; /* не требуется, но хорошая мысль */
После вызова free(coordinates)
доступ к памяти, на которую указывает coordinates
, запрещен. Она теперь «принадлежит» процедурам выделения, и они могут поступать с ней как сочтут нужным. Они могут изменить содержимое памяти или даже удалить ее из адресного пространства процесса! Таким образом, есть несколько типичных ошибок, которых нужно остерегаться при использовании free()
:
Доступ к освобожденной памяти
Если она не была освобождена, переменная coordinates
продолжает указывать на блок памяти, который больше не принадлежит приложению. Это называется зависшим указателем (dangling pointer). На многих системах вы можете уйти от наказания, продолжая использовать эту память, по крайней мере до следующего выделения или освобождения памяти. На других системах, однако, такой доступ не будет работать. В общем, доступ к освобожденной памяти является плохой мыслью: это непереносимо и ненадежно, и GNU Coding Standards отвергает его. По этой причине неплохо сразу же установить в указателе программы значение NULL
. Если затем вы случайно попытаетесь получить доступ к освобожденной памяти, программа немедленно завершится с ошибкой нарушения сегментации (надеемся, до того, как вы успели вашу программу выпустить в свет).
Освобождение одного и того же указателя дважды
Это создает «неопределенное поведение». После передачи блока памяти обратно выделяющим процедурам они могут объединить освобожденный блок с другой свободной памятью, которая есть в их распоряжении. Освобождение чего-то уже освобожденного ведет к неразберихе и в лучшем случае к крушению; известно, что так называемые двойные освобождения приводили к проблемам безопасности.
Передача указателя, полученного не от функций malloc()
, calloc()
или realloc()
Это кажется очевидным, но тем не менее важно. Плоха даже передача указателя на адрес где-то в середине динамически выделенной памяти:
free(coordinates + 10);
/* Освободить все кроме первых 10 элементов */
Этот вызов не будет работать и, возможно, приведет к пагубным последствиям, таким как крушение. (Это происходит потому, что во многих реализациях malloc()
«учетная» информация хранится перед возвращенными данными. Когда free()
пытается использовать эту информацию, она обнаружит там недействительные данные. В других реализациях, где учетная информация хранится в конце выделенного блока; возникают те же проблемы.)
Выход за пределы буфера
Доступ к памяти за пределами выделенного блока также ведет к неопределенному поведению, опять из-за того, что она может содержать учетную информацию или, возможно, вообще не принадлежать адресному пространству процесса. Запись в такой участок памяти гораздо хуже, поскольку это может уничтожить учетные данные.
Отказ в освобождении памяти
Любая динамическая память, которая больше не нужна, должна быть освобождена. В частности, необходимо тщательно управлять памятью и освобождать ее, когда она выделяется внутри циклов или рекурсивных или глубоко вложенных вызовов функций. Отказ от этого ведет к утечкам памяти, при которых память процесса может неограниченно расти; в конце концов, процесс завершается из-за нехватки памяти. Эта ситуация может быть особенно разрушительной, если память выделяется для ввода записи или как-то еще связана с вводом: утечка памяти будет незаметна при использовании незначительных объемов ввода, но внезапно станет очевидной (и приведет в замешательство) при больших. Эта ошибка еще хуже для систем, которые должны работать непрерывно, как в системах телефонных коммутаторов. Утечка памяти, вызывающая крушение такой системы, может привести к значительным денежным или другим потерям.
Даже если программа никогда не завершается из-за недостатка памяти, постоянно увеличивающиеся программы теряют производительность, поскольку операционная система должна сохранять использующиеся данные в физической памяти. В худшем случае, это может привести к поведению, известному как пробуксовка (thrashing), при которой операционная система так занята перекачкой содержимого адресного пространства в и из физической памяти, что реальная работа не делается.
Хотя free()
может вернуть освобожденную память системе и сократить адресное пространство процесса, это почти никогда не делается. Вместо этого освобожденная память готова для нового выделения при следующем вызове malloc()
, calloc()
или realloc()
.
При условии, что освобожденная память продолжает оставаться в адресном пространстве процесса, стоит обнулить ее перед освобождением. Например, такой способ может оказаться предпочтительным для программ с повышенными требованиями к безопасности.
Обсуждение ряда полезных инструментов для отладки динамической памяти см в разделе 15.5.2 «Отладчики выделения памяти».