read и write

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

Методы read и write выполняют аналогичные задачи, то есть копирование данных из и в код приложения. Таким образом, их прототипы очень похожи и стоит представить их в одно время:

 

ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);

ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);

 

Для обоих методов filp является указателем на file, а count - это размер запрашиваемой передачи данных. Аргумент buff указывает на пользовательский буфер данных, удерживающий данные, которые будут записаны, или пустой буфер, в котором должны быть размещены вновь считанные данные. Наконец, offp является указателем на объект "типом длинного смещения" (“long offset type”), который указывает на позицию файла, к которой обращается пользователь. Возвращаемое значение является "типом знакового размера" (“signed size type”); его использование будет рассмотрено позже.

 

Давайте повторим, что аргумент buff для методов read и write является указателем пространства пользователя. Поэтому он не может быть непосредственно разыменовываться кодом ядра. Есть несколько причин для этого ограничения:

 

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

Даже если указатель означает что-то в пространстве ядра, память пользовательского пространства организована странично и память в запросе может не находиться в ОЗУ, когда сделан этот системный вызов. Попытка сослаться на память пользовательского пространства непосредственно может сгенерировать ошибку страничного доступа, это то, что код ядра не разрешает делать. Результатом будет "Ой" (“oops”), что приведёт к гибели процесса, который сделал этот системный вызов.
 

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

 

Очевидно, что ваш драйвер должен иметь возможность доступа в буфер пользовательского пространства для того, чтобы выполнить свою работу. Однако, чтобы быть безопасным, этот доступ должен всегда быть выполнен через специальные функции, поставляемые для того ядром. Приведём некоторые из этих функций (которые определены в <asm/uaccess.h>) здесь, а остальные в разделе "Использование аргумента ioctl" в Главе 6; они используют некоторую особую, архитектурно-зависимую магию, чтобы гарантировать, что передача данных между ядром и пользовательским пространством происходит безопасным и правильным способом.

 

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

 

unsigned long copy_to_user(void __user *to,

                           const void *from,

                           unsigned long count);

unsigned long copy_from_user(void *to,

                             const void __user *from,

                             unsigned long count);

 

Хотя эти функции ведут себя как нормальные функции memcpy, следует применять немного дополнительной осторожности, когда из кода ядра адресуется пространство пользователя. Адресуемые пользовательские страницы могут не быть в настоящее время в памяти и подсистема виртуальной памяти может отправить процесс в спячку, пока страница не будет передана на место. Это происходит, например, когда страница должна быть получена из свопа. Конечный результат для автора драйвера в том, что любая функция, которая осуществляет доступ к пространству пользователя, должна быть повторно-входимой, должна быть в состоянии выполняться одновременно с другими функциями драйвера, и, в частности, должна быть в состоянии, когда она может законно спать. Мы вернемся к этому вопросу в Главе 5.

 

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

 

Тема доступа в пространство пользователя и недействительных указателей пространства пользователя более широка и обсуждается в Главе 6. Однако, стоит заметить, что если вам не требуется проверять указатель пользовательского пространства, вы можете вызвать взамен __copy_to_user и __copy_from_user. Это полезно, например, если вы знаете, что уже проверили аргумент. Тем не менее, будьте осторожны, если в действительности вы не проверяете указатель пользовательского пространства, который вы передаёте в эти функции, вы можете создать аварии ядра и/или дыры в системе безопасности.

 

Что же касается самих методов устройства, задача метода read - скопировать данные из устройства в пространство пользователя (используя copy_to_user), а метод write должен скопировать данные из пространства пользователя на устройство (с помощью copy_from_user). Каждый системный вызов read или write запрашивает передачу определённого числа байт, но драйвер может свободно передать меньше данных - точные правила немного отличаются для чтения и записи и описаны далее в этой главе.

 

Независимо от количества переданных методами данных, обычно они должны обновить позицию файла в *offp, представляющего текущую позицию в файле после успешного завершения системного вызова. Затем когда требуется, ядро передаёт изменение позиции файла обратно в структуру file. Однако, системные вызовы pread и pwrite имеют различную семантику; они работают от заданного смещения файла и не изменяют позицию файла, видимую любым другим системным вызовом. Эти вызовы передают указатель на позицию, заданную пользователем, и отменяют изменения, которые делает ваш драйвер. Рисунок 3-2 показывает, как использует свои аргументы типичная реализация read.

 

