kernel de Linux - proceso

Procesos e hilos

Un proceso es una abstracción de un programa en ejecución, que incluye:

  • espacio de direcciones (independiente)
  • uno o más hilos
  • Abrir archivo (presentado como descriptor fd)
  • enchufe
  • Semáforo
  • área de memoria compartida
  • Temporizador
  • manejador de señales manejador de señales
  • Otra información sobre recursos y estado

Todas estas cosas existen en el bloque de control de procesos (PCB). En linux si struct task_struct.

recursos de proceso

Cuando miramos en /proc/<pid>el directorio, podemos ver <pid>información sobre el proceso con el número de proceso.

Si un proceso quiere ver su propia información relacionada, puede acceder /proc/selfal directorio.

                +-------------------------------------------------------------------+
                | dr-x------    2 tavi tavi 0  2021 03 14 12:34 .                   |
                | dr-xr-xr-x    6 tavi tavi 0  2021 03 14 12:34 ..                  |
                | lrwx------    1 tavi tavi 64 2021 03 14 12:34 0 -> /dev/pts/4     |
           +--->| lrwx------    1 tavi tavi 64 2021 03 14 12:34 1 -> /dev/pts/4     |
           |    | lrwx------    1 tavi tavi 64 2021 03 14 12:34 2 -> /dev/pts/4     |
           |    | lr-x------    1 tavi tavi 64 2021 03 14 12:34 3 -> /proc/18312/fd |
           |    +-------------------------------------------------------------------+
           |                 +----------------------------------------------------------------+
           |                 | 08048000-0804c000 r-xp 00000000 08:02 16875609 /bin/cat        |
$ ls -1 /proc/self/          | 0804c000-0804d000 rw-p 00003000 08:02 16875609 /bin/cat        |
cmdline    |                 | 0804d000-0806e000 rw-p 0804d000 00:00 0 [heap]                 |
cwd        |                 | ...                                                            |
environ    |    +----------->| b7f46000-b7f49000 rw-p b7f46000 00:00 0                        |
exe        |    |            | b7f59000-b7f5b000 rw-p b7f59000 00:00 0                        |
fd --------+    |            | b7f5b000-b7f77000 r-xp 00000000 08:02 11601524 /lib/ld-2.7.so  |
fdinfo          |            | b7f77000-b7f79000 rw-p 0001b000 08:02 11601524 /lib/ld-2.7.so  |
maps -----------+            | bfa05000-bfa1a000 rw-p bffeb000 00:00 0 [stack]                |
mem                          | ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]                 |
root                         +----------------------------------------------------------------+
stat                 +----------------------------+
statm                |  Name: cat                 |
status ------+       |  State: R (running)        |
task         |       |  Tgid: 18205               |
wchan        +------>|  Pid: 18205                |
                     |  PPid: 18133               |
                     |  Uid: 1000 1000 1000 1000  |
                     |  Gid: 1000 1000 1000 1000  |
                     +----------------------------+

hilo

Los subprocesos son la unidad básica para que el kernel programe tareas para ejecutarse en la CPU. Un hilo tiene las siguientes propiedades:

  • Cada hilo tiene su propia pila y su propio valor de registro (usado para guardar en qué paso se ha ejecutado)
  • Los subprocesos se ejecutan en el contexto de un proceso y los subprocesos de un proceso comparten recursos.
  • El kernel programa subprocesos en lugar de procesos. Además, el kernel no reconoce los subprocesos en modo de usuario (como goroutine en golang).
  • En una implementación de subprocesos clásica, la información del subproceso se trata como una estructura de datos separada (nodo de lista vinculada), que luego se vincula a la estructura de datos del proceso. Por ejemplo, el subproceso de verificación de Windows se implementa como se muestra en la siguiente figura:
    Insertar descripción de la imagen aquí
    Podemos ver que hay una lista vinculada de subprocesos en un bloque de control de proceso, y cada elemento de la lista vinculada (subproceso) apunta al proceso al que pertenece.

Linux implementa subprocesos de manera diferente. La unidad básica (de subprocesos y procesos) se llama tarea, por lo que la estructura de datos correspondiente a los procesos y subprocesos es struct task_struct, esta estructura se utiliza para describir procesos y subprocesos. En struct task_struct, los recursos no se registran, pero se utilizan punteros para señalar los recursos correspondientes.

