Драйверы сетевых контроллеров (devnp-*)
Go to file
2023-09-23 15:21:57 +03:00
hardware Драйвер devnp-e100.so для ЗОСРВ Нейтрино редакции 2020 2022-10-05 18:16:38 +03:00
Makefile Драйвер devnp-e100.so для ЗОСРВ Нейтрино редакции 2020 2022-10-05 18:16:38 +03:00
netdrivers.mk Драйвер devnp-e100.so для ЗОСРВ Нейтрино редакции 2020 2022-10-05 18:16:38 +03:00
README.md README.md: актуализация ссылок на документацию 2023-09-23 15:21:57 +03:00

Общая структура сетевого стека

    ┌─────────────────────────────┐ 
    │                             │
    │     Сетевой контроллер      │
    │                             │
    └──────────────▴──────────────┘
                   │
    ┌──────────────┴──────────────┐
    │                             │
    │  Сетевой драйвер (devnp-*)  │
    │                             │
    └──────────────▴──────────────┘
                   │
                   *
                   │
    ┌──────────────┴──────────────┐         ┌───────────────────────────┐
    │                             │         │                           │
    │ Сетевой менеджер (io-pkt-*) ◂─── * ───┤   Клиентское приложение   │
    │                             │         │                           │
    └─────────────────────────────┘    ▲    └───────────────────────────┘
                                       │
                                       │
            Интерфейс libsocket ───────┘

Дерево исходных кодов

|- hardware/devnp/
|  |- e100/             - Исходный код драйвера Fast Ethernet контроллеров Intel 8255x
|  |- sample/           - Исходный код примера сетевого драйвера (поясняет изложенное в данном readme)
|  `- Makefile          - Правила сборки дерева исходников
|
|- netdrivers.mk        - Параметры сборки драйверов
`- Makefile             - Правила сборки дерева исходников

Сборка драйвера

make

Запуск драйвера

Общая схема запуска драйвера:

io-pkt-* -d e100 [опция[,опция ...]] ...

или

io-pkt-*
...
mount -T io-pkt -o [опция[,опция ...]] devnp-e100.so

Разработка сетевого драйвера

Сетевой драйвер выступает прослойкой между оборудованием и сетевым стеком. Он включает низкоуровневые обращения к конкретному оборудованию и высокоуровневые интерфейсы взаимодействия с io-pkt. Первая часть уникальна для каждого конкретного устройства и в данном материале не рассматривается.

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

Далее будут рассматриваться следующие особенности драйверов:

  • Инициализация
  • Обработка прерываний и получение пакетов
  • Отправка пакетов
  • Периодические таймеры
  • Состояние линка
  • Контроль и управление
  • Завершение драйвера
  • Краткосрочные ожидания в драйвере
  • Многопоточность
  • Функция отсоединения

Инициализация

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

Инициализация начинается с регистрации точки входа:

struct nw_dll_syms sam_syms[] = {
        { "iopkt_drvr_entry", &IOPKT_DRVR_ENTRY_SYM( sam ) },
        { NULL, NULL }
};

Это определение требует от сетевого стека в момент инициализации вызвать для данного драйвера функцию sam_entry(). Задача этой фунции в конечном счете вызвать dev_attach() для каждого экземпляра поддерживаемого оборудования, обнаруженного драйвером. Очевидно, экземпляров может быть один или несколько.

Прототип функции dev_attach():

int dev_attach( char            *drvr,
                char            *options,
                struct cfattach *ca,
                void            *cfat_arg,
                int             *single,
                struct device   **devp,
                int (*print)( void *, const char * ) );

