Proceso de creación de API relacionado con el control de procesos, separación de procesos, salida de procesos, bloqueo de procesos

Tabla de contenido

API relacionada con el control de procesos

El proceso padre crea el proceso hijo fork()

Funciones familiares ejecutivas de separación de procesos

Salida del proceso retorno/salida()

Espera de bloqueo de proceso()

Otras API


API relacionada con el control de procesos

Los usuarios rara vez utilizan la API relacionada con la transición de estado en el control de procesos ps y no se mencionará aquí.

En términos generales, estas API estándar del kernel devolverán un valor negativo (como -1) y establecerán el valor errno cuando ocurra un error de ejecución (quizás recursos insuficientes, permisos insuficientes, etc.).

El proceso padre crea el proceso hijo fork()

En Linux, para crear un proceso hijo, el proceso padre utiliza la llamada al sistema fork() para crear el proceso hijo. fork() en realidad hace una copia del proceso padre (el proceso hijo tiene sus propias características, como identificación, estado, espacio de datos (área de pila y área de datos), etc. (éstas son exclusivas del proceso hijo); proceso y el proceso padre Uso común de código de programa, intervalos de tiempo compartidos (estos se comparten, etc.).

Por lo general, después de llamar a la función fork, el programa diseñará una estructura de selección if. Cuando el PID es igual a 0, significa que el proceso es un proceso hijo, luego le permite ejecutar ciertas instrucciones, como usar la función de biblioteca exec (función de biblioteca) para leer otro archivo de programa y ejecutarlo en el espacio de proceso actual ( En realidad, uno de los propósitos principales de usar fork es crear un proceso para un determinado programa); cuando el PID es un número entero positivo, significa que es el proceso principal y se ejecutan otras instrucciones. A partir de esto, una vez establecido el proceso hijo, puede realizar funciones diferentes a las del proceso padre.

pid_t fork();, fork() devuelve 0 para el proceso hijo y devuelve el ID del proceso hijo para el proceso padre. Devolver un valor menor que 0 es un error.

#include<stdio.h> 
#include<unistd.h> 
​int
main() 
{ 
  int p_num = 0; 
  int c_num = 0; 
  int pid = fork(); 
  if(pid == 0) // El pid devuelto es 0 es el proceso hijo 
  { 
        c_num++; 
  } 
  else 
  { 
        p_num++; // El pid devuelto mayor que 0 es el proceso padre 
  } 
  printf("p_num=%d, c_num=%d\n",p_num,c_num); 
  printf(" pid=% d\n",pid); 
  return 0; 
} 
​//
Los resultados de la ejecución son los siguientes: 
p_num=1, c_num=0 
pid=36101 
p_num=0, c_num=1 
pid=0

El proceso hijo siempre puede consultar su PPID para saber quién es su proceso padre, de modo que un par de procesos padre e hijo puedan consultarse entre sí en cualquier momento.

otro:

  • El concepto de copia en escritura de fork() se puede encontrar en línea. Es decir, un recurso solo se copiará cuando se utilice, los recursos que no necesiten ser modificados no se copiarán, intente posponer las operaciones con alto consumo del sistema hasta que sea necesario.

  • vfork() no se usa comúnmente y es posible que la implementación no esté completamente libre de problemas. Puede buscar en línea para comprender el concepto.

Funciones familiares ejecutivas de separación de procesos

Después de pasar la bifurcación, el proceso hijo no es independiente del proceso padre y utiliza el mismo código. Hay otro problema: en este momento, el segmento de tiempo del proceso hijo se divide en dos y se comparte con el proceso padre. Para separar completamente el proceso padre y el proceso hijo, se utiliza una función familiar ejecutiva de llamada al sistema, que lee otro archivo de programa y lo ejecuta en el espacio de proceso actual. Después de crear un proceso, generalmente reemplazamos el proceso hijo con una nueva imagen de proceso, lo que se puede hacer usando la serie de funciones exec y el nuevo proceso tiene el mismo PID que el proceso original.

Consulte el uso y comparación de la familia de funciones ejecutivas (execl, execv, execle, execve, execlp, execvp, fexecve) en Linux Blog de Leumber: blog CSDN exec y execv, el uso de funciones de la serie exec (execl, execlp, execle , execv, execvp ) Blog de _gauss-CSDN blog Hay ejemplos de uso de cada API, la diferencia entre exec y execv-CSDN .

  • Cada prototipo de API: #include <unistd.h>

    int execl(const char *ruta, const char *arg, ...);

    int execlp(const char *archivo, const char *arg, ...);

    int execle(const char *ruta, const char *arg, ..., char * const envp[]);

    int execv(const char *ruta, char *const argv[]);

    int execvp(const char *archivo, char *const argv[]);

    int execve(const char *ruta, char *const argv[], char *const envp[]);

    Parámetros entrantes: el parámetro de ruta indica el nombre del programa que desea iniciar, incluido el nombre de la ruta; el parámetro de archivo indica el nombre del programa/archivo que se iniciará (el sistema busca el programa en la variable de entorno RUTA, por lo que no es necesario incluir el nombre de la ruta completa); el parámetro arg indica los parámetros que lleva el programa de inicio. Generalmente, el primer parámetro es el nombre del comando que se ejecutará, no la ruta, y arg debe terminar en NULL ;

    Valor de retorno: devuelve 0 en caso de éxito, -1 en caso de error.

  • En el nombre de la función de la familia exec, l representa la lista y v representa la matriz.

    • execl, execlp y execle utilizan un parámetro separado para cada parámetro de línea de comando del nuevo programa. Esta lista de parámetros termina en NULL.

    • Para execv, execvp, execve y fexecve, primero debe construir una matriz de punteros que apunten a cada parámetro y luego pasar la dirección de la matriz como parámetro.

  • La terminación p en el nombre de la función de la familia exec indica que el primer parámetro de la función es el nombre del archivo.

    • La diferencia entre execlp y execvp de otras funciones es que el primer parámetro es el nombre del archivo y la variable de entorno PATH se usa para encontrar el archivo ejecutable. El nombre del archivo puede ser la ruta del archivo más el nombre del programa o puede ser /sbin: / bin: /usr bajo la variable de entorno PATH. /bin: El comando de shell.

  • El final de e en el nombre de la función de la familia exec indica que se puede pasar la información de la tabla de entorno.

    • A execle, execve y fexecve se les puede pasar un puntero a una matriz de punteros de cadena de entorno.

// proceso.c 
#include<stdio.h> 
#include<unistd.h> 
​int
main() 
{ 
    int pid = fork(); 
    if(pid == 0) 
    { 
        execv("./test.o", NULL); // test.o es un archivo compilado en lenguaje C, recuerde poner la ruta absoluta de test.o aquí 
    } 
    printf("Este es el proceso padre\n"); 
    return 0; 
} 
​//
test.c 
# include<stdio.h> 
int main() 
{ 
    printf("Este es el proceso hijo"); 
    return 0; 
} 
​//
El resultado de la ejecución es el siguiente 
Este es el proceso padre 
Este es el proceso hijo

Ejemplos de uso de funciones familiares ejecutivas

/* exec.c */ 
#include <unistd.h> 
main() 
{ 
    char *envp[]={"PATH=/tmp","USER=lei","STATUS=testing",NULL}; /* Matriz Debe terminar con NULL*/ 
    char *argv_execv[]={"echo", "excuted by execv", NULL}; 
    char *argv_execvp[]={"echo", "ejecutado por execvp", NULL}; 
    char *argv_execve [ ]={"env", NULL}; 
    if(fork()==0) 
        if(execl("/bin/echo", "echo", "ejecutado por execl", NULL)<0) /* Nombre de ruta completo , escriba todos los parámetros pasados ​​y finalice con NULL*/ 
            perror("Err on execl"); 
    if(fork()==0) 
        if(execlp("echo", "echo", "executed by execlp",NULL)<0) /* Escribe solo el nombre del archivo del programa ejecutable, el sistema lo buscará en la variable de entorno PATH*/ 
            perror("Err on execlp"); 
    if(fork()==0)
        if(execle("/usr/bin/env", "env", NULL, envp)<0) /* Se pueden pasar variables de entorno*/ perror("Error en 
ejecutada por execv
            perror("Err on execle"); 
    if(fork()==0) 
        if(execv("/bin/echo", argv_execv)<0) /* Con v, el parámetro entrante son datos de puntero (datos de cadena) son pasadas, otras API son las mismas que las anteriores*/ 
            perror("Err on execv"); 
    if(fork()==0) 
        if(execvp("echo", argv_execvp)<0) 
            perror("Err on execvp" ); 
    if(fork()==0) 
        if(execve("/usr/bin/env", argv_execve, envp)<0) 
            perror("Error en execve"); 
} 
/
* Ejecutar./exec 后返回: 
ejecutado por execl 
PATH=/tmp 
USER=lei 
STATUS=prueba 
ejecutado por execlp 
ejecutado por execvp 
PATH=/tmp 
USER=lei 
STATUS=testing 
*/

Devoluciones de errores comunes de las funciones de la familia exec (exec devuelve -1 y establece errno en los siguientes valores):

  • No se puede encontrar el archivo o la ruta y errno está establecido en ENOENT;

  • Las matrices argv y envp se olvidan de terminar en NULL y errno se establece en EFAULT en este momento;

  • No hay permiso para ejecutar el archivo que se va a ejecutar y errno está configurado en EACCES.

  • Espera, hay muchos tipos de devoluciones de errores.

Más cosas a tener en cuenta:

  • En la operación real, todos los archivos abiertos generalmente se cierran antes de llamar a la función ejecutiva. También puedes dejar que el kernel lo haga a través de fcntl().

Salida del proceso retorno/salida()

Consulte varios mecanismos de salida del blog-CSDN del proceso_Leon_George , sistema operativo-salida del proceso (salida) Salida del blog-CSDN del blog de Dawn_sf ,  función de salida y la diferencia con el blog-CSDN de return_panda19881 .

Varios métodos de salida.

  • Salir normalmente

    1. Ejecute return en la función main() (después de ejecutar rentturn, el control se entrega a la función que llama).

    2. Llame a la función exit() (después de ejecutar la salida, el control se entrega al sistema).

    3. Llame a la función _exit() (igual que arriba).

  • dejar de fumar inesperadamente

    1. Llame a la función de aborto (salir significa la terminación normal del proceso, cancelar significa una terminación anormal y resalta la excepción).

    2. Un proceso recibe una señal que hace que el programa finalice.

exit()_exit()La diferencia : la exit()función es _exit()un contenedor encima de la función, que se llamará _exit()y eliminará la secuencia (stdin, stdout, stderr...) antes de llamar, es decir, escribirá el contenido del búfer del archivo nuevamente en el archivo. exit se declara en el archivo de encabezado stdlib.h y _exit() se declara en el archivo de encabezado unistd.h. exit()Es más seguro de usar . El parámetro exit_code en la salida es 0, lo que significa que el proceso termina normalmente (es decir exit(0);), y si son otros valores, significa que ocurrió un error durante la ejecución del programa.

exitreturnDiferencia con : Si aparece retorno o salida en la función principal, las dos funciones son iguales (es decir, return 0;iguales exit(0);que). Si return aparece en una subrutina, significa regresar (solo significa salir/finalizar la función o subproceso en el que se encuentra), mientras que exit aparece en un proceso hijo, lo que significa terminar el proceso hijo.

Pero no importa qué método de salida se utilice, el sistema eventualmente ejecutará un determinado código en el kernel. Este código se utiliza para cerrar el descriptor de archivo abierto utilizado por el proceso y liberar la memoria y otros recursos que ocupa.

La diferente secuencia de terminación de los procesos padre e hijo producirá resultados diferentes.

  • El proceso hijo termina antes que el proceso padre, y el proceso padre llama a la función de espera: el proceso hijo sale y el proceso padre lo recicla (bueno)

En este momento, el proceso principal esperará a que finalice el proceso secundario.

  • El proceso padre termina antes que el proceso hijo: el proceso hijo se convierte en un proceso huérfano (medio)

Esta situación es el proceso huérfano que utilizamos anteriormente. Cuando el proceso principal sale primero, el sistema permitirá que el proceso de inicio se haga cargo del proceso secundario. El proceso huérfano se adoptará en el proceso de inicio y el proceso de inicio se convertirá en el proceso principal del proceso. El proceso de inicio es responsable de llamar a la función de espera cuando finaliza el proceso hijo. El proceso de inicio llamará a la función de espera cuando salga un proceso hijo.

  • El proceso hijo termina antes que el proceso padre y el proceso padre no llama a la función de espera: el proceso hijo se convierte en un proceso zombie (malo)

En este caso, el proceso hijo entra en un estado zombie y permanecerá así hasta que se reinicie el sistema. Cuando el proceso hijo está en estado zombie, el kernel solo guarda cierta información necesaria sobre el proceso para el proceso padre. En este momento, el proceso hijo siempre ocupa recursos y también reduce la cantidad máxima de procesos que el sistema puede crear.

Por tanto, debemos intentar evitar que ocurra esta situación, de lo contrario el proceso zombie no solo ocupará recursos, sino que también acumulará cada vez más recursos. Un programa incorrecto también puede hacer que la información de salida del proceso hijo permanezca en el kernel (el proceso padre no llama a la función de espera en el proceso hijo), en cuyo caso el proceso hijo se convierte en un proceso zombie. Cuando se acumula una gran cantidad de procesos zombies, se ocupará espacio en la memoria.

Estado zombi: un proceso que ha finalizado pero cuyo proceso principal aún no se ha ocupado de él (obteniendo información sobre el proceso secundario finalizado y liberando los recursos que aún ocupa) se denomina proceso zombi (zombi).

Definición de proceso zombie y proceso huérfano:

Resumen: cuando estos tres procesos salen, la mejor situación es que el proceso padre llame a esperar y el proceso hijo terminado se recicle normalmente. La segunda es que el proceso padre termina temprano/el proceso hijo se llama proceso huérfano/el proceso hijo es adoptado para el proceso de inicio/el proceso de inicio estará allí. La función de espera se llama cuando el proceso hijo sale. Lo peor es que el proceso padre no llama a esperar y el proceso hijo sale/el proceso hijo se convierte en un proceso zombie.

Registre la función llamada cuando el proceso sale: #include <stdlib.h> int atexit (void (*function)(void));, la función registrada se exit();llamará una vez al llamar o salir de main o recibir una señal para finalizar el proceso (SIGTERM o SIGKILL). La función registrada no se puede volver a llamar exit(), de lo contrario provocará una recursividad infinita.

Espera de bloqueo de proceso()

Un proceso en estado de ejecución espera que ocurra un determinado evento durante su proceso de ejecución, como esperar la entrada del teclado, esperar a que se complete la transmisión de datos del disco y esperar a que otros procesos envíen información.Cuando no ocurre el tiempo de espera, el proceso mismo realizará el bloqueo. Primitivo para cambiar su estado de ejecución al estado de bloqueo.

Es decir, dormir/bloquear para esperar una señal (señal) o el proceso padre espera a que el proceso hijo salga (y luego reclame sus recursos) .

ps. La espera de señales se presenta en 进程间通讯(IPC)la 信号(Signal)siguiente sección. Aquí solo analizamos el proceso padre que espera a que salga el proceso hijo (y luego recupere sus recursos).

Citando la diferencia entre exec y execv - CSDN (Mirando hacia atrás, me di cuenta de que había ordenado el formato de manera más hermosa...).

El proceso padre espera bloques para esperar a que el proceso hijo salga (y luego recupere sus recursos)

Cuando finaliza un proceso hijo, el núcleo envía una señal SIGCHILD a su proceso padre.

Cuando finaliza el proceso hijo, notificará al proceso padre, borrará la memoria que ocupa y dejará su propia información de salida en el kernel (el código de salida, si se ejecuta sin problemas, es 0; si hay un error o una condición anormal, es > 0 entero). Cuando el proceso padre se entera de que el proceso hijo ha terminado, es responsable de utilizar la llamada al sistema de espera en el proceso hijo (de lo contrario, el proceso hijo se convertirá en un proceso zombie y debe evitarse tanto como sea posible). Esta función de espera puede recuperar la información de salida del proceso hijo del kernel y borrar el espacio ocupado por esta información en el kernel.

#include <sys/types.h> /* Proporciona la definición de tipo pid_t*/ 
#include <sys/wait.h> 
pid_t wait(int *status);

Una vez que un proceso llama a esperar, se bloquea inmediatamente. Wait analiza automáticamente si un proceso hijo del proceso actual ha salido. Si encuentra un proceso hijo que se ha convertido en un zombi, esperar recopilará información sobre el proceso hijo y lo destruirá. completamente antes de regresar; si no se encuentra dicho proceso secundario, esperar se bloqueará aquí hasta que aparezca uno.

El parámetro de estado se utiliza para guardar algún estado cuando sale el proceso recopilado y es un puntero al tipo int. Pero si no nos importa cómo se vuelca el proceso hijo y solo queremos destruir el proceso zombie, podemos establecer este parámetro en NULL.

Si tiene éxito, esperar devolverá el ID del proceso secundario recopilado. Si el proceso que llama no tiene procesos secundarios, la llamada fallará. En este momento, esperar devolverá -1 y errno se establecerá en ECHILD.

ejemplo:

/* wait1.c */ 
#include <sys/types.h> 
#include <sys/wait.h> 
#include <unistd.h> 
#include <stdlib.h> 
main() 
{ 
    pid_t pc, pr; 
    pc = fork(); 
    if(pc < 0) /* Si ocurre un error*/ 
        printf("¡Ocurrió un error!\n"); 
    else if(pc == 0){ /* Si es un proceso hijo*/ 
        printf( "Este es un proceso hijo con pid de %d\n",getpid()); 
        sleep(10); /* dormir durante 10 segundos*/ 
    } 
    else{ /* si es un proceso padre*/ 
        pr = wait(NULL ); /* aquí Espere la salida del proceso hijo, independientemente de su valor de retorno de salida*/ 
        printf("Capté un proceso hijo con pid de %d\n"),pr); 
    } 
    exit(0); 
}

Macros para un análisis más detallado de los valores de retorno de salida:

  • WIFEXITED(estado) Esta macro se utiliza para indicar si el proceso hijo salió normalmente. Si es así, devolverá un valor distinto de cero. (Tenga en cuenta que, aunque el nombre es el mismo, el estado del parámetro aquí no es diferente del único parámetro de espera: estado, un puntero a un número entero, pero el número entero al que apunta ese puntero)

  • WEXITSTATUS(estado) Cuando WIFEXITED devuelve un valor distinto de cero, podemos usar esta macro para extraer el valor de retorno del proceso hijo. Si el proceso hijo llama a exit(5) para salir, WEXITSTATUS(estado) devolverá 5; si el el proceso hijo llama a exit(7)), WEXITSTATUS(estado) devolverá 7. Tenga en cuenta que si el proceso no finaliza correctamente, es decir, WIFEXITED devuelve 0, este valor no tiene sentido.

