netlib.narod.ru | < Назад | Оглавление | Далее > |
Printk (строка 25836) представляет собой внутреннюю функцию поддержки журнала сообщений ядра. При генерации какого-либо сообщения, например, когда ядро обнаруживает несовместимость в своих структурах данных, функция printk вызывается для отображения соответствующей информации на системной консоли. Обращения к printk попадают под одну из следующих категорий:
Просмотрев упомянутые выше строки кода, несложно убедиться, что аргументы printk подобны аргументам printf: строка формата, за которой следует 0 или более аргументов. Строка формата может начинаться с последовательности символов в форме «<N>», где N — цифра, от 0 до 7 включительно. Цифра определяет уровень регистрации сообщения; сообщение будет выводиться только если этот уровень меньше текущего уровня, определенного для консоли (console_loglevel, строка 25650). Уровень для консоли можно снижать, тем самым отфильтровывая менее важные сообщения. Если в строке формата не задается ни одного уровня регистрации, сообщение будет выводиться всегда. (В настоящий момент уровень регистрации не должен обязательно присутствовать в строке формата — он отыскивается в форматированном тексте.)
Блок конструкций #define, начинающийся в строке 14946, присваивает имена специальным последовательностям, что упрощает использование printk. Так уж вышло, что уровни с 0 по 4 относятся к тем, которые я называю «аварийными ситуациями», уровни 5 и 6 — к «общей информации», а 7 — к «отладке».
Обратим свой взгляд на код.
25836: Аргумент fmt — это строка формата наподобие используемой в функции printf. Если имеются какие-либо затруднения в работе с частью "...", стоит обратиться к любой книге по программированию на языке С. Кроме того, неплохую справку можно получить в руководстве по GNU/Linux на странице stdarg (просто наберите man stdarg).
Выражаясь кратко, часть "..." уведомляет компилятор, что за fmt может следовать любое количество параметров любых типов. Поскольку на этапе компиляции эти параметры не имеют ни имен ни типов, необходимо манипулировать тройкой макросов va_start, va_arg и va_end и типом va_list.
25842: msg_level записывает уровень регистрации текущего сообщения. Может вызвать удивление, что msg_level имеет тип static — зачем при следующем вызове printk помнить текущий уровень регистрации? Ответ заключается в следующем: текущее сообщение завершается только после вывода символа новой строки (\n) либо после получения новой последовательности, определяющей уровень регистрации. Такой подход позволяет выводить длинное сообщение по частям, при этом последний вызов printk должен включать в себя символ новой строки.
25845: В версии SMP ядро может пытаться выводить на консоль буквально одновременно из различных центральных процессоров (ЦП). (Хоть это и неочевидно, но нечто подобное может возникать и в однопроцессорной версии. Оставим это до обсуждения прерываний.) Если не предпринять меры по координации, в результате получится жуткая путаница — хаотично перемешанные куски различных сообщений.
Для обеспечения защиты доступа к консоли в ядре используется механизм взаимной блокировки (см. главу 10).
Не стоит удивляться, что flags передается в spin_lock_irqsave без предварительной инициализации: spin_lock_irqsave (различные его версии реализованы в строках 12614, 12637, 12716 и 12837) — это макрос, а не функция. Этот макрос записывает в flags, но не читает из него. Информация, сохраненная в flags читается spin_unlock_irqrestore в строке 25895 (см. строки 12616, 12639, 12728 и 12841).
25846: Инициализация переменной args, которая представляет часть "..." параметров printk.
25848: Вызывает собственную реализацию ядра функции vsprintf (с опущенными пробелами). Это действует подобно обычному vsprintf, записывая форматированный текст в buf (строка 25634) и возвращая количество записанных символов (исключая завершающий строку нулевой символ). Далее будет показано, почему пропускаются первых три символа в buf.
Заметьте, что ничего не препятствует переполнению буфера (см. комментарий в строке 25847). В данном случае предполагается, что 1024-символьного буфера должно оказаться достаточно. Было бы гораздо лучше, если бы ядро использовало здесь функцию vsnprintf, которая имеет дополнительный параметр, определяющий количество записываемых символов.
25849: Определение элемента в buf, который использовался последним, и завершение прохода по параметру "..." путем вызова va_end.
25851: Начинает итерацию по форматированному сообщению. Существует внутренний цикл, обеспечивающий дополнительную обработку (это можно заметить сейчас). Упомянутый цикл вызывается при каждом прогоне внешнего цикла, соответствующего началу каждой отображаемой строки. Поскольку обычно printk вызывается только для печати одной строки, цикл выполняется один раз на вызов.
25853: Если уровень регистрации сообщения еще не известен, printk проверяет, соответствует ли начало строки последовательности, определяющей уровень регистрации.
25860: Если нет, первых три символа buf будут использоваться. (Для итераций, следующих после первой, это перезаписывает часть текста сообщения; тем не менее, все правильно, поскольку перезаписываемый текст относится к предыдущей строке, которая уже отображена и больше не нужна.) Последовательность, определяющая уровень регистрации, вставляется в buf.
25866: К этому моменту фиксируются следующие свойства: p указывает на последовательность, определяющую уровень регистрации (за которой следует текст сообщения), а msg — на текст сообщения (обратите внимание на установку msg в строках 25852 и 25865). Поскольку p известен как указатель на начало последовательности, определяющей уровень регистрации (возможно, построенной самой функцией), уровень регистрации может быть получен из p и сохранен в msg_level.
25868: Очистка флага line_feed, т.е. пока не встретилось ни одного символа новой строки.
25869: Внутренний цикл, упоминаемый ранее; он выполняется вплоть до конца строки (т.е. до получения символа новой строки) либо до конца буфера.
25870: В дополнение к выводу сообщений на консоль, printk запоминает последние выведенные LOG_BUF_LEN символов. (LOG_BUF_LEN равно 16К — см. строку 25632.) Если ядро вызывает printk перед открытием консоли, очевидно, что сообщение не может быть напечатано на консоли, однако оно сохраняется (во всяком случае, максимум возможного) в log_buf (строка 25656). Как только консоль откроется, данные из log_buf немедленно переносятся в нее (см. строку 25988).
Массив log_buf реализован в виде циклического буфера, в котором переменные log_start и log_size (строки 25657 и 25646) хранят, соответственно, начало буфера и его длину. Поразрядное AND в данной строке обеспечивает быстрое взятие по модулю (%); корректность выполнения операции зависит от того, является ли LOG_BUF_LEN степенью двойки.
25872: Поддержка переменных, отслеживающих циклический журнал регистрации. Очевидно, что размер журнала (log_size) может увеличиваться, только если он не превышает LOG_BUF_LEN. В противном случае log_size не изменяет своего значения, а вставка дополнительных символов приводит к увеличению log_start.
25878: Отметьте, что logged_chars (строка 25658), т.е. общее количество символов, записанное printk с момента перезагрузки компьютера, обновляется в каждой итерации цикла, а не один раз после завершения цикла. То же самое справедливо и для log_start и log_size. Это выглядит как возможность оптимизации, однако оставим обсуждение до того момента, когда рассмотрение функции будет завершено.
25879: Сообщения разбиты на строки, разделенные символами новой строки. Как только встречается один из символов новой строки, текущая строка выводится, а внутренний цикл завершается раньше.
25884: Не принимая во внимание более раннее завершение внутреннего цикла, символы от msg до p предназначены для вывода на консоль. (Я буду ссылаться на эту последовательность символов как на строку, однако не забывайте, что строка может и не завершаться символом новой строки, поскольку последний может и не присутствовать в buf.) Строка будет выведена, если ее уровень регистрации меньше уровня, определенного для системной консоли, а также при условии, что имеются доступные консоли. (Не следует забывать, что printk может вызываться и до открытия консолей.)
Если в анализируемом фрагменте сообщения не найдено ни одной последовательности, определяющей уровень регистрации, и msg_level не установлен во время предыдущего вызова printk, для данной строки msg_level будет равным -1. Поскольку console_loglevel всегда равен по меньшей мере 1 (если только администратор не счел необходимым изменить это), такого рода строки будут выводиться на консоль всегда.
25886: Эта строка будет напечатана. printk пройдется по списку драйверов открытых консолей, уведомляя каждый из них о необходимости вывода строки. (Ввиду того, что рассмотрение драйверов устройств выходит за рамки этой книги, код драйвера консоли здесь не приводится.)
25888: Здесь сообщение записывается без последовательности, определяющей уровень регистрации, — в качестве начала текста сообщения используется msg, а не p. Однако последовательность, определяющая уровень регистрации, записывается в буфер log_buf. Последнее разрешает выполнение кода, читающего log_buf с целью получения уровня регистрации сообщения (см. строку 25998) без искажений в отображении последовательностей.
25892: Если внутренний цикл наталкивается на символ новой строки, оставшиеся символы buf (если таковые имеются) перемещаются в начало нового сообщения, поэтому msg_level сбрасывается. В противном случае внешний цикл продолжается до тех пор, пока не будет исчерпан buf.
25895: Освобождение блокировки консоли, обеспеченной в строке 25845.
25896: Активизация всех процессов, которые ожидают освобождения консоли для записи в нее сообщений. Следует отметить, что подобное имеет место даже в случае, когда текст на консоль еще не выводился. Это вполне нормально, поскольку ожидающим процессам необходимо прочитать log_buf, который может содержать текст, подлежащий передаче на консоль. Ожидание доступа к log_buf для процессов реализовано в строке 25748. Использованный здесь механизм ожидания и очередизации описывается в следующем разделе.
25897: Возвращает количество символов, записанных в журнал.
Цикл for, определенный в строке 25869, мог работать быстрее, если бы не объем работы, выполняемый над каждым символом. Небольшое ускорение можно получить за счет лишь однократного обновления logged_chars после завершения цикла. Однако мы должны попытаться достигнуть большего. Размер сообщения известен заранее, поэтому log_size и log_start не должны увеличиваться до конца цикла. Вот как простенько можно ускорить цикл:
do { static int wrapped = 0; const int x = wrapped ? log_start : log_size; const int lim = LOG_BUF_LEN - x; int n = buf_end - p; if (n >= lim) n = lim; memcpy (log_buf+x, p, n); p += n; if (log_size < LOG_BUF_LEN) log_size += n; else { wrapped = 1; log_start += n; log_start &= LOG_BUF_LEN - 1; } } while (p < buf_end);
Не следует забывать, что цикл, как правило, выполняется один раз; большее количество выполнений цикла имеет место тогда, когда запись по достижении конца log_buf переходит на начало. Следовательно, log_size и log_buf обновляются только один раз (или два, если случается переход на начало).
Ускорение достигнуто, но мы не будем поступать таким образом по двум причинам. Во-первых, ядро имеет собственную версию memcpy, и мы должны быть четко уверены, что вызов memcpy никогда не приведет к возврату в printk. (Некоторые версии ядра определяют собственные более быстрые версии memcpy, поэтому они должны быть согласны с последним утверждением.) Если memcpy обращается к printk для выдачи уведомления об ошибке, есть риск попасть в бесконечный цикл.
Однако не это самая большая проблема. Самое печальное, что версия цикла, реализованная в ядре, отслеживает также символы новой строки, так что применение memcpy для копирования всего сообщения в log_buf оказывается некорректным — при появлении символа новой строки, он попросту «перепрыгивается».
Можно попытаться убить двух зайцев одним выстрелом. Приведенная ниже замена оказывается несколько медленнее рассмотренной ранее, однако она не противоречит семантике существующей версии ядра:
/* В разделе объявлений */ int n; char * start; static char * log = log_buf; /* . . . */ for (start = p; p < buf_end; p++) { *log++ = *p; if (log >= (log_buf + LOG_BUF_LEN)) log = log_buf; /* Wrap. */ if (*p == '\n') { line_feed = 1; break; } } /* p - start представляет количество копируемых символов */ n = p - start; logged_chars += n; /* * Задание для читателя: * Воспользуйтесь n для обновления log_size и log_start * (Это не так просто, как может показаться на первый взгляд) */
(Следует отметить, что оптимизатор gcc достаточно интеллектуален, чтобы определить, что выражение log_buf + LOG_BUF_LEN внутри цикла не изменяется, поэтому никакого выигрыша от выноса этого выражения за пределы цикла не будет.)
К сожалению, предложенное решение оказывается ненамного более быстродействующим, нежели реализованное в ядре, к тому же оно сложнее в понимании. Можно реализовать собственный подход, если того требуют принятые компромиссы. Какой бы ни был избран путь, полезное зерно всегда присутствует. Общий вывод таков: чем бы ни увенчались попытки усовершенствования кода ядра, успехом или неудачей, польза от исследования ядра неоценима.
netlib.narod.ru | < Назад | Оглавление | Далее > |