Приложения-серверы, такие как 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);Функция принимает следующие входные параметры:
В Linux файловый дескриптор может соответствовать как обычному файлу так и устройству, например -- сокету. На сегодняшний день, реализация sendfile требует, чтобы исходный файловый дескриптор соответствовал обычному файлу или устройству, поддерживаемому mmap. Это означает, например, что исходный файл не может быть сокетом. Файл назначения может быть и сокетом, и это обстоятельство широко используется приложениями.
Рассмотрим простой пример работы с системным вызовом 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:
Первый пример достаточно прост, но он не дает представления о том, как выполнять копирование в сокет. Второй пример показывает как выполняется операция передачи файла через сеть. Текст программы достаточно велик, поскольку в данном случае она включает код инициализации и работы с сокетами, и потому он не включен в текст статьи. Полный текст примера вы сможете взять здесь.
Программа называется server и выполняет следующие действия:
Программа "server" ожидает подключений на порту 1234. Номер порта выбран совершенно случайно и для себя вы можете выбрать другой номер, указав его в командной строке. Запустите программу командой "./server". В качестве клиентского приложения можно использовать программу telnet. Запустите ее из другой консоли, не забыв при этом указать имя хоста и номер порта (например, "telnet localhost 1234"). После появления сообщения об установке соединения, введите имя существующего файла, например /etc/hosts. Программа server должна передать содержимое файла клиенту и закрыть соединение.
По завершении сеанса связи программа server остается работать, в результате вы опять сможете подключиться к ней и запросить другой файл. Если программе передать в качестве имени файла слово "quit", то она завершит свою работу. Если в вашем распоряжении есть две машины, объединенные сетью, то можете попробовать выполнить передачу файлов через сеть.
Обратите внимание: данная реализация сервера очень упрощена -- программа может работать только с одним клиентом одновременно, а проверка на ошибки очень поверхностна. Кроме того, отсутствует оптимизация работы с уровнем TCP, поскольку данный вопрос далеко выходит за рамки статьи.
Системный вызов sendfile значительно повышает скорость передачи файлов по сети и широко используется сетевыми приложениями, такими как ftp и web серверы. Если вы занимаетесь разработкой серверных приложений, то вам определенно пригодится этот системный вызов. Если нет -- то при творческом подходе к проблеме, вы сможете самостоятельно найти весьма интересные применения этому вызову.
В заключение обсуждения системного вызова sendfile, я предлагаю вам ответить на вопрос: "Почему нет соответствующего вызова с именем receivefile?".
Джефф (Jeff) пишет о Linux и для Linux начиная с 1992 года. Он работает в корпорации Xandros в столице Канады -- городе Оттава.