C언어 레코드 잠금 함수 fcntl()

2020. 3. 16. 07:57 컴퓨터/프로그래밍

C함수 fcntl() 레코드 잠금

fcntl() 함수는 파일을 전체나 파일의 일부를 다른 프로세스에서 사용하는 것을 제한 해 주는 함수입니다.
  • 헤더: unistd.h, fcntl.h
  • 형태: int fcntl(int fd, int cmd, struct flock * lock)
  • 인수: int fd 제어 대상 파일 디스크립터
    int cmd 제어 동작 명령
    struct flock * lock 잠금을 위한 옵션
  • 반환: -1 == 실패, 이외는 cmd에 따라 달라짐

파일을 열기할 때 open() 함수나 fopen() 함수의 mode를 이용하여 다른 프로세스가 읽기나 쓰기를 제한할 수 있습니다. 그러나 이것은 파일 전체에 대해 적용되며 제한을 변경하기 위해서는 파일을 닫았다가 다시 열열어 합니다.

fcntl()은 오픈된 파일에 대해서 필요에 따라 제한을 여러 번 변경하실 수 있으며, 파일 전체 뿐만 아니라 일부만 제한할 수 있습니다. 즉, 파일의 특정 부분을 "잠금" 상태를 만들 수 있어서 fcntl() 함수를 "파일 잠금 함수"라기 보다는 "레코드 잠금 함수"로 불리워 집니다.

caution

주의 하실 점은 파일에 대한 잠금은 프로세스별로 잠금 정보를 지정하는 것 뿐이지 실제로 다른 프로세스가 읽고 쓰는 것을 못하게 하는 것은 아닙니다. 즉, 쓰기를 제한했다고 해서 다른 프로세스에서 write() 가 실행이 안 되거나 에러가 발생하지 않습니다.

잠금 정보는 하나의 파일을 여러 프로세스가 동시에 사용하는 경우, 같은 시간에 쓰기를 하거나 아직 한쪽에서 변경 중이라면 다른 프로세스가 읽지를 못하게 하기 위한 정보를 주고 받기 위한 방법으로 이해햐셔야 합니다.

아래 예제에서도 쓰기를 하기 전에 쓰기 잠금이 가능한지를 확인한 후에 write() 를 실행했습니다. 그러나 확인없이 쓰기를 한다면 쓰기가 가능합니다. 그러므로 잠금 정보는 프로세스가 쓰기를 못하게 한다가 아니라 지금은 읽어 서는 안 된다 또는 쓰기를 해서는 안 된다라는 정보로 이용하셔야 합니다.

int fcntl(int fd, int cmd, struct flock * lock);
cmd 의미
F_GETLK 레코의 잠금 상태를 구해지며, 정보는 세번째 인수인 lock에 담겨져 옮니다.
F_SETLK 레코드 잠금을 요청하며, 다른 프로세스가 먼저 선점해서 실패했다면 즉시 -1 로 복귀합니다.
F_SETLKW 끝의 W는 wait의 약자로 레코드 잠금을 요청했는데, 다른 프로세스가 먼저 선점해서 실패했다면 그 프로세스가 해제할 때까지 대기합니다.

이번에는 struct flock * lock의 내용을 알아 봐야 겠지요. ^^

struct flock {
        short   l_type;
        short   l_whence;
        off_t   l_start;
        off_t   l_len;
        pid_t   l_pid;
        __ARCH_FLOCK_PAD
};

l_type 은 어떻게 잠금을 할지, 해제할지를 정합니다. 즉, 아래와 같은 상수가 정의되어 있습니다.


l_type 의미
F_RDLCK 다른 프로세스가 읽기 잠금만 가능하게하고 쓰기 잠금은 못하게 합니다.
F_WRLCK 다른 프로세스는 읽기 잠금과 쓰기 잠금 모두 불가능하도록 합니다.
F_UNLCK 잠금을 해제합니다.

l_whence 는 블록할 영역을 지정하는 기준 위치를 지정합니다. 즉, 파일 첫 부분부터 따질지, 아니면 현제 읽기/쓰기 포인터를 기준으로 따질지를 정합니다.


l_whence 의미
SEEK_SET 파일의 시작 위치
SEEK_CUR 현재 읽기/쓰기 포인터를 기준
SEEK_END 파일의 끝을 기준

즉, l_whence가 SEEK_CUR이고 l_start가 0이면서 l_len이 0 이면 파일 전체를 가르키게 됩니다. 아래의 그림을 보십시오.

l_pid 는 F_GETLK 를 실행하여 레코드에 대한 잠금 상태 정보를 구할 때, 이미 잠금을 실행하고 있는 프로세스의 ID 입니다.

예제 1

예제에서는 같은 파일을 부모 프로세스가 열기를 하고 5번째 문자부터 7개의 문자 영역을 읽기만 가능할 뿐 쓰기를 해서는 안 된다는 잠금 정보를 설정합니다. 차일드 프로세스는 쓰기할 영역이 쓰기가 가능한 지 확인한 후에 쓰기를 합니다.

