학습자료(~2017)/리눅스

[리눅스] 파일 락에 관한 좋은 블로그- 글펌

단세포소년 2012. 1. 27. 10:12
반응형


출처 : http://blog.naver.com/PostView.nhn?blogId=hgh73&logNo=90116412714

 

 

================================================================


파일 잠금

파일 잠금은 다중 사용자,, 멀티태스킹 운영 시스템에서 매우 중요한 부분이다. 프로그램은 종종 파일을
통하여 데이터를 공유하며, 이러한 프로그램들이 파일을 제어하는 몇 가지 방식을 채택하는 것이
매우 중요하다.

파일 잠금을 통해 프로그램은 안전한 방식으로 파일을 갱신할 수 있으며, 혹은 이미 다른 프로그램에서
기록중인 상태인 파일에서 데이터를 읽으려는 시도를 방지할 수 있다.

리눅스는 파일 잠금을 위해 사용할 수 있는 몇 가지 기능이 있는데, 가장 간단한 방법은 원자적인
방식으로 잠금 파일을 만드는 기술이다.

즉, 잠금이 만들어지는 동안에 는 어떠한 일도 발생할 수 없다.
이것을 통해 프록램은 유일성이 보장된 파일을 생성할 수 있으며, 다른 프로그램이 동일한
파일을 생성할 수 없도록 할 수 있다.

두번째 방법은 좀더 진보적인데, 프로그램은 독점 액세스를 위해 파일의 일부를 잠글 수 있다.
이러한 방식의 잠금을 수행하기 위한 두 가지 방법이 있는데, 프로그래밍 인터페이스 만 약간 다르고
매우 비슷하므로 한 가지만 상세하게 알아보자.

잠금 파일 만들기

많은 응용 프로그램이 ㄹ;소스(보호) 를 위해 잠금 파일을 만들려고 한다. 그러면 다른 프로그램은
리소스에 액세스할 수 있는지를 확인하기 위해 파일을 점검할 수 있다.

일반적으로 이러한 잠금 파일들은 제어되는 리소스에 관련된 이름으로 특정 위치에 있다. 예를 들어
모뎀이 사용중일 경우, 리눅스는 흔히 사용하는 디렉토리인 /usr/spool 혹은 /var/spool
디렉토리에 잠금 파일을 만든다.

잠금 파일은 단순히 지시자의 역할을 수행한다는 것에 주목하자. 잠금 파일을 사용하기 위해서는
프로그램들이 서로 협동해야  한다.

시스템이 잠금의 동작을 집행하는 필수 잠금과는 반대로, 이러한 권고 잠금(advisor lock)이라고 칭한다.

잠금 지시자로 사용할  파일을 만들기 위해 O-CREAT와 O_EXCL 플래그 를 지정하며, fcntlh(이전
장에서 다룬 적이 잇다)에 정의된 open 시스템 호출을 사용한다. 이 함수는 파일이 존재하는지 점검하여,
단일, 원자 연산으로 파일을 만든다.

lock1.c를 통해 작동을 살펴보자.

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

int main()
{
    int file_desc;
    int save_errno;

    file_desc = open("/tmp/LCK.test", O_RDWR | O_CREAT | O_EXCL, 0444);
    if (file_desc == -1) {
        save_errno = errno;
        printf("Open failed with error %d\n", save_errno);
    }
    else {
        printf("Open succeeded\n");
    }
    exit(EXIT_SUCCESS);
}

프로그램을 처음 실행하면 결과는 다음과 같다.

gildang@gildang ~/build $ ./lock1
Open succeeded

또 한 번 시도하면 다음과 같은 결과가 나온다.

gildang@gildang ~/build $ ./lock1
Open failed with error 17
gildang@gildang ~/build $

프로그램은 /tmp/LCK.test라는 이름의 파일을 만들기 위해, O_CREAT와 O_EXCL 플래그를 사용하여
open을 호출한다.

프로그램을 처음 실행하면, 파일이 존재하지 않기 때문에 open은 성공한다.

프로그램은 또 한 번 호출하면 파일이 이미 존재하기 때문에 실패한다. 프로그램이 성공하려면
잠금 파일이 지워져야 한다.

리눅스 시스템에서 에러 17은 파일이 이미 존재함을 가리키는 EXIST를 의미한다.
에러 번호는 헤더 파일 errno.h 혹은 이 파일이 흔히 포함하는 파일에 정의되어 있다.

앞의 경우에는 /usr/include/asm/errno.h에 다음과 같이 정의 되어 있다.

#define EEXIST    17 /* 파일이 존재한다 */

이것은 open(O_CREAT | O_EXCL) 실패에 대한 적절한 에러이다.

프로그램이 단순히 실행하는 짧은 기간동안 리소스를 독점해야 한다면, 즉 전문 용어로 임계 영역
(critical section)이 필요하다면, 임게 영역에 진입하기 전에 잠금 파일을 만들고 임계 영역을
빠져나와서 unlink를 지워주어야 한다.

