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