Системный вызов sendfile

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

[Jeff Tranter. Перевод: Андрей Киселев]

Системный вызов sendfile

Автор: Jeff Tranter
Перевод: Андрей Киселев

Введение

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

История вопроса

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

    открыть исходный файл (на диске)
    открыть файл назначения (сетевое соединение)
    пока файл не передан:
      прочитать блок данных из исходного файла в буфер
      записать данные из буфера в файл назначения
    закрыть оба файла

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

Если следовать вышеприведенному алгоритму, то получается так, что данные копируются несколько раз, прежде чем они "уйдут" в сеть. Каждый раз, когда вызывается read, данные копируются с жесткого диска в буфер ядра (обычно посредством DMA). Затем буфер копируется в буфер приложения. Затем вызывается write и данные из буфера приложения опять копируются в буфер ядра и лишь потом этот буфер отправляется в сеть. Каждый раз, когда приложение обращается к системному вызову, происходит переключение контекста между пользовательским режимом и режимом ядра, а это весьма "дорогостоящая" операция. И чем больше в программе будет обращений к системным вызовам read и write, тем больше времени будет потрачено на выполнение переключений контекста исполнения.

Операции копирования данных из области ядра в область приложения и обратно, в данном случае, излишни, поскольку сами данные в приложении не изменяются и не анализируются. Многие операционные системы, такие как Windows NT, FreeBSD и Solaris предоставляют в распоряжение программиста системный вызов, который выполняет передачу файла за одно обращение. Ранние версии Linux часто критиковали за отсутствие подобной возможности, в результате, начиная с версии 2.2.x, такой вызов появился. Теперь он широко используется такими серверными приложениями как Apache и Samba.

Реализация sendfile различна для разных операционных систем. Поэтому, в данной статье мы будем говорить о версии sendfile в Linux. Обратите внимание: утилита sendfile не то же самое, что системный вызов sendfile.

Подробное описание

Чтобы использовать sendfile в своих программах, вы должны подключить заголовочный файл <sys/sendfile.h>, в котором находится описание прототипа функции-вызова:

    ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
Функция принимает следующие входные параметры:
out_fd
файловый дескриптор файла назначения, открытого на запись. В этот файл производится запись данных
in_fd
файловый дескриптор исходного файла, открытого на чтение. Из этого файла читаются данные
offset
смещение от начала исходного файла, с этой точки будет начата передача данных (т.е. значение 0 соответствует началу файла). Это значение изменяется в процессе работы функции и ваше приложение получит его в измененном виде после того, как функция вернет управление.
count
количество байт, которое необходимо передать
В случае успеха функция возвращает количество переданных байт, и -1 -- в случае ошибки.

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

Пример 1

Рассмотрим простой пример работы с системным вызовом sendfile. В листинге ниже приведен текст программы fastcp.c, которая выполняет простое копирование файла.

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


Листинг 1: fastcp.c

1     int main(int argc, char **argv) {
2         int src;               /* дескриптор исходного файла */
3         int dest;              /* дескриптор файла назначения */
4         struct stat stat_buf;  /* сведения об исходном файле */
5         off_t offset = 0;      /* смещение от начала исходного файла */
6
7         /* проверить -- существует ли исходный файл и открыть его */
8         src = open(argv[1], O_RDONLY);

9         /* запросить размер исходного файла и права доступа к нему */
10        fstat(src, &stat_buf);

11        /* открыть файл назначения */
12        dest = open(argv[2], O_WRONLY|O_CREAT, stat_buf.st_mode);

13        /* скопировать файл */
14        sendfile (dest, src, &offset, stat_buf.st_size);

15        /* закрыть файлы и выйти */
16        close(dest);
17        close(src);
18    }

В строке 8 открывается исходный файл, имя которого передается программе, как первый аргумент командной строки. В строке 10 программа получает дополнительные сведения о файле, с помощью fstat, таким образом мы получаем длину файла и права доступа к нему, которые понадобятся нам позднее. В строке 12 открывается на запись файл назначения. В строке 14 производится вызов sendfile, которому передаются файловые дескрипторы, смещение от начала исходного файла (в данном случае -- 0) и количество байт для копирования, которое соответствует размеру исходного файла. И в строках 16 и 17, после выполнения копирования, файлы закрываются.

Попробуйте скомпилировать полную версию программы и поэкспериментируйте с файлами различного типа и посмотрите -- как работает с ними вызов sendfile:

  • скопируйте дисковый файл в другой дисковый файл
  • попробуйте выполнить копирование на другой раздел или диск
  • смонтированный CD-ROM в файл
  • дисковый файл на устройство /dev/null или /dev/full
  • из устройства /dev/zero или /dev/null в дисковый файл
  • дисковый файл на дискету (устройство /dev/fd0)

Пример 2

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

Программа называется server и выполняет следующие действия:

  • Ожидает подключение клиента.
  • После подключения ожидает пока клиент не передаст имя файла.
  • Передает запрошенный файл клиенту с помощью sendfile.
  • Завершает соединение и переходит в ожидание следующего подключения.
Я надеюсь, что вы, уважаемый читатель, уже знакомы с основами написания сетевых приложений. Если нет, то от себя могу порекомендовать прекрасную книгу Ричарда Стивенса (Richard Stevens) UNIX Network Programming.

Программа "server" ожидает подключений на порту 1234. Номер порта выбран совершенно случайно и для себя вы можете выбрать другой номер, указав его в командной строке. Запустите программу командой "./server". В качестве клиентского приложения можно использовать программу telnet. Запустите ее из другой консоли, не забыв при этом указать имя хоста и номер порта (например, "telnet localhost 1234"). После появления сообщения об установке соединения, введите имя существующего файла, например /etc/hosts. Программа server должна передать содержимое файла клиенту и закрыть соединение.

По завершении сеанса связи программа server остается работать, в результате вы опять сможете подключиться к ней и запросить другой файл. Если программе передать в качестве имени файла слово "quit", то она завершит свою работу. Если в вашем распоряжении есть две машины, объединенные сетью, то можете попробовать выполнить передачу файлов через сеть.

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

Итоги

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

В заключение обсуждения системного вызова sendfile, я предлагаю вам ответить на вопрос: "Почему нет соответствующего вызова с именем receivefile?".

Ссылки

  1. Страницы справочного руководства "man sendfile(2)".
  2. Исходные тексты реализации sendfile.

Джефф (Jeff) пишет о Linux и для Linux начиная с 1992 года. Он работает в корпорации Xandros в столице Канады -- городе Оттава.


Copyright © 2003, Jeff Tranter. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 91 of Linux Gazette, June 2003
Перевод можно найти по адресу: http://gazette.linux.ru.net/lg91/tranter.html

[ опубликовано 18/08/2004 ]

Jeff Tranter. Перевод: Андрей Киселев - Системный вызов sendfile   Версия для печати