Programação C

Seu primeiro programa C usando a chamada do sistema Fork

Seu primeiro programa C usando a chamada do sistema Fork
Por padrão, os programas C não têm simultaneidade ou paralelismo, apenas uma tarefa acontece por vez, cada linha de código é lida sequencialmente. Mas às vezes, você tem que ler um arquivo ou - pior ainda - um soquete conectado a um computador remoto e isso leva muito tempo para um computador. Geralmente leva menos de um segundo, mas lembre-se de que um único núcleo de CPU pode execute 1 ou 2 bilhões de instruções durante esse tempo.

Então, como um bom desenvolvedor, você ficará tentado a instruir seu programa C a fazer algo mais útil enquanto espera. É aí que a programação simultânea está aqui para o seu resgate - e torna o seu computador infeliz porque tem que funcionar mais.

Aqui, vou mostrar a você a chamada de sistema do Linux fork, uma das maneiras mais seguras de fazer programação simultânea.

A programação simultânea pode ser insegura?

Sim pode. Por exemplo, também há outra maneira de chamar multithreading. Tem a vantagem de ser mais leve, mas pode realmente dê errado se você usar incorretamente. Se o seu programa, por engano, lê uma variável e escreve no mesma variável ao mesmo tempo, seu programa se tornará incoerente e quase indetectável - um dos piores pesadelos do desenvolvedor.

Como você verá abaixo, o fork copia a memória, então não é possível ter tais problemas com variáveis. Além disso, fork faz um processo independente para cada tarefa simultânea. Devido a essas medidas de segurança, é aproximadamente 5 vezes mais lento para iniciar uma nova tarefa simultânea usando fork do que com multithreading. Como você pode ver, isso não é muito pelos benefícios que traz.

Agora, chega de explicações, é hora de testar seu primeiro programa C usando fork call.

O exemplo de fork do Linux

Aqui está o código:

#incluir
#incluir
#incluir
#incluir
#incluir
int main ()
pid_t forkStatus;
forkStatus = fork ();
/* Filho… */
if (forkStatus == 0)
printf ("Criança em execução, processando.\ n ");
dormir (5);
printf ("Criança está pronta, saindo.\ n ");
/ * Pai… * /
else if (forkStatus != -1)
printf ("Pai está esperando ... \ n");
esperar (NULL);
printf ("Pai está saindo ... \ n");
senão
perror ("Erro ao chamar a função fork");

return 0;

Eu convido você a testar, compilar e executar o código acima, mas se você quiser ver como a saída ficaria e você for muito “preguiçoso” para compilá-lo - afinal, você talvez seja um desenvolvedor cansado que compilou programas em C o dia todo - você pode encontrar a saída do programa C abaixo, juntamente com o comando que usei para compilá-lo:

$ gcc -std = c89 -Wpedantic -Wall forkSleep.c -o forkSleep -O2
$ ./ forkSleep
O pai está esperando ..
Criança está correndo, processando.
Criança acabou, saindo.
O pai está saindo ..

Por favor, não tenha medo se a saída não for 100% idêntica à minha saída acima. Lembre-se de que executar as coisas ao mesmo tempo significa que as tarefas estão fora de ordem, não há uma ordem predefinida. Neste exemplo, você pode ver que a criança está correndo antes pai está esperando, e não há nada de errado com isso. Em geral, a ordem depende da versão do kernel, do número de núcleos da CPU, dos programas que estão sendo executados no seu computador, etc.

OK, agora volte para o código. Antes da linha com fork (), este programa C é perfeitamente normal: 1 linha está sendo executada por vez, há apenas um processo para este programa (se houver um pequeno atraso antes do fork, você pode confirmar isso em seu gerenciador de tarefas).

Após o fork (), agora existem 2 processos que podem ser executados em paralelo. Primeiro, há um processo filho. Este processo é aquele que foi criado no fork (). Este processo filho é especial: ele não executou nenhuma das linhas de código acima da linha com fork (). Em vez de procurar pela função principal, ele irá executar a linha fork ().

E as variáveis ​​declaradas antes da bifurcação?

Bem, Linux fork () é interessante porque responde a esta questão de forma inteligente. Variáveis ​​e, de fato, toda a memória em programas C é copiada para o processo filho.

Deixe-me definir o que está fazendo bifurcação em algumas palavras: isso cria um clone do processo chamando-o. Os 2 processos são quase idênticos: todas as variáveis ​​conterão os mesmos valores e ambos os processos executarão a linha logo após fork (). No entanto, após o processo de clonagem, eles estão separados. Se você atualizar uma variável em um processo, o outro processo não vai ter sua variável atualizada. É realmente um clone, uma cópia, os processos não compartilham quase nada. É muito útil: você pode preparar muitos dados e então fazer um fork () e usar esses dados em todos os clones.

A separação começa quando fork () retorna um valor. O processo original (é chamado o processo pai) obterá o ID do processo clonado. Por outro lado, o processo clonado (este é chamado o processo filho) obterá o número 0. Agora, você deve começar a entender por que coloquei instruções if / else if após a linha fork (). Usando o valor de retorno, você pode instruir a criança a fazer algo diferente do que o pai está fazendo - e acredite em mim, é útil.

De um lado, no código de exemplo acima, a criança está fazendo uma tarefa que leva 5 segundos e imprime uma mensagem. Para imitar um processo que leva muito tempo, uso a função dormir. Então, a criança sai com sucesso.

Por outro lado, o pai imprime uma mensagem, espere até que o filho saia e finalmente imprima outra mensagem. O fato de o pai esperar por seu filho é importante. Como é um exemplo, o pai está pendente na maior parte do tempo para esperar por seu filho. Mas, eu poderia ter instruído o pai a fazer qualquer tipo de tarefa de longa duração antes de dizer a ele para esperar. Dessa forma, ele teria feito tarefas úteis em vez de esperar - afinal, é por isso que usamos garfo (), não?

No entanto, como eu disse acima, é muito importante que pai espera por seus filhos. E é importante por causa de processos zumbis.

Como esperar é importante

Os pais geralmente querem saber se os filhos terminaram o processamento. Por exemplo, você deseja executar tarefas em paralelo, mas você certamente não quer o pai deve sair antes que os filhos terminem, porque se isso acontecesse, o shell retornaria um prompt enquanto os filhos ainda não terminaram - o que é estranho.

A função de espera permite esperar até que um dos processos filhos seja encerrado. Se um pai ligar 10 vezes para fork (), ele também precisará ligar 10 vezes para esperar (), uma vez para cada criança criada.

Mas o que acontece se o pai chama a função de espera enquanto todos os filhos têm saiu? É aí que os processos zumbis são necessários.

Quando um filho sai antes que o pai chame wait (), o kernel do Linux deixará o filho sair mas vai manter um tíquete dizendo que a criança saiu. Então, quando o pai chamar wait (), ele encontrará o tíquete, exclua esse tíquete e a função wait () retornará imediatamente porque sabe que o pai precisa saber quando o filho terminou. Este tíquete é chamado de processo zumbi.

É por isso que é importante que o pai chame wait (): se não fizer isso, os processos zumbis permanecerão na memória e no kernel do Linux não pode manter muitos processos zumbis na memória. Uma vez que o limite é atingido, seu computador ié incapaz de criar qualquer novo processo e então você estará em um muito mau estado: até para eliminar um processo, pode ser necessário criar um novo processo para esse. Por exemplo, se você deseja abrir seu gerenciador de tarefas para encerrar um processo, você não pode, porque seu gerenciador de tarefas precisará de um novo processo. Pior ainda, você não pode matar um processo de zumbi.

É por isso que chamar wait é importante: permite que o kernel Limpar o processo filho em vez de continuar acumulando uma lista de processos encerrados. E se o pai sair sem nunca ligar esperar()?

Felizmente, como o pai é encerrado, ninguém mais pode chamar wait () para esses filhos, então há sem razão para manter esses processos zumbis. Portanto, quando um pai sai, todos restantes processos zumbis ligado a este pai estão removidos. Processos zumbis são realmente útil apenas para permitir que os processos pai descubram que um filho terminou antes do pai chamado wait ().

Agora, você pode preferir conhecer algumas medidas de segurança para permitir o melhor uso do garfo sem nenhum problema.

Regras simples para fazer o fork funcionar conforme o pretendido

Primeiro, se você conhece multithreading, por favor não bifurque um programa usando threads. Na verdade, evite, em geral, misturar várias tecnologias de simultaneidade. fork assume que funciona em programas C normais, ele pretende apenas clonar uma tarefa paralela, não mais.

Em segundo lugar, evite abrir ou fopen arquivos antes de fork (). Arquivos é uma das únicas coisas compartilhado e não clonado entre pai e filho. Se você ler 16 bytes no pai, ele moverá o cursor de leitura para frente em 16 bytes Ambas no pai e na criança. Pior, se o filho e o pai gravam bytes no mesmo arquivo ao mesmo tempo, os bytes do pai podem ser misturado com bytes da criança!

Para ser claro, fora de STDIN, STDOUT e STDERR, você realmente não deseja compartilhar nenhum arquivo aberto com clones.

Terceiro, tome cuidado com os soquetes. Soquetes são também compartilhado entre pais e filhos. É útil para ouvir uma porta e, em seguida, permitir que vários trabalhadores filhos estejam prontos para lidar com uma nova conexão de cliente. Contudo, se você usá-lo incorretamente, você terá problemas.

Quarto, se você quiser chamar fork () dentro de um loop, faça isso com extremo cuidado. Vamos pegar este código:

/ * NÃO COMPILAR ISTO * /
const int targetFork = 4;
pid_t forkResult
 
para (int i = 0; i < targetFork; i++)
forkResult = fork ();
/ *… * /
 

Se você ler o código, pode esperar que ele crie 4 filhos. Mas vai sim criar 16 crianças. É porque as crianças vão tb execute o loop e assim os filhos irão, por sua vez, chamar fork (). Quando o loop é infinito, é chamado de bomba de garfo e é uma das maneiras de desacelerar um sistema Linux tanto que não funciona mais e precisará de uma reinicialização. Em suma, tenha em mente que a Guerra dos Clones não é perigosa apenas em Guerra nas Estrelas!

Agora você viu como um simples loop pode dar errado, como usar loops com fork ()? Se você precisar de um loop, sempre verifique o valor de retorno do fork:

const int targetFork = 4;
pid_t forkResult;
int i = 0;
Faz
forkResult = fork ();
/ *… * /
i ++;
while ((forkResult != 0 && forkResult != -1) && (i < targetFork));

Conclusão

Agora é hora de você fazer seus próprios experimentos com fork ()! Experimente novas maneiras de otimizar o tempo realizando tarefas em vários núcleos da CPU ou faça algum processamento em segundo plano enquanto espera a leitura de um arquivo!

Não hesite em ler as páginas de manual através do comando man. Você aprenderá como fork () funciona precisamente, quais erros você pode obter, etc. E aproveite a simultaneidade!

Como mostrar a sobreposição de OSD em aplicativos e jogos Linux em tela cheia
Jogar jogos em tela cheia ou usar aplicativos em modo de tela cheia sem distração pode cortar você das informações relevantes do sistema visíveis em u...
Top 5 cartas de captura de jogos
Todos nós vimos e amamos streaming de jogos no YouTube. PewDiePie, Jakesepticye e Markiplier são apenas alguns dos melhores jogadores que ganharam mil...
Como desenvolver um jogo no Linux
Uma década atrás, poucos usuários de Linux preveriam que seu sistema operacional favorito um dia seria uma plataforma de jogos popular para videogames...