@sarnold | bienvenidos al cuarto día de presentaciones de umeet :) |
---|---|
@sarnold | el primer conferenciante de hoy es william lee irwin, III |
@wli | Para dar un trasfondo general, el "hashing" (o desmenuzado) generalizado de pid se supone que mejora el rendimiento de los algoritmos internos al núcleo de manera que escalen mejor con el número de tareas. |
@sarnold | (por favor, dirijan sus preguntas y comentarios a #qc; gracias) |
@wli | En general, los sistemas grandes tienden a ejecutar cosas como servidores que hacen fork o muchos servidores diferentes (posiblemente de diferentes usuarios) simultáneamente. |
@wli | Casi todas las alteraciones al 2.5.x se hechas para esto tratan directamente con los problemas de complejidad computacional tradicional del análisis básico de algoritmos. |
@wli | De manera que para empezar a mirar más a fondo, examinemos algo de código del 2.4.x que se ha visto que tiene problemas. |
@wli | (no sólo los que tienen que ver directamente con el pidhashing, sino aquellos que implican búsquedas sobre todas las tareas) |
@wli | (1) count_active_tasks() |
@wli | Esto está intentando calcular la carga media. |
@wli | En 2.4.x, el cuerpo de la función es: |
@wli | for_each_task(p) { |
@wli | if ((p->state == TASK_RUNNING || |
@wli | (p->state & TASK_UNINTERRUPTIBLE))) |
@wli | nr += FIXED_1; |
@wli | } |
@wli | La noción es simple: contar el número de tareas que están en TASK_RUNNING o TASK_UNINTERRUPTIBLE |
@wli | el código de 2.5.x hace lo mismo, pero se implementa usando un principio diferente: |
@wli | "Ve contando, y cuando se te diga, devuelve el número." |
@wli | <grimoire:#qc> puede parecer tonto, ¿pero cómo se define FIXED_1? |
@wli | FIXED_1 es una constante que se usa para cálculos en aritmética de coma fija. |
@wli | Representa al 1. |
@wli | Hay un problema con esta estrategia: cambiar el estado provoca una sobrecarga en la contabilidad, pero examinar el estado es mucho menos costoso. |
@wli | La nueva sobrecarga en el conteo toma la forma de incrementar y decrementar contadores y mover cosas entre listas, etc. |
@wli | count_active_tasks() es bastante instrumental de otra manera: se ha comprobado que existe un problema de fiabilidad con el crecimiento de la velocidad de reloj en los servidores SMP (o al menos, he provocado el problema durante algunas pruebas). |
@wli | Esto se debe a que los tic del reloj ocurren con frecuencia, pero el tiempo que se pierde en él depende del número de tareas |
@wli | De manera que cuando se tarda demasiado en examinar todas las tareas, se puede experimentar lo que se llama "timeslice overrun". |
@wli | Este tipo de problema no es se restringe a esta función. |
@wli | (2) sys_setpgid() |
@wli | Esta función no se ejercita a menudo, o al menos no se considera normalmente como de rendimiento crítico, pero su implementación en 2.4.x presenta algunas desventajas. |
@wli | Si puedo detenerme un momento: |
@wli | <jmgv:#qc> ¿pero cuándo actualizas el contador de tareas? supuse que cada tarea añade 1 cuando nace y resta 1 antes de morir... ¿no es así? |
@wli | En general, las alteraciones sobre task-state se realizan para pasar un tipo de argumento implícito al planificador. |
@wli | De manera que básicamente, el planificador incrementa y decrementa el contador de TASK_RUNNING cuando las cosas duermen o despiertan, e incrementa y decrementa un segundo contador para TASK_UNINTERRUPTIBLE cuando las cosas duermen y despiertan. En principio, se podría usar un único contador, pero se necesita contar aisladamente TASK_RUNNING. |
@wli | Esto es nr_uninterruptible() y rq->nr_uninterruptible de 2.5.x |
@wli | De manera que volvamos a (2) |
@wli | La parte crítica a examinar en esto es: |
@wli | if (pgid != pid) { |
@wli | struct task_struct * tmp; |
@wli | for_each_task (tmp) { |
@wli | if (tmp->pgrp == pgid && |
@wli | tmp->session == current->session) |
@wli | goto ok_pgid; |
@wli | } |
@wli | goto out; |
@wli | En este punto vemos que suceden 2 comprobaciones: |
@wli | (A) comprobamos si algo alguien tiene ya un ID de grupo de procesos dado. |
@wli | (B) comprobamos si alguien está en la misma sesión. |
@wli | Le hace pensar a uno "¿No sería bueno si hubiera otra manera de averiguarlo?" |
@wli | En 2.5.x las cosas son un poco diferentes |
@wli | La vieja infraestructura de 2.4.x tiene una tabla hash sólo para task-pid |
@wli | En 2.5.x tenemos tablas hash para task->pid, task->tgid, task->pgrp, y task->session |
@wli | El mismo código se ve ahora así: |
@wli | for_each_task_pid(pgid, PIDTYPE_PGID, p, l, pid) |
@wli | if (p->session == current->session) |
@wli | goto ok_pgid; |
@wli | <seletz:#qc> puede que sea una pregunta tonta: ¿qué contienen estas tablas hash? valor y clave, quiero decir. |
@wli | bien, esta pregunta me ayudará a explicar qué es lo que sucede en ese código. |
@wli | Hay una "struct pid" que representa un "id de proceso" dado |
@wli | La noción de "id de proceso" es una generalización, de manera que no significa sólo PID, sino que incluye PID, PGID, TGID y SID. |
@wli | La struct pid tiene en su interior una lista de todas las tareas que comparten el mismo valor para ese ID. |
@wli | Por ejemplo, supongamos que hay 20 tareas en un grupo de procesos dado. |
@wli | La struct pid de ese pgrp aparecerá en las listas de 'buckets' de la tabla hash, y miraremos esa estructura, avanzando por la lista que hay en su interior. |
@wli | Esto debería darnos una idea de qué hay en for_each_task_pid(). |
@wli | #define for_each_task_pid(who, type, task, elem, pid) \ |
@wli | if ((pid = find_pid(type, who))) \ |
@wli | for (elem = pid->task_list.next, \ |
@wli | prefetch(elem->next), \ |
@wli | task = pid_task(elem, type); \ |
@wli | elem != &pid->task_list; \ |
@wli | elem = elem->next, prefetch(elem->next), \ |
@wli | task = pid_task(elem, type)) |
@wli | pid = find_pid(type, who) /* find_pid() hace búsquedas en tablas hash, y encuentra una struct pid que coincida */ |
@wli | específicamente, find_pid() toma como primer argumento el tipo de pid (PGID/SID/etc.), y como segundo argumento el valor. |
@wli | Se usa para inicializar el bucle. |
@wli | La parte del bucle examina la pid->task_list |
@wli | y esencialmente es un list_for_each_entry() |
@wli | Observarán que en una struct pid hay dos conjuntos de enlaces de lista: |
@wli | Uno para la tabla hash y otro para la lista de tareas: esto es para ayudar la compartición. |
@wli | Ahora es cuando entra en juego el principio que mencioné al respecto de la sobrecarga de cambios en contraposición a la sobrecarga de examen: |
@wli | Esta tablas tienen que actualizarse al hacer fork(), exec() y exit(). |
@wli | Hay otra clase de efectos secundarios: |
@wli | El bloqueo cambia en algunos casos (menos importantes). |
@wli | sys_setpgid() es uno de estos casos. |
@wli | En 2.4.x no había estructura asociada a pgrp que tuviera que ser alterada cuando cambiase, pero 2.4.x obtenía tasklist_lock al leer. |
@wli | En 2.5.x la hay, de manera que se debe obtener tasklist_lock al escribir. |
@wli | Este es un problema diferente: algunas operaciones son menos paralelizables ahora, pero son mucho más eficientes al hacerse en serie. |
@wli | La tendencia general con estas operaciones que han sido serializadas en 2.5.x es que son operaciones "poco comunes", aquellas que no se piden con frecuencia o no se consideran de rendimiento crítico, mientras que las operaciones aque han sido aceleradas no se veían afectadas seriamente por la sobrecarga del conteo, o fueron aceleradas drásticamente, y ya se realizaban en serie. |
@wli | Hay otro efecto secundario, y de hecho, se usó para promocionar esta estructura para el núcleo: |
@wli | Esta contabilidad evita la notificación explícita cuando se deja de usar un pid. |
@wli | A primera vista, esto no parece terriblemente importante, pero hay una operación en particular que me gustaría que conociesen. |
@wli | Durante fork(), se necesita asingar a un proceso un ID unívoco. A esto se le llama "asignación de pid". |
@wli | El asignador de pid de 2.4.x tiene una estrategia básica de «examina la lista de tareas y comprueba si el ID que he supuesto al principio sirve realmente». |
@wli | En realidad es un algoritmo un tanto refinado, pero no muy determinista. |
@wli | El asignador de 2.4.x mantiene una "ventana" de pids seguros, y mueve, y hace crecer o decrecer esta ventana según asigna pids y examina la lista de tareas. |
@wli | En el "mejor" caso, esto resulta en un rendimiento de O(tareas), pero en el peor, es de O(tareas**2) (cuadrático). Y en realidad es probable que tras una secuencia de operaciones, las cosa pasa a un comportamiento medio, de tal manera que aunque a veces lleve un tiempo O(tareas**2), en general es de O(tareas). |
@wli | Normalmente esto está bien, pero es más cuestionable en SMP: |
@wli | el problema es que su peor caso puede ser lo suficientemente severo cuando hay suficientes tareas como para que se puedan desatar los mecanismos de detección de interbloqueo, como el 'watchdog' de NMI, al competir las cosas por intentar hacer write_lock_irq(&tasklist_lock). |
@wli | El asignador de pid de 2.5.x usa las actualizaciones de la tabla de hash para ayudarse. |
@wli | void detach_pid(task_t *task, enum pid_type type) |
@wli | { |
@wli | int nr = __detach_pid(task, type); |
@wli | if (!nr) |
@wli | return; |
@wli | for (type = 0; type < PIDTYPE_MAX; ++type) |
@wli | if (find_pid(type, nr)) |
@wli | return; |
@wli | free_pidmap(nr); |
@wli | } |
@wli | El bucle for () es la parte importante aquí. |
@wli | Está haciendo «para todos los tipos de ID de procesos, si podemos encontrar este número de ID en uso, vuelve sin hacer nada" |
@wli | Si hay un número de ID completamente sin usar, lo marca en el mapa de bits. |
@wli | Este mapa de bits tiene un lugar para cada número pid posible, y si el bit está marcado, entonces el número de ID está siendo usado, y si no, no lo está. |
@wli | <sarnold:#qc> un mapa de bits de 32k para los pid? |
@wli | En realidad no |
@wli | El espacio de PID se ha hecho algo más grande para poder ejecutar más procesos. |
@wli | El espacio máximo permisible de PID es de 4M, y su tamaño se controla mediante sysctl |
@wli | En realidad hay dos niveles en el mapa: |
@wli | El primero es un nivel que apunta a páginas con una cuenta de pid libres en su interior. |
@wli | El segundo es una página cuyos bits se usan para llevar el rastro de los pid individuales. |
@wli | <chbm:#qc> uint32 |
@wli | <chbm:#qc> por qué no usarlo :) |
@wli | La única razón es que en general la sobrecarga de espacio de suficientes tareas para utilizar un espacio de PID tan grande lo hace impracticable, al menos en 32 bits. |
@wli | En máquinas de 64 bits con mucha memoria podría ser posible un número de tareas suficiente como para utilizarlo del todo, pero no es útil debido a la sobrecarga generada. |
@wli | Es lo suficientemente grande como para acomodar los números de tareas que piden las cargas de trabajo reales, lo cual normalmente no es mucho más de 100K en los casos más grandes, y como el tiempo de búsqueda es O(1) (volveré a esto en un momento), el tamaño del espacio ocupado es casi arbitrario. |
@wli | Lo notarán en alloc_pidmap(): |
@wli | scan_more: |
@wli | offset = find_next_zero_bit(map->page, BITS_PER_PAGE, offset); |
@wli | if (offset >= BITS_PER_PAGE) |
@wli | goto next_map; |
@wli | if (test_and_set_bit(offset, map->page)) |
@wli | goto scan_more; |
@wli | Esto significa que realmente está mirando el mapa de bits y (en teoría) tardando O(sizeof(pidspace)); en la práctica dominan los efectos de caché, y el algoritmo hace con frecuencia consultas directas a la lista de tareas, y se espera un rendimiento de O(1). |
@wli | Si ocupásemos algo de espacio adicional para indizado, se podría hacer que el peor caso fuera O(1), pero cualquier ganancia sobre el caso medio sería marginal. |
@wli | Direcciones para el futuro: |
@wli | El resto de consulta a la lista de tareas no son tan críticas en rendimiento como las anteriores. |
@wli | En el centro del núcleo, hay una operación poco común, que es establecer la prioridad de todas las tareas que pertenecen a un usuario dado, y varios caminos erróneos que buscan tareas que tengan un mm dado. |
@wli | También el establecer máscaras globales de capacidades (capabilities), y enviar señales a todas las tareas. |
@wli | No se consideró (bueno, yo no consideré) muy útil optimizar la búsqueda de mm para volcar un core ni el OOM killing, y la búsqueda de uid es una operación tan rara que no está muy claro que sea beneficiosa la sobrecarga de mantener los enlaces de una lista. |
@wli | También puede aparecer algunos otros casos "misceláneos": |
@wli | el módulo ipt_owner de netfilter comprueba la lista de tareas en busca de una tarea que tenga un task->comm coincidente |
@wli | En realidad esto puede ser de rendimiento crítico en máquinas que aguanten cargas de red intensivas mientras usan ipt_owner, pero no hay suficiente infraestructura para copar esto siendo un módulo. |
@wli | También hay una comprobación de mal comportamiento cuadrático de la lista de tareas en /proc, para simular la lectura del directorio raíz de /proc, donde aún no tenemos una solución aceptable. |
@wli | Una cosa en la que tenemos un camino claro es la validación de tareas. |
@wli | Hay un nuevo mecanismo para llevar una cuenta de referencias de estructuras de tareas que nos da algunas garantías de que una estructura de tarea siga siendo válida. |
@wli | Hay varias partes del núcleo, siendo notables los driver de control de entorno vm82 y SBUS, que quieren mantener referencias a tareas, pero el único método que tienen (creo) de verificar es comparar los punteros a la tabla de estructuras en una búsqueda por la lista de tareas. |
@wli | Esto no funciona realmente, porque las estructuras se reservan con slab y pueden ser reutilizadas por otras tareas. |
@wli | La solución ahora es bien llevar una cuenta de referencias a la estructura de la tarea o poner un 'hook' en release_thread() |
@wli | Claramente esto constituye una eliminación de fallos, de manera que no hay duda de que esta estrategia va a ser útil. |
@wli | Hay una infraestructura adicional que podría potencialmente permitir a netfilter, por ejemplo, acabar con el problema de la actualización de la tabla de hash: |
@wli | Esto es, registrar funciones para que sean llamadas durante fork(), exec(), exit() y otros eventos útiles relacionados con las tareas. |
@wli | En este momento, no hay una gran variedad de usuarios potenciales, y dada la naturaleza poco usual de las cosas que lo necesitan, puede que nunca llegue a hacerse. |
@wli | Pero tiene la posibilidad de proporcionar la infraestructura para características como el entorno ambiental de SBUS, vm86 y netfilter sean módulos y aún así sean eficientes. |
@wli | <cdub:#qc> ¿aún pretendes proporcionar una función de comprobación genérica con una callback para las comparaciones? |
@wli | Ahí mi intención en realidad era controlar muy estrictamente a los usuarios de tal interfaz y limitar su uso sólo a eventos catastróficos pre-aprobados de manear que se evitase la reintroducción de algoritmos de examen de la lista de tareas o código ineficiente en general; hoy día estoy trabajando menos en eso, y creo que dudo que fuera popular este tipo de control de admisión. |
@wli | <cdub:#qc> y LSM tiene 'hooks' en estos puntos que podrían hacer las actualizaciones (de la misma manera, creo que podrían ayudar los 'task ornaments'). |
@wli | Estaba intentando conectar los 'task ornaments' para este tipo de cosas. =) |
@wli | Reutilizar los 'hooks' de LSM para este tipo de eventos suena muy interesante, aunque podría ser difícil justificar poner algo como las necesidades de hacer hash de ->comm de ipt_owner en lo que se supone que es un modelo deseguridad... algo así como "renombrar el juego" (rename game) podría ser útil para tratar con este problema. |
@wli | Otra dirección a seguir que tiene bastantes posibilidades de realizarse es algún método de reducir el espacio reservado de forma estática para los hash de pid. |
@wli | Y algo para hacer a más largo plazo sería comparar los enfoques más deterministas de mapas de bits multinivel con el actual para determinar si el rendimiento medio durante cargas de trabajo reales se ve afectado de forma negativa por los efectos de caché al estar actualizando siempre varios niveles hasta el punto en que la ganancia en determinismo del algoritmo actual no merezca la pena. |
@wli | No tengo planeado investigar esto a menos que me encuentre realmente aburrido; ha sido imposible reproducir los peores casos. |
@wli | También hay que tener en cuenta que en las máquinas de 32 bits con PAE (que precisan tablas de páginas de tres niveles) las páginas de segundo nivel (pmd's) siempre están instanciadas. |
@wli | Esto significa 12KB de sobrecarga por tarea. |
@wli | 32K tareas * 12KB == 768MB, de manera que las pmd consumen toda la memoria baja llegados a ese punto, de manera que para un grupo muy importante, el espacio de 32K es bastante. |
@wli | Por último, pero no por ello menos, me gustaría dar las gracias a Ingo Molndar, cuyos arreglos de fallos, limpiezas de código y promoción de estos métodos consiguieron introducirlos en el núcleo. |
@wli | Bien, gracias a todos los que asistieron y escucharon, también. =) |
@sarnold | gracias wli :) |
@sarnold | gracias también a gareoda, que tradujo a holandés en #taee... |
@sarnold | vizard traducirá esto a español más tarde, para el sitio web... |
nota de traducción: al final, vizard no tuvo tiempo :P |