Quando você quer ler o arquivo, você pede ao sistema operacional pelo conteúdo do arquivo.
Quando você quer escrever no arquivo, você pede ao sistema operacional para inserir o conteúdo no arquivo.
É importante saber que o sistema operacional toma diversos cuidados para que desenvolvedores não acessem diretamente o hardware, ou seja, por baixo dos panos você está pedindo para o sistema operacional ler/escrever.
- C
- fgets()
- fwrite()
 
- Python
- file.read()
- file.write()
 
- Rust
- file.read_to_string()
- file.write_all()
 
- Go
- os.ReadFile()
- os.WriteFile()
 
Veremos como garantir a segurança de um arquivo quando se tem múltiplos processos querendo altera-lo.
Process
A função utilizada para se criar processos é fork(), está função faz com que o atual processo crie um processo filho quase idêntico e executando o mesmo código que o pai.
Olhe este código que printa duas vezes "Hi":
#include <stdio.h>
#include <unistd.h>
int main() {
    fork();
    printf("Hi\n");
}
Se você executa-lo irá notar que o filho é tão igual ao pai que ele continua exatamente do mesmo local que o pai se encontrava (logo após fork() retornar um valor). Se tivessemos variáveis, poderiamos ver que até o valor delas são idênticos ao do pai.
No entanto, precisamos de uma maneira de reconhecer quem é o pai e filho, caso contrário este código executária exatamente a mesma coisa para ambos (não seria nada produtivo). Acontece que a função fork() retorna um valor e este valor é utilizado para sabermos se estamos no pai ou no filho.
#include <stdio.h>
#include <unistd.h>
int main() {
  int pid = fork();
  if (pid == -1) {
    printf("Failed to create child process\n");
  } else if (pid == 0) {
    printf("I'm the child process\n");
  } else {
    printf("I'm the parent process\n");
  }
}
A função fork() vai retornar ao pai o PID do filho (ou -1 em caso de error).
A função fork() vai retornar ao filho zero.
Normalmente o código do pai e filho são inseridos em funções em vez de deixar tudo dentro de um if/else.
if(pid == 0) {
  child_code();
} else {
  parent_code();
}
Ou se utiliza funções exec para transformar completamente o código executado naquele processo.
Problem
Quando dois processos interagem com o mesmo arquivo, pode acontecer da informação ser preenchida incorretamente? Afinal, precisamos primeiramente descobrir se isso é possível ou não de acontecer.
Como o escalonamento pode ser imprevisivel, uma maneira de testar se durante a interação com um arquivo houve troca de processo é repetindo a ação diversas vezes e ver se pelo menos uma vez ocorreu.
O seguinte código irá ser executado para o processo pai e filho:
int count = 0;
FILE* file = fopen("example.txt", "w+");
while(count < 10000) {
    int i;
    
    fseek(file, 0, SEEK_SET);
    fscanf(file, "%d", &i);
    fseek(file, 0, SEEK_SET);
    fprintf(file, "%d     ", ++i);
    
    count++;
}
fclose(file);
O código irá garantir que o cursor está no início do arquivo, ler o atual número do arquivo, mover o cursor para o início do arquivo e sobreescrever o número.
fprintf(file, "%d     ", ++i);
Por que inserir espaço após o número? Foi uma maneira de evitar que o número de ambos processos se misturem.
Por exemplo: Processo 1 escreve 5000 e processo 2 escreve 9, o arquivo irá conter "9000" pois o 9 foi escrito em cima do 5.
Agora só precisamos adicionar a lógica de criar processo vista anteriormente:
#include <stdio.h>
#include <unistd.h>
void code() {
  int count = 0;
  FILE* file = fopen("example.txt", "w+");
  
  while(count < 10000) {
    int i;
    
    fseek(file, 0, SEEK_SET);
    fscanf(file, "%d", &i);
    fseek(file, 0, SEEK_SET);
    fprintf(file, "%d     ", ++i);
    
    count++;
  }
  
  fclose(file);
}
int main() {
  FILE* file = fopen("example.txt", "w");
  fputc('0', file);
  fclose(file);
  
  int pid = fork();
  if (pid == -1) {
    printf("Failed to create child process\n");
  } else if (pid == 0) {
    code();
    printf("Child finished\n");
  } else {
    code();
    printf("Parent finished\n");
  }
}
Quando executei este código para 10 iterações, o valor final do arquivo foi 20.
Quando executei este código para 1000 iterações, o valor final do arquivo foi 1000.
Quando executei este código para 10000 iterações, o valor final do arquivo foi 10015.
O que somos capaz de deduzir com isto?
- O resultado é imprevisível pois não temos controle de quando o escalonador vai trocar os processos
- Dependendo do volume de iterações e da máquina do usuário, um processo pode ou não conseguir fazer a tarefa antes do escalonador trocar o processo
- Se houver troca durante uma tarefa, pode corromper o resultado do arquivo
Quais as chances disto acontecer? Depende do software, pois existem arquivos que a chance de dois softwares interagirem ao mesmo tempo é 0%.
Locks
Acontece que existe mais de uma maneira de aplicar locks no Linux.
- flock
- file lock
- Origem do sistema operacional BSD
 
- lockf
- lock file
- POSIX
- É uma versão simplificada do fcntl "Advisory record locking"
 
