Pthreads y tipos opacos
design struct (4)
Sí, normalmente una implementación escondería la mayoría de los detalles de dicha estructura, ya sea de esta manera (donde presumiblemente __SIZEOF_PTHREAD_MUTEX_T
está definido en algún archivo de encabezado de sistema previamente incluido):
typedef union
{
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
O así:
typedef union
{
#if __COMPILE_FOR_SYSTEM
struct __pthread_mutex_s
{
...internal struct member declarations...
} __data;
#endif
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
La primera forma aísla por completo las partes internas de la declaración struct del código del cliente. Conseguir acceso a las partes internas reales de la estructura requeriría entonces incluir un archivo de encabezado del kernel del sistema con la declaración de estructura completa, algo a lo que el código de cliente normal normalmente no tendría acceso. Como el código del cliente debe tratar solo con punteros a este tipo de estructura / unión, los miembros reales pueden permanecer ocultos de todos los códigos del cliente.
La segunda forma expone las estructuras internas de la estructura al programador, pero no al compilador (presumiblemente el __COMPILE_FOR_SYSTEM
se define en algún otro archivo de cabecera del sistema que solo se usaría al compilar el código del kernel).
La pregunta sigue siendo, entonces, ¿por qué los implementadores de esta biblioteca optaron por dejar los detalles internos visibles para el compilador? Después de todo, parece que la segunda solución sería muy fácil de proporcionar.
Supongo que cualquiera de los implementadores simplemente lo olvidó en este caso particular. O tal vez su código fuente y el archivo de encabezado está arreglado de manera imperfecta, por lo que necesitan mantener a los miembros expuestos para que funcionen sus compilaciones (pero esto es bastante dudoso).
Lamento que esto realmente no responda tu pregunta.
Estaba leyendo los archivos de encabezado de la biblioteca pthreads y encontré esta definición particular del mutex (y otros tipos) en bits / pthreadtypes.h:
typedef union
{
struct __pthread_mutex_s
{
int __lock;
unsigned int __count;
int __owner;
/* KIND must stay at this position in the structure to maintain
binary compatibility. */
int __kind;
unsigned int __nusers;
__extension__ union
{
int __spins;
__pthread_slist_t __list;
};
} __data;
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
No es exactamente así, pero lo simplifiqué para mayor claridad. Crear una estructura con dos definiciones diferentes en el encabezado y en el archivo de implementación, siendo la implementación la definición de estructura real y el encabezado solo un búfer de caracteres del tamaño de la estructura real, se usa como una técnica para ocultar la implementación (tipo opaco) ) pero aún asignar la cantidad correcta de memoria al llamar malloc o asignar un objeto en la pila.
Esta implementación particular está usando una unión y sigue exponiendo tanto la definición de la estructura como del búfer de caracteres, pero no parece proporcionar ningún beneficio en términos de ocultar el tipo ya que la estructura aún está expuesta y la compatibilidad binaria aún depende de las estructuras están sin cambios.
- ¿Por qué los tipos definidos en pthreads siguen este patrón?
- ¿Cuáles son los beneficios de tener tipos opacos si no se proporciona compatibilidad binaria (como en el patrón de puntero opaco)? Entiendo que la seguridad es una de ellas ya que no le está permitiendo al usuario manipular los campos de la estructura, pero ¿hay algo más?
- ¿Están los tipos pthread expuestos principalmente para permitir inicializaciones estáticas o hay alguna otra razón específica para esto?
- ¿Sería factible una implementación pthreads siguiendo el patrón de puntero opaco (es decir, no exponiendo ningún tipo en absoluto y no permitiendo inicializaciones estáticas)? o más específicamente, ¿hay alguna situación en la que un problema solo pueda resolverse con inicializaciones estáticas?
- Y sin ninguna relación, ¿hay hilos "antes principales" en C?
Los comités de estándares como IEEE y POSIX desarrollan y desarrollan estándares con iteraciones que proporcionan más funcionalidad o para corregir problemas con versiones anteriores de los estándares. Este proceso está impulsado por las necesidades de las personas que tienen necesidades de software de dominio de problemas, así como por los proveedores de productos de software que respaldan a esas personas. Por lo general, la implementación de un estándar variará en cierto grado entre los proveedores. Al igual que cualquier otro software, diferentes personas proporcionan diferencias en la implementación en función del entorno objetivo, así como de sus propias habilidades y conocimientos. Sin embargo, a medida que el estándar madura hay una especie de selección darwiniana en la que existe un acuerdo sobre las mejores prácticas y las diversas implementaciones comienzan a converger.
Las primeras versiones de una biblioteca pthreads POSIX fueron en la década de 1990 dirigidas a entornos de sistema operativo estilo UNIX, por ejemplo, ver POSIX. 4: Programación para el mundo real y ver también PThreads Primer: una guía para la programación multiproceso . Las ideas y conceptos para la biblioteca se originaron del trabajo realizado anteriormente en un intento de proporcionar una co-rutina o tipo de hilo de funcionalidad que funcionó en un nivel más fino que el nivel de proceso del sistema operativo para reducir los gastos generales que crean, administran y destruyen procesos involucrado. Hubo dos enfoques principales para el enhebrado, el nivel de usuario con poca compatibilidad con el kernel y el nivel del núcleo, dependiendo del sistema operativo para proporcionar la gestión de subprocesos, con capacidades algo diferentes, como el cambio anticipado de subprocesos o no estar disponible.
Además, también estaban las necesidades de los fabricantes de herramientas, como los depuradores, para proporcionar soporte para trabajar en un entorno de subprocesos múltiples y poder ver el estado del subproceso e identificar subprocesos específicos.
Existen varias razones para usar un tipo opaco dentro de la API para una biblioteca. La razón principal es permitir a los desarrolladores de la biblioteca la flexibilidad para modificar el tipo sin causar problemas a los usuarios de la biblioteca. Hay varias formas de crear tipos opacos en C.
Una forma es exigir a los usuarios de la API que utilicen un puntero a algún área de memoria que esté administrada por la biblioteca API. Puede ver ejemplos de este enfoque en la biblioteca estándar C con las funciones de acceso a archivos tales como fopen()
que devuelve un puntero a un tipo de archivo.
Si bien esto logra el objetivo de crear un tipo opaco, requiere que la biblioteca API administre la asignación de memoria. Como se trata de punteros, puede tener problemas para asignar la memoria y nunca liberarla o intentar utilizar un puntero cuya memoria ya se haya liberado. También significa que las aplicaciones especializadas en hardware especializado pueden tener dificultades para trasladar la funcionalidad, por ejemplo, a un sensor especializado con soporte básico que no incluye un asignador de memoria. Este tipo de sobrecarga oculta también puede afectar las aplicaciones especializadas con recursos limitados y ser capaz de predecir o modelar los recursos utilizados por una aplicación.
Una segunda forma es proporcionar a los usuarios de la API una estructura de datos que tenga el mismo tamaño que la estructura de datos real utilizada por la API, pero que utilice un búfer de caracteres para asignar la memoria. Este enfoque oculta los detalles del diseño de la memoria, ya que todo el usuario de la API ve que hay un único búfer o matriz, pero también asigna la cantidad correcta de memoria que utiliza la API. La API tiene su propia estructura que establece cómo se usa realmente la memoria y la API hace una conversión de puntero internamente para cambiar la estructura utilizada para acceder a la memoria.
Este segundo enfoque proporciona un par de bonitos beneficios. En primer lugar, la memoria utilizada por la API ahora la administra el usuario de la API y no la biblioteca en sí misma. El usuario de la API puede decidir si desea utilizar la asignación de la pila o la asignación estática global o alguna otra asignación de memoria como malloc()
. El usuario de la API puede decidir si desea ajustar la asignación de memoria en algún tipo de seguimiento de recursos, como un recuento de referencias o alguna otra gestión que el usuario quiera hacer de su parte (aunque esto también podría hacerse con tipos opacos de puntero) también). Este enfoque también permite al usuario de la API tener una mejor idea del consumo de memoria y modelar el consumo de memoria para aplicaciones especializadas en hardware especializado.
El diseñador de la API también podría proporcionar algunos tipos de datos para el usuario de la API que podrían ser útiles, como la información de estado. El objetivo de esta información de estado es permitir que el usuario de la API consulte qué equivale a leer solo miembros de la estructura directamente en lugar de pasar por la sobrecarga de algún tipo de función auxiliar en aras de la eficiencia. Si bien los miembros no se especifican como const
(para alentar al compilador de C a hacer referencia al miembro real en lugar de almacenar en caché el valor en algún momento dependiendo de que no cambie), la API puede actualizar los campos durante las operaciones para proporcionar información al usuario de la API sin depender de los valores de esos campos para su propio uso.
Sin embargo, dichos campos de datos corren el riesgo de presentar problemas de compatibilidad con versiones anteriores, así como también cambios que introducen problemas de diseño de memoria. El compilador de CA puede introducir relleno entre los miembros de una estructura para proporcionar instrucciones de máquina eficientes al cargar y almacenar datos en esos miembros o debido a la arquitectura de CPU que requiere algún tipo de límite de dirección de memoria de inicio para algunos tipos de instrucciones.
Específicamente para la biblioteca pthreads, tenemos la influencia de la programación estilo C de UNIX de los años 1980 y 1990 que solía tener estructuras de datos abiertas y visibles y archivos de cabecera que permitían a los programadores leer las definiciones de estructura y constantes definidas con comentarios ya que gran parte de la documentación disponible fue la fuente.
Un breve ejemplo de una estructura opaca sería la siguiente. Existe el archivo de inclusión, thing.h, que contiene el tipo opaco y que está incluido por cualquiera que use la API. Luego hay una biblioteca cuyo archivo fuente, thing.c, contiene la estructura real utilizada.
cosa.h puede parecerse
#define MY_THING_SIZE 256
typedef struct {
char array[MY_THING_SIZE];
} MyThing;
int DoMyThing (MyThing *pMyThing, int stuff);
Luego, en el archivo de implementación, thing.c, puede tener una fuente como la siguiente
typedef struct {
int thingyone;
int thingytwo;
char aszName[32];
} RealMyThing;
int DoMyThing (MyThing *pMyThing, int stuff)
{
RealMyThing *pReal = (RealMyThing *)pMyThing;
// do stuff with the real memory layout of MyThing
return 0;
}
Con respecto a los hilos "antes principales"
Cuando se inicia una aplicación que utiliza el tiempo de ejecución C, el cargador utiliza el punto de entrada para el tiempo de ejecución C como el lugar de inicio de la aplicación. El tiempo de ejecución C luego realiza la inicialización y la configuración ambiental que necesita hacer y luego invoca el punto de entrada designado para la aplicación real. Históricamente, este punto de entrada designado es la función main()
sin embargo, lo que utiliza el tiempo de ejecución C puede variar entre los sistemas operativos y los entornos de desarrollo. Por ejemplo, para una aplicación GUI de Windows, el punto de entrada designado es WinMain()
(vea el punto de entrada de WinMain ) en lugar de main()
.
Es hasta el tiempo de ejecución de C determinar las condiciones bajo las cuales se llama el punto de entrada designado para la aplicación. Si hay subprocesos "pre-main" en ejecución dependerá del tiempo de ejecución de C y el entorno de destino.
Con una aplicación de Windows que use controles Active-X con su propia bomba de mensajes, podría haber hilos "pre-main". Trabajo con una gran aplicación de Windows que usa varios controles que proporcionan varios tipos de interfaces de dispositivos y cuando miro en el depurador, puedo ver una cantidad de subprocesos que el origen de mi aplicación no crea con una llamada específica de creación de subprocesos. Estos subprocesos se inician por el tiempo de ejecución cuando los controles Active-X utilizados se cargan y se inician.
Mi opinión es que los campos __size
y __align
especifican (adivina qué :-)) el tamaño y la alineación de la estructura independientemente de la estructura __data
. Por lo tanto, los datos pueden ser de menor tamaño y tener menos requisitos de alineación, se pueden modificar libremente sin romper estas suposiciones básicas al respecto. Y viceversa, estas características básicas se pueden cambiar sin alterar la estructura de datos, como aquí .
Es importante tener en cuenta que si el tamaño de __data
vuelve más grande que lo especificado por __SIZEOF_PTHREAD_MUTEX_T
, una aserción falla en __pthread_mutex_init()
:
assert (sizeof (pthread_mutex_t) <= __SIZEOF_PTHREAD_MUTEX_T);
Considere esta afirmación como una parte esencial de este enfoque.
Entonces, la conclusión es que esto se hizo para no ocultar los detalles de implementación, sino para hacer que la estructura de datos sea más predecible y manejable. Es muy importante para una biblioteca ampliamente utilizada que debería preocuparse mucho por la compatibilidad con versiones anteriores y el impacto en el rendimiento de otros códigos a partir de los cambios que se pueden realizar en esta estructura.
He visto una unión de una estructura y un buffer de datos antes para soportar una palabra doble comparar e intercambiar instrucciones (que tienen requisitos de alineación específicos); esta instrucción puede ser lo que están usando para implementar las funciones mutex.
Permitir a los implementadores una mayor libertad para implementar su visión en una biblioteca de pthreads rápida y eficiente al tiempo que proporciona al usuario final una inferencia unificada.
Main es un concepto intrínseco, normalmente una función se llama before main para configurar descriptores de archivos estándar, entre otras cosas. En GCC puedes agregar el atributo ''__attribute__ ((constructor))'' a una función y se llamará antes de main (podría lanzar un montón de hilos y luego salir). Sin embargo, un proceso / subproceso de raíz que genere otros procesos o subprocesos siempre tiene que ser lo primero (en caso de que esa sea su pregunta).