샘플 프로그램을 작성하고 두 개의 프로세스를 동시에 실행하여, 여러 프로그램이 어떻게 이 잠금
매커니즘으로 협력하는지 볼 수 있다.

4장에서 살펴본 getpid를 사용할 것이다. 이 함수는 현재 실행하고 있는 프로그램에 대한
유일한 숫자인 프로세스 식별자를 반환한다.

다음은 테스트 프로그램 lock2.c의 소스이다.

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

const char *lock_file = "/tmp/LCK.test2";

int main() {
    int file_desc;
    int tries = 10;

    while (tries--) {
        file_desc = open(lock_file, O_RDWR | O_CREAT | O_EXCL, 0444);
        if (file_desc == -1) {
            printf("%d - Lock already present\n", getpid());
            sleep(3);
        }
        else {
                /* critical region */
임계 영역은 요기에서 시작한다.
            printf("%d - I have exclusive access\n", getpid());
            sleep(1);
            (void)close(file_desc);
            (void)unlink(lock_file);
                /* non-critical region */
            sleep(2);
        }
    } /* while */
    exit(EXIT_SUCCESS);
}

프로그램을 실행하기 위해 , 다움 명령을 사용하여 잠금 파일이 존재하지 않음을 확인하자.

rm -f /tmp/LCK.test2

다음 명령을 사용하여 두 개의 프로세스를 실행한다.



gildang@gildang ~/build $ rm -f /tmp/LCK.test2
gildang@gildang ~/build $ ./lock2 & ./lock2
[1] 4051
4051 - I have exclusive access
4052 - Lock already present
4051 - I have exclusive access
4052 - Lock already present
4051 - I have exclusive access
4052 - Lock already present
4052 - I have exclusive access
4051 - Lock already present
4051 - I have exclusive access
4052 - Lock already present
4052 - I have exclusive access
4051 - Lock already present
4051 - I have exclusive access
4052 - Lock already present
4052 - I have exclusive access
4051 - Lock already present
4052 - Lock already present
4051 - I have exclusive access

앞의 예제는 동일한 프로그램의 두 프로세스가 서로 협동하는 모습을 보여준다. 직접 실행해 보면
다름 프로세스 식별자가 출력됨을 보게 될 것이다.

하지만 프로그램의 동작은 같을 것이다.

데모를 위해 while 문을 10번 반복하도록 프로그램을 만들었다. 프로그램은 유일한 잠금 파일
/tmp/LCK.test2를 만들어서 임계 리소스에 액세스를 시도한다.

파일이 이미 존재하여 이 시도가 실패하면, 프로그램은 잠깐 기다린 후 다시 시도한다.

성공하면 앞에서 임계 영역이라고 이야기한 리소스에 액세스할 수 있으며, 단독으로
액세스가 필요한 어떤 작업이라도 수행할 수 있다.

이것은 단지 데모이기 때문에 대기 시간이 짧다. 프로그램이 리소스 사용을 마치면 잠금 파일을
지워서 잠금을 해지한다.

이후 잠금을 다시 획득하기 전에 다른 작업(이 경우에 는 sleep 함수)을 수행한다.
잠금 파일은 바이너리 세마포어(binary semafore)처럼 작동하며, 각 프로그램은 "리소스를
사용할 수 있는가?"의 질문에 "예" 혹은 "아니오"의 대답만을 제공한다.

14장에서 세마포어에 관하여 자세히 알아볼 것이다.

잠금 영역

잠금 파일을 만드는 것은 직렬 포트와 같은 리소스에 대한 독점 액세스를 제어하기 위해 좋은
방식이다.

하지만 큰 공유 파일에 대한 액세스를 제어하기 위해서는 그다지 좋지 않다. 한 프로그램
이 작성하고 다른 여러 프로그램이 동시에 갱신하는 큰 파일이 있다고 가정하자. 프로그램이
장시간 동안 지속되는 데이터를 로그로 기록하고,

다른 여러 프로그램이 이 로그를 처리하는 상황이다. 처리 프로그램은 로그를 기록하는
프로그램이 종료하기를 기다릴 수 없으며 계속 실행해야 한다.

그러므로 동일한 파일에 동시 액세스할 수 있도록 협동하는 방법이 필요하다.

파일의 특정 부분만 잠그고 다른 프로그램이 파일의 다른 부분에는 액세스할 수 있도록 파일의
잠금 영역을 제공하여 이러한 상황을 해결할 수 있다.

이것을 파일 - 세그먼트 잠금 혹은 퍼일 - 영역 잠금 이라고 부른다.

리눅스는 (적어도) 두 가지 방법을 제공하는데, 하나는 fcntl 시스템 호출을 사용하는 것이고, 다른 하나는
lockf 호출을 사용하는 것이다.

fcntl 인터페이스가 가장 흔히 사용되는 인터페이스이므로 이것을 중점적으로 살펴보자.

lockf는 상당히 비슷하며, 리눅스에서 fcntl에 대한 대체 인터페이스로 사용된다.
fctnl과 lockf 잠금 매커니즘은 함게 작동하지는 않는다.

