Опубликован: 06.12.2004 | Доступ: свободный | Студентов: 1179 / 142 | Оценка: 4.76 / 4.29 | Длительность: 20:58:00
ISBN: 978-5-9556-0021-5
Лекция 5:

Объекты в памяти

< Лекция 4 || Лекция 5: 1234 || Лекция 6 >

При работе с объектами в памяти полезны функции truncate() и, особенно, ftruncate() (см. листинг 5.5), позволяющие установить размер объекта равным заданной величине length. Напомним, что попытка записи за пределы отображенного объекта не только не расширит его, но, весьма вероятно, приведет к доставке процессу сигнала SIGBUS.

#include <unistd.h>

int truncate (const char *path, 
    off_t length);

int ftruncate (int fildes, off_t length);
Листинг 5.5. Описание функций truncate() и ftruncate.

Функции truncate() и ftruncate() применимы к обычным файлам, которые в первом случае задаются маршрутным именем, а во втором - открытым дескриптором. Кроме того, функция ftruncate() применима к объектам в разделяемой памяти (и это единственная узаконенная стандартом POSIX-2001 файловая операция над дескриптором подобного объекта). Других способов изменить размер объекта в разделяемой памяти стандарт не предусматривает.

Применение объектов в разделяемой памяти в сочетании с функциями mmap() и ftruncate() иллюстрируется модифицированным вариантом двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод (см. листинги 5.6, 5.7, 5.8). Как и для исходного варианта, предполагается, что заголовочный файл называется "g_shm.h", а файл с образом процесса, запускаемого посредством execl(), - "g_r_shm".

#ifndef g_SHM
#define g_SHM

/* Имя объекта в разделяемой памяти */
#define O_SHM_NAME 		"/g_o.shm"

/* Номер сигнала для синхронизации 
/* доступа к разделяемому сегменту */
#define SIG_SHM		SIGALRM

#endif
Листинг 5.6. Заголовочный файл "g_shm.h" программы, копирующей строки со стандартного ввода на стандартный вывод.
/* * * * * * * * * * * * * * * * * * * * * */
/* Программа, состоящая из двух процессов, */
/* копирует строки со стандартного ввода на*/
/* стандартный вывод, используя в качестве */
/* буфера разделяемый сегмент, отображенный*/
/* в адресные пространства процессов.      */
/* Для синхронизации доступа к разделяемому*/
/* сегменту используется сигнал SIGALRM    */
/* * * * * * * * * * * * * * * * * * * * * */

#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/wait.h>
#include <assert.h>

#include "g_shm.h"

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Создание разделяемого сегмента памяти,            */
/* отображение его в адресное пространство процесса, */
/* чтение строк со стандартного ввода в сегмент      */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
int main (void) {
	int fd_shm; /* Дескриптор объекта в разделяемой памяти */
	void *addr_shm;	/* Адрес отображенного в память объекта */
	sigset_t smask;	/* Маска блокируемых сигналов */
	siginfo_t sinfo;/* Структура для получения данных о сигнале */
		/* Длительность ожидания прихода сигнала */
	struct timespec stmspc = {10, 0};
	pid_t cpid; /* Идентификатор порожденного процесса 	*/

	/* Создадим разделяемый сегмент памяти */
	if ((fd_shm = shm_open (O_SHM_NAME, O_RDWR | O_CREAT, 
			0777)) < 0) {
		perror ("SHM_CREAT");
		return (1);
	}

	/* Сделаем размер созданного объекта равным LINE_MAX */
	if (ftruncate (fd_shm, LINE_MAX) != 0) {
		perror ("FTRUNCATE");
		return (2);
		}

	/* Отобразим сегмент в память */
	if ((addr_shm = mmap (NULL, LINE_MAX, 
			PROT_READ | PROT_WRITE, MAP_SHARED, 
			fd_shm, 0)) == MAP_FAILED) {
		perror ("MMAP-P");
		return (3);
	}

	/* Сформируем маску сигналов (блокируем SIG_SHM) */
	(void) sigemptyset (&smask);
	(void) sigaddset (&smask, SIG_SHM);
	(void) sigprocmask (SIG_BLOCK, &smask, 
			(sigset_t *) NULL);

	/* Подготовительная работа закончена */

	switch (cpid = fork ()) {
		case -1:
			perror ("FORK");
			return (4);
		case 0:
			/* Чтение из объекта и выдачу на стандартный*/
			/* вывод реализуем в порожденном процессе*/
			if (execl ("./g_r_shm", "g_r_shm", (
				char *) NULL) < 0) {
			perror ("EXECL");
			return (5);
			}
	}

	/* Чтение строк со стандартного ввода в объект 	*/
	/* возложим на родительский процесс. */
	/* В начальный момент объект в разделяемой памяти */
	/* доступен для записи*/
	while (fgets (addr_shm, LINE_MAX, stdin) != NULL) {
		/* Сообщим порожденному процессу, */
		/* что в объект в разделяемой памяти */
		/* помещена очередная строка */
		assert (kill (cpid, SIG_SHM) == 0);
		/* Дождемся, когда в объект можно будет прочитать*/
		/* следующую строку */
		if (sigtimedwait (&smask, 
			&sinfo, &stmspc) != SIG_SHM) {
			break;
		}
	}

	/* Порожденный процесс должен завершиться*/
	/* по контролю времени ожидания*/
	(void) wait (NULL);

	if (shm_unlink (O_SHM_NAME) != 0) {
		perror ("SHM_UNLINK");
		return (6);
	}

	return (0);
}
Листинг 5.7. Исходный текст начального процесса двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод.
/* * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Процесс берет строки из объекта в разделяемой памяти*/
/* и выдает их на стандартный вывод                    */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <limits.h>
#include <assert.h>