Рисунок 3-2. Аргументы для чтения

Рисунок 3-2. Аргументы для чтения

 

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

 

Хотя функции ядра возвращают отрицательное число как сигнал об ошибке, а значение числа показывает вид произошедшей ошибки (как рассказывалось в Главе 2), программы, которые выполняются в пользовательском пространстве, всегда видят -1 в качестве возвращаемого ошибочного значения. Они должны получить доступ к переменной errno, чтобы выяснить, что случилось. Поведение в пользовательском пространстве диктуется стандартом POSIX, но этот стандарт не предъявляет требований к внутренним операциям ядра.

Метод read

Возвращаемое значение для read интерпретируется вызывающей прикладной программой:

 

Если значение равно аргументу count, переданному системному вызову read, передано запрашиваемое количество байт. Это оптимальный случай.
 

Если число положительное, но меньше, чем count, только часть данных была передана. Это может произойти по ряду причин, в зависимости от устройства. Чаще всего прикладная программы повторяет чтение. Например, если вы читаете с помощью функции fread, библиотечная функция повторяет системный вызов до завершения запрошенной передачи данных.
 

Если значение равно 0, был достигнут конец файла (и данные не были прочитаны).
 

Отрицательное значение означает, что произошла ошибка. Значение указывает, какая была ошибка согласно <linux/errno.h>. Типичные возвращаемые значения ошибки включают -EINTR (прерванный системный вызов) или -EFAULT (плохой адрес).

 

Что отсутствует в этом списке, так это случай "нет данных, но они могут прийти позже". В этом случае системный вызов read должен заблокироваться. Мы будем работать с блокирующим вводом в Главе 6.

 

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

 

Если текущая позиция чтения больше размера устройства, метод read драйвера scull возвращает 0, чтобы сигнализировать, что данных больше нет (другими словами, мы в конце файла). Эта ситуация может произойти, если процесс А читает устройство в то время, как процесс Б открывает его для записи, тем самым укорачивая размер устройства до 0. Процесс вдруг обнаруживает себя в конце файла и следующий вызов read возвращает 0.

 

Вот код для read (игнорируйте сейчас вызовы down_interruptible и up, мы узнаем о них в следующей главе):

 

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

{

    struct scull_dev *dev = filp->private_data;

    struct scull_qset *dptr; /* первый списковый объект */

    int quantum = dev->quantum, qset = dev->qset;

    int itemsize = quantum * qset; /* как много байт в списковом объекте */

    int item, s_pos, q_pos, rest;

    ssize_t retval = 0;

 

    if (down_interruptible(&dev->sem))

        return -ERESTARTSYS;

    if (*f_pos >= dev->size)

        goto out;

    if (*f_pos + count > dev->size)

        count = dev->size - *f_pos;

 

    /* найти списковый объект, индекс qset, и смещение в кванте */

    item = (long)*f_pos / itemsize;

    rest = (long)*f_pos % itemsize;

    s_pos = rest / quantum; q_pos = rest % quantum;

 

    /* следовать за списком до правой позиции (заданной где-то) */

    dptr = scull_follow(dev, item);

 

    if (dptr == NULL || !dptr->data || !dptr->data[s_pos])

        goto out; /* не заполнять пустые пространства */

 

    /* читать только до конца этого кванта */

    if (count > quantum - q_pos)

        count = quantum - q_pos;

 

    if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {

        retval = -EFAULT;

        goto out;

    }

    *f_pos += count;

    retval = count;

 

out:

    up(&dev->sem);

    return retval;

}

 

Присвоения данных для 'quantum' и 'qset' (3-я строка) должны быть защищены семафором, чтобы избежать следующих (правда, маловероятных) состояний гонок:

 

- Данное устройство открыто для чтения каким-либо процессом.

