7. 파일 탐색하기 - lseek()
System/System Programming

7. 파일 탐색하기 - lseek()


개요


파일 입출력을 좀 더 재미있게 할 수 있는 방법은 없을까? 


  파일 입출력을 할 때 무료함을 느낀다면, 그건 파일 입출력 오프셋 위치 때문일 것이다. 지금까지 알아보았던 방식 (write() 시스템 콜이나 O_APPEND 플래그를 사용하는 것)으로는 항상 파일의 시작이나 끝에서밖에 입출력을 하는 것에서 그쳤다. 하지만 중간에 다른 데에서부터 입출력을 하고 싶을 때도 있다. 이를 위해서 파일 탐색 방법을 알아두어야 할 것이다.


lseek() 시스템 콜


  이번에 배울 lseek()파일 디스크립터에 연결된 파일의 오프셋을 특정 값으로 지정할 수 있게 해주는 시스템 콜이다. 사실 이 기능을 제외하고는 다른 동작은 하지 않으며, 어떤 입출력도 하지 않는다. 단지 파일 오프셋만 옮길 뿐이다.

1
2
3
4
#include <sys/types.h>
#include <unistd.h>
 
off_t lseek(int fd, off_t pos, int origin);
cs


  lseek() 시스템 콜의 반환 자료형인 off_t 자료형은 long int 형이라고 생각하면 이해하기 쉽다. size_t, ssize_t처럼 <sys/types.h>에 정의된 시스템 자료형이다.


  fd는 파일 시스템 콜이 사용할 파일 디스크립터, pos는 얼마만큼 움직일 것인지에 대한 정수값, origin은 lseek()의 동작 방식을 정해 준다. origin 인자는 크게 세 가지 방법으로 사용할 수 있는데, SEEK_CUR, SEEK_END, SEEK_SET이다.


 SEEK_CUR



여기서 앞으로 몇 칸이나 더 가실까요?


  메모장이나 블로그의 텍스트 에디터 등을 사용할 때 원하는 곳에 커서를 위치시키면 그 자리에서부터 입력할 수 있다는 것을 우리는 아주 잘 알고 있다. SEEK_CUR는 말 그대로 파일 입출력 시에 이러한 커서 역할을 해 준다. 다시 한 번 lseek() 시스템 콜의 원형을 봐 보자.


1
off_t lseek(int fd, off_t pos, int origin);
cs


  origin에 SEEK_CUR를 집어넣으면, 파일 디스크립터의 파일 오프셋을 현재 오프셋에서 pos만큼 더한 값으로 설정해 준다.



  예를 들어 이름이 "TEXT"인 파일에 "Welcome to VALLHALLA!" 라는 문자열이 들어가 있다고 가정해 보자. 여기서 우리는 VALLHALLA 대신 CONDENCIA로 바꾸어 버리고 싶다. 그러기 위해서는 공백을 포함했을 때 12번째 문자인 'V' 바로 앞에 파일 오프셋을 위치시켜줄 필요가 있다. 여기서 바로 write()를 해버린다면 앞에 있던 "Welcome to"는 사라져 버리고 "CONDENCIA"라는 단어만 덜렁 남게 될 것이다. 이는 곧 오프셋이 파일의 맨 처음(0)에 있다는 말과 같다. 아래는 실제로 문자열을 바꾸는 기능을 수행하는 코드와 그의 실행 결과이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
int main() {
    int fd = open("TEXT", O_RDWR | O_CREAT, 0666);
    if(fd == -1) perror("Failed to open file.\n");
    off_t ret;
 
    ret = lseek(fd, (off_t)11, SEEK_CUR);
    if(ret == (off_t) -1) perror("Failed lseek().\n");
    else printf("Successed lseek()!\n");
 
    const char *buf = "CONDENCIA";
    ssize_t nr = write(fd, buf, strlen(buf));
    if(nr == -1) perror("Failed to write buf.\n");
    else printf("Successed writing!\n");
 
    return 0;
}
cs




  성공적으로 "CONDENCIA"를 집어넣었다! 오프셋을 얼마만큼 움직이는지 나타내는 pos 값은 음수, 0, 양수 등 정수라면 어떤 값이건 들어갈 수 있다. 양수 값이라면 예제와 같이 앞으로 가고, 음수면 뒤로 간다. 0이면 현재 파일 오프셋을 반환한다.



SEEK_END


  


  SEEK_END는 SEEK_CUR와 조금 비슷하다. 다른 것은 파일 오프셋을 현재 오프셋에서 pos를 더한 값이 아니라 현재 파일 크기에서 pos를 더한 값으로 설정한다는 것이다.


 


  방금 사용했던 TEXT 파일에는 현재 Welcome to CONDENCIA! 라는 문자열이 저장되어 있다. 이제 이 뒤에 Do you know where PERIWINKLE ANGEL is? 라고 추가로 기입하고 싶다. 현재 파일 오프셋은 맨 마지막의 느낌표(!) 앞에 와 있다. 앞에 있던 문장 뒤에 또 다른 문자열을 쓰려면 파일 오프셋을 파일의 끝으로 설정할 필요가 있다.물론 O_APPEND 플래그를 사용할 수도 있겠지만, 파일 오프셋을 이리저리 바꾸어야 할 때에는 lseek()를 사용하는 것이 편할 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
int main() {
    int fd = open("TEXT", O_RDWR | O_CREAT, 0666);
    if(fd == -1) perror("Failed to open file.\n");
    off_t ret;
 
    ret = lseek(fd, 0, SEEK_END);
    if(ret == (off_t) -1) perror("Failed lseek().\n");
    else printf("Successed lseek()!\n");
 
    const char *buf = "Do you know where PERIWINKLE ANGEL is?\n";
    ssize_t nr = write(fd, buf, strlen(buf));
    if(nr == -1) perror("Failed to write buf.\n");
    else printf("Successed writing!\n");
 
    return 0;
}
cs



  SEEK_END를 사용하는 lseek()에서 pos를 0으로 설정하면 파일 오프셋을 현재 파일의 끝으로 설정하는 것을 볼 수 있었다. 마찬가지로 음수나 양수 값이 들어가도 된다.


