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

Системный вызов 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.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: