Control de procesos de Linux (1) --- creación y finalización de procesos (copia en escritura, salida y _salida, etc.)

Tabla de contenido

creación de procesos

función tenedor()

¿Cómo un proceso hijo hereda datos del proceso padre?

1. Separación de copias en el momento de la creación

2. Copia sobre escritura★

proceso terminado

¿Qué hace el sistema operativo cuando finaliza un proceso?

Formas comunes de terminación de procesos.

El código se ejecuta y el resultado es correcto.

código de salida★

El código se ejecuta hasta el final con resultados incorrectos

Terminación de código anormal

Cómo terminar un proceso con código

devolver

salir y _salir★


creación de procesos

función tenedor()

Ya expliqué el uso de esta función en detalle en el artículo anterior, por lo que este artículo analiza principalmente fork() desde una perspectiva más profunda .

La función de bifurcación es una función muy importante en Linux, que crea un nuevo proceso a partir de un proceso existente. El nuevo proceso es el proceso hijo y el proceso original es el proceso padre.

#include <unistd.h>
pid_t fork(void);
valor devuelto: devuelve 0 en el proceso secundario, devuelve el id del proceso secundario en el proceso principal, devuelve -1 por error

Ahora vamos a explicar desde una perspectiva más sistemática:

Describa, ¿qué hace el sistema operativo cuando fork() crea un proceso secundario?

1. En primer lugar, el punto de partida de esta pregunta es: fork() crea un proceso secundario, ¡y habrá un proceso más en el sistema!

2. Luego responde cuál es el proceso:

Proceso = estructura de datos del kernel (SO) + código de proceso y datos (generalmente del disco, es decir, el resultado de la carga del programa C/C++)

3. Después de eso, se realizarán las siguientes operaciones en el proceso secundario:

        a. Asignar nuevos bloques de memoria y estructuras de datos al proceso hijo

        b. Copie parte del contenido de la estructura de datos del proceso principal al proceso secundario

        c. Agregar procesos secundarios a la lista de procesos del sistema

        d.fork() regresa e inicia la llamada del programador.

¿Cómo un proceso hijo hereda datos del proceso padre?

La creación de un proceso secundario y la asignación de la estructura del kernel correspondiente al proceso secundario deben ser exclusivos del proceso secundario, ya que el proceso es independiente. ¡En teoría, los subprocesos también deberían tener su propio código y datos!

Puede ser que en general, el proceso hijo no tenga un proceso cargado, es decir, el proceso hijo no tenga su propio código y datos, por lo que el proceso hijo solo puede usar "código y datos del proceso padre"

Esto está mal, ¿no significa que el proceso es independiente? ¿Cómo se puede usar el código y los datos del proceso padre?

Aquí también se discuten caso por caso:

Código : no se puede reescribir, solo se puede leer , así que padre e hijo comparten, ¡no hay problema!

Datos : pueden modificarse, por lo que deben separarse.

Entonces , para los datos , ¿cómo separarlos?

1. Separación de copias en el momento de la creación

Veamos primero el primer método: al crear un proceso secundario, simplemente cópielo y sepárelo directamente, entonces, ¿cuál es el problema?

1. Cuando creamos un proceso hijo, ¿podemos ejecutarlo inmediatamente? Este es uno de ellos.

2. Incluso si funciona de inmediato, ¿tendrá acceso a todos los datos? Este es el segundo.

3. Incluso si puede acceder a todos los datos, ¿su acceso a todos los datos está escrito? Si no está escrito, no hay necesidad de copiar en absoluto. 

Entonces, el problema es que es posible copiar el espacio de datos que el proceso secundario no usará en absoluto, incluso si se usa, ¡solo se puede leer!

Esta situación también está bien ilustrada:

 Dos constantes de cadena diferentes, cuando imprimimos las direcciones, ¡encontraremos que sus direcciones son las mismas!

Debido a que el compilador sabe que el contenido de esta variable modificada const no se puede modificar, las siguientes variables con el mismo contenido apuntarán directamente a ella.

 Esto es solo para decirles a todos: cuando el compilador compila el programa, sabe cómo ahorrar espacio. Además, este tipo de interfaz de sistema que utiliza directamente la memoria prestará más atención a los archivos .

