[Notas de lectura] Diseño del kernel de Linux y gestión del proceso de implementación

1. Proceso

Un proceso es un programa en ejecución (el código de destino se almacena en un medio de almacenamiento).

ps: el proceso no se limita a una sección de código ejecutable (también llamada sección de código, sección de texto). Por lo general, el proceso también contiene otros recursos, como: archivos abiertos, semáforos suspendidos.

Un hilo de ejecución, o hilo para abreviar, es un objeto que está activo en un proceso. Cada hilo tiene un contador de programa independiente, una pila de procesos y un conjunto de registros de proceso.

El objeto programado por el núcleo es un hilo, no un proceso.

ps: La implementación de subprocesos del sistema Linux es muy especial: no distingue entre subprocesos y procesos. Es decir, un hilo no es más que un proceso especial.

2. Descriptor de proceso y estructura de tareas

El núcleo almacena la lista de procesos en una lista enlazada doblemente circular llamada lista de tareas.
Cada elemento de la lista vinculada es del tipo task_struct, denominado estructura de descriptor de proceso (descriptor de proceso), definida en <linux / sched.h>.
El descriptor de proceso contiene toda la información sobre un proceso específico.

Los datos contenidos en el descriptor de proceso pueden describir completamente un programa que se está ejecutando: el archivo que abre, el espacio de direcciones del proceso, las señales pendientes, el estado del proceso, etc.
Inserte la descripción de la imagen aquí

2.1 Asignación de descriptores de proceso

Inserte la descripción de la imagen aquí

2.2 Almacenamiento de descriptores de proceso-PID

El núcleo identifica cada proceso por un valor de identificación de proceso único o PID.
PID es un número, expresado como el tipo implícito de pid_t, que en realidad es un tipo int.
PID es en realidad el número máximo de procesos concurrentes permitidos en el sistema.
El valor predeterminado máximo de PID es 32768 (limitado por el valor máximo de PID definido en <linux / thread.h>), que se puede ver a través de / proc / sys / kernel / pid_max.

2.3 Estado del proceso

El campo de estado en el descriptor de proceso describe el estado actual del proceso. Los siguientes cinco tipos :

  1. TASK_RUNNING (Run R): el proceso es ejecutable: se está ejecutando o está esperando ser ejecutado en la cola de ejecución.
  2. TASK_INTERRUPTIBLE (S interrumpible): el proceso está en suspensión (bloqueado), esperando que se cumplan ciertas condiciones.
  3. TASK_UNINTERRUPTIBLE (Ininterrumpible D): este estado es el mismo que el estado interrumpible, excepto que no se despertará ni estará listo para funcionar incluso si se recibe una señal.
  4. __TASK_TRACED (Z) -Procesos rastreados por otros procesos, como el proceso del proceso de seguimiento a través de ptrace.
  5. __TASK_STOPPED (detener T): el proceso deja de ejecutarse; el proceso no se pone en funcionamiento ni se puede poner en funcionamiento. Por lo general, este estado se produce cuando se reciben SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU y otras señales Además, cualquier señal recibida durante la depuración hará que el proceso ingrese a este estado.

Inserte la descripción de la imagen aquí

2.4 Establecer el estado actual del proceso - set_task_state

set_task_state(task,state);  /*等价于*/
task->state = state;

PD:

set_current_state(state); /* 等价于,参考<linux/sched.h> */
set_task_state(current,state);

2.5 Contexto del proceso

El código del programa ejecutable es una parte importante del proceso.
Estos códigos se cargan desde un archivo ejecutable en el espacio de direcciones del proceso para su ejecución.
Los programas generales se ejecutan en el espacio del usuario. Cuando una llamada de programa ejecuta una llamada del sistema o desencadena una excepción, cae en el espacio del kernel .
En este punto, llamamos al núcleo "ejecutado en nombre del proceso" y en el contexto del proceso .

2.6 Árbol genealógico del proceso: todos los procesos son descendientes del proceso init con PID 1

La relación entre procesos se almacena en el descriptor de procesos.
Cada task_struct contiene un puntero a su proceso padre tast_struct, j llamado padre, y también contiene una lista de procesos hijos llamados hijos.

P: ¿Cómo obtener el descriptor de proceso del proceso padre?
A:struct task_struct *my_parent = current->parent;

P: ¿Cómo acceder al proceso secundario?
A:

struct task_struct *task;
struct list_head *list;

