본문 바로가기
Study/System Programming

lseek, pread, pwrite 사용하기

by 개발새-발 2021. 11. 17.
반응형

lseek을 이용하면 파일의 offset을 조작하여 이후 offset이 설정된 위치부터 read, write를 할 수 있게 해준다. 즉, 원하는 곳에서부터 읽고 쓸 수 있게 된다는 것이다.

lseek

주어진 file descriptor의 file offset을 조작한다.

#include <unistd.h>

off_t lseek(int fd, off_t pos, int whence);
  • lseek
    • 인자
      • int fd : 조작할 파일과 관련된 file descriptor다.
      • off_t pos : 이동할 file offset의 위치이다. 정확한 위치는 whence의 값에 따라 달라진다.
      • int whence : SEEK_SET, SEEK_END, SEEK_SET 중 하나이다.
    • 반환값
      • 새로 설정된 offset을 반환한다. 실패한 경우 -1을 반환한다.

whence 값에 따른 lseek의 작동

  • SEEK_SET : offset이 pos로 설정된다.
  • SEEK_END : offset이 파일의 크기 + pos로 설정된다.
  • SEEK_CUR : offset이 현재 offset + pos로 설정된다.

lseek 예제

아래와 같이 1, 2, 3, 4, 5가 10개씩 연속으로 나열된 파일이 있다.

11111111112222222222333333333344444444445555555555

lseek으로 파일의 원하는 부분을 읽고 써보자.

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

#define BUF_SIZE 10

int main(void){
    char buffer[BUF_SIZE+1];

    int fd = open("file1.txt",O_RDWR);

    // example 1 : first of file
    off_t offset = lseek(fd,0,SEEK_SET); // to first of file
    printf("offset 1 : %ld\n",offset);

    int bytes = read(fd,buffer,BUF_SIZE);
    buffer[bytes]=0;
    puts(buffer);

    // example 2 : SEEK_SET
    offset = lseek(fd,5,SEEK_SET);
    printf("offset 2 : %ld\n",offset);

    bytes = read(fd,buffer,BUF_SIZE);
    buffer[bytes]=0;
    puts(buffer);

    // example 3 : next byte after last byte of file
    offset = lseek(fd,0,SEEK_END); 
    printf("offset 3 : %ld\n",offset);

    // example 4
    offset = lseek(fd,-15,SEEK_END);
    printf("offset 4 : %ld\n",offset);

    bytes = read(fd,buffer,BUF_SIZE);
    buffer[bytes]=0;
    puts(buffer);

    // example 5 : SEEK_CUR
    offset = lseek(fd,30,SEEK_SET);
    printf("offset 5 : %ld\n",offset);
    offset = lseek(fd,-3,SEEK_CUR);
    printf("offset 6 : %ld\n",offset);

    bytes = read(fd,buffer,BUF_SIZE);
    buffer[bytes]=0;
    puts(buffer);

    // example 6 : lseek with SEEK_CUR to get current offset
    printf("offset 7 : %ld\n", lseek(fd,0,SEEK_CUR));
    write(fd,"000",3);
    printf("offset 8 : %ld\n", lseek(fd,0,SEEK_CUR));

    return 0;
}

출력은 다음과 같다.

offset 1 : 0
1111111111
offset 2 : 5
1111122222
offset 3 : 50
offset 4 : 35
4444455555
offset 5 : 30
offset 6 : 27
3334444444
offset 7 : 37
offset 8 : 40

실행 후 파일은 다음과 같이 수정되었다.

11111111112222222222333333333344444440005555555555

각 부분을 천천히 보자.

example 1

// example 1 : first of file
off_t offset = lseek(fd,0,SEEK_SET); // to first of file
printf("offset 1 : %ld\n",offset);

int bytes = read(fd,buffer,BUF_SIZE);
buffer[bytes]=0;
puts(buffer);
offset 1 : 0
1111111111

