Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 8 (всего у книги 55 страниц)
getline()
и getdelim()
Теперь, когда вы увидели, как читать строки произвольной длины, вы можете сделать вздох облегчения, что вам не нужно самим писать такую функцию. GLIBC предоставляет вам для этого две функции:
#define _GNU_SOURCE 1 /* GLIBC */
#include
#include
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
ssize_t getdelim(char **lineptr, size_t *n, int delim, FILE *stream);
Определение константы _GNU_SOURCE
вводит объявления функций getline()
и getdelim()
. В противном случае они неявно объявлены как возвращающие int
. Для объявления возвращаемого типа ssize_t
нужен файл
. (ssize_t
является «знаковым size_t
». Он предназначен для такого же использования, что и size_t
, но в местах, где может понадобиться использование также и отрицательных значений.)
Обе функции управляют для вас динамической памятью, гарантируя, что буфер, содержащий входную строку, достаточно большой для размещения всей строки. Их отличие друг от друга в том, что getline()
читает до символа конца строки, a getdelim()
использует в качестве разделителя символ, предоставленный пользователем. Общие аргументы следующие:
char **lineptr
Указатель на char*
указатель для адреса динамически выделенного буфера. Чтобы getline()
сделала всю работу, он должен быть инициализирован NULL
. В противном случае, он должен указывать на область памяти, выделенную с помощью malloc()
.
size_t *n
Указатель на размер буфера. Если вы выделяете свой собственный буфер, *n
должно содержать размер буфера. Обе функции обновляют *n
новым значением размера буфера, если они его изменяют.
FILE* stream
Место, откуда следует получать входные символы.
По достижении конца файла или при ошибке функция возвращает -1. Строки содержат завершающий символ конца строки или разделитель (если он есть), а также завершающий нулевой байт. Использование getline()
просто, как показано в ch03-getline.с
:
/* ch03-getline.c – демонстрация getline(). */
#define _GNU_SOURCE 1
#include
#include
/* main – прочесть строку и отобразить ее, пока не достигнут EOF */
int main(void) {
char *line = NULL;
size_t size = 0;
ssize_t ret;
while ((ret = getline(&line, &size, stdin)) != -1)
printf("(%lu) %s", size, line);
return 0;
}
Вот эта функция в действии, показывающая размер буфера. Третья входная и выходная строки намеренно длинные, чтобы заставить getline()
увеличить размер буфера:
$ ch03-getline /* Запустить программу */
this is a line
(120) this is a line
And another line.
(120) And another line.
A llllllllllllllllloooooooooooooooooooooooooooooooonnnnnnnnnnnnnnnngnnnggggggggggg llliiiiiiiiiiiiiiiiiiinnnnnnnnnnnnnnnnnnnneeeeeeeeee
(240) A llllllllllllllllloooooooooooooooooooooooooooooooonnnnnnnnnnnnnnnngnnnggggggggggg llliiiiiiiiiiiiiiiiiiinnnnnnnnnnnnnnnnnnnneeeeeeeeee
strdup()
Одной чрезвычайно типичной операцией является выделение памяти для копирования строки. Это настолько типично, что многие программисты предусматривают для нее простую функцию вместо использования внутритекстового кодирования, и часто эта функция называется strdup()
:
#include
/* strdup – выделить память с malloc() и скопировать строку */
char *strdup(const char *str) {
size_t len;
char *copy;
len = strlen(str) + 1;
/* включить место для завершающего ' ' */
copy = malloc(len);
if (copy != NULL) strcpy(copy, str);
return copy; /* при ошибке возвращает NULL */
}
С появлением стандарта POSIX 2001 программисты по всему миру могут вздохнуть свободнее: эта функция является теперь частью POSIX в виде расширения XSI:
#include
char *strdup(const char *str); /* Копировать str */
Возвращаемое значение равно NULL
, если была ошибка, или указатель на динамически выделенную память с копией str
. Возвращенное значение должно быть освобождено с помощью free()
, когда больше не требуется.
brk()
и sbrk()
Четыре функции, которые мы рассмотрели (malloc()
, calloc()
, realloc()
и free()
) являются стандартными, переносимыми функциями для управления динамической памятью.
На Unix-системах стандартные функции реализованы поверх двух дополнительных, очень примитивных процедур, которые непосредственно изменяют размер адресного пространства процесса. Мы представляем их здесь, чтобы помочь вам понять, как работают GNU/Linux и Unix (снова «под капотом»); крайне маловероятно, что вам когда-нибудь понадобится использовать эти функции в обычных программах. Они определены следующим образом:
#include
#include
int brk(void *end_data_segment);
void *sbrk(ptrdiff_t increment);
Системный вызов brk()
действительно изменяет адресное пространство процесса. Адрес является указателем, представляющим окончание сегмента данных (на самом деле, области кучи, как было показано ранее на рис. 3.1). Ее аргумент является абсолютным логическим адресом, представляющим новое окончание адресного пространства. В случае успеха функция возвращает 0, а в случае неуспеха (-1).
Функцию sbrk()
использовать проще; ее аргумент является числом байтов, на которое нужно увеличить адресное пространство. Вызвав ее с приращением 0, можно определить, где в настоящее время заканчивается адресное пространство. Таким образом, чтобы увеличить адресное пространство на 32 байта, используется код следующего вида:
char *p = (char*)sbrk(0); /* получить текущий конец адресного
пространства */
if (brk(p + 32) < 0) {
/* обработать ошибку */
}
/* в противном случае, изменение сработало */
Практически, вам не нужно непосредственно использовать brk()
. Вместо этого используется исключительно sbrk()
для увеличения (или даже сокращения) адресного пространства. (Вскоре мы покажем, как это делать, в разделе 3.2.5. «Исследование адресного пространства».)
Еще более практично вообще никогда не использовать эти процедуры. Программа, которая их использует, не может затем использовать также и malloc()
, и это создает большую проблему, поскольку многие элементы стандартной библиотеки полагаются на использование malloc()
. Поэтому использование brk()
или sbrk()
может приводить к трудно обнаруживаемым крушениям программы.
Но знать о низкоуровневых механизмах стоит, и конечно же, набор функций malloc()
реализован с помощью sbrk()
и brk()
.
alloca()
«Опасность, Билл Робинсон! Опасность!»
– Робот -
Есть еще одна дополнительная функция выделения памяти, о которой вам нужно знать. Мы обсуждаем ее лишь для того, чтобы вы поняли ее, когда увидите, но не следует использовать ее в новых программах! Эта функция называется alloca()
; она объявлена следующим образом:
/* Заголовок в GNU/Linux, возможно, не на всех Unix-системах */
#include
void *alloca(size_t size);
Функция alloca()
выделяет size
байтов из стека. Хорошо, что выделенная память исчезает после возвращения из функции. Нет необходимости явным образом освобождать память, поскольку это осуществляется автоматически, как в случае с локальными переменными.
На первый взгляд, alloca()
выглядит чем-то типа панацеи для программистов, можно выделять память, о которой можно вовсе не беспокоиться. Подобно Темной Стороне Силы, это, конечно, привлекает. И подобным же образом этого нужно избегать по следующим причинам:
• Функция не является стандартной; она не включена ни в какой стандарт, ни в ISO, ни в С или POSIX.
• Функция не переносима. Хотя она существует на многих системах Unix и GNU/Linux, она не существует на не-Unix системах. Это проблема, поскольку код часто должен быть многоплатформенным, выходя за пределы просто Linux и Unix.
• На некоторых системах alloca()
невозможно даже реализовать. Весь мир не является ни процессором Intel x86, ни GCC.
• Цитируя справку[45]45
alloca(3) – Примеч. науч. ред.
[Закрыть] (добавлено выделение): «Функция alloca
зависит от машины и от компилятора. На многих системах ее реализация ошибочна. Ее использование не рекомендуется».
• Снова цитируя справку: «На многих системах alloca
не может быть использована внутри списка аргументов вызова функции, поскольку резервируемая в стеке при помощи alloca
память оказалась бы в середине стека в пространстве для аргументов функции».
• Она потворствует неряшливому программированию. Тщательная и корректная работа с памятью не сложна; вам просто нужно подумать о том, что вы делаете, и планировать заранее.
GCC обычно использует встроенную версию функции, которая действует с использованием внутритекстового (inline) кода. В результате есть другие последствия alloca()
. Снова цитируя справку:
Факт, что код является внутритекстовым (inline), означает, что невозможно получить адрес этой функции или изменить ее поведение путем компоновки с другой библиотекой.
Внутритекстовый код часто состоит из одной инструкции, подгоняющей указатель стека, и не проверяет переполнение стека. Поэтому нет возврата
NULL
при ошибке.
Справочная страница не углубляется в описание проблемы со встроенной alloca()
GCC. Если есть переполнение стека, возвращаемое значение является мусором. И у вас нет способа сообщить об этом! Это упущение делает невозможным использование GCC alloca()
в устойчивом коде.
Все это должно убедить вас избегать alloca()
в любом новом коде, который вы пишете. В любом случае, если приходится писать переносимый код с использованием malloc()
и free()
, нет причины в использовании также и alloca()
.
Следующая программа, ch03-memaddr.c
, подводит итог всему, что мы узнали об адресном пространстве. Она делает множество вещей, которые не следует делать на практике, таких, как вызовы alloca()
или непосредственные вызовы brk()
и sbrk()
.
1 /*
2 * ch03-memaddr.с – Показать адреса секций кода, данных и стека,
3 * а также BSS и динамической памяти.
4 */
5
6 #include
7 #include
8 #include
9 #include
10
11 extern void afunc(void); /* функция, показывающая рост стека */
12
13 int bss_var; /* автоматически инициализируется в 0, должна быть в BSS */
14 int data_var = 42; /* инициализируется в не 0, должна быть
15 в сегменте данных */
16 int
17 main(int argc, char **argv) /* аргументы не используются */
18 {
19 char *p, *b, *nb;
20
21 printf("Text Locations:n");
22 printf("tAddress of main: %pn", main);
23 printf("tAddress of afunc: %pn", afunc);
24
25 printf("Stack Locations.n");
26 afunc();
27
28 p = (char*)alloca(32);
29 if (p != NULL) {
30 printf("tStart of alloca()'ed array: %pn", p);
31 printf("tEnd of alloca()'ed array: %pn", p + 31);
32 }
33
34 printf("Data Locations:n");
35 printf("tAddress of data_var: %pn", &data_var);
36
37 printf("BSS Locations:n");
38 printf("tAddress of bss_var: %pn", &bss_var);
39
40 b = sbrk((ptrdiff_t)32); /* увеличить адресное пространство */
41 nb = sbrk((ptrdiff_t)0);
42 printf("Heap Locations:n");
43 printf("tInitial end of heap: %pn", b);
44 printf("tNew end of heap: %pn", nb);
45
46 b = sbrk((ptrdiff_t)-16); /* сократить его */
47 nb = sbrk((ptrdiff_t)0);
48 printf("tFinal end of heap: %pn", nb);
49 }
50
51 void
52 afunc(void)
53 {
54 static int level = 0; /* уровень рекурсии */
55 auto int stack_var; /* автоматическая переменная в стеке */
56
57 if (++level == 3) /* избежать бесконечной рекурсии */
58 return;
59
60 printf("tStack level %d: address of stack_var: %pn",
61 level, &stack_var);
62 afunc(); /* рекурсивный вызов */
63 }
Эта программа распечатывает местонахождение двух функций main()
и afunc()
(строки 22–23). Затем она показывает, как стек растет вниз, позволяя afunc()
(строки 51–63) распечатать адреса последовательных экземпляров ее локальной переменной stack_var
. (stack_var
намеренно объявлена как auto
, чтобы подчеркнуть, что она находится в стеке.) Затем она показывает расположение памяти, выделенной с помощью alloca()
(строки 28–32). В заключение она печатает местоположение переменных данных и BSS (строки 34–38), а затем памяти, выделенной непосредственно через sbrk()
(строки 40–48). Вот результаты запуска программы на системе Intel GNU/Linux:
$ ch03-memaddr
Text Locations:
Address of main: 0x804838c
Address of afunc: 0x80484a8
Stack Locations:
Stack level 1: address of stack_var: 0xbffff864
Stack level 2: address of stack_var: 0xbffff844
/* Стек растет вниз */
Start of alloca()'ed array: 0xbffff860
End of alloca()'ed array: 0xbffff87f
/* Адреса находятся в стеке */
Data Locations:
Address of data_var: 0x80496b8
BSS Locations:
Address of bss_var: 0x80497c4
/* BSS выше инициализированных данных */
Heap Locations:
Initial end of heap: 0x80497c8
/* Куча непосредственно над BSS */
New end of heap: 0x80497e8
/* И растет вверх */
Final end of heap: 0x80497d8
/* Адресные пространства можно сокращать */
• У каждой программы Linux и (Unix) есть различные области памяти. Они хранятся в разных частях файла исполняемой программы на диске. Некоторые из секций загружаются при запуске программы в одну и ту же область памяти. Все запушенные экземпляры одной и той же программы разделяют исполняемый код (сегмент текста). Программа size
показывает размеры различных областей переместимых объектных файлов и полностью скомпонованных исполняемых файлов.
• В адресном пространстве запушенной программы могут быть дыры, а размер адресного пространства может изменяться при выделении и освобождении памяти. На современных системах адрес 0 не является частью адресного пространства, поэтому не пытайтесь разыменовывать указатели NULL
.
• На уровне языка С память выделяется с помощью одной из функций malloc()
, calloc()
или realloc()
. Память освобождается с помощью free()
. (Хотя с помощью realloc()
можно делать все, использование ее таким образом не рекомендуется.) Освобожденная память обычно не удаляется из адресного пространства; вместо этого она используется повторно при последующих выделениях.
• Необходимо предпринять чрезвычайные меры осторожности в следующих случаях
• освобождать лишь память, выделенную с помощью соответствующих процедур,
• освобождать память один и только один раз,
• освобождать неиспользуемую память и
• не допускать «утечки» динамически выделяемой памяти.
• POSIX предоставляет для удобства функцию strdup()
, a GLIBC предоставляет функции getline()
и getdelim()
для чтения строк произвольной длины. Функции интерфейса низкоуровневых системных вызовов brk()
и sbrk()
предоставляют непосредственный, но примитивный доступ к выделению и освобождению памяти. Если вы не создаете свой собственный распределитель памяти, следует избегать их. Существует функция alloca()
для выделения памяти в стеке, но ее использование не рекомендуется. Подобно умению распознавать ядовитый плющ, про нее нужно знать лишь для того, чтобы избегать ее.
1. Начав со структуры —
struct line {
size_t buflen;
char *buf;
FILE* fp;
};
– напишите свою собственную функцию readline()
, которая будет читать строки любой длины. Не беспокойтесь о строках, продолженных с помощью обратного слеша. Вместо использования fgetc()
для чтения строк используйте getc()
для чтения одного символа за раз.
2. Сохраняет ли ваша функция завершающий символ конца строки? Объясните, почему.
3. Как ваша функция обрабатывает строки, оканчивающиеся CR-LF?
4. Как вы инициализируете структуру? В отдельной процедуре? С помощью документированных условий для определенных значений в структуре?
5. Как вы обозначаете конец файла? Как вы указываете, что возникла ошибка ввода/вывода? Должна ли ваша функция сообщать об ошибках? Объясните, почему.
6. Напишите программу, которая использует вашу функцию для ее тестирования, а также другую программу, создающую входные данные для первой программы. Протестируйте функцию.
7. Перепишите вашу функцию с использованием fgets()
и протестируйте ее. Является ли новый код более сложным или менее сложным? Какова его производительность по сравнению с версией getc()
?
8. Изучите страницу справки V7 для end(3) (/usr/man/man3/end.3
в дистрибутиве V7). Пролила ли она свет на то, как может работать 'sbrk(0)
'?
9. Усовершенствуйте ch03-memaddr.c
так, чтобы она печатала расположение аргументов и переменных окружения. В какой области адресного пространства они находятся?
Глава 4
Файлы и файловый ввод/вывод
Данная глава описывает базовые файловые операции: открытие и создание файлов, чтение и запись в них, перемещение в них и их закрытие. По ходу дела она представляет стандартные механизмы для обнаружения ошибок и сообщений о них. Глава заканчивается описанием того, как установить длину файла и принудительно сбросить данные файла и вспомогательные данные на диск.
4.1. Введение в модель ввода/вывода Linux/UnixМодель API Linux/Unix для ввода/вывода проста. Ее можно суммировать четырьмя словами. открыть, прочитать, записать, закрыть. Фактически, это имена системных вызовов: open()
, read()
, write()
, close()
. Вот их объявления:
#include
#include
#include
#include
int open(const char *pathname, int flags, mode_t mode);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
int close(int fd);
В следующем и дальнейших разделах мы проиллюстрируем модель, написав очень простую версию cat
. Она так проста, что даже не имеет опций; все, что она делает, – объединяет содержимое двух именованных файлов и выводит его в стандартный вывод. Она выводит минимум сообщений об ошибках. Написав, мы сравним ее с V7 cat
.
Мы представим программу сверху вниз, начав с командной строки. В последующих разделах мы представим обработку ошибок, а затем перейдем к сущностным задачам, показав, каким образом осуществляется реальный файловый ввод/вывод.
4.2. Представление базовой структуры программыНаша версия cat следует структуре, которая обычно является полезной. Первая часть начинается с комментариев, заголовочных файлов, объявлений и функции main():
1 /*
2 * ch04-cat.c – Демонстрация open(), read(), write(), close(),
3 * errno и strerror().
4 */
5
6 #include
7 #include
8 #include
9 #include
10 #include
11 #include
12 #include
13
14 char *myname;
15 int process(char *file);
16
17 /* main – перечислить аргументы файла */
18
19 int
20 main(int argc, char **argv)
21 {
22 int i;
23 int errs = 0;
24
25 myname = argv[0];
26
27 if (argc == 1)
28 errs = process("-");
29 else
30 for (i = 1; i < argc; i++)
31 errs += process(argv[i]);
32
33 return (errs != 0);
34 }
…продолжение далее в главе.
Переменная myname
(строка 14) используется далее для сообщений об ошибках; main()
первым делом устанавливает в ней имя программы (argv[0]
). Затем main()
в цикле перечисляет аргументы. Для каждого аргумента она вызывает функцию process()
.
Когда в качестве имени файла дано -
(простая черточка, или знак минус), cat
Unix вместо попытки открыть файл с именем читает стандартный ввод. Вдобавок, cat
читает стандартный ввод, когда нет аргументов. ch04-cat
реализует оба этих поведения. Условие 'argc == 1
' (строка 27) истинно, когда нет аргументов имени файла; в этом случае main()
передает «-
» функции process()
. В противном случае, main()
перечисляет аргументы, рассматривая их как файлы, которые необходимо обработать. Если один из них окажется «-
», программа обрабатывает стандартный ввод.
Если process()
возвращает ненулевое значение, это означает, что случилась какая– то ошибка. Ошибки подсчитываются в переменной errs
(строки 28 и 31). Когда main()
завершается, она возвращает 0, если не было ошибок, и 1, если были (строка 33). Это довольно стандартное соглашение, значение которого более подробно обсуждается в разделе 9.1.5.1 «Определение статуса завершения процесса».
Структура, представленная в main()
, довольно общая: process()
может делать с файлом все, что мы захотим. Например (игнорируя особый случай «-
»), process() также легко могла бы удалять файлы вместо их объединения!
Прежде чем рассмотреть функцию process()
, нам нужно описать, как представлены ошибки системных вызовов и как осуществляется ввод/вывод. Сама функция process()
представлена в разделе 4.4.3 «Чтение и запись».