The website "dmilvdv.narod.ru." is not registered with uCoz.
If you are absolutely sure your website must be here,
please contact our Support Team.
If you were searching for something on the Internet and ended up here, try again:

About uCoz web-service

Community

Legal information

Блокирующий Ввод/Вывод

Блокирующий Ввод/Вывод

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

В Главе 3 мы рассмотрели, как осуществлять методы драйвера read и write. В тот момент, однако, мы пропустили один важный вопрос: как должен отреагировать драйвер, если он не может сразу удовлетворить запрос? Вызов read может прийти, когда данные не доступны, но ожидаемы в будущем. Или процесс может попытаться вызвать write, но устройство не готово принять данные, потому что ваш выходной буфер полон. Вызывающий процесс обычно не заботится о таких вопросах; программист просто ожидает вызвать read или write и получить возвращение вызова после выполнения необходимой работы. Таким образом, в подобных случаях драйвер должен (по умолчанию) блокировать процесс, помещая его в режим сна, на время выполнения запроса.

 

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

Знакомство с засыпанием

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

 

Драйверу устройства в Linux отправить процесс в сон легко. Есть, тем не менее, несколько правил, которые необходимо иметь в виду, чтобы код имел возможность заснуть безопасным образом.

 

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

 

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

 

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

 

В Linux очередь управляется с помощью "головы очереди ожидания", структуры типа wait_queue_head_t, которая определена в <linux/wait.h>. Голова очереди ожидания может быть определена и проинициализирована статически:

 

DECLARE_WAIT_QUEUE_HEAD(name);

 

или динамически, как здесь:

 

wait_queue_head_t my_queue;

init_waitqueue_head(&my_queue);

 

Мы вернёмся к структуре очереди ожидания в ближайшее время, но мы знаем достаточно, чтобы сейчас бросить первый взгляд на засыпание и пробуждение.

Простое засыпание

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

 

wait_event(queue, condition)

wait_event_interruptible(queue, condition)

wait_event_timeout(queue, condition, timeout)

wait_event_interruptible_timeout(queue, condition, timeout)

 

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

 

Если вы используете wait_event, ваш процесс помещается в непрерываемый сон, который, как мы уже отмечали ранее, как правило, не то, что вы хотите. Предпочтительной альтернативой является wait_event_interruptible, который может быть прерван сигналом. Эта версия возвращает целое число, которое вы должны проверить; ненулевое значение означает, что ваш сон был прерван каким-то сигналом и ваш драйвер должен, вероятно, вернуть -ERESTARTSYS. Последние версии (wait_event_timeout и wait_event_interruptible_timeout) ожидают ограниченное время; после истечения этого временного периода (в пересчёте на тики (jiffies), которые мы будем обсуждать в Глава 7), макросы возвращаются со значением 0 независимо от оценки condition.

 

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

 

void wake_up(wait_queue_head_t *queue);

void wake_up_interruptible(wait_queue_head_t *queue);

 

wake_up будит все процессы, ожидающие в данной очереди (хотя ситуация немного сложнее, как мы увидим позже). Другая форма (wake_up_interruptible) ограничивает себя процессами, находящимися в прерываемом сне. В общем, эти две на практике неотличимы (если вы используете прерываемый сон); принято использовать wake_up, если вы используете wait_event и wake_up_interruptible, если вы используете wait_event_interruptible.

 

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

 

static DECLARE_WAIT_QUEUE_HEAD(wq);

static int flag = 0;

 

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

{

    printk(KERN_DEBUG "process %i (%s) going to sleep\n",

                        current->pid, current->comm);

    wait_event_interruptible(wq, flag != 0);

    flag = 0;

    printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);

    return 0; /* конец файла */

}

 

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

{

    printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",

                        current->pid, current->comm);

    flag = 1;

    wake_up_interruptible(&wq);

    return count; /* успешно, избегаем повтора */

}

 

Обратите внимание на использование в этом примере переменной flag. Используя wait_event_interruptible для проверки условия, которое должно стать истинным, мы используем flag, чтобы создать это условие.

 

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

Блокирующие и неблокирующие операции

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

 