- fcntl "Advisory record locking"
- file control
- POSIX
- Uma função capaz de fazer diversas operações sobre file descriptors
- Uma delas é utilizar "Advisory record locking"
 
 
- fcntl "Open file description locks (non-POSIX)"
- file control
- Linux specific
- Existem propostas para ser adicionado ao padrões POSIX
 
- Uma função capaz de fazer diversas operações sobre file descriptors
- Uma delas é utilizar "Open file description locks (non-POSIX)"
 
 
Se quiser saber mais sobre cada um, é bom ler o post no blog: https://gavv.net/articles/file-locks/
Incrivel como um blog de 8 anos atrás me ajudou mais do que pesquisas no Google.
flock
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <unistd.h>
#define BUFFER_SIZE 256
void code() {
  int count = 0;
  char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
  int fd = open("example.txt", O_RDWR);
  while (count < 10000) {
    int i;
    flock(fd, LOCK_EX);
    lseek(fd, 0, SEEK_SET);
    read(fd, buffer, BUFFER_SIZE);
    i = atoi(buffer) + 1;
    sprintf(buffer, "%d     ", i);
    lseek(fd, 0, SEEK_SET);
    write(fd, buffer, strlen(buffer));
    flock(fd, LOCK_UN);
    count++;
  }
  close(fd);
}
int main() {
  FILE *file = fopen("example.txt", "w");
  fputc('0', file);
  fclose(file);
  int pid = fork();
  if (pid == -1) {
    printf("Failed to create child process\n");
  } else if (pid == 0) {
    code();
    printf("Child finished\n");
  } else {
    code();
    printf("Parent finished\n");
  }
}
lockf
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUFFER_SIZE 256
void code() {
  int count = 0;
  char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
  int fd = open("example.txt", O_RDWR);
  while (count < 10000) {
    int i;
    lockf(fd, F_LOCK, 0);
    lseek(fd, 0, SEEK_SET);
    read(fd, buffer, BUFFER_SIZE);
    i = atoi(buffer) + 1;
    sprintf(buffer, "%d     ", i);
    lseek(fd, 0, SEEK_SET);
    write(fd, buffer, strlen(buffer));
    lockf(fd, F_ULOCK, 0);
    count++;
  }
  close(fd);
}
int main() {
  FILE *file = fopen("example.txt", "w");
  fputc('0', file);
  fclose(file);
  int pid = fork();
  if (pid == -1) {
    printf("Failed to create child process\n");
  } else if (pid == 0) {
    code();
    printf("Child finished\n");
  } else {
    code();
    printf("Parent finished\n");
  }
}
fcntl - "Advisory record locking"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUFFER_SIZE 256
void code() {
  int count = 0;
  struct flock fl;
  char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
  int fd = open("example.txt", O_RDWR);
  fl.l_whence = SEEK_SET;
  fl.l_start = 0;
  fl.l_len = 0;
  while (count < 10000) {
    int i;
    fl.l_type = F_WRLCK;
    fcntl(fd, F_SETLKW, &fl);
    lseek(fd, 0, SEEK_SET);
    read(fd, buffer, BUFFER_SIZE);
    i = atoi(buffer) + 1;
    sprintf(buffer, "%d     ", i);
    lseek(fd, 0, SEEK_SET);
    write(fd, buffer, strlen(buffer));
    fl.l_type = F_UNLCK;
    fcntl(fd, F_SETLKW, &fl);
    count++;
  }
  close(fd);
}
int main() {
  FILE *file = fopen("example.txt", "w");
  fputc('0', file);
  fclose(file);
  int pid = fork();
  if (pid == -1) {
    printf("Failed to create child process\n");
  } else if (pid == 0) {
    code();
    printf("Child finished\n");
  } else {
    code();
    printf("Parent finished\n");
  }
}
References
- https://www.youtube.com/watch?v=ioJkA7Mw2-U
- O importante do video é o início que explica como chamadas ao sistema são feitas
 
- https://man7.org/linux/man-pages/man3/lockf.3.html
- https://man7.org/linux/man-pages/man2/flock.2.html
- https://man7.org/linux/man-pages/man2/fcntl.2.html
- https://man7.org/linux/man-pages/man3/flockfile.3.html
- https://man7.org/linux/man-pages/man3/fdopen.3.html
- https://en.wikipedia.org/wiki/Unistd.h
- https://en.wikipedia.org/wiki/C_standard_library
- https://en.wikipedia.org/wiki/C_file_input/output
- https://en.wikipedia.org/wiki/File_descriptor
- https://en.wikipedia.org/wiki/File_locking
- https://gavv.net/articles/file-locks/
Extra
Por diversão, eu experimentei replicar o mesmo problema em outras linguagens.
package main
import (
	"fmt"
	"os"
	"os/exec"
)
func code() {
	var count int = 0
	file, _ := os.OpenFile("example.txt", os.O_RDWR, 0666)
	for count < 10000 {
		var i int
		file.Seek(0, 0)
		fmt.Fscanf(file, "%d", &i)
		file.Seek(0, 0)
		fmt.Fprintf(file, "%d     ", i+1)
		count++
	}
	file.Close()
}
func main() {
	if len(os.Args) >= 2 {
		code()
		fmt.Println("Child finished")
	} else {
		os.WriteFile("example.txt", []byte{'0'}, 0600)
		command := exec.Command("/usr/bin/go", "run", "main.go", "fork")
		command.Start()
		code()
		command.Wait()
		fmt.Println("Parent finished")
	}
}