Por lo tanto, la creación de un proceso secundario no necesita copiar los datos a los que no se accederá o solo se leerán.

Pero aquí viene la pregunta, ¿qué tipo de datos vale la pena copiar y qué tipo de datos se deben copiar?

Deben ser datos que puedan ser escritos por el proceso principal o el proceso secundario en el futuro .

Sin embargo, en términos generales, incluso el sistema operativo no puede saber de antemano qué espacios se pueden escribir.

Pero incluso si lo sabe, cópielo con anticipación, ¿lo usará de inmediato?

La respuesta es que hay tantos datos que se pueden escribir, definitivamente no, pero se te ha dado el espacio, pero no lo usas, por lo que esto genera un desperdicio de espacio.

Entonces, OS eligió una tecnología: copia en escritura, para separar los datos de los procesos principal y secundario.

2. Copia sobre escritura★

Entonces, combinado con lo anterior, la copia en escritura significa que cuando necesite datos que se puedan escribir , el sistema operativo le dará el espacio correspondiente.

Por qué el sistema operativo adopta la tecnología de copia en escritura para separar los datos del proceso primario y secundario:

1. Cuando se usa, se vuelve a asignar, lo que es una manifestación del uso eficiente de la memoria.

2. El sistema operativo no puede predecir a qué espacios se accederá antes de que se ejecute el código.

 Aquí hay otro problema:

Luego, el código anterior al proceso principal fork(), ¿se comparte el proceso secundario?

La respuesta se comparte y el proceso hijo comparte todo el código antes que el proceso padre fork().

A continuación se explicará una imagen. También he descrito esta imagen en detalle en la ejecución concurrente del concepto y el estado del proceso .

 Es decir, existe la dirección de la siguiente línea de código en EIP, y estos datos almacenados se denominan datos de contexto.

Cuando el proceso secundario hereda el proceso principal, se produce la copia en escritura y los datos de contexto del proceso principal se copian en el proceso secundario.

Aunque los procesos padre e hijo se programan por separado más adelante, los códigos son diferentes y cada uno modificará el EIP, pero ya no es importante, porque el proceso hijo ya piensa que el valor inicial de su propio código EIP es el código después de la bifurcación. ().

Entonces, aunque el proceso secundario se ejecuta después de fork(), ¡no significa que el proceso secundario de código anterior a fork() no pueda verlo!

proceso terminado

¿Qué hace el sistema operativo cuando finaliza un proceso?

Sabemos que un programa se convierte en un proceso cuando se ejecuta, y un proceso se compone de código de proceso y datos + estructura de datos del kernel .

Por lo tanto, la terminación del proceso es liberar la estructura de datos del kernel y los datos y códigos correspondientes aplicados por el proceso. La esencia es liberar los recursos del sistema.

Formas comunes de terminación de procesos.

El código se ejecuta y el resultado es correcto.

Esta situación es muy común.Cuando escribimos preguntas de algoritmo, como Lituo, si el resultado de una pregunta es correcto, se mostrará como aprobado después del envío final.

O escríbalo usted mismo en el compilador, y el resultado final también está en línea con nuestros resultados esperados, etc.

Me pregunto si hemos notado que la función principal tiene un valor de retorno, devuelve 0, ¿cuál es el significado de su valor de retorno y por qué siempre devuelve 0?

De hecho, el valor de retorno de la función principal no siempre es 0, pero a menudo escribimos 0 cuando solemos escribir.

El valor devuelto al final de la función principal se denomina código de salida del proceso.

Generalmente, 0 representa el éxito, lo que indica que el resultado de la ejecución del proceso es correcto.

El resultado de la operación del indicador distinto de cero es incorrecto, lo cual se describirá en detalle más adelante.

Por ejemplo, devolvemos un 10.

 Luego usamos $? para generar el código de salida del proceso más reciente.

 Se puede encontrar que la primera ejecución, el código de salida obtenido es el 10 devuelto más recientemente.

La razón por la que es 0 por segunda vez es porque el último echo $? también es un proceso, se ejecutó con éxito, por lo que devuelve 0.

Entonces, ¿ cuál es el significado del retorno de la función principal ?

