[Linux_System_programming] 04 고급파일 입출력

Linux System Programming (Robert Love) chapter 04 Advanced File I/O summary

벡터 입출력

  • 벡터 입출력은 한번의 시스템콜을 사용해서 여러개의 버퍼 벡터에 쓰거나 여러 개의 버퍼 벡터로 읽어 들일때 사용
  • 2장의 표준 읽기 쓰기는 선형 입출력이라 하며 이에 비해 다음의 장점이 있다.
    • 여러 필드에 걸쳐 데이터가 분리되어 있다면 직관적인 방법으로 조작 가능
    • 여러번의 선형입출력 대체 가능하므로 효율적
    • 시스템콜의 횟수를 줄이고 선형입출력 구현에 비해 최적화 되어 효율적
    • 선형 입출력과 대조적으로 벡터 입출력 연산은 원자성을 보장

readv()와 writev()

1
2
3
4
5
6
7
8
9
#include <sys/uio.h>

struct iovec{
  void* iov_base; //버퍼의 시작포인터
  size_t iov_len; //버퍼의 크기(바이트)
};

size_t readv(int fd, const struct iovec* iov, int count);
size_t writev(int fd, const struct iovec* iov, int count);
  • readv()는 파일 디스크립터 fd에서 데이터를 읽어서 count 개수만큼 iov버퍼에 저장
  • writev()는 count 개수의 iov버퍼 데이터를 파일 디스크립터 fd에 씀
  • iovec구조체는 세그멘트라고 하는 독립적으로 분리된 버퍼, 세그멘테이션 집합을 벡터라 부름
  • readv(), writev()는 성공 시 읽거나 쓴 바이트 개수 반환, 에러 발생 시 -1을 반환 후 errno를 설정

epoll

  • 커널 2.6에서 poll과 select의 한계를 극복하기 위한 epoll 도입
    • poll과 select가 실행시 마다 전체 파일 디스크립터를 요구하는 문제, epoll은 실제 검사할 파일디스크립터를 등록 부분을 분리

epoll 인스턴스 생성

1
2
3
4
#include <sys/epoll.h>

int epoll_create1(int flag)
// epoll_create는 구식 방법임
  • epoll 인스턴스 생성 및 그 인스턴스와 연관된 파일 디스크립터를 반환, 에러 발생시 -1 반환 후 errno를 설정
  • flag는 EPOLL_CLOSEXEC만 유효, 새 프로세스가 실행될 때 이 파일을 자동적으로 닫아 줌
  • epoll_create1에서 반환되는 파일 디스크립터는 사용 완료후 close()로 닫아 줄 것

epoll 제어

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <sys/epoll.h>

struct epoll_event {
  __u32 event;
  union {
    void* ptr;
    int fd;
    __u32 u32;
    __u64 u64;
  } data;
}

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epoll context에 파일 디스크립터를 추가하거나 삭제할때 사용, 호출이 성공하면 0을 반환, 실패시 -1을 반환 후 errno를 설정
  • epfd : 호출이 성공시 epoll 인스턴스는 epfd 와 연결
  • op : 파일드스크립터 fd가 가리키는 파일에 대한 작업을 명시
    • EPOLL_CTL_ADD 감시 추가, EPOLL_CTL_DEL 감시 삭제, EPOLL_CTL_MOD 감시 이벤트 갱신
  • epoll_event 구조체 event 필드는 감시할 이벤트 목록으로 OR연산으로 묶을 수 있음
    • EPOLLIN 지연되지 않고 읽기 가능 감시, EPOLLOUT 지연되지 않고 파일 쓰기 가능 감시, EPOLLPRI 읽어야할 OOB데이터 존재 여부 감시
    • EPOLLERR 에러 상황 감시(default), EPOLLHUP 행업 감시(default)
    • EPOLLET 감시 시 에지 트리거 사용, EPOLLONESHOT 한번 만 감시(다시 활성화 하려면 EPOLL_CTL_MOD로 다시 설정 필요)
  • epoll_evnet 구조체 data 필드는 사용자를 위한 필드로 이벤트가 발생해서 사용자에게 반환될때 함께 반환, 보통 data.fd를 fd로 채워씀

