Системный вызов 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. Это означает, например, что исходный файл не может быть сокетом. Файл назначения может быть и сокетом, и это обстоятельство широко используется приложениями.
Пример
Рассмотрим простой пример работы с системным вызовом sendfile. В листинге ниже приведен текст программы 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; /* смещение от начала исходного файла */
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, после выполнения копирования, файлы закрываются.
15. Функции для преобразования из хостового порядка байт в сетевой и наоборот в стандарте POSIX.
Выше было отмечено, что преобразование значений типов uint16_t и uint32_t из хостового порядка байт в сетевой выполняется посредством функций htons() и htonl(); функции ntohs() и ntohl() осуществляют обратную операцию (см. листинг 11.6).
#include <arpa/inet.h>
uint32_t htonl (uint32_t hostlong);
uint16_t htons (uint16_t hostshort);
uint32_t ntohl (uint32_t netlong);
uint16_t ntohs (uint16_t netshort);
16. Функции для работы с базой данных узлов сети в стандарте POSIX.
Данные о хостах как узлах сети хранятся в сетевой базе, последовательный доступ к которой обслуживается функциями sethostent(), gethostent() и endhostent()
#include <netdb.h>
void sethostent (int stayopen);
struct hostent *gethostent (void);
void endhostent (void);
Описание функций последовательного доступа к сетевой базе данных о хостах - узлах сети.
Функция sethostent() устанавливает соединение с базой, остающееся открытым после вызова gethostent(), если значение аргумента stayopen отлично от нуля. Функция gethostent() последовательно читает элементы базы, возвращая результат в структуре типа hostent, содержащей по крайней мере следующие поля.
|
|
char *h_name;
/* Официальное имя хоста */
char **h_aliases;
/* Массив указателей на альтернативные */
/* имена хоста, завершаемый пустым */
/* указателем */
int h_addrtype;
/* Тип адреса хоста */
int h_length;
/* Длина в байтах адреса данного типа */
char **h_addr_list;
/* Массив указателей на сетевые адреса */
/* хоста, завершаемый пустым указателем */
Функция endhostent() закрывает соединение с базой.
В пример показана программа, осуществляющая последовательный просмотр сетевой базы данных о хостах - узлах сети,
#include <stdio.h>
#include <netdb.h>
int main (void) {
struct hostent *pht;
char *pct;
int i, j;
sethostent (1);
while ((pht = gethostent ())!= NULL) {
printf ("Официальное имя хоста: %s\n", pht->h_name);
printf ("Альтернативные имена:\n");
for (i = 0; (pct = pht->h_aliases [i])!= NULL; i++) {
printf (" %s\n", pct);
}
printf ("Тип адреса хоста: %d\n", pht->h_addrtype);
printf ("Длина адреса хоста: %d\n", pht->h_length);
printf ("Сетевые адреса хоста:\n");
for (i = 0; (pct = pht->h_addr_list [i])!= NULL; i++) {
for (j = 0; j < pht->h_length; j++) {
printf (" %d", (unsigned char) pct [j]);
}
printf ("\n");
}
}
endhostent ();
return 0;
}
17. Функции для работы с базой данных сетевых сервисов в стандарте POSIX.
Еще одно проявление той же логики работы - база данных сетевых сервисов
#include <netdb.h>
void setservent (int stayopen);
struct servent *getservent (void);
struct servent *getservbyname
(const char *name, const char *proto);
struct servent *getservbyport
(int port, const char *proto);
void endservent (void);
Листинг 11.16. Описание функций доступа к базе данных сетевых сервисов.
Обратим внимание на то, что в данном случае можно указывать второй аргумент поиска - имя протокола. Впрочем, значение аргумента proto может быть пустым указателем, и тогда поиск производится только по имени сервиса (функция getservbyname()) или номеру порта (getservbyport()), который должен быть задан с сетевым порядком байт.
Структура типа servent содержит по крайней мере следующие поля.
char *s_name;
/* Официальное имя сервиса */
char **s_aliases;
/* Массив указателей на альтернативные */
/* имена сервиса, завершаемый пустым */
/* указателем */
int s_port;
/* Номер порта, соответствующий сервису */
/* (в сетевом порядке байт) */
char *s_proto;
/* Имя протокола для взаимодействия с */
|
|
/* сервисом */
В пример 11.17 приведен пример программы, использующей функции доступа к базе данных сервисов, а также функции преобразования между хостовым и сетевым порядками байт
#include <stdio.h>
#include <netdb.h>
int main (void) {
struct servent *pht;
char *pct;
int i;
setservent (1);
while ((pht = getservent ())!= NULL) {
printf ("Официальное имя сервиса: %s\n", pht->s_name);
printf ("Альтернативные имена:\n");
for (i = 0; (pct = pht->s_aliases [i])!= NULL; i++) {
printf (" %s\n", pct);
}
printf ("Номер порта: %d\n", ntohs ((in_port_t) pht->s_port));
printf ("Имя протокола: %s\n\n", pht->s_proto);
}
if ((pht = getservbyport (htons ((in_port_t) 21), "udp"))!= NULL) {
printf ("Официальное имя сервиса: %s\n", pht->s_name);
printf ("Альтернативные имена:\n");
for (i = 0; (pct = pht->s_aliases [i])!= NULL; i++) {
printf (" %s\n", pct);
}
printf ("Номер порта: %d\n", ntohs ((in_port_t) pht->s_port));
printf ("Имя протокола: %s\n\n", pht->s_proto);
} else {
perror ("GETSERVBYPORT");
}
if ((pht = getservbyport (htons ((in_port_t) 21), (char *) NULL))!= NULL) {
printf ("Официальное имя сервиса: %s\n", pht->s_name);
printf ("Альтернативные имена:\n");
for (i = 0; (pct = pht->s_aliases [i])!= NULL; i++) {
printf (" %s\n", pct);
}
printf ("Номер порта: %d\n", ntohs ((in_port_t) pht->s_port));
printf ("Имя протокола: %s\n\n", pht->s_proto);
} else {
perror ("GETSERVBYPORT");
}
endservent ();
return 0;
}
18. Функции для работы с базой данных сетевых протоколов в стандарте POSIX.
Точно такой же программный интерфейс предоставляет база данных сетевых протоколов
#include <netdb.h>
void setprotoent (int stayopen);
struct protoent *getprotoent (void);
struct protoent *getprotobyname
(const char *name);
struct protoent *getprotobynumber (int proto);
void endprotoent (void);
Описание функций доступа к базе данных сетевых протоколов.
Структура типа protoent содержит по крайней мере следующие поля.
char *p_name;
/* Официальное имя протокола */
char **p_aliases;
/* Массив указателей на альтернативные */
/* имена протокола, завершаемый пустым */
/* указателем */
int p_proto;
/* Номер протокола */
В пример 11.14 показан пример программы, осуществляющей последовательный и случайный доступ к базе данных сетевых протоколов
#include <stdio.h>
#include <netdb.h>
int main (void) {
struct protoent *pht;
char *pct;
int i;
setprotoent (1);
while ((pht = getprotoent ())!= NULL) {
printf ("Официальное имя протокола: %s\n", pht->p_name);
printf ("Альтернативные имена:\n");
for (i = 0; (pct = pht->p_aliases [i])!= NULL; i++) {
printf (" %s\n", pct);
}
printf ("Номер протокола: %d\n\n", pht->p_proto);
}
if ((pht = getprotobyname ("ipv6"))!= NULL) {
printf ("Номер протокола ipv6: %d\n\n", pht->p_proto);
} else {
fprintf (stderr, "Протокол ip в базе не найден\n");
}
if ((pht = getprotobyname ("IPV6"))!= NULL) {
printf ("Номер протокола IPV6: %d\n\n", pht->p_proto);
} else {
fprintf (stderr, "Протокол IPV6 в базе не найден\n");
}
endprotoent ();
return 0;
}
Листинг 11.14. Пример программы, осуществляющей последовательный и случайный доступ к базе данных сетевых протоколов.
19. Функции для работы с базой данных сетей в стандарте POSIX.
Наряду с базой данных хостов (узлов сети) поддерживается база данных сетей с аналогичной логикой работы и набором функций (см. пример 11.12).
#include <netdb.h>
void setnetent (int stayopen);
struct netent *getnetent (void);
struct netent *getnetbyaddr (uint32_t net,
int type);
struct netent *getnetbyname (const char *name);
void endnetent (void);
Листинг 11.12. Описание функций доступа к базе данных сетей.
Функция getnetent() обслуживает последовательный доступ к базе, getnetbyaddr() осуществляет поиск по адресному семейству (аргумент type) и номеру net сети, а getnetbyname() выбирает сеть с заданным (официальным) именем. Структура типа netent, указатель на которую возвращается в качестве результата этих функций, согласно стандарту POSIX-2001, должна содержать по крайней мере следующие поля.
char *n_name;
/* Официальное имя сети *
char **n_aliases;
/* Массив указателей на альтернативные */
/* имена сети, завершаемый пустым указателем */
int n_addrtype;
/* Адресное семейство (тип адресов) сети */
uint32_t n_net;
/* Номер сети (в хостовом порядке байт) */
20. Программирование на уровне TLI. Функции установления связи.
Интерфейс транспортного уровня (TLI) был разработан как альтернатива более раннему socket-интерфейсу. Он базируется на средстве ввода-вывода STREAMS, первоначально реализованном в версиях System V операционной системы UNIX. Основное достоинство STREAMS заключается в гибкой, управляемой пользователем многослойности модулей, по конвейерному принципу обрабатывающих информацию, передаваемую от прикладной программы к физической среде хранения/пересылки и обратно. Это делает STREAMS удобным инструментом для реализации стеков протоколов сетевого взаимодействия различной архитектуры (OSI, TCP/IP, DECnet, SNA, XNS и т.п.).
Хотя все современные реализации и версии ОС UNIX поддерживают socket-интерфейс по крайней мере для TCP/IP, для вновь разрабатываемых сетевых приложений настоятельно рекомендуется использовать TLI, что обеспечит их независимость от используемых сетевых протоколов.
С точки зрения прикладного программиста логика TLI очень похожа на логику socket-интерфейса (даже имена функций первого образованы от имен системных вызовов второго добавлением префикса "t_"). TLI реализован в виде библиотеки функций языка программирования СИ, разделенных (как и в случае с socket-интерфейсом) на четыре группы:
локального управления;
установления связи;
обмена данными (ввода/вывода);
закрытия связи.
Основу концепции TLI составляют три базовых понятия:
поставщик транспортных услуг
пользователь транспорта
транспортная точка.
Поставщиком транспортных услуг (transport provider) называется набор модулей, реализующих какой-либо конкретный стек протоколов сетевого взаимодействия (в данном учебном пособии - TCP/IP) и обеспечивающий сервис транспортного уровня модели OSI [REF].
Пользователем транспорта (transport user) является любая прикладная программа, использующая сервис, предоставляемый ПТС на локальном узле сети.
Транспортная точка (transport endpoint) - абстрактное понятие (аналогичное socket'у), используемое для обозначения канала связи между пользователем транспорта и поставщиком транспортных услуг на локальном узле сети. Транспортная точка имеем уникальный для всей сети транспортный адрес (для сетей TCP/IP этот адрес образуется триадой: адрес узла сети, номер порта, используемый протокол транспортного уровня). Для ссылки на транспортные точки в функциях TLI используются их дескрипторы, подобные дескрипторам обычных файлов и socket'ов ОС UNIX.