Высокопроизводительное сетевое программирование: Часть 2. Ускоряем обработку данных на клиенте и на сервере

В этой статье представлен ряд дополнительных приемов, предназначенных для UNIX®-программистов, желающих улучшить производительность своих сетевых приложений. Узнайте, как ускорить обработку данных на клиенте и на сервере с помощью вызова mmap, векторного ввода-вывода (scatter-gather) и других приемов.

Гириш Венкатачалам, консультант и популяризатор технологий Open Source, консультант

Photo of Girish VenkatachalamГириш Венкатачалам занимается UNIX программированием более десяти лет. Он разработал протокол IPSec для Nucleus - ОС для встроенных систем. Область его интересов включает в себя криптографию, мультимедиа, сети и встроенные системы. Он любит плавать, ездить на велосипеде, заниматься йогой и просто одержим фитнесом. С ним можно связаться по электронной почте girish1729@gmail.com.



16.12.2009

Введение

В части 1 этой серии (см. Ресурсы) обсуждались некоторые приемы оптимизации использования сети с помощью продвинутых методик программирования, таких как неблокирующий ввод/вывод. В этой статье показано еще несколько полезных приемов.

В наши дни большинство устройств хранения данных снабжены жесткими дисками. Будучи механическими устройствами, жесткие диски никогда не смогут приблизиться к скоростям первичных устройств хранения, таких как оперативная память или, в некоторых случаях, сеть. Рекомендуется использовать диски SCSI или SATA, так как они гораздо производительнее старых добрых IDE-дисков.

Также проследите, чтобы ваш диск использовал DMA для передачи данных.

Как программисту вам следует постараться найти способы уменьшения задержек при работе с диском. Два тесно связанных аспекта здесь - это минимизация издержек, связанных с переключением контекста системными вызовами, и минимизация издержек копирования (memcpy(2)). Полезным методом уменьшения издержек системных вызовов является использование разумных размеров TCP-буферов отправки и приема данных. Обычно в коде UNIX® используется значение 8192 или 0x8000.

Конечно, также вам следует:

  • Снижать нагрузку на клиентский и серверный процессоры, помещая дорогостоящий (по времени выполнения) код за пределы циклов.
  • Никогда не использовать sleep(2) или примитивы синхронизации.
  • Даже и не думать об использовании потоков для увеличения производительности сети.

Многофункциональный метод mmap(2)

Наверняка вы слышали о вводе/выводе с отображением в память и с отображением в порты ввода/вывода. Обычно они применяются в драйверах устройств и при работе с периферийными устройствами. С помощью системного вызова mmap(2) в UNIX-системах вы можете отображать первичную память так, чтобы она ссылалась непосредственно на файл на вторичном устройстве, таком как жесткий диск.

Системный вызов mmap(2) - весьма универсален, подобно, например, select(2), но в данном случае нас интересует увеличение производительности дискового ввода/вывода и уменьшение затрат памяти при копировании. Достигнуть и того и другого можно, применяя mmapp(2) вместо системных вызовов read(2)/fread(2) для чтения содержимого файла в основную память.

Вызов mmap(2) может дать заметный прирост в производительности, так как в нем отсутствует избыточное копирование в буфер и обратно. Однако семантика его использования не вполне очевидна. Прежде чем отобразить файл для записи с помощью mmap(2), необходимо выделить место в основной памяти с помощью вызова ftruncate(2), чтобы сообщить ядру, сколько места требуется. В листинге 1 показан подробный пример.

Листинг 1. Запись в файл с помощью mmap(2)
/**************************************************************/
/**************************************************************/

	/******************************************
	 *    запись в файл с помощью mmap(2)     *
	 *                                        *
	 *****************************************/
	caddr_t *mm = NULL;

	fd = open (filename, O_RDWR | O_TRUNC | O_CREAT, 0644);

	if(-1 == fd)
		errx(1, "File write");
	/* Ошибка. Дальше не идем */

	/* Если вы этого не сделаете, отображение с помощью
	 * mmap не будет работать для записи файлов.
	 * Если размер файла заранее неизвестен, как часто
	 * бывает с потоковыми данными в сети, здесь можно
	 * использовать заведомо большое значение.
	 * Когда весь файл будет записан, вы сможете 
	 * скорректировать его размер еще одним 
	 * вызовом ftruncate
	 */

	ret = ftruncate(ctx->fd,filelen);

	mm = mmap(NULL, header->filelen, PROT_READ | PROT_WRITE, 
			MAP_SHARED, ctx->fd, 0);
	if (NULL == mm)
		errx(1, "mmap() problem");

	memcpy(mm + off, buf, len);
	off += len;
	/* Не забывайте освобождать выделенную
       с помощью mmap(2) память! */  
	munmap(mm, filelen);
	close(fd);

