Cómo realizar la exclusión mutua y la sincronización entre múltiples subprocesos en Linux
Un problema que debe resolverse en los controladores de dispositivos Linux es el acceso concurrente de múltiples procesos a recursos compartidos. El acceso concurrente dará lugar a condiciones de carrera. Linux proporciona una variedad de formas de resolver estas condiciones. son adecuados para diferentes escenarios de aplicación.
El kernel de Linux es un sistema operativo multiproceso y multiproceso, que proporciona un método de sincronización del kernel bastante completo. La lista de métodos de sincronización del kernel es la siguiente:
Enmascaramiento de interrupción
Operaciones atómicas
Bloqueos de giro
Lectura y escritura de bloqueos de giro
Bloqueo secuencial
Semáforo
Semáforo de lectura y escritura
BKL (Big Kernel Lock)
Bloqueo secuencial
1. Concurrencia y condiciones de carrera:
Definición:
La concurrencia se refiere al hecho de que se ejecutan múltiples unidades de ejecución al mismo tiempo y en paralelo, y de manera concurrente. Las unidades de ejecución son El acceso a recursos compartidos (recursos de hardware y variables globales de software, variables estáticas, etc.) puede conducir fácilmente a condiciones de carrera.
En Linux, las principales condiciones de carrera ocurren en las siguientes situaciones:
1. Múltiples CPU multiprocesador simétrico (SMP)
Las características son: Uso de múltiples CPU. el mismo bus del sistema y por lo tanto tienen acceso a los mismos periféricos y memoria.
2. El proceso dentro de una sola CPU y el proceso que lo reemplaza
3. Entre interrupciones (interrupciones duras, interrupciones suaves, Tasklets, mitad inferior) y procesos
Mientras varias unidades de ejecución simultáneas tengan acceso a recursos compartidos, pueden ocurrir condiciones de carrera.
También puede ocurrir una condición de carrera si el controlador de interrupciones accede a un recurso al que accede el proceso.
Las interrupciones múltiples también pueden causar simultaneidad y generar condiciones de carrera (las interrupciones son interrumpidas por interrupciones de mayor prioridad).
La forma de resolver el problema de la carrera es garantizar el acceso mutuamente exclusivo a los recursos compartidos. El llamado acceso mutuamente exclusivo significa que cuando una unidad de ejecución accede a los recursos compartidos, otras unidades de ejecución están prohibidas.
El área de código que accede a los recursos compartidos se denomina sección crítica. La sección crítica debe estar protegida por algún tipo de mecanismo de exclusión mutua, las operaciones atómicas, los bloqueos de giro y los semáforos son todos Linux mutuamente. enfoques exclusivos disponibles en los controladores de dispositivos.
regiones críticas y condiciones de competencia:
las llamadas regiones críticas son segmentos de código que acceden y operan datos compartidos para evitar el acceso concurrente en regiones críticas, la programación del operador debe. asegúrese de que estos códigos se ejecuten de forma atómica, es decir, el código no se puede interrumpir antes de que se complete la ejecución, del mismo modo que toda la sección crítica es una instrucción indivisible. Si es probable que dos subprocesos de ejecución estén en la misma sección crítica, entonces el programa contiene. un error si esto sucede, lo llamamos condición de carrera. Evitar la concurrencia y prevenir las condiciones de carrera se llama sincronización.
Interbloqueo:
La aparición de un punto muerto requiere ciertas condiciones: debe haber uno o más subprocesos de ejecución y uno o más recursos, cada subproceso está esperando uno de los recursos, pero todos Los recursos han sido ocupados y todos los subprocesos se esperan entre sí, pero nunca liberarán los recursos que han ocupado, por lo que ningún subproceso puede continuar, lo que significa que se produce un punto muerto.
2. Enmascaramiento de interrupciones
Una forma sencilla de evitar condiciones de carrera dentro de una sola CPU es enmascarar las interrupciones del sistema antes de ingresar a la sección crítica.
Dado que la programación de procesos y otras operaciones del kernel de Linux dependen de interrupciones, el kernel puede evitar la concurrencia entre procesos.
Cómo utilizar el enmascaramiento de interrupciones:
local_irq_disable()//Enmascarar interrupciones
//Sección crítica
local_irq_enable()// Activar interrupciones
Características:
Dado que muchas operaciones importantes, como la E/S asíncrona y la programación de procesos en el sistema Linux, dependen de las interrupciones, no todas las interrupciones se pueden procesar durante el período de interrupción enmascarada, por lo que El enmascaramiento de tiempo a largo plazo es muy peligroso y puede causar pérdida de datos o incluso fallas del sistema. Esto requiere que después de enmascarar la interrupción, la ruta de ejecución actual del kernel ejecute el código en la sección crítica lo antes posible.
El enmascaramiento de interrupciones solo puede deshabilitar las interrupciones dentro de esta CPU, por lo que no puede resolver la condición de carrera causada por múltiples CPU. Por lo tanto, usar el enmascaramiento de interrupciones por sí solo no es un método recomendado para evitar condiciones de carrera. con Se utiliza con bloqueo de giro.
3. Operaciones atómicas
Definición: Las operaciones atómicas se refieren a operaciones que no serán interrumpidas por otras rutas de código durante la ejecución.
(Los átomos originalmente se referían a partículas indivisibles, por lo que las operaciones atómicas son instrucciones que no se pueden dividir)
(Asegura que las instrucciones se ejecuten de manera "atómica" y no se puedan Interrumpir)
Las operaciones atómicas son indivisibles y no serán interrumpidas por ninguna otra tarea o evento después de su ejecución. En un sistema monoprocesador (UniProcessor), las operaciones que se pueden completar en una sola instrucción pueden considerarse "operaciones atómicas" porque las interrupciones solo pueden ocurrir entre instrucciones. Esta es también la razón por la que instrucciones como test_and_set y test_and_clear se introducen en algunos sistemas de instrucciones de CPU para la exclusión mutua de recursos críticos. Sin embargo, es diferente en una estructura de multiprocesador simétrico (multiprocesador simétrico). Dado que hay varios procesadores ejecutándose de forma independiente en el sistema, incluso las operaciones que se pueden completar en una sola instrucción pueden verse interferidas. Tomemos como ejemplo decl (instrucción de disminución). Este es un proceso típico de "lectura-modificación-escritura" que implica dos accesos a la memoria.
Entendimiento popular:
Las operaciones atómicas, como su nombre indica, significan que no se pueden subdividir como los átomos. Una operación es una operación atómica, lo que significa que la operación se ejecuta de forma atómica y debe ejecutarse de una vez. El proceso de ejecución no puede ser interrumpido por otras acciones del sistema operativo. OS Otros comportamientos no pueden ser intervenidos.
Categoría: El kernel de Linux proporciona una serie de funciones para implementar operaciones atómicas en el kernel, que se dividen en operaciones atómicas enteras y operaciones atómicas de bits. El punto más común es que las operaciones son atómicas bajo cualquier circunstancia. , el código del kernel puede llamarlos de forma segura sin ser interrumpido.
Operaciones con enteros atómicos:
Las operaciones atómicas con números enteros solo pueden procesar datos de tipo atomic_t. La razón por la que aquí se introduce un tipo de datos especial en lugar de usar C directamente El tipo int del lenguaje. se usa principalmente por dos razones:
Primero, permitir que la función atómica solo acepte operandos del tipo atomic_t garantiza que las operaciones atómicas solo se puedan usar con este tipo especial de datos. que los datos de este tipo no se pasarán a ninguna otra función no atómica;
En segundo lugar, el uso del tipo atomic_t garantiza que el compilador no realice optimización de acceso en el valor correspondiente; esto hace que las operaciones atómicas finalmente reciban la dirección de memoria correcta, no un alias, y finalmente use atomic_t para enmascarar las diferencias al implementar operaciones atómicas en diferentes arquitecturas.
El uso más común de las operaciones con enteros atómicos es implementar contadores.
Otro punto a tener en cuenta es que las operaciones atómicas solo pueden garantizar que la operación sea atómica, ya sea completada o no, y no hay ni la mitad de posibilidades de que la operación se realice. Sin embargo, las operaciones atómicas no garantizan el orden de las operaciones. operaciones, es decir, no pueden garantizar que dos operaciones se completen en un orden determinado. Si desea garantizar el orden de las operaciones atómicas, utilice instrucciones de barrera de memoria.
Definición de Atomic_t y ATOMIC_INIT(i)
typedef struct { volatile int counter } atomic_t;
#define ATOMIC_INIT(i) { (i) }
Cuando escriba código, cuando pueda usar operaciones atómicas, trate de no usar mecanismos de bloqueo complejos. Para la mayoría de las arquitecturas, en comparación con métodos de sincronización más complejos, las operaciones atómicas le dan al sistema La sobrecarga que trae es pequeña y el costo. El impacto en la línea de caché es pequeño. Sin embargo, para aquellos códigos con requisitos de alto rendimiento, es aconsejable probar y comparar múltiples métodos de sincronización.
Operaciones de bits atómicos:
Las funciones que operan sobre datos a nivel de bits operan en direcciones de memoria ordinarias. Sus parámetros son un puntero y un número de bit.
Por conveniencia, el kernel también proporciona un conjunto de funciones de bits no atómicos correspondientes a las operaciones anteriores. Las operaciones de las funciones de bits no atómicos y las funciones de bits atómicos son exactamente las mismas. no garantiza la atomicidad y sus operaciones son El nombre tiene el prefijo dos guiones bajos más. Por ejemplo, la contraparte no atómica de test_bit() es _test_bit(). Si no necesita operaciones atómicas (por ejemplo, si ha protegido sus datos con un candado), entonces estas funciones de bits no atómicos son mejores que. las atómicas. Las funciones de bits pueden ejecutarse más rápido.
4. Bloqueo de giro
La introducción del bloqueo de giro:
Sería genial si cada sección crítica pudiera ser tan simple como agregar una variable, pero Es una lástima que en realidad este no sea el caso, pero la sección crítica puede abarcar múltiples funciones. Por ejemplo, los datos deben eliminarse de un resultado de datos, formatearse y analizarse, y finalmente agregarse a otra estructura de datos. ser atómico. Ningún otro código puede leer los datos antes de que se actualicen. Obviamente, las operaciones atómicas simples no tienen poder (en un sistema monoprocesador (UniProcessor), cualquier operación que se pueda completar en una sola instrucción puede considerarse "atómica"). operación" porque las interrupciones sólo pueden ocurrir entre instrucciones), lo que requiere el uso de un método de sincronización más complejo: un bloqueo para brindar protección.
Introducción a los bloqueos de giro:
El bloqueo más común en el kernel de Linux es un bloqueo de giro. Un bloqueo de giro solo puede ser mantenido por como máximo un subproceso ejecutable. intenta adquirir un bloqueo de giro en disputa (ya retenido), el hilo se mantendrá ocupado haciendo bucles, girando, esperando que el bloqueo esté disponible nuevamente. Si el bloqueo no está en disputa, el hilo de ejecución que solicita el bloqueo podrá obtenerlo inmediatamente y. Continuar la ejecución en cualquier momento, un bloqueo de giro evita que más de un hilo de ejecución ingrese al área de comprensión al mismo tiempo. Tenga en cuenta que el mismo bloqueo se puede usar en varios lugares, por ejemplo, para todos los accesos a un dato determinado. Se puede proteger y sincronizar.
Un bloqueo de giro disputado hace que el hilo que lo solicita gire mientras espera que el bloqueo vuelva a estar disponible (especialmente una pérdida de tiempo del procesador), por lo que los bloqueos de giro no deben mantenerse durante largos períodos de tiempo, en De hecho, esta es la intención original de usar bloqueos giratorios para realizar un bloqueo liviano en un corto período de tiempo, también puede tomar otra forma de manejar la contienda por el bloqueo: dejar que el hilo solicitante duerma hasta que el bloqueo vuelva a estar disponible y. luego despiértelo para que el procesador no tenga que esperar en un bucle y pueda ejecutar otro código, lo que también generará una cierta sobrecarga: aquí hay dos cambios de contexto obvios y el hilo bloqueado debe intercambiarse. fuera y se cambiara. Por lo tanto, el tiempo que lleva mantener un bloqueo de giro debe ser menor que el tiempo que lleva completar dos cambios de contexto. Por supuesto, la mayoría de nosotros no estamos lo suficientemente aburridos como para medir el tiempo que lleva cambiar de contexto, por lo que dejamos pasar el tiempo. Si mantenemos el bloqueo de giro lo más largo posible. Lo más corto posible, el semáforo puede proporcionar el segundo mecanismo mencionado anteriormente, que permite que el hilo en espera duerma en lugar de girar cuando ocurre la contención.
Los bloqueos de giro se pueden usar en manejadores de interrupciones (los semáforos no se pueden usar aquí porque causarán suspensión). Cuando use bloqueos de giro en manejadores de interrupciones, asegúrese de adquirir el bloqueo antes de deshabilitar primero las interrupciones locales (solicitudes de interrupción). en el procesador actual). De lo contrario, el controlador de interrupciones interrumpirá el código del núcleo que mantiene el bloqueo y puede intentar competir por el bloqueo de giro ya retenido. De esta manera, el controlador de interrupciones girará, esperando que se active el bloqueo. estará disponible nuevamente, pero el titular del bloqueo no puede ejecutarse antes de que el controlador de interrupciones complete la ejecución. Este es el punto muerto de solicitud doble que mencionamos en el capítulo anterior. Tenga en cuenta que solo es necesario desactivar la interrupción en el procesador actual. en un procesador diferente, incluso si el controlador de interrupciones gira en el mismo bloqueo, no impedirá que el titular del bloqueo (en un procesador diferente) finalmente libere el bloqueo.
Una comprensión simple de los bloqueos de giro:
La forma más fácil de entender los bloqueos de giro es tratarlos como una variable, que marca una sección crítica o "Actualmente estoy ejecutando, espere". un momento" o marcado como "No estoy ejecutando actualmente y puedo usarlo". Si la unidad de ejecución A ingresa primero a la rutina, mantendrá el bloqueo de giro. Cuando la unidad de ejecución B intenta ingresar a la misma rutina, aprenderá que el bloqueo de giro se ha mantenido y deberá esperar hasta que la unidad de ejecución A lo libere antes de poder ingresar. .
Funciones API de bloqueos de giro:
De hecho, los códigos fuente subyacentes de varios semáforos y mecanismos de exclusión mutua introducidos utilizan bloqueos de giro, que pueden entenderse como bloqueos de giro. Entonces, desde aquí puede comprender por qué los bloqueos de giro generalmente brindan un mayor rendimiento que los semáforos.
El bloqueo de giro es un dispositivo mutuamente excluyente. Sólo puede tener dos valores: "bloqueado" y "desbloqueado". Generalmente se implementa como un solo bit dentro de algún número entero.
La operación de "prueba y configuración" debe realizarse de forma atómica.
Cada vez que el código del kernel posee un bloqueo de giro, se desactivará la preferencia en la CPU asociada.
Reglas básicas que se aplican a los bloqueos de giro:
(1) Cualquier código que posea un bloqueo de giro debe ser atómico, excepto en el caso de interrupciones del servicio (y en algunos casos no puede abandonarse). la CPU, como el servicio de interrupción, también debe obtener un bloqueo de giro. Para evitar esta trampa de bloqueo, debe deshabilitar las interrupciones cuando tiene un bloqueo de giro), y no puede abandonar la CPU (como la hibernación, que puede ocurrir en muchos lugares inesperados). De lo contrario, es probable que la CPU gire para siempre (bloquee).
(2) Cuanto más corto sea el tiempo que tengas un bloqueo giratorio, mejor.
Cabe enfatizar que los bloqueos de giro no están diseñados para mecanismos de sincronización multiprocesador. Para procesadores únicos (para núcleos de un solo procesador y no interrumpibles, los bloqueos de giro no hacen nada), el kernel no introduce un. Mecanismo de bloqueo de giro al compilar. Para kernels interrumpibles, solo se usa como un interruptor para establecer si el mecanismo de preferencia del kernel está habilitado, es decir, el bloqueo y el desbloqueo en realidad deshabilitan o habilitan la función de preferencia. Si el kernel no admite la preferencia, los bloqueos de giro no se compilarán en el kernel en absoluto.
El tipo spinlock_t se utiliza en el kernel para representar bloqueos de giro, que se define en:
typedef struct {
raw_spinlock_t raw_lock;
# si está definido(CONFIG_PREEMPT) & definido(CONFIG_SMP)
unsigned int break_lock;
#endif
} spinlock_t;
Para los kernels que no soportan SMP, la estructura raw_spinlock_t no tiene nada y es una estructura vacía.
Para núcleos que admiten multiprocesadores, struct raw_spinlock_t se define como
typedef struct {
unsigned int slock
} raw_spinlock_t
< p; >slock indica el estado del bloqueo de giro, "1" indica que el bloqueo de giro está en el estado desbloqueado (UNLOCK) y "0" indica que el bloqueo de giro está en el estado bloqueado (LOCKED).break_lock indica si el proceso está actualmente esperando un bloqueo de giro. Obviamente, solo funciona en kernels SMP que admiten preferencia.
La implementación del bloqueo de giro es un proceso complejo, no por la cantidad de código o lógica que se requiere para implementarlo. De hecho, su código de implementación es muy pequeño. La implementación de bloqueos de giro está estrechamente relacionada con la arquitectura. El código central está escrito básicamente en lenguaje ensamblador. Los códigos centrales relacionados con la estructura de la asociación deportiva se colocan en directorios relevantes, como. Para nosotros, los desarrolladores de controladores, no necesitamos conocer los detalles internos de dicho spinlock. Si está interesado, consulte la lectura del código fuente del kernel de Linux. Para la interfaz spinlock de nuestro controlador, solo necesitamos incluir el archivo de encabezado. Antes de presentar la API de spinlock en detalle, primero echemos un vistazo al formato de uso básico de un bloqueo de giro:
#include
spinlock_t lock = SPIN_LOCK_UNLOCKED
<; p>spin_lock(amp; lock);....
spin_unlock(amp; lock);
En términos de uso, la API de spinlock es Aún así, es muy simple. Generalmente, las API que utilizamos son las siguientes. De hecho, todas son interfaces macro definidas en
#include
SPIN_LOCK_UNLOCKED
DEFINE_SPINLOCK
spin_lock_init( spinlock_t *)
spin_lock(spinlock_t *)
spin_unlock(spinlock_t *)
spin_lock_irq ( spinlock_t *)
spin_unlock_irq(spinlock_t *)
spin_lock_irqsace(spinlock_t *, banderas largas sin firmar)
spin_unlock_irqsace(spinlock_t *, banderas largas sin firmar) p >
spin_trylock(spinlock_t *)
inicialización de spin_is_locked(spinlock_t *)
Spinlock tiene dos formas de inicialización, una es inicialización estática y la otra es inicialización dinámica. Para objetos spinlock estáticos, usamos SPIN_LOCK_UNLOCKED para inicializar, que es una macro. Por supuesto, también podemos declarar spinlock e inicializarlo juntos. Este es el trabajo de la macro DEFINE_SPINLOCK. Por lo tanto, las siguientes dos líneas de código son equivalentes.
DEFINE_SPINLOCK (lock);
spinlock_t lock = SPIN_LOCK_UNLOCKED;
La función spin_lock_init se usa generalmente para inicializar el objeto spinlock_t creado dinámicamente y su parámetro es un puntero a spinlock_t Puntero al objeto. Por supuesto, también puede inicializar un objeto spinlock_t estático no inicializado.
spinlock_t *lock
......
spin_lock_init(lock); Obtener bloqueo
El kernel proporciona tres funciones utilizadas para adquirir un bloqueo de giro.
spin_lock: Obtiene el bloqueo de giro especificado.
spin_lock_irq: Desactiva las interrupciones locales y adquiere bloqueos de giro.
spin_lock_irqsace: guarda el estado de interrupción local, deshabilita las interrupciones locales y adquiere el bloqueo de giro y vuelve al estado de interrupción local.
Los bloqueos de giro se pueden usar en manejadores de interrupciones. En este caso, se debe usar una función con la función de desactivar las interrupciones locales. Recomendamos usar spin_lock_irqsave, porque guardará el indicador de interrupción antes del bloqueo. para que el indicador de interrupción al desbloquear se restablezca correctamente. Si la interrupción spin_lock_irq se desactiva al bloquear, la interrupción se activará por error al desbloquear.
Las otras dos funciones relacionadas con la adquisición del bloqueo de giro son:
spin_trylock(): intenta adquirir el bloqueo de giro. Si la adquisición falla, devolverá inmediatamente un valor distinto de cero. , de lo contrario devolverá 0 .
spin_is_locked(): determina si se ha adquirido el bloqueo de giro especificado. Si es así, devuelve un valor distinto de cero; de lo contrario, devuelve 0. Liberar bloqueos
En correspondencia con la adquisición de bloqueos, el kernel proporciona tres funciones relativas para liberar bloqueos de giro.
spin_unlock: Libera el bloqueo de giro especificado.
spin_unlock_irq: Libera el bloqueo de giro y activa la interrupción local.
spin_unlock_irqsave: libera el bloqueo de giro y restaura el estado de interrupción local guardado.
5. Bloqueo de giro de lectura y escritura
Si los datos protegidos por la sección crítica se pueden leer y escribir, siempre que no haya ninguna operación de escritura, se pueden admitir operaciones de lectura simultáneas. . Para este requisito de que solo las operaciones de escritura sean mutuamente excluyentes, obviamente es imposible cumplir con este requisito si todavía usamos bloqueos de giro (es un desperdicio para las operaciones de lectura). Para este propósito, el kernel proporciona otro tipo de bloqueo: un bloqueo de giro de lectura y escritura. Un bloqueo de giro de lectura también se denomina bloqueo de giro compartido, y un bloqueo de giro de escritura también se denomina bloqueo de giro exclusivo.
El bloqueo de giro de lectura y escritura es un mecanismo de bloqueo con una granularidad menor que el bloqueo de giro. Conserva el concepto de "giro", pero en términos de operaciones de escritura, solo puede haber como máximo una escritura. En términos de operaciones de lectura, puede haber varias unidades de ejecución de lectura al mismo tiempo. Por supuesto, la lectura y la escritura no se pueden realizar al mismo tiempo.
El uso de bloqueos de giro de lectura y escritura es muy similar al uso de bloqueos de giro ordinarios. Primero, el objeto de bloqueo de giro de lectura y escritura debe inicializarse:
// Inicialización estática.
rwlock_t rwlock = RW_LOCK_UNLOCKED;
//Inicialización dinámica
rwlock_t *rwlock;
...
rw_lock_init(rwlock);
Adquiere un bloqueo de giro de lectura para datos compartidos en el código de operación de lectura:
read_lock(amp; rwlock);
.. . p>
read_unlock(amp; rwlock);
Adquiera el bloqueo de giro de escritura para datos compartidos en el código de operación de escritura:
write_lock(amp; rwlock);
...
write_unlock(amp; rwlock);
Cabe señalar que si hay una gran cantidad de operaciones de escritura, las operaciones de escritura spin El bloqueo de giro de escritura está en un estado de inanición de escritura (esperando que se liberen todos los bloqueos de giro de lectura), porque el bloqueo de giro de lectura adquirirá libremente el bloqueo de giro de lectura.
Las funciones de lectura y escritura de bloqueos de giro son similares a los bloqueos de giro ordinarios, por lo que no los presentaremos uno por uno aquí. Los enumeramos en la siguiente tabla.
RW_LOCK_UNLOCKED
rw_lock_init(rwlock_t *)
read_lock(rwlock_t *)
read_unlock(rwlock_t *)
read_lock_irq(rwlock_t *)
read_unlock_irq(rwlock_t *)
read_lock_irqsave(rwlock_t *, largo sin firmar)
read_unlock_irqsave(rwlock_t *, largo sin firmar) p> p>
write_lock(rwlock_t *)
write_unlock(rwlock_t *)
write_lock_irq(rwlock_t *)
write_unlock_irq(rwlock_t *) p>
write_lock_irqsave(rwlock_t *, unsigned long)
write_unlock_irqsave(rwlock_t *, unsigned long)
rw_is_locked(rwlock_t *)
6.
El bloqueo secuencial (seqlock) es una optimización del bloqueo de lectura y escritura. Si se utiliza el bloqueo secuencial, la unidad de ejecución de lectura nunca será bloqueada por la unidad de ejecución de escritura. ser bloqueado por la unidad de ejecución de escritura Al escribir en un recurso compartido protegido por un bloqueo de secuencia, aún puede continuar leyendo sin esperar a que la unidad de ejecución de escritura complete la operación de escritura. unidades de ejecución para completar la operación de lectura antes de realizar la operación de escritura.
Sin embargo, la unidad de ejecución de escritura y la unidad de ejecución de escritura siguen siendo mutuamente excluyentes, es decir, si una unidad de ejecución de escritura está realizando una operación de escritura, otras unidades de ejecución de escritura deben girar donde están hasta que se ejecute la escritura. La unidad es liberada. El orden es trivial.
Si la unidad de ejecución de escritura ya ha realizado una operación de escritura durante la operación de lectura de la unidad de ejecución de lectura, entonces la unidad de ejecución de lectura debe volver a leer los datos para garantizar que los datos obtenidos estén completos. se utiliza en operaciones de lectura y escritura, la probabilidad de ejecución simultánea es relativamente pequeña, el rendimiento es muy bueno y permite realizar lectura y escritura al mismo tiempo, lo que mejora enormemente la concurrencia.
Tenga en cuenta que el bloqueo de secuencia tiene una limitación, es decir, debe protegerse. El recurso compartido no contiene un puntero, porque la unidad de ejecución de escritura puede invalidar el puntero, pero si la unidad de ejecución de lectura está intentando acceder al puntero, causará Ups.
7. Semáforo
El semáforo en Linux es un bloqueo de suspensión. Si una tarea intenta obtener un semáforo ya ocupado, el semáforo lo empujará hacia adelante. déjelo dormir. En este momento, el procesador puede recuperar su libertad para ejecutar otro código. Cuando el proceso que contiene el semáforo libera el semáforo, qué tarea en la cola de espera se despierta y obtiene el semáforo.
El semáforo, o bandera, es la operación primitiva P/V clásica que aprendemos en el sistema operativo.
P: Si el valor del semáforo es mayor que 0, disminuya el valor del semáforo y el programa continúa ejecutándose. De lo contrario, duerma y espere a que el semáforo sea mayor que 0.
V: Incrementa el valor del semáforo. Si el valor del semáforo incrementado es mayor que 0, despierta el proceso de espera.
El valor del semáforo determina cuántos procesos pueden ingresar a la sección crítica al mismo tiempo. Si el valor inicial del semáforo es 1, el semáforo es un semáforo mutuamente excluyente (MUTEX). Para semáforos con valores distintos de cero mayores que 1, también se les puede llamar semáforo de conteo. Los semáforos utilizados por los conductores generales son semáforos mutuamente excluyentes.
Al igual que los bloqueos de giro, la implementación de los semáforos también está estrechamente relacionada con la arquitectura. La implementación específica se define en el archivo de encabezado. Para los sistemas x86_32, su definición es la siguiente:
recuento atómico_t;
int durmientes;
wait_queue_head_t espera;
};
El recuento de valores iniciales del semáforo es de tipo atomic_t, que es un tipo de operación atómica. También es una tecnología de sincronización del núcleo. Se puede ver que el semáforo se basa en operaciones atómicas. Presentaremos las operaciones atómicas en detalle en la sección de operaciones atómicas más adelante.
El uso de semáforos es similar a los bloqueos de giro, incluyendo creación, adquisición y liberación.
Primero mostremos la forma de uso básica de los semáforos:
static DECLARE_MUTEX(my_sem);
......
if (down_interruptible(amp ;my_sem ))
{
return -ERESTARTSYS;
}
......
arriba (amp;my_sem)
La interfaz de la función de semáforo en el kernel de Linux es la siguiente:
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name
seam_init(struct semaphore *, int);
init_MUTEX(struct semaphore *);
init_MUTEX_LOCKED(struct semaphore *)
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *) señal de inicialización Cantidad
La inicialización de semáforos incluye inicialización estática e inicialización dinámica. La inicialización estática se utiliza para declarar e inicializar el semáforo estáticamente.
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
Para semáforos declarados o creados dinámicamente, puede utilizar la siguiente función para inicializar :
seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)
Obviamente, con La función con MUTEX inicializa el semáforo mutex. LOCKED inicializa el semáforo al estado bloqueado. Usando el semáforo
Una vez completada la inicialización del semáforo, podemos usarlo
down_interruptible(struct semaphore *);
down(struct semaphore *) p> p>
down_trylock(struct semaphore *)
up(struct semaphore *)
La función down intentará obtener el semáforo especificado si el semáforo ya está en uso. Luego, el proceso entra en un estado de suspensión ininterrumpible. down_interruptible hará que el proceso entre en un estado de suspensión interrumpible. En cuanto a los detalles del estado del proceso, lo introduciremos en detalle en la gestión de procesos del kernel.
down_trylock intenta obtener el semáforo. Si la adquisición tiene éxito, devolverá 0. Si falla, inmediatamente devolverá un valor distinto de 0.
Utilice la función up para liberar el semáforo al salir de la sección crítica. Si la cola de suspensión en el semáforo no está vacía, active uno de los procesos en espera.
8. Leer y escribir semáforos.
Al igual que los bloqueos giratorios, los semáforos también tienen semáforos de lectura y escritura.
La API de lectura y escritura del semáforo se define en el archivo de encabezado. Su definición en realidad está relacionada con la arquitectura, por lo que la implementación específica se define en el archivo de encabezado. El siguiente es un ejemplo para x86:
struct rw_semaphore. {
cuenta larga firmada;
spinlock_t wait_lock;
struct list_head wait_list;
};