SEEK_SET


  지금까지는 현재 파일의 끝이나 현재 오프셋에서 pos값을 더하는 식으로 오프셋을 이리저리 움직여 보았다. 하지만 특정 위치로 파일 오프셋을 옮기고 싶을 때가 있을 것이다. SEEK_SET은 파일 디스크립터의 파일 오프셋을 pos 값으로 설정한다. pos가 0이면 파일의 처음으로 설정된다.


  아까 문자열을 추가했던 TEXT 파일이다. 현재 오프셋은 파일 끝에 있는데, 여기서 ANGEL을 DEVIL로 바꾸려 한다. ANGEL의 'A'는 TEXT 파일에 있는 52번째 문자이므로, ANGEL을 DEVIL로 바꾸려면 51로 파일 오프셋을 옮겨 놓아야 한다. 사실 이 경우엔 SEEK_CUR를 사용하는 사용하는 게 더 빠르긴 한데... 일단 실습이 목적이니까 제쳐두자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
int main() {
    int fd = open("TEXT", O_RDWR | O_CREAT, 0666);
    if(fd == -1) perror("Failed to open file.\n");
    off_t ret;
 
    ret = lseek(fd, (off_t)51, SEEK_SET);
    if(ret == (off_t) -1) perror("Failed lseek().\n");
    else printf("Successed lseek()!\n");
 
    const char *buf = "DEVIL";
    ssize_t nr = write(fd, buf, strlen(buf));
    if(nr == -1) perror("Failed to write buf.\n");
    else printf("Successed writing!\n");
 
    return 0;
}
cs




파일 끝을 넘어서 탐색하기




난 파일 안에 갇혀 있을 순 없어! 밖으로 나가버릴 방법 없을까?



  물론 가능하다! 파일 오프셋을 파일 끝을 넘어서도록 지정하면 된다.


1
2
3
int ret;
 
ret = lseek(fd, (off_t) 1688, SEEK_END);
cs


  파일 끝을 넘어서도록 위치를 지정하는 것만으로는 아무런 일도 발생하지 않는다. 갱신된 오프셋에서 데이터를 읽어들이면... EOF를 반환한다. 하지만 쓰기 요청이 들어오면 파일의 마지막 오프셋과 새로운 오프셋 사이에 공간이 만들어지며 0으로 채워진다.


  UNIX 계열 파일시스템에서 이 사이의 공간, 즉 구멍은 물리적인 디스크 공간을 차지하지 않는다. 따라서 파일 시스템에서 모든 파일을 합친 크기가 물리적 디스크 크기보다 더 클 수 도 있다! 이렇게 구멍이 생긴 파일을 성긴 파일(spare file)이라고 하는데, 성긴 파일들은 공간을 상당히 절약하며 효율을 크게 높일 수 있다. 구멍을 다루는 과정에서 물리적인 입출력 작업이 필요하지 않기 때문이다.


  구멍에 읽기 요청을 하면 적절한 개수만큼 0으로 채워진 값을 반환한다.



에러 값


  에러가 발생하면 -1을 반환하고 errno가 다음 네 가지 값 중 하나로 설정된다.


에러 코드

내용

EBADF

 주어진 파일 디스크립터가 열린 파일 디스크립터가 아니다.

EINVAL

 origin 값이 SEEK_SET, SEEK_CUR, SEEK_END 중 하나가 아니거나,

 새로운 오프셋 값이 음수가 되는 경우이다.[각주:1]

EOVERFLOW

 탐색 결과로 반환해야 할 새 오프셋이 off_t 값으로 표현되지 못한다.[각주:2]

ESPIPE

 파일 디스크립터가 오프셋 지정이 불가능한 객체[각주:3]와 연결되어 있다.


제약 사항


  파일 오프셋의 최댓값은 off_t의 크기[각주:4]에 제한된다. 대부분의 아키텍처에서는 off_t를 C의 long 타입으로 정의하며, 리눅스에서는 항상 워드 크기[각주:5]가 된다. 하지만 커널은 내부적으로 오프셋 값을 C의 long long 타입으로 저장한다. 이러한 타입 크기 문제는 64bit에서는 문제가 되지 않지만, 32bit 머신에서는 EOVERFLOW 에러가 발생할 수 있다.


  1. 전자는 거의 확실히 컴파일 과정에서 에러를 낼 것이고, 후자는 실행 중 모르는 새에 논리 에러로 나타날 것이다. [본문으로]
  2. 32bypte 아키텍처에서만 나타나는 에러. 에러가 발생하더라도 파일 오프셋은 갱신되며 단지 새로 갱신된 오프셋을 반환하지 못한다는 사실을 알려준다. [본문으로]
  3. 파이프, FIFO, 소켓 등 [본문으로]
  4. long int, –2,147,483,648 ~ 2,147,483,647 [본문으로]
  5. 일반적으로 기계의 범용 레지스터 크기 [본문으로]

'System > System Programming' 카테고리의 다른 글

8. 지정한 위치 읽고 쓰기 - pread(), pwrite()  (0) 2019.04.29
6. 파일 닫기 - close()  (0) 2019.04.24
5. 직접 입출력  (0) 2019.04.24
4. 동기식 입출력 - fsync(), fdatasync()  (2) 2019.04.22
3. 파일 쓰기 - write()  (0) 2019.04.16