메뉴

문서정보

fcntl을 이용한 파일 및 레코드잠금

Docbook 원문

목차

소개

fcntl을 이용한 파일잠금, 레코드 잠금의 방법과 그 응용에 대해서 알아본다.

파일잠금과 레코드 잠금은 여러 응용에서 필요로 한다. 멀티 쓰레드(:12) 프로그램에서, 여러 쓰레드가 하나의 파일에 동시에 접근할 경우 파일 잠금이 필요할 수 있다. 한번에 하나의 쓰레드만 파일에 읽기및 쓰기를 해야 하는 경우가 있을 수 있기 때문이다.

DB와 같은경우에는 단지 파일잠금을 넘어서, 파일의 일정부분을 잠그는 레코드 잠금 기능을 필요로 할 것이다. 특정 레코드영역에 한번에 하나의 쓰레드만 접근하도록 제어해야 하기 때문이다. 특히 레코드 쓰기를 할 경우 작업 레코드를 잠궈서 다른 쓰레드가 접근하지 못하게 해야 한다.

유닉스에서는 fcntl(2) 함수를 이용해서 파일잠금과 레코드 잠금을 구현할 수 있다. fcntl은 그 이름에서 알 수 있듯이, 단순히 레코드 잠금만을 위해서 만들어진 함수는 아니며, 다양한 파일 조작 기능을 제공한다. 여기에서는 fcntl 을 이용한 레코드 잠금에 대해서 알아볼 것이다.

레코드 잠금이 필요한 이유

프로세스간 파일 공유 문제는 파일을 잠그는 정도로 간단하게 해결할수 있다. 하나의 프로세스가 파일을 열기전에 파일이 잠겨 있는지 검사하고, 파일이 잠겨 있지 않다면, 파일을 열고 파일에 잠금을 걸면 되기 때문이다. 하지만 많은 사용자가 동시에 접근 해서 파일을 조작해야 하는 DBMS 같은경우, 유저가 작업을 할때 마다 파일 전체을 잠그게 되면, BMS의 성능에 좋지 않은 영향을 끼치게 될것이다.

이럴경우에는 파일 전체를 잠그는것 보다 해당 프로세스가 엑세스 하는 부분만을 잠그고 나머지 부분은 다른 프로세스에서 접근가능하도록 하는게 효율적이다. 동시처리가 가능하기 때문이다.

fcnt

파일 기술자를 조작하는 fcntl 함수를 이용하면, 파일의 일부분을 잠글수 있다. 다음은 fcntl 함수 원형이다.
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock* lock);  // 파일잠금을 위한 fcntl 원형
fcntl 은 위에서 설명했듯이 파일잠금외에도 다양한 파일 기술자 조작기능을 제공한다. 그러므로 여러개의 함수원형이 존재하는데 그중 3번재 함수 원형이 레코드 잠금을 위해서 사용된다.

3번째 매개변수를 보면 flock 라는 구조체가 있는데, 이 구조체에 파일 레코드 잠금을 위해 필요한 여러가지 정보 즉, 잠금의 형태, 어디서 부터 어띠 까지를 잠글것인지, 몇바이트 크기만큼을 잠글것인지 하는 정보를 설정할 수 있다. 이러한 정보의 저장을 위해서 flock 구조체는 아래와 같은 멤버들을 포함한다.

struct flock
{
    short int l_type;   /* 잠김 타입: F_RDLCK, F_WRLCK, or F_UNLCK.  */
    short int l_whence; /* 파일의 절대적 위치 */
    __off_t l_start;    /* 파일의 offset */ 
    __off_t l_len;      /* 잠그고자 하는 파일의 길이 */
    __pid_t l_pid;      /* 잠그을 얻은 프로세스의 pid */
};
flock.l_type 는 F_RDLCK, 와 F_WRLCK, F_UNLCK 3가지로 세팅가능하다. F_RDLCK와 F_WRLCK 는 읽기와 쓰기전용으로 잠금이 걸려있음을 뜻하며, F_UNLCK 는 잠금이 되어 있지 않음을 나타낸다.