Hablemos de ello nuevamente en otro artículo:

pid_t wait(int *status);, la llamada al sistema de espera hace que el proceso principal se bloquee hasta que finalice un proceso secundario o el proceso principal reciba una señal. Si no hay ningún proceso principal ni ningún proceso secundario o su proceso secundario ha finalizado, la espera regresará inmediatamente. En caso de éxito (debido a la terminación de un proceso hijo), la espera devolverá el ID del proceso hijo; de lo contrario, devolverá -1 y establecerá la variable global errno. status es el estado de salida del proceso hijo. El proceso hijo llama a exit/_exit o return para establecer este valor. Para obtener este valor, Linux define varias macros para probar este valor de retorno.

Otro artículo habla específicamente sobre las diferencias entre varias macros:

#include <sys/wait.h> 
int WIFEXITED (estado); 
int WIFSIGNALED (estado); 
int WIFSTOPPED (estado); 
int WIFCONTINUED (estado); 
int WEXITSTATUS (estado); 
int WTERMSIG (estado); 
int WSTOPSIG (estado); 
int WCOREDUMP (estado);

waitpid bloquea y espera a que salga el proceso hijo de un pid específico

#include <sys/types.h> /* Proporciona la definición de tipo pid_t*/ 
#include <sys/wait.h> 
pid_t waitpid(pid_t pid,int *status,int options);