Como se muestra en la figura siguiente, si hay dos subprocesos en un proceso (con el mismo ID de grupo de subprocesos, es decir, PID), apuntarán a la misma estructura de datos que describe recursos (como archivos abiertos, espacios de direcciones, espacios de nombres). . Si dos subprocesos no pertenecen al mismo proceso, entonces las estructuras de datos que describen los recursos a los que apuntan deben ser diferentes.

En términos generales, PID y TGID son iguales. Pero en teoría, el kernel del sistema operativo puede asignar diferentes TGID a subprocesos dentro de un proceso, pero este no suele ser el caso en las implementaciones reales de Linux.

Insertar descripción de la imagen aquí

llamada al sistemaclone()

En Linux, al abrir un nuevo hilo pthread_create()o un nuevo proceso , fork()se utilizan clone()llamadas al sistema :

int clone(int (*fn)(void *_Nullable), void *stack, int flags,
                 void *_Nullable arg, ...  /* pid_t *_Nullable parent_tid,
                                              void *_Nullable tls,
                                              pid_t *_Nullable child_tid */ );

Permite a la persona que llama decidir qué recursos se pueden compartir, principalmente clone()transmitiendo la siguiente información a la función a través de una máscara binaria compuesta de banderas:

  • CLONE_FILES: comparte la tabla de descriptores de archivos con el proceso principal
  • CLONE_VM: comparte espacio de direcciones con el proceso principal
  • CLONE_FS: comparte información del sistema de archivos (como el directorio raíz, pwd) con el proceso principal
  • CLONE_NEWNS: no comparta el espacio de nombres de montaje con el proceso principal, cree uno nuevo usted mismo
  • CLONE_NEWIPC: no comparta el espacio de nombres para la comunicación entre procesos (como objetos IPC de System V, colas de mensajes POSIX, etc.) con el proceso principal. Abra uno nuevo usted mismo.
  • CLONE_NEWNET: no compartir el espacio de nombres de la red con el proceso principal

Por ejemplo, usar estos tres indicadores: CLONE_FILES | CLONE_VM | CLONE_FS significa abrir un hilo. Si no se utilizan significa que se abre un proceso.

Espacios de nombres y tecnología de contenedores

En la tecnología de contenedores, los cgroups y los espacios de nombres se utilizan principalmente para aislar recursos. Por ejemplo, si no existe tecnología de contenedor, todos los procesos se pueden /procver en el directorio. Los procesos que se ejecutan en un contenedor no son visibles (ni eliminables) para otros contenedores.

/*
 * A structure to contain pointers to all per-process
 * namespaces - fs (mount), uts, network, sysvipc, etc.
 *
 * The pid namespace is an exception -- it's accessed using
 * task_active_pid_ns.  The pid namespace here is the
 * namespace that children will use.
 *
 * 'count' is the number of tasks holding a reference.
 * The count for each namespace, then, will be the number
 * of nsproxies pointing to it, not the number of tasks.
 *
 * The nsproxy is shared by tasks which share all namespaces.
 * As soon as a single namespace is cloned or unshared, the
 * nsproxy is copied.
 */
struct nsproxy {
	atomic_t count;
	struct uts_namespace *uts_ns;
	struct ipc_namespace *ipc_ns;
	struct mnt_namespace *mnt_ns;
	struct pid_namespace *pid_ns_for_children;
	struct net 	     *net_ns;
	struct time_namespace *time_ns;
	struct time_namespace *time_ns_for_children;
	struct cgroup_namespace *cgroup_ns;
};

En el bloque de control de procesos,

struct task_struct {
	... ...
        struct fs_struct *fs;
	struct files_struct *files;
	struct nsproxy *nsproxy; // 名称空间指针
	... ...
};

La struct nsproxyestructura anterior se puede utilizar para separar diferentes tipos de recursos (implementados según el espacio de nombres).

Actualmente, admite procesos IPC, red (aislamiento de la pila de protocolos de red, consulte la red acoplable), cg (aislamiento del uso de recursos informáticos, como la proporción de CPU y el límite superior de memoria), montaje (aislamiento de archivos de acceso), PID (que permite diferentes espacios de nombres). puede tener el mismo PID), espacio de nombres de tiempo.