이 두 함수는 다른 기반 구현을 사용하기 때문에 두 가지 방식을 혼합해서는 안 되며, 오직 한 가지를
사용해야 한다.

3장에서 fcntl을 본 적이 있다. 이 함수의 정의는 다음과 같다.

#include <stdio.h>

int fcntl (int fildes, int command, ...);

fcntl은 열린 파일 설명자에 작동하며, command 매개변수에 따라 다른 작업을 수행할 수 있다.
다음은 파일 잠금에 대한 세 가지 명령이다.

F_GETLK
F_SETLK
F_SETLKW

이 명령들을 사용할 때, 세번째 인자는 struct flock에 대한 포인터이여야 한다. 그러므로 프로토타입
은 실제로 다음과 같다.

int fcntl(int fildes, int command, struct flock *flock_structure);

flock(file lock) 구조체는 구현에 종속적이지만, 적어도 다음과 같은 멤버를 포함하고 있다.

short l_type;
short l-whence;
off_t l_start;
off-t l_len;
pid_t l_pid;

l_type 멤버는 fcntl.h에 정의된 몇 가지 값 중에 하나를 가진다. 사용할 수 있는 값은 다음 표에 나타
나 있다.

값 설명

F_RDLCK 공유(혹은 "읽기") 잠금. 여러 다른 프로세스가 파일과 같은 (혹은 겹치는 ) 영역에 대한
공유 잠금을 가질 수 있다.

어떤 프로세스가 공유 잠금을 가진다면, 다른 프로세스는 그 영역에 대해 독점 잠금을 가질 수 없다.
공유 잠금을 획득하려면, 읽기 혹은 읽기/쓰기 액세스로 파일을 열어야 한다.

F_ULINK
잠금 해지

F_WRICK
독점(혹은 "쓰기") 잠금. 오직 하나의 프로세스만이 파일의 특정 영역에 대해 독점 잠금
을 가질 수 있다.

일단 프로세스가 잠금을 획득하고 나면, 어떠한 프로세스도 그 영역에 대한 어떠한 잠금도
가질 수 없다.
독점 잠금을 획득하려면, 쓰기 혹은 읽기/쓰기 액세스로 파일을 열어야 한다.

l_whence 멤버는 파일에서 연속된 바이트의 집합 영역을 정의한다. l_whence는 SEEK_SET,
SEEK_CUR, SEEK_END(unistd.h에 정의되어 있다) 중에 하나가 되어야 한다.

각각 파일의 시작, 파일의 현재 위치, 파일 끝에 해당한다.

l_whence 는 영역의 첫 바이트인 l_start로부터의 상대적인 옵셋을 정의한다.

보통 이 값으로 SEEK_SET을 많이 쓰는데, 이때 l_start는 파일의 처음부터 계산된다.

l_len 매개변수는 영역에 있는 바이트 의 수를 정의한다.

l_pid 매개변수는 잠금을 가지고 있는 프로세스를 보고할 때 사용된다. 다음의 F_GETLK 설명을
보자.

파일의 각 바이트는 한번에 단 하나의 잠금 형식을 가질 수 있는데, 공유 액세스로 잠긴 경우 , 독점
액세스로 잠긴 경우, 잠금이 해지된 경우 중 하나가 된다.

fctnl 호출에 사용할 수 있는 명령과 옵션의 몇 가지 조합이 있다. 하나씩 알아보자.

F_GETLK 명령

첫번째 명령은 F_GETLK이다. 이 명령은 fildes(첫번째 매개변수)가 열어놓은 파일에 관한 잠금
정보를 구하고 파일을 잠그지는 않는다.

호출 프로세스는 만들고자 하는 잠금의 형식에 관한 정보를 전달하며, F_GETLK 명령을 실행하는
fcntl 은 어떤 정보를 반환하며 잠금이 실제로 일어나는 것을 방지한다.

다음 표는 flock 구조체에 사용되는 값들이다.

l_type 공유(읽기 전용) 잠금을 위한 F_LOCK 혹은 독점 (쓰기) 잠금을 위한 F_WRICK
둘 중에 하나가 될 수 있다.

l_whence SEEK_SET,, SEEK_CUR, SEEK_END 중에 하나

l_start 잠금 파일 영역의 바이트 수

l_pid 잠금을 가진 프로세스의 식별자

프로세스는 파일 잠금 영역의 현재 상태를 결정하기 위해 F_GETLK 호출을 사용한다. 이 호출은 자신이
회득한 잠금의 형식을 가리키고 잠금 영역을 정의하도록 flock 구조체를 설정해야 한다.

fctnl 호출은 성공하면 -1이 아닌 값을 반환한다. 파일이 이미 존재하여 잠금 요청이 성공하지 못할 예정이라면
관련 정보로 flock 구조체를 덮어쓴다.

잠금이 성공할 예정이면, flock 구조체는 변하지 않는다.

F_GETLK 호출이 정보를 구할 수 없다면 실패를 가리키기 위해 -1을 반환한다.

