Outono 2020 _ notas do sistema operacional (1): processos e threads

Processo, discussão

Estado da linha

Estado da linha

Status do processo

Processo 3.0 - Estado do processo, Processo zumbi, Processo órfão
5 estados do processo
O ciclo de vida de um processo pode ser dividido em um conjunto de estados, que descrevem todo o processo. O estado do processo reflete o estado de vida de um processo.
De modo geral, existem cinco estados de um processo:
Estado de criação : quando um processo é criado, ele precisa se inscrever para um PCB em branco, preencher as informações do processo de controle e gerenciamento e concluir a alocação de recursos. Se o trabalho de criação não puder ser concluído, como os recursos não podem ser satisfeitos, não pode ser agendado para ser executado. O estado do processo neste momento é chamado de estado de criação estado
pronto : o processo está pronto e os recursos necessários foram alocados, desde que a CPU seja alocada, pode ser imediatamente correndo
estado de execução : Após o processo está no estado pronto e está programado, o processo entra no estado de execução.
bloqueio estado : o processo de execução é temporariamente incapaz de executar devido a certos eventos (I / pedido ó, não aplicação de uma área de buffer), e o processo é bloqueado. Quando a solicitação é atendida, ele entra no estado pronto e aguarda o
estado de finalização da chamada do sistema : o processo termina ou ocorre um erro ou é finalizado pelo sistema e entra no estado de finalização. Não pode mais executar

Processo pai, processo filho, zumbis entram na cidade, processo órfão

Processo 3.0 - Status do Processo e Processo Zombie, Processo Órfão
Quando um processo filho termina de ser executado (geralmente causado pela chamada de saída, um erro fatal ou recebendo um sinal de encerramento), o status de saída do processo filho (valor de retorno) será relatado Para o sistema operacional, o sistema informa ao processo pai do evento que o processo filho é encerrado com o sinal SIGCHLD, e o bloco de controle de processo (PCB) do processo filho ainda reside na memória neste momento. De modo geral, após receber o SIGCHLD, o processo pai usará a chamada do sistema de espera para obter o status de saída do processo filho e, em seguida, o kernel pode liberar o PCB do processo filho encerrado da memória; se o processo pai não fizer isso, o processo filho O PCB do processo sempre residirá na memória, ou seja, se tornará um processo zumbi .
Quando um processo está sempre no estado Z, seu PCB sempre será mantido. Como o próprio PCB é uma estrutura que ocupa espaço, os processos zumbis também causarão desperdício de recursos, portanto, devemos evitar processos zumbis.

Processo órfão significa que quando um processo pai sai enquanto um ou mais de seus processos filho ainda estão em execução, esses processos filhos se tornam processos órfãos. O processo órfão será adotado pelo processo init (o número do processo é 1) e o processo init completará o trabalho de coleta de estado para eles.
O processo órfão não tem nenhum dano devido à reciclagem wait () do ciclo do processo init.

A diferença entre processo e thread

Um processo tem um espaço de usuário independente; vários threads do mesmo processo compartilham os recursos de espaço do usuário do processo e há problemas de segurança. No caso de simultaneidade multi-thread, os recursos compartilhados precisam ser bloqueados para obter os resultados corretos.
Vantagens do processo: Os processos são independentes uns dos outros, o que pode reduzir o risco causado por uma exceção ou saída do processo; o processo não precisa ser bloqueado, o que pode economizar o custo dos bloqueios.
Vantagem da rosca: o processo do garfo é muito caro. Fork copia a imagem da memória do processo pai para o processo filho e copia todos os descritores no processo filho e assim por diante. Threads fornecem um recurso de simultaneidade que pode executar vários threads ao mesmo tempo em um processo. Cada thread tem seus próprios registros de hardware e pilha. Todos os threads em um processo compartilham todos os endereços de espaço virtual, todos os descritores de arquivo, comportamentos de sinal e outros recursos de processo.

Processos e threads são essencialmente um período de trabalho da CPU, e o processo inclui o contexto do programa de carregamento da CPU, a execução da CPU e o contexto do programa de economia da CPU. Threads são incluídos no processo. Um processo tem pelo menos um thread ou vários threads. Threads diferentes do processo compartilham a CPU e o contexto do programa.
Processo é a menor unidade de alocação de recursos do sistema operacional (incluindo CPU, memória, E / S de disco, etc.); thread é a unidade básica de agendamento e alocação de CPU e a menor unidade de execução.
PS: O espaço de endereço virtual contínuo entre diferentes processos é mapeado para diferentes endereços físicos discretos pela MMU, de modo que os dados do espaço do usuário entre diferentes processos não são compartilhados. Além disso, o código e os dados no espaço do kernel são compartilhados entre diferentes processos.

