PVOID - Быстрый синхронизированный доступ к разделяемой памяти для Windows и для i86 Unix-ов

Быстрый синхронизированный доступ к разделяемой памяти для Windows и для i86 Unix-ов

Эта статья является переводом документации кроссплатформенной библиотеки shmem. Статья переводилась на автомате поздно ночью, поэтому иногда путаются понятия хранилища (storage) и разделяемой памяти (shared memory), что в контексте статьи одно и то же. Поправлю на досуге:) Эта библиотека (а также многие другие) связаны с именем Константина Книжника, а ссылки на сайты, где размещены материалы, почему-то ломаются. Так что если ссылка не работает, то стоит Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в вашем браузере должен быть включен Javascript. и поискать в поисковике e-mail разработчика библиотеки: Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в вашем браузере должен быть включен Javascript.. Этим способом я его и нашел:)

Описание


Класс shared_memory предоставляет эффективный платформонезависимый интерфейс для разделяемой памяти. Этот класс дает возможность нескольким приложениям получать доступ к разделяемой памяти. Этот класс предоставляет методы для создания/освобождения объектов в разделяемой памяти, установку исключительной или общей блокировки. Блокировки могут быть вложенными и могут быть использованы для синхронизации доступа к разделяемым объектам различными потоками внутри процесса, а также различными процессами.

Основная идея этого класса это поддержка эффективного механизма межпроцессного обмена данными и разделение данных. Было сделано предположение, что большую часть времени нет конфликта доступа к данным между конкурирующими процессами. Так что класс shared_memory был разработан  для оптимизации такого случая: блокировка объекта процессом без ожидания. Использование специальных атомарных инструкций процессора i486 (или выше) XADD и CMPXCHG позволяет избежать переключения контекста для блокировки ресурса, который не блокирован другим процессом. Классом shared_memory поддерживаются только блокировки памяти. Так что блокировка различных объектов невозможна.

Возможно отобразить секцию разделяемой памяти в файл и использовать ее как постоянное хранилище для объектов. Транзакции не поддерживаются этим классом. Если вашему приложению требуется атомарность и отказоустойчивость, смотрите Generic Object Oriented Database System или Persistent Object Storage for C++.

Так как объекты, размещенные в разделяемой памяти могут содержать ссылки на другие объекты в разделяемой памяти, необходимо гарантировать, что секция разделяемой памяти отображена в тот же диапазон виртуальных адресов во всех приложениях, использующих эту секцию. Этот адрес может быть указан в методе open() или, если не указан, определен системой. Когда секция отображается в адресное пространство другого процесса, метод open() пытается отобразить секцию на такой же виртуальный адрес. Если это невозможно ( например, если какая-либо другая секция отображена на тот же диапазон адресов), тогда open() завершается с ошибкой (POST++ позволяет перемещать секцию и регулировать ссылки, но это возможно только когда секция используется только одним приложением).

Размещение объектов в разделяемой памяти производится алгоритмом с использованием списка свободных ячеек памяти с перемещаемым указателем текущей позиции в списке. Два дополнительных слова выделяются перед объектом и после него, делая возможным эффективное слияние последующих ячеек во время операции free() (постоянное время освобождения объектов). После создания, секция разделяемой памяти не может быть увеличена. Максимальный размер секции должен быть определен в параметрах  open().

Интерфейс


static shared_memory* find_storage(void* obj);

Получая указатель на объект, этот статический метод возвращает указатель на секцию разделяемой памяти, к которой принадлежит этот объект. Если объект не размещен классом shared_memory или секция уже закрыта, этот метод возвращает NULL.

status lock(lock_descriptor& lck, unsigned msec = INFINITE);

Блокирует память как в общем, так и в исключительном режиме. Режим блокировки указывается через объект lock_descriptor, предоставляемый приложением. Этот объект не должен быть удален до вызова метода unlock() для этого дескриптора блокировки. Блокировки могут быть вложенными, так что вызов метода lock() дважды требует двух вызовов unlock() для отмены блокировки памяти. Ошибочным является использование одного дескриптора блокировки для более, чем одного запроса блокировки.

Необязательный параметр msec определяет значение таймаута ожидания удовлетворения запроса блокировки. Если таймаут истечет до предоставления блокировки, метод lock() вернет код ошибки shared_memory::timeout_expired. Если msec установлено в 0, lock() завершится незамедлительно, если блокировка невозможна. Смотрите Удобный механизм блокировки для альтернативного пути установки блокировок.

