[Linux Advanced I/O (6)] Primeira compreensão dos bloqueios de arquivo - método flock() (com exemplos de código)

        Imagine o que acontece quando duas pessoas editam o mesmo arquivo no disco ao mesmo tempo? Em sistemas Linux, o estado final do arquivo geralmente depende do último processo que gravou o arquivo. Vários processos operam o mesmo arquivo ao mesmo tempo, o que pode facilmente levar à confusão dos dados no arquivo, porque quando vários processos executam operações de E/S no arquivo, é fácil gerar uma condição de corrida, fazendo com que o conteúdo em o arquivo seja inconsistente com o esperado!

        Para alguns aplicativos, um processo às vezes precisa garantir que apenas ele mesmo possa executar operações de E/S em um determinado arquivo, e outros processos não têm permissão para executar operações de E/S no arquivo durante esse período. Para fornecer essa função ao processo, o sistema Linux fornece um mecanismo de bloqueio de arquivo.  

        Eu aprendi sobre mutexes, bloqueios de rotação e bloqueios de leitura e gravação antes. Como esses bloqueios, os bloqueios de arquivo são mecanismos de bloqueio fornecidos pelo kernel. O mecanismo de bloqueio é implementado para proteger o acesso a recursos compartilhados; Bloqueios de gravação e bloqueios de arquivo são diferentes. Bloqueios mutex, bloqueios de rotação e bloqueios de leitura/gravação são usados ​​principalmente em ambientes multiencadeados para proteger o acesso a recursos compartilhados e obter sincronização de encadeamento.

        O bloqueio de arquivo, como o nome indica, é um mecanismo de bloqueio aplicado aos arquivos. Quando vários processos operam no mesmo arquivo ao mesmo tempo, como garantimos a exatidão dos dados do arquivo? O Linux geralmente adota um método de bloqueio do arquivo para evitar vários processos Uma condição de corrida ocorre ao operar no mesmo arquivo simultaneamente. Por exemplo, quando um processo executa operações de E/S em um arquivo, ele primeiro bloqueia o arquivo, bloqueia-o e, em seguida, executa operações de leitura e gravação; enquanto o processo não desbloquear o arquivo, outros processos não poderão operar nele; assim, pode-se garantir que somente ele (o processo) pode ler e gravar no arquivo enquanto ele estiver bloqueado.

        Como um arquivo pode ser operado por vários processos ao mesmo tempo, isso significa que o arquivo deve ser um recurso compartilhado, portanto, pode-se ver que, em última análise, o bloqueio de arquivo também é um mecanismo de proteção de acesso a recursos compartilhados. Bloqueando o arquivo , para evitar condições de corrida ao acessar recursos compartilhados.

        Classificação de bloqueios de arquivos

        Os bloqueios de arquivos podem ser divididos em dois tipos: bloqueios consultivos e bloqueios obrigatórios:

        ⚫ Fechaduras consultivas

        O bloqueio consultivo é essencialmente um protocolo. Antes de o programa acessar o arquivo, ele bloqueia o arquivo primeiro e, em seguida, acessa o arquivo após o bloqueio ser bem-sucedido. Este é um uso do bloqueio consultivo; mas se o seu programa não se importar, é também é possível acessar diretamente o arquivo sem bloqueá-lo; se for esse o caso, o bloqueio consultivo não desempenha nenhum papel. Se o bloqueio consultivo funcionar, todos devem cumprir o contrato. Bloqueie o arquivo antes de acessá-lo . Isso é como um semáforo, que estipula que o sinal vermelho não pode passar, só o verde pode passar, mas se você insistir em passar no sinal vermelho, ninguém pode te parar, então as consequências vão levar a acidentes de trânsito; então todos devem trabalhar juntos Obedeça às regras de trânsito, os semáforos podem desempenhar um papel.

        ⚫ Bloqueio obrigatório:

        O bloqueio obrigatório é mais fácil de entender. É um requisito obrigatório. Se um processo tiver um bloqueio obrigatório em um arquivo, outros processos não poderão acessar o arquivo sem obter o bloqueio do arquivo. A razão essencial é que o bloqueio obrigatório permitirá que o kernel verifique todas as operações de E/S (como read(), write()) para verificar se o processo de chamada é o proprietário do bloqueio de arquivo e, caso contrário, o arquivo será não seja acessível. Quando um arquivo é bloqueado para gravação, o kernel impede que outros processos o leiam e gravem. A adoção de bloqueios obrigatórios tem um grande impacto no desempenho, e os bloqueios de arquivo devem ser verificados sempre que uma operação de leitura ou gravação é executada.

        No sistema Linux, você pode chamar as três funções flock(), fcntl() e lockf() para bloquear o arquivo.A seguir, apresentaremos o uso de cada função.

A função floc() bloqueia

       Vamos primeiro aprender sobre a chamada de sistema flock(), que pode ser usada para bloquear ou desbloquear arquivos, mas a função flok() só pode gerar bloqueios consultivos, e seu protótipo de função é o seguinte:

#include <sys/file.h>