Ее аргументы:

  • drvr. Строка, используемая в качестве префикса имени интерфейса. В нашем примере оно указано как "sam", и по умолчанию создается интерфейс "sam0".

  • options. Строка параметров, переданая драйверу. Она анализируется dev_attach() в поисках параметров name, lan и unit, которые переопределяют имя интерфейса по умолчанию. Параметры lan и unit идентичны по смыслу - они переопределяют число, добавляемое к имени интерфейса. Опция name переопределяет аргумент drvr.

  • ca. Указатель на структуру cfattach, которая определяет размер структуры устройства, а также detach/attach-функции драйвера. Для инициализации экземпляра этой структуры используется макрос CFATTACH_DECL().

  • cfat_arg. Аргумент, который передается attach-функции драйвера в качестве третьего аргумента.

  • single. Если параметр lan или unit находится в строке параметров, то целое число, на которое указывает single, устанавливается равным 1.

  • devp. Указатель наструктуру struct device:

    • Если не равно NULL, параметр определяет родительское устройство. При завершении драйвера проверяется, что удаляемое устройство не является родителем других устройств. Большинство драйверов устанавливают devp в NULL.
    • Функция dev_attach() через devp преедает указатель на структуру, созданную функцией для нового устройства. Этот указатель также передается в качестве второго аргумента attach-функции.
  • print. NULL или указатель на функцию отладки. dev_attach() будет вызывать ее следующим способом:

    if ( print != NULL )
        (*print)( cfat_arg, NULL );
    

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

Функция dev_attach() через препроцессор с помощью параметра &sam_ca получает указатель на:

CFATTACH_DECL( sam, sizeof( struct sam_dev ), NULL, sam_attach, sam_detach, NULL );

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

Функция sam_attach() подключается к стеку двумя способами:

  • Настройка callout-фукнций в структуре ifp. Например, когда стек хочет отправить пакет в сеть, вызывается указатель на callout-фукнцию ifp->if_start(). Обратите внимание, что он не имеет никакого отношения к инициализации. В шаблоне драйвера в функции sam_attach() указатель ifp->if_start устанавливается на адрес функции sam_start().

  • Настройка обработки прерываний. Вызвав функцию interrupt_entry_init(), стеку передается в качестве параметра указатель на структуру sc_inter. Эта структура должна быть размещена в пределах уникального для экземпляра оборудования дескриптора устройства, обслуживаемого драйвером. Структура sc_inter включает указатели на функции sam_process_interrupt() и sam_enable_interrupt(), а также указатель на уникальный дескриптор устройства (sam, от названия драйвера - sample).

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

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

ifconfig sam0 10.42.107.238

В этот момент стек вызовет callout-функцию ifp->if_init() (которая указывает на функцию sam_init()) для включения аппаратного обеспечения.

Имейте в виду, callout-функция ifp->if_init() будет вызываться стеком регулярно при практически каждом вызове утилиты ifconfig. Например, при вызове:

ifconfig sam0 mtu 8100

Таким образом, данная функция должна обеспечить завершение инициализации оборудования. Можно увидеть, что было бы ошибкой устанавливать MTU в функции sam_attach(). Функция периодической инициализации sam_init() должна постоянно проверять текущую конфигурацию оборудования и корректировать ее в соответствии с требованиями пользователя. При этом было бы ошибкой отключать аппаратуру и инициализировать ее заново, так как даже небольшое изменение конфигурации будет прерывать любые текущие потоки трафика.

Резюмируя сказанное: функция sam_attach() вызывается однократно для выделения ресурсов и подключения к сетевому стеку, а sam_init() вызывается многократно для настройки и включения оборудования.

Если разрабатывается драйвер для сетевого контроллера на шине PCI, драйверу придется озаботиться вопросом идентификации оборудования посредством VID:DID идентификаторов. В рассматриваемом примере драйвера такого кода по понятным причинам нет. Аналогичные задачи должны быть решены и при обращении к шине USB.

Обработка прерываний и получение пакетов

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

После завершения ISR возвращаемое interrupt_queue() значение пробуждает сетевой стек и вызывает callout-функцию драйвера sam_process_interrupt(), определенную через указатель sam->sc_inter.func(). Функция sam_process_interrupt() и является фактическим обработчиком прерывания, который должен обслуживать запросы оборудования: выполнять работу с регистрами, обрабатывать ошибки и т.д. Она также может обслуживать TX-логику оборудования (обычно это не рекомендуется из-за негативного влияния на производительность).

При этом, ISR должен обслуживать RX-логику оборудования. Любой заполненный контроллером входящий пакет должен удаляться из оборудования, а новые пустые пакеты должны возвращаться в оборудование. Заполненные полученные пакеты передаются в стек с помощью callout-функции ifp->if_input().

Возвращаемое ISR значение 0 означает, что функция завершилась не окончив всю работу. Это позволит другим сетевым интерфейсам выполнять обработку своих ISR, помещая sam_process_interrupt() в конец очереди выполнения. Возвращение 1 сигнализирует о том, что драйвер завершил обработку прерываний. В этом случае стеком будет вызвана функция sam_enable_interrupt(), которая должна размаскировать (включить) прерывания и обратить действия, выполненные sam_isr().

