Cada proceso bajo Linux es dinámicamente asignado a una estructura
struct task_struct. El número máximo de procesos que pueden ser creados bajo Linux está solamente limitado por la cantidad de memoria física presente, y es igual a (ver
kernel/fork.c:fork_init()):
/*
* El número máximo por defecto de hilos es establecido
* a un valor seguro: las estructuras de hilos pueden ocupar al
* menos la mitad de la memoria.
*/
max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 2;
lo cual, en la arquitectura IA32, básicamente significa
num_physpages/4. Como ejemplo, en una máquina de 512M, puedes crear 32k de hilos. Esto es una mejora considerable sobre el límite de 4k-epsilon para los núcleos viejos (2.2 y anteriores). Es más, esto puede ser cambiado en tiempo de ejecución usando el KERN_MAX_THREADS
sysctl(2), o simplemente usando la interfaz procfs para el ajuste del núcleo:
# cat /proc/sys/kernel/threads-max
32764
# echo 100000 > /proc/sys/kernel/threads-max
# cat /proc/sys/kernel/threads-max
100000
# gdb -q vmlinux /proc/kcore
Core was generated by `BOOT_IMAGE=240ac18 ro root=306 video=matrox:vesa:0x118'.
#0 0x0 in ?? ()
(gdb) p max_threads
$1 = 100000
El conjunto de procesos en el sistema Linux está representado como una colección de estructuras
struct task_struct, las cuales están enlazadas de dos formas:
- como una tabla hash, ordenados por el pid, y
- como una lista circular doblemente enlazada usando los punteros p->next_task y p->prev_task.
La tabla hash es llamada
pidhash[] y está definida en
include/linux/sched.h:
/* PID hashing. (¿no debería de ser dinámico?) */
#define PIDHASH_SZ (4096 >> 2)
extern struct task_struct *pidhash[PIDHASH_SZ];
#define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))
Las tareas son ordenadas por su valor pid y la posterior función de ordenación se supone que distribuye los elementos uniformemente en sus dominios de (
a
PID_MAX-1). La tabla hash es usada para encontrar rápidamente una tarea por su pid usando
find_task_pid() dentro de
include/linux/sched.h:
static inline struct task_struct *find_task_by_pid(int pid)
{
struct task_struct *p, htable = &pidhash[pid_hashfn(pid)];
for(p = *htable; p && p->pid != pid; p = p->pidhash_next)
;
return p;
}
Las tareas en cada lista ordenada (esto es, ordenadas por el mismo valor) son enlazadas por
p->pidhash_next/pidhash_pprev el cual es usado por
hash_pid() y
unhash_pid() para insertar y quitar un proceso dado en la tabla hash. Esto es realizado bajo la protección del spinlock read/write (lectura/escritura) llamado
tasklist_lock tomado para ESCRITURA.
La lista circular doblemente enlazada que usa
p->next_task/prev_task es mantenida para que uno pueda ir fácilmente a través de todas las tareas del sistema. Esto es realizado por la macro
for_each_task() desde
include/linux/sched.h:
#define for_each_task(p) \
for (p = &init_task ; (p = p->next_task) != &init_task ; )
Los usuarios de
for_each_task() deberían de coger la tasklist_lock para LECTURA. Destacar que
for_each_task() está usando
init_task para marcar el principio (y el final) de la lista - esto es seguro porque la tarea vacía (pid 0) nunca existe.
Los modificadores de los procesos de la tabla hash y/o los enlaces de la tabla de procesos, notablemente
fork(),
exit() y
ptrace(), deben de coger la
tasklist_lock para ESCRITURA. El motivo por el que esto es interesante es porque los escritores deben de deshabilitar las interrupciones en la CPU local. El motivo para esto no es trivial: la función
send_sigio() anda por la lista de tareas y entonces coge
tasklist_lock para ESCRITURA, y esta es llamada desde
kill_fasync() en el contexto de interrupciones. Este es el motivo por el que los escritores deben de deshabilitar las interrupciones mientras los lectores no lo necesitan.
Ahora que entendemos cómo las estructuras
task_struct son enlazadas entre ellas, déjanos examinar los miembros de
task_struct. Ellos se corresponden débilmente con los miembros de las estructuras de UNIX 'struct proc' y 'struct user' combinadas entre ellas.
Las otras versiones de UNIX separan la información del estado de las tareas en una parte, la cual deberá de ser mantenida en memoria residente durante todo el tiempo (llamada 'proc structure' la cual incluye el estado del proceso, información de planificación, etc.), y otra parte, la cual es solamente necesitada cuando el proceso está funcionando (llamada 'u_area' la cual incluye la tabla de descriptores de archivos, información sobre la cuota de disco etc.). El único motivo para este feo diseño es que la memoria era un recurso muy escaso. Los sistemas operativos modernos (bueno, sólo Linux por el momento, pero otros, como FreeBSD ( que parece que avanzan en esta dirección, hacia Linux) no necesitan tal separación y entonces mantienen el estado de procesos en una estructura de datos del núcleo residente en memoria durante todo el tiempo.
La estructura task_struct está declarada en
include/linux/sched.h y es actualmente de un tamaño de 1680 bytes.
El campo de estado es declarado como:
volatile long state; /* -1 no ejecutable, 0 ejecutable, >0 parado */
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 4
#define TASK_STOPPED 8
#define TASK_EXCLUSIVE 32
¿Por qué
TASK_EXCLUSIVE está definido como 32 y no cómo 16? Porque 16 fue usado por
TASK_SWAPPING y me olvidé de cambiar
TASK_EXCLUSIVE cuando quité todas las referencias a
TASK_SWAPPING (en algún sitio en 2.3.x).
La declaración
volatile en
p->statesignifica que puede ser modificada asincrónicamente (desde el manejador de interrupciones);
- TASK_RUNNING: significa que la tarea está "supuestamente" en la cola de ejecución. El motivo por lo que quizás no esté aún en la cola de ejecución es porque marcar una tarea como TASK_RUNNING y colocarla en la cola de ejecución no es atómico. Necesitarás mantener el spinlock read/write runqueue_lock en lectura para mirar en la cola de ejecución. Si lo haces, verás que cada tarea en la cola de ejecución está en el estado TASK_RUNNING. Sin embargo, la conversión no es verdad por los motivos explicados anteriormente. De una forma parecida, los controladores pueden marcarse a ellos mismos (o en realidad, en el contexto del proceso en el que están) como TASK_INTERRUPTIBLE (o TASK_UNINTERRUPTIBLE) y entonces llaman a schedule(), el cual entonces los quita de la cola de ejecución (a memos que exista una señal pendiente, en tal caso permanecen en la cola de ejecución).
- TASK_INTERRUPTIBLE: significa que la tarea está durmiendo pero que puede ser despertada por una señal o por la terminación de un cronómetro.
- TASK_UNINTERRUPTIBLE: lo mismo que TASK_INTERRUPTIBLE, excepto que no puede ser despertado.
- TASK_ZOMBIE: tareas que han terminado pero que no tienen su estado reflejado (para wait()-ed) por el padre (natural o por adopción).
- TASK_STOPPED: tarea que fue parada, por señales de control de trabajos o por ptrace(2).
- TASK_EXCLUSIVE: este no es un estado separado pero puede ser uno de TASK_INTERRUPTIBLE o TASK_UNINTERRUPTIBLE. Esto significa que cuando esta tarea está durmiendo o un una cola de espera con otras tareas, puede ser despertada sóla en vez de causar el problema de "movimiento general" despertando a todos los que están esperando.
Las banderas de las tareas contienen información sobre los estados de los procesos, los cuales no son mutuamente exclusivos:
unsigned long flags; /* banderas para cada proceso, definidas abajo */
/*
* Banderas para cada proceso
*/
#define PF_ALIGNWARN 0x00000001 /* Imprime mensajes de peligro de alineación */
/* No implementada todavía, solo para 486 */
#define PF_STARTING 0x00000002 /* Durante la creación */
#define PF_EXITING 0x00000004 /* Durante la destrucción */
#define PF_FORKNOEXEC 0x00000040 /* Dividido pero no ejecutado */
#define PF_SUPERPRIV 0x00000100 /* Usados privilegios de super-usuario */
#define PF_DUMPCORE 0x00000200 /* Núcleo volcado */
#define PF_SIGNALED 0x00000400 /* Asesinado por una señal */
#define PF_MEMALLOC 0x00000800 /* Asignando memoria */
#define PF_VFORK 0x00001000 /* Despertar al padre en mm_release */
#define PF_USEDFPU 0x00100000 /* La tarea usó de FPU este quantum (SMP) */
Los campos
p->has_cpu,
p->processor,
p->counter,
p->priority,
p->policy y
p->rt_priority son referentes al planificador y serán mirados más tarde.
Los campos
p->mm y
p->active_mm apuntan respectivamente a la dirección del espacio del proceso descrita por la estructura
mm_struct y al espacio activo de direcciones, si el proceso no tiene una verdadera (ej, hilos de núcleo). Esto ayuda a minimizar las descargas TLB en los intercambios del espacio de direcciones cuando la tarea es descargada. Por lo tanto, si nosotros estamos planificando el hilo del núcleo (el cual no tiene
p->mm) entonces su
next->active_mm será establecido al
prev->active_mm de la tarea que fue descargada, la cual será la misma que
prev->mm si
prev->mm != NULL. El espacio de direcciones puede ser compartido entre hilos si la bandera
CLONE_VM es pasada a las llamadas al sistema clone(2)
o vfork(2)
.
Los campos p->exec_domain y p->personality se refieren a la personalidad de la tarea, esto es, la forma en que ciertas llamadas al sistema se comportan para emular la "personalidad" de tipos externos de UNIX.
El campo p->fs contiene información sobre el sistema de archivos, lo cual significa bajo Linux tres partes de información:
- dentry del directorio raíz y punto de montaje,
- dentry de un directorio raíz alternativo y punto de montaje,
- dentry del directorio de trabajo actual y punto de montaje.
Esta estructura también incluye un contador de referencia porque puede ser compartido entre tareas clonadas cuando la bandera CLONE_FS es pasada a la llamada al sistema clone(2)
.
El campo p->files contiene la tabla de descriptores de ficheros. Esto también puede ser compartido entre tareas, suministrando CLONE_FILES el cual es especificado con clone(2)
.
El campo p->sig contiene los manejadores de señales y puede ser compartido entre tareas clonadas por medio de CLONE_SIGHAND.