int flock(int fd, int operation);

        Para usar esta função, o arquivo de cabeçalho <sys/file.h> precisa ser incluído.

        Parâmetros de função e valores de retorno têm os seguintes significados:

        fd: O parâmetro fd é um descritor de arquivo, especificando o arquivo a ser bloqueado.

        operation: O parâmetro operation especifica o modo de operação e pode ser definido para um dos seguintes valores:

  •  LOCK_SH: Coloque um bloqueio compartilhado no arquivo referenciado por fd. O chamado compartilhamento significa que vários processos podem possuir um bloqueio compartilhado no mesmo arquivo, e o bloqueio compartilhado pode pertencer a vários processos ao mesmo tempo.
  • LOCK_EX: Coloque um bloqueio exclusivo (ou mutex) no arquivo referenciado por fd. A chamada exclusão mútua significa que o mutex só pode pertencer a um processo ao mesmo tempo.
  • LOCK_UN: Desbloqueie o status de bloqueio do arquivo, desbloqueie e libere o bloqueio. Além das três bandeiras acima, há mais uma bandeira:
  • LOCK_NB: Indica que o bloqueio é adquirido de forma não bloqueante. Por padrão, quando o bloqueio de arquivo não pode ser obtido pela chamada de floc(), ele será bloqueado até que outros processos liberem o bloqueio. Se você não deseja que o programa seja bloqueado, pode especificar o sinalizador LOCK_NB. Se o bloqueio não puder ser obtido , ele deve retornar imediatamente (retorno de erro e errno é definido como EWOULDBLOCK), geralmente usado com LOCK_SH ou LOCK_EX, combinado pelo operador OR bit a bit.

        Valor de retorno: sucesso retornará 0, falha retornará -1 e errno será definido. Para flock(), deve-se observar que o mesmo arquivo não possui bloqueios compartilhados e bloqueios mutex ao mesmo tempo.

        Exemplo de uso

        O código a seguir demonstra o uso da função rebanho() para bloquear e desbloquear um arquivo (bloqueio consultivo). O programa primeiro chama a função open() para abrir o arquivo, e o caminho do arquivo é passado por parâmetros; depois que o arquivo é aberto com sucesso, ele chama a função flock() para bloquear o arquivo (modo sem bloqueio, exclusivo lock) e imprime "arquivo Se o bloqueio falhar, a mensagem "Falha ao bloquear o arquivo" será impressa. Em seguida, chame a função de sinal para registrar uma função de processamento de sinal para o sinal SIGINT. Quando o processo receber o sinal SIGINT, ele executará a função sigint_handler(), desbloqueará o arquivo na função de processamento de sinal e encerrará o processo.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <signal.h>

static int fd = -1; //文件描述符

/* 信号处理函数 */
static void sigint_handler(int sig){
    if (SIGINT != sig)
    return;
    /* 解锁 */
    flock(fd, LOCK_UN);
    close(fd);
    printf("进程 1: 文件已解锁!\n");
}    