Corrotina (corrotina)

A troca de thread precisa entrar no modo kernel, e a troca de co-rotina é realizada inteiramente no modo de usuário, que é mais eficiente.
Uma co-rotina, também conhecida como micro-thread, é uma sub-rotina. No processo de execução, a sub-rotina pode ser interrompida, e então outra sub-rotina pode ser executada, e então retornar para continuar a execução no momento apropriado. (Interromper em uma sub-rotina para executar outras sub-rotinas, não chamadas de função!) A
co-rotina é a alternância de sub-rotinas ao invés da alternância de threads. Comparado com o multithreading, ele economiza a sobrecarga da alternância de threads e tem alta eficiência de execução.
PS: a alternância da co-rotina é totalmente realizada no modo de usuário. Sua sobrecarga é apenas para alternar o contexto da CPU; a alternância de thread só pode ser concluída no modo kernel com a autoridade mais alta. Além da alternância de contexto da CPU, há também a sobrecarga de alternar entre o modo de usuário e o modo kernel. O algoritmo de escalonamento de thread completa a sobrecarga do escalonamento de thread.
PS: Multi-thread + co-rotina, ou seja, uso total de multi-core, e dar jogo total à alta eficiência da co-rotina, consegue um desempenho extremamente alto.

Estratégia de agendamento de processos

  1. Algoritmo de agendamento por ordem de chegada (FCFS): implementação de fila, o trabalho que entra na fila de trabalho primeiro aloca recursos, cria um processo e, em seguida, o coloca na fila pronta, não preemptiva, e o tempo médio de espera é geralmente relativamente longo.
  2. Algoritmo de escalonamento Shortest Job First (SJF): Prioriza o escalonamento de um ou vários jobs curtos ou processos curtos, com o menor tempo médio de espera, mas é impossível saber a duração do próximo intervalo de CPU do processo.
  3. Algoritmo de escalonamento de prioridade: quanto maior a prioridade, mais cedo ela é alocada e a mesma prioridade é alocada de acordo com o FCFS. Processos com baixa prioridade esperam indefinidamente pela CPU, o que pode causar problemas de bloqueio infinitos ou inanição . Este problema pode ser resolvido com o envelhecimento da tecnologia , ou seja, aumentando gradativamente a prioridade de processos que estavam há muito tempo esperando no sistema.
  4. Algoritmo de escalonamento round-robin de fração de tempo: especialmente desenvolvido para sistemas de compartilhamento de tempo, se o intervalo de CPU de um processo exceder uma fração de tempo, o processo será interrompido e colocado de volta na fila de espera.
  5. Algoritmo de agendamento de fila multinível: A fila pronta é dividida em várias filas independentes, cada fila tem seu próprio algoritmo de agendamento e o agendamento de preempção de prioridade fixa é adotado entre as filas . Um processo é alocado permanentemente a uma fila de acordo com seus próprios atributos.
  6. Algoritmo de agendamento de fila de feedback de vários níveis: Comparado com o algoritmo de agendamento de fila de feedback de vários níveis, o algoritmo de agendamento de fila de feedback de vários níveis permite que os processos se movam entre as filas de acordo com as condições reais. Se um processo usar muito tempo de CPU, ele será transferido para uma fila de prioridade mais baixa; processos que esperaram muito tempo em uma fila de prioridade mais baixa serão transferidos para uma fila de prioridade mais alta para evitar fome.

Comunicação entre processos e threads

Blog de referência
Comunicação entre processos: pipes e pipes nomeados, sinais / eventos, filas de mensagens, memória compartilhada, semáforos, sockets.
Pipes e pipes nomeados são usados ​​principalmente para comunicação local entre processos, soquetes são usados ​​principalmente para comunicação entre processos de rede e memória compartilhada e semáforos são usados ​​principalmente para sincronização entre processos.
Comunicação entre threads: mecanismo de bloqueio (mutexes, variáveis ​​de condição, bloqueios de leitura e gravação, bloqueios de rotação), mecanismo de semáforo, mecanismo de sinal / mecanismo de evento.
O propósito da comunicação entre threads é principalmente para sincronização de threads, portanto, as threads não têm um mecanismo de comunicação para troca de dados como na comunicação de processo.