- Кто-то изменяет значение 'quantum' с помощью ioctl().

- Начинается чтение из устройства, читается его значение 'quantum', но семафор ещё не захвачен.

- Устройство открывается вторым процессом в режиме O_WRONLY, вызывая его повторную инициализацию, что приводит к изменению значения 'quantum' устройства.

- Этот процесс пишет в устройство scull.

- Возобновление первого чтения.

Метод write

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

 

Если значение равно count, передано запрашиваемое количество байт.

Если число положительное, но меньше, чем count, была передана только часть данных. Программа, скорее всего, повторит запись остальных данных.

Если значение равно 0, ничего не записано. Этот результат не является ошибкой и нет никаких оснований для возвращения кода ошибки. И снова стандартная библиотека повторит вызов write. Мы рассмотрим точное значение этого случая в Главе 6, где познакомимся с блокирующей записью.

Отрицательное значение означает ошибку; как и для read, допустимые значения ошибок определены в <linux/errno.h>.

 

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

 

Код scull для write выполняется с одним квантом за раз, так же, как это делает метод read:

 

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

{

    struct scull_dev *dev = filp->private_data;

    struct scull_qset *dptr;

    int quantum = dev->quantum, qset = dev->qset;

    int itemsize = quantum * qset;

    int item, s_pos, q_pos, rest;

    ssize_t retval = -ENOMEM; /* значение используется в инструкции "goto out" */

 

    if (down_interruptible(&dev->sem))

        return -ERESTARTSYS;

 

    /* найти списковый объект, индекс qset, и смещение в кванте */

    item = (long)*f_pos / itemsize;

    rest = (long)*f_pos % itemsize;

    s_pos = rest / quantum; q_pos = rest % quantum;

 

    /* следовать за списком до правой позиции */

    dptr = scull_follow(dev, item);

    if (dptr = = NULL)

        goto out;

    if (!dptr->data) {

        dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);

        if (!dptr->data)

            goto out;

        memset(dptr->data, 0, qset * sizeof(char *));

    }

    if (!dptr->data[s_pos]) {

        dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);

        if (!dptr->data[s_pos])

            goto out;

    }

    /* записать только до конца этого кванта */

    if (count > quantum - q_pos)

        count = quantum - q_pos;

    if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {

        retval = -EFAULT;

        goto out;

    }

    *f_pos += count;

    retval = count;

 

    /* обновить размер */

    if (dev->size < *f_pos)

        dev->size = *f_pos;

 

out:

    up(&dev->sem);

    return retval;

}

Функции readv и writev

Системы Unix уже давно поддерживают два системных вызова, названных readv и writev. Эти "векторные" версии read и write берут массив структур, каждая из которых содержит указатель на буфер и значение длины. Вызов readv будет читать указанное количество в каждый буфер по порядку. writev, вместо этого, будет собирать вместе содержимое каждого буфера и передавать их наружу как одну операцию записи.

 

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

 

Прототипами векторных операций являются:

 

ssize_t (*readv) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);

ssize_t (*writev) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);

 

Здесь аргументы filp и ppos такие же, как для read и write. Структура iovec, определённая в <linux/uio.h>, выглядит следующим образом:

 

struct iovec

{

    void __user *iov_base;

    __kernel_size_t iov_len;

};

 

Каждая iovec описывает одну порцию данных для передачи; она начинается в iov_base (в пространстве пользователя) и имеет длину iov_len байт. Параметр count говорит методу, сколько существует структур iovec. Эти структуры созданы приложением, но ядро копирует их в пространство ядра до вызова драйвера.

 

Простейшей реализацией векторных операций будет прямой цикл, который просто передаёт адрес и длину каждой iovec функциям драйвера read или write. Часто, однако, эффективное и правильное поведение требует, чтобы драйвер делал что-то поумнее. Например, writev на ленточном накопителе должна сохранить содержимое всех структур iovec одной операцией записи на магнитную ленту.

 

Однако, многие драйверы не получили никакой выгоды от реализации самими этих методов. Таким образом, scull опускает их. Ядро эмулирует их с помощью read и write, и конечный результат тот же.

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