#include "g_shm.h"

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Открытие разделяемого сегмента памяти,            */
/* отображение его в адресное пространство процесса, */
/* чтение из сегмента и выдача строк на стандартный вывод */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
int main (void) {
	int fd_shm; /* Дескриптор объекта в разделяемой */ 
	    /* памяти */
	void *addr_shm; /* Адрес отображенного в память	*/
		/* объекта */
	sigset_t smask; /* Маска ожидаемых сигналов */
	siginfo_t sinfo; /* Структура для получения данных*/
		/* о сигнале */
		/* Длительность ожидания строки */
	struct timespec stmspc = {10, 0};
	pid_t ppid; /* Идентификатор родительского процесса */

	/* Откроем разделяемый сегмент памяти */
	if ((fd_shm = shm_open (O_SHM_NAME, O_RDONLY, 
			0777)) < 0) {
		perror ("SHM_OPEN");
		return (1);
	}

	/* Отобразим сегмент в память */
	if ((addr_shm = mmap (NULL, LINE_MAX, PROT_READ, 
			MAP_SHARED, fd_shm, 0)) == MAP_FAILED) {
		perror ("MMAP-C");
		return (2);
		}

	/* Запомним идентификатор родительского процесса */
	ppid = getppid ();

	/* Сформируем маску ожидаемых сигналов (SIG_SHM) */
	(void) sigemptyset (&smask);
	(void) sigaddset (&smask, SIG_SHM);

	/* Подготовительная работа закончена */
	fputs ("Вводите строки\n", stdout);

	while (sigtimedwait (&smask, &sinfo, &stmspc) == SIG_SHM) {
		/* Дождались, когда в объекте  появилась строка.*/
		/* Выдадим ее на стандартный вывод*/
		assert (fputs ("Вы ввели: ", stdout) != EOF);
		assert (fputs (addr_shm, stdout) != EOF);
		/* Сообщим родительскому процессу, */
		/* что данные из объекта извлечены */
		assert (kill (ppid, SIG_SHM) == 0);
	}

	return 0;
}
Листинг 5.8. Исходный текст порождаемого процесса (файл g_r_shm.c) двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод.

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

Отметим, что при использовании объектов в разделяемой памяти, отображенных в адресные пространства процессов, передачи данных как таковой не требуется. В приведенной программе начальный процесс читает строки в разделяемый объект, а порожденный берет их оттуда же и выводит без каких-либо дополнительных копирований. Флаг MAP_SHARED в вызове mmap() обеспечивает доступность данных, записанных в объект одним процессом, всем другим процессам-читателям.

Еще одна возможность, полезная в связи с отображением объектов в адресное пространство процессов, - синхронизация (согласование состояния) оперативной и долговременной памяти. Это возможность реализует функция msync() (см. листинг 5.9).

#include <sys/mman.h>
int msync (void *addr, 
    size_t len, int flags);
Листинг 5.9. Описание функции msync().