Modo de comunicação entre processos - qual é o cenário de uso da fila de mensagens de pipe (pipe)
?

Sincronização de processos e threads

A sincronização do processo / thread é principalmente para resolver o problema do acesso concorrente aos dados compartilhados (área compartilhada), então a sincronização refere-se à sincronização do acesso à área compartilhada (de acordo com a sequência estabelecida, um acesso precisa ser bloqueado e aguarda o acesso anterior ser concluído antes de começar )

Sincronização do processo : semáforo (contador, operação PV), bloqueio de rotação (o chamador espera ciclicamente), monitor (a operação definida no monitor é chamada apenas por um processo ao mesmo tempo), encontro (encontro adequado para não público Mecanismo de sincronização do sistema de memória distribuída).


Existem duas implementações de semáforos Linux : semáforos System V e semáforos Posix. Os semáforos System V são implementados por semget, semop e semctl. Os semáforos Posix são implementados por sem_init, sem_destroy, sem_wait (equivalente à operação P ), sem_post (equivalente à operação V) essas funções para alcançar. (System V e Posix são protocolos de interface aplicados a sistemas operacionais)
Operações P e V de semáforos: Existem apenas duas operações para semáforos, esperando e enviando sinais, que são representados por P (s) e V (s) respectivamente. As operações P e V são inseparáveis ​​(operações atômicas). Chame a operação P para testar se a mensagem chegou e chame a operação V para enviar a mensagem.

  • P (s): O processo se aplica aos recursos. Se o recurso atingiu o limite máximo de acesso ao processo (s é 0), você precisa esperar. Se o valor de s for maior que 0, então P diminui o valor de s em 1. Se s for 0, o processo é suspenso até que s se torne diferente de zero. Uma operação em V despertará este segmento.
  • V (s): Liberar recursos. Aumente s em 1. Se houver um processo esperando que s se torne diferente de zero, ative o processo e diminua s em 1.

Explicação detalhada: Defina s como o número máximo de processos que acessam este recurso. Para cada acesso de processo adicional a recursos compartilhados, s é reduzido em 1. Contanto que s seja maior que 0, um semáforo pode ser enviado para permitir que o processo que solicita o recurso acesse o recurso compartilhado . Mas quando s é reduzido a 0, significa que o número de processos atualmente ocupando recursos atingiu o número máximo permitido e outros processos não têm permissão para acessar o recurso compartilhado.Neste momento, o semáforo não pode ser enviado. Depois que o processo termina de processar o recurso compartilhado, s é incrementado em 1.

Sincronização de thread : seção crítica (seção crítica), mecanismo de bloqueio [mutex e variável de condição (Condition_variable), bloqueio de leitura e gravação, bloqueio de rotação], semáforo (semáforo) , Sinal / Evento (Evento).

Resumo:

  • A seção crítica acessa recursos comuns ou um pedaço de código por meio da serialização de vários threads, o que é rápido; o mutex é projetado para coordenar o acesso a um recurso compartilhado; o semáforo é projetado para controlar um recurso com um número limitado de usuários; Os eventos são usados ​​para encadear alguns eventos ocorridos, iniciando assim o início das tarefas subsequentes.

A diferença entre semáforo e mutex?
A diferença entre semáforo e mutex:

  • Mutex: uma variável binária, 0-desbloqueio, 1-bloqueio, operação de desbloqueio deve ser realizada pelo segmento bloqueado, o objetivo principal é proteger os recursos compartilhados , ou seja, garantir que os recursos compartilhados só possam ser usados ​​por um segmento por vez (recursos críticos );
  • Semáforo: um valor inteiro não negativo, o objetivo principal é agendar threads / processos , permitindo que vários threads / processos acessem um recurso compartilhado ao mesmo tempo, mas limitará o número máximo de threads / processos que podem acessar este recurso ao mesmo tempo. Se o valor do semáforo for apenas 0 ou 1, ele é um semáforo binário e a função é como um bloqueio de exclusão mútua.

