¿Es C++ 11 atómico? utilizable con mmap?

9 minutos de lectura

avatar de usuario
ben voigt

Quiero agregar el control de red de un puñado de parámetros utilizados por un servicio (daemon) que se ejecuta en un sistema integrado de Linux. No hay necesidad de llamadas a procedimientos, cada parámetro se puede sondear de una manera muy natural. La memoria compartida parece una buena manera de mantener el código de red fuera del daemon y limitar el acceso compartido a un conjunto de variables cuidadosamente controlado.

Como no quiero escrituras parciales para causar visibilidad de valores nunca escritos, estaba pensando en usar std::atomic<bool> y std::atomic<int>. Sin embargo, me preocupa que std::atomic<T> podría implementarse de una manera que solo funcione con subprocesos C ++ 11 y no con múltiples procesos (potencialmente, ni siquiera con subprocesos del sistema operativo). Específicamente, si la implementación usa cualquier estructura de datos almacenada fuera del bloque de memoria compartida, en un escenario de múltiples procesos esto fallaría.

Veo algunos requisitos que sugieren que std::atomic no retendrá un objeto de bloqueo incrustado o un puntero a datos adicionales:

Las especializaciones integrales atómicas y la especialización atomic<bool> tendrá un diseño estándar. Cada uno tendrá un constructor predeterminado trivial y un destructor trivial. Cada uno de ellos soportará la sintaxis de inicialización agregada.

Habrá especializaciones parciales de puntero de la plantilla de clase atómica. Estas especializaciones tendrán un diseño estándar, constructores por defecto triviales y destructores triviales. Cada uno de ellos soportará la sintaxis de inicialización agregada.

Me parece que la construcción y destrucción por defecto triviales excluyen los datos asociados por objeto, ya sea almacenados dentro del objeto, a través de una variable de miembro de puntero o a través de un mapeo externo.

Sin embargo, no veo nada que excluya a una implementación del uso de una única sección crítica / mutex global (o incluso una colección global, siempre que los elementos de la colección no estén asociados con objetos atómicos individuales, algo similar a un esquema de asociación de caché podría usarse para reducir los falsos conflictos). Obviamente, el acceso desde múltiples procesos fallaría en una implementación que usa un mutex global, porque los usuarios tendrían mutex independientes y no se sincronizarían entre sí.

es una implementación de atomic<T> ¿Se le permite hacer cosas que son incompatibles con la memoria compartida entre procesos, o hay otras reglas que lo hacen seguro?


Me acabo de dar cuenta de que la construcción predeterminada trivial deja el objeto en un estado no listo y una llamada a atomic_init es requerido. Y el Estándar menciona la inicialización de bloqueos. Si estos se almacenan dentro del objeto (y la asignación de memoria dinámica parece imposible, ya que el destructor sigue siendo trivial), entonces se compartirían entre procesos. Pero todavía estoy preocupado por la posibilidad de un mutex global.

En cualquier caso, garantizar una única llamada a atomic_init para cada variable en una región compartida parece difícil… así que supongo que tendré que alejarme de los tipos atómicos de C++11.

  • Como apéndice, la gente ha estado recomendando el uso de operaciones atómicas con memoria compartida, aunque no está claro si pretendían incluir o excluir std::atomic o si se garantiza que otras API funcionen.

    – Ben Voigt

    19/08/2013 a las 19:50

  • Espero que un sistema razonable no utilice estructuras de datos externas para atomic variables; derrotaría el punto de lo atómico en primer lugar…

    – usuario541686

    19 de agosto de 2013 a las 20:28


  • @Mehrdad: No veo cómo tomar un bloqueo global anularía el propósito más que tomar un bloqueo local, y el Estándar habla específicamente sobre implementaciones que hacen lo último.

    – Ben Voigt

    19/08/2013 a las 20:45

  • Me refiero al rendimiento. El objetivo de un atómico es ser rápido, ¿verdad? De lo contrario, también podría haber usado un candado…

    – usuario541686

    19 de agosto de 2013 a las 21:09


  • @Mehrdad Suponiendo que esté utilizando algún tipo de mecanismo de bloqueo entre procesos, sí. Pero, sospecho que parte de la razón por la que el OP deseaba usar std::atomic<T> es que proporciona una interfaz agradable en la que no necesita recordar adquirir y liberar bloqueos. Hará lo que sea necesario para que el acceso variable sea atómico, dentro de ese programa C++ bien formado. Pero dado que el estándar no habla de problemas entre procesos, los mecanismos de sincronización utilizados por std::atomic puede que no funcione en todos los procesos.

    – André Kostur

    19/08/2013 a las 21:40

Tengo dos meses de retraso, pero tengo exactamente el mismo problema en este momento y creo que he encontrado algún tipo de respuesta. La versión corta es que debería funcionar, pero no estoy seguro si dependería de ello.