l_whence, l_start, l_len 은 잠금할 레코드의 위치와 크기를 지정하기 위해서 사용한다. l_whence 는 파일에서의 절대적위치이며, l_start 는 l_whence 에서 이동한 거리 즉 offset 이며, l_len 는 잠금할 레코드의 크기이다. 예를 들어서 l_whence 가 SEEK_SET 이고 l_start 가 16이라면 레코드의 위치는 처음 + 16 이므로 16번째 가 된다. l_len 이 16 이라고 가정하면, 레코드 잠금을 위해서 가르키는 레코드 블럭은 16 에서 32 사이에 있는 데이타 블럭이 될것이다.

fd 는 조작하기 위한 파일 기술자를 나타내며, cmd 는 조작명령을 나타낸다. cmd 는 여러가지 조작명령을 포함하고 있는데, 레코드 잠금을 위해서 사용하는 명령어들은 다음과 같은것들이 있다.

F_SETLK flock 구조체에 설정된 잠금을 얻거나, 혹은 잠금을 풀기 위해서 사용된다. 프로세스는 특정 영영의 잠금을 검사해서 잠금을 사용할수 있으면, 잠금을 얻게 되고, 영역에 대한 작업이 끝나면 잠금을 풀어서 다른 프로세스가 사용가능하도록 만든다. 만약 다른 프로세스가 이미 잠금을 얻은 등의 이유로 잠금을 얻을수 없으면, -1을 반환하고 EACCESS 혹은 EAGAIN을 설정한다. 잠금을 검사하는 용도로도 사용할 수 있다.

F_SETLKW F_SETLK 와 같은 일을하지만, 에러를 리턴하는데신 잠금이 풀릴때까지 해당영역에서 기다린다(block). F_SETLK 의 봉쇄형이다.

F_GETLK 잠금이 있는지 없는지 검사를한다. 만약 잠금이 없다면 flock.l_type 를 F_UNLCK로 설정한다. 만약 잠금이 있다면, 현재의 flock 정보를 flock 구조체로 돌려준다.

파일잠그기

fcntl 을 이용한 파일잠그기와 레코드 잠그기의 방법은 동일하다. 파일(:12)을 잠그고자 한다면, 파일을 열때 fcntl 을 이용해서 특정바이트(보통 파일의 첫바이트)를 잠그고, 이 파일을 열고자 하는 다른 모든 프로세스는 약속된 바이트를 검사해서 잠겨있다면 풀릴때까지 기다리거나, 리턴한다.

레코드 잠금은 단지 이러한 검사를 레코드 단위별로 나눠서 한다는것만 다를 뿐이다.

다음은 fcntl 을 이용해서 파일잠금을 이용한 예이다. 이 프로그램은 세마포어의 사용에 나오는 예제 sem_test.c 을 fcnlt 버젼으로 재 작성한 것이다.

예제: fcntl_test.c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int fd_lock(int fd);
int fd_open(int fd);
int fd_isopen(int fd);

int main()
{
    int fd;
    int n_read;
    char buf[11];
    char wbuf[11];

    fd = open("counter.txt", O_CREAT|O_RDWR); 
    if (fd == -1)
    {
        perror("file open error : ");
        exit(0);
    }
    if (fd_isopen(fd) == -1)
    {
        perror("file is lock "); 
        exit(0);
    }
    printf("I get file lock\n");

    memset(buf, 0x00, 11);
    memset(wbuf, 0x00, 11);
    if ((n_read = read(fd, buf, 11)) > 0)
    {
        printf("%s\n", buf); 
    }

    // 처음위치로 되돌린다. 
    lseek(fd, 0, SEEK_SET);
    sprintf(wbuf, "%d", atoi(buf) + 1);
    write(fd, wbuf, 11);
    
    // 숫자외의 필요없는 부분을 자른다.  
    ftruncate(fd, strlen(wbuf)); 
    // 10 초를 쉰다. 
    sleep(10);

    // 파일잠김을 푼다. 
    if (fd_unlock(fd) == -1)
    {
        perror("file unlock error ");
    }
    printf("file unlock success\n");
    sleep(5);

    close(fd);
}