Использование блокировок может вызвать проблему взаимной блокировки. Рассмотрим следующую ситуацию: приложения A и B блокируют память в общем режиме, затем оба пытаются обновить свою блокировку до исключительной. Но обе эти блокировки не могут быть предоставлены по причине общей блокировки другого приложения. Так что эти приложения не могут продолжить выполнение и будут ждать друг друга. Для избежания взаимных блокировок, вы должны использовать обновления блокировок с осторожностью и, если вы знаете, что метод разделяемого объекта, который не изменяет объект, может вызвать может вызвать другой метод, который изменяет объект, лучше использовать исключительную блокировку в первом методе.

status unlock(lock_descriptor& lck);

Снимает блокировку, предварительно установленную  методом lock(). Блокировки могут быть вложенными, так что вызов метода unlock() не будет необходимым для разблокировки памяти. Дескриптор блокировки, передаваемый в метод unlock() должен быть тем же объектом, что был передан в lock().

void* allocate(size_t size, bool initialize_by_zero = true);

Размещение объектов в секции разделяемой памяти. Если нет достаточного пространства для размещения нового объекта, возвращается NULL. Если второй параметр initialize_by_zero установлен в true, то объект инициализируется нулями.

void free(void* ptr);

Освобождает указанный объект. Файл может быть усечен в результате освобождения объектов в его конце.

static void deallocate(void* obj);

Этот метод делает то же, что и предыдущий, но может вызываться без указателя на объект shared_memory. Этот метод вызывает метод find_storage(), чтобы найти пространство, в котором размещен объект, а затем освобождает память объекта методом free().

status open(const char* file_name, const char* shared_name, size_t max_size, open_mode mode = read_write, void* desired_address = NULL);

Создает или открывает  секцию разделяемой памяти. Параметр shared_name определяет системное имя объекта и не должно конфликтовать с именами других объектов системы (события, семафоры, мьютексы,...). Точнее, shared_name используется для генерации набора идентификаторов, которые присваиваются объектам синхронизации, используемым классом shared_memory и самим объектом отображения файла. Эти идентификаторы создаются из  shared_name, дополненным десятичным числом (1,2,...).

Если параметр file_name не равен NULL, секция памяти отображается в файл, позволяя сохранять данные между сессиями. Если параметр file_name равен NULL, создается анонимный объект отображения памяти с памятью, выделенной из файла подкачки.

Параметр max_size определяет размер создаваемой секции памяти. Этот параметр вызывается в первый раз для создания объекта разделяемой памяти. Все последующие вызовы open() другими процессами открывают существующий объект и не могут изменить его размер. Если размер отображаемого файла больше, чем max_size, то размер создаваемой секции устанавливается равным размеру файла (он не может быть расширен в этом случае). Если max_size больше, чем размер файла, тогда Windows сначала расширит файл до размера объекта отображаемой памяти. Файл будет усечен до актуального размера использованием размера метода close() (но необходимо иметь достаточно свободного пространства на диске для хранения всех max_size байт отображаемого файла).

Параметр mode может быть использован для выбора режима доступа read_only или read_write для секции памяти. В обоих случаях файл открывается для чтения и записи и создается объекта памяти с полными правами доступа. Но при использовании режима read_only, секция отображается на виртуальную память с правами доступа только для чтения. Так что любая попытка модифицировать объект в секции, открытой в режиме только для чтения, приведет к нарушению доступа (access violation).

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

status flush();

Сбрасывает измененные страницы на диск. Этот метод имеет смысл только если используется отображение в файл.

void close();

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

char* get_error_text(status code, char* buf, size_t buf_size) const;

Получая код статуса, возвращаемый методом shared_memory, этот метод копирует в предоставляемый буфер текст сообщения для этого кода.

void set_root_object(void* root);

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

Обычно создание корневого объекта происходит в момент создания хранилища (но root можно изменить в любой момент). Не забывайте про исключительную блокировку базы данных во время инициализации хранилища, если несколько процессов могут одновременно пытаться открыть хранилище.

void* get_root_object() const;

Извлекает ссылку на корневой объект хранилища. Эта ссылка должна быть предварительно  сохранена через set_root_object(void* root).