epoll 이벤트 대기

1
2
3
#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epoll_wait()를 호출하면 timeout 밀리 초 동안 epfd와 연관된 파일의 이벤트 대기, 호출이 성공 시 발생 이벤트 갯수를 실패시 -1을 반환 후 errno를 기록
  • 호출 성공 events에 해당 이벤트를 기록, 최대 maxevent만큼의 이벤트 기록
  • timeout시 반환값음 0

에지 트리거와 레벨트리거

  • 트리거 종류, 당신이 생가하는 그거

메모리에 파일 매핑

mmap()

1
2
3
#include <sys/mman.h>

void* mmap(void* addr, size_t len, int prot, int flags, int fd, off_t offset);
  • mmap()을 호출하면 파일디스크립터 fd가 가리키는 파일의 offset위치에서 len 바이트만큼 메모리에 매핑하도록 요청
  • addr이 포함되면 메모리에서 해당주소를 선호한다고 커널에 알림(단지 제안일 뿐)
  • mmap()은 성공시 맵핑의 실제 시작주소를 반환, 실패 시 MAP_FAILED 반환 후 errno를 설정
  • prot인자는 맵핑에 원하는 메모리 보호 정책을 명시, OR 연산으로 묶어서 설정 가능
    • PROT_NONE 접근불가(거의 미사용), PROT_READ 읽기 가능, PROT_WRITE 쓰기가능, PROT_EXEC 실행가능
  • flags 인자에는 맵핑의 유형 및 동작 요소
    • MAP_FIXED : addr인자를 제안이 아닌 요구사항으로 취급, 커널이 해당주소를 확보 못할 시 호출 실패
    • MAP_PRIVATE : 맵핑 미공유, 파일은 copy-on-write로 매핑, 매핑된 내용에 변경이 발생하더라도 실제파일이나 다른 프로세스에 미영향
    • MAP_SHARED 같은 파일을 맵핑한 모든 프로세스와 맵핑 공유, 변경이 일어나면 실제 파일에서도 동일한 내용 기록
  • mmap()시스템콜은 페이지를 다루며 addr과 offset은 페이지 크기로 정렬되어야 함(정렬이 되지않은 값이 전달되면 정렬된 값으로 할당)
  • 페이지의 크기를 구하는 방법은 sysconf(_SC_PAGESIZE), getpagesize() 등이 있음(이식성을 고려 시 sysconf를 사용)
  • mmap()관련 시그널은 SIGBUS(프로세스가 유효하지 않은 맵핑영역 접근), SIGSEGV(읽기전용 매핑영역에 쓰려고 할 때)가 있음

munmap()

1
2
3
#include <sys/mman.h>

int munmap(void* addr, size_t len);
  • munmap()은 addr에서 len 바이트만큼 이어지는 프로세스 주소 공간 내 존재하는 페이지를 포함하는 맵핑을 해제
  • 성공시 0을 반환, 실패 시 -1을 반환하고 errno를 적절한 값으로 설정

mmap 장점

  1. read/write 시스템콜을 사용할 때 발생하는 불필요한 복사 방지
  2. 잠재적인 페이지 폴트를 제외하면 매핑된 파일을 읽고 쓰는데 다른 시스템 콜이나 컨택스트 스위칭이 발생하지 않음
  3. 여러 프로세스가 같은 객체를 메모리에 매핑한다면 데이터는 모든 프로세스 사이에서 공유
  4. lseek()같은 시스템콜을 사용하지 않고 매핑영역 탐색 가능

mmap 단점

  1. 메모리 매핑은 페이지 크기의 정수배만 가능하므로 작은 파일이 많다면 공간이 낭비됨
  2. 32비트 주소공간에서 다양한 크기의 수많은 매핑 사용시 주소공간이 파편화 됨(64비트 주소공간에서는 상관없음)
  3. 메모리 매핑과 관련된 자료구조를 커널 내부에서 생성, 유지하는 오버헤드가 있음

매핑 크기 변경

1
2
3
4
#define _GNU_SOURSE
#include <sys/mman.h>