// 파일이 잠겨있는지 확인하고 잠겨 있지 않으면
// 잠금을 얻고 
// 잠겨 있을경우 잠김이 풀릴때까지 기다린다(F_SETLKW) 
int fd_isopen(int fd)
{
    struct flock lock;

    lock.l_type = F_WRLCK; 
    lock.l_start = 0;
    lock.l_whence = SEEK_SET;
    lock.l_len = 0;

    return fcntl(fd, F_SETLKW, &lock);
}

// 파일 잠금을 얻은후 모든 작업이 끝난다면 
// 파일 잠금을 돌려준다. 
int fd_unlock(int fd)
{
    struct flock lock;

    lock.l_type = F_UNLCK; 
    lock.l_start = 0;
    lock.l_whence = SEEK_SET;
    lock.l_len = 0;

    return fcntl(fd, F_SETLK, &lock);
}
테스트 하기전에 counter.txt 파일을 하나 만들고 내용은 '1' 로 한다음에 저장하자. 위의 예제는 counter 를 증가시키는 프로그램이다. counter.txt 에 대해서는 여러개의 프로세스가 동시에 접근이 가능하므로 한번에 하나의 프로세스만 접근해서 counter 을 증가 시키도록 파일 잠금을 해야 한다. 이전의 "세마포어의 사용"에서는 세마포어를 이용해서 구현했는데, 이번에는 fcntl 을 이용해서 구현했다. 세마포어 버젼에 비해서 코드가 한결간단해 졌음을 알수 있다.

프로그램은 파일의 내용을 읽어들여서 읽어들인 문자를 숫자로 변환한다음 1을 더한후 다시 저장하게 된다. 숫자를 읽어서 저장하는 도중에 다른 프로세스가 끼어들면 안되므로 파일을 열기전에 fcntl 을 이용해서 잠금을 얻고, 파일을 닫기전에 잠금을 풀도록 했다.

레코드 잠그기

기본적으로 파일잠그는 방법과 동일하다. 단지 파일의 특정 범위에 대해서만 잠금을 허용하는게 다르다.

테스트를 위해서 셈플 프로그램을 만들 것이다. 셈플 프로그램은 다중 프로세스 카운터로써 하나의 파일에 여러 개의 프로세스가 자신에게 할당된 레코드에 카운팅하는 일을 한다. 이것은 아마도 가장 간단한 형태의 db시스템이 될것이다. 잠금과 풀기를 할때 레코드의 영역을 쉽게 정의 하기 위해서 하나의 카운터 블럭은 16 바이트로 고정할 것이고, 카운터 숫자이외의 영역은 널로 채워질 것이다.

파일은 위와 같은 내용을 가지게 될것이다. 프로세스가 1번째 counter 데이타에 접근할때 굳이 1번부터 16번까지 전부다 잠글필요는 없으므로 1번만 잠그도록 한다. 그러면 프로세스가 1번째 counter 데이타에 접근중일때 다른 프로세스는 접근을 하지 못할것이다. 그렇지만 2번째와 3번째 counter 데이타에는 접근이 가능할것이다.

먼저 예제 프로그램이 사용할 counter 파일을 만들겠다. 아래는 counter 파일을 만들기 위한 예제 프로그램이다. 카운터 파일의 이름은 b_counter로 했다.

예제: mkcount.c
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BLOCK_SIZE 16
int main()
{
    int fd;
    int i;
    char buf[BLOCK_SIZE];
    if ((fd = open("b_counter", O_CREAT|O_WRONLY)) == -1)
    {
        perror("file open error : ");
        exit(0);
    }
    for(i = 1; i < 4; i++)
    {
        memset(buf, 0x00, BLOCK_SIZE);
        sprintf(buf,"%d", i);
        write(fd, buf, BLOCK_SIZE);
    }    
    close(fd);
}
위 코드를 실행시키면 b_counter 이란 파일을 만들어낸다. od(:12)명령을 이용 counter 정보가 제대로 만들어졌는지 확인했다.
[root@localhost test]# od -c b_counter 
0000000   1                                             
0000020   2                                             
0000040   3                                             
0000060
[root@localhost test]# 
다음은 위의 b_counter를 조작하는 프로그램이다.

예제: fcntl_counter.c
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

