2009-06-17

Sinais, parte II

Background/Foreground

Até aqui a nossa shell corre sempre os processos em foreground. Isto significa que a shell pára até cada processo terminar. Assim não podemos correr mais que um processo de cada vez.

Para isso temos de manter alguns processos em background. Isto significa apenas que a shell não espera por eles. Temos no entanto de manter uma lista destes processos. Vou recorrer a uma lista ligada simples, sem ordem, com inserção na cauda:

typedef struct job
{
int id; // Identificador
char* command; // O comando que está a ser executado
pid_t pid; // O PID do processo
struct job* next; // Job seguinte
} Job;

typedef struct
{
Job* first;
Job* last;
} JobList;

Job* get_job(int id);
Job* find_job(pid_t pid);
int add_job(const char* command, pid_t pid);
void remove_job(pid_t pid);
void print_jobs(void);

E temos esta variável global:

JobList jobs;

Por esta altura já devem ser capazes de implementar esta lista. Se não for o caso… está na hora de resolver novamente o primeiro guião. A prática faz a perfeição.

Com isto vou adicionar as seguintes capacidades à nossa shell:

  • Iniciar um processo em background se o último caractere na linha for &;
  • Parar o processo actual e retomar a shell se for premido CTRL-Z;
  • Listar os processos em background com o comando jobs;
  • Continuar um processo em background com o comando bg;
  • Retomar um processo em foreground com o comando fg;
  • Reportar quando um processo em background terminou;

Iniciar em background

Temos de alterar a função getCommand para permitir o uso do caractere & como marca para iniciar em background:

char* getCommand(const char* prompt, int* background);

E assim passamos como parâmetro o endereço de um inteiro que dita se o comando deve ou não ser corrido em background. A função fica então assim:

char* getCommand(const char* prompt, int* background)
{
// Tudo igual até add_history()

int len = strlen(line);
*background = line[len - 1] == '&'
if(*background)
{
// Retirar o caractere &
line[len - 1] = '\0';
}

return line;
}

Temos também de alterar a função run para ao invés de esperar por um processo, adicioná-lo à lista de processos em background, quando for o caso. Para adicionar a esta lista também vamos receber o comando original, que é para apresentarmos no ecrã de modo a distinguir entre diferentes execuções do mesmo comando.

int run(char** args, int background, const char* command)
{
pid_t pid = fork();
if(pid == 0)
{
// Código do filho
}
else
{
int status;
if(background)
{
add_job(command, pid);
}
else
{
// Código normal de foreground
}
}
}

CTRL-Z

Quando é premido CTRL-Z, é enviado o sinal SIGTSTP para o processo actual. A resposta por defeito a este sinal é parar o processo. A nossa shell não vai parar, mas sim capturar este sinal e reenviá-lo ao processo actual. Isto fará com que o processo pare. Depois só temos de adicionar esse processo à lista de jobs.

Para isso vamos registar um signal handler para o SIGTSTP muito semelhante ao propagate que usámos para propagar o SIGINT:

void stop(int signum)
{
if(actual_pid != -1)
{
kill(actual_pid, signum);
add_job(actual_command, actual,_pid);
}
}

Retomar

Para continuar um processo que foi parado temos duas opções: retomá-lo em background, ou em foreground.

Para continuar em background é simples: basta enviar o sinal SIGCONT ao processo. Na minha shell basta-me adicionar este código à função runBuiltin, que é uma função que recebe o comando a executar e verifica se é um comando interno da shell como por exemplo exit, fg, bg, jobs ou mesmo cd. Se for um destes executa-o e devolve 1 e assim volta-se à prompt. Senão devolve 0 e o código continua, executando o comando de forma normal.

if(strcmp(args[0]), "bg") == 0)
{
// Obter o job pelo seu id
Job* j = get_job(atoi(args[1]));
kill(j->pid, SIGCONT);
return 1;
}

Para retomar em foreground, é necessário mais qualquer coisinha… Temos de esperar pelo processo antes de voltar à prompt. E também temos de o retirar da lista de jobs:

if(strcmp(args[0]), "bg") == 0)
{
int status;
// Obter o job pelo seu id
Job* j = get_job(atoi(args[1]));
kill(j->pid, SIGCONT);
remove_job(j->pid);
waitpid(j->pid, &status, 0);
return 1;
}

Reportar término

Para reportar quando um processo em background terminou temos de registar um handler para o sinal SIGCHLD, que é um sinal que é enviado sempre que um processo-filho muda de estado (pára, termina, etc).

signal(SIGCHLD, children);

E agora vamos escrever o handler. O que é que este handler tem de fazer?

  • Verificar se um filho terminou ou parou;
  • Verificar se existe um job com esse PID (não queremos relatar isto sobre processos em foreground);
  • Remover o job da lista, se já terminou;
  • Reportar com uma mensagem;

Um signal handler só recebe o número do sinal. Como obtemos as informações sobre o término ou paragem, ou mesmo o PID? Com a já conhecida syscall waitpid.

void children(int signum)
{
int status;
pid_t pid = waitpid(-1, &status, WUNTRACED);
int exited = WIFEXITED(status);
int stopped = WIFSTOPPED(status);

Job* j = find_job(pid);
if(j == NULL)
return;

if(exited)
{
remove_job(pid);
printf("Job [%d] %s terminated\n", j->id, j->command);
}
else if(stopped)
{
printf("Job [%d] %s stopped\n", j->id, j->command);
}
}

Aqui, a opção WUNTRACED usada na chamada a waitpid serve para esperar que o filho termine ou pare. Por defeito waitpid só espera que o filho pare. Eu sei que o nome da opção não ajuda nada, mas é o que se arranja…

Agora já temos estas funcionalidades todas implementadas. O código completo está aqui. Aviso já que existem algumas discrepâncias entre o código que está neste post e o que está no zip devido a terem sido escritos com bastante tempo entre eles.

0 commentários:

Enviar um comentário