È possibile, sebbene ci siano problemi di coerenza della cache specifici dell'architettura che potresti dover considerare. Alcune architetture semplicemente non consentono l'accesso simultaneo alla stessa pagina da più indirizzi virtuali senza perdere coerenza. Quindi, alcune architetture gestiranno bene questa operazione, altre no.
A cura di aggiungere:AMD64 Architecture Programmer's Manual vol. 2, Programmazione del sistema, sezione 7.8.7 Modifica del tipo di memoria, afferma:
Una pagina fisica non dovrebbe avere diversi tipi di capacità di memorizzazione nella cache assegnati tramite diverse mappature virtuali; dovrebbero essere tutti di un tipo memorizzabile nella cache (WB, WT, WP) o tutti di un tipo non memorizzabile nella cache (UC, WC, CD). In caso contrario, ciò potrebbe comportare una perdita di coerenza della cache, con conseguenti dati obsoleti e comportamenti imprevedibili.
Quindi, su AMD64, dovrebbe essere sicuro mmap()
lo stesso file o area di memoria condivisa di nuovo, purché lo stesso prot
e flags
sono usati; dovrebbe far sì che il kernel utilizzi lo stesso tipo memorizzabile nella cache per ciascuna delle mappature.
Il primo passo è utilizzare sempre un file di supporto per le mappe di memoria. Usa mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_NORESERVE, fd, 0)
in modo che le mappature non riservino lo scambio. (Se lo dimentichi, ti imbatterai nei limiti di swap molto prima di quanto raggiungi i limiti effettivi della vita reale per molti carichi di lavoro.) L'overhead aggiuntivo causato dall'avere un file di supporto è assolutamente trascurabile.
Modificato per aggiungere:l'utente strcmp ha sottolineato che i kernel attuali non applicano la randomizzazione dello spazio degli indirizzi agli indirizzi. Fortunatamente, questo è facile da risolvere, semplicemente fornendo indirizzi generati casualmente a mmap()
invece di NULL
. Su x86-64, lo spazio degli indirizzi utente è a 47 bit e l'indirizzo deve essere allineato alla pagina; potresti usare ad es. Xorshift* per generare gli indirizzi, quindi mascherare i bit indesiderati:& 0x00007FFFFE00000
ad esempio, darebbe indirizzi a 47 bit allineati a 2097152 byte.
Poiché il supporto è su un file, puoi creare una seconda mappatura sullo stesso file, dopo aver ingrandito il file di supporto utilizzando ftruncate()
. Solo dopo un adeguato periodo di grazia -- quando sai che nessun thread sta più usando la mappatura (magari usi un contatore atomico per tenerne traccia?) --, annulli la mappatura originale.
In pratica, quando una mappatura deve essere ingrandita, prima ingrandisci il file di supporto, poi prova mremap(mapping, oldsize, newsize, 0)
per vedere se la mappatura può essere ampliata, senza spostare la mappatura. Solo se la rimappatura sul posto fallisce, devi passare alla nuova mappatura.
Modificato per aggiungere:Sicuramente vuoi usare mremap()
invece di usare semplicemente mmap()
e MAP_FIXED
per creare una mappatura più ampia, perché mmap()
annulla la mappatura (atomica) di qualsiasi mappatura esistente, incluse quelle appartenenti ad altri file o regioni di memoria condivisa. Con mremap()
, ricevi un errore se la mappatura ingrandita si sovrappone alle mappature esistenti; con mmap()
e MAP_FIXED
, qualsiasi mappatura esistente che la nuova mappatura si sovrappone viene ignorata (non mappata).
Sfortunatamente, devo ammettere che non ho verificato se il kernel rileva le collisioni tra le mappature esistenti, o se presuppone semplicemente che il programmatore sia a conoscenza di tali collisioni -- dopotutto, il programmatore deve conoscere l'indirizzo e la lunghezza di ogni mappatura, e quindi dovrebbe sapere se la mappatura entrerebbe in collisione con un'altra esistente. Modificato per aggiungere:i kernel della serie 3.8 lo fanno, restituendo MAP_FAILED
con errno==ENOMEM
se la mappatura ingrandita entrasse in collisione con le mappe esistenti. Mi aspetto che tutti i kernel Linux si comportino allo stesso modo, ma non ho prove, a parte il test su 3.8.0-30-generic su x86_64.
Si noti inoltre che in Linux, la memoria condivisa POSIX è implementata utilizzando un filesystem speciale, tipicamente un tmpfs montato a /dev/shm
(o /run/shm
con /dev/shm
essendo un collegamento simbolico). Il shm_open()
et. al sono implementati dalla libreria C. Invece di avere una grande capacità di memoria condivisa POSIX, userei personalmente un tmpfs appositamente montato per l'uso in un'applicazione personalizzata. Se non altro, i controlli di sicurezza (utenti e gruppi in grado di creare nuovi "file" al loro interno) sono molto più semplici e chiari da gestire.
Se la mappatura è, e deve essere, anonima, puoi comunque utilizzare mremap(mapping, oldsize, newsize, 0)
provare e ridimensionarlo; potrebbe fallire.
Anche con centinaia di migliaia di mappature, lo spazio degli indirizzi a 64 bit è vasto e il caso di errore raro. Quindi, sebbene tu debba gestire anche il caso di fallimento, non deve necessariamente essere veloce . Modificato per modificare:su x86-64, lo spazio degli indirizzi è a 47 bit e le mappature devono iniziare a un limite di pagina (12 bit per le pagine normali, 21 bit per le pagine enormi da 2 M e 30 bit per le pagine enormi da 1 G), quindi c'è solo 35, 26 o 17 bit disponibili nello spazio degli indirizzi per le mappature. Quindi, le collisioni sono più frequenti, anche se vengono suggeriti indirizzi casuali. (Per mappature 2M, 1024 mappe hanno avuto una collisione occasionale, ma a 65536 mappe, la probabilità di una collisione (errore di ridimensionamento) era di circa il 2,3%.)
Modificato per aggiungere:l'utente strcmp ha sottolineato in un commento che per impostazione predefinita Linux mmap()
restituirà indirizzi consecutivi, nel qual caso l'aumento della mappatura avrà sempre esito negativo, a meno che non sia l'ultima o una mappa non sia stata mappata proprio lì.
L'approccio che so funzionare in Linux è complicato e molto specifico per l'architettura. Puoi rimappare la mappatura originale in sola lettura, creare una nuova mappa anonima e copiare lì i vecchi contenuti. Hai bisogno di un SIGSEGV
gestore (SIGSEGV
segnale generato per il particolare thread che tenta di scrivere sulla mappatura ora di sola lettura, essendo questo uno dei pochi SIGSEGV
recuperabili situazioni in Linux anche se POSIX non è d'accordo) che esamina l'istruzione che ha causato il problema, la simula (modificando invece il contenuto della nuova mappatura), e poi salta l'istruzione problematica. Dopo un periodo di grazia, quando non ci sono più thread che accedono alla vecchia mappatura, ora di sola lettura, puoi eliminare la mappatura.
Tutta la cattiveria è nel SIGSEGV
gestore, ovviamente. Non solo deve essere in grado di decodificare tutte le istruzioni macchina e simularle (o almeno quelle che scrivono in memoria), ma deve anche attendere se la nuova mappatura non è stata ancora completamente copiata. È complicato, assolutamente non portabile e molto specifico per l'architettura... ma possibile.
Questo è stato aggiunto nel kernel 5.7 come nuovo flag a mremap(2) chiamato MREMAP_DONTUNMAP. Questo lascia la mappatura esistente al suo posto dopo aver spostato le voci della tabella delle pagine.
Vedi https://github.com/torvalds/linux/commit/e346b3813067d4b17383f975f197a9aa28a3b077#diff-14bbdb979be70309bb5e7818efccacc8
Sì, puoi farlo.
mremap(old_address, old_size, new_size, flags)
elimina la vecchia mappatura solo della dimensione "old_size". Quindi, se passi 0 come "old_size", non verrà annullata la mappatura.
Attenzione:funziona come previsto solo con mappature condivise, quindi tale mremap() dovrebbe essere utilizzato su una regione precedentemente mappata con MAP_SHARED. Questo è in realtà tutto questo, cioè non hai nemmeno bisogno di una mappatura supportata da file, puoi usarla con successo Combinazione "MAP_SHARED | MAP_ANONYMOUS" per i flag mmap(). Alcuni sistemi operativi molto vecchi potrebbero non supportare "MAP_SHARED | MAP_ANONYMOUS", ma su Linux sei al sicuro.
Se lo provi su una regione MAP_PRIVATE, il risultato sarebbe approssimativamente simile a memcpy(), ovvero non verrà creato alcun alias di memoria. Ma utilizzerà ancora i macchinari della Mucca. Non è chiaro dalla tua domanda iniziale se hai bisogno di un alias o se va bene anche la copia di CoW.
AGGIORNAMENTO:affinché funzioni, devi anche specificare ovviamente il flag MREMAP_MAYMOVE.