Есть также случаи, когда вызывающий процесс информирует вас, что он не хочет блокирования, независимо от того, сможет ввод/вывод что-то выполнить вообще. Явно неблокирующий ввод/вывод обозначается флагом O_NONBLOCK в filp->f_flags. Этот флаг определён в <linux/fcntl.h>, который автоматически подключается <linux/fs.h>. Флаг получил свое название "открыть без блокировки" (“open-nonblock”), поскольку он может быть указан во время открытия (и первоначально мог быть указан только там). Если вы просмотрите исходный код, то сможете найти несколько ссылок на флаг O_NDELAY; это альтернативное имя для O_NONBLOCK, принятое для обеспечения совместимости с кодом System V. Флаг очищен по умолчанию, так как нормальным поведением процесса при ожидании данных является только сон. В случае блокирующий операции, которая является такой по умолчанию,  для соблюдения стандартной семантики должно быть реализовано следующее поведение:

 

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

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

 

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

 

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

 

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

 

Поведение read и write различно, если не указан O_NONBLOCK. В этом случае вызовы просто вернут -EAGAIN (“try it again”, "попробуйте снова"), если процесс вызывает read, когда данные не доступны или если он вызывает write, когда в буфере нет места.

 

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

 

Естественно, O_NONBLOCK имеет смысл также и в методе open. Это происходит, когда вызов может действительно заблокироваться на длительное время; например, при открытии (для доступа на чтение) FIFO, в который никто не пишет (пока), или при доступе к заблокированному файлу на диске. Обычно открытие устройство или успешно или неудачно, без необходимости ожидания внешних событий. Иногда, однако, открытие устройства требует долгой инициализации и вы можете выбрать поддержку O_NONBLOCK в вашем методе open возвращая сразу после начала процесса инициализации устройства -EAGAIN, если флаг установлен. Драйвер может также реализовать блокирующее открытие для поддержки политик доступа по аналогии с блокировками файла. Мы увидим одну из таких реализаций далее в этой главе в разделе "Блокирующее открытие как альтернатива EBUSY".

 

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

 

Флаг неблокирования воздействует только файловые операции read, write и open.

Пример блокирующего ввода/вывода

Наконец, мы переходим к примеру реального метода драйвера, который реализует блокирующий ввод/вывод. Этот пример взят из драйвера scullpipe; это особая форма scull, которая реализует канало-подобное устройство.

 

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

 

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

 

 

struct scull_pipe {

    wait_queue_head_t inq, outq;       /* очереди чтения и записи */

    char *buffer, *end;                /* начало и конец буфера */

    int buffersize;                    /* используется при работе с указателем */

    char *rp, *wp;                     /* откуда читать, куда писать */

    int nreaders, nwriters;            /* число открытий для чтения/записи */

    struct fasync_struct *async_queue; /* асинхронные читатели */

    struct semaphore sem;              /* семафор взаимного исключения */

    struct cdev cdev;                  /* структура символьного устройства */

};

 

Реализация read управляет и блокирующим и неблокирующим вводом и выглядит следующим образом:

 

static ssize_t scull_p_read (struct file *filp, char __user *buf, size_t count, loff_t *f_pos)

{

    struct scull_pipe *dev = filp->private_data;

 

    if (down_interruptible(&dev->sem))

        return -ERESTARTSYS;

 

    while (dev->rp == dev->wp) { /* читать нечего */

        up(&dev->sem); /* освободить блокировку */

        if (filp->f_flags & O_NONBLOCK)

            return -EAGAIN;

        PDEBUG("\"%s\" reading: going to sleep\n", current->comm);

        if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))

            return -ERESTARTSYS; /* сигнал: передать уровню файловой системы для обработки */

        /* иначе цикл, но сначала перезапросим блокировку */

        if (down_interruptible(&dev->sem))

            return -ERESTARTSYS;

    }

    /* ok, данные есть, вернуть что-нибудь */

    if (dev->wp > dev->rp)

        count = min(count, (size_t)(dev->wp - dev->rp));

    else /* указатель записи возвращён в начало, вернуть данные в dev->end */

        count = min(count, (size_t)(dev->end - dev->rp));

    if (copy_to_user(buf, dev->rp, count)) {

        up (&dev->sem);

        return -EFAULT;

    }

    dev->rp += count;

    if (dev->rp == dev->end)

        dev->rp = dev->buffer; /* вернуться к началу */

    up (&dev->sem);

 

    /* и наконец, разбудить всех писателей и вернуться */

    wake_up_interruptible(&dev->outq);

    PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count);

    return count;

}

 

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

 

