Выполнение прямого ввода/вывода

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

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

 

Одним из примеров прямого ввода/вывода, используемого в ядре версии 2.6, является драйвер ленточного SCSI накопителя. Ленточные накопители (стримеры) могут передавать много данных через систему и передача данных, как правило, ориентирована на запись, так что от буферизации данных в ядре пользы мало. Таким образом, при выполнении соответствующих условий (например, буфер пользовательского пространства выровнен постранично), драйвер SCSI накопителя выполняет свой ввод/вывод без копирования данных.

 

Тем не менее, важно понимать, что прямой ввод/вывод не всегда обеспечивают повышение производительности, как можно было ожидать. Накладные расходы инициализации прямого ввода/вывода (которая включает в себя обработку ошибок и точное определение соответствующих пользовательских страниц) могут быть значительными и выгоды от прямого ввода/вывода теряются. Например, использование прямого ввода/вывода требует, чтобы системный вызов write выполнялся синхронно; в противном случае, приложение не узнает, когда оно может повторно использовать свой буфер ввода/вывода. Остановка приложения до завершения каждой записи может замедлить ход событий, вот почему приложения, использующие прямой ввод/вывод часто также используют асинхронные операции ввода/вывода.

 

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

 

Ключом к реализации прямого ввода/вывода в ядре версии 2.6 является функция, названная get_user_pages, которая объявлена в <linux/mm.h> со следующим прототипом:

 

int get_user_pages(struct task_struct *tsk,

                   struct mm_struct *mm,

                   unsigned long start,

                   int len,

                   int write,

                   int force,

                   struct page **pages,

                   struct vm_area_struct **vmas);

 

Эта функция имеет несколько аргументов:

 

tsk

Указатель на задачу, выполняющую ввод/вывод; его основной целью является сообщить ядру, кто должен отвечать за любые ошибки страницы, происходящие при создании буфера. Этот аргумент почти всегда передается как current.

 

mm

Указатель на структуру управления памятью, описывающую адресное пространство для отображения. Структура mm_struct - часть, которая связывает воедино все части (области VMA) виртуального адресного пространства процесса. Для использования в драйвере этот аргумент всегда должен быть current->mm.

 

start

len

start является (странично-выровненным) адресом в буфере пользовательского пространства, а len является длиной буфера в страницах.

 

write

force

Если write не равен нулю, страницы отображаются для доступа на запись (подразумевая, конечно, что пространство пользователя выполняет операцию чтения). Флаг force приказывает get_user_pages отменить защиту на данных страницах для обеспечения запрашиваемого доступа; драйверы должны всегда передавать здесь 0.

 

pages

vmas

Выходные параметры. После успешного завершения pages содержат список указателей на структуры struct page, описывающие буфер пользовательского пространства и vmas содержит указатели на соответствующие области VMA. Очевидно, параметры должны указывать на массивы, способные содержать по меньшей мере len указателей. Любой параметр может быть NULL, но для действительной работы с буфером вам необходимы, по крайней мере, указатели struct page.

 

get_user_pages является низкоуровневой функцией управления памятью с достаточно сложным интерфейсом. Она также требует, чтобы перед вызовом для адресного пространства были получены в режиме чтения семафоры чтения/записи mmap. В результате, вызов get_user_pages обычно выглядит примерно так:

 

down_read(&current->mm->mmap_sem);

result = get_user_pages(current, current->mm, ...);

up_read(&current->mm->mmap_sem);

 

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

 

После успешного завершения вызывающий имеет массив pages, указывающий на буфер пользовательского пространства, который  заблокирован в памяти. Для работы непосредственно с буфером, на код пространства ядра должен преобразовать каждый указатель struct page в виртуальный адрес ядра с помощью kmap или kmap_atomic. Обычно, однако, устройства, для которых прямой ввод/вывод является оправданным, используют операции DMA, так что ваш драйвер, вероятно, захочет создать из массива указателей struct page список разборки/сборки. Мы обсудим, как это сделать в разделе "Преобразования разборки/сборки".

 

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

 

void SetPageDirty(struct page *page);

 

(Этот макрос определён в <linux/page-flags.h>). Большинство кода, который выполняет эту операцию сначала выполняет проверку, чтобы убедиться, что эта страница не в зарезервированной части карты памяти, которая никогда не выгружается. Таким образом, код обычно выглядит следующим образом:

 

if (! PageReserved(page))

    SetPageDirty(page);

 

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

 

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

 

void page_cache_release(struct page *page);

 

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

Асинхронный ввод/вывод

Одной из новых функций, добавленных в ядро версии 2.6, была возможность асинхронного ввода/вывода. Асинхронный ввод/вывод позволяет пользовательскому пространству инициировать операции, не ожидая их завершения; таким образом, во время выполнения ввода/вывода приложение может выполнять другую обработку. Сложные, высокопроизводительные приложения могут также использовать асинхронный ввод/вывод для выполнения множества операций, происходящих в одно и то же время.

 

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

 

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

 

Драйверы, поддерживающие асинхронный ввод/вывод должна подключать <linux/aio.h>. Для реализации асинхронного ввода/вывода имеются три метода file_operations :

 