acceder al proceso actual

Acceder al proceso actual es una operación frecuente, como por ejemplo:

  • Para abrir un archivo es necesario acceder al fd correspondiente
  • Para acceder a la memoria virtual, debe acceder a la tabla de páginas del proceso actual
  • Más del 90% de las llamadas al sistema requieren acceso al bloque de control de procesos
  • Acceda a la macro actual, que es un puntero global que apunta a struct task_structla estructura del proceso actual, que representa el proceso actual. Por ejemplo, current ->pidpuede obtener el pid del proceso actual y current->commel nombre del proceso actual.

Como se muestra en la figura siguiente, para admitir un acceso rápido al bloque de control de procesos en un entorno de múltiples núcleos, cada núcleo de CPU tiene una variable para almacenar el puntero del bloque de control del proceso actualmente en ejecución: Otra forma de acceder al La estructura es utilizar la
Insertar descripción de la imagen aquí
macro actual struct task_struct. El siguiente código muestra los detalles de la macro actual que se utiliza para acceder al bloque de control de proceso.

/* how to get the current stack pointer from C */
register unsigned long current_stack_pointer asm("esp") __attribute_used__;

/* how to get the thread information struct from C */
static inline struct thread_info *current_thread_info(void)
{
   return (struct thread_info *)(current_stack_pointer & ~(THREAD_SIZE – 1));
}

#define current current_thread_info()->task

Cambio de contexto de proceso

La siguiente figura muestra el proceso de cambio de contexto del kernel de Linux:
Insertar descripción de la imagen aquí
T0 aquí se refiere al subproceso 0 y T1 se refiere al subproceso 1.

En el proceso anterior, por ejemplo, cuando un subproceso de usuario llama a una llamada al sistema, primero ingresa al estado del kernel, escribe el contexto de la CPU del estado del usuario en la propia pila del kernel del subproceso, luego llama a un método, abandona activamente la CPU y realiza el contexto. Cambia y cambia a otro estado schedule(). Un hilo continúa ejecutándose.

Bloquear y reactivar tareas (incluido el subprocesamiento)

Estado de la tarea

La siguiente figura muestra la lógica de transformación del estado de la tarea.

Insertar descripción de la imagen aquí
Las diferencias entre TASK_INTERRUPTIBLE y TASK_UNINTERRUPTIBLE son las siguientes:

En el estado TASK_INTERRUPTIBLE, el hilo está esperando que se cumpla una determinada condición, pero puede ser despertado por una interrupción de señal.
Cuando un hilo ingresa al estado TASK_INTERRUPTIBLE, ingresará a la cola de espera interrumpible y esperará a que se cumpla la condición.
Si un hilo recibe una señal, como SIGINT enviada por Ctrl+C, se despertará del modo de suspensión y luego podrá elegir cómo manejar la señal (como finalizar el proceso directamente).

En el estado TASK_UNINTERRUPTIBLE, el hilo también está esperando que se cumpla la condición, pero no puede ser interrumpido por una señal.
Cuando un hilo ingresa al estado TASK_UNINTERRUPTIBLE, ingresa a una cola de espera ininterrumpida.
Este estado se utiliza normalmente para operaciones críticas, como operaciones de escritura del sistema de archivos. En este caso, incluso si el hilo recibe la señal, no se puede interrumpir para garantizar la integridad de las operaciones críticas.

Bloquear el hilo actual

Bloquear el subproceso actual es una operación importante para lograr un alto rendimiento: mientras el subproceso actual espera operaciones de E/S, ejecute otros subprocesos.

Para completar el paso de bloqueo, necesita:

  • Establezca el estado actual del hilo en TASK_UINTERRUPTIBLE o TASK_INTERRUPTIBLE
  • Añade el hilo a la cola de espera.
  • Obtenga un hilo programable del programador de Linux
  • Cambie el contexto a este hilo que se puede programar e iniciar la ejecución.

despertar una tarea

Podemos llamar wake_upa la función para activar el hilo, que principalmente hace:

  • Seleccione un hilo de la cola de espera
  • Establece el estado del hilo en TASK_READY
  • Coloque el hilo en la cola LISTO del programador
  • En un sistema SMP, hay más cosas a considerar: cada CPU tiene su propia cola, por lo que se deben considerar una serie de cosas como el equilibrio de carga y la afinidad del procesador.