lseek(fd,0,SEEK_SET);으로 파일 offset은 파일의 맨 처음으로 이동하게 된다. 사실 파일을 연 이후 아무것도 하지 않았기 때문에 이전에도 offset은 0이다.

example 2

// example 2 : SEEK_SET
offset = lseek(fd,5,SEEK_SET);
printf("offset 2 : %ld\n",offset);

bytes = read(fd,buffer,BUF_SIZE);
buffer[bytes]=0;
puts(buffer);
offset 2 : 5
1111122222

lseek(fd,5,SEEK_SET)으로 offset이 5가 되었다. 출력에서도 offset의 위치부터 출력하는 것을 볼 수 있다.

example 3

// example 3 : next byte after last byte of file
offset = lseek(fd,0,SEEK_END); 
printf("offset 3 : %ld\n",offset);
offset 3 : 50

lseek(fd,0,SEEK_END)로 인해 offset은 파일의 끝을 가리키고 있다. SEEK_END는 offset을 파일의 끝 + pos의 값으로 정하게 하는데, pos가 0 임으로 파일의 끝을 가리키게 된 것이다. 여기서 파일의 끝은 마지막 바이트인 '5'가 아니라 그다음의 null 문자인 것을 볼 수 있다.

example 4

// example 4
offset = lseek(fd,-15,SEEK_END);
printf("offset 4 : %ld\n",offset);
bytes = read(fd,buffer,BUF_SIZE);
buffer[bytes]=0;
puts(buffer);
offset 4 : 35
4444455555

lseek(fd,-15,SEEK_END);로 offset은 파일의 끝에서 15byte 앞선 부분을 가리킨다.

example 5

// example 5 : SEEK_CUR
offset = lseek(fd,30,SEEK_SET);
printf("offset 5 : %ld\n",offset);
offset = lseek(fd,-3,SEEK_CUR);
printf("offset 6 : %ld\n",offset);

bytes = read(fd,buffer,BUF_SIZE);
buffer[bytes]=0;
puts(buffer);
offset 5 : 30
offset 6 : 27
3334444444

lseek(fd,30,SEEK_SET);로 offset을 30으로 바꾸었다. 이후 lseek(fd,-3,SEEK_CUR);로 지금 offset의 위치에서 -3 이동한 위치를 가리키는 것을 볼 수 있다.

example 6

// example 6 : lseek with SEEK_CUR to get current offset
printf("offset 7 : %ld\n", lseek(fd,0,SEEK_CUR));
write(fd,"000",3);
printf("offset 8 : %ld\n", lseek(fd,0,SEEK_CUR));
offset 7 : 37
offset 8 : 40

lseek(fd,0,SEEK_CUR) 으로 file descriptor의 현재 offset을 알 수 있다. 이 경우 ftell과 같은 기능을 하는 것이다. 그런데, 6번째 offset 출력값은 27인데 7번째 출력값은 37인 것을 볼 수 있다. 이는 read를 수행하여 읽은 byte 수만큼 offset이 변하였기 때문이다. 이는 write도 마찬가지이다. offset이 37일 때 3개의 바이트를 '0'으로 수정함으로 '4' 3개가 '0'이 된 것을 볼 수 있다. 또, 3만큼 offset이 증가한 것도 확인할 수 있다.

Race condition from lseek, read, write

lseek과 read, write를 혼용하는 방법은 여러 프로세스 혹은 쓰레드에서 파일에 접근하는 경우 위험하다. offset값은 각각의 프로세스마다 따로 존재하는 것이 아니라 global file table의 값으로 존재한다. 즉, 여러 프로세스가 이 file table의 동일한 entry에 접근하는 경우가 존재하게 된다. 이 경우에 race condition 이 발생할 수 있다. Process A는 lseek(fd,30,SEEK_SET); 이후 read를 수행하고 프로세스 B는 lseek(fd,0,SEEK_SET); 이후 write를 수행한다고 하자. 만약 두 프로세스의 file descriptor가 file table의 동일 entry를 가리킨다면 아래 같은 경우가 발생할 수 있다. 아래의 순서로 작업이 진행된다고 하자.