F_GETLK 호출이 성공하면 (-1이 아닌 값을 반환하면 ) 호출 응용 프로그램은 flock 구조체의
내용을 점검하여 변경되었는가 확인한다.

l_pid 값이 잠금 프로세스(잠금  프로세스가 발견되면)로 설정하되었는가의 여부를 점검하여
flock 구조체가 변경되었는지 편리하게 획인할 수 있다.

F_SETLK 명령

이 명령은 fildes가 참조하는 파일의 부분을 잠그거나 잠금을 해지한다. flock 구조체에 사용되는
값(F_GETLK)에 사용된 값들과 다르다)은 다음과 같다.

l_type F_RDLCK는 읽기 전용 혹은 공유 잠금을 위한 값이며, F_WRLCK는 독점, 쓰기,
잠금을 위한 값이며, F_ULINK는 영역의 잠금을 해지하기 위한 값이다)

l_pid 사용되지 않는다.

잠금이 성공적이면 fcntl은 -1이 아닌 값을 반환하고 , 실패하면 -1을 반환한다.
함수는 항상 즉시 반환한다.

F_SETLKW 명령

F_SETLKW 명령은 앞에서 살펴본 F_SETLK 명령과 같은데, 함수가 잠금을 획득할 수 없으면 가능할
때까지 기다린다는 차이점이 있다.

일단 이 함수가 기다리기 시작하면 , 잠금을 획득하거나 신호가 발생할 때에만 반환한다.

11장에서 신호에 대해서 알아볼 예정이다.

프로그램이 파일에 대해 가지고 있는 모든 잠금은 관련 파일 설명서가 닫히면 자동으로 해지된다.
물론 프로그램이 종료해도 마찬가지이다.

잠금과 함께 읽고 쓰기

파일의 영역에 대해 잠금을 사용할 때, 파일의 데이터에 액세스하기 고수준 fread 와 fwrite
보다 저수준 read와 write를 사용하는 것이 매우 중요하다.

fread와 fwrite는 라이브러리에서 읽거나 쓰는 데이터의 버퍼링을 수행하기 때문에,
파일의 처음 100 바이트를 읽기 위해 fread를 실행하면 100 바이트 이상을 읽고
나머지 데이터를 라이브러리 버퍼에 저장한다(사실 항상 그러하다)
프로그램이 다음 100바이트를 읽기 위해 fread를 사용하면, 실제로 는 아미 라이브러리 버퍼에
저장된 데이터를 읽고 파일로부터 더 맣은 데이터를 가져오기 위해 저수준 read를
호출하지 않는다.

이것이 왜 문제인지 이해하기 위해, 같은 파일을 갱신하는 두 개의 프로그램을 생각해 보자.

파일이 200 바이트의 데이터로 구성되어 있고 모두 0이라고 가정하자. 첫번째 프로그램이 먼저 시작하고
파일의 처음 100바이트에 쓰기 잠금을 회득한다.
그런 다음 fread를 사용하여 이 100바이트를 읽는다.

하지만 이전 장에서 보았듯이 fread는 한번에 BUFSIZE 만큼을 미리 읽어놓는다. 그러므로 실제로눈
전체 파일을 메모리로 읽지만 처음 100바이트만 프로그램에 전달하는 것이다.

두번째 프로그램이 시작하고 프로그램의 두번째 100바이트에 대해 쓰기 잠금을 획득한다. 첫번째
프로그램이 처음 100바이트에 대해서만 잠금을 획득하였기 때문에 이 작업은 성공할 수 있다.

두번째 프로그램이 100에서 199바이트까지 2를 쓰고, 파일을 닫고 잠금을 해지하고 종료한다. 첫번째
프로그램은 파일의 두번째 100바이트에 대해서 잠금을 획득하고 fread를 획득하고
fread를 호출하여 읽는다.

데이터 가 버퍼에 저장되어 있기 때문에, 프로그램이 실제로 보는 것은 0으로 된 100바이트이지
실제 파일에 존재하는 2로 된 100바이트가 아니다.

이문제는 read와 write를 사용하면 일어나지 않는다.

파일 잠금의 이러한 과정이 복잡하게 보이지만, 사용하는 것보다 설명을 하는 것이 실제로
더욱 어렵다.

파일 잠금이 어떻게 작동하는지 알기 위해 lock3.c 예제를 보자. 잠금을 실습하기 위해 서 두 개의
프로그램이 필요하다. 하나는 잠금을 수행하는 프로그램이고 다른 하나는 테스트하는
프로그램이다.

헤더 파일을 포함하고 변수를 선언하는 것으로 시작하자.

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

const char *test_file = "/tmp/test_lock";

