netlib.narod.ru | < Назад | Оглавление | Далее > |
Одна из наиболее важных структур данных, относящихся к сигналам, sigset_t, обслуживается набором простых функций, определенных в файле include/linux/signal.h, начало которого находится в строке 17123. Те же функции для платформы х86 реализованы более оптимально, на ассемблере; их реализация начинается со строки 12204. (Единственная платформа, которая отличается специфической реализацией рассматриваемого кода, — это m68k.) Поскольку для понимания важны как платформенно-независимая, так и версия для х86, мы рассмотрим обе реализации.
Код упомянутых функций находится в файле include/linux/signal.h, который начинается в строке 17123. Эти функции, также называемые «bitops» (bit-level operations, т.е. поразрядные операции), описываются ниже.
17145: sigaddset добавляет один сигнал в набор, т.е. устанавливает один разряд набора.
17147: Преобразует нумерацию сигналов с основания 1 в основание 0, чтобы достигнуть соответствия нумерации разрядов в поразрядных операциях.
17149: Если сигналы помещаются в одно значение типа unsigned long, соответствующий разряд устанавливается в нем.
17151: В противном случае sigaddset должна предпринять дополнительные действия, определив соответствующий элемент в массиве и установив корректный разряд в нем.
Строка 17148, подобно и другому коду в этом файле, на первый взгляд вызывает легкое замешательство. Сущностью всего кода ядра является быстродействие. Таким образом, можно было бы рассчитывать, что вместо решения на этапе выполнения, подобного:
if (_NSIG_WORDS == 1) set->sig[0] |= 1UL << sig; else set->sig[sig / _NSIG_BPW] |= 1UL << (sig % _NSIG_BPW);
должно присутствовать решение на этапе компиляции:
#if (_NSIG_WORDS == 1) set->sig[0] |= 1UL << sig; #else set->sig[sig / _NSIG_BPW] |= 1UL << (sig % _NSIG_BPW); #endif
Неужели последний код не будет работать быстрее? В конце концов, ведь все условия в операторах if поддаются расчету на этапе компиляции, поэтому препроцессор может убрать все, что связано с проверкой на этапе выполнения.
Туман рассеивается, стоит только подумать о том, что те же действия делает также и оптимизатор. Оптимизатор gcc интеллектуален настолько, что способен выяснить, что оператор if может работать только в одной своей части и удалить код, который никогда не будет выполняться. Результирующий объектный код оказывается одинаковым как для версии «этапа выполнения», так и для версии «этапа компиляции».
Но почему бы не счесть второй вариант кода, основанный на использовании препроцессора, в любом случае более удачным, вдобавок предположив наличие не очень хорошего оптимизатора? Нет необходимости. С одной стороны, такой код более труден в понимании. Разница в читабельности становится более ощутимой, когда речь идет о более сложном коде. Возьмите, например, фрагмент с оператором switch в sigemptyset, который начинается в строке 17264. Сейчас упомянутый фрагмент кода выглядит так:
switch (_NSIG_WORDS) { default: memset(set, 0, sizeof(sigset_t)); break; case 2: set->sig[1] = 0; case 1: set->sig[0] = 0; break; }
(Отметьте преднамеренную перестановку порядка случаев 2 и 1.) Если код переписать так, чтобы задействовать препроцессор, он может выглядеть так:
#if ((_NSIG_WORDS != 2) && \ (_NSIG_WORDS != 1)) memset(set, 0, sizeof(sigset_t)); #else /* (_NSIG_WORDS is 2 or 1. */ #if (_NSIG_WORDS == 2) set->sig[1] = 0; #endif set->sig[0] = 0; #endif /* _NSIG_WORDS test. */
Оптимизатор gcc сгенерирует один и тот же объектный код для обоих случаев. Какая из версий кода более удобочитаема?
Кроме того, если оптимизатор не настоль хорош (это один из относительно простых случаев оптимизации), то компилятор, возможно, в любом случае не сможет сгенерировать качественный код. Поэтому все жертвы, которые мы ему принесем, окажутся напрасными. Пожалуй, в такой ситуации лучше все-таки писать более понятный код, который будет проще и сопровождать. (Кстати, это еще один общепринятый компромисс.) В конечном итоге, как было показано ранее и вновь можно заметить сейчас, компиляция ядра не под gcc большого воодушевления не вызывает.
17154: Этот код весьма похож на код sigaddset, с той лишь разницей, что здесь разряд удаляется из набора, т.е. сбрасывается.
17163: Опять-таки, код похож на код sigaddset, но здесь выполняется проверка, установлен ли разряд. Заметьте, что строка 17167 работала бы так же хорошо, имея вид:
return set->sig[0] & (1UL << sig);
Большинство сказанного применимо и к строке 17169. Это нельзя расценивать как усовершенствование, однако можно воспринимать как существенно более согласованый вариант по сравнению с текущей реализацией этих функций.
После такого изменения поведение функций приобрело бы слегка другой оттенок: сейчас они возвращают 0 или 1, а в нашем случае они возвращали бы другое ненулевое значение, если разряд установлен. Однако подобное изменение не разрушило бы существующий код, поскольку вызывающие функции проверяют, равно возвращаемое значение 0 или нет, а равно ли оно конкретно 1 — их особо не заботит.
17172: Эта функция возвращает первый установленный в слове разряд. Функция ffz (не рассматриваемая в книге) возвращает позицию первого равного 0 разряда, содержащегося в передаваемом ей аргументе. Позиция первого равного 0 разряда в поразрядно инвертированном слове (что, собственно, и делает функция) — суть то же самое, что и позиция первого разряда, равного 1, в оригинальном слове. Подсчет позиции ведется с 0 и в направлении от младших разрядов к старшим.
17177: В конце всего полезный макрос sigmask преобразует номер сигнала в поразрядную маску с соответствующим набором разрядов.
Несмотря на простоту и эффективность кода на С, имеется возможность уточнить платформенно-независимую версию рассмотренных функций для семейства процессоров х86, воспользовавшись удобными и мощными поразрядными операциями для х86. В большинстве случаев функции сокращаются до одной машинной команды, потому обсуждение будет коротким.
Для платформы х86 (а также и m68k) компилятор не использует платворменно-независимые версии функций. В строке 17126 включается файл asm/signal.h, который в случае х86, благодаря символической ссылке, устанавливаемой makefile, разрешается файлом include/asm-i386/signal.h. Строка 12202 определяет препроцессорный символ __HAVE__ARCH_SIG_BITOPS, который обеспечивает замену определений платформенно-независимых функций (см. строку 17140).
12204: Специфическая для платформы х86 реализация sigaddset использует команду btsl, которая устанавливает один разряд в передаваемом ей операнде.
12210: Аналогично, специфическая для платформы х86 реализация sigdelset с использованием команды btrl очищает один разряд в передаваемом ей операнде.
12233: sigismember выбирает реализацию на основе того, является ли передаваемый аргумент константным выражением, подсчитанным на этапе компиляции. Недокументированное чудо __builtin_constant_p компилятора gcc представляет собой оператор этапа компиляции (наподобие sizeof), который выясняет, может ли его аргумент быть вычислен на этапе компиляции.
Если это так, sigismember использует для обсчета значения функцию __const_sigismember (строка 12216). В противном случае применяется более общая версия __gen_sigismember (строка 12224). В этой более общей версии задействована команда btl, проверяющая один разряд в передаваемом ей операнде.
Следует отметить, что константа времени компиляции в результирующий код не попадает, а это значит, что вся проверка осуществляется во время компиляции — по сути, sigismember просто заменяется либо __const_sigismember, либо __gen_sigismember, безо всякого намека в объектном кода даже на сам факт подобной замены. Правда, неплохо?
12238: Специфическая для платформы х86 реализация sigmask идентична своей платформенно-независимой версии.
12240: В завершение, специфическая для платформы х86 реализация sigfindinword использует команду bsfl, которая отыскивает установленный разряд в передаваемом ей операнде.
В дополнение к предшествующей группе, следующая группа макросов и функций выполняет операции с sigset_ts как над набором. Как и в предшествующей группе, функции предохраняются препроцессорным символом __HAVE_ARCH_SIG_SETOPS. На данный момент не существует ни одной платформенно-зависимой версии этой группы функций, а лишь только универсальная.
17184: Все три двоичных операции, которые требуется определить (sigorsets, sigandsets и signandsets), имеют практически одинаковые реализации. Рассматриваемый макрос просто выносит за скобки код, общий для всех трех операций, т.е. передаваться должны только имя и операция. Это очень похоже на шаблонные функции C++, за исключением того, что мы делаем работу самостоятельно, а не перекладываем ее на компилятор — цена, которую приходится платить за использование языка С.
17191: Для начала следует пройтись по всей четверке значений типа unsigned long в sigset_ts, применяя заданные операции. Цикл организуется только для скорости — хорошо известная технология увеличения быстродействия за счет сокращения накладных расходов по управлению циклом. Однако, в большинстве случаев цикл никогда не выполняется. Например, для платформы х86 компилятор в состоянии выяснить, что тело цикла никогда не будет выполняться, поскольку результат целочисленного деления _NSIG_WORDS / 4 оказывается равным 0. (Вспомните, что для х86 _NSIG_WORDS определено как 2.)
17201: Оператор switch в этой строке обрабатывает все результаты, полученные из цикла. Если имеет место платформа, для которой _NSIG_WORDS равен 6, цикл for будет выполняться один раз, равно как и случай 2 оператора switch. Для платформы х86 цикл for не выполняется ни разу, поэтому выполнится только случай 2 оператора switch.
Кстати, не вижу ни одной причины, почему switch не был реализован прямолинейно, подобно родственному ему _SIG_SET_OP, который рассматривается ниже.
Существующая версия несомненно лучше использует кэш (можете убедиться сами, когда начнете пытаться переписывать ее код), однако если уж это причина, то она в равной степени применима и для _SIG_SET_OP.
17238: _SIG_SET_OP подобен _SIG_SET_BINOP, но реализует унарные, а не бинарные операции. Поэтому не стоит рассматривать реализацию очень подробно. Несложно заметить, что код используется всего один раз (для генерации signotset в строке 17257), в отличие от _SIG_SET_BINOP. Следовательно, в определенном смысле этот код не нужен, поскольку можно было реализовать код напрямую, без посредничества _SIG_SET_OP. Однако, существующий подход позволяет упростить добавление кода других унарных операций, если в будущем возникнет подобная необходимость.
17262: sigemptyset очищает поддерживаемый набор, т.е. сбрасывает все разряды. (Следующая функция sigfillset идентична данной, с той лишь разницей, что она устанавливает все разряды. Подробно ее реализацию рассматривать не будем.)
17265: В общем случае производится обнуление всех разрядов набора при помощи функции memset.
17268: Для небольших значений _NSIG_WORDS быстрее просто напрямую установить один или два элемента sigset_t, что, собственно, здесь и делается.
17292: Эта и несколько следующих функций содержат по одной строке кода, которые упрощают и ускоряют чтение и установку младших 32-х (или стольких, сколько определяет размер слова) сигнала. sigaddsetmask устанавливает разряды в соответствие с mask.
17310: Устанавливает младшие 32 (или сколько-то) разрядов из передаваемого mask и сбрасывает все другие разряды. Следующая функция, siginitsetinv (строка 17323), делает обратное: устанавливает младшие 32 (или сколько-то) разрядов из передаваемого инвертированного mask и устанавливает все прочие разряды.
netlib.narod.ru | < Назад | Оглавление | Далее > |