1) 프로세스 A : lseek(fd,30,SEEK_SET); 수행, 수행 완료
2) 프로세스 A -> 프로세스 B (Context Switching)
3) 프로세스 B : lseek(fd,0,SEEK_SET); 수행, 수행 완료
4) 프로세스 B : write 수행, 완료
5) 프로세스 B -> 프로세스 A (Context Switching)
6) 프로세스 A : read 수행. 이때 read는 몇 번째 byte부터 읽는가?

우리는 프로세스 A 이 30번째 바이트부터 read를 하기를 원했다. 프로세스 B가 offset을 0으로 바꾸고 write 작업을 수행하였기 때문에 table entry의 offset은 0 + write가 끝난 지점이 된다. 결국 프로세스 A는 이 지점부터 read를 하게 되어 의도한 곳이 아닌 전혀 다른 곳을 read 하게 된다.

pread, pwrite

위의 위험을 피하고자 이 두 system call을 사용할 수 있다. file table의 entry 값에 offset과는 무관하게 동작하며, 이 두 call은 atomic 하다. 이 둘은 pos를 입력으로 받는다는 점을 제외하면 read, write와 사용법이 같다.

#include <unistd.h>

ssize_t pread(int fd, void *buffer, size_t count, off_t pos);
ssize_t pwrite(int fd, const void *buffer, size_t count, off_t pos);
  • pread

    • 인자
      • int fd : file descriptor이다
      • void* buffer : 읽은 결과물을 받을 buffer이다.
      • size_t count : 읽을 byte 수이다.
      • off_t pos : 파일의 몇 번째 byte부터 읽을지 정한다.
    • 반환 값
      • 읽은 바이트 수를 반환한다.
      • EOF인 경우 0을 반환한다.
      • 실패할 때 -1을 반환하고 errno를 설정한다.
  • pwrite

    • 인자
      • int fd : file descriptor이다
      • const void* buffer : write 할 내용이 담긴 buffer이다.
      • size_t count : write 할 byte 수이다.
      • off_t pos : 파일의 몇 번째 byte부터 쓰기 시작할지 정한다.
    • 반환 값
      • 쓴 바이트 수를 반환한다.
      • 실패할 때 -1을 반환하고 errno를 설정한다.

pread, pwrite 예제

위 예제와 동일한 파일에 대해 pread, pwrite를 사용해본다.

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

#define BUF_SIZE 10

int main(void){
    char buffer[BUF_SIZE+1];

    int fd = open("file1.txt",O_RDWR);

    off_t offset = lseek(fd,0,SEEK_CUR);
    printf("offset = %ld\n",offset);

    // pread
    int bytes = pread(fd,buffer,BUF_SIZE,35);
    printf("in buffer : %s\n",buffer);

    offset = lseek(fd,0,SEEK_CUR);
    printf("Offset after pread = %ld\n",offset);

    // pwrite
    bytes = pwrite(fd,"0000000000",10,15);

    offset = lseek(fd,0,SEEK_CUR);
    printf("Offset after pwrite = %ld\n",offset);

    close(fd);
}
$ ./prdwr
offset = 0
in buffer : 4444455555
Offset after pread = 0
Offset after pwrite = 0
$ cat file1.txt
11111111112222200000000003333344444444445555555555$

pread와 pwrite를 사용하여 원하는 부분의 파일을 읽고 쓸 수 있다는 것을 보았다. 또, 이는 file table 내의 entry의 offset 값을 변하게 하지 않는다는 것도 볼 수 있었다.

반응형

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

open, read, write, close 사용하기  (0) 2021.11.15
wait, waitpid의 사용  (0) 2021.10.19
fork 사용하기  (0) 2021.10.17
sigprocmask을 사용하여 signal block 하기  (0) 2021.10.16

댓글