예제에서 차일드는 2 곳에 따로따로 쓰기를 하는데, 한 번은 쓰기가 가능한 곳이지만 다른 한 곳은 부모 프로세스에 의해 읽기 잠금한 영역입니다. 이 때 쓰기가 어떻게 되는지 보겠습니다. 예제에서 사용하는 data.txt의 내용은 "Good morning jwmx.tistory.com!!" 입니다.

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
   char    *filename = "data.txt";
   int      fd;
   pid_t    pid;
   struct   flock filelock;

   pid   = fork();
   switch( pid){
   case -1  :
      printf( "자식 프로세스 생성 실패\n");
      return -1;
   case 0   :
      printf( "자식: 부모 프로세스를 위해 잠시 대기하겠습니다.\n");
      sleep( 1);

      printf( "자식: 파일의 내용을 수정합니다.\n");
      fd = open( filename, O_RDWR ¦ O_CREAT, 0666);

      filelock.l_type   = F_WRLCK;
      filelock.l_whence = SEEK_SET;
      filelock.l_start  = 0;
      filelock.l_len    = 4;

      if ( -1 == fcntl( fd, F_SETLK, &filelock)){
         printf( "자식:레코드 잠금에 실패해서 jwmx를 쓰지를 못했습니다.\n");
      } else {
         write( fd, "jwmx", 4);
         printf( "자식:jwmx 쓰기 완료\n");
      }

      filelock.l_type   = F_WRLCK;
      filelock.l_whence = SEEK_SET;
      filelock.l_start  = 5;
      filelock.l_len    = 7;

      if ( -1 == fcntl( fd, F_SETLK, &filelock)){
         // 여기서는 이 if 절을 만족하게 됩니다.
         printf( "자식:레코드 잠금에 실패해서 badayak를 쓰지 못했습니다.\n");
      } else {
         write( fd, "badayak", 7);
         printf( "자식:badyak 쓰기 완료\n");
      }
      close( fd);
      break;
   default  :
      printf( "부모: 파일을 열고 레코드 잠금하겠습니다.\n");
      fd = open( filename, O_RDWR, 0666);

      filelock.l_type   = F_RDLCK;
      filelock.l_whence = SEEK_SET;
      filelock.l_start  = 5;
      filelock.l_len    = 7;

      if ( -1 != fcntl( fd, F_SETLK, &filelock)){
         printf( "부모: 잠금에 성공했으며 3초간 대기합니다.\n");
         sleep( 3);
      }
      close( fd);
   }
}

실행 결과

]$ cat data.txt
Good morning jwmx.tistory.com!!
]$ ./a.out
부모: 파일을 열고 레코드 잠금하겠습니다.
자식: 부모 프로세스를 위해 잠시 대기하겠습니다.
부모: 잠금에 성공했으며 3초간 대기합니다.
자식: 파일의 내용을 수정합니다.
자식:jwmx 쓰기 완료
자식:레코드 잠금에 실패해서 badayak를 쓰지 못했습니다.
]$ cat data.txt
jwmx morning jwmx.tistory.com!!
]$

예제 2

예제 1은 cmd를 F_SETLK를 사용했지만 이번에는 잠금 상태가 해제될 때까지 기다리는 F_SETLKW를 사용해 보겠습니다.

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
   char    *filename = "data.txt";
   int      fd;
   pid_t    pid;
   struct   flock filelock;

   pid   = fork();
   switch( pid){
   case -1  :
      printf( "자식 프로세스 생성 실패\n");
      return -1;
   case 0   :
      printf( "자식: 부모 프로세스를 위해 잠시 대기하겠습니다.\n");
      sleep( 1);

      printf( "자식: 파일의 내용을 수정합니다.\n");
      fd = open( filename, O_RDWR ¦ O_CREAT, 0666);

      filelock.l_type   = F_WRLCK;
      filelock.l_whence = SEEK_SET;
      filelock.l_start  = 0;
      filelock.l_len    = 4;

      if ( -1 == fcntl( fd, F_SETLKW, &filelock)){
         printf( "자식:레코드 잠금에 실패해서 jwmx를 쓰지를 못했습니다.\n");
      } else {
         write( fd, "jwmx", 4);
         printf( "자식:jwmx 쓰기 완료\n");
      }

      filelock.l_type   = F_WRLCK;
      filelock.l_whence = SEEK_SET;
      filelock.l_start  = 5;
      filelock.l_len    = 7;

      if ( -1 == fcntl( fd, F_SETLKW, &filelock)){
         // 여기서는 이 if 절을 만족하게 됩니다.
         printf( "자식:레코드 잠금에 실패해서 badayak를 쓰지 못했습니다.\n");
      } else {
         write( fd, "badayak", 7);
         printf( "자식:badyak 쓰기 완료\n");
      }
      close( fd);
      break;
   default  :
      printf( "부모: 파일을 열고 레코드 잠금하겠습니다.\n");
      fd = open( filename, O_RDWR, 0666);

      filelock.l_type   = F_RDLCK;
      filelock.l_whence = SEEK_SET;
      filelock.l_start  = 5;
      filelock.l_len    = 7;

      if ( -1 != fcntl( fd, F_SETLK, &filelock)){
         printf( "부모: 잠금에 성공했으며 3초간 대기합니다.\n");
         sleep( 3);
      }
      close( fd);
   }
}

실행 결과

]$ cat data.txt
Good morning jwmx.tistory.com!!
]$ ./a.out
부모: 파일을 열고 레코드 잠금하겠습니다.
자식: 부모 프로세스를 위해 잠시 대기하겠습니다.
부모: 잠금에 성공했으며 3초간 대기합니다.
자식: 파일의 내용을 수정합니다.
자식:jwmx 쓰기 완료
자식:badyak 쓰기 완료
]$cat data.txt
jwmxbadayakg jwmx.tistory.com!!
]$
이 댓글을 비밀 댓글로