list_for_each(list,&current->children)
{
	task = list_entry(list,struct task_struct,sibling);
}

El descriptor de proceso del proceso init está asignado estáticamente como init_task.
Tales como:

struct task_struct *task;
for(task = current;task != &init_task; task = task->parent);
/*task 现在指向init*/

ps: la
macro for_each_process (task) proporciona la capacidad de acceder secuencialmente a toda la cola de tareas (lista enlazada circular bidireccional), pero el costo de atravesar todos los procesos a través de la repetición en un sistema con una gran cantidad de procesos es muy alto.
Por lo tanto, si no hay una buena razón (o no hay otra forma), no lo hagas.
por ejemplo:

struct task_struct *task;
for_each_process(task){
	/*打印出每一个任务(进程)的名称和PID*/
	printk("%s[%d]\n",task->comm,task->pid);
}

3. Creación de procesos-fork y funciones de familia ejecutiva

fork () crea un proceso hijo copiando el proceso actual.
Las funciones de la familia exec copian y leen el archivo ejecutable y lo cargan en el espacio de direcciones para comenzar a ejecutarse.

3.1 Copia en escritura una de las razones por las que Linux tiene la capacidad de ejecutar procesos rápidamente

La tecnología COW se refiere a la copia del recurso solo cuando necesita ser escrita, antes de eso, solo se compartía de una manera de solo lectura (es decir, los procesos padre e hijo comparten la misma copia).

3.2 tenedor ()

Linux implementa fork () a través de la llamada al sistema clone ().
El siguiente es el flujo general de llamadas:

fork()->clone()->do_fork()->copy_process()

ps: do_fork () se define en el archivo kernel / fork.c. (Puede existir en diferentes rutas con diferentes versiones de kernel) La
función do_fork llama a la función copy_process y luego permite que la ciudad comience a ejecutarse.
El flujo de trabajo de la función Copy_process es el siguiente:

  1. Llame a dup_task_struct () para crear una pila de kernel, una estructura thread_info y task_struct para el nuevo proceso. Estos valores son los mismos que los valores actuales ingresados ​​en la ciudad. En este momento, los descriptores del proceso secundario y del proceso primario son exactamente los mismos.
  2. Verifique para asegurarse de que después de crear este subproceso nuevamente, el número de procesos que posee el usuario actual no exceda el límite de recursos asignados a él.
  3. El proceso hijo se propone distinguirse del proceso padre. Muchos miembros en el descriptor de proceso deben borrarse o establecerse en valores iniciales. Los que no son miembros heredados del descriptor de entrada son principalmente información estadística. La mayoría de los datos en task_struct permanece sin modificar.
  4. El estado del proceso secundario se establece en TASK_UNINTERRUPTIBLE para garantizar que no se ponga en funcionamiento.
  5. copy_process () llama a copy_flags () para actualizar el miembro de banderas de task_struct. El indicador PF_SUPERPRIV que indica si el proceso tiene autoridad de superusuario se borra a 0. El indicador PF_FORKNOEXEC que indica que el proceso aún no ha llamado a la función exec () está configurado.
  6. Llame a alloc_pid () para asignar un PID válido al nuevo proceso.
  7. De acuerdo con los indicadores de parámetros pasados ​​a clone (), copy_process () copia o comparte archivos abiertos, información del sistema de archivos, funciones de procesamiento de señales, espacio de direcciones de proceso y espacio de nombres. En circunstancias normales, estos recursos serán compartidos por todos los hilos de un proceso dado, de lo contrario, estos recursos son diferentes para cada proceso, por lo que se copian aquí (COW).
  8. Finalmente, copy_process () realiza el trabajo de limpieza de cola y devuelve un puntero al proceso secundario.

Luego regrese a la función do_fork (). Si la función copy_process () regresa con éxito, el proceso hijo recién creado se activa y se pone en funcionamiento.
El núcleo elige deliberadamente ejecutar primero el proceso hijo (que no siempre es el caso). Generalmente, el proceso secundario llamará inmediatamente a la función de familia ejecutiva, lo que puede evitar la sobrecarga adicional de la copia al escribir. Si el proceso principal se ejecuta primero, puede comenzar a escribir en el espacio de direcciones.

3.3 vfork ()

La llamada al sistema vfork () tiene la misma función que fork (), excepto que no copia la entrada de la tabla de páginas del proceso padre.
El proceso secundario se ejecuta como un subproceso separado del proceso primario en su espacio de direcciones. El proceso primario se bloquea hasta que el proceso secundario salga o ejecute exec ().
Los procesos secundarios no pueden escribir en el espacio de direcciones.