#define wake_up(x)                        __wake_up(x, TASK_NORMAL, 1, NULL)

/**
 * __wake_up - wake up threads blocked on a waitqueue.
 * @wq_head: the waitqueue
 * @mode: which threads
 * @nr_exclusive: how many wake-one or wake-many threads to wake up
 * @key: is directly passed to the wakeup function
 *
 * If this function wakes up a task, it executes a full memory barrier before
 * accessing the task state.
 */
void __wake_up(struct wait_queue_head *wq_head, unsigned int mode,
               int nr_exclusive, void *key)
{
    __wake_up_common_lock(wq_head, mode, nr_exclusive, 0, key);
}

static void __wake_up_common_lock(struct wait_queue_head *wq_head, unsigned int mode,
                  int nr_exclusive, int wake_flags, void *key)
{
  unsigned long flags;
  wait_queue_entry_t bookmark;

  bookmark.flags = 0;
  bookmark.private = NULL;
  bookmark.func = NULL;
  INIT_LIST_HEAD(&bookmark.entry);

  do {
          spin_lock_irqsave(&wq_head->lock, flags);
          nr_exclusive = __wake_up_common(wq_head, mode, nr_exclusive,
                                          wake_flags, key, &bookmark);
          spin_unlock_irqrestore(&wq_head->lock, flags);
  } while (bookmark.flags & WQ_FLAG_BOOKMARK);
}

/*
 * The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
 * wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve
 * number) then we wake all the non-exclusive tasks and one exclusive task.
 *
 * There are circumstances in which we can try to wake a task which has already
 * started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
 * zero in this (rare) case, and we handle it by continuing to scan the queue.
 */
static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
                            int nr_exclusive, int wake_flags, void *key,
                  wait_queue_entry_t *bookmark)
{
    wait_queue_entry_t *curr, *next;
    int cnt = 0;

    lockdep_assert_held(&wq_head->lock);

    if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
          curr = list_next_entry(bookmark, entry);

          list_del(&bookmark->entry);
          bookmark->flags = 0;
    } else
          curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);

    if (&curr->entry == &wq_head->head)
          return nr_exclusive;

    list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
          unsigned flags = curr->flags;
          int ret;

          if (flags & WQ_FLAG_BOOKMARK)
                  continue;

          ret = curr->func(curr, mode, wake_flags, key);
          if (ret < 0)
                  break;
          if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
                  break;

          if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) &&
                          (&next->entry != &wq_head->head)) {
                  bookmark->flags = WQ_FLAG_BOOKMARK;
                  list_add_tail(&bookmark->entry, &next->entry);
                  break;
          }
    }

    return nr_exclusive;
}

int autoremove_wake_function(struct wait_queue_entry *wq_entry, unsigned mode, int sync, void *key)
{
    int ret = default_wake_function(wq_entry, mode, sync, key);

    if (ret)
        list_del_init_careful(&wq_entry->entry);

    return ret;
}

int default_wake_function(wait_queue_entry_t *curr, unsigned mode, int wake_flags,
                    void *key)
{
    WARN_ON_ONCE(IS_ENABLED(CONFIG_SCHED_DEBUG) && wake_flags & ~WF_SYNC);
    return try_to_wake_up(curr->private, mode, wake_flags);
}