void check_heap();

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

 Удобный механизм блокировки


 При использовании методов lock()/unlock() вы должны сохранять баланс вызовов lock/unlock и создавать объекты дескрипторов блокировки. Но обычно блокировки используются структурным образом, защищая блоки кода программы. Есть два класса, которые помогают избежать написания дополнительного кода и снизить вероятность возникновения ошибок: exclusive_lock и shared_lock. Эти классы устанавливают блокировку в конструкторе и снимают блокировку в деструкторе. Так что, если вы хотите защитить блок кода, единственное, что вы должны сделать - создать локальный (автоматический) объект классов exclusive_lock или shared_lock в стеке. Компилятор сделает всю остальную работа за вас, вызывая конструктор перед входом в блок и деструктор после выхода их него. Смотрите пример в шаблоне приложения.

Примитивы синхронизации


В дополнение к механизму блокировки хранилища, имеются два примитива синхронизации: семафор (semaphore) и событие (event). Эти классы, определенные в shmem.h, предоставляют платформонезависимый интерфейс для операций синхронизации. Таблица ниже содержит описание методов этих классов:

семафор:

Метод Описание
 open  Создает семафор с указанным глобальным именем и устанавливает  его значение в init_value. Если name равно NULL, семафор будет локальным внутри процесса.  Метод возвращает false, если семафор не может быть создан.
 wait  Ожидает определенный период времени, пока значение семафора не станет отличным от нуля. Метод возвращает false, если таймаут истек и true в противном случае.
 signal  Увеличивает семафор на inc
 close  Закрывает семафор (в Unix этот метод не делает ничего)

 

событие:

Метод Описание
 open  Создает событие с указанным глобальным именем и устанавливает состояние из параметра signaled. Если name равно NULL, событие является локальным внутри процесса. Метод возвращает false, если событие не может быть создано.
 wait  Ожидает определенный период времени, пока событие не сигнализирует. Метод возвращает false, если таймаут истек прежде, чем событие переключилось в сигнализирующее состояние, и true в противном случае.
 signal  Устанавливает состояние события в сигнализирующее.
 reset  Сбрасывает состояние события в несигнализирующее.
 close  Закрывает событие (в Unix этот метод не делает ничего).

 

Основанные (based) указатели


Использование квалификатора __based(), поддерживаемого компилятором Microsoft Visual C++, делает возможность отображать секцию разделяемой памяти на различные виртуальные адреса в различных приложениях. Статический указатель используется для указания на начало отображаемой секции (так что в каждый момент времени только одна секция может быть отображена). Для использования этой схемы, вы должны объявить все поля, являющимися ссылками, разделяемых объектов с помощью макроса REF(type) вместо TYPE* и компилировать ваше приложение с опцией -DUSE_BASED_POINTERS.

Шаблон приложения


shared_memory shmem;
class tree { 
  public:
    tree* left;
    tree* right;
    int   val;

    void* operator new(size_t size) { 
	return shmem.alloc(size);
    }
    void operator delete(void* p) { 
	shmem.free(p);
    }
    tree(int key) { val = key; left = right = NULL; }
};

class root_object { 
  public:
    tree* root;

    void  insert(int key) { 
	exclusive_lock x_lock(shmem);
	...
    }
    tree* search(int key) { 
	shared_lock s_lock(shmem); 
	...
    }
    void  remove(int key) { 
	exclusive_lock x_lock(shmem);
	...
    }
    void* operator new(size_t size) { 
	return shmem.alloc(size);
    }
    void operator delete(void* p) { 
	shmem.free(p);
    }
    root_object() { root = NULL; }
};

main()
{
    shared_memory::status rc;
    root_object* root;
    rc = shmem.open("test.odb", "test", max_size);
    if (rc != shared_memory::ok) { 
	shmem.get_error_text(rc, buf, sizeof buf);
	fprintf(stderr, "Field to open file: %s\n", buf);
	return EXIT_FAILURE;
    } else { 
	exclusive_lock x_lock(shmem);
	root = (root_object*)shmem.get_root_object();
	if (root == NULL) { 
	    root = new root_object;
	    shmem.set_root_object(root);
	}
    }
    root->insert(0);
    ...
    shmem.close();
    return EXIT_SUCCESS;
}