void* mremap(void* addr, size_t old_size, size_t new_size, unsigned long flags);
  • mremap()은 [addr, addr+old_size)에 매핑된 영역을 new_size만큼의 크기로 변경
  • flags인자는 0이거나 MREMAP_MAYMOVE(크기변경 수행하는데 필요시 맵핑의 위치가 변경되도 됨)가 될 수 있다.
  • 성공 시 크기가 조정된 메모리 맵핑의 시작주소 반환, 실패시 MAP_FAILED 반환 후 errno를 설정

매핑의 보호모드 변경

1
2
3
#include <sys/mman.h>

int mprotect(const void* addr, size_t len, int prot);
  • mremap()은 [addr, addr+len)내에 포함된 메모리의 페이지 보호모드를 prot로 변경(추가 아님)
  • 성공시 0을 반환, 실패 시 -1을 반환하고 errno를 설정

파일과 매핑의 동기화

1
2
3
#include <sys/mman.h>

int msync(void* addr, size_t len, int flags);
  • msync()는 addr에서 len 바이트 만큼 매핑된 파일이나 파일의 일부를 디스크로 동기화
  • msync()를 호출하지 않으면 매핑이 해제되기 전까지는 맵핑된 메모리에 쓰여진 내용이 디스크로 반영됨을 보장할 수 없다.
  • flags는 동기화 방식 제어
    • MS_SYNC 동기화 호출, MS_ASYNC 비동기식 동기화, MS_INVALIDATE 맵핑의 캐시 복사본을 모두 무효화
  • 성공시 0을 반환, 실패 시 -1을 반환하고 errno를 설정

맵핑의 사용처 알려주기

1
2
3
#include <sys/mman.h>

int madvise(void* addr, size_t len, int advice);
  • madvice() 시스템콜은 주어진 힌트에 따라 맵핑의 동작 방식을 최적화(캐시 및 미리읽기 방식) 가능
  • advice 인자는 커널에 알려줄 힌트 기술
    • MADV_NOMAL : 일반적 영역, 적당양 미리 읽기
    • MADV_RANDOM : 랜덤하게 접근하는 영역, 미리읽기 미사용(최소한의 데이터만 가져옴)
    • MADV_SEQUENTIAL : 순차적으로 접근하는 영역, 공격적인 미리 일기 수행
    • MADV_WILLNEDD : 곧 접근하는 영역, 미리읽기 활성화 후 주어진 페이지를 메모리로 읽음
    • MADV_DONTNEED : 당분간 접근하지 않는 영역, 페이지와 곤련된 자원 해제 후 동기화 되지 않은 페이지 버림
  • 성공시 0을 반환, 실패 시 -1을 반환하고 errno를 설정

일반 파일 입출력에 대한 힌트

posix_fadvise() 시스템콜

1
2
3
#include <fcntl.h>

int posix_fadvise(int fd, off_t offset, off_t len, int advice);
  • fd의 [offset, offset+len] 범위에 대한 흰트를 제공
  • 주로 len을 0으로 넘겨서 파일 전체에 대한 힌트를 제공하는 방식으로 사용
  • 힌트에 대해 커널이 등으하는 방식은 구현에 따라 커널버전에 따라 다름
    • POSIX_FADV_NORMAL : 힌트없음, 적당한 미리읽기
    • POSIX_FADV_RANDOM : 랜덤 접근, 미리읽기를 하지 않고 매번 일기마다 최소한의 데이터만 읽음
    • POSIX_FADV_SEQUENTIAL : 순차접근, 미리읽기 윈도우 크기를 두배로 늘림
    • POSIX_FADV_WILLNEED : 곧 접근함, 미리읽기 활성화하고 주어진 페이지를 메모리로 읽음
    • POSIX_FADV_NOREUSE : 곧 한번만 접근, POSIX_FADV_WILLNEED와 동일하게 동작
    • POSIX_FADV_DONTNEED : 당분간 안 접근함, 범위내의 캐싱 중인 데이터를 페이지 캐시에서 제거
  • 성공 시 0 반환, 실패 시 -1 반환 후 errno 값을 설정

readahead() 시스템콜

1
2
3
4
#define _GNU_SOURCE
#include <fcntl.h>

ssize_t readahead(int fd, off64_t offset, size_t count);
  • 리눅스 전용으로 POSIX_FADV_WILLNEED와 동일한 방식을 제공하기 위해 사용
  • fd가 가리키는 파일의 [offset, offset+counter) 영역의 페이지를 캐시 생성

부담없이 힌트를 사용하자

  • 책에서는 힌트는 도움이 많이 된다고 기술
  • POSIX_FADV_WILLNEED를 이용하여 읽으려는 파일을 미리 캐시에 넣어 어플리케이션에서 블로킹을 방지하거나
  • 비디오 스트림 기록 같은 경우 POSIX_FADV_DONTNEED로 캐시에서 제거 할 수 있음

동기화, 동기식, 비동기식 연산

  • 동기식, 비동기식 용어는 입출력 연산이 반환하기 전 어떤 이벤트(ex. 데이터 저장)을 기다리는지 여부
  • 동기화, 비동기화 용어는 정학한 어떤 이벤트(ex. 데이터를 디스크에 기록)이 발생해야 함을 나타냄
  • 책에 표를 읽어보자

비동기식 입출력

  • aio라이브러리는 비동기식 입출력을 요청하고 작업이 완료되면 알림을 받는 함수를 제공
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <aio.h>

struct aiocb{
  int aio_fildes;     // 파일 디스크립터
  int aio_lio_opcode; // 수행할 작업
  int aio_reqrio;     // 요청 우선순위 오프셋
  volatile void* aio_buf; // 버퍼에 대한 포인터
  size_t aio_nbytes; // 연산의 크기
  struct sigevent aio_sigevent; // 시그널의 번호와 값
  /* 내부적으로 사용하는 프라이빗 멤버 */
}

int aio_read(struct aiocb* aiocbp);
int aio_write(struct aiocb* aiocbp);
int aio_error(const struct aiocb* aiocbp);
int aio_return(struct aiocb* aiocbp);
int aio_cancel(int fd, struct aiocb* aiocbp);
int aio_fsync(int op, struct aiocb* aiocbp)l
int aio_suspend(const struct aiocb* const cblist[], int n, const struct timespec* timeout);

입출력 스케줄러와 성능

디스크 주소 지정 방식

  • 하드디스크는 CHS(cylinder, Head, Sector) 주소 지정방식 사용하며 요즘은 이 값대신 유일한 블록 번호를 CHS에 맵핑

입출력 스케줄러 동작 방식

  • 입출력 스케줄러는 병합과 정렬이라는 두가지 기본 동작 수행(하드디스크의 입출력 성능 극대화를 위함)
    • 병합은 둘 이상의 인접한 입출력 요청을 단일 요청으로 합침
    • 대기 중인 입출력 요청을 블록 순서의 오름 차순으로 정렬

읽기 개선

  • 입출력 스케줄러에는 다음과 같은 방식이 있다.
    • 데드라인 입출력 스케줄러
    • 예측 입출력 스케줄러
    • CFQ 입출력 스케줄러
    • Noop 입출력 스케줄러

입출력 스케줄러 선택과 설정

  • 기본 입출력 스케줄러는 부팅 시 커널 명령행 인자인 iosched를 통해서 선택 가능
  • `/sys/block/[device]/queue/scheduler 값을 변경해서 입출력 스케줄러 선택가능

입출력 성능 최적화

  • 자잘한 연산을 묶어 입출력 연산을 최소화 하거나 입출력을 블록 크기에 정렬되도록 수행하거나 3장의 사용자 버퍼링, 벡터 입출력, 2장의 위치를 지정한 입출력, 비동기식 입출력 등을 고려할 수 있다.
  • 사용자 영역 어플리케이션에서 커널과 유사한 방식을 사용하여 더 나은 성능을 얻을 수 있도록 노력해야함
    • 사용자 영역에서 입출력 스케줄링 하기
    • 경로로 정렬하기
    • inode로 정렬하기
    • 물리 블록으로 정렬하기
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy