Содержание


Высокопроизводительное сетевое программирование

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

Comments

Серия контента:

Этот контент является частью # из серии # статей: Высокопроизводительное сетевое программирование

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Высокопроизводительное сетевое программирование

Следите за выходом новых статей этой серии.

В части 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 с неблокирующими сокетами
Применение техники uio с неблокирующими сокетами

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

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

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

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

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

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

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

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

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

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

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

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