используем системный вызов sendfile

Системный вызов sendfile — это относительно недавнее введение в ядро Linux, предлагающее значительные улучшения производительности приложений вроде FTP- и веб-серверов, которым необходима эффективная передача файлов. Sendfile позволяет производить высокоскоростную передачу данных по сети, что как раз и необходимо для таких приложений, как веб и FTP-серверы. Если вы являетесь разработчиком подобных приложений, вы можете воспользоваться новым системным вызовом, чтобы получить существенный прирост производительности. Вне серверной области данная возможность также представляет интерес и, возможно, вы найдете ее полезной для чего-либо еще.

В данной статье будет показано, как можно использовать sendfile, и приведены примеры программ.

общая теория

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

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


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

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

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

Реализация sendfile варьируется на различных операционных системах. Далее речь пойдет о Linux-версии. Кстати, обратите внимание, что существует утилита передачи файлов sendfile, которая не имеет абсолютно ничего общего с системным вызовом sendfile.

подробный осмотр

Для того чтобы использовать sendfile, включите заголовочный файл, декларирующий следующий прототип:

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 — простой программы для копирования файлов. Для упрощения приведенный исходный код немного сокращен.

Listing 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; /* смещение в байтах для sendfile */
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 /*копирование файла с помощью sendfile */
14 sendfile (dest, src, &offset, stat_buf.st_size);

15 /* очистка и выход*/
16 close(dest);
17 close(src);
18 }

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

пример 2

Первый пример был простым, но не раскрывающим типичной функции sendfile — передачи данных в сеть. Следующий пример показывает использование sendfile с сетевым сокетом. Исходный текст приведен ниже:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

int main(int argc, char **argv)
{
int port = 1234; /* номер порта */
int sock; /* дескриптор сокета */
int desc; /* файловый дескриптор для сокета */
int fd; /* файловый дескриптор для посылаемого файла */
struct sockaddr_in addr; /* параметры сокета для вызова bind */
struct sockaddr_in addr1; /* параметры сокета для вызова accept */
int addrlen; /* аргумент для accept */
struct stat stat_buf; /* аргумент для fstat */
off_t offset = 0; /* смещение */
char filename[PATH_MAX]; /* имя посылаемого файла */
int rc; /* хранит коды возврата системных вызовов */

/* проверяем аргументы коммандной строки, содержащие, в частности опциональный номер порта */
if (argc == 2) {
port = atoi(argv[1]);
if (port<= 0) {
fprintf(stderr, "invalid port: %s\n", argv[1]);
exit(1);
}
} else if (argc != 1) {
fprintf(stderr, "usage: %s [port]\n", argv[0]);
exit(1);
}

/* создаем сокет */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
fprintf(stderr, "unable to create socket: %s\n", strerror(errno));
exit(1);
}

/* заполняем структуру сокета */
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);

/* привязываем сокет к порту (bind) */
rc = bind(sock, (struct sockaddr *)&addr, sizeof(addr));
if (rc == -1) {
fprintf(stderr, "unable to bind to socket: %s\n", strerror(errno));
exit(1);
}

/* слушаем сокет */
rc = listen(sock, 1);
if (rc == -1) {
fprintf(stderr, "listen failed: %s\n", strerror(errno));
exit(1);
}

while (1) {

/* обрабатываем подключение клиента */
addrlen = sizeof(addr);
desc = accept(sock, (struct sockaddr *) &addr1, &addrlen);
if (desc == -1) {
fprintf(stderr, "accept failed: %s\n", strerror(errno));
exit(1);
}

/* получаем имя файла от клиента */
rc = recv(desc, filename, sizeof(filename), 0);
if (rc == -1) {
fprintf(stderr, "recv failed: %s\n", strerror(errno));
exit(1);
}

/* вырезаем \r и \n из имени файла */
filename[rc] = '\0';
if (filename[strlen(filename)-1] == '\n')
filename[strlen(filename)-1] = '\0';
if (filename[strlen(filename)-1] == '\r')
filename[strlen(filename)-1] = '\0';

/* заканчиваем работу. если имя файла "quit" */
if (strcmp(filename, "quit") == 0) {
fprintf(stderr, "quit command received, shutting down server\n");
break;
}

fprintf(stderr, "received request to send file %s\n", filename);

/* открываем посылаемый файл*/
fd = open(filename, O_RDONLY);
if (fd == -1) {
fprintf(stderr, "unable to open '%s': %s\n", filename, strerror(errno));
exit(1);
}

/* считываем статисктику файла */
fstat(fd, &stat_buf);

/* копируем файл при помощи sendfile */
offset = 0;
rc = sendfile (desc, fd, &offset, stat_buf.st_size);
if (rc == -1) {
fprintf(stderr, "error from sendfile: %s\n", strerror(errno));
exit(1);
}
if (rc != stat_buf.st_size) {
fprintf(stderr, "incomplete transfer from sendfile: %d of %d bytes\n",
rc,
(int)stat_buf.st_size);
exit(1);
}

/* закрываем дексриптор посланного файла*/
close(fd);

/* закрываем дескриптор сокета */
close(desc);
}

/* закрываем сокет */
close(sock);
return 0;
}

Программа является сервером и делает следующее: слушает сетевой сокет и ждет, пока не подключится клиент. После подключения она ждет пока клиент не пришлет имя файла. Затем она отсылает указанный файл клиенту с использованием sendfile. Наконец, сервер отключает клиента и ждет следующего подключения. Если вы не знакомы с принципами сетевого программирования под UNIX, советую вам прочесть книгу автора Richard Stevens: «UNIX Network Programming».

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

После этого сервер должен продолжать работу, так что вы сможете подключиться к нему снова. Если в качестве имени файла вы введете «quit», то сервер должен завершить работу.
Приведенный пример является сильно упрощенным. Он может одновременно работать только с одним клиентом, и проводить простую проверку ошибок, завершая работу при их обнаружении. Существует также множество способов поднять производительность на уровне TCP, но они выходят за рамки данной статьи.

Наконец, после рассмотрения sendfile, я хотел бы задать читателю вопрос для размышления: почему не существует соответствующего системного вызова receivefile?



Джеф Трэнтер, перевод Дмитрия Герусса.


Сетевые решения. Статья была опубликована в номере 09 за 2005 год в рубрике программирование

©1999-2024 Сетевые решения