int main() {
    int file_desc;
    int byte_count;
    char *byte_to_write = "A";
    struct flock region_1;
    struct flock region_2;
    int res;


파일 설명자를 연다.

file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
    if (!file_desc) {
        fprintf(stderr, "Unable to open %s for read/write\n", test_file);
        exit(EXIT_FAILURE);
    }

파일에 데이터를 쓴다.

/* put some data in the file */
    for(byte_count = 0; byte_count < 100; byte_count++) {
        (void)write(file_desc, byte_to_write, 1);
    }

영역 1을 10바이트에서 30바이트까지 공유 잠금으로 설정한다.

  /* setup region 1, a shared lock, from bytes 10 -> 30 */
    region_1.l_type = F_RDLCK;
    region_1.l_whence = SEEK_SET;
    region_1.l_start = 10;
    region_1.l_len = 20;
   
영역 2를 40바이트에서 50바이트까지의 독점 잠금으로 설정한다.

/* setup region 2, an exclusive lock, from bytes 40 -> 50 */
    region_2.l_type = F_WRLCK;
    region_2.l_whence = SEEK_SET;
    region_2.l_start = 40;
    region_2.l_len = 10;

파일을 잠근다.

 /* now lock the file */
    printf("Process %d locking file\n", getpid());
    res = fcntl(file_desc, F_SETLK, &region_1);
    if (res == -1) fprintf(stderr, "Failed to lock region 1\n");
    res = fcntl(file_desc, F_SETLK, &region_2);
    if (res == -1) fprintf(stderr, "Failed to lock region 2\n");   

잠시 기다린다.

 /* and wait for a while */
    sleep(60);

    printf("Process %d closing file\n", getpid());   
    close(file_desc);
    exit(EXIT_SUCCESS);
}


프로그램은 먼저 파일을 생성하고, 읽기와 쓰기를 위해 열고, 파일에 데이터 를 쓴다.
이후 두 영역 을 설정하는데 , 첫번째 영역은 10바이트에서 30바이트까지의
공유(읽기) 잠금이고 , 두번째 영역은 40바이트에서 50바이트까지의 독점(쓰기)
잠금이다.

fcntl 을 호출하여 두 개의 영역을 잠그고 잠시 기다린 다음 파일을 받고 종료한다.


[그림 7-1]은 프로그램이 시작하고 기다릴 때의 잠금의 상황을 보여준다.

프로그램 자체만 보면 그다지 유용하지 않다. 잠금을 테스트할 두번째 프로그램인
lock4.c 가 필요하다.

파일의 다른 영역에 대해 다른 종류의 잠금을 테스트하는 프로그램을 작성해 보자.

평소와 같이 헤더 파일을 포함하고 변수를 선언하는 것으로 시작하자.

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


const char *test_file = "/tmp/test_lock";
#define SIZE_TO_TRY 5

void show_lock_info(struct flock *to_show);


int main() {
    int file_desc;
    int res;
    struct flock region_to_test;
    int start_byte;
   
        /* open a file descriptor */
    file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
    if (!file_desc) {
        fprintf(stderr, "Unable to open %s for read/write", test_file);
        exit(EXIT_FAILURE);
    }

    for (start_byte = 0; start_byte < 99; start_byte += SIZE_TO_TRY) {
            /* set up the region we wish to test */
        region_to_test.l_type = F_WRLCK;
        region_to_test.l_whence = SEEK_SET;
        region_to_test.l_start = start_byte;
        region_to_test.l_len = SIZE_TO_TRY;
        region_to_test.l_pid = -1;    

        printf("Testing F_WRLCK on region from %d to %d\n",
               start_byte, start_byte + SIZE_TO_TRY);
       
            /* now test the lock on the file */
        res = fcntl(file_desc, F_GETLK, &region_to_test);
        if (res == -1) {
            fprintf(stderr, "F_GETLK failed\n");
            exit(EXIT_FAILURE);
        }
        if (region_to_test.l_pid != -1) {
            printf("Lock would fail. F_GETLK returned:\n");
            show_lock_info(&region_to_test);
        }
        else {
            printf("F_WRLCK - Lock would succeed\n");
        }

            /* now repeat the test with a shared (read) lock */
       
            /* set up the region we wish to test */
        region_to_test.l_type = F_RDLCK;
        region_to_test.l_whence = SEEK_SET;
        region_to_test.l_start = start_byte;
        region_to_test.l_len = SIZE_TO_TRY;
        region_to_test.l_pid = -1;    

        printf("Testing F_RDLCK on region from %d to %d\n",
               start_byte, start_byte + SIZE_TO_TRY);
       
            /* now test the lock on the file */
        res = fcntl(file_desc, F_GETLK, &region_to_test);
        if (res == -1) {
            fprintf(stderr, "F_GETLK failed\n");
            exit(EXIT_FAILURE);
        }
        if (region_to_test.l_pid != -1) {
            printf("Lock would fail. F_GETLK returned:\n");
            show_lock_info(&region_to_test);           
        }
        else {
            printf("F_RDLCK - Lock would succeed\n");
        }

    } /* for */
   
    close(file_desc);
    exit(EXIT_SUCCESS);
}

void show_lock_info(struct flock *to_show) {
    printf("\tl_type %d, ", to_show->l_type);
    printf("l_whence %d, ", to_show->l_whence);
    printf("l_start %d, ", (int)to_show->l_start);       
    printf("l_len %d, ", (int)to_show->l_len);
    printf("l_pid %d\n", to_show->l_pid);
}

