Sistemas operacionais: três peças fáceis. Parte 3: API de Processo (tradução)

Introdução aos Sistemas Operacionais

Oi, Habr! Gostaria de chamar sua atenção para uma série de artigos-traduções de uma literatura interessante na minha opinião - OSTEP. Este material discute profundamente o trabalho de sistemas operacionais do tipo unix, ou seja, trabalhar com processos, vários agendadores, memória e outros componentes semelhantes que compõem um sistema operacional moderno. Você pode ver o original de todos os materiais aqui aqui. Por favor, note que a tradução foi feita de forma pouco profissional (com bastante liberdade), mas espero ter mantido o significado geral.

O trabalho de laboratório sobre este assunto pode ser encontrado aqui:

Outras partes:

Você também pode conferir meu canal em telegrama =)

Alarme! Existe um laboratório para esta palestra! Olhar GitHub

API de processo

Vejamos um exemplo de criação de um processo em um sistema UNIX. Isso acontece através de duas chamadas de sistema garfo() и exec ().

Bifurcação de chamada()

Sistemas operacionais: três peças fáceis. Parte 3: API de Processo (tradução)

Considere um programa que faz uma chamada fork(). O resultado de sua execução será o seguinte.

Sistemas operacionais: três peças fáceis. Parte 3: API de Processo (tradução)

Primeiramente, entramos na função main() e imprimimos a string na tela. A linha contém o identificador do processo que no original é chamado PID ou identificador do processo. Este identificador é usado no UNIX para se referir a um processo. O próximo comando chamará fork(). Neste ponto, é criada uma cópia quase exata do processo. Para o sistema operacional, parece que há 2 cópias do mesmo programa em execução no sistema, que por sua vez sairá da função fork(). O processo filho recém-criado (em relação ao processo pai que o criou) não será mais executado, a partir da função main(). Deve ser lembrado que um processo filho não é uma cópia exata do processo pai; em particular, ele possui seu próprio espaço de endereço, seus próprios registradores, seu próprio ponteiro para instruções executáveis ​​e assim por diante. Assim, o valor retornado ao chamador da função fork() será diferente. Em particular, o processo pai receberá como retorno o valor PID do processo filho, e o filho receberá um valor igual a 0. Usando esses códigos de retorno, você pode então separar processos e forçar cada um deles a fazer seu próprio trabalho . No entanto, a execução deste programa não está estritamente definida. Após dividir em 2 processos, o SO passa a monitorá-los, bem como a planejar seu trabalho. Se executado em um processador single-core, um dos processos, neste caso o pai, continuará funcionando e então o processo filho receberá o controle. Ao reiniciar, a situação pode ser diferente.

Chamada em espera()

Sistemas operacionais: três peças fáceis. Parte 3: API de Processo (tradução)

Considere o seguinte programa. Neste programa, devido à presença de uma chamada esperar() O processo pai sempre aguardará a conclusão do processo filho. Neste caso, obteremos uma saída de texto estritamente definida na tela

Sistemas operacionais: três peças fáceis. Parte 3: API de Processo (tradução)

chamada exec()

Sistemas operacionais: três peças fáceis. Parte 3: API de Processo (tradução)

Considere o desafio exec (). Esta chamada de sistema é útil quando queremos executar um programa completamente diferente. Aqui vamos ligar execvp () para executar o programa wc, que é um programa de contagem de palavras. O que acontece quando exec() é chamado? Esta chamada recebe o nome do arquivo executável e alguns parâmetros como argumentos. Depois disso, o código e os dados estáticos deste arquivo executável são carregados e seu próprio segmento com o código é sobrescrito. As áreas de memória restantes, como pilha e heap, são reinicializadas. Depois disso, o sistema operacional simplesmente executa o programa, passando-lhe um conjunto de argumentos. Portanto, não criamos um novo processo, simplesmente transformamos o programa atualmente em execução em outro programa em execução. Depois de executar a chamada exec() no descendente, parece que o programa original não foi executado.

Esta complicação de inicialização é completamente normal para um shell Unix e permite que esse shell execute código após chamar garfo(), mas antes da chamada exec (). Um exemplo desse código seria ajustar o ambiente shell às necessidades do programa que está sendo lançado, antes de lançá-lo.

concha - apenas um programa de usuário. Ela mostra a linha do convite e espera que você escreva algo nela. Na maioria dos casos, se você escrever o nome de um programa lá, o shell encontrará sua localização, chamará o método fork() e, em seguida, chamará algum tipo de exec() para criar um novo processo e esperará que ele seja concluído usando um espera() chamada. Quando o processo filho for encerrado, o shell retornará da chamada wait() e imprimirá o prompt novamente e aguardará a inserção do próximo comando.

A divisão fork() & exec() permite que o shell faça o seguinte, por exemplo:
arquivo wc> novo_arquivo.

Neste exemplo, a saída do programa wc é redirecionada para um arquivo. A maneira como o shell consegue isso é bastante simples - criando um processo filho antes de chamar exec (), o shell fecha a saída padrão e abre o arquivo novo arquivo, portanto, toda a saída do programa em execução adicional wc será redirecionado para um arquivo em vez de uma tela.

tubo Unix são implementados de maneira semelhante, com a diferença de que usam uma chamada pipe(). Neste caso, o fluxo de saída do processo será conectado a uma fila de tubos localizada no kernel, à qual será conectado o fluxo de entrada de outro processo.

Fonte: habr.com

Adicionar um comentário