ssize_t (*aio_read) (struct kiocb *iocb, char *buffer, size_t count, loff_t offset);

ssize_t (*aio_write) (struct kiocb *iocb, const char *buffer, size_t count, loff_t offset);

int (*aio_fsync) (struct kiocb *iocb, int datasync);

 

Операция aio_fsync представляет интерес только для кода файловой системы, поэтому мы далее не будем обсуждать её здесь. Два других, aio_read и aio_write, выглядят очень похожими на обычные методы read и write, но с несколькими исключениями. Одно из них то, что параметр offset передается по значению; асинхронные операции никогда не изменяют позицию в файле, поэтому передавать указатель на неё нет никаких причин. Эти методы также принимают параметр iocb (“I/O control block”, "блок управления ввода/вывода"), до которого мы  доберёмся через мгновение.

 

Целью методов aio_read и aio_write является начать операции чтения или записи, которые могут или не могут быть завершёнными на момент их возвращения. Если возможно немедленно завершить операцию, метод должен сделать это и вернуть обычный статус: число переданных байт или отрицательный код ошибки. Таким образом, если ваш драйвер имеет метод read, названный my_read, следующий метод aio_read совершенно правильный (хотя и довольно бессмысленный):

 

static ssize_t my_aio_read(struct kiocb *iocb, char *buffer, ssize_t count, loff_t offset)

{

    return my_read(iocb->ki_filp, buffer, count, &offset);

}

 

Обратите внимание, что указатель struct file можно найти в поле ki_filp структуры kiocb.

 

Если вы поддерживаете асинхронный ввод/вывод, вы должны осознавать тот факт, что ядро может при необходимости создать "синхронные IOCB". Это, по существу, асинхронные операции, которые должны быть выполнены на самом деле синхронно. Впору задаться вопросом, почему это делается так, но лучше просто сделать то, что запрашивает ядро. Синхронные операции помечены в IOCB; ваш драйвер должен запросить этот статус следующим образом:

 

int is_sync_kiocb(struct kiocb *iocb);

 

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

 

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

 

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

 

int aio_complete(struct kiocb *iocb, long res, long res2);

 

Здесь iocb является тем же IOCB, который была первоначально вам передан, и res является обычным статусом результата для этой операции. res2 является вторым кодом результата, который будет возвращён в пользовательское пространство; большинство реализаций асинхронного ввода/вывода передают res2 как 0. После того, как вы вызвали aio_complete, вы не должны больше прикасаться к IOCB или пользовательскому буферу.

Пример асинхронного ввода/вывода

Асинхронный ввод/вывод в исходниках примеров реализует странично-ориентированный драйвер scullp. Реализация проста, но вполне достаточна, чтобы показать, как должны быть устроены асинхронные операции.

 

Методы aio_read и aio_write на самом деле делают немногое:

 

static ssize_t scullp_aio_read(struct kiocb *iocb, char *buf, size_t count, loff_t pos)

{

    return scullp_defer_op(0, iocb, buf, count, pos);

}

 

static ssize_t scullp_aio_write(struct kiocb *iocb, const char *buf, size_t count, loff_t pos)

{

    return scullp_defer_op(1, iocb, (char *) buf, count, pos);

}

 

Эти методы просто вызывают общие функции:

 

struct async_work {

    struct kiocb *iocb;

    int result;

    struct work_struct work;

};

 

static int scullp_defer_op(int write, struct kiocb *iocb, char *buf, size_t count, loff_t pos)

{

    struct async_work *stuff;

    int result;

 

    /* Копируем сейчас, пока мы может иметь доступ к буферу */

    if (write)

        result = scullp_write(iocb->ki_filp, buf, count, &pos);

    else

        result = scullp_read(iocb->ki_filp, buf, count, &pos);

 

    /* Если это синхронная IOCB, мы возвращаем наш статус сейчас. */

    if (is_sync_kiocb(iocb))

        return result;

 

    /* В противном случае отложим завершение на несколько миллисекунд. */

    stuff = kmalloc (sizeof (*stuff), GFP_KERNEL);

    if (stuff == NULL)

        return result; /* Памяти нет, так что просто завершаем */

    stuff->iocb = iocb;

    stuff->result = result;

    INIT_WORK(&stuff->work, scullp_do_deferred_op, stuff);

    schedule_delayed_work(&stuff->work, HZ/100);

    return -EIOCBQUEUED;

}

 

Более полная реализация использовала бы get_user_pages, чтобы отобразить пользовательский буфер в пространство ядра. Мы решили сохранить жизнь простой просто копируя данные вначале. Затем делается вызов is_sync_kiocb, чтобы увидеть, должна ли эта операция быть выполнена синхронно; если да, то возвращается статус результата, и мы завершаем работу. В противном случае, мы запоминаем соответствующую информацию в небольшой структуре, организуем "завершение" через очередь задач и возвращаем -EIOCBQUEUED. На данный момент управление возвращается к пользовательскому пространству.

 

Позднее, очередь задач выполняет нашу функцию завершения работы:

 

static void scullp_do_deferred_op(void *p)

{

    struct async_work *stuff = (struct async_work *) p;

    aio_complete(stuff->iocb, stuff->result, 0);

    kfree(stuff);

}

 

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

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