c++ - subprocesos - que es una funcion en pseint
¿Cómo se programan/crean los subprocesos de nivel de usuario y cómo se crean los subprocesos de nivel de kernel? (3)
Esto está precedido por los mejores comentarios.
La documentación que estás leyendo es genérica [no específica de Linux] y está un poco desactualizada. Y, más concretamente, está utilizando una terminología diferente. Esa es, creo, la fuente de la confusión. Entonces, sigue leyendo ...
Lo que llama un subproceso de "nivel de usuario" es lo que yo llamo un subproceso de LWP [obsoleto]. Lo que llama un subproceso de "nivel de kernel" es lo que se llama un subproceso nativo en linux. Bajo Linux, lo que se llama un hilo "kernel" es algo completamente distinto [Vea a continuación].
al usar pthreads, se crean hilos en el espacio de usuario, y el kernel no es consciente de esto y lo ve como un solo proceso, sin darse cuenta de cuántos hilos hay dentro.
Así era como se hacían los hilos del espacio de usuario antes de la NPTL
(biblioteca de hilos posix nativos). Esto también es lo que SunOS / Solaris llama un proceso ligero de LWP
.
Hubo un proceso que se multiplexó y creó hilos. IIRC, se llamó proceso maestro de hilos [o algo así]. El núcleo no estaba al tanto de esto. El kernel aún no entendía ni proporcionaba soporte para subprocesos.
Pero, debido a que estos subprocesos "ligeros" se cambiaron por código en el maestro de subprocesos basado en el espacio de usuario (también conocido como "programador de procesos liviano") [solo un programa / proceso de usuario especial], fueron muy lentos para cambiar de contexto.
Además, antes de la llegada de los hilos "nativos", es posible que tenga 10 procesos. Cada proceso recibe el 10% de la CPU. Si uno de los procesos era un LWP que tenía 10 subprocesos, estos subprocesos tenían que compartir ese 10% y, por lo tanto, obtuvieron solo el 1% de la CPU cada uno.
Todo esto fue reemplazado por los hilos "nativos" que el programador del kernel conoce. Este cambio se realizó hace 10-15 años.
Ahora, con el ejemplo anterior, tenemos 20 subprocesos / procesos que obtienen cada uno el 5% de la CPU. Y, el cambio de contexto es mucho más rápido.
Todavía es posible tener un sistema LWP en un subproceso nativo, pero ahora es una opción de diseño, más que una necesidad.
Además, LWP funciona muy bien si cada hilo "coopera". Es decir, cada bucle de hilo realiza periódicamente una llamada explícita a una función de "cambio de contexto". Se está renunciando voluntariamente a la ranura del proceso para que se pueda ejecutar otro LWP.
Sin embargo, la implementación pre-NPTL en glibc
también tuvo que [forzar] anticiparse a los subprocesos LWP (es decir, implementar la división de tiempo). No puedo recordar el mecanismo exacto usado, pero aquí hay un ejemplo. El hilo maestro tenía que programar una alarma, irse a dormir, despertarse y luego enviar una señal al hilo activo. El manejador de señal afectaría el cambio de contexto. Esto era desordenado, feo y poco fiable.
Joachim mencionó que la función
pthread_create
crea un hilo de núcleo
Eso es [técnicamente] incorrecto para llamarlo un hilo del núcleo . pthread_create
crea un hilo nativo . Esto se ejecuta en el espacio de usuario y compite por intervalos de tiempo en igualdad de condiciones con los procesos. Una vez creado, hay poca diferencia entre un hilo y un proceso.
La principal diferencia es que un proceso tiene su propio espacio de direcciones único. Sin embargo, un subproceso es un proceso que comparte su espacio de direcciones con otros procesos / subprocesos que forman parte del mismo grupo de subprocesos.
Si no crea un subproceso de nivel de kernel, ¿cómo se crean los subprocesos de kernel a partir de programas de espacio de usuario?
Los subprocesos del núcleo no son subprocesos del espacio de usuario, NPTL, nativos o de otro tipo. Son creados por el núcleo a través de la función kernel_thread
. Se ejecutan como parte del kernel y no están asociados con ningún programa / proceso / subproceso del espacio de usuario. Tienen acceso completo a la máquina. Los dispositivos, MMU, etc. Los subprocesos del kernel se ejecutan en el nivel de privilegio más alto: anillo 0. También se ejecutan en el espacio de direcciones del kernel y no en el espacio de direcciones de cualquier proceso / subproceso del usuario.
Un programa / proceso de espacio de usuario no puede crear un hilo del kernel. Recuerda, crea un subproceso nativo utilizando pthread_create
, que invoca la clone
syscall para hacerlo.
Los hilos son útiles para hacer cosas, incluso para el núcleo. Por lo tanto, ejecuta parte de su código en varios subprocesos. Puedes ver estos hilos haciendo ps ax
. Mire y verá kthreadd, ksoftirqd, kworker, rcu_sched, rcu_bh, watchdog, migration
, etc. Estos son hilos del kernel y no programas / procesos.
ACTUALIZAR:
Usted mencionó que el núcleo no sabe acerca de los hilos de usuario.
Recuerde que, como se mencionó anteriormente, hay dos "eras".
(1) Antes de que el kernel obtuviera soporte para hilos (¿alrededor de 2004?). Esto usó el patrón de hilos (que, aquí, llamaré el programador LWP). El núcleo acaba de tener el syscall fork
.
(2) Todos los núcleos después de eso que entienden los hilos. No hay un patrón de subprocesos, pero tenemos pthreads
y el clone
syscall. Ahora, fork
se implementa como clone
. clone
es similar a la fork
pero toma algunos argumentos. En particular, un argumento flags
y un argumento child_stack
.
Más sobre esto a continuación ...
entonces, ¿cómo es posible que los hilos de nivel de usuario tengan pilas individuales?
No hay nada "mágico" en una pila de procesadores. Limitaré la discusión [principalmente] a x86, pero esto sería aplicable a cualquier arquitectura, incluso a aquellas que ni siquiera tienen un registro de pila (por ejemplo, los mainframes IBM de la década de 1970, como el IBM System 370)
Bajo x86, el puntero de pila es %rsp
. El x86 tiene instrucciones push
y pop
. Usamos estos para guardar y restaurar cosas: push %rcx
y [más tarde] pop %rcx
.
¿Pero, supongamos que el x86 no tenía %rsp
o instrucciones push/pop
? ¿Podríamos tener todavía una pila? Claro, por convención . Nosotros [como programadores] estamos de acuerdo en que (por ejemplo) %rbx
es el puntero de pila.
En ese caso, un "impulso" de %rcx
sería [usando el ensamblador AT&T]:
subq $8,%rbx
movq %rcx,0(%rbx)
Y, un "pop" de %rcx
sería:
movq 0(%rbx),%rcx
addq $8,%rbx
Para hacerlo más fácil, voy a cambiar a C "pseudo código". Aquí están los anteriores push / pop en pseudo código:
// push %ecx
%rbx -= 8;
0(%rbx) = %ecx;
// pop %ecx
%ecx = 0(%rbx);
%rbx += 8;
Para crear un hilo, el programador LWP tuvo que crear un área de pila utilizando malloc
. Luego tuvo que guardar este puntero en una estructura por hilo y luego lanzar el LWP secundario. El código real es un poco complicado, supongamos que tenemos una (por ejemplo) función LWP_create
que es similar a pthread_create
:
typedef void * (*LWP_func)(void *);
// per-thread control
typedef struct tsk tsk_t;
struct tsk {
tsk_t *tsk_next; //
tsk_t *tsk_prev; //
void *tsk_stack; // stack base
u64 tsk_regsave[16];
};
// list of tasks
typedef struct tsklist tsklist_t;
struct tsklist {
tsk_t *tsk_next; //
tsk_t *tsk_prev; //
};
tsklist_t tsklist; // list of tasks
tsk_t *tskcur; // current thread
// LWP_switch -- switch from one task to another
void
LWP_switch(tsk_t *to)
{
// NOTE: we use (i.e.) burn register values as we do our work. in a real
// implementation, we''d have to push/pop these in a special way. so, just
// pretend that we do that ...
// save all registers into tskcur->tsk_regsave
tskcur->tsk_regsave[RAX] = %rax;
// ...
tskcur = to;
// restore most registers from tskcur->tsk_regsave
%rax = tskcur->tsk_regsave[RAX];
// ...
// set stack pointer to new task''s stack
%rsp = tskcur->tsk_regsave[RSP];
// set resume address for task
push(%rsp,tskcur->tsk_regsave[RIP]);
// issue "ret" instruction
ret();
}
// LWP_create -- start a new LWP
tsk_t *
LWP_create(LWP_func start_routine,void *arg)
{
tsk_t *tsknew;
// get per-thread struct for new task
tsknew = calloc(1,sizeof(tsk_t));
append_to_tsklist(tsknew);
// get new task''s stack
tsknew->tsk_stack = malloc(0x100000)
tsknew->tsk_regsave[RSP] = tsknew->tsk_stack;
// give task its argument
tsknew->tsk_regsave[RDI] = arg;
// switch to new task
LWP_switch(tsknew);
return tsknew;
}
// LWP_destroy -- destroy an LWP
void
LWP_destroy(tsk_t *tsk)
{
// free the task''s stack
free(tsk->tsk_stack);
remove_from_tsklist(tsk);
// free per-thread struct for dead task
free(tsk);
}
Con un núcleo que comprende subprocesos, usamos pthread_create
y clone
, pero aún tenemos que crear la pila del nuevo subproceso. El kernel no crea / asigna una pila para un nuevo hilo. El clone
syscall acepta un argumento child_stack
. Por lo tanto, pthread_create
debe asignar una pila para el nuevo hilo y pasarlo a clone
:
// pthread_create -- start a new native thread
tsk_t *
pthread_create(LWP_func start_routine,void *arg)
{
tsk_t *tsknew;
// get per-thread struct for new task
tsknew = calloc(1,sizeof(tsk_t));
append_to_tsklist(tsknew);
// get new task''s stack
tsknew->tsk_stack = malloc(0x100000)
// start up thread
clone(start_routine,tsknew->tsk_stack,CLONE_THREAD,arg);
return tsknew;
}
// pthread_join -- destroy an LWP
void
pthread_join(tsk_t *tsk)
{
// wait for thread to die ...
// free the task''s stack
free(tsk->tsk_stack);
remove_from_tsklist(tsk);
// free per-thread struct for dead task
free(tsk);
}
El núcleo solo asigna su pila inicial a un proceso o subproceso principal, normalmente en una dirección de memoria alta. Por lo tanto, si el proceso no utiliza subprocesos, normalmente solo usa esa pila asignada previamente.
Pero, si se crea un hilo, ya sea un LWP o uno nativo , el proceso de inicio / hilo debe asignar previamente el área para el hilo propuesto con malloc
. Nota al malloc
: el uso de malloc
es la forma normal, pero el creador de hilos solo puede tener una gran cantidad de memoria global: char stack_area[MAXTASK][0x100000];
Si quisiera hacerlo así.
Si tuviéramos un programa ordinario que no utiliza subprocesos [de cualquier tipo], es posible que desee "anular" la pila predeterminada que se le ha dado.
Ese proceso podría decidir utilizar malloc
y el truco del ensamblador anterior para crear una pila mucho más grande si estuviera haciendo una función enormemente recursiva.
Vea mi respuesta aquí: ¿Cuál es la diferencia entre la pila definida por el usuario y la pila integrada en el uso de la memoria?
Disculpas si esta pregunta es estúpida. Intenté encontrar una respuesta en línea durante bastante tiempo, pero no pude y por eso lo pregunto aquí. Estoy aprendiendo temas, y he estado revisando este enlace y este video de la Conferencia de Plomeros de Linux 2013 sobre el nivel del kernel y los hilos del nivel del usuario, y por lo que entendí, el uso de pthreads crea hilos en el espacio del usuario y el núcleo no es consciente sobre esto y verlo como un solo proceso, sin saber cuántos hilos hay dentro. En ese caso,
- ¿Quién decide la programación de estos subprocesos de usuario durante el intervalo de tiempo que obtiene el proceso, ya que el kernel lo ve como un proceso único y no conoce los subprocesos, y cómo se realiza la programación?
- Si pthreads crea subprocesos de nivel de usuario, ¿cómo se crean los subprocesos de nivel de kernel o sistema operativo a partir de programas de espacio de usuario, si es necesario?
- De acuerdo con el enlace anterior, dice que el kernel de sistemas operativos proporciona una llamada al sistema para crear y administrar subprocesos. Entonces, ¿una llamada al sistema
clone()
crea un hilo de nivel de kernel o un hilo de nivel de usuario?- Si crea un subproceso de nivel de kernel, la
strace
de un programa pthreads simple también muestra el uso de clone () mientras se ejecuta, pero ¿por qué se considera un subproceso de nivel de usuario? - Si no crea un subproceso de nivel de kernel, ¿cómo se crean los subprocesos de kernel a partir de programas de espacio de usuario?
- Si crea un subproceso de nivel de kernel, la
- Según el enlace, dice "Requiere un bloque de control de subprocesos completo (TCB) para que cada subproceso mantenga información sobre los subprocesos. Como resultado, hay una sobrecarga significativa y un aumento en la complejidad del kernel". ¿Se comparte el montón, y el resto todos son individuales para el hilo?
Editar:
Estaba preguntando acerca de la creación de subprocesos a nivel de usuario y su programación porque here, hay una referencia al Modelo Muchos a Uno donde muchos subprocesos de nivel de usuario se asignan a un subproceso a nivel de Kernel, y biblioteca de hilos. Solo he visto referencias al uso de pthreads, pero no estoy seguro de si crea subprocesos de nivel de usuario o de nivel de kernel.
LinuxThreads sigue el modelo denominado "uno a uno": cada hilo es en realidad un proceso separado en el kernel. El programador del kernel se encarga de programar los subprocesos, al igual que programa los procesos regulares. Los subprocesos se crean con la llamada al sistema clone () de Linux, que es una generalización de fork () que permite que el nuevo proceso comparta el espacio de memoria, los descriptores de archivos y los manejadores de señales del padre.
Fuente: entrevista a Xavier Leroy (persona que creó LinuxThreads) http://pauillac.inria.fr/~xleroy/linuxthreads/faq.html#K
Los hilos de nivel de usuario suelen ser coroutines, de una forma u otra. Cambie el contexto entre los flujos de ejecución en modo de usuario , sin la participación del núcleo. Del núcleo POV, es todo un hilo. Lo que realmente hace el hilo se controla en el modo de usuario, y el modo de usuario puede suspender, conmutar, reanudar flujos lógicos de ejecuciones (es decir, corrutinas). Todo sucede durante los cuantos programados para el hilo real. El kernel puede, y sin interrupción, interrumpirá el hilo real (hilo del kernel) y le dará el control del procesador a otro hilo.
Modo de usuario: las rutinas requieren multitarea cooperativa. Los subprocesos de modo de usuario deben renunciar periódicamente al control de otros subprocesos de modo de usuario (básicamente, la ejecución cambia el contexto al nuevo subproceso de modo de usuario, sin que el subproceso del núcleo haya notado nada). Por lo general, lo que sucede es que el código sabe mucho mejor cuando quiere liberar el control que haría el kernel. Una coroutina mal codificada puede robar el control y matar de hambre a todas las demás coroutinas.
La implementación histórica usó setcontext
pero ahora está en desuso. Boost.context reemplaza, pero no es completamente portátil:
Boost.Context es una biblioteca fundamental que proporciona una especie de multitarea cooperativa en un solo hilo. Al proporcionar una abstracción del estado de ejecución actual en el subproceso actual, incluida la pila (con variables locales) y el puntero de pila, todos los registros e indicadores de la CPU, y el puntero de instrucción, un contexto de ejecución representa un punto específico en la ruta de ejecución de la aplicación.
No es sorprendente que Boost.coroutine se base en Boost.context.
Windows proporciona Fibers . El tiempo de ejecución de .Net tiene tareas y async / await.