Отправка пакетов

Когда сетевой стек хочет передать пакет, он вызывает callout-функцию ifp->if_start() драйвера, которая была задана в dev_attach() и соответствует sam_start().

В первую очередь стоит убедиться, что имеются аппаратные ресурсы (дескрипторы, буферы и т.д.), доступные для передачи пакетов. Если ресурсы отсутствуют, драйвер должен вернуться из функции ifp->if_start(), установив статус IFF_OACTIVE:

ifp->if_flags_tx |= IFF_OACTIVE;

Также необходимо освободить мьютекс передачи, о котором будет сказано ниже.

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

Большинство драйверов, не возвращают управление из ifp->if_start(), пока не закончатся поступающие от стека пакеты или не закончатся аппаратные ресурсы.

Есть несколько удобных макросов, которые можно использовать при этом:

  • Макрос IFQ_POLL() позволяет определить имеются ли у стека другие доступные для отправки пакеты. Если их нет, обработку исходящих пакетов можно завершить.
  • Макрос IFQ_DEQUEUE() извлекает из очереди стека первый пакет, готовый к отправке. Некоторые драйверы не используют первый макрос, ожидая, что стек повторно вызовет ifp->if_start() при наличии других готовых исходящих пакетов. При извлечении пакета из очереди, его обязательно передать в сеть.

Прежде чем вернуться из этой callout-функции, следует освободить мьютекс передачи:

NW_SIGUNLOCK_P( &ifp->if_snd_ex, iopkt_selfp, wtp );

Обратите внимание, что в рассматриваемом драйвере функция ifp->if_start() вызывает m_free( m ) для освобождения переданного пакета. Это осуществляется для избежания утечки памяти. Но если сетевая карта функционирует на основе дескрипторов, выполнять это в общем случае не требуется, так как дескрипторы обычно циклически переиспользуются.

Если сетевая карта требует копирования передаваемого пакета в буфер, вызвать m_free( m ) скорее всего придется. Она сообщит стеку, что буфер доступен для повторного использования и в него можно производить запись новых данных.

Для сетевых карт, ориентированных на дескрипторы, передаваемый пакет не копируется: аппаратное обеспечение выполняет DMA-операцию и вы буфер пакета будет освобожден только после того, как операция завершится. Это позвлит избежать перезаписи данных пакета до его передачи в сеть. В исходном коде таких драйверов можно обнаружить функцию "harvest" или "reap", которая будет проверять переданные дескрипторы и попутно освобождать их буферы.

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

Обычно пакеты поступают от стека в виде нескольких буферов. Для TCP-пакетов их количество равно 3, где первый из которых содержит заголовки, второй содержит остатки предыдущего mbuf, а третий содержит начало следующего mbuf. Плохо фрагментированные пакеты могут потребовать копирования в новый непрерывный буфер в зависимости от возможностей оборудования и степени фрагментации буфера. Очевидно, что это негативно влияет на производительность и следует всячески избегать таких ситуаций.

Периодические таймеры

Сетевым драйверам часто требуются периодические таймеры для выполнения вспомогательных функций. Например, для обслуживания канала и освобождения TX-дескрипторов. При этом, они не должны создавать собственный поток или асинхронные таймеры средствами ОС. Корректный способ установки периодического таймера в callout-функции ifp->if_init() следующий:

callout_msec( &dev->mii_callout, 2 * 1000, dev_monitor, dev );

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

Создавать таймеры следует только один раз вызовом callout_init():

callout_init( &dev->mii_callout );

callout_msec() может быть вызван несколько раз. Этот вызов запускает остановленный таймер или сбрасывает работающий. Вызов callout_stop() для остановленного таймера не имеет негативных последствий, но вызов callout_init() более одного раза приводит к неустранимой ошибке. callout_init() обычно используется в callout-функции ifp->if_attach(), которая вызывается единожды для каждого устройства. callout_msec() используется в ifp->if_init(), а также в самом callback-вызове. Поскольку он сбрасывает работающий таймер и запускает остановленный, дальнейшая блокировка не требуется. callout обычно останавливается вызовом callout_stop() из функции ifp->if_stop().