Se utiliza para volver al proceso de nivel superior, se utiliza para juzgar el resultado de ejecución del proceso, se puede ignorar. 

 Por ejemplo, si escribimos un programa de suma y suma, si la respuesta es correcta, devuelve 0, y si la respuesta es incorrecta, devuelve 1.

En este punto, nuestra suma es el proceso correcto, por lo que el resultado debería devolver 0

Y cuando escribimos mal la lógica de cálculo de la suma y no obtenemos el resultado correcto, devolverá 1, lo que indica que el resultado final del programa es incorrecto, que también es el significado del valor de retorno de la función principal. 

código de salida★

Volviendo al código de salida distinto de cero que acabamos de mencionar, hay innumerables valores distintos de cero que se pueden utilizar para identificar diferentes causas de error.

Luego de finalizar nuestro programa, es conveniente ubicar los detalles de la causa del error.

Estas razones también están definidas en Linux, podemos imprimir para ver, aquí necesitamos usar una función strerror()

Lo que hace es devolver una descripción de cadena del código de error.

Ingresamos el código:

 

 Luego emite el resultado:

 

Hasta 100, encontramos que hay muchos tipos de errores.

Por supuesto, podemos usar estos códigos de salida y significados nosotros mismos, pero si desea definirlos usted mismo, también puede diseñar un conjunto de esquemas de salida. 

El código se ejecuta hasta el final con resultados incorrectos

Ídem.

Terminación de código anormal

Cuando el programa finaliza de manera anormal, el código de salida no tiene sentido. En términos generales, ¡la declaración de retorno correspondiente al código de salida no se ejecuta!

Entonces, ¿por qué falla el programa?

Cómo terminar un proceso con código

devolver

En primer lugar, sabemos que el código de salida de retorno finaliza el proceso. Por supuesto, solo la declaración de retorno en la función principal finaliza el proceso.

salir y _salir★

Veamos primero la introducción.

 Vea lo que esto hace es dejar que el proceso termine con gracia.

Luego, el parámetro de la función es el estado, que se utiliza para identificar el código de salida.

Es diferente de la función principal: la función de salida se llama en cualquier lugar y el proceso finaliza directamente.

Vea el siguiente ejemplo:

 

 Si es return 200, solo devolverá 200 a a, y el programa no terminará.

 Se puede ver que la declaración del mundo de salida después de func() no se ejecuta, y el código de salida es 200 por primera vez.

Entonces, ¿qué es _exit y cuál es la diferencia?

 Describe mucho y es bastante abstracto. Usaré un ejemplo y una imagen para explicar la diferencia entre it y exit.

En primer lugar, el ejemplo de ahora, cambiamos exit a _exit.

Se puede encontrar que el resultado no es diferente de exit:

 

Pero cambiamos el programa a algo como esto:

 

En este momento, esperamos que debido a que printf no tiene el carácter de nueva línea '\n' para actualizar el búfer, el contenido se almacena en el búfer en este momento, por lo que el programa mostrará el contenido en la pantalla después de ejecutarse durante 1 segundo. y luego el código de salida devuelve 111. 

Dado que es una imagen estática, el efecto no se puede demostrar, pero el resultado se muestra después de 1 segundo.

Pero si cambiamos exit a _exit.

 

 Pero descubrió que no se emitía nada.

Aquí está la conclusión directamente, exit actualizará el contenido del búfer en la pantalla al final del programa, y ​​_exit finalizará directamente sin actualizar el contenido del búfer.

Como se muestra en la siguiente figura

Pero generalmente recomendamos usar exit.

Sabemos que la función de biblioteca es una interfaz de sistema encapsulada, y que la función de biblioteca interna en realidad es exit, y la interfaz de sistema es _exit.

Y dónde está el búfer del que solemos hablar, hablaré de eso más adelante, ¡ pero no debe estar dentro del sistema operativo

Si está dentro del sistema operativo y es mantenido por el sistema operativo, entonces _exit aún se puede actualizar, pero no se puede actualizar, lo que indica que no debe estar dentro del sistema operativo, sino proporcionado por la biblioteca de lenguaje C.

Esto en cuanto a la creación y terminación de procesos.

Supongo que te gusta

Origin blog.csdn.net/weixin_47257473/article/details/131784854
Recomendado
Clasificación