- C
- Sistemas Operacionais
- Processos
- POSIX
ProcessFlow
Orquestrador de processos em C que executa tarefas de forma sequencial, paralela ou encadeada por pipe.
Neste projeto implementei, em C, um orquestrador de processos, o ProcessFlow. Ele cadastra tarefas (um nome ligado a um programa com seus argumentos) e as executa criando processos filhos com fork, exec, waitpid, dup2 e pipe, sem delegar para nenhum shell. Os modos de execução são sequencial, paralelo e encadeado por pipe, com redirecionamento de entrada e saída para arquivos.
Como o programa funciona
O programa tem dois modos: interativo, com o prompt processflow>, e workflow, lendo um arquivo .pf passado como argumento (cada linha é impressa antes de executar). Os dois caem no mesmo laço: leio uma linha, tiro o \n, tokenizo com strtok por espaços e tabulações e despacho pelo primeiro token:
while (1) {
if (interativo) {
printf("processflow> ");
fflush(stdout);
}
if (!fgets(linha, sizeof(linha), input)) break;
linha[strcspn(linha, "\n")] = '\0';
if (strlen(linha) == 0) continue;
if (!interativo) {
printf("%s\n", linha);
fflush(stdout); // descarrega o buffer na hora
}
if (strcmp(linha, "exit") == 0) break;
char *tokens[MAX_TOKENS];
int n = 0;
char *palavra = strtok(linha, " \t");
while (palavra != NULL && n < MAX_TOKENS - 1) {
tokens[n++] = palavra;
palavra = strtok(NULL, " \t");
}
tokens[n] = NULL;
// despacho por tokens[0]: task, run, input, output, append, workdir, start, jobs, wait
}
As tarefas ficam em um array fixo tasks[MAX_TASKS] (limite de 104, guardado), cada uma com nome único: uma task com nome repetido é rejeitada e a definição original é mantida.
Um bug de buffering no eco do workflow
No modo workflow eu imprimo cada linha do arquivo antes de executá-la, para cumprir o requisito de cada linha lida ser impressa antes de ser processada. Por um tempo esse eco foi só um printf("%s\n", linha) sem fflush, e o programa passava nos meus testes locais. O problema é que o printf não escreve direto na saída: ele copia os dados para um buffer interno da stdio, e a escrita real (o syscall write) só acontece quando esse buffer é descarregado. Quando isso ocorre depende do modo de buffering do stdout:
stderré unbuffered, escreve na hora;stdoutem um terminal é line buffered, descarrega a cada\n;stdoutem um arquivo ou pipe é fully buffered, só descarrega quando o buffer enche, ou nofflush, ou no fim do programa.
A libc escolhe o modo pelo destino do descritor, não pelo conteúdo. Eu testei só no terminal (line buffered), então cada \n já descarregava e a ordem saía certa, o eco sempre vinha antes da execução. Foi o autocheck que expôs o problema: o checker io_static roda o programa com a saída capturada por um pipe, o que muda o stdout para fully buffered, e compara o esperado com o obtido. Nos três checkers (Execução simples + exit, Pipe e Redirecionamento) o veredito foi o mesmo, saída divergente: no esperado cada eco vinha antes da sua execução, mas no obtido as saídas dos filhos apareciam primeiro (eles escrevem direto no fd 1, sem passar pelo buffer da stdio) e os ecos do workflow só vinham no fim, todos juntos, no descarregamento final do buffer do pai.
Um detalhe que me confundiu no começo: o filho herda uma cópia do buffer da stdio no fork, mas o exec substitui a imagem do processo e descarta essa cópia sem descarregar. Ou seja, a inversão não vinha do filho, e sim do buffer do pai, que só era descarregado no encerramento do programa.
A correção é forçar o descarregamento logo depois do eco:
if (!interativo) {
printf("%s\n", linha);
fflush(stdout);
}
Com o fflush(stdout), o write acontece naquele instante, e o eco chega ao pipe antes de qualquer execução posterior, o destino sendo terminal ou não.
Cadastro de tarefas (task)
Cadastrar não executa nada, só registra. Guardo o nome, o programa e os argumentos com strdup, com a lista terminada em NULL para o execvp:
if (numTasks == MAX_TASKS) {
fprintf(stderr, "Erro: limite de tasks atingido\n");
continue;
}
Task *t = &tasks[numTasks];
t->nome = strdup(tokens[1]);
t->programa = strdup(tokens[2]);
t->args[0] = strdup(tokens[2]); // args[0] = nome do programa
int j = 1;
for (int i = 3; i < n; i++)
t->args[j++] = strdup(tokens[i]);
t->args[j] = NULL; // terminador p/ execvp
t->inputFile = NULL;
t->outputFile = NULL;
t->append = 0;
numTasks++;
Execução sequencial e paralela (run)
run sozinho vira run sequential de uma tarefa; com o modo explícito ele aceita várias. Para cada tarefa eu faço fork: o filho executa direto (rodarNoFilho) e o pai decide o que esperar. No sequencial o waitpid é imediato, um processo por vez; no paralelo eu guardo os pids e espero o grupo todo no fim:
pid_t pid = fork();
if (pid < 0) {
fprintf(stderr, "Erro: fork falhou.\n");
continue;
} else if (pid == 0) {
rodarNoFilho(t); // o filho não volta daqui
} else if (pid > 0) {
if (strcmp(modo, "sequential") == 0) {
waitpid(pid, &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) != 0)
fprintf(stderr, "Erro: tarefa '%s' terminou com código %d\n",
t->nome, WEXITSTATUS(status));
} else if (strcmp(modo, "parallel") == 0 || strcmp(modo, "pipe") == 0) {
pids[k++] = pid; // espero o grupo no fim
}
}
Execução encadeada por pipe (run pipe)
Para o pipe eu crio n - 1 pipes antes de fazer os fork (um entre cada par de estágios) e cada filho conecta a ponta certa com dup2: o estágio da posição pos lê do pipe anterior e escreve no próximo. Todos os descritores extras são fechados no filho e, depois, no pai, sem isso o último estágio nunca recebe EOF:
if (strcmp(modo, "pipe") == 0)
for (int i = 0; i < n - start - 1; i++)
pipe(pipes[i]);
...
else if (pid == 0) {
if (strcmp(modo, "pipe") == 0) {
int pos = i - start;
if (pos > 0) dup2(pipes[pos - 1][0], 0); // lê da saída do anterior
if (pos < n - start - 1) dup2(pipes[pos][1], 1); // escreve para o próximo
for (int p = 0; p < n - start - 1; p++) {
close(pipes[p][0]);
close(pipes[p][1]);
}
}
rodarNoFilho(t);
}
Assim, run pipe listar ordenar contar equivale a ls -l | sort | wc -l, com cada tarefa num processo próprio.
Redirecionamento e rodarNoFilho
input, output e append não executam, eles só gravam inputFile, outputFile e append na Task; o efeito aparece quando a tarefa roda. No filho eu abro o arquivo e aponto o descritor certo para a entrada ou a saída padrão antes do execvp:
void rodarNoFilho(Task *t) {
if (t->inputFile != NULL) {
int fd = open(t->inputFile, O_RDONLY);
if (fd != -1) {
dup2(fd, 0);
close(fd);
} else {
perror("Erro: Arquivo não existe.");
_exit(1);
}
}
if (t->outputFile != NULL && !t->append) {
int fd = open(t->outputFile, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd != -1) {
dup2(fd, 1);
close(fd);
} else {
perror("Erro: Falha ao escrever no arquivo");
_exit(1);
}
}
if (t->outputFile != NULL && t->append) {
int fd = open(t->outputFile, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd != -1) {
dup2(fd, 1);
close(fd);
} else {
perror("Erro: Falha ao escrever no arquivo");
_exit(1);
}
}
execvp(t->programa, t->args);
perror("Erro: exec falhou.");
_exit(1);
}
O output zera o append e o append o seta, então os dois são mutuamente exclusivos (um sobrescreve, o outro acrescenta).
Jobs em background (start, jobs, wait)
Além dos run, existe start para rodar em background: o pai registra o pid num vetor de Job, imprime um id e devolve o prompt. jobs consulta cada um com waitpid(WNOHANG) e wait <id> bloqueia até aquele job terminar. Detalhe: o README ainda marca workdir, start, jobs e wait como “em desenvolvimento”, mas todos já estão implementados no código:
else if (pid > 0) {
jobs[numJobs].id = numJobs + 1;
jobs[numJobs].pid = pid;
jobs[numJobs].nome = t->nome;
jobs[numJobs].terminou = 0;
printf("[%d] %d\n", jobs[numJobs].id, jobs[numJobs].pid);
numJobs++;
}
Particularidades e pontos que revisaria
-
Vetor de jobs sem limite. As
taskstêm a guarda de 104, mas ostartescreve emjobs[numJobs]sem checar se o vetor encheu; mais de 104 jobs em background estouram o array. Faltou o mesmoif (numJobs == MAX_TASKS)que existe no cadastro de tarefas:// start: escreve sem checar o limite de jobs jobs[numJobs].id = numJobs + 1; jobs[numJobs].pid = pid; ... numJobs++; -
Memória nunca liberada. Tudo que vem do
strdup(nome, programa, argumentos, arquivos) fica vivo até o processo sair, e redefinirinput/output/appendvaza a string antiga. Numa sessão curta isso é benigno, mas em um workflow longo vai acumulando. -
Morte por sinal não é reportada. No
waitpideu só olhoWIFEXITED/WEXITSTATUS; se um filho for morto por um sinal, o erro passa despercebido. Faltaria oWIFSIGNALED. -
Excesso de tokens é truncado em silêncio. A tokenização para em
MAX_TOKENS - 1e ignora o resto da linha; uma linha com muitos argumentos perde o final sem nenhum aviso. O certo seria rejeitar com uma mensagem ou alocar dinamicamente. -
Um
maingrande. O despacho de comandos é uma cadeia deifno mesmo laço domain; funciona, mas o arquivo fica difícil de navegar. Vale separar cada comando em uma função.
Pontos de melhoria
- Aplicar a guarda de limite no
start(aviso quandonumJobschegar ao teto). - Reportar morte por sinal (
WIFSIGNALED) além do código de saída. - Liberar as strings do
strdupao redefinir arquivos e ao encerrar, ou usar um pool de memória. - Rejeitar linha com excesso de tokens em vez de truncar em silêncio.
- Quebrar o
mainem funções de despacho por comando.
Gabriel Rocha