[Multithreading] A diferença entre um mutex e uma seção crítica A diferença entre uma
seção crítica e um mutex:

  • Seção crítica: seção crítica refere-se a um trecho de código, que é usado para acessar recursos críticos. Os recursos críticos podem ser recursos de hardware ou recursos de software. Mas eles têm uma característica que permite o acesso apenas a um processo ou thread de cada vez. Quando vários threads tentam acessar a seção crítica ao mesmo tempo, mas um thread já está acessando a seção crítica, os outros threads serão suspensos. Depois que a seção crítica for lançada, outros threads podem continuar a antecipar a seção crítica.
  • A diferença com o bloqueio mutex: a seção crítica é um mecanismo de sincronização leve. Comparada com mutexes e eventos, esses objetos de sincronização do kernel, a seção crítica é um objeto no modo de usuário, ou seja, a exclusão mútua de threads só pode ser realizada no mesmo processo. . Como não há necessidade de alternar entre o modo de usuário e o modo principal, a eficiência do trabalho é muito maior para exclusão mútua; mutexes podem ser nomeados, portanto, mutexes não podem ser usados ​​apenas para acesso a recursos compartilhados em diferentes threads do mesmo aplicativo A sincronização também pode ser usada para sincronizar o acesso a recursos compartilhados entre threads de aplicativos diferentes. (O mutex pode ser acessado por qualquer thread de qualquer processo em todo o sistema, mas limita estritamente que apenas o thread que adquiriu o mutex pode liberar o mutex )

Espaço do usuário e espaço do kernel, modo do usuário e modo do kernel:

  • Os recursos do espaço do kernel entre diferentes processos são compartilhados e os recursos do espaço do usuário são independentes. Portanto, para conseguir a sincronização de fontes de acesso a recursos entre diferentes processos ou entre threads de diferentes processos, é necessário entrar no estado do kernel, e os objetos de sincronização no kernel realizam a sincronização do processo. Os recursos do espaço do usuário e do espaço do kernel são compartilhados entre diferentes threads do mesmo processo, de modo que a sincronização das threads pode ser realizada apenas pelo objeto de sincronização no modo de usuário.

Bloqueio de leitura e gravação:

  • Se o thread atual lê dados, outros threads têm permissão para ler dados, mas não para gravar;
  • Se o thread atual gravar dados, outros threads não têm permissão para ler ou gravar dados.

Condições e estratégias de processamento para deadlock

Quatro condições necessárias para o impasse

  1. Mutuamente exclusivo: pelo menos um recurso só pode ser usado por um processo por vez.
  2. Ocupar e aguardar: quando um processo é bloqueado por solicitação de recursos, ele continua retendo os recursos adquiridos.
  3. Não preemptivo: Os recursos adquiridos pelo processo não podem ser antecipados antes de serem usados.
  4. Espera cíclica: Uma relação de recurso de espera cíclica é formada entre vários processos.

Tratamento de deadlock

  1. Impedir deadlock: Certifique-se de que pelo menos uma das quatro condições necessárias para o deadlock não seja estabelecida. Romper a condição "não preemptiva" permite que o processo forçosamente agarrar certos recursos do ocupante; quebrar a condição "ocupar e aguardar" permite que o processo solicite recursos apenas quando não os ocupa.
  2. Evite deadlock: detecte dinamicamente o status de alocação de recursos para garantir que a condição de espera do loop não seja estabelecida.
  3. Detectar e liberar deadlock: permite a ocorrência de deadlock, mas pode detectar a ocorrência de deadlock e limpar o deadlock do sistema.Os métodos comumente usados ​​para remover deadlock são o encerramento do processo e a preempção de recursos.

Modelo de consumidor produtor

O problema do produtor e do consumidor é um problema clássico do modelo de threading: produtores e consumidores compartilham o mesmo espaço de memória no mesmo período, o produtor adiciona ao espaço de armazenamento produtos, produtos de consumo removidos do espaço de armazenamento quando armazenados Quando o espaço está vazio, o consumidor bloqueia, e quando o espaço de armazenamento está cheio, o produtor bloqueia .
Único produtor - único consumidor: fila de cache + mutex + implementação de variável de condição. (A fila simula o espaço de armazenamento; o mutex garante a exclusão mútua entre vários threads de leitura e gravação; a variável de condição garante que o thread do consumidor será bloqueado quando a fila estiver vazia, a fila de espera não está vazia e o thread do produtor será definido quando a fila estiver cheia , A fila de espera não está cheia) -> Cenário de aplicação : Cache IO (como o buffer do soquete TCP)
Implementação simples:
manter duas variáveis ​​de posição e variáveis ​​de condição:

size_t read_pos; // 消费者读取产品位置.
size_t write_pos; // 生产者写入产品位置.
std::mutex mtx; // 互斥量,保护产品缓冲区
std::condition_variable repo_not_full; // 条件变量, 指示产品缓冲区不为满.
std::condition_variable repo_not_empty; // 条件变量, 指示产品缓冲区不为空.

Quando (write_pos + 1)% repository_size == read_pos indica que a fila está cheia e a produção deve ser bloqueada. (Repository_size é o tamanho do espaço de armazenamento (cache))

...
while(((ir->write_pos + 1) % repository_size) == ir->read_pos) {
    
     
// item buffer is full, just wait here.
    (ir->repo_not_full).wait(lock); // 生产者等待"产品库缓冲区不为满"这一条件发生.
}
... // 写入产品,写入位置后移
(ir->repo_not_empty).notify_all(); // 通知消费者产品库不为空.

Quando write_pos == read_pos, indica que a fila está vazia e os consumidores precisam ser bloqueados.

...
while(ir->write_pos == ir->read_pos) {
    
    
// item buffer is empty, just wait here
    (ir->repo_not_empty).wait(lock); // 消费者等待"产品库缓冲区不为空"这一条件发生.
}
... // 读取产品,读取位置后移
(ir->repo_not_full).notify_all(); // 通知消费者产品库不为满.

Multiprodutor-multi-consumidor: É necessário manter adicionalmente o balcão que o consumidor retira do produto e o balcão que o produtor coloca no produto, para determinar se o programa deve terminar.
PS: O modelo de consumidor produtor também pode ser implementado com uma co - rotina . Mude para uma co-rotina. Depois que o produtor produz a mensagem, ela salta diretamente para o consumidor através do rendimento para iniciar a execução. Depois que o consumidor termina a execução, ela volta para o produtor para continuar a produção, o que é extremamente eficiente. ( Blog de referência )
Corrotina

Nginx

Nginx é um servidor da web HTTP e proxy reverso de alto desempenho.
Insira a descrição da imagem aqui
Várias perguntas do Nginx feitas com frequência pelos entrevistadores

Servidor Nginx de alto desempenho, por que alto desempenho?

Resumo: Vantagens de multiprocessos, E / S assíncrona sem bloqueio (o processo principal assíncrono é conduzido por evento + soquete sem bloqueio), modelo epoll (um único processo trata com eficiência de várias conexões).

  • Limite o número de processos de trabalho tanto quanto possível para reduzir a sobrecarga causada pela troca de contexto. A configuração padrão e recomendada é permitir que cada núcleo da CPU corresponda a um processo de trabalho, de modo a usar de forma eficiente os recursos de hardware.
  • O processo de trabalho usa um único thread e usa um mecanismo de processamento de evento sem bloqueio assíncrono para lidar com várias conexões simultâneas, e a multiplexação de E / S usa o modelo epoll .

Como o Nginx consegue alta simultaneidade?

Multi-epoll + multi-processo, cada processo pode lidar com várias conexões. Epoll é adequado para lidar com um grande número de conexões simultâneas (mas cenários em que o número de conexões ativas é relativamente pequeno).
O princípio do nginx para alcançar alta simultaneidade,
uma imagem para entender a alta simultaneidade multi-thread do nginx

Benefícios de usar processos em vez de threads

  • Salve a sobrecarga causada por bloqueios, cada processo de trabalho é um processo independente, não compartilha recursos e não precisa ser bloqueado. Ao mesmo tempo, será muito mais conveniente ao programar e encontrar problemas.
  • Processo independente para redução de riscos. O uso de processos independentes pode evitar que um ao outro se afete. Depois que um processo sai, outros processos ainda estão funcionando, o serviço não será interrompido e o processo mestre iniciará rapidamente um novo processo de trabalho.

Proxy de encaminhamento e proxy reverso