Если TX-код вызывается для освобождения дескрипторов, следует заблокировать мьютекс передачи с помощью макроса NW_SIGLOCK(). Это позволит избежать повреждения данных и регистров.

Состояние линка

Пользователи должны быть уведомлены об изменениях состояния линка. Это осуществляется следующим образом:

if_link_state_change( ifp, LINK_STATE_UP );
if_link_state_change( ifp, LINK_STATE_DOWN );

Контроль и управление

Управление драйвером осуществляется с помощью callout-функции ifp->if_ioctl(), соответствующей sam_ioctl().

Данная функция может быть пустой или довольно сложной, в зависимости от перечня поддерживаемых функций. Для совместимости с утилитой nicinfo (например, для успешного выполнения nicinfo sam0) следует добавить поддержку команд SIOCGDRVCOM DRVCOM_CONFIG/DRVCOM_STATS. Для поддержки аппаратного рассчета контрольных сумм следует поддержать команду SIOCSIFCAP.

Для отображения скорость канала (режима работы линка) и дуплекс с помощью:

ifconfig -v

следует поддержать команды SIOCGIFMEDIA и SIOCSIFMEDIA. Они также могут использоваться для установки данных параметров. Драйверы, поддерживающие настройку параметров среды передачи, имеют в своем составе файл с именем bsd_media.c. Для вывода перечня поддерживаемых режимов используется вызов:

ifconfig -m

Кроме того, ioctl() позволяет многоадресную передачу. В sam.c имеется пример работы с такими адресами (см. использование макросов ETHER_FIRST_MULTI() и ETHER_NEXT_MULTI()).

Завершение драйвера

Возможны следующие сценарии завершения драйвера:

  • Команда ifconfig sam0 down, приводящая к вызову callout-функции sam_stop(). Сценарий должен остановить все операции приема и передачи, а также очистить все используемые буферы (чтобы данные в них не могли появиться при следующем включении интерфейса). Обратите внимание, что кроме буферов и Tx/Rx передач, остальные структуры драйвера и аппаратные ресурсы освобождать не следует. Следующее обращение к драйверу, скорее всего, будет вызвано командой ifconfig up, что приведет к исполнению sam_init() и повторной инициализации оборудования.

  • Команда ifconfig sam0 destroy, приводящая к вызову callout-функции sam_detach(). Сценарий должен сбросить оборудование и освободить все ресурсы. Ожидается, что драйвер в скором времени будет выгружен из сетевого стека, но сам стек продолжит работать. Типичный тест корректности обработки данного сценария включает: в цикле осуществлять монтирование драйвера, вызов ifconfig с указанием адреса, выполнить несколько операций передачи трафика и выполнение ifconfig ... destroy. Корректно функционирующий драйвер не должен приводить к падению стека, утечкам памяти и невозможности передачи трафика.

  • Завершение сетевого стека или аварийное завершение с вызовом callout-функции sam_shutdown(). Сценарий должен сбросить оборудование, чтобы остановить любые операции (включая DMA). При этом следует избегать любого освобождения драйверных ресурсов, поскольку это может привести к рекурсивному вызову callout-а (маскируя первоначальную причину в дампе аварийного завершения процесса).

Функция sam_detach() для драйвера определяется уже известным образом:

CFATTACH_DECL( sam, sizeof( struct sam_dev ), NULL, sam_attach, sam_detach, NULL );

А вот аварийный callout-вызов sam_shutdown() определяется чуть сложнее:

sam->sc_sdhook = shutdownhook_establish( sam_shutdown, sam );

Важно не забыть установить этот хук в callout-функции sam_attach(), а также удалить его в sam_detach() с помощью:

shutdownhook_disestablish( sam->sc_sdhook );

Краткосрочные ожидания в драйвере

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

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

Пример задержки на 0.5 секунд:

if ( !ISSTART && ISSTACK )
{
    /*
     * Called from an io-pkt thread and not at startup so can't use normal delay,
     * work out what type of delay to use.
     */
    if ( curproc == stk_ctl.proc0 )
    {
        /*
         * Called from a callout, can only do another callout. If ltsleep is tried
         * it returns success without actually sleeping.
         */
        callout_msec( &dev->delay_callout, 500, next_part, dev );

        return;
    }

    /* Normal io-pkt thread case. Use ltsleep to avoid blocking other interfaces */
    timo = hz / 2;
    ltsleep( &wait, 0, "delay", timo, NULL );
} else {
    /*
     * Either io-pkt is starting up or called from a different
     * thread so will not block other interfaces. Just use delay.
     */
    delay(500);
}