Давайте внимательно посмотрим, как scull_p_read обрабатывает ожидание данных. Цикл while проверяет буфер, удерживая семафор устройства. Если мы узнаём, что там есть данные, мы можем вернуть их пользователю сразу же, без сна, поэтому всё тело цикла пропускается. Вместо этого, если буфер пуст, мы должны заснуть. Однако, прежде чем мы сможем это сделать, мы должны освободить семафор устройства; если бы мы заснули, удерживая его, писатели никогда бы не имели возможность разбудить нас. После того, как семафор был освобождён, мы делаем быструю проверку, чтобы увидеть, не запросил ли пользователь неблокирующий ввод/вывод и вернуться, если это так. В противном случае, настало время вызова wait_event_interruptible.

 

Как только мы получили вызов, что-то разбудило нас, но мы не знаем, что. Одна возможность заключается в том, что процесс получил сигнал. Оператор if, который содержит вызов wait_event_interruptible, проверяет этот случай. Эта проверка обеспечивает правильную и ожидаемую реакцию на сигналы, которые могли бы быть ответственными за пробуждение этого процесса (так как мы были в прерываемом сне). Если получен сигнал и он не был заблокирован процессом, правильное поведение - дать верхним слоям ядра обработать это событие. Для этого драйвер возвращает вызывающему -ERESTARTSYS; это значение используется внутри слоем виртуальной файловой системы (VFS), который либо перезапускает системный вызов, либо возвращает -EINTR в пространство пользователя. Мы используем такой же тип проверки при обработке сигнала в каждой реализации read и write.

 

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

 

Просто для полноты позвольте нам заметить, что scull_p_read может заснуть в другом месте, после того, как мы получили семафор устройства: это вызов copy_to_user. Если scull засыпает при копировании данных от ядра к пользовательскому пространству, он спит удерживая семафор устройства. Удержание семафора в данном случае является оправданным, поскольку он не вызывает взаимоблокировку системы (мы знаем, что ядро будет выполнять копирование в пространство пользователя и разбудит нас, не пытаясь в процессе заблокировать тот же семафор) и важно то, что массив памяти устройства не меняется, пока драйвер спит.

Подробности засыпания

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

Как процесс засыпает

Если вы загляните в <linux/wait.h>, то увидите, что структура данных типа wait_queue_head_t довольно проста; она содержит спин-блокировку и связный список. Этот список является очередью ожидания, объекты которой объявлены с типом wait_queue_t. Эта структура содержит информацию о спящем процессе и как он хотел бы проснуться.

 

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

 

Следующим шагом будет установить состояние процесса, чтобы пометить его как спящий. Есть несколько состояний задачи, определённых в <linux/sched.h>. TASK_RUNNING означает, что процесс способен работать, хотя не обязательно выполнение процессором в какой-то определённый момент. Есть два состояния, которые показывают, что процесс спит: TASK_INTERRUPTIBLE и TASK_UNINTERRUPTIBLE; они соответствуют, конечно, двум типам сна. Другие состояния обычно не представляют интерес для авторов драйверов.

 

В ядре версии 2.6 коду драйвера обычно нет необходимости напрямую управлять состоянием процессом. Однако, если вам необходимо сделать это, используется вызов:

 

void set_current_state(int new_state);

 

В старом коде вместо этого вы часто встретите что-то наподобие такого:

 

current->state = TASK_INTERRUPTIBLE;

 

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

 

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

 

if (!condition)

    schedule( );

 

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

 

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

 

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

Ручное управление засыпанием