descripción del parámetro waitpid():

  • pid:

    Cuando pid>0, solo espere el proceso hijo cuyo ID de proceso sea igual a pid. No importa cuántos otros procesos hijos hayan terminado de ejecutarse y hayan salido, waitpid continuará esperando mientras el proceso hijo especificado no haya finalizado.

    Cuando pid = -1, espere a que cualquier proceso hijo salga sin restricciones. En este momento, waitpid y wait tienen exactamente la misma función.

    Cuando pid = 0, espere cualquier proceso hijo en el mismo grupo de procesos. Si el proceso hijo se ha unido a otro grupo de procesos, waitpid no le prestará atención.

    Cuando pid <-1, espere cualquier proceso hijo en un grupo de procesos específico cuyo ID sea igual al valor absoluto de pid.

  • estado: acepte el valor de retorno de salida del proceso hijo, complete NULL cuando no esté en uso.

  • opciones: las opciones proporcionan algunas opciones adicionales para controlar waitpid. Actualmente, solo se admiten dos opciones, WNOHANG y WUNTRACED, en Linux. Estas son dos constantes. Se pueden conectar usando el operador "|" si no queremos usarlas. , también puede configurar las opciones en 0 (establecer en 0 cuando no esté en uso).

    WNOHANG: Incluso si no existe ningún proceso hijo, regresará inmediatamente y no esperará eternamente como esperar.

    WUNTRACED: Implica cierto conocimiento sobre seguimiento y depuración, y rara vez se utiliza. Los lectores interesados ​​pueden consultar los materiales relevantes por sí mismos.

  • valor de retorno:

    El valor de retorno de waitpid es un poco más complicado que esperar, hay tres situaciones:

    1. Cuando regresa normalmente, waitpid devuelve el ID del proceso recopilado del proceso hijo;

    2. Si se establece la opción WNOHANG y waitpid no encuentra procesos secundarios salidos para recopilar durante la llamada, se devuelve 0;

    3. Si ocurre un error durante la llamada, se devuelve -1 y errno se establecerá en el valor correspondiente para indicar el error;

    Cuando el proceso hijo indicado por pid no existe, o el proceso existe pero no es un proceso hijo del proceso que llama, waitpid devolverá un error y errno se establece en ECHILD.

    El estado final del proceso hijo se devuelve y se almacena en estado. Hay varias macros a continuación para determinar el estado final:

    • WIFEXITED(estado) es un valor distinto de cero si el proceso secundario finaliza normalmente.

    • WEXITSTATUS (estado) obtiene el código final devuelto por la salida del proceso hijo (). Generalmente, WIFEXITED se usa primero para determinar si finaliza normalmente antes de usar esta macro.

    • WIFSIGNALED(estado) Este valor de macro es verdadero si el proceso secundario finaliza debido a una señal.

    • WTERMSIG (estado) obtiene el código de señal del proceso hijo que finaliza debido a una señal. Generalmente, WIFSIGNALED se usa para juzgar antes de usar esta macro.

    • WIFSTOPPED (estado) Este valor de macro es verdadero si el proceso secundario está suspendido. Generalmente esto sucede sólo cuando se utiliza WUNTRACED.

    • WSTOPSIG(status) obtiene el código de señal que provocó la pausa del proceso secundario.