/**
 * try_to_wake_up - wake up a thread
 * @p: the thread to be awakened
 * @state: the mask of task states that can be woken
 * @wake_flags: wake modifier flags (WF_*)
 *
 * Conceptually does:
 *
 *   If (@state & @p->state) @p->state = TASK_RUNNING.
 *
 * If the task was not queued/runnable, also place it back on a runqueue.
 *
 * This function is atomic against schedule() which would dequeue the task.
 *
 * It issues a full memory barrier before accessing @p->state, see the comment
 * with set_current_state().
 *
 * Uses p->pi_lock to serialize against concurrent wake-ups.
 *
 * Relies on p->pi_lock stabilizing:
 *  - p->sched_class
 *  - p->cpus_ptr
 *  - p->sched_task_group
 * in order to do migration, see its use of select_task_rq()/set_task_cpu().
 *
 * Tries really hard to only take one task_rq(p)->lock for performance.
 * Takes rq->lock in:
 *  - ttwu_runnable()    -- old rq, unavoidable, see comment there;
 *  - ttwu_queue()       -- new rq, for enqueue of the task;
 *  - psi_ttwu_dequeue() -- much sadness :-( accounting will kill us.
 *
 * As a consequence we race really badly with just about everything. See the
 * many memory barriers and their comments for details.
 *
 * Return: %true if @p->state changes (an actual wakeup was done),
 *           %false otherwise.
 */
 static int
 try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
 {
     ...

Apropiarse de tareas

Kernel en modo no preventivo

  • En cada interrupción del temporizador, el núcleo comprueba si se ha agotado el intervalo de tiempo del proceso actual.
  • Si está agotado, establezca un indicador específico en el contexto de interrupción
  • Cuando el procesamiento de la interrupción está a punto de finalizar, el kernel verifica este indicador y llama a la función Schedule() según corresponda.
  • En este caso, la tarea no es preferible en el kernel (como cuando se ejecuta una llamada al sistema), por lo que no hay problema de sincronización.

Núcleo en modo preventivo

En este caso, incluso si estamos ejecutando una llamada al sistema, otros subprocesos pueden adelantarse a ella. El proceso de preferencia requiere primitivas de sincronización especiales: preempt_disable y preempt_enable.

Deshabilitar la preferencia y los bloqueos de giro: para simplificar el procesamiento en núcleos interrumpibles, y considerando que los mecanismos de sincronización aún son necesarios en casos de multiprocesador (SMP), el kernel deshabilita automáticamente la preferencia cuando se usan bloqueos de giro. Un bloqueo de giro es un mecanismo de bloqueo que sigue girando y esperando cuando un subproceso o proceso intenta adquirir el bloqueo, en lugar de ceder los derechos de ejecución de la CPU. Por lo tanto, para evitar condiciones de carrera de subprocesos múltiples, el kernel deshabilita la preferencia para garantizar que la tarea que se está ejecutando actualmente no sea reemplazada mientras se mantenga el bloqueo de giro.

Establecer indicadores y volver a habilitar la preferencia: si se produce una condición durante la ejecución que requiere la preferencia de la tarea actual, como que se haya agotado el intervalo de tiempo de la tarea actual, se establecerá una marca. El kernel verifica este indicador cuando se vuelve a habilitar la preferencia, por ejemplo realizando una operación de desbloqueo de bloqueo de giro (spin_unlock()). Si se requiere preferencia, se llamará al programador para seleccionar una nueva tarea para su ejecución. Esto significa que el kernel verificará si necesita cambiar a otras tareas cuando el bloqueo de giro esté desbloqueado para garantizar una ejecución justa de las tareas y la asignación de intervalos de tiempo.

contexto del proceso

Decimos que el kernel se ejecuta en el contexto del proceso si el kernel está ejecutando una llamada al sistema.

En el contexto del proceso, podemos usar la macro actual para acceder a información sobre el proceso actual.

En el contexto del proceso podemos dormir() (esperar una condición específica)

En el contexto del proceso podemos acceder al espacio del usuario (a menos que estemos ejecutando en el contexto del hilo del kernel, en cuyo caso el espacio del usuario no está involucrado)

hilo del núcleo

El núcleo del kernel o el controlador del dispositivo a veces necesitan realizar operaciones que requieren bloqueo (es decir, esperar a que se cumplan ciertas condiciones). Esto puede implicar esperar a que los datos de un dispositivo de hardware estén listos, esperar a que se completen otros subprocesos del kernel, etc. Debido a que estas operaciones pueden causar que los subprocesos se bloqueen, el kernel necesita un mecanismo para administrar estos subprocesos para que puedan ejecutarse de manera bloqueante.

Los subprocesos del kernel son una clase especial de tareas que no utilizan recursos de espacio del usuario. Esto significa que no tienen asignado ningún espacio de direcciones de usuario, no abren archivos de espacio de usuario y no realizan llamadas al sistema relacionadas con el espacio de usuario. Los subprocesos del kernel funcionan completamente en el espacio del kernel y se utilizan principalmente para tareas dentro del kernel.

Supongo que te gusta

Origin blog.csdn.net/weixin_43466027/article/details/132926750
Recomendado
Clasificación