В предыдущих версиях ядра Linux нетривиальное засыпание требовало от программиста обрабатывать все вышеперечисленные шаги вручную. Это был утомительный процесс с привлечением значительного количества подверженного ошибкам шаблонного кода. Программисты могут всё ещё кодировать ручное управление засыпанием таким образом, если они этого хотят; <linux/sched.h> содержит все необходимые определения, а ядра изобилует примерами. Однако, существует более простой способ.

 

Первым шагом является создание и инициализация объекта очереди ожидания. Это, как правило, делается таким макросом:

 

DEFINE_WAIT(my_wait);

 

здесь name является именем объекта переменной очереди ожидания. Вы также можете делать это в два этапа:

 

wait_queue_t my_wait;

init_wait(&my_wait);

 

Но обычно легче поставить строчку с DEFINE_WAIT в начало цикла, реализующего ваш сон.

 

Следующим шагом будет добавить ваш объект очереди ожидания в очередь и установить состояние процесса. Обе эти задачи решаются этой функцией:

 

void prepare_to_wait(wait_queue_head_t *queue,

                     wait_queue_t *wait,

                     int state);

 

Здесь, queue и wait являются головой очереди ожидания и очередью ожидания процесса соответственно. state является новым состоянием процесса; оно должна быть либо TASK_INTERRUPTIBLE (для прерываемых состояний сна, которые, как правило, то, что вы хотите) или TASK_UNINTERRUPTIBLE (для непрерываемых состояний сна).

 

После вызова prepare_to_wait этот процесс может вызвать schedule - после проверки, чтобы быть уверенным, что ждать всё ещё необходимо. После возвращения из schedule наступает время очистки. Эта задача тоже обрабатывается специальной функцией:

 

void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);

 

После этого ваш код может проверить своё состояние и посмотреть, нет ли необходимости подождать ещё.

 

Давно настало время для примера. Раньше мы рассматривали метод read в scullpipe, который использует wait_event. Метод write в этом же драйвере вместо этого выполняет свои ожидания с prepare_to_wait и finish_wait. Обычно вы не будете таким образом смешивать методы в рамках одного драйвера, но мы сделали это для того, чтобы показать оба способа обработки засыпания.

 

Во-первых, давайте для полноты посмотрим на сам метод записи:

 

/* Как много свободного места? */

static int spacefree(struct scull_pipe *dev)

{

    if (dev->rp = = dev->wp)

        return dev->buffersize - 1;

    return ((dev->rp + dev->buffersize - dev->wp) % dev->buffersize) - 1;

}

 

static ssize_t scull_p_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)

{

    struct scull_pipe *dev = filp->private_data;

    int result;

    if (down_interruptible(&dev->sem))

        return -ERESTARTSYS;

 

    /* Убедимся, что для записи есть место */

    result = scull_getwritespace(dev, filp);

    if (result)

        return result; /* scull_getwritespace вызвал up(&dev->sem) */

 

    /* ok, место есть, примем что-то */

    count = min(count, (size_t)spacefree(dev));

    if (dev->wp >= dev->rp)

        count = min(count, (size_t)(dev->end - dev->wp)); /* к концу буфера */

    else /* the write pointer has wrapped, fill up to rp-1 */

        count = min(count, (size_t)(dev->rp - dev->wp - 1));

    PDEBUG("Going to accept %li bytes to %p from %p\n", (long)count, dev->wp, buf);

    if (copy_from_user(dev->wp, buf, count)) {

        up (&dev->sem);

        return -EFAULT;

    }

    dev->wp += count;

    if (dev->wp == dev->end)

        dev->wp = dev->buffer; /* вернуть указатель в начало */

    up(&dev->sem);

 

    /* наконец, пробудить любого читателя */

    wake_up_interruptible(&dev->inq); /* блокированного в read( ) и select( ) */

 

    /* и послать сигнал асинхронным читателям, объясняется позже в Главе 6 */

    if (dev->async_queue)

        kill_fasync(&dev->async_queue, SIGIO, POLL_IN);

    PDEBUG("\"%s\" did write %li bytes\n",current->comm, (long)count);

    return count;

}

 