평소와 같이 헤더 파일을 포함하고 변수를 선언하는 것으로 시작하자.

파일 설명자를 연다.

테스트할 영역을 설정한다.

파일에 대한 잠금을 테스트한다.

공유(읽기) 잠금으로 테스트를 반복한다. 테스트할 영역을 다시 설정한다.

파일에 대한 잠금을 다시 테스트한다.

잠금을 테스트하려면 먼저 lock3 프로그램을 실행하고 잠겨진 파일에 대해 테스트하는 lock
프로그램을 실행한다.
다음 명령을 사용하여 lock3를 백그라운드로 실행한다.

gildang@gildang ~/build $ gcc -o lock4 lock4.c
gildang@gildang ~/build $ ./lock3 &
[2] 15974
gildang@gildang ~/build $ Process 15974 locking file
Failed to lock region 2

lock3이 백그라운드로 실행하기 때문에 명령 프롬프트는 반환하고, 다음 명령으로 lock4 프로그램을 즉시
실행한다.

$ ./lock4

간략함을 위해 약간 정리하면 다음과 같은 출력을 볼 수 있다.

gildang@gildang ~/build $ ./lock4
Testing F_WRLCK on region from 0 to 5
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 0 to 5
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 5 to 10
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 5 to 10
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 10 to 15
Lock would fail. F_GETLK returned:
    l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 15306
Testing F_RDLCK on region from 10 to 15
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 15 to 20
Lock would fail. F_GETLK returned:
    l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 15306
Testing F_RDLCK on region from 15 to 20
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 20 to 25
Lock would fail. F_GETLK returned:
    l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 15306
Testing F_RDLCK on region from 20 to 25
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 25 to 30
Lock would fail. F_GETLK returned:
    l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 15306
Testing F_RDLCK on region from 25 to 30
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 30 to 35
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 30 to 35
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 35 to 40
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 35 to 40
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 40 to 45
Lock would fail. F_GETLK returned:
    l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 15306
Testing F_RDLCK on region from 40 to 45
Lock would fail. F_GETLK returned:
    l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 15306
Testing F_WRLCK on region from 45 to 50
Lock would fail. F_GETLK returned:
    l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 15306
Testing F_RDLCK on region from 45 to 50
Lock would fail. F_GETLK returned:
    l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 15306
Testing F_WRLCK on region from 50 to 55
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 50 to 55
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 55 to 60
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 55 to 60
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 60 to 65
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 60 to 65
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 65 to 70
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 65 to 70
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 70 to 75
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 70 to 75
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 75 to 80
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 75 to 80
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 80 to 85
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 80 to 85
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 85 to 90
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 85 to 90
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 90 to 95
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 90 to 95
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 95 to 100
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 95 to 100
F_RDLCK - Lock would succeed
gildang@gildang ~/build $ Process 15974 closing file

[2]-  완료                  ./lock3
gildang@gildang ~/build $

파일의 5바이트 단위의 각 그룹에 대해서 잠금을 테스트하기 위해, lock4는 영역(region) 구조체를
설정하고 이 영역에 대해 읽기 잠금 혹은 쓰기 잠금을 회득할 수 있는지 확인한다.

반환된 정보는 영역 바이트, 바이트 0에서부터의 옵셋, 잠금 요청이 실패할 예정인지에 대한 정보이다.
반환된 구조체의 l_pid 부분은 현재 잠금 파일을 가진 프로세스의 식별자이며,
프로그램은 이 부분을 -1(유효하지 않은 값)로 설정하여
fcntl 호출이 반환하였을 때 이 값이 변화하였는지 여부를 알려준다.

영역이 현재 잠겨지지 않았다면 l_pid는 변하지 않는다.

앞 출력의 의미를 이해하려면,  헤더 파일 fctl.h (보통 /usr/include/fcntl.h)를 살펴봐야 한다.
l_type의 값 1이 F_WRICK가 1로 정의된 것에서부터 왔고, l_type의 값 0이 F_RDLCK가 0으로
정의된 것으로부터 왔음을 알 수 있다.

그러므로 l_type의 값 1은 기존 읽기 잠금 때문임을 의미하다.
lock3이 잠그지 않은 파일의 영역에서는 , 공유 잠금과 독점 잠금이 모두 성공할 것이다.

10바이트에서 30바이트까지는 lock3 프로그램이 독점 잠금이 아니라 공유 잠금을 획득하였기
때문에 , 두 가지 형식의 잠금 모두 실패한다.

경쟁 잠금

파일에 대해 기존 잠금을 테스트하는 법을 알았으므로, 두 프로그램이 파일의 같은 섹션에 대한
잠금을 경쟁할 때 어떤 일이 일어나는지 보자.

lock3 프로그램을 사용하여 먼저 파일을 잠그고, 새 프로그램이 다시 잠금을 시도할 것이다.
예제를 완성시키기 위해 잠금을 해지하는 코드도 추가할 것이다.