O resumo é muito bom: proxy de encaminhamento e proxy reverso [Resumo]

  • O proxy de encaminhamento é um servidor localizado entre o cliente e o servidor de origem.Para obter conteúdo do servidor de origem, o cliente envia uma solicitação ao proxy de encaminhamento e especifica o destino (servidor de origem). Em seguida, o proxy de encaminhamento encaminha a solicitação ao servidor original e retorna o conteúdo obtido ao cliente. O cliente pode usar o proxy de encaminhamento. O resumo do proxy de encaminhamento é uma frase: O proxy do proxy é o cliente .
    O cliente deve configurar um servidor proxy de encaminhamento.Claro, a premissa é saber o endereço IP do servidor proxy de encaminhamento e a porta do programa proxy.
  • Proxy reverso significa usar um servidor proxy para aceitar solicitações de conexão na Internet e, em seguida, enviar a solicitação para o servidor na rede interna e retornar o resultado do servidor para o cliente solicitando a conexão na Internet. Quando o servidor proxy aparece externamente como um servidor proxy reverso. O resumo do proxy reverso é uma frase: O proxy proxy é o servidor .
    Ao contatar um proxy reverso pela primeira vez, a sensação é que o cliente não pode perceber a existência do proxy.O proxy reverso é transparente para o exterior e os visitantes não sabem que estão acessando um proxy. Porque o cliente pode acessar sem nenhuma configuração.

Balanceamento de carga

Balanceamento de carga significa que o servidor proxy distribui as solicitações recebidas a cada servidor de maneira equilibrada. O balanceamento de carga resolve principalmente o problema de congestionamento de rede, melhora a velocidade de resposta do servidor, fornece serviços próximos, atinge melhor qualidade de acesso e reduz a grande pressão de simultaneidade de servidores em segundo plano.

Recursos dinâmicos e recursos estáticos

  • Recursos estáticos: geralmente, o cliente envia uma solicitação ao servidor da web, o servidor da web busca o arquivo correspondente na memória, retorna-o ao cliente e o cliente o analisa e renderiza.
  • Recursos dinâmicos: geralmente, os recursos dinâmicos solicitados pelo cliente são entregues primeiro ao contêiner da web, o contêiner da web se conecta ao banco de dados e, após o banco de dados processar os dados, o conteúdo é entregue ao servidor da web e o servidor da web retorna ao cliente para análise e processamento de renderização.

Nginx realiza a separação estática e dinâmica do Tomcat

O seguinte conteúdo vem de: Explicação detalhada da separação estática e dinâmica do Tomcat por nginx

Por que alcançar a separação dinâmica e estática

  1. A forte capacidade do Nginx de lidar com recursos estáticos é
    principalmente porque a eficiência do nginx para processar páginas estáticas é muito maior do que a do tomcat. Se o volume da solicitação do tomcat for 1000, o volume da solicitação do nginx é 6000 vezes e a taxa de transferência do tomcat por segundo é 0,6 M, o throughput por segundo do nginx é de 3,6 M. Pode-se dizer que a capacidade do nginx de lidar com recursos estáticos é 6 vezes maior do que o tomcat, e as vantagens são evidentes.
  2. A separação de recursos dinâmicos e recursos estáticos torna a estrutura do servidor mais clara.

Princípio de separação dinâmica e estática

Dentre as solicitações recebidas pelo servidor do cliente, algumas são solicitações de recursos estáticos, como html, css, js e recursos de imagem, e algumas são solicitações de dados dinâmicos. Como o tomcat é lento no processamento de recursos estáticos, podemos considerar separar todos os recursos estáticos e entregá-los a um servidor que processa recursos estáticos mais rápido, como nginx, e entregar solicitações dinâmicas ao tomcat.

Conforme mostrado na figura abaixo, instalamos nginx e tomcat na máquina. Todos os recursos estáticos são colocados no diretório webroot do nginx e os programas solicitados dinamicamente são colocados no diretório webroot do tomcat. Quando o cliente acessa o servidor Nesse momento, se for uma solicitação de recurso estático, vá diretamente para o diretório webroot do nginx para obter o recurso. Se for uma solicitação de recurso dinâmico, o nginx usa o princípio do proxy reverso para encaminhar a solicitação ao tomcat para processamento, realizando assim a separação de dinâmico e estático. Melhore o desempenho do servidor para processar solicitações.
Insira a descrição da imagem aqui

Acho que você gosta

Origin blog.csdn.net/XindaBlack/article/details/105520268
Recomendado
Clasificación