Otras API

Archivo de cabeza:

#incluye <unistd.h>; 
#incluir <contraseña.h>; 
#incluye <sys/types.h>;
  • pid_t getpid(void);, obtiene el pid del proceso actual.

    pid_t getppid(void);, obtiene el pid del proceso padre del proceso actual.

  • Conceptos relacionados con el usuario real y usuario efectivo del proceso:

  • Cambie la API de usuario real:

  • Cambie la API de usuario efectiva:

  • Obtener ID de usuario:

    uid_t getuid(void);, uid_t geteuid(void);, obtienen el ID y el ID de usuario efectivo del usuario propietario del proceso respectivamente. gid_t getgid(void);, git_t getegid(void);, obtienen el ID de grupo y el ID de grupo efectivo respectivamente.

  • Obtenga más información sobre el usuario:

    struct passwd { /* Esta estructura se define en tipos.h */ 
        char *pw_name; /* Nombre de inicio de sesión */ 
        char *pw_passwd; /* Contraseña de inicio de sesión */ 
        uid_t pw_uid; /* ID de usuario */ 
        gid_t pw_gid; /* Usuario ID de grupo */ 
        char *pw_gecos; /* Nombre real del usuario */ 
        char *pw_dir; /* Directorio del usuario */ 
        char *pw_shell; /* SHELL del usuario */ 
    }; 
    struct passwd *getpwuid(uid_t uid); / * Devuelve el struct passwd puntero de estructura de la información del usuario cuyo ID de usuario es uid*/
  • sleep(x);, bloqueando/retrasando durante x segundos.

  • strerror(errno), devuelve una cadena de información de error para el número de error especificado.

  • etc.

Supongo que te gusta

Origin blog.csdn.net/Staokgo/article/details/132630662
Recomendado
Clasificación