Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

Sdílení paměti mezi procesy v Linuxu (C++)

3.3.2019

Úvod

Ukážeme si jak lze v Linuxu sdílet paměť mezi dvěma či více běžícími procesy. Samozřejmě nepůjde o celý paměťový prostor procesu, ale o paměťový blok.

Existují (v C/C++) různé knihovny, které můžeme využít, například i známá knihovna boost. Nyní si ale ukážeme jak cíle dosáhnout pouze s použitím standardu jazyka C/C++ a funkcí standardních linuxových knihoven.

Klíčovou funkcí je mmap, která nám vrátí adresu mapované paměti. Její 4. parametrem je deskriptor souboru, který můžeme získat funkcí open, tj. otevřením souboru, který pak namapujeme do paměti. Další možností (kterou použijeme pro náš případ), je získání deskriptoru funkcí shm_open, které předáme v parametru požadované jméno sdíleného paměťového objektu a v dalších 2 parametrech pak režim otevření/vytvoření a přístupová práva. Jméno (jako nulou ukončený řetězec) musí začínat znakem lomítka ('/') a být dlouhé max. 255 znaků (resp. NAME_MAX) . Pokud tento "objekt" (jak uvidíme dále je vždy pod ním fyzický soubor na disku) vytváříme, nastavíme požadovanou velikost (ještě před namapováním) funkcí ftruncate.

Když vše výše zmíněné projde bez chyb, pracujeme s adresou získanou funkcí mmap jako s běžným ukazatelem, tj. můžeme na něj aplikovat funkce jako memcpy, strcpy a další. Na konci pak pouze musíme zavřít deskriptor funkcí close.

Jak to funguje uvnitř?

Jak jsem už výše zmínil, sdílený paměťový objekt je ve skutečnosti namapován na fyzický soubor na disku. V případě jeho vytvoření funkcí shm_open je umístěn pod zadaným jménem (samozřejmě bez úvodního lomítka) do adresáře "/dev/shm". Výhodou oproti použití "běžného" souboru (funkce open) může být to, že soubory v této složce jsou automaticky smazány při vypnutí/restartu systému. Na druhou stranu je třeba vzít v potaz omezení velikosti tohoto souboru. Adresář "/dev/shm" (resp. celý "/dev") má ve výchozím stavu k disposici omezený prostor. Jeho konkrétní velikost závisí na velikosti diskového oddílu na kterém je systém nainstalovaný (a může se lišit i v různých linuxových distribucích. Například tento článek píšu v Ubuntu Xfce na diskovém oddílu o velikost 120 GB a adresář "/dev" má k disposici 4.1 GB. Samozřejmě v případě potřeby lze tento prostor rozšířit - konkrétní postup není těžké dohledat na internetu, jen je třeba s tímto počítat.

Ukázkový příklad

Následující ukázkový příklad realizuje v jednom programu "server" i "klient". Server vytvoří sdílený objekt a v sekundových intervalech na jeho začátek bude zapisovat aktuální čas ve formátu hh:mm:ss a pro sledování také vypisovat do terminálu. Klient pak objekt otevře a v sekundových intervalech bude načítat zapsaný text a vypisovat do terminálu. Zápis i načítání probíhá ve vlastním vlákně (thread) a hlavní vlákno čeká na vstup (getchar) po kterém se aplikace ukončí. Pokud je program spuštěn s parametrem 'k' (resp. první znak v parametru je 'k', spustí se v režimu klienta, opačném případě se spustí jako server.

Programu můžeme sestavit pomocí GCC, resp G++, kde musíme jako parametry linkeru uvést -ltr a -lpthread, tj. např.:

g++ -O3 main.cpp -lrt -lpthread -oaplikace-release

Nyní již kompletní výpis demonstračního programu:

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <stdexcept>

const char* _jmeno = "/chalupa-sdilena-pamet";
void* _pamet = nullptr;
bool _konec = false;
char _sz_cas[50];

void* thread_zapis(void* param)
{
	time_t cas;
	struct tm* timeinfo;
	while (!_konec)
	{
		cas = time(nullptr);
		timeinfo = localtime(&cas);
		strftime(_sz_cas, sizeof(_sz_cas), "%H:%M:%S", timeinfo);
		strcpy((char*)_pamet, _sz_cas);
		printf("%s - stiskni enter pro ukončení\n", _sz_cas);
		sleep(1);
	}
	strcpy((char*)_pamet, "server zastaven");
	return nullptr;
}

void* thread_cteni(void* param)
{
	while (!_konec)
	{
		strncpy(_sz_cas, (const char*)_pamet, sizeof(_sz_cas));
		printf("načteno: %s - enter pro ukončení\n", _sz_cas);
		sleep(1);
	}
	return nullptr;
}

int main(int argc, char **argv)
{
	bool mod_klient = false;
	if (argc > 1)
		mod_klient = (*argv[1] == 'k');
	int o_flags;
	int flags;
	off_t velikost_souboru = 1024;
	if (mod_klient)
	{
		o_flags = O_RDONLY;
		flags =  S_IROTH | S_IRUSR | S_IRGRP;
	}
	else
	{
		o_flags = O_RDWR | O_CREAT;
		flags =  S_IROTH | S_IRUSR | S_IWUSR | S_IRGRP;
	}
	int fd_soubor = 0;
	try
	{
		fd_soubor = shm_open(_jmeno, o_flags, flags);
		if (fd_soubor <= 0)
			throw std::runtime_error("chyba shm_open");
		if (!mod_klient)
		{
			if (0 != ftruncate(fd_soubor, velikost_souboru))
				throw std::runtime_error("chyba ftruncate");
		}
		_pamet = mmap(nullptr, velikost_souboru,
			mod_klient ? PROT_READ : PROT_READ | PROT_WRITE, MAP_SHARED, fd_soubor, 0);
		if ((void*)-1 == _pamet)
			throw std::runtime_error("chyba mmap");
		pthread_t p_thread;
		int iret = pthread_create(&p_thread, nullptr,
			mod_klient ? thread_cteni : thread_zapis, nullptr);
		if (iret) // při chybě vrátí nenulovou hodnotu
			throw std::runtime_error("chyba pthread_create");
		getchar();
		_konec = true;
		pthread_join(p_thread, nullptr);
		if (fd_soubor >= 0)
			close(fd_soubor);
		return EXIT_SUCCESS;
	}
	catch (const std::exception& v)
	{
		perror(v.what());
		if (fd_soubor >= 0)
			close(fd_soubor);
		return EXIT_FAILURE;
	}
}

Projekt si můžete stáhnout v příloze níže.