Многопоточность

Драйвер не должен создавать собственные потоки и должен работать под правлением потоков сетевого стека. Однако, бывают ситуации, когда драйверу действительно требуется отдельный поток (например, для обслуживания взаимодействия по USB или SDIO). Создавать стандартные потоки с помощью pthread_create() не рекомендуется, поскольку они не будут связаны с обработкой mbufs. В случае острой необходимости потоки обработки mbuf должны быть созданы с помощью nw_pthread_create() и ни в коем случае через pthread_create():

nw_pthread_create( &tid, NULL, thread_fn, dev, 0, thread_init_fn, dev );

При этом, потоковая функция должна установить отдельное имя потоку, чтобы отличить его от стандартных потоков стека. Кроме того, следует также установить callout-обработчик wtp->quiesce_callout(). Допустимо выполнение и других инициализаций:

static int thread_init_fn( void *arg )
{
    struct nw_work_thread   *wtp;
    dev_handle_t            *dev = (dev_handle_t *)arg;

    pthread_setname_np( 0, "My driver thread" );

    wtp = WTP;

    wtp->quiesce_callout = thread_quiesce;
    wtp->quiesce_arg     = dev;

    return (EOK);
}

Имеет смысл назначать имя потока исходя из принадлежности конкретному драйверу и контуру обработки. Например:

# pidin -p io-pkt-v4 thread
     pid name               thread name          STATE       Blocked
    4100 sbin/io-pkt-v4     io-pkt main          SIGWAITINFO
    4100 sbin/io-pkt-v4     io-pkt#0x00          RECEIVE     1
    4100 sbin/io-pkt-v4     asixx Rx             RECEIVE     22

В примере есть следующие потоки:

  • io-pkt main. Используется для обработчика сигналов и обработки блокирующих запросов.

  • io-pkt#0x00. Поток для выполнения основной работы сетевого стека. Другие нумерованные потоки создаются стеком для работы на отдельных процессорных ядрах и при дополнительных вызовах interrupt_entry_init().

  • asixx Rx. Драйверный поток, ассоциированный с devnp-asixx.so и его Rx-контуром. Его задачей является обработка специальных пакетов с малой задержкой, когда стек занят обслуживанием других запросов. Отсутствие у потока имени приведет к тому, что он автоматически получит нумерование и не будет отличим от потоков второго типа.

Сценарии вызова callout-обработчика wtp->quiesce_callout():

  • Стеку необходимо изменить некоторые структуры (например, при других вызовах nw_pthread_create()), что требует, чтобы все остальные потоки были заблокированы, пока обновление не завершится.
  • При терминировании потоков, например, во время завершения сетевого стека.

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

static int quiescing = 0;
static int quiesce_die = 0;

static void thread_quiesce( void *arg, int die )
{
    dev_handle_t *dev = (dev_handle_t *)arg;

    quiescing   = 1;
    quiesce_die = die;
}

static void *thread_fn( void *arg )
{
    while ( 1 )
    {
        if ( quiescing )
        {
            if ( quiesce_die )
            {
                /* Thread will terminate on calling quiesce_block(), clean up here if required. */
            }
            quiesce_block( quiesce_die );
            quiescing = 0;
        }

        /* Do normal thread work */
    }
}

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

sam->dev.dv_flags |= DVF_QUIESCESELF;

Затем он может вызывать функции приостановки в detach-функции:

/* self quiesce */
quiesce_all();
ether_ifdetach( ifp );
if_detach( ifp );
unquiesce_all();

Функция отсоединения (detach-функции)

Одной из обязанностей detach-функции драйвера является определение необходимости отмонтирования драйвера. Она вызывается для каждого устройства, обслуживаемого драйвером. Таким образом, отмонтирование должно происходить при обработке последнего из устройств. Драйвер сам определяет способ отслеживания доступных устройств.

Если драйвер определяет, что отмонтирование преждевременно, следует выполнить следующее:

sam->dev.dv_dll_hdl = NULL; 

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