int main(int argc, char *argv[])    {
    if (2 != argc) {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        exit(-1);
    }
    /* 打开文件 */
    fd = open(argv[1], O_WRONLY);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }
    /* 以非阻塞方式对文件加锁(排它锁) */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {
        perror("进程 1: 文件加锁失败");
        exit(-1);
    }
    printf("进程 1: 文件加锁成功!\n");

    /* 为 SIGINT 信号注册处理函数 */
    signal(SIGINT, sigint_handler);
    for ( ; ; )
        sleep(1);
}

        Após o bloqueio ser adicionado com sucesso, o programa entra no loop for infinito e segura o bloqueio o tempo todo; neste momento podemos executar outro programa, conforme mostrado abaixo, o programa também abrirá o arquivo primeiro, e o caminho do arquivo é passado passando parâmetros, também em O programa também chamará a função flock() para bloquear o arquivo (bloqueio exclusivo, modo sem bloqueio). Independentemente de o bloqueio ser bem-sucedido ou não, as seguintes operações de E/S serão executadas , e os dados serão gravados no arquivo, lidos e impressos. .

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[100] = "Hello World!";
    int fd;
    int len;
    if (2 != argc) {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        exit(-1);
    }
    
    /* 打开文件 */
    fd = open(argv[1], O_RDWR);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }

    /* 以非阻塞方式对文件加锁(排它锁) */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB))
        perror("进程 2: 文件加锁失败");
    else
        printf("进程 2: 文件加锁成功!\n");

    /* 写文件 */
    len = strlen(buf);
    if (0 > write(fd, buf, len)) {
        perror("write error");
        exit(-1);
    }
    printf("进程 2: 写入到文件的字符串<%s>\n", buf);

    /* 将文件读写位置移动到文件头 */
    if (0 > lseek(fd, 0x0, SEEK_SET)) {
        perror("lseek error");
        exit(-1);
    }
    /* 读文件 */
    memset(buf, 0x0, sizeof(buf)); //清理 buf
    if (0 > read(fd, buf, len)) {
        perror("read error");
        exit(-1);
    }
    printf("进程 2: 从文件读取的字符串<%s>\n", buf);

    /* 解锁、退出 */
    flock(fd, LOCK_UN);
    close(fd);
    exit(0);
}

        Pegue os códigos acima como aplicativo 1 e aplicativo 2, respectivamente, e compile-os em diferentes arquivos executáveis ​​testApp1 e testApp2, como segue:

         Antes de testar, crie um arquivo de teste infile, basta usar o comando touch para criá-lo, primeiro execute o aplicativo testApp1, use o arquivo infile como um arquivo de entrada e coloque-o em segundo plano para executar:

         testApp1 será executado em segundo plano e seu pid pode ser visualizado como 20710 pelo comando ps. Em seguida execute a aplicação testApp2 e passe no mesmo arquivo infile, conforme abaixo:

         A partir das informações impressas, podemos ver que o processo testApp2 falha ao bloquear o arquivo infile porque o bloqueio já está mantido pelo processo testApp1, portanto, o bloqueio testApp2 falhará naturalmente; mas pode-se descobrir que, embora o bloqueio falhe, a leitura e as operações de gravação no arquivo por testApp2 são Se não houver problema, foi bem-sucedido. Este é o recurso do bloqueio consultivo (não obrigatório) ; a maneira correta de usá-lo é não executar operações de E/S no arquivo depois que o bloqueio falhar e siga este protocolo.

        Em seguida, enviamos um sinal SIGIO para o processo testApp1 para permitir que ele desbloqueie o arquivo infile e, em seguida, executamos testApp2 novamente, conforme mostrado abaixo:

         Use o comando kill para enviar um sinal de número 2 para o processo testApp1, que é o sinal SIGIO. Após receber o sinal, testApp1 desbloqueia o arquivo infile e sai; em seguida, executa o programa testApp2 novamente. A partir das informações impressas, pode-se visto que o arquivo infile pode ser processado com sucesso desta vez. O arquivo está bloqueado e não há problemas com leitura e gravação.

        Algumas regras sobre o rebanho ()

  • Bloquear um arquivo várias vezes pelo mesmo processo não causará impasse. Quando o processo chamar flock() para travar o arquivo com sucesso, chame flock() novamente para travar o arquivo (o mesmo descritor de arquivo), para que nenhum impasse seja causado, e o travamento recém-adicionado substituirá o travamento antigo. Por exemplo, chame flock() para adicionar um bloqueio compartilhado ao arquivo, chame flock() novamente para adicionar um bloqueio exclusivo ao arquivo e, finalmente, o bloqueio do arquivo será substituído por um bloqueio compartilhado com um bloqueio exclusivo.
  • Quando o arquivo for fechado, ele será desbloqueado automaticamente. O processo chama floc() para bloquear o arquivo.Se o arquivo for fechado antes de ser desbloqueado, o bloqueio de arquivo será desbloqueado automaticamente, ou seja, o bloqueio de arquivo será liberado automaticamente após o descritor de arquivo correspondente ser fechado. Da mesma forma, quando um processo termina, todos os bloqueios que ele estabeleceu serão liberados.
  • Um processo não pode desbloquear um bloqueio de arquivo mantido por outro processo.
  • Processos filho criados por fork() não herdam bloqueios criados pelo processo pai. Isso significa que, se um processo bloqueia um arquivo com sucesso e, em seguida, o processo chama fork() para criar um processo filho, o processo filho é considerado outro processo para o bloqueio criado pelo processo pai, embora o processo filho do processo pai processo herda seu descritor de arquivo, mas não o bloqueio de arquivo. Essa restrição faz sentido, porque a função do bloqueio é impedir que vários processos gravem o mesmo arquivo ao mesmo tempo. Se o processo filho herdar o bloqueio do processo pai por meio de fork(), o processo pai e o processo filho podem gravar o mesmo arquivo ao mesmo tempo.

        Além disso, quando um descritor de arquivo é copiado (como usar a operação dup(), dup2() ou fcntl() F_DUPFD), esses descritores de arquivo copiados e os descritores de arquivo de origem se referirão ao mesmo arquivo lock , desbloqueando com qualquer um desses arquivos descritores funciona da seguinte forma:

flock(fd, LOCK_EX); //加锁
new_fd = dup(fd);
flock(new_fd, LOCK_UN); //解锁

        Esse código primeiro define um bloqueio exclusivo em fd, depois usa dup() para copiar fd para obter um novo descritor de arquivo new_fd e, finalmente, desbloqueia-o por meio de new_fd, que pode ser desbloqueado com sucesso. No entanto, se você não chamar explicitamente uma operação de desbloqueio, o bloqueio não será liberado até que todos os descritores de arquivo sejam fechados. Por exemplo, no exemplo acima, se flock(new_fd, LOCK_UN) não for chamado para desbloquear, o bloqueio será liberado automaticamente somente quando fd e new_fd forem fechados.

        O conteúdo de floc() parará aqui por enquanto, e aprenderemos a usar fcntl() para bloquear arquivos mais tarde.

Acho que você gosta

Origin blog.csdn.net/cj_lsk/article/details/130873466
Recomendado
Clasificación