Функция msync() записывает в долговременную память все измененные страницы объекта в памяти, пересекающиеся с фрагментом адресного пространства процесса, начинающимся с адреса addr (это должна быть граница страницы) и имеющим длину len байт.

Если объектом является файл, отображенный в память (естественно, без флага MAP_PRIVATE ), то вызов msync() гарантирует завершение всех операций записи с обеспечением целостности данных синхронизированного ввода/вывода (см. курс [1]). Эффект от применения msync() к объектам в разделяемой и типизированной памяти не специфицирован.

Аргумент flags определяет характер действий при записи измененных страниц. Флаг MS_ASYNC означает асинхронную, а MS_SYNC - синхронную запись. Кроме того, флаг MS_INVALIDATE предписывает выполнить инициализацию кэша данных (приведение его в соответствие с содержимым долговременной памяти).

Отметим, что функция msync() обязана присутствовать только на системах, поддерживающих две необязательные возможности стандарта POSIX-2001 - файлы, отображенные в память, и синхронизированный ввод/вывод. В этом смысле ее можно считать дважды необязательной.

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

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

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Программа реализует некоторые функции систем управления базами*/
/* данных в памяти, следуя принципу неуничтожения информации     */
/* (изменения записываются не в сам объет, а в его новую версию)  */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/stat.h>
#include s<ys/mman.h>
#include <assert.h>

/* Размер страницы */
static int pgsz;

/* Структура для получения */
/* информации об исходном файле*/
static struct stat sbuf_src;

/* Адрес отображенного в память исходного файла*/
static char *saddr;

/* Признак того, что выполнялась функция */
/* обработки сигнала SIGSEGV */
static char sigsegv_catching;

/* * * * * * * * * * * * * * * * * * * * */
/* Функция обработки сигнала SIGSEGV.    */
/* Предполагается, что этот сигнал       */
/* генерируется при попытке записи в     */
/* страницы, доступные только на чтение. */
/* Функция добавляет разрешение на запись*/
/* * * * * * * * * * * * * * * * * * * * */
static void sigsegv_sigaction (int sig, siginfo_t *sig_info, 
		void *addr) {
	void *page_addr; /* Адрес страницы, в которую*/
		/*пытались писать*/

	page_addr = (char *) sig_info->si_addr - (off_t) 
		((char *) sig_info->si_addr - saddr) % pgsz;
	assert (mprotect (page_addr, pgsz, 
		PROT_READ | PROT_WRITE) == 0);
	sigsegv_catching = 1;
}


/* * * * * * * * * * * * * * * * * * * * * * * */
/* Установка способа обработки сигнала SIGSEGV */
/* * * * * * * * * * * * * * * * * * * * * * * */
static int sigsegv_set_proc (void (*sigsegv_actn) (int, 
	siginfo_t *, void *), struct sigaction *old_sigsegv_sact) {
		struct sigaction sact;

		(void) sigemptyset (&sact.sa_mask);
		sact.sa_flags = SA_SIGINFO;
		sact.sa_sigaction = sigsegv_actn;
		if (sigaction (SIGSEGV, &sact, old_sigsegv_sact) != 0) {
		perror ("SIGACTION");
		return (-1);
	}

	return 0;
}

/* * * * * * * * * * * * * * * * * * * * * */
/* Функция начала работы с исходным файлом.*/
/* Нормальный результат равен нулю         */
/* * * * * * * * * * * * * * * * * * * * * */
static int init_src (char *name_src) {
	int fd_src; /* Дескриптор исходного файла */

	/* Откроем исходный файл и отобразим его в память */
	if ((fd_src = open (name_src, O_RDONLY)) < 0) {
		perror ("OPEN-SRC");
		return (-1);
	}
	if (fstat (fd_src, &sbuf_src) != 0) {
		perror ("FSTAT");
		return (-1);
	}
	if ((saddr = (char *) mmap (NULL, sbuf_src.st_size, 
			PROT_READ, MAP_PRIVATE, fd_src, 0)) == MAP_FAILED) {
		perror ("MMAP");
		return (-1);
	}

	return 0;
}
/* * * * * * * * * * * * * * * * * * */
/* Опрос конфигурационных параметров */
/* * * * * * * * * * * * * * * * * * */
static int init_conf_params (void) {
	if ((pgsz = sysconf (_SC_PAGESIZE)) == -1) {
		perror ("SYSCONF");
		return (-1);
	}

	return 0;
}

