Ядро Linux в комментариях

       

Printk


Printk (строка ) представляет собой внутреннюю функцию поддержки журнала сообщений ядра. При генерации какого-либо сообщения, например, когда ядро обнаруживает несовместимость в своих структурах данных, функция printk вызывается для отображения соответствующей информации на системной консоли. Обращения к printk попадают под одну из следующих категорий:

  • Аварийные ситуации. Например, в функции panic (строка ) printk вызывается множество раз. Функция panic вызывается, когда ядро определяет неразрешимую внутреннюю ошибку; в такой ситуации лучшее решение— перегрузить систему. Обращения к printk уведомляют пользователя о том, что система завершает работу.
  • Отладка. Блок #ifdef, начинающийся в строке , использует printk для вывода в версии SMP информации о конфигурации каждого процессора, если код компилировался с флагом SMP_DEBUG.
  • Общая информация. Например, во время загрузки ядро должно определить быстродействие системы, чтобы обеспечить корректное время ожидания для драйверов устройств. Эти расчеты обеспечивает функция calibrate_delay (строка ), в которой вызов printk (строка ) выводит сообщение о начале расчетов, а другой вызов printk (строка ) отображает результаты. Функция calibrate_delay подробно рассматривается в .
  • Просмотрев упомянутые выше строки кода, несложно убедиться, что аргументы printk подобны аргументам printf: строка формата, за которой следует 0 или более аргументов. Строка формата может начинаться с последовательности символов в форме «<N>», где N — цифра, от 0 до 7 включительно. Цифра определяет уровень регистрации сообщения; сообщение будет выводиться только если этот уровень меньше текущего уровня, определенного для консоли (console_loglevel, строка ). Уровень для консоли можно снижать, тем самым отфильтровывая менее важные сообщения. Если в строке формата не задается ни одного уровня регистрации, сообщение будет выводиться всегда. (В настоящий момент уровень регистрации не должен обязательно присутствовать в строке формата — он отыскивается в форматированном тексте.)

    Блок конструкций #define, начинающийся в строке , присваивает имена специальным последовательностям, что упрощает использование printk. Так уж вышло, что уровни с 0 по 4 относятся к тем, которые я называю «аварийными ситуациями», уровни 5 и 6 — к «общей информации», а 7 — к «отладке».

    Обратим свой взгляд на код.


    Инициализация переменной args, которая представляет часть "..." параметров printk.

    Вызывает собственную реализацию ядра функции vsprintf (с опущенными пробелами). Это действует подобно обычному vsprintf, записывая форматированный текст в buf (строка ) и возвращая количество записанных символов (исключая завершающий строку нулевой символ). Далее будет показано, почему пропускаются первых три символа в buf.

    Заметьте, что ничего не препятствует переполнению буфера (см. комментарий в строке ). В данном случае предполагается, что 1024-символьного буфера должно оказаться достаточно. Было бы гораздо лучше, если бы ядро использовало здесь функцию vsnprintf, которая имеет дополнительный параметр, определяющий количество записываемых символов.

    Определение элемента в buf, который использовался последним, и завершение прохода по параметру "..." путем вызова va_end.

    Начинает итерацию по форматированному сообщению. Существует внутренний цикл, обеспечивающий дополнительную обработку (это можно заметить сейчас). Упомянутый цикл вызывается при каждом прогоне внешнего цикла, соответствующего началу каждой отображаемой строки. Поскольку обычно printk вызывается только для печати одной строки, цикл выполняется один раз на вызов.



    Если уровень регистрации сообщения еще не известен, printk проверяет, соответствует ли начало строки последовательности, определяющей уровень регистрации.

    Если нет, первых три символа buf будут использоваться. (Для итераций, следующих после первой, это перезаписывает часть текста сообщения; тем не менее, все правильно, поскольку перезаписываемый текст относится к предыдущей строке, которая уже отображена и больше не нужна.) Последовательность, определяющая уровень регистрации, вставляется в buf.

    К этому моменту фиксируются следующие свойства: p указывает на последовательность, определяющую уровень регистрации (за которой следует текст сообщения), а msg — на текст сообщения (обратите внимание на установку msg в строках и ). Поскольку p известен как указатель на начало последовательности, определяющей уровень регистрации (возможно, построенной самой функцией), уровень регистрации может быть получен из p и сохранен в msg_level.



    Очистка флага line_feed, т.е. пока не встретилось ни одного символа новой строки.

    Внутренний цикл, упоминаемый ранее; он выполняется вплоть до конца строки (т.е. до получения символа новой строки) либо до конца буфера.

    В дополнение к выводу сообщений на консоль, printk запоминает последние выведенные LOG_BUF_LEN символов. (LOG_BUF_LEN равно 16К — см. строку .) Если ядро вызывает printk перед открытием консоли, очевидно, что сообщение не может быть напечатано на консоли, однако оно сохраняется (во всяком случае, максимум возможного) в log_buf (строка ). Как только консоль откроется, данные из log_buf немедленно переносятся в нее (см. строку ).

    Массив log_buf реализован в виде циклического буфера, в котором переменные log_start и log_size (строки и ) хранят, соответственно, начало буфера и его длину. Поразрядное AND в данной строке обеспечивает быстрое взятие по модулю (%); корректность выполнения операции зависит от того, является ли LOG_BUF_LEN степенью двойки.

    Поддержка переменных, отслеживающих циклический журнал регистрации. Очевидно, что размер журнала (log_size) может увеличиваться, только если он не превышает LOG_BUF_LEN. В противном случае log_size не изменяет своего значения, а вставка дополнительных символов приводит к увеличению log_start.

    Отметьте, что logged_chars (строка ), т.е. общее количество символов, записанное printk с момента перезагрузки компьютера, обновляется в каждой итерации цикла, а не один раз после завершения цикла. То же самое справедливо и для log_start и log_size. Это выглядит как возможность оптимизации, однако оставим обсуждение до того момента, когда рассмотрение функции будет завершено.

    Сообщения разбиты на строки, разделенные символами новой строки. Как только встречается один из символов новой строки, текущая строка выводится, а внутренний цикл завершается раньше.

    Не принимая во внимание более раннее завершение внутреннего цикла, символы от msg до p предназначены для вывода на консоль. (Я буду ссылаться на эту последовательность символов как на строку, однако не забывайте, что строка может и не завершаться символом новой строки, поскольку последний может и не присутствовать в buf.) Строка будет выведена, если ее уровень регистрации меньше уровня, определенного для системной консоли, а также при условии, что имеются доступные консоли. (Не следует забывать, что printk может вызываться и до открытия консолей.)



    Если в анализируемом фрагменте сообщения не найдено ни одной последовательности, определяющей уровень регистрации, и msg_level не установлен во время предыдущего вызова printk, для данной строки msg_level будет равным -1. Поскольку console_loglevel всегда равен по меньшей мере 1 (если только администратор не счел необходимым изменить это), такого рода строки будут выводиться на консоль всегда.

    Эта строка будет напечатана. printk пройдется по списку драйверов открытых консолей, уведомляя каждый из них о необходимости вывода строки. (Ввиду того, что рассмотрение драйверов устройств выходит за рамки этой книги, код драйвера консоли здесь не приводится.)

    Здесь сообщение записывается без последовательности, определяющей уровень регистрации, — в качестве начала текста сообщения используется msg, а не p. Однако последовательность, определяющая уровень регистрации, записывается в буфер log_buf. Последнее разрешает выполнение кода, читающего log_buf с целью получения уровня регистрации сообщения (см. строку ) без искажений в отображении последовательностей.

    Если внутренний цикл наталкивается на символ новой строки, оставшиеся символы buf (если таковые имеются) перемещаются в начало нового сообщения, поэтому msg_level сбрасывается. В противном случае внешний цикл продолжается до тех пор, пока не будет исчерпан buf.

    Освобождение блокировки консоли, обеспеченной в строке .

    Активизация всех процессов, которые ожидают освобождения консоли для записи в нее сообщений. Следует отметить, что подобное имеет место даже в случае, когда текст на консоль еще не выводился. Это вполне нормально, поскольку ожидающим процессам необходимо прочитать log_buf, который может содержать текст, подлежащий передаче на консоль. Ожидание доступа к log_buf для процессов реализовано в строке . Использованный здесь механизм ожидания и очередизации описывается в .

    Возвращает количество символов, записанных в журнал.

    Цикл for, определенный в строке , мог работать быстрее, если бы не объем работы, выполняемый над каждым символом. Небольшое ускорение можно получить за счет лишь однократного обновления 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 внутри цикла не изменяется, поэтому никакого выигрыша от выноса этого выражения за пределы цикла не будет.)

    К сожалению, предложенное решение оказывается ненамного более быстродействующим, нежели реализованное в ядре, к тому же оно сложнее в понимании. Можно реализовать собственный подход, если того требуют принятые компромиссы. Какой бы ни был избран путь, полезное зерно всегда присутствует. Общий вывод таков: чем бы ни увенчались попытки усовершенствования кода ядра, успехом или неудачей, польза от исследования ядра неоценима.


    Содержание раздела