TP 1
TP 1
Este trabalho tem por objetivo fazer com que os alunos experimentem na prática o ambiente
de programação Unix/Linux para tratamento de processos e para a comunicação entre processos
por pipes (canais de envio de bytes).
1 O problema
Neste trabalho você deve implementar um interpretador de comandos (chamado no Unix/Linux
de shell). Para isso, você deverá aprender como disparar processos e como encadear sua comu-
nicação usando pipes. Além disso, você deverá utilizar pipes e manipulação de processos para
criar um par produtor/consumidor por envio de mensagens que trabalhará dentro da shell para
"alimentar"programas que você disparar com dados, ou para receber os dados de um programa.
2 Implementação
Seu programa deverá ser implementado em C (não serão aceitos recursos de C++, como classes
da biblioteca padrão ou de outras fontes).
O programa, que deverá se chamar shellso deverá ser iniciado sem argumentos de linha de
comando, ou com apenas um. Caso seja disparado sem argumentos, ele deverá escrever um
prompt no início de cada linha na tela (um símbolo como "$"ou uma mensagem como "Sim,
mestre?") e depois ler comandos da sua entrada padrão (normalmente, o teclado). Mensagens
de erro e o resultado dos programas, salvo quando redirecionados pelos comandos fornecidos,
devem ser exibidos na saída padrão (usualmente, a janela do terminal). Essa forma de operação é
denominada interativa. Já se um parâmetro for fornecido, ele deve ser interpretado como o nome
de um arquivo de onde comandos serão lidos. Nesse caso, apenas o resultado dos comandos deve
ser exibido na saída padrão, sem a exibição de prompts nem o nome dos comandos executados.
Em ambos os modos de operação, sua shell termina ao encontrar o comando "fim"no início de
uma linha ou ao encontrar o final do fluxo de bytes de entrada (ao fim do arquivo de entrada, ou
se o usuário digita Ctrl-D no teclado).
Cada linha deve conter um comando ou mais comandos para ser(em) executado(s). No caso de
haver mais de um programa a ser executado na linha, eles devem obrigatoriamente ser encadeados
por pipes (|), indicando que a saída do programa à esquerda deve ser associada à entrada
do programa à direita. Um novo prompt só deve ser exibido (se necessário) e um novo
comando só deve ser lido quando o comando da linha anterior terminar sua execução, exceto
caso a linha de comando termine com um "&";, quando o programa deve ser executado em
background. Em qualquer caso, o interpretador deve sempre receber o valor de saída dos
programas executados –isto é, ele não deve deixar zumbis no sistema (confira as chamadas de
sistema wait() e waitpid()para esse fim). Para o caso dos programas executados em
background, você deve se informar sobre o tratamento de sinais, em particular o sinal
SIGCHLD, para tratar o término daqueles programas.
Cada programa a ser executado deve começar com o nome do arquivo de programa a ser
executado e pode ter um número de argumentos de linha de comando, que serão passados para o
novo processo através da interface argc/argv do programa em C. Como mencionado anteriormente,
diversos processos podem ser encadeados usando pipes. O interpretador deve também aceitar
1
linhas vazias, para as quais nada deve ser executado e simplesmente deve-se iniciar uma nova
linha de comando.
O seu interpretador não aceitará os comandos de redirecionamento de entrada e saída normal-
mente utilizados, “<” e “>”. Ao invés disso, ele aceitará os símbolos “=>” e “<=”, que indicarão
entrada e saída por pipes para processos produtores/consumidores. O sinal “=>” indica que o
interpretador de comandos deverá conectar um pipe à saída padrão do processo que vai ser execu-
tado e disparar um processo para ler desse pipe para escrever o que for enviado pelo processo para
o arquivo indicado (como um processo consumidor do pipe). Se o arquivo não existir, ele deve ser
criado; se já existir, ele deve ser sobre-escrito com o novo conteúdo). Um nome de arquivo depois
de um comando e separado dele por um “<=” indica que o interpretador de comandos deve conec-
tar um pipe à entrada padrão do processo e disparar um outro processo para ler o arquivo e enviar
pelo pipe o conteúdo do arquivo indicado. Nesse caso, o arquivo deve existir (ou o interpretador
deve indicar um sinal de erro). Os dois redirecionamentos podem ser usados ao mesmo tempo
em uma mesma linha de comando. Caso a linha inclua mais de um comando, obrigatoriamente o
“<=” deve ser associado ao primeiro programa na linha e o “=>” deve ser associado ao último.
Com base nessa descrição, são comandos válidos (supondo que o prompt seja “Qual o seu
comando?”:
A terceira linha ilustra o caso da linha de comando vazia. Note que o comando fim não é um
programa que será executado, mas um comando embutido (built-in) do interpretador. Cada parte
do comando deve ser separada das demais por pelo menos um espaço em branco, ou caracteres de
tabulação, inclusive os sinais de uso de arquivos.
3 Programação defensiva
Uma vez que seu programa esteja funcionando, você deve se certificar de que o código seja ro-
busto contra erros do usuário (afinal, um interpretador de comandos não deve parar de executar
inesperadamente). Condições que você deve testar e tratar adequadamente (com uma mensagem
de erro, quando for o caso) incluem: o interpretador de comandos ser iniciado com um número
errado de argumentos ou com um nome de arquivo que não existe (o programa deve exibir uma
mensagem de erro e terminar); um comando ou um arquivo de entrada que não existem, ou um
arquivo de saída que não pode ser escrito por algum motivo (o programa deve exibir uma mensa-
gem de erro e voltar para o prompt, para o próximo comando). Você pode considerar que linhas
de comando nunca serão maiores que 512 caracteres, incluindo o caractere de fim de linha ("\n"),
e que nenhum componente da linha (nomes de programas, argumentos ou nomes de arquivos) não
ultrapassará 64 caracteres.
4 Informações úteis
4.1 Forma de operação
O seu interpretador deve ser basicamente um loop que exibe o prompt (no modo interativo), lê e
interpreta a entrada, executa o comando, espera pelo seu término e reinicia a sequência, até que
o fluxo de entrada termine ou o usuário digite fim.
2
4.2 Execução de comandos
Você deve estruturar o seu interpretador de forma que ele crie pelo menos um novo processo para
cada novo comando. Existem duas vantagens nessa forma de operação. Primeiro, ele protege o
interpretador de todos os erros que pode ocorrer no novo comando. Além disso, permite expressar
concorrência de forma fácil, isto é, vários comandos pode ser disparados para executar simulta-
neamente (concorrentemente). Isso é importante para se criar os módulos produtor e consumidor
que serão necessários para manipular arquivos de entrada e saída e essencial para implementar o
comando pipe.
3
Você vai notar que há uma variedade de comandos na família exec. Para este trabalho, para
facilitar, recomendamos você use execvp(). Lembre-se que se essa chamada for bem sucedida, ele
não vai voltar, pois aquele programa deixa de executar e o processo passa a executar o código do
programa indicado na chamada. Dessa forma, se a chamada voltar, houve um erro (por exemplo,
o comando não existe). A parte mais desafiadora está em passar os argumentos corretamente
especificados, como discutido anteriormente sobre argc e argv. As chamadas de sistema wait()
e waitpid() permitem que o processo pai espere por seus filhos. Leia as páginas de manual para
obter mais detalhes.
4
sistema operacional Linux que foi distribuída no início do curso. A avaliação do seu funcionamento
será feita naquele ambiente.