Aprendi recentemente que fork aplica COW (copy on write) até mesmo em registradores.
Brinquei com uma caixa de brinquedo simples para verificar:
(gdb) info inferiors
Num Description Connection Executable
* 1 process 59316 1 (native) /home/Drew/mycode/a.out
2 process 59386 1 (native) /home/Drew/mycode/a.out
(gdb) print( $rdi )
$1 = 18874385
(gdb) set $rdi=42
(gdb) print( $rdi )
$2 = 42
(gdb) inferior 2
[Switching to inferior 2 [process 59386] (/home/Drew/mycode/a.out)]
[Switching to thread 2.1 (process 59386)]
#0 0x00002aaaab090291 in fork () from /lib64/libc.so.6
(gdb) print( $rdi )
$3 = 18874385
Como vocês podem ver, modificar o rdi no proc pai não interfere no proc filho.
Gostaria de saber como isso é feito? As CPUs modernas fornecem múltiplos registradores iguais exclusivamente para multiprocessos?
EDITAR:
Obrigado a vocês, companheiros, por comentarem. Responderei às perguntas nos comentários aqui coletivamente, espero que ajude a esclarecer.
A referência que estou usando é esta nota de aula .
Geralmente quero descobrir:
- O Linux alguma vez copia os registradores, seja COW ou não?
- Se a resposta for sim, isso é feito pelo hardware, ou seja, os fabricantes da CPU fornecem suporte de design como fazem para dar suporte a máquinas virtuais, ou pelo software, digamos, o sistema operacional armazena o valor do registro em algum lugar (na RAM?), então, em vez de operar nos próprios registros, ele efetivamente modifica a página, mas de alguma forma consegue imitar o comportamento do registro?
- Se fork não copia registradores de forma alguma, por que estou vendo o fenômeno no GDB?
Sistemas que fornecem multitarefa preventiva fornecem virtualização de CPU. Isso permite que cada thread se comporte como se tivesse um núcleo de CPU (e seus registradores) para si. Mais especificamente, isso permite que uma thread use registradores sem ter que se preocupar com eles sendo alterados por outra thread (do mesmo processo ou de um diferente) no meio de uma operação.
Este é o caso não importa como o thread é criado, e, portanto, inclui threads criados por
fork
. Como o sistema consegue isso não é relevante. Mas ele garante que quaisquer alterações em, digamos,rdi
ou o ponteiro de instrução em um thread não seja visto em outro.Não sei quais garantias
fork
ele fornece sobre o valor inicial dos registradores após umfork
. Poderia ser as mesmas garantias de outras chamadas cobertas pelo ABI do sistema.Curiosidade: uma vez criei um programinha que não usava cópia na escrita. Depois de gerar um processo filho, eles eram os mesmos, exceto pelos registradores e identificadores de arquivo.
Na verdade é bem fácil, só não precisa gravar nada na RAM.
Olhando para a página man para
clone()
, podemos até mesmo compartilhar RAM entre pai e filho. Podemos passar CLONE_VM, mas não CLONE_PID para realizar isso.A cópia do registro acontece exatamente uma vez, quando você cria um novo thread com chamadas de sistema fork(), vfork() ou clone().
Cada processo, cada thread deve ter inerentemente um banco de registros separado; isso é inerente ao que é ser um processo (na terminologia antiga) ou um thread (na terminologia nova).
Então como isso é feito?
Sempre que o kernel troca de threads, o antigo banco de registradores é armazenado na estrutura de dados do processo. (Lembre-se da coisa sobre terminologia; o kernel usa a terminologia antiga.) Sempre que ele troca de volta, ele as restaura. Na verdade, há um pouco mais nisso; a estrutura de dados do processo na verdade tem o banco de registradores do kernel; o banco de registradores do espaço do usuário foi empurrado para a pilha do kernel para o thread quando a interrupção aconteceu, o que acionou a troca de contexto.