Система отладки неисправностей

Предыдущая  Содержание  Следующая V*D*V

Даже если вы использовали все техники мониторинга и отладки, иногда в драйвере остаются ошибки и система выдаёт ошибки при работе драйвера. Когда это происходит, важно уметь собрать столько информации, сколько возможно, чтобы решить эту проблему.

 

Отметим, что "ошибка", не означает "паника". Код Linux является достаточно прочным, чтобы корректно отреагировать на большинство ошибок: ошибки обычно приводят к уничтожению текущего процесса, в то время как система продолжает работать. Система может паниковать, и это может быть, если ошибка происходит за пределами контекста процесса или если скомпрометированы некоторые жизненно важные части системы. Но когда проблема вызвана ошибкой драйвера, это обычно приводит только к внезапной смерти процесса, которому не повезло использовать драйвер. Только неустранимые повреждения при разрушении процесса приводят к тому, что некоторый объём памяти, выделенной контексту процесса, теряется; например, могли бы быть утеряны динамические списки, выделенные драйвером через kmalloc. Однако, поскольку ядро вызывает операцию close для любого открытого устройства, когда процесс умирает, драйвер может освободить то, что выделено методом open.

 

Хотя даже Oops (ой) обычно не доводит до падения всей системы, вы можете посчитать, что необходима перезагрузка после того, как он случился. Драйвер с ошибками может оставить оборудование в непригодном для использования состоянии, оставить ресурсы ядра в неустойчивом состоянии, или, в худшем случае, в случайных местах повредить память ядра. Часто после Oops вы можете просто выгрузить ваш ошибочный драйвер и попробовать ещё раз. Однако, если вы увидите что-то, что позволяет предположить, что система в целом не очень хороша, лучшим решением, как правило, является незамедлительная перезагрузка.

 

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

Сообщения Oops

Большинство ошибок показывают себя в разыменовании указателя NULL или других некорректных значений указателя. Обычным результатом таких ошибок является сообщение Oops.

 

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

 

Oops отображает состояние процессора в момент ошибки, в том числе содержание регистров процессора и другую, казалось бы, непонятную информацию. Сообщение создаётся и отправляется с помощью printk в обработчике ошибок (arch/*/kernel/traps.c), как описано выше в разделе "printk".

 

Давайте посмотрим на одно из таких сообщений. Вот какие результаты от разыменования указателя NULL на ПК под управлением ядра версии 2.6. Наиболее относящейся к делу информацией здесь является указатель команд (EIP), адрес ошибочной инструкции.

 

Unable to handle kernel NULL pointer dereference at virtual address 00000000

 printing eip:

d083a064

Oops: 0002 [#1]

SMP

CPU: 0

EIP: 0060:[<d083a064>] Not tainted

EFLAGS: 00010246 (2.6.6)

EIP is at faulty_write+0x4/0x10 [faulty]

eax: 00000000 ebx: 00000000 ecx: 00000000 edx: 00000000

esi: cf8b2460 edi: cf8b2480 ebp: 00000005 esp: c31c5f74

ds: 007b es: 007b ss: 0068

Process bash (pid: 2086, threadinfo=c31c4000 task=cfa0a6c0)

Stack: c0150558 cf8b2460 080e9408 00000005 cf8b2480 00000000 cf8b2460 cf8b2460

       fffffff7 080e9408 c31c4000 c0150682 cf8b2460 080e9408 00000005 cf8b2480

       00000000 00000001 00000005 c0103f8f 00000001 080e9408 00000005 00000005

Call Trace:

 [<c0150558>] vfs_write+0xb8/0x130

 [<c0150682>] sys_write+0x42/0x70

 [<c0103f8f>] syscall_call+0x7/0xb

Code: 89 15 00 00 00 00 c3 90 8d 74 26 00 83 ec 0c b8 00 a6 83 d0

 

Это сообщение было создано записью в устройство, принадлежащее модулю faulty, модуль создан специально для демонстрации ошибки. Реализация метода write из faulty.c тривиальна:

 

ssize_t faulty_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)

{

    /* создать простую ошибку, используя разыменование NULL указателя */

    *(int *)0 = 0;

    return 0;

}

 

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

 

Модуль faulty имеет другое ошибочное условие в реализации read:

 

ssize_t faulty_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)

{

    int ret;

    char stack_buf[4];

 

    /* Давайте попробуем переполнить буфер */

    memset(stack_buf, 0xff, 20);

    if (count > 4)

        count = 4; /* скопировать 4 байта пользователю */

    ret = copy_to_user(buf, stack_buf, count);

    if (!ret)

        return count;

    return ret;

}

 