#define BLOCK_SIZE 16
int fd_isopen(int fd, int data_index);
int fd_unlock(int fd, int data_index);
int main(int argc, char **argv)
{
    int fd;
    int n_read;
    int data_index;
    char buf[BLOCK_SIZE];
    char wbuf[BLOCK_SIZE];

    if (argc < 2)
    {
        printf("Usage: ./fcntl_counter.c [데이타번호]\n");
        exit(0);
    }

	// 데이타의 위치를 찾음
    data_index = BLOCK_SIZE * (atoi(argv[1]) - 1); 

    fd = open("b_counter", O_RDWR);
    if(fd == -1)
    {
        perror("file open error : ");
        exit(0);
    }

    // 해당 데이타를 포함한 레코드 영역을 
    // 데이타 위치에서 부터 BLOCK_SIZE 크기 만큼 잠금
    if (fd_isopen(fd, data_index) == -1)
    {
        perror("file is lock ");
        exit(0);
    }

    // 파일내에서 조작할 데이타 위치로 이동
    // 해서 counter 정보를 읽어들임
    lseek(fd, data_index, SEEK_SET);
    memset(buf, 0x00, BLOCK_SIZE);
    if ((n_read = read(fd, buf, BLOCK_SIZE)) < 0)
    {
        perror("read error : ");
        exit(0);
    }    

    memset(wbuf, 0x00, BLOCK_SIZE);
    sprintf(wbuf, "%d", atoi(buf)+1);
    printf("count %s -> %s\n", buf, wbuf);

    
    // 파일내에서 조작할 데이타 위치로 이동해서
    // counter 정보를 씀
    lseek(fd, data_index, SEEK_SET);
    write(fd, wbuf, BLOCK_SIZE);

    sleep(10);

    // 잠금을 해제함
    if (fd_unlock(fd, data_index) == -1)
    {
        perror("file unlock error ");
        exit(0);
    }
    close(fd);
}

int fd_isopen(int fd, int data_index)
{
    struct flock lock;

    lock.l_type = F_WRLCK;
    lock.l_start = data_index;
    lock.l_whence = SEEK_SET;
    lock.l_len = 1;

    return fcntl(fd, F_SETLKW, &lock);
}

int fd_unlock(int fd, int data_index)
{
    struct flock lock;

    lock.l_type = F_UNLCK;
    lock.l_start = data_index;
    lock.l_whence = SEEK_SET;
    lock.l_len = 1;

    return fcntl(fd, F_SETLK, &lock);
}

이 예제는 fcntl_test.c 를 약간 수정해서 작성했다. fcntl_test.c 에서 레코드 잠금을 블럭단위로 지정할수 있도록 fd_isopen, fd_unlock 함수를 약간 수정했으며, 실지로 counting 할수 있도록 만들었다.

테스트 방법은 간단하다. ./fcntl_counter 은 하나의 아규먼트를 가질수 있으며, 아규먼트는 각 데이타에 접근하기 위한 인덱스 값을 가진다. 즉 첫번째 데이타에 접근하기 위해서는 1, 2번재는 2, 3번째는 3 이런식으로 아규먼트를 주면 된다. 그러면 프로그램은 해당 데이타의 값을 읽어들이고 +1을 시켜준다음에 저장한다. 이때 해당 데이타블럭은 한번에 하나의 프로세스만 접근할수 있도록 "레코드잠금" 을 사용한다.

이 응용은 파일잠금이 아니므로 하나의 프로세스가 첫번재 데이타에 접근할때, 파일전체가 잠기지 않고 해당 레코드 영역만 잠근다. 그러므로 해당 레코드에 대한 접근만 할수 없으며 다른 데이타 블럭에는 접근가능하다. ./fcntl_counter 을 여러가지 방법으로 테스트 해보면, 파일잠금과 레코드 잠금의 특징을 이해할수 있을것이다.

멀티 쓰레드 프로그램에서의 레코드 잠금

pthread 구현은 fcntl을 이용한 레코드 잠금을 공유하지 않는다. 즉 각각의 쓰레드가 독립된 프로세스인 것처럼, 배타적으로 레코드를 잠글 수 있다.