Этот код выглядит похожим на метод read за исключением того, что мы поместили код, который засыпает, в отдельную функцию, названную scull_getwritespace. Его задача заключается в том, чтобы убедиться, что в буфере есть место для новых данных, или спать в случае необходимости, пока пространство не освободится. После того, как пространство появилось, scull_p_write может просто скопировать данные пользователя туда, настроить указатели и пробудить любые процессы, которые могут находится в ожидании чтения данных.

 

Код, который фактически обрабатывает сон:

 

/* Ожидание пространства для записи; вызывающий должен удерживать семафор устройства.

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

static int scull_getwritespace(struct scull_pipe *dev, struct file *filp)

{

    while (spacefree(dev) == 0) { /* полон */

        DEFINE_WAIT(wait);

 

        up(&dev->sem);

        if (filp->f_flags & O_NONBLOCK)

            return -EAGAIN;

        PDEBUG("\"%s\" writing: going to sleep\n",current->comm);

        prepare_to_wait(&dev->outq, &wait, TASK_INTERRUPTIBLE);

        if (spacefree(dev) == 0)

            schedule( );

        finish_wait(&dev->outq, &wait);

        if (signal_pending(current))

            return -ERESTARTSYS; /* сигнал: передать на уровень файловой системы для обработки */

        if (down_interruptible(&dev->sem))

            return -ERESTARTSYS;

    }

    return 0;

}

 

Отметим ещё раз содержание цикла while. При наличии свободного места сон не требуется, эта функция просто возвращается. В противном случае, она должна отказаться от семафора устройства и ждать. Код использует DEFINE_WAIT для подготовки объекта очереди  ожидания и prepare_to_wait для подготовки к фактическому засыпанию. Затем идёт обязательная проверка буфера; мы должны обработать случай, когда в буфере появляется доступное пространство после того, как мы вошли в цикл while (и отказались от семафора), но прежде чем поместим себя в очередь ожидания. Без такой проверки, если бы читающие процессы смогли бы полностью освободить буфер в это время, мы могли бы не только пропустить сигнал пробуждения, а даже когда-нибудь заснуть навечно. Убедившись, что мы должны заснуть, мы можем вызвать schedule.

 

Стоит снова посмотреть на этот случай: что случится, если пробуждения произойдёт между проверкой if и вызовом schedule? В таком случае всё хорошо. Сигнал пробуждения сбрасывает состояние процесса в TASK_RUNNING и schedule возвращается, хотя не обязательно сразу. Пока проверка происходит после того, как процесс поместил себя в очередь ожидания и изменил своё состояние, всё будет работать.

 

Для завершения мы вызываем finish_wait. Вызов signal_pending говорит нам, были ли мы разбужены сигналом; если так, то мы должны вернуться к пользователю и дать ему возможность попробовать позже. В противном случае, мы повторно получаем семафор и снова как обычно проверяем наличие свободного пространства.

Эксклюзивные ожидания

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

 

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

 

Если объект очереди ожидания имеет установленный флаг WQ_FLAG_EXCLUSIVE, он добавляется в конец очереди ожидания. Без этого флага, наоборот, добавляется в начало.

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

 

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

 

Об использовании эксклюзивного ожидания в драйвере стоит задуматься, если встретились два условия: вы ожидаете значительной борьбы за ресурс и пробуждения одного процесса достаточно, чтобы полностью употребить ресурс, когда он станет доступен. Эксклюзивные ожидания хорошо работают, например, на веб-сервере Apache; когда приходит новое соединение, только один из (часто многих) процессов Apache в системе должен пробудиться для его обслуживания. Мы, однако, не использовали эксклюзивные ожидания в драйвере scullpipe; редко, чтобы читатели боролись за данные (или писатели в пространство буфера) и мы не можем знать, что один из читателей после пробуждения употребит все имеющиеся данные.

 

Помещение процесса в прерываемое ожидание является просто вопросом вызова prepare_to_wait_exclusive:

 

void prepare_to_wait_exclusive(wait_queue_head_t *queue,

                               wait_queue_t *wait,

                               int state);

 

