Sistemas Operacionais: Três Peças Fáceis. Parte 3: API de Processos (tradução)

Introdução aos sistemas operacionais

Olá, Habr! Quero apresentar a vocês uma série de artigos traduzidos de uma literatura que considero interessante — OSTEP. Este material explora a fundo o funcionamento dos sistemas operacionais semelhantes ao Unix, particularmente — o trabalho com processos, vários escalonadores, memória e outros componentes que compõem um sistema operacional moderno. Você pode olhar o original de todo o material aqui aqui. Peço que considerem que a tradução foi feita de forma não profissional (de maneira bastante livre), mas espero ter mantido o sentido geral.

Os trabalhos de laboratório sobre este assunto podem ser encontrados aqui:

Partes adicionais:

E você também pode acessar meu canal em telegram =)

Alerta! há um laboratório para esta aula! veja github

API do Processo

Vamos analisar um exemplo de criação de um processo no sistema UNIX. Isso ocorre por meio de duas chamadas de sistema fork() e exec().

Chamando fork()

Sistemas Operacionais: Três Peças Fáceis. Parte 3: API de Processos (tradução)

Vamos considerar um programa que executa a chamada fork(). O resultado da sua execução será o seguinte.

Sistemas Operacionais: Três Peças Fáceis. Parte 3: API de Processos (tradução)

Em primeiro lugar, entramos na função main() e realizamos a exibição de uma string na tela. A string contém o identificador do processo, que originalmente é chamado PID ou identificador do processo. Este identificador é utilizado no UNIX para referenciar o processo. A próxima instrução será a chamada de fork(). Nesse momento, uma cópia quase exata do processo é criada. Para o sistema operacional, parece que duas cópias do mesmo programa estão em execução, que, por sua vez, sairão da execução da função fork(). O processo-filho criado (em relação ao processo-pai que o criou) não será mais executado a partir da função main(). É importante lembrar que o processo-filho não é uma cópia exata do processo-pai, pois ele tem 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 para o chamador da função fork() será diferente. O processo-pai receberá como retorno o valor PID do processo filho, enquanto o filho receberá um valor igual a 0. Esses códigos de retorno poderão ser usados posteriormente para diferenciar os processos e fazer com que cada um execute seu trabalho. Vale lembrar que a execução deste programa não é estritamente definida. Após a separação em 2 processos, o sistema operacional começa a monitorá-los e a planejar seu funcionamento. No caso de execução em um processador de um único núcleo, um dos processos continuará a execução, neste caso — o pai, e então o controle será passado para o processo-filho. Em uma nova execução, a situação pode ser diferente.

Chamando wait()

Sistemas Operacionais: Três Peças Fáceis. Parte 3: API de Processos (tradução)

Vamos considerar o seguinte programa. Neste programa, devido à presença da chamada wait() o processo-pai sempre esperará a conclusão do trabalho do processo-filho. Nesse caso, teremos uma saída de texto estritamente definida na tela.

Sistemas Operacionais: Três Peças Fáceis. Parte 3: API de Processos (tradução)

Chamando exec()

Sistemas Operacionais: Três Peças Fáceis. Parte 3: API de Processos (tradução)

Vamos analisar a chamada exec(). Esta chamada de sistema é útil quando queremos iniciar um programa completamente diferente. Aqui chamaremos execvp() para iniciar o programa wc, que é um programa de contagem de palavras. O que acontece durante a chamada de exec()? Este chamado recebe como argumentos o nome do arquivo executável e alguns parâmetros. Em seguida, o código e os dados estáticos desse arquivo executável são carregados e seu segmento de código é sobrescrito. As outras áreas de memória, como a pilha e o heap, são reinicializadas. Depois, o SO simplesmente executa o programa, passando a ele um conjunto de argumentos. Assim, não criamos um novo processo; simplesmente transformamos o programa atualmente em execução em outro programa em execução. Após a execução da chamada de exec(), o filho tem a impressão de que o programa original, na verdade, nunca foi iniciado.

Essa complexidade no início é absolutamente normal para o shell do Unix, e permite que esse shell execute código após a chamada fork(), mas antes da chamada exec(). Um exemplo desse código pode ser a adaptação do ambiente do shell para atender às necessidades do programa a ser iniciado, antes de seu lançamento imediato.

Shell — é apenas um programa de usuário. Ele exibe um prompt e aguarda até que você digite algo nele. Na maioria dos casos, se você digitar o nome de um programa, o shell encontrará sua localização, chamará o método fork(), e depois, para criar um novo processo, chamará algum dos tipos de exec() e aguardará sua execução com a chamada wait(). Quando o processo filho terminar, o shell retornará da chamada wait() e exibirá novamente o prompt, aguardando a entrada do próximo comando.

A separação entre fork() & exec() permite que o shell faça as seguintes coisas, por exemplo:
wc file > new_file.

Neste exemplo, a saída do programa wc é redirecionada para um arquivo. O método pelo qual o shell atinge isso é bastante simples — ao criar o processo filho antes da chamada exec(), o shell fecha o fluxo padrão de saída e abre o arquivo new_file, assim, toda a saída do programa que será iniciado a seguir wc será redirecionada para o arquivo em vez da tela.

O pipe do Unix é implementado de maneira semelhante, com a diferença de que eles usam a chamada pipe(). Nesse caso, o fluxo de saída do processo será conectado a uma fila pipe, situada no núcleo, que também será unida ao fluxo de entrada de outro processo.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster