2009-05-25

Execução de programas

A lista de system calls deste guião é deveras assustadora. Não contando com a system, são nada mais, nada menos que sete variantes da mesma função: exec.

A system é bastante simples. Recebe uma string com um comando para correr, executa-o e returna o seu código de saída. Experimentem executar isto:

#include <stdlib.h>

int main(void)
{
int exit = system("ls –l");
return exit;
}

E agora a família exec. Conforme é dito no manual, as seis primeiras variantes são apenas frontends para a system call execve (por esta razão a página das primeiras seis está na secção 3 e a página da execve na secção 2). Vamos então analisar primeiro a execve.

$ man 2 execve

Esta função “substitui” o nosso processo pelo programa que é passado como argumento. Em caso de sucesso, a função não retorna, porque quando o novo programa terminar, o processo termina.

O primeiro parâmetro é o nome do programa. O segundo é a lista de argumentos para esse programa. Esta lista é a mesma lista que é recebida na função main no parâmetro argv:

int main(int argc, char** argv);

Isto significa que o primeiro elemento dessa lista deve ser o nome do programa a executar e que essa lista deve ser terminada com NULL.

O terceiro parâmetro são as variáveis de ambiente que são passadas no terceiro argumento da função main: envp.

int main(int argc, char** argv, char** envp);

Esta lista de variáveis de ambiente também pode ser acedida a partir da variável global environ. Como esta parte das variáveis de ambiente é pouco relevante para o problema em mãos, vamos ignorá-la e usar uma variante da execve que não recebe esse parâmetro: a execvp.

Se quiserem conhecer as diferenças entre cada uma das outras variantes, visitem a página do manual:

$ man 3 exec

Usando apenas a execvp já se safam. A diferença entre a execvp e a execv é que a primeira procura pelo programa nas directorias da variável de ambiente PATH. A execv só funciona com caminhos completos.

Como vamos então implementar a nossa shell, com a função execvp? A shell deve apresentar uma prompt, ler um comando, executar o programa que foi pedido (com os respectivos argumentos), e quando este terminar deve voltar à prompt. Mas a exec nunca retorna! Não podemos usar exec no processo da shell, porque senão esta terminaria logo. Mas podemos usá-la noutro processo se recorrermos a fork(). Vamos escrever uma função que trata de toda esta operação:

int run(char** args)
{
pid_t pid = fork();
if(pid == 0)
{
// Substituir o filho pelo programa que queremos executar
execvp(args[0], args);
// Se chegarmos aqui é porque o programa não foi executado
perror(args[0]);
exit(1);
}
else
{
int status;
// Esperar que o programa termine
waitpid(pid, &status, 0);
if(WIFEXITED(status))
{
return WEXITSTATUS(status);
}
else
{
return -1;
}
}
}

Este código já deve ser familiar. Reparem que o filho, que inicialmente é uma cópia da nossa shell, é substituído pelo comando que queremos executar usand a execvp.

Agora só falta a parte de partir a string em partes para usar como uma lista de argumentos. Não sei o que sugeriu o prof, talvez a strtok()? Pois, eu sugiro que usem wordexp. Tempo para mais uma consulta ao manual:

$ man wordexp

Com esta biblioteca não temos trabalho nenhum para separar os argumentos numa lista de strings e temos ainda os seguintes bonús:

  • substituição de ~utilizador pela pasta home deste utilizador;
  • substituição de $VAR pelo valor da variável de ambient VAR;
  • substituição de $(comando) ou `comando` pelo output de comando;
  • substituição de wildcards pelos nomes dos ficheiros correspondentes;

… entre outros. A bash faz todas estas substituições, e usando a biblioteca wordexp a nossa shell também fará isso.

Esta fase de processamento da nossa shell fica então assim:

#include <wordexp.h>

char** getArgs(const char* s)
{
static wordexp_t words;
static int initialized = 0;

if(s && !initialized)
{
wordexp(s, &words, 0);
initialized = 1;
}
else if(s)
{
// Reutilizar a memória que já tinha sido alocada previamente
wordexp(s, &words, WRDE_REUSE);
}
else
{
// Libertar a memória quando s == NULL
wordfree(&words);
initialized == 0;
return NULL;
}

return words.we_wordv;
}

Agora só falta escrevermos uma função para apresentar uma prompt e ler uma linha como um comando:

char* getCommand(const char* prompt);

E depois escrevemos a função main desta forma:

#include <string.h>

int main(char* argc, char** argv)
{
char* prompt = " > ";

char* command = getCommand(prompt);
while(strcmp(command, "exit") != 0)
{
char** args = getArgs(command);
run(args);
// Evitar uma memory leak
free(command);
command = getCommand(prompt);
}
return 0;
}

Se quiserem podem escrever a função getCommand com recurso à scanf. Eu vou usar duas bibliotecas (a GNU readline e a GNU history) para ler essa linha sem ter de me preocupar como o tamanho máximo da string, disponibilizando funcionalidades de edição que não estão disponíveis na scanf e com histórico dos últimos comandos.

#include <stdio.h>
#include <readline/readline.h>
#include <readline/history.h>

char* getCommand(const char* prompt)
{
static int initialized = 0;

if(!initialized)
{
// Initializar o histórico
using_history();
initialized = 1;
}

// Ler uma linha
char* line = readline(prompt);

// Adicionar essa linha ao histórico
add_history(line);

return line;
}

Para compilar isto é preciso linkar com a biblioteca readline. Para isso usamos este Makefile:

OBJECT_FILES=shell.o read.o parse.o run.o
CFLAGS=-g
# Linkar com a readline
LIBS=-lreadline

shell : $(OBJECT_FILES)
$(CC) $(CFLAGS) $(LIBS) $^ -o $@

clean :
$(RM) *.o *~ shell

# Dependências
read.c : read.h
parse.c : parse.h
run.c : run.h

Como sempre, podem fazer download do código aqui.

4 commentários:

Anónimo disse...

Porquê que a função run() devolve WEXITSTATUS(status) ou -1 se depois não faz nada com o respectivo valor?

E na aula o prof sugeriu o uso do strsep() para separar os argumentos e não o strtok(). Suponho que pela descrição, o wordexp seja melhor mas muito mais confuso de usar e precisa de mais mariquices (como a série de ifs) e como o interesse aqui é sinais/processos, penso que o trabalho adicional não valha a pena :P

Martinho Fernandes disse...

Eu gosto de escrever as peças "em isolação". Acabei por não usar o valor de retorno da run porque ela já apresenta os erros. Podia ter tirado essa parte...

A strsep é o substituto da strtok. Não gosto de nenhuma destas duas funções porque alteram a própria string. Mas é apenas uma questão pessoal.

Como tu mesmo disseste, o que interessa são os processos, por isso essa parte fica às preferências de cada um.

Anónimo disse...

Em que situações é que o strsep() altera a própria string? Não reparei em tais casos...

Mas se for para se fazer uma shell em condições, concordo que a outra biblioteca é mais recomendada.

Drean disse...

Será que podes fazer um exemplo de utilização do strsep()?

Enviar um comentário