Этот вызов при использовании вместо prepare_to_wait устанавливает "эксклюзивный" флаг в объекте очереди ожидания и добавляет процесс в конец очереди ожидания. Обратите внимание, что нет способ выполнения эксклюзивного ожидания с помощью wait_event и её вариантов.

Детали пробуждения

Объяснение, которое мы представили для пробуждения процесса проще, чем то, что на самом деле происходит внутри ядра. Фактическое поведение является результатом контроля функцией объекта очереди ожидания пробуждаемого процесса. Функция по умолчанию для пробуждения (* Она имеет образное имя default_wake_function.) устанавливает процесс в работающее состояние и, возможно, выполняет переключение контекста для этого процесса, если он имеет более высокий приоритет. Драйверы устройств никогда не должны заменять её другой функцией пробуждения; если вы уверены, что ваш случай является исключением, смотрите <linux/wait.h> для информации о том, как это делать.

 

Мы ещё не видели все варианты wake_up. Большинству авторов драйвера другие не требуются, но для полноты здесь полный набор:

 

wake_up(wait_queue_head_t *queue);

wake_up_interruptible(wait_queue_head_t *queue);

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

 

wake_up_nr(wait_queue_head_t *queue, int nr);

wake_up_interruptible_nr(wait_queue_head_t *queue, int nr);

Эти функции выполняются аналогично wake_up за исключением того, что они могут разбудить до nr ожидающих эксклюзивно вместо одного. Обратите внимание, что передача 0 интерпретируется как запрос на пробуждение всех ожидающих эксклюзивно, а не ни одного из них.

 

wake_up_all(wait_queue_head_t *queue);

wake_up_interruptible_all(wait_queue_head_t *queue);

Эта форма wake_up пробуждает все процессы, независимо от того, выполняют ли эксклюзивное ожидание или нет (хотя прерываемая форма всё же пропускает процессы, выполняющие непрерываемое ожидания).

 

wake_up_interruptible_sync(wait_queue_head_t *queue);

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

 

Если всё перечисленное выше на первый взгляд не совсем ясно, не беспокойтесь. Очень малому числу драйверов необходимо вызывать что-то, кроме wake_up_interruptible.

Древняя история: sleep_on

Если вы проводите какое-то время, копаясь в исходных текстах ядра, вы, скорее всего, встречали две функции, которыми мы до сих пор пренебрегли для обсуждения:

 

void sleep_on(wait_queue_head_t *queue);

void interruptible_sleep_on(wait_queue_head_t *queue);

 

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

 

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

Тестирование драйвера Scullpipe

Мы видели, как реализует блокирующий ввод/вывод драйвер scullpipe. Если вы захотите попробовать его, исходник этого драйвера может быть найден в другой книге примеров. Блокирующий ввод/вывод в действии можно увидеть, открыв два окна. В первом можно запустить такую команду, как cat /dev/scullpipe. Если затем в другом окне скопировать любой файл в /dev/scullpipe, вы должны увидеть содержимое файла, которое появится в первом окне.

 

Тестирование неблокирующей деятельности сложнее, поскольку обычные программы, доступные оболочке, не выполняют неблокирующие операции. Каталог исходников misc-progs содержит следующую простую программу, названную nbtest, для тестирования неблокирующих операций. Всё, что она делает, это копирование своего входа на свой выход, используя неблокирующий ввод/вывод и задержку между попытками. Время задержки задаётся в командной строке и по умолчанию равно одной секундой.

 

int main(int argc, char **argv)

{

    int delay = 1, n, m = 0;

 

    if (argc > 1)

        delay = atoi(argv[1]);

    fcntl(0, F_SETFL, fcntl(0,F_GETFL) | O_NONBLOCK); /* stdin */

    fcntl(1, F_SETFL, fcntl(1,F_GETFL) | O_NONBLOCK); /* stdout */

 

    while (1) {

        n = read(0, buffer, 4096);

        if (n >= 0)

            m = write(1, buffer, n);

        if ((n < 0 || m < 0) && (errno != EAGAIN))

            break;

        sleep(delay);

    }

    perror(n < 0 ? "stdin" : "stdout");

    exit(1);

}

 

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

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