/**************************************************************/
/**************************************************************/

	/******************************************
	 *   чтение из файла с помощью mmap(2)    *
	 *                                        *
	 *****************************************/
	fd = open(filename, O_RDONLY, 0);
	if ( -1 == fd) 
		errx(1, " File read err");
	/* Ошибка. Дальше не идем */

	fstat(fd, &statbf);
	filelen = statbf.st_size;

	mm = mmap(NULL, filelen, PROT_READ, MAP_SHARED, fd, 0);

	if (NULL == mm) 
		errx(1, "mmap() error");
	/* NOT REACHED */

	/* Теперь мы можем копировать данные напрямую из
	 * указателя mm, который указывает на файл с данными
	 */


	bufptr = mm + off;
	/* Мы можем скопировать отображенную память
	   напрямую в сетевой буфер для последующей отправки */

	memcpy(pkt.buf + filenameoff, bufptr, bytes);

	/* Не забывайте освобождать выделенную
       с помощью mmap(2) память! */
	munmap(mm, filelen);
	close(fd);

Можно использовать вызов mmap(2) для чтения данных для передачи их в сеть, а также для помещения их из сети в файловую систему на принимающей стороне.

Иногда mmap(2) помогает, но иногда может и навредить - например, если попробовать отобразить область памяти, находящуюся в NFS. Однако в большинстве ситуаций лучше использовать mmap(2) везде, где это возможно.

Реализация ввода/вывода по принципу scatter-gather с помощью вызовов readv или writev

В дополнение к mmap(2) для ускорения обработки данных на клиенте и сервере можно использовать технику scatter-gather (векторного ввода/вывода), также называемую uio. Вместо использования единственного массива байтов в качестве буфера можно манипулировать сразу целым массивом буферов, каждый из которых может указывать на отличный от других источник или пункт назначения данных. Эта техника имеет ограниченное применение, однако может быть полезна в трудный момент. Например, можно заполнить заголовочные поля (пакета) данными из различных источников без дополнительного копирования, напрямую с помощью вызова writev(2), вместо нескольких вызовов write(2), или одного write(2) и нескольких вызовов memcpy(2).

В листинге 2 приведен довольно сложный пример совместного использования uio и неблокирующего ввода/вывода.

Листинг 2. Нетривиальное использование uio совместно с неблокирующим вводом/выводом
writeiovall(int fd, struct iov *iov, int nvec) {

	int i, bytes;

	i = 0;
	while (i < nvec) {
		do
		{
			rv = writev(fd, &vec[i], nvec - i);
		} while (rv == -1 &&
				(errno == EINTR || errno == EAGAIN));

		if (rv == -1) {
			if (errno != EINTR && errno != EAGAIN) {
				perror("write");
			}
			return -1;
		}
		bytes += rv;
		/* пересчитываем vec чтобы обработать случаи 
		   частичной записи буфера */
		while (rv > 0) {
			if (rv < vec[i].iov_len) {
				vec[i].iov_base = (char *) vec[i].iov_base + rv;
				vec[i].iov_len -= rv;
				rv = 0;
			}
			else {
				rv -= vec[i].iov_len;
				++i;
			}
		}
	}

	/* мы должны попасть сюда только после того, как запишем всё, 
	   что есть */

	return 0;

}

В этом коде сокет может либо частично записать буфер ввода/вывода, либо записать несколько буферов полностью, либо несколько буферов полностью и один из буферов частично. Рисунок 1 иллюстрирует работу этого кода.

Figure 1. Применение техники uio с неблокирующими сокетами
Применение техники uio с неблокирующими сокетами

Другие продвинутые приемы

Код ядра BSD использует связанный список буферов, называемых mbuf. Вы можете прочитать о них на странице руководства man для mbuf(9) в любой BSD-системе, а также во множестве посвященных этой теме статей. Мы не будем подробно обсуждать их в этой статье, но все же кратко о них упомянем.

В сущности, mbuf-ы представляют собой связанный список буферов, каждый из которых имеет размер от 128 до 1024 байт, обычно около 128 байт.

Так как за очистку аппаратных буферов при поступлении данных на сетевой интерфейс отвечает ядро, подсистема буферов mbuf является очень эффективной при обработке данных из сети. Например, при неправильной настройке гигабитной сети нагрузка на ядро может быть очень большой.

Каждый буфер mbuf содержит в себе следующую важную информацию:

  • адрес начала блока данных
  • размер данных
  • тип данных

В них содержится еще много информации, но это - ключевые поля, помогающие обрабатывать данные.

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

Если вы хотите выжать последние капли производительности из вашего приложения, можете попробовать реализовать нечто похожее для обработки вашего протокола пользовательского уровня. Но следите за тем, чтобы издержки этого кода не перевесили издержки обработки (от которых вы хотите избавиться).

Вместо использования жестких дисков вы можете использовать флэш-память, написать драйвер диска под ваши нужды или перейти на технологию RAID.

Увеличение производительности - история, которую можно продолжать до бесконечности.

Ресурсы

Научиться

Получить продукты и технологии

  • Разработайте ваш следующий проект с помощью ознакомительного ПО от IBM, которое можно загрузить непосредственно с сайта developerWorks.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=AIX и UNIX
ArticleID=457029
ArticleTitle=Высокопроизводительное сетевое программирование: Часть 2. Ускоряем обработку данных на клиенте и на сервере
publish-date=12162009