Этот метод копирует строку в локальную переменную; к сожалению, строка больше, чем массив назначения. Переполнение буфера в результате приводит к Oops, когда функция возвращается. Так как инструкция return приносит никуда не указывающий указатель команд, этот вид ошибки гораздо труднее отследить и вы можете получить что-то похожее на это:

 

EIP: 0010:[<00000000>]

Unable to handle kernel paging request at virtual address ffffffff

 printing eip:

ffffffff

Oops: 0000 [#5]

SMP

CPU: 0

EIP: 0060:[<ffffffff>] Not tainted

EFLAGS: 00010296 (2.6.6)

EIP is at 0xffffffff

eax: 0000000c ebx: ffffffff ecx: 00000000 edx: bfffda7c

esi: cf434f00 edi: ffffffff ebp: 00002000 esp: c27fff78

ds: 007b es: 007b ss: 0068

Process head (pid: 2331, threadinfo=c27fe000 task=c3226150)

Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7

       bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000

       00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70

Call Trace:

 [<c0150612>] sys_read+0x42/0x70

 [<c0103f8f>] syscall_call+0x7/0xb

Code: Bad EIP value.

 

В этом случае мы видим только часть стека вызовов (vfs_read и faulty_read отсутствуют) и ядро жалуется на "плохое значение EIP" (“bad EIP value”). Эта жалоба и адрес вызова (ffffffff), показанный в начале, оба намекают, что был повреждён стек ядра.

 

В общем, если вы столкнулись с Oops, первое, что нужно сделать, это посмотреть на место, где произошла проблема, которое обычно показывается отдельно от стека вызовов. В первом Oops, показанном выше, соответствующими строками являются:

 

EIP is at faulty_write+0x4/0x10 [faulty]

 

Здесь мы видим, что мы были в функции faulty_write, которая находится в модуле faulty (который указан в квадратных скобках). Шестнадцатеричные цифры показывают, что указатель команд имел смещение 4 байта в функции, которая имеет размер 10 (hex) байтов. Часто этого достаточно, чтобы понять, в чём состоит проблема.

 

Если вам требуется больше информации, стек вызовов покажет вам, как вы попали туда, где всё распалось. Стек печатается в шестнадцатеричной форме; немного поработав, из листинга стека часто можно определить значения локальных переменных и параметров функции. Опытные разработчики ядра могут извлечь пользу из определенного количества распознанных здесь образов; например, если мы посмотрим на распечатку стека из faulty_read Oops:

 

Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7

       bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000

       00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70

 

ffffffff на вершине стека является частью нашей строки, которая всё поломала. По умолчанию на архитектуре x86 стек пользовательского пространства начинается чуть ниже 0xc0000000; таким образом, повторяющиеся значения 0xbfffda70, вероятно, стековый адрес в пользовательском пространстве; это, фактически, адрес буфера, переданный системному вызову read, повторенный каждый раз при передаче вниз по цепочке вызова ядра. На платформе x86 (опять же, по умолчанию), пространство ядра начинается с 0xc0000000, так что  значения выше этого - почти наверняка адреса пространства ядра, и так далее.

 

Наконец, при просмотре распечаток Oops всегда будьте бдительны к значениям "отравленных шаблонов" о которых рассказывалось в начале этой главы. Так, например, если вы получите от ядра Oops, в котором вызывающий адрес 0xa5a5a5a5, вы почти наверняка забываете где-то проинициализировать динамическую память.

 

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

Зависания системы

Хотя большинство ошибок в коде ядра заканчивается сообщениями Oops, иногда они могут полностью завесить систему. Если система зависает, сообщение не печатается. Например, если код входит в бесконечный цикл, ядро останавливает планировщик (переключения процессов), (* На самом деле, многопроцессорные системы по-прежнему переключают процессы на других процессорах и даже однопроцессорная машина может переключать, если в ядре включено вытеснение. Однако, для наиболее распространенного случая (однопроцессорная с отключенным вытеснением), система вообще прекращает переключение.) и система не реагирует на любые действия, включая специальную комбинацию Ctrl-Alt-Del. Вы имеете два варианта действий с зависаниями системы - либо предотвратить их заранее или заниматься их поиском и устранением постфактум.

 

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

 

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

 

Если ваш драйвер действительно висит в системе и вы не знаете, где вставить вызов schedule, лучшим путём может быть добавление печати каких-нибудь сообщений и выводить их на консоль (изменяя значение console_loglevel, если это необходимо).

 

Иногда система может казаться зависшей, но это не так. Это может произойти, например, если клавиатура остаётся заблокированной каким-то странным образом. Эти ложные зависания можно обнаружить, глядя на вывод программы, который вы держите работающим только для этой цели. Часы или измеритель системой нагрузки на вашем дисплее - хороший монитор состояния; пока он продолжает обновление, планировщик работает.

 

Незаменимым инструментом для многих зависаний является "системная кнопка SysRq" (системный запрос), которая доступна на большинстве архитектур. Системный SysRq вызывается комбинацией клавиш Alt и SysRq на клавиатуре компьютера или с помощью других специальных клавиш на других платформах (смотрите для подробностей Documentation/sysrq.txt) и также доступна на последовательной консоли. Третья кнопка, нажатая вместе с этими двумя, выполняет одно из перечисленных полезных действий:

 

r   Отключает режим raw (сырой, возвращение скан-кодов) клавиатуры; полезно в ситуациях, когда порушенное приложение (такое, как X сервер) могло оставить вашу клавиатуру в странном состоянии.

 

k   Вызывает функцию "ключ безопасного внимания", “secure attention key” (SAK). SAK убивает все процессы, запущенные на текущей консоли, оставив вас с чистым терминалом.

 

s   Выполняет аварийную синхронизацию всех дисков.

 

u   Размонтирование. Пытается перемонтировать все диски в режиме "только для чтения". Эта операция обычно вызывается сразу после s, можно сэкономить массу времени на проверку файловой системой в тех случаях, когда система находится в тяжелом состоянии.

 

b   Загрузка. Сразу же перезагружает систему. Будьте уверены, что сначала засинхронизировали и перемонтировали диски.

 

p   Печатает информацию о регистрах процессора.

 

t   Печатает текущий список задач.

 

m   Печатает информацию о памяти.

 

Существуют и другие функции системного SysRq; смотрите sysrq.txt в каталоге Documentation исходных текстов ядра для полного списка. Обратите внимание, что системный SysRq должен быть явно включен в конфигурации ядра и что большинство дистрибутивов его не разрешает по очевидным соображениям безопасности. Однако, в системе, используемой для разработки драйверов, разрешение системного SysRq стоит того, чтобы собрать себе новое ядро. Magic SysRq может быть отключен во время работы такой командой:

 

echo 0 > /proc/sys/kernel/sysrq

 

Вы должны предусмотреть его отключение для предотвращения случайного или намеренного ущерба, если непривилегированные пользователи могут получить доступ к вашей системной клавиатуры. Некоторые предыдущие версии ядра имели sysrq отключенным по умолчанию, так что для его разрешения вам необходимо во время выполнения записать 1 в тот же  самый файл в /proc/sys.

 

Операции sysrq чрезвычайно полезны, так что они делаются доступными для системных администраторов, которые не могут добраться до консоли. Файл /proc/sysrq-trigger является точкой "только для записи", где можно задать определённые действия sysrq, записав соответствующий символ команды; потом вы сможете собрать любые выходные данные из логов ядра. Эта точка входа для sysrq работает всегда, даже если на консоли sysrq отключен.

 

Если вы столкнулись с "живым зависанием", в котором ваш драйвер застрял в цикле, но система в целом ещё функционирует, полезно знать несколько методов. Зачастую функция p SysRq прямо указывает на виновную процедуру. В противном случае, вы также можете использовать функции профилирования (протоколирования) ядра. Постройте ядро с включенным профилированием и запустите его с profile=2 в командной строке. Сбросьте счётчики профиля утилитой readprofile, а затем отправьте ваш драйвер в цикл. Через некоторое время используйте readprofile снова, чтобы увидеть, на что ядро тратит своё время. Другой, более сложной альтернативой является oprofile, которую вы также можете рассмотреть. Файл Documentation/basic_profiling.txt расскажет вам всё, что необходимо знать, чтобы начать работу с профайлерами.

 

Ценной предосторожностью при отладке системных зависаний является монтирование всех ваших дисков в режиме "только для чтения" (или их отключение). Если диски находятся в режиме "только для чтения" или демонтированы, нет риска повреждения файловой системы или оставить её в неустойчивом состоянии. Другая возможность заключается в использовании компьютера, который подключает все его файловые системы через NFS, сетевую файловую систему. В ядре должна быть включена возможность "NFS-Root" и во время загрузки должны быть переданы специальные параметры. В этом случае вы избежите повреждения файловой системы, даже не прибегая к SysRq, потому что согласованная файловая система находится под управлением сервера NFS, который не повреждается вашим драйвером устройства.

Предыдущая  Содержание  Следующая