다음 lock5.c 프로그램은 이미 잠겨진 파일 영역을 잠그려고 시도한다. 이 프로그램은
lock4.c와는 달리 파일의 다른 부분에 대해 잠금 상태를 테스트하지 않는다.

#include를 작성하고, 변수를 선언한 다음, 파일 설명자를 연다.

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


const char *test_file = "/tmp/test_lock";

int main() {
    int file_desc;
    struct flock region_to_lock;
    int res;

        /* open a file descriptor */
    file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
    if (!file_desc) {
        fprintf(stderr, "Unable to open %s for read/write\n", test_file);
        exit(EXIT_FAILURE);
    }

    region_to_lock.l_type = F_RDLCK;
    region_to_lock.l_whence = SEEK_SET;
    region_to_lock.l_start = 10;
    region_to_lock.l_len = 5;
    printf("Process %d, trying F_RDLCK, region %d to %d\n", getpid(),
           (int)region_to_lock.l_start, (int)(region_to_lock.l_start + region_to_lock.l_len));
    res = fcntl(file_desc, F_SETLK, &region_to_lock);
    if (res == -1) {
        printf("Process %d - failed to lock region\n", getpid());
    } else {
        printf("Process %d - obtained lock region\n", getpid());
    }
  
    region_to_lock.l_type = F_UNLCK;
    region_to_lock.l_whence = SEEK_SET;
    region_to_lock.l_start = 10;
    region_to_lock.l_len = 5;
    printf("Process %d, trying F_UNLCK, region %d to %d\n", getpid(),
           (int)region_to_lock.l_start, (int)(region_to_lock.l_start + region_to_lock.l_len));
    res = fcntl(file_desc, F_SETLK, &region_to_lock);
    if (res == -1) {
        printf("Process %d - failed to unlock region\n", getpid());
    } else {
        printf("Process %d - unlocked region\n", getpid());
    }

    region_to_lock.l_type = F_UNLCK;
    region_to_lock.l_whence = SEEK_SET;
    region_to_lock.l_start = 0;
    region_to_lock.l_len = 50;
    printf("Process %d, trying F_UNLCK, region %d to %d\n", getpid(),
           (int)region_to_lock.l_start, (int)(region_to_lock.l_start + region_to_lock.l_len));
    res = fcntl(file_desc, F_SETLK, &region_to_lock);
    if (res == -1) {
        printf("Process %d - failed to unlock region\n", getpid());
    } else {
        printf("Process %d - unlocked region\n", getpid());
    }
   
    region_to_lock.l_type = F_WRLCK;
    region_to_lock.l_whence = SEEK_SET;
    region_to_lock.l_start = 16;
    region_to_lock.l_len = 5;
    printf("Process %d, trying F_WRLCK, region %d to %d\n", getpid(),
           (int)region_to_lock.l_start, (int)(region_to_lock.l_start + region_to_lock.l_len));
    res = fcntl(file_desc, F_SETLK, &region_to_lock);
    if (res == -1) {
        printf("Process %d - failed to lock region\n", getpid());
    } else {
        printf("Process %d - obtained lock on region\n", getpid());
    }

    region_to_lock.l_type = F_RDLCK;
    region_to_lock.l_whence = SEEK_SET;
    region_to_lock.l_start = 40;
    region_to_lock.l_len = 10;
    printf("Process %d, trying F_RDLCK, region %d to %d\n", getpid(),
           (int)region_to_lock.l_start, (int)(region_to_lock.l_start + region_to_lock.l_len));
    res = fcntl(file_desc, F_SETLK, &region_to_lock);
    if (res == -1) {
        printf("Process %d - failed to lock region\n", getpid());
    } else {
        printf("Process %d - obtained lock on region\n", getpid());
    }
   
    region_to_lock.l_type = F_WRLCK;
    region_to_lock.l_whence = SEEK_SET;
    region_to_lock.l_start = 16;
    region_to_lock.l_len = 5;
    printf("Process %d, trying F_WRLCK with wait, region %d to %d\n", getpid(),
           (int)region_to_lock.l_start, (int)(region_to_lock.l_start + region_to_lock.l_len));
    res = fcntl(file_desc, F_SETLKW, &region_to_lock);
    if (res == -1) {
        printf("Process %d - failed to lock region\n", getpid());
    } else {
        printf("Process %d - obtained lock on region\n", getpid());
    }

    printf("Process %d ending\n", getpid());   
    close(file_desc);
    exit(EXIT_SUCCESS);
}

먼저 프로그램은 10바이트에서 15바이트까지 영역에 공유 잠금을 획득한다. 이 영역은 이미 공유
잠금으로 잠겨져 있으며 , 동시에 여러 공유 잠금이 만들어질 수 있기 때문에 잠금은 성공한다.

그런 다음 그 영역에 대한 (자체적으로 소유한) 공유 잠금을 성공적으로 해지한다.
프로그램은 파일의 처음 50바이트에 대해 잠금을 해지하지만, 이 영역은 어떠한 잠금도 설정되어 있지
않다.

