Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 37 (всего у книги 55 страниц)
abort()
Бывают моменты, когда программа просто не может продолжаться. Обычно лучше всего при этом выдать сообщение об ошибке и вызвать exit()
. Однако, особенно для ошибок, являющихся проблемами программирования, полезно не только завершиться, но и создать дамп ядра, который сохраняет в файле состояние работающей программы для последующего исследования в отладчике. В этом заключается работа функции abort()
:
#include
void abort(void);
Функция abort()
посылает сигнал SIGABRT
самому процессу. Это случится, даже если SIGABRT
заблокирован или игнорируется. После этого осуществляется обычное для SIGABRT
действие, которое заключается в создании дампа ядра.
Примером abort()
в действии является макрос assert()
, описанный в начале данной главы. Когда assert()
обнаруживает, что его выражение ложно, он выводит сообщение об ошибке, а затем вызывает abort()
для создания дампа ядра.
В соответствии со стандартом С, осуществляет abort()
очистку или нет, зависит от реализации. Под GNU/Linux она выполняет очистку: все потоки
перед завершением программы закрываются. Обратите, однако, внимание, что для открытых файлов, использующих системные вызовы на основе дескрипторов файлов, ничего не делается. (Если открыты лишь файлы или каналы, ничего не нужно делать. Хотя мы не обсуждали это, дескрипторы файлов используются также для сетевых соединений, и оставление их открытыми является плохой практикой.)
«Идите прямо в тюрьму. Не проходите GO. Не забирайте 200$».
– Монополия -
Вы, без сомнения, знаете, чем является goto
: передачей потока управления на метку где-то в текущей функции. Операторы goto
при скупом употреблении могут послужить удобочитаемости и правильности функции (Например, когда все проверки ошибок используют goto
для перехода на метку в конце функции, такую, как clean_up
, код с этой меткой проводит очистку [закрывая файлы и т.п.] и возвращается.) При плохом использовании операторы goto
могут привести к так называемой «лапше» в коде, логику которого становится невозможно отследить.
Оператор goto
в языке С ограничен переходом на метку в текущей функции. Многие языки в семействе Алгола, такие, как Паскаль, допускают использование goto
для выхода из вложенной функции в предшествующую вызывающую функцию. Однако в С нет способа, в пределах синтаксиса самого языка, перейти в определенную точку другой функции, пусть даже и вызывающей. Такой переход называется нелокальным переходом.
Почему полезен нелокальный переход? Рассмотрите интерактивную программу, которая считывает и выполняет программы. Предположим, пользователь запускает длительное задание, разочаровывается или меняет мнение о данном задании и нажимает CTRL-С для генерирования сигнала SIGINT
. Когда запускается обработчик сигнала, он может перейти обратно в начало главного цикла чтения и обработки команд. Строковый редактор ed представляет простой пример этого:
$ ed -p '> ' sayings /* Запуск ed, '> ' используется как приглашение */
sayings: No such file or directory
> a /* Добавить текст */
Hello, world
Don't panic
^C /* Сгенерировать SIGINT */
? /* Сообщение об ошибке ''один размер подходит всем'' */
> 1,$p /* ed возвращается в командную строку */
Hello, world /* '1,$p' prints all the lines */
Don't panic
> w /* Сохранить файл */
25
> q /* Все сделано */
Внутри себя ed
устанавливает перед циклом команд точку возврата, и обработчик сигнала осуществляет нелокальный переход на эту точку возврата.
setjmp()
и longjmp()
Нелокальные переходы осуществляются с помощью функций setjmp()
и longjmp()
. Эти функции используются в двух разновидностях. Традиционные процедуры определены стандартом ISO С:
#include
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
Тип jmp_buf
определен через typedef
в
. setjmp()
сохраняет текущее «окружение» в env
. env
обычно является глобальной или статической на уровне файла переменной, так что она может использоваться из вызванной функции. Это окружение включает любую информацию, необходимую для перехода на местоположение, из которого была вызвана setjmp()
. Содержание jmp_buf
по своей природе машинно-зависимо; таким образом, jmp_buf
является непрозрачным типом: тем, что вы используете, не зная, что находится внутри него.
setjmp()
возвращает 0, когда она вызывается для сохранения в jmp_buf
текущего окружения. Ненулевое значение возвращается, когда с использованием окружения осуществляется нелокальный переход:
jmp_buf command_loop; /* На глобальном уровне */
/* ... затем в main() ... */
if (setjmp(command_loop) == 0) /* Состояние сохранено, продолжить */
;
else /* Мы попадаем сюда через нелокальный переход */
printf("?n"); /* ed's famous message */
/* ... теперь начать цикл команд ... */
longjmp()
осуществляет переход. Первым параметром является jmp_buf
, который должен быть инициализирован с помощью setjmp()
. Второй является целым ненулевым значением, которое setjmp()
возвращает в первоначальное окружение. Это сделано так, что код, подобный только что показанному, может различить установку окружения и прибытие путем нелокального перехода.
Стандарт С утверждает, что даже если longjmp()
вызывается со вторым аргументом, равным 0, setjmp()
по-прежнему возвращает ненулевое значение. В таком случае она возвращает 1.
Возможность передать целое значение и вернуться обратно из setjmp()
полезна; это позволяет коду уровня пользователя различать причину перехода. Например, gawk
использует эту возможность для обработки операторов break
и continue
внутри циклов. (Язык awk осознанно сделан похожим на С в своем синтаксисе для циклов, с использованием while
, do-while
, for
, break
и continue
.) Использование setjmp()
выглядит следующим образом (из eval.c
в дистрибутиве gawk
3.1.3):
507 case Node_K_while:
508 PUSH_BINDING(loop_tag_stack, loop_tag, loop_tag_valid);
509
510 stable_tree = tree;
511 while (eval_condition(stable_tree->lnode)) {
512 INCREMENT(stable_tree->exec_count);
513 switch (setjmp(loop_tag)) {
514 case 0: /* обычный не переход */
515 (void)interpret(stable_tree->rnode);
516 break;
517 case TAG_CONTINUE: /* оператор continue */
518 break;
519 case TAG_BREAK: /* оператор break */
520 RESTORE_BINDING(loop_tag_stack, loop_tag, loop_tag_valid);
521 return 1;
522 default:
523 cant_happen();
524 }
525 }
526 RESTORE_BINDING(loop_tag_stack, loop_tag, loop_tag_valid);
527 break;
Этот фрагмент кода представляет цикл while
. Строка 508 управляет вложенными циклами посредством стека сохраненных переменных jmp_buf
. Строки 511–524 выполняют цикл while
(используя цикл С while
!). Строка 511 проверяет условие цикла. Если оно истинно, строка 513 выполняет switch
на возвращаемое значение setjmp()
. Если оно равно 0 (строки 514–516), строка 515 выполняет тело оператора. Однако, когда setjmp()
возвращает TAG_BREAK
или TAG_CONTINUE
, оператор switch
обрабатывает их соответствующим образом (строки 517–518 и 519–521 соответственно).
Оператор break
на уровне awk
передает TAG_BREAK
функции longjmp()
, a continue
уровня awk
передает TAG_CONTINUE
. Снова из eval.c
с некоторыми пропущенными не относящимися к делу подробностями:
657 case Node_K_break:
658 INCREMENT(tree->exec_count);
/* ... */
675 longjmp(loop_tag, TAG_BREAK);
676 break;
677
678 case Node_K_continue:
679 INCREMENT(tree->exec_count);
/* ... */
696 longjmp(loop_tag, TAG_CONTINUE);
670 break;
Вы можете думать о setjmp()
как об установке метки, а о longjmp()
как выполнении goto
с дополнительным преимуществом возможности сказать, откуда «пришел» код (по возвращаемому значению).
sigsetjmp()
и siglongjmp()
По историческим причинам, которые, скорее всего, утомили бы вас до слез, стандарт С 1999 г. ничего не говорит о влиянии setjmp()
и longjmp()
на состояние сигналов процесса, а POSIX явно констатирует, что их влияние на маску сигналов процесса (см. раздел 10.6 «Сигналы POSIX») не определено.
Другими словами, если программа изменяет свою маску сигналов процесса между первым вызовом setjmp()
и вызовом longjmp()
, каково состояние маски сигналов процесса после longjmp()
? Та ли эта маска, когда была впервые вызвана setjmp()
? Или это текущая маска? POSIX явно утверждает, что «нет способа это узнать».
Чтобы сделать обработку маски сигналов процесса явной, POSIX ввел две дополнительные функции и один typedef
:
#include
int sigsetjmp(sigjmp_buf env, int savesigs); /* Обратите внимание:
sigjmp_buf, не jmp_buf! */
void siglongjmp(sigjmp_buf env, int val);
Главным отличием является аргумент savesigs
функции sigsetjmp()
. Если он не равен нулю, текущий набор заблокированных сигналов сохраняется в env
вместе с остальным окружением, которое сохраняется функцией setjmp()
. siglongjmp()
с env
, в которой savesigs
содержала true, восстанавливает сохраненную маску сигналов процесса
ЗАМЕЧАНИЕ. POSIX также ясен в том, что если
savesigs
равен нулю (false), сохраняется ли маска сигналов процесса или восстанавливается, не определено, как в случае сsetjmp()
/longjmp()
. Это, в свою очередь, предполагает, что если собираетесь использовать 'sigsetjmp(env, 0)
', вы также можете не беспокоиться: все дело в том, чтобы иметь контроль над сохранением и восстановлением маски сигналов процесса!
Есть несколько технических предостережений, о которых нужно знать.
Во-первых, поскольку сохранение и восстановление среды может быть беспорядочной машинно-зависимой задачей, setjmp()
и longjmp()
могут быть макросами
Во-вторых, стандарт С ограничивает использование setjmp()
следующими ситуациями.
• В качестве единственного контролирующего выражения в операторе цикла или условном операторе (if
, switch
).
• В качестве одного операнда выражения сравнения (==
, <
и т.д.), с целой константой в качестве другого операнда. Выражение сравнения может быть единственный контролирующим выражением цикла или условного оператора.
• В качестве операнда унарного оператора '!
', причем результирующее выражение является единственным контролирующим выражением цикла или условного оператора.
• В качестве всего выражения оператора-выражения, возможно, приведенного к типу void
. Например:
(void)setjmp(buf);
В-третьих, если вы хотите изменить локальную переменную в функции, которая вызывает setjmp()
, после вызова и хотите, чтобы эта переменная сохранила свое последнее присвоенное после longjmp()
значение, нужно объявить эту переменную как volatile
. В противном случае все локальные переменные, не являющиеся volatile
и изменившиеся после того, как была первоначально вызвана setjmp()
, имеют неопределенные значения. (Обратите внимание, что сама переменная jmp_buf
не должна объявляться как volatile
.) Например:
1 /* ch12-setjmp.с – демонстрирует setjmp()/longjmp() и volatile. */
2
3 #include
4 #include
5
6 jmp_buf env;
7
8 /* comeback – выполнение longjmp */
9
10 void comeback(void)
11 {
12 longjmp(env, 1);
13 printf("This line is never printedn");
14 }
15
16 /* main – вызов setjmp, действия с переменными, вывод значений */
17
18 int main(void)
19 {
20 int i = 5;
21 volatile int j = 6;
22
23 if (setjmp(env) == 0) { /* первый раз */
24 i++;
25 j++;
26 printf("first time: i = %d, j = %dn", i, j);
27 comeback));
28 } else /* второй раз */
29 printf("second time: i = %d, j = %dn", i, j);
30
31 return 0;
32 }
В этом примере сохранение своего значения ко второму вызову printf()
гарантируется лишь
j (строка 21). Значение (строка 20) в соответствии со стандартом С 1999 г. не определено. Это может быть 6, может быть 5, а может даже какое-нибудь другое значение!
В-четвертых, как описано в разделе 12.5.2 «Обработка масок сигналов: sigsetjmp()
и siglongjmp()
», стандарт С 1999 г. не делает никаких утверждений о влиянии, если оно есть, setjmp()
и longjmp()
на состояние сигналов программы. Если это важно, вам придется вместо них использовать sigsetjmp()
и siglongjmp()
.
В-пятых, эти процедуры содержат поразительные возможности для утечек памяти! Рассмотрим программу, в которой main()
вызывает setjmp()
, а затем вызывает несколько вложенных функций, каждая из которых выделяет с помощью malloc()
динамическую память. Если наиболее глубоко вложенная функция делает longjmp()
обратно в main()
, указатели на динамическую память теряются. Взгляните на ch12-memleak.c
:
1 /* ch12-memleak.с – демонстрирует утечки памяти с помощью setjmp()/longjmp(). */
2
3 #include
4 #include
5 #include
6 #include
7
8 jmp_buf env;
9
10 void f1(void), f2(void);
11
12 /* main – утечка памяти с помощью setjmp() и longjmp() */
13
14 int main(void)
15 {
16 char *start_break;
17 char *current_break;
18 ptrdiff_t diff;
19
20 start_break = sbrk((ptrdiff_t)0);
21
22 if (setjmp(env) == 0) /* первый раз */
23 printf("setjmp calledn");
24
25 current_break = sbrk((ptrdiff_t) 0);
26
27 diff = current_break – start_break;
28 printf("memsize = %ldn", (long)diff);
29
30 f1();
31
32 return 0;
33 }
34
35 /* f1 – выделяет память, осуществляет вложенный вызов */
36
37 void f1(void)
38 {
39 char *p = malloc(1024);
40
41 f2();
42 }
43
44 /* f2 – выделяет память, выполняет longjmp */
45
46 void f2(void)
47 {
48 char *p = malloc(1024);
49
50 longjmp(env, 1);
51 }
Эта программа устанавливает бесконечный цикл, используя setjmp()
и longjmp()
. Строка 20 использует для нахождения текущего начала кучи sbrk()
(см. раздел 3.2.3 «Системные вызовы: brk()
и sbrk()
»), а затем строка 22 вызывает setjmp()
. Строка 25 получает текущее начало кучи; это место каждый раз изменяется, поскольку longjmp()
повторно входит в код. Строки 27–28 вычисляют, сколько было выделено памяти, и выводят это количество. Вот что происходит при запуске:
$ ch12-memleak /* Запуск программы */
setjmp called
memsize = 0
memsize = 6372
memsize = 6372
memsize = 6372
memsize = 10468
memsize = 10468
memsize = 14564
memsize = 14564
memsize = 18660
memsize = 18660
...
Память утекает из программы, как через решето. Она работает до тех пор, пока не будет прервана от клавиатуры или пока не закончится память (в этом случае образуется основательный дамп ядра).
Каждая из функций f1()
и f2()
выделяют память, a f2()
выполняет longjmp()
обратно в main()
(строка 51). Когда это происходит, локальные указатели (строки 39 и 48) на выделенную память пропали! Такие утечки памяти может оказаться трудно отследить, поскольку часто выделяются небольшие размеры памяти, и как таковые, они могут оставаться незамеченными в течение ряда лет[128]128
Такая утечка была у нас в gawk
К счастью, она исправлена – Примеч. автора.
[Закрыть].
Этот код явно патологический, но он предназначен для иллюстрации нашей мысли: setjmp()
и longjmp()
могут вести к трудно обнаруживаемым утечкам памяти. Предположим, что f1()
правильно вызвал free()
. Было бы далеко неочевидно, что память никогда не будет освобождена. В более крупной и более реалистичной программе, в которой longjmp()
мог быть вызван лишь посредством if
, найти такую утечку становится даже еще труднее.
Таким образом, при наличии setjmp()
и longjmp()
динамическая память должна управляться посредством глобальных переменных, а у вас должен быть код, который обнаруживает вход через longjmp()
(посредством проверки возвращаемого значения setjmp()
). Такой код должен затем освободить динамически выделенную память, которая больше не нужна.
В-шестых, longjmp()
и siglongjmp()
не следует использовать из функций, зарегистрированных посредством atexit()
(см. раздел 9.1.5.3 «Функции завершения»).
В-седьмых, setjmp()
и longjmp()
могут оказаться дорогими операциями на машинах с множеством регистров.
При наличии всех этих проблем вы должны строго рассмотреть дизайн своей программы. Если вам не нужно использовать setjmp()
и longjmp()
, то, может, стоит обойтись без их использования. Однако, если их использование является лучшим способом структурировать свою программу, продолжайте и используйте их, но делайте это осмотрительно.
Многим приложениям нужны последовательности случайных чисел. Например, игровые программы, имитирующие бросание костей, раздачу карт или вращение барабанов игровой машины, нуждаются в возможности случайного выбора одного из возможных значений. (Подумайте о программе fortune
, содержащей большую коллекцию афоризмов; каждый раз при запуске она «случайно» выдает новое высказывание.) Многие криптографические алгоритмы также требуют наличия случайных чисел «высокого качества». В данном разделе описываются различные способы получения последовательностей случайных чисел.
ЗАМЕЧАНИЕ. Природа случайности, генерация случайных чисел и их «качество» являются обширными темами, выходящими за рамки данной книги. Мы предоставляем введение в доступные функции API, но это все, что мы можем сделать Другие источники с более подробной информацией см в разделе 12.9 «Рекомендуемая литература»
Компьютеры по своему строению являются детерминистическими. Одно и то же вычисление с одними и теми же входными данными всегда должно давать одни и те же результаты. Соответственно, они не годятся для генерации истинно случайных чисел, то есть последовательностей чисел, в которых каждое число в последовательности полностью независимо от числа (или чисел), идущих перед ним. Вместо этого разновидности чисел, обычно используемых на программном уровне, называются псевдослучайными числами. То есть в любой данной последовательности номера выглядят независимыми друг от друга, но сама последовательность в целом повторяющаяся. (Эта повторяемость может быть ценным качеством; она обеспечивает детерминизм для программы в целом.)
Многие методы предоставления последовательностей псевдослучайных чисел работают посредством осуществления каждый раз одного и того же вычисления с начальным значением (seed). Сохраненное начальное значение затем обновляется для использования в следующий раз. API предоставляет способ указания нового начального значения. Каждое начальное значение дает одну и ту же последовательность псевдослучайных чисел, хотя различные начальные числа дают (должны давать) различные последовательности.
rand()
и srand()
Стандартный С определяет две связанные функции для псевдослучайных чисел.
#include
int rand(void);
void srand(unsigned int seed);
rand()
каждый раз после вызова возвращает псевдослучайное число в диапазоне от 0 до RAND_MAX
(включительно, насколько мы можем судить по стандарту C99). Константа RAND_MAX
должна быть по крайней мере 32 767; она может быть больше.
srand()
дает генератору случайных чисел в качестве начального значения seed
. Если srand()
никогда не вызывался приложением, rand()
ведет себя так, как если бы seed был равен 1.
Следующая программа, ch12-rand.c
, использует rand()
для вывода граней игральных костей.
1 /* ch12-rand.c – генерирует игральные кости, используя rand(). */
2
3 #include
4 #include
5
6 char *die_faces[] = { /* Управляет ASCII графика! */
7 " ",
8 " * ", /* 1 */
9 " ",
10
11 " ",
12 " * * ", /* 2 */
13 " ",
14
15 " ",
16 " * * * ", /* 3 */
17 " ",
18
19 " * * ",
20 " ", /* 4 */
21 " * * ",
22
23 " * * ",
24 " * ", /* 5 */
25 " * * ",
26
27 " * * * ",
28 " ", /* 6 */
29 " * * * ",
30 };
31
32 /* main – выводит N различных граней костей */
33
34 int main(int argc, char **argv)
35 {
36 int nfaces;
37 int i, j, k;
38
39 if (argc !=2) {
40 fprintf(stderr, "usage: %s number-die-facesn", argv[0]);
41 exit(1);
42 }
43
44 nfaces = atoi(argv[1]);
45
46 if (nfaces <= 0) {
47 fprintf(stderr, "usage: %s number-die-facesn", argv[0]);
48 fprintf(stderr, "tUse a positive number!n");
49 exit(1);
50 }
51
52 for (i = 1; i <= nfaces; i++) {
53 j = rand() % 6; /* force to range 0 <= j <= 5 */
54 printf("+–+n" );
55 for (k = 0; k < 3; k++)
56 printf("|%s|n", die_faces[(j * 3) + k]);
57 printf ("+–+nn");
58 }
59
60 return 0;
61 }
Эта программа использует простую ASCII-графику для распечатывания подобия грани игральной кости. Вы вызываете ее с числом граней для вывода. Это вычисляется в строке 44 с помощью atoi()
. (В общем, atoi()
следует избегать в коде изделия, поскольку она не осуществляет проверку на ошибки или переполнение, также как не проверяет вводимые данные.)
Ключевой является строка 53, которая преобразует возвращаемое значение rand()
в число от нуля до пяти, используя оператор остатка, %
. Значение 'j * 3
' действует в качестве начального индекса массива die_faces
для трех строк, составляющих каждую грань кости. Строки 55 и 56 выводят саму грань. При запуске появляется вывод наподобие этого:
$ ch12-rand 2 /* Вывести две кости */
+–+
| |
| * * |
| |
+–+
+–+
| * * |
| * |
| * * |
+–+
Интерфейс rand()
восходит еще к V7 и PDP-11. В частности, на многих системах результатом является лишь 16-разрядное число, что значительно ограничивает диапазон чисел, которые могут быть возвращены. Более того, используемый им алгоритм по современным стандартам считается «слабым». (Версия rand()
GLIBC не имеет этих проблем, но переносимый код должен быть написан со знанием того, что rand()
не является лучшим API для использования.)
ch12-rand.c
использует для получения значения в определенном интервале простую методику: оператор %
. Эта методика использует младшие биты возвращенного значения (как при десятичном делении, когда остаток отделения на 10 или 100 использует одну или две младшие десятичные цифры). Оказывается, исторический генератор rand()
производил лучшие случайные значения в средних и старших битах по сравнению с младшими битами. Поэтому, если вы должны использовать rand()
, постарайтесь избежать младших битов. Справочная страница GNU/Linux rand(3) цитирует «Числовые рецепты на С»[129]129
Numerical Recipes in С. The Art of Scientific Computing,, 2nd edition, by William H. Press, Brian P. Plannery, Saul A. Teukolsky, and William T. Vetterling. Cambridge University Press, USA, 1993, ISBN 0-521-43108-5 – Примеч. автора.
[Закрыть], которая рекомендует эту методику:
j = 1+ (int)(10.0*rand()/(RAND_MAX+1.0)); /* для числа от 1 до 10 */