ps: Idealmente, el sistema no debería llamar a vfork (), ni el núcleo necesita implementarlo.

4. Implementación de hilos en Linux

El mecanismo de subprocesos proporciona un conjunto de subprocesos que se ejecutan en un espacio de direcciones de memoria compartida dentro del mismo programa.
Linux trata todos los hilos como procesos.
Un hilo simplemente se considera como un proceso que comparte ciertos recursos con otros procesos . (Cada subproceso tiene su propia task_struct, por lo que en el núcleo, parece un proceso ordinario, pero el subproceso y algunos otros procesos comparten ciertos recursos, como el espacio de direcciones)

4.1 Crear un hilo

La creación de subprocesos es similar a la creación de procesos ordinarios, excepto que cuando se llama a clone (), debe pasar algunos indicadores de parámetros para indicar los recursos que deben compartirse:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0);  /*结果和调用fork()差不多,只是父子俩共享地址空间,文件系统资源,文件描述符和信号处理程序*/

El indicador de parámetro pasado a la función de clonación determina el comportamiento del proceso recién creado y el tipo de recursos compartidos entre los procesos padre e hijo.
Como se muestra en la siguiente tabla: (<linux / sched.h>)

Bandera de parámetros Significado
CLONE_FILES Los procesos primarios y secundarios comparten archivos abiertos
CLONE_FS Proceso padre-hijo compartido información del sistema de archivos
CLONE_IDLETASK Establezca PID en 0 (solo utilizado por procesos inactivos)
CLONE_NEWNS Crear un nuevo espacio de nombres para el proceso secundario
CLONE_PARENT Especifica que el proceso secundario y el proceso primario tienen el mismo proceso primario
CLONE_PTRACE Continuar depurando el proceso hijo
CLONE_SETTID Escribir TID de vuelta al espacio del usuario
CLONE_SETTLS Crear un nuevo TLS para el proceso secundario
CLONE_SIGHAND Los procesos padre e hijo comparten funciones de procesamiento de señales y señales bloqueadas
CLONE_SYSVSEM Los procesos padre e hijo comparten la semántica del Sistema V SEM_UNDO
CLONE_THREAD Los procesos padre e hijo se colocan en el mismo grupo de hilos
CLONE_VFORK Se llama a Vfork (), por lo que el proceso padre está listo para dormir y esperar a que el proceso hijo lo despierte
CLONE_UNTRACED Evite que el proceso de seguimiento imponga CLONE_PTRACE en el proceso secundario
CLONE_STOP Inicie el proceso en estado TASK_STOPPED
CLONE_SETTLS Cree un nuevo TLS (almacenamiento local de subprocesos) para el proceso secundario
CLONE_CHILD_CLEARTID Borrar el TID del proceso hijo
CLONE_CHILD_SETTID Establecer el TID del proceso hijo
CLONE_PARENT_SETTID Establecer el TID del proceso padre
CLONE_VM Espacio de direcciones compartidas de proceso padre-hijo

ps:
El concepto de proceso inactivo:
simplemente decir inactivo es un proceso y su número pid es 0. Su predecesor fue el primer proceso creado por el sistema, y ​​el único proceso que no fue generado por fork (). En el sistema smp, cada unidad de procesador tiene una cola de ejecución independiente, y cada cola de ejecución tiene un proceso inactivo, es decir, tantas unidades de procesador como procesos inactivos. El tiempo de inactividad del sistema en realidad se refiere al "tiempo de ejecución" del proceso de inactividad. El proceso inactivo pid == o, que es init_task.

4.2 Subprocesos del núcleo: procesos estándar que se ejecutan independientemente en el espacio del núcleo

La diferencia entre los hilos del núcleo y los procesos ordinarios es que los hilos del núcleo no tienen un espacio de direcciones independiente (en realidad, el puntero mm al espacio de direcciones se establece en NULL).
Solo se ejecutan en el espacio del kernel y nunca cambian al espacio del usuario.
El proceso del kernel y el proceso ordinario se alejan y se pueden invocar y evitar.

5. Terminación del proceso - do_exit ()