이 프로그램이 처음에는 어떠한 잠금도 가지지 않았다고 하더라도, 잠금 해지 요청의 최종 결과로
처음 50바이트에 대해 이 프로그램은 어떠한 잠금도 가지고 있지 않게 된다.

그러므로 이 작업 또한 성공한다.

다음, 프로그램은 16바이트에서 21바이트까지 독점 잠금으로 영역을 잠그기를 시도한다. 이 영역도
이미 공유 잠그믕로 잠겨져 있기 때문에 독점 잠금은 만들어질 수 없다.

그러므로 이번에 새 잠금은 실패한다.

마지막으로 프로그램은 16바이트에서 21바이트까지의 영역에 독점 잠금을 획득하는 것을
시도하지만, 이번에는 F_SETLCK 명령을 사용하여 잠금을 획득할 수 있을 때까지 기다린다.

이때 그 영역에 대해 잠금을 획득한 lock3 프로그램이 파일을 닫아서 사전에 획득한 모든 잠금을
해지할 때 출력은 장시간 정지하게 된다.

lock5 프로그램은 실행을 재개하고 성공적으로 영역을 잠그고 종료한다.

다른 잠금 명령

파일을 잠그기 위한 두번째 방법은 lockf 함수를 사용하는 것이다. 이 함수도 역시 파일 설명자를
사용하여 작동하며, 다음과 같은 프로토타입을 가진다.

#include <,unistd.h>

int lockf(int fildes, int function, off_t size_to_lock);

function 값은 다음과 같은 값들이 될 수 있다.

F_UNLOCK : 잠금 해지
F_LOCK : 독점 잠금
F_TLOCK : 테스트하고 독점 잠금
F_TEST : 다른 프로세스가 잠금을 획득하고 있는지 테스트한다.

size_to_lock 매개변수는 파일의 현재 옵셋부터 몇 바이트까지 잠금을 수행할지 가리키는 숫자이다.

lockf는 fcntl 인터페이스보다 간단한 인터페이스를 가지고 있는데, 다소 적은 기능과 유연성을
가지고 있기 때문이다.

함수를 사용하려면 잠그길 원하는 영역의 시작 지점까지 파일을 이동한 다음, 잠그고자 하는 바이트의
수를 전달하며 함수를 호출해야 한다.


fcntl을 사용하여 파일을 잠그는 방법과 같이, 모든 잠금은 오직 권고적이며, 실제로 파일로부터
읽기와 스기를 금지하지는 않는다.

잠금을 점검하는 것은 프로그래머의 책임이다.

fcntl 잠금과 lockf 를 혼합하여 사용하는 효과는 정의되어 있지 않기 때문에, 어떤 형식의 잠금을
선택하여 꾸준히 사용할 것인지 결정해야 할 것이다.

교착 상태

교착 상태의 위험성에 대해 이야기하기 전에는 잠금에 대한 공부를 마무리할 수 없다. 두 개의 프로그램이
같은 파일을 갱신할길 원한다고 가정하자.

이 두 프로그램은 모두 바이트 1과 바이트 2를 동시에 갱신해야 한다. 프로그램 A는 바이트 2를 갱신한 다음,
바이트 1을 갱신해야 한다.

프로그램 A는 바이트 2를 갱신한 다음, 바이트 1을 갱신하기로 선택하였다. 프로그램 B는 바이트 1을 먼저
갱신하고 바이트 2를 갱신할 것이다.

두 프로그램은 동시에 시작한다. 프로그램 A는 바이트 2를 잠그고 프로그램 B는 바이트 1을 잠근다.
프로그램 A는 바이트 1에 대한 잠금을 시도한다.

이 부분은 프로그램 B에 의해 잠겨져 있기 때문에 프로그램 A는 기다린다.
프로그램 B는 바이트 2에 대한 잠금을 시도한다. 이 부분은 프로그램 A에 의해
잠겨져 있기 때문에 프로그램 B도 역시 기다린다.

두 프로그램 더 이상 진행할 수 없는 이 상황을 교착 상태(deadlock 혹은 deadly embrace)라고 부른다.
데이터베이스 응용프로그램과 같이 다수의 사용자들이 같은 데이터에 자주 액세스하고
시도하는 경우에 흔히 발생하는 문제이다.

대부분의 상용 관계형 데이터베이스 는 교착 상태를 검출하고 자동으로 해결하지만, 리눅스 커널은
그렇지 않다.

강제로 한 프로그램을 종료하는 것과 같은 어떤 외부 의 간섭이 교착 상태의 불안정함을 해결하기 위해 필요하다.

프로그래머는 이 상황을 반드시 알아야 한다.
잠금을 기다리는 여러 프로그램이 있다면, 교착 상태가 일어나지 않도록 매우 조심해야 한다.
이 예제에서는 교착 상태를 피하기가 매우 쉽다.

두 프로그램이 단순히 같은 순서로 잠금을 획득하거나 잠금을 위해 더 큰 영역을 사용한다.

반응형