/* * * * * * * * * * * */
/* Общая инициализация */
/* * * * * * * * * * * */
static int init_all (char *name_src) {
	if ((init_src (name_src) != 0) || (sigsegv_set_proc 
		(sigsegv_sigaction, (struct sigaction *) NULL) != 0) || 
		(init_conf_params () != 0)) {
		return (-1);
	}

	return 0;
}

/* * * * * * * * * * * * * * * * * */
/* Запись в файл измененных страниц*/
/* * * * * * * * * * * * * * * * * */
static int write_dlt (char *name_dlt) {
	int fd_dlt; /* Дескриптор файла изменений*/
	volatile char tmp; /* Значение пробного байта*/
	off_t pos;/* Позиция в отображенной памяти*/

	if ((fd_dlt = open (name_dlt, 
			O_CREAT | O_WRONLY | O_TRUNC, 0777)) < 0) {
		perror ("OPEN-DLT");
		return (-1);
	}

	/* Измененные страницы выявим путем пробных записей. */
	for (pos = 0; pos < sbuf_src.st_size; pos += pgsz) {
		tmp = saddr [pos];
		sigsegv_catching = 0;
		saddr [pos] = tmp;
		if (sigsegv_catching == 0) {
			/* В страницу удалось записать */
			/* без генерации сигнала SIGSEGV.*/
			/* Значит, она была доступна на запись.*/
			/* Следовательно, ее модифицировали.*/
			/* Запишем ее в файл изменений */
			(void) lseek (fd_dlt, pos, SEEK_SET);
			assert (write (fd_dlt, saddr + pos, pgsz) == pgsz);
		}
		/* Уберем добавленное разрешение на запись */
		assert (mprotect (saddr + pos, pgsz, 
					PROT_READ) == 0);
	} /* for */

	return (close (fd_dlt));
}

/* * * * * * * * * * * * * */
/* Функция main() вызывает */
/* описанные выше функции  */
/* и обращается на запись  */
/* к отображенной памяти   */
/* * * * * * * * * * * * * */
int main (int argc, char *argv []) {
	if (argc != 3) {
		fprintf (stderr, "Использование: %s исходный_файл "
			"файл_изменений\n", argv [0]);
		return (-1);
	}

	if (init_all (argv [1]) != 0) {
		fprintf (stderr, "Ошибка инициализации\n");
		return (-1);
	}

	printf ("Размер страницы: %d\n"
		"Адрес отображенного в память исходного файла: %p\n"
		"Длина отображенного в память исходного файла: %ld\n", 
			pgsz, saddr, sbuf_src.st_size);

	saddr [0] = '*';

	return (write_dlt (argv [2]));
}
Листинг 5.10. Пример программы, использующей файлы, отображенные в память.

Приведенная программа следует фундаментальному принципу неуничтожения информации. При модификациях порождаются новые версии объектов; первоначальные объекты остаются неизменными. Подобный подход особенно полезен, когда СУБД является технологическим элементом инструментальной среды разработки программ, поскольку историю создания и модификации программных систем обязательно нужно сохранять.

Обратим внимание на изменение разрешенных видов доступа к отображенным страницам, реализуемое обращениями к функции mprotect(). Первоначально отображенная память доступна только на чтение. При попытке записи в отображенную память генерируется сигнал SIGSEGV, и функция его обработки добавляет доступ на запись к соответствующей странице. В результате появляется возможность отслеживания изменений и сохранения в последующем только измененных страниц, так что дисковое пространство на версии объектов будет расходоваться экономно.

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

К сожалению, приведенная программа не вполне соответствует стандарту POSIX-2001 в части обработки сигналов. По стандарту поведение процесса после нормального выхода из функций обработки сигналов SIGBUS, SIGFPE, SIGILL и SIGSEGV не определено, если только последние не были сгенерированы вызовами kill(), sigqueue() или raise(). (Заметим попутно, что и игнорирование перечисленных сигналов приводит к неопределенному поведению.) Строго говоря, и вызов mprotect() из функций обработки сигналов не считается безопасным. В общем, функция sigsegv_sigaction() - сама нестандартность и немобильность, и это, конечно, плохо, но то, что вся нестандартность и немобильность программы сосредоточена в одной небольшой функции, безусловно, хорошо.

< Лекция 4 || Лекция 5: 1234 || Лекция 6 >