Jak zapisywać wartości wielobajtowe do pamięci współdzielonej w C ++ 14?
Załóżmy, że mam dwa procesy, które współużytkują blok pamięci przy użyciu shm_open
i mmap
istnieje współdzielony prymityw synchronizacji - powiedzmy semafor - który zapewnia wyłączny dostęp do pamięci. Tj. Żadnych warunków wyścigu.
Rozumiem, że wskaźnik zwrócony z mmap
musi być nadal oznaczony jako nietrwały, aby zapobiec odczytom w pamięci podręcznej.
A teraz, jak napisać np. A std::uint64_t
w jakimkolwiek wyrównanym miejscu w pamięci?
Oczywiście użyłbym po prostu, std::memcpy
ale nie działa to ze wskaźnikami do ulotnej pamięci.
Pierwsze podejscie
// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;
// Value to store, initialize "randomly" to prevent compiler
// optimization, for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(nullptr);
// Store byte-by-byte
unsigned char* src = reinterpret_cast<unsigned char*>(&value);
for(std::size_t i=0;i<sizeof(value);++i)
ptr[i]=src[i];
Godbolt .
Jestem przekonany, że to rozwiązanie jest poprawne, ale nawet przy -O3
8 transferach 1-bajtowych. To naprawdę nie jest optymalne.
Drugie podejście
Skoro wiem, że nikt nie zmieni pamięci, kiedy mam ją zablokowaną, to może niestabilność jest jednak niepotrzebna?
// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;
// Value to store, initialize "randomly" to prevent compiler
// optimization for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(0xAA);
unsigned char* src = reinterpret_cast<unsigned char*>(&value);
//Obscure enough?
auto* real_ptr = reinterpret_cast<unsigned char*>(reinterpret_cast<std::uintptr_t>(ptr));
std::memcpy(real_ptr,src,sizeof(value));
Godbolt .
Ale to nie wydaje się działać, kompilator przegląda obsadę i nic nie robi. Clang generuje ud2
instrukcję, nie wiem dlaczego, czy w moim kodzie jest UB? Oprócz value
inicjalizacji.
Trzecia próba
Ten pochodzi z tej odpowiedzi . Ale myślę, że to łamie ścisłą regułę aliasingu, prawda?
// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;
// Value to store, initialize "randomly" to prevent compiler
// optimization for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(0xAA);
unsigned char* src = reinterpret_cast<unsigned char*>(&value);
volatile std::uint64_t* dest = reinterpret_cast<volatile std::uint64_t*>(ptr);
*dest=value;
Godbolt .
Gcc faktycznie robi to, co chcę - prostą instrukcję kopiowania wartości 64-bitowej. Ale jest to bezużyteczne, jeśli jest to UB.
Jednym ze sposobów, w jaki mogłem to naprawić, jest naprawdę stworzenie std::uint64_t
obiektu w tym miejscu. Ale najwyraźniej umieszczanie new nie działa również ze volatile
wskaźnikami.
pytania
- Czy jest więc lepszy (bezpieczny) sposób niż kopiowanie bajt po bajcie?
- Chciałbym również skopiować jeszcze większe bloki surowych bajtów. Czy można to zrobić lepiej niż przy użyciu pojedynczych bajtów?
- Czy jest jakaś możliwość zmuszenia do
memcpy
zrobienia właściwej rzeczy? - Czy niepotrzebnie martwię się o wydajność i powinienem po prostu iść z pętlą?
- Jakieś przykłady (głównie C) w ogóle nie używam
volatile
, czy powinienem to zrobić? Czymmap
ed pointer jest już traktowany inaczej? W jaki sposób?
Dzięki za wszelkie sugestie.
EDYTOWAĆ:
Oba procesy działają w tym samym systemie. Załóżmy również, że wartości mogą być kopiowane bajt po bajcie, nie mówiąc o złożonych klasach wirtualnych przechowujących gdzieś wskaźniki. Wszystkie liczby całkowite i brak zmiennoprzecinków będą w porządku.
Odpowiedzi
Rozumiem, że wskaźnik zwracany przez mmap musi być nadal oznaczony jako ulotny, aby zapobiec odczytom w pamięci podręcznej.
Twoje rozumienie jest błędne. Nie używaj volatile
do kontrolowania widoczności pamięci - nie do tego służy. Będzie to albo niepotrzebnie drogie, albo niewystarczająco rygorystyczne, albo jedno i drugie.
Rozważ na przykład dokumentację GCC dotyczącą volatile , która mówi:
Dostęp do obiektów nieulotnych nie jest uporządkowany w odniesieniu do dostępów nietrwałych. Nie można użyć obiektu ulotnego jako bariery pamięci w celu zamówienia sekwencji zapisów do pamięci nieulotnej
Jeśli chcesz tylko uniknąć rozrywania, buforowania i zmiany kolejności - użyj uint64_t
(i jest on prawidłowo wyrównany), po prostu uzyskaj do niego dostęp za pośrednictwem pliku std::atomic_ref
Jeśli potrzebujesz normalnej synchronizacji, twój istniejący semafor będzie w porządku. Jak poniżej, zapewnia już niezbędne ogrodzenia i zapobiega ponownemu zamawianiu połączeń oczekujących / pocztowych. Nie zapobiega zmianie kolejności lub innych optymalizacji między nimi, ale ogólnie jest w porządku.
Jeśli chodzi o
Jakieś przykłady (głównie C) w ogóle nie używają volatile, czy powinienem to zrobić? Czy wskaźnik mmaped już jest traktowany inaczej? W jaki sposób?
odpowiedź brzmi, że jakakolwiek synchronizacja jest używana, wymaga również zastosowania odpowiednich ogrodzeń.
POSIX wymienia te funkcje jako „synchronizujące pamięć”, co oznacza, że muszą zarówno emitować wymagane bariery pamięci, jak i zapobiegać niewłaściwej zmianie kolejności kompilatora. Na przykład, Twoja implementacja musi unikać przenoszenia dostępu do pamięci przez wywołania pthread_mutex_*lock()
lub sem_wait()
/ sem_post()
, aby była zgodna z POSIX, nawet jeśli w innym przypadku byłaby to legalna C lub C ++.
Kiedy używasz wbudowanego wątku C ++ lub obsługi atomowej, poprawna semantyka jest częścią standardu języka zamiast rozszerzenia platformy (ale pamięć współdzielona nie jest).
Załóżmy, że mam dwa procesy, które współdzielą blok pamięci za pomocą shm_open i mmap i istnieje współdzielony prymityw synchronizacji - powiedzmy semafor - który zapewnia wyłączny dostęp do pamięci. Tj. Żadnych warunków wyścigu.
Potrzebujesz czegoś więcej niż tylko wyłącznego dostępu do pamięci. Musisz zsynchronizować pamięć. Każdy semafor, jaki kiedykolwiek widziałem, już to robi. Jeśli nie, oznacza to niewłaściwy prymityw synchronizacji. Przełącz się na inny.
Rozumiem, że wskaźnik zwracany przez mmap musi być nadal oznaczony jako ulotny, aby zapobiec odczytom w pamięci podręcznej.
Cóż volatile
, nie zapobiega buforowanym odczytom, ale prawie wszystkie semafory, muteksy i inne prymitywy synchronizacji działają tak, jakby zapobiegały buforowanym odczytom i zapisom w nich. W przeciwnym razie ich użycie byłoby prawie niemożliwe.
Jakiego semafora używasz? Jeśli nie synchronizuje pamięci, jest to niewłaściwe narzędzie do tego zadania.