Инициализация и выключение

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

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

 

static int __init initialization_function(void)

{

    /* Здесь размещается код инициализации */

}

module_init(initialization_function);

 

Функции инициализации должны быть объявлены статическими, так как они не предназначены быть видимыми за пределами определённого файла; тем не менее, нет жёсткого правила по этому поводу, так как ни одна функция не экспортируется в остальную часть ядра, если это не указано явно. Признак __init в определении может показаться немного странным; это подсказка ядру, что данная функция используется только во время инициализации. Загрузчик модуля отбрасывает функцию инициализации после загрузки модуля, делая память доступной для другого использования. Существует аналогичный атрибут (__initdata) для данных, используемых только в процессе инициализации. Использование __init и __initdata является необязательным, но стоит побеспокоиться об этом. Просто убедитесь, что не используете их для функций (или структуры данных), которые будут использоваться после завершения инициализации. Вы можете встретить в исходном коде ядра __devinit и __devinitdata, они транслируются к __init и __initdata только тогда, когда ядро не было сконфигурировано для поддержки устройств, подключаемых без выключения системы. Мы будем рассматривать поддержку "горячей" коммутации в Главе 14.

 

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

 

Число объектов, которые могут быть зарегистрированы, превышает список типов устройств, упомянутых в Главе 1. Среди прочего, они включают последовательные порты, разнообразные устройства, sysfs записи, /proc файлы, исполняемые домены и дисциплину линий связи. Многие из этих регистрируемых объектов поддерживают функции, не связанные непосредственно с аппаратурой, а остаются в виде "программных абстракций". Эти объекты могут быть зарегистрированы, потому что они, тем не менее, интегрированы в функциональность драйвера (например, /proc файлы и дисциплина линий связи).

 

Есть и другие средства, которые могут быть зарегистрированы в качестве дополнений для определённого драйвера, но их использование является настолько специфичным, что не стоит говорить о них; они используют технику стека, как описано в разделе "Символьная таблица ядра". Если вы хотите исследовать поглубже, вы можете поискать EXPORT_SYMBOL в исходниках ядра и найти точки входа, предлагаемые разными драйверами. Большинство функций регистрации имеют префикс register_, так что ещё одним возможным способом является поиск в исходниках ядра по register_.

Функция очистки

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

 

static void __exit cleanup_function(void)

{

    /* Здесь размещается код очистки */

}

 

module_exit(cleanup_function);

 

Функция очистки не имеет возвращаемого значения и объявляется как void. Признак __exit указывает, что этот код будет только выгружать модуль (чтобы компилятор поместил его в специальный раздел ELF). Если ваш модуль собирается прямо в ядре или если ваше ядро сконфигурировано не разрешать выгрузку модулей, функции, отмеченные __exit, просто отбрасываются. По этой причине функции, отмеченные __exit, могут быть вызваны только при выгрузке модуля или время завершения работы системы; любое другое использование является ошибкой. И снова, декларация module_exit необходима, чтобы позволить ядру найти функцию очистки.

 

Если модуль не определяет функцию очистки, ядро не позволит ему быть выгруженным.

Перехват ошибок во время инициализации

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

 

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

 

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

 

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

 

Следующий пример кода (использующий фиктивные функции регистрации и разрегистрации) ведёт себя правильно, если инициализация в любой момент даёт ошибку:

 

int __init my_init_function(void)

{

    int err;

 

    /* регистрация использует указатель и имя */

    err = register_this(ptr1, "skull");

    if (err) goto fail_this;

    err = register_that(ptr2, "skull");

    if (err) goto fail_that;

    err = register_those(ptr3, "skull");

    if (err) goto fail_those;

 

    return 0; /* успешно */

 

fail_those: unregister_that(ptr2, "skull");

fail_that: unregister_this(ptr1, "skull");

fail_this: return err; /* передать ошибку для наследования */

}

 

Этот код пытается зарегистрировать три (фиктивных) средства. Инструкция goto используется в случае ошибки, чтобы разрегистрировать только средства, которые были успешно зарегистрированы до того, как дела пошли плохо.

 

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

 

Возвращаемым значением my_init_function является err, код ошибки. В ядре Linux коды ошибок - это отрицательные числа, принадлежащие к определённым в <linux/errno.h>. Если вы хотите сгенерировать собственные коды ошибок взамен возвращаемых другими функциями, вы должны подключить <linux/errno.h>, чтобы использовать символические значения, такие как -ENODEV, -ENOMEM и так далее. Возврат соответствующих кодов ошибок это всегда хорошая практика, потому что пользователь программы может превратить их в значимые строки используя perror или аналогичные средства.

 

Очевидно, что функция очистки модуля должна отменить регистрацию всего, что выполнила функция инициализации, и привычно (но обычно не обязательно), чтобы разрегистрация средств выполнялась в порядке, обратном использованному для регистрации:

 

void __exit my_cleanup_function(void)

{

    unregister_those(ptr3, "skull");

    unregister_that(ptr2, "skull");

    unregister_this(ptr1, "skull");

    return;

}

 

Если ваша инициализация и очистка являются более сложными, чем работа с несколькими объектами, подход через goto может стать трудным для управления, потому что весь код очистки должен быть повторен в функции инициализации, перемежаясь несколькими метками. Следовательно, иногда оказывается более удобной другая структура кода. То, что необходимо сделать, чтобы минимизировать дублирование кода и сохранять всё упорядоченным, это вызывать функцию очистки в функции инициализации, когда возникает ошибка. Функция очистки затем должна проверить состояние каждого объекта перед отменой его регистрации. В своей простейшей форме код выглядит следующим образом:

 

struct something *item1;

struct somethingelse *item2;

int stuff_ok;

 

void my_cleanup(void)

{

    if (item1)

        release_thing(item1);

    if (item2)

        release_thing2(item2);

    if (stuff_ok)

        unregister_stuff( );

    return;

}

 

int __init my_init(void)

{

    int err = -ENOMEM;

 

    item1 = allocate_thing(arguments);

    item2 = allocate_thing2(arguments2);

    if (!item1 || !item2)

        goto fail;

    err = register_stuff(item1, item2);

    if (!err)

        stuff_ok = 1;

    else

        goto fail;

    return 0; /* успешно */

 

fail:

    my_cleanup( );

    return err;

}

 

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

Гонки при загрузке модуля

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

 

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

 

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

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