do_exit () es invocado por la llamada al sistema exit (), definida en kernel / exit.c, y hace lo siguiente:

  1. Establezca el miembro indicador en task_struct (escrito como tast_struct en el libro) en PF_EXITING.
  2. Llame a del_timer_sync () para eliminar cualquier temporizador central. Según los resultados devueltos, garantiza que no se pongan en cola los temporizadores y que no se ejecuten controladores de temporizadores.
  3. Si la función de contabilidad del proceso BSD está habilitada, do_exit () llama a acct_update_integrals () para generar información de contabilidad.
  4. Luego llame a la función exit_mm () para liberar el mm_struct ocupado por el proceso. Si ningún otro proceso los usa (es decir, este espacio de direcciones no se comparte), libérelos por completo.
  5. Luego llame a la función sem__exit (). Si el proceso pone en cola la señal IPC, deja la cola.
  6. Llame a exit_files () y exit_fs () para disminuir el recuento de referencias de descriptores de archivos y datos del sistema de archivos, respectivamente. Si el valor de uno de los recuentos de referencia cae a cero, significa que ningún proceso está utilizando los recursos correspondientes y puede liberarse en este momento.
  7. Luego configure el código de salida de la tarea almacenado en el miembro de código_salida de task_struct como el código de salida proporcionado por exit (), o complete cualquier otra acción de salida especificada por el mecanismo del núcleo. El código de salida se almacena aquí para que el proceso principal lo recupere en cualquier momento.
  8. Llame a exit_notify () para enviar una señal al proceso padre para encontrar el padre adoptivo para el proceso hijo. El padre adoptivo es otro hilo en el grupo de hilos o el proceso init, y el estado del proceso (almacenado en el estado de salida de la estructura task_struct) se establece en EXIT_ZOMBIE .
  9. do_exit () llama a schedule () para cambiar al nuevo proceso. Debido a que el proceso en el estado EXIT_ZOMBIE ya no se programará, este es el último fragmento de código ejecutado por el proceso y do_exit () nunca regresa.

ps: si el proceso es el único usuario de estos recursos, el proceso no se puede ejecutar (de hecho, no hay espacio de direcciones para que se ejecute) y está en el estado de salida EXIT_ZOMBIE. Toda la memoria que ocupa es la pila del núcleo, la estructura thread_info y la estructura task_struct.
El único propósito del proceso en este momento es proporcionar información a su proceso principal. Después de que el proceso padre recupera la información o notifica al núcleo que la información es irrelevante, la memoria restante retenida por el proceso se libera y se devuelve al sistema.

5.1 Eliminar el proceso descriptor-esperar función familiar

El trabajo de limpieza requerido al final del proceso y la eliminación del descriptor del proceso se realizan por separado .
Las funciones de la familia wait () se implementan mediante la única llamada al sistema wait4 (). Su acción estándar es suspender el proceso de llamada hasta que salga uno de los procesos secundarios, en cuyo punto la función devuelve el PID del proceso secundario.

Cuando finalmente sea necesario liberar el descriptor de proceso, se llamará a release_task (), el proceso es el siguiente:

  1. Llama a __exit_signal (), que llama a __unhash_process (), que a su vez llama a detach_pid () para eliminar el proceso de pidhash, y también elimina el proceso de la lista de tareas.
  2. __exit_signal () libera todos los recursos restantes utilizados en el proceso zombie actual, y el proceso finalmente se cuenta y se registra.
  3. Si este proceso es el último en el grupo de subprocesos y el proceso principal está inactivo, release_task () notificará al proceso principal el proceso principal zombie.
  4. release_task () llama a put_task_struct () para liberar la página ocupada por la pila del núcleo del proceso y la estructura thread_info, y libera la caché de losa ocupada por task_struct.

5.2 El dilema causado por el proceso huérfano

Si el proceso padre sale antes que el proceso hijo , debe haber un mecanismo para garantizar que el proceso hijo pueda encontrar un nuevo padre, de lo contrario, estos procesos huérfanos siempre estarán en estado muerto cuando salgan, desperdiciando recursos.
Este mecanismo consiste en encontrar un subproceso como padre en el grupo de subprocesos actual para el proceso secundario; de lo contrario, deje que el proceso init sea su proceso principal .
El proceso ps: init rutinariamente llamará a wait () para verificar sus procesos secundarios y eliminar todos los procesos zombies relacionados.

91 artículos originales publicados · 17 elogiados · 50,000+ vistas

Supongo que te gusta

Origin blog.csdn.net/qq_23327993/article/details/105065705
Recomendado
Clasificación