Esto es lo que encontré:

  • El estándar C ++ 11 define un nuevo modelo de memoria, pero no tiene noción de “proceso” a nivel del sistema operativo, por lo que cualquier cosa relacionada con el multiprocesamiento no es estándar.

  • Sin embargo, la sección 29.4 “Propiedad sin bloqueo” del estándar (o al menos el borrador que tengo, N3337) termina con esta nota:

    [ Note: Operations that are lock-free should also be address-free. That is, atomic operations on the same
    memory location via two different addresses will communicate atomically. The implementation should not
    depend on any per-process state. This restriction enables communication by memory that is mapped into a
    process more than once and by memory that is shared between two processes. — end note ]

    Esto suena muy prometedor. 🙂

  • Esa nota parece provenir de N2427que es aún más explícito:

    Para facilitar la comunicación entre procesos a través de la memoria compartida, nuestra intención es que las operaciones sin bloqueo también estén libres de direcciones. Es decir, las operaciones atómicas en la misma ubicación de memoria a través de dos direcciones diferentes se comunicarán atómicamente. La implementación no dependerá de ningún estado por proceso. Si bien dicha definición está más allá del alcance del estándar, una declaración clara de nuestra intención permitirá una expresión portátil de clase de programas ya existentes.

    Entonces parece que sí, se supone que todas las operaciones sin bloqueo funcionan en este escenario exacto.

  • Ahora, las operaciones en std::atomic<type> son atómicos, pero pueden o no estar libres de bloqueo para determinados type, dependiendo de las capacidades de la plataforma. Y podemos comprobar cualquier variable x llamando x.is_lock_free().

  • Entonces, ¿por qué escribí que no dependería de esto? No puedo encontrar ningún tipo de documentación para gcc, llvm o cualquier otra persona que sea explícita al respecto.

  • En cualquier arquitectura “normal”, como x86, ARM, PowerPC, MIPS, etc., gcc y llvm (y otros compiladores sensatos) implementan atómicas sin bloqueo utilizando instrucciones asm que no tienen dirección y solo se preocupan por la dirección física. es decir, simplemente funcionan incluso cuando dos procesos separados tienen la misma página física asignada a diferentes direcciones virtuales con mmap.

    – Peter Cordes

    6 de febrero de 2019 a las 6:01

  • Las operaciones atómicas de lectura, modificación y escritura evitan que otros observadores lean o escriban la misma línea de caché en medio de un RMW al asegurarse de que este núcleo sea el único núcleo con una copia válida de la línea durante la operación de RMW. Todos los sistemas modernos usan alguna variante de MESI para la coherencia de caché, donde el estado Modificado significa que ningún otro núcleo puede tener ningún estado excepto Inválido. En x86, el núcleo toma un “bloqueo de caché” (no responde a las solicitudes para compartir la línea de caché hasta que se completa el RMW). En la mayoría de los otros con LL/SC, el almacenamiento condicional SC aborta la “transacción” si la línea no permaneció en M.

    – Peter Cordes

    6 de febrero de 2019 a las 6:04

  • Ver también ¿Puede num++ ser atómico para ‘int num’? para obtener más detalles sobre cómo funciona esto. Pero la clave es que los sistemas multinúcleo ya mantienen cachés coherentes, por lo que el RMW atómico se basa en eso.

    – Peter Cordes

    6 de febrero de 2019 a las 6:06

Hasta C++11, el estándar no especificaba cómo varios subprocesos compartían la memoria, por lo que escribimos programas con varios subprocesos que dependían del comportamiento específico de la implementación. El estándar aún no especifica cómo interactúan los procesos con memoria compartida, o si lo prefiere, los hilos que solo comparten parcialmente la memoria. Independientemente de lo que termine haciendo, confiará en garantías específicas de implementación.

Dicho esto, creo que una implementación que admita la memoria compartida de procesos intentará hacer que sus mecanismos de sincronización de subprocesos, como los atómicos, se puedan usar en la memoria compartida de procesos para la sincronización de procesos. Como mínimo, creo que sería difícil idear una implementación sin bloqueo de una especialización std::atomic que no funcione correctamente entre procesos.

  • Estoy de acuerdo, pero el Estándar explícitamente no requiere std::atomic estar libre de bloqueo.

    – Ben Voigt

    19 de agosto de 2013 a las 20:24

  • @BenVoigt True, pero lo consideraría una QoI deficiente hasta el punto de ser un error si una implementación de C ++ no admitiera atómicos de 64 bits sin bloqueo en X64, por ejemplo. Hay muchas áreas grises en la especificación estándar para el comportamiento indefinido, pero en muchos casos el comportamiento está limitado de manera realista por nuestras expectativas de lo que se permite para una implementación de calidad razonable. Si elimino la referencia a un puntero NULL, los demonios no saldrán volando de mi nariz; obtendré un SIGSEGV porque mi implementación no es una basura.

    – Casey

    19 de agosto de 2013 a las 20:34


  • No todas las implementaciones con memoria compartida admitirán std::atomic<int> en la memoria compartida, pero no es probable que use uno que no lo haga. Ya tomó la decisión de confiar en un comportamiento no estándar al usar la memoria compartida del proceso en primer lugar, lo que requiere más std::atomic<int> trabajar en esa memoria compartida no restringirá de manera realista su portabilidad potencial en mucho, si es que lo hace.

    – Casey

    19/08/2013 a las 20:35


  • @Casey desreferenciar un puntero NULL aún puede hacer que el demonio vuele desde tu nariz: pdos.csail.mit.edu/~xi/papers/stack-sosp13.pdf caso del kernel de linux.

    – Yanquis

    19 de agosto de 2013 a las 20:54

  • @Yankes No encuentro ocurrencias de las palabras “demonio” o “nasal” en el documento citado; tu argumento es invalido. Bromas aparte, artículo muy interesante.

    – Casey

    19 de agosto de 2013 a las 21:24

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad