[Ch04 고급 버퍼 입출력]
2장에서는 파일입출력의 근본일 뿐만 아니라, 리눅스에서 일어나는 모든 통신의 토대인 기본 입출력 시스템 콜
을 배웠다.
3장에서는 기본 입출력 시스템 콜에 사용자 영역 버퍼링
이 필요한 때를 알아보고 해법으로 C언어의 표준 입출력 라이브러리
에 대해 공부했다.
4장에서는 리눅스의 고급 입출력 시스템 콜
에 대해 알아본다.
벡터 입출력
- 한번의 호출로 여러 버퍼에서 데이터를 읽거나 쓸 수 있도록 해줌.
- 다양한 자료구조를 단일 입출력 트랜젝션으로 다룰 때 유용하다.
epoll
- poll()과 select() 시스템 콜을 개선한 시스템 콜이다.
- 싱글 스레드에서 수백 개의 FD를 poll해야 하는 경우에 유용하다.
메모리맵 입출력
- 파일을
메모리
에 맵핑해서 간단한 메모리 조작을 통해 파일 입출력을 수행함. - 특정 패턴의 입출력에 유용하다.
파일 활용법 조언
- 프로세스에서
파일을 사용하려**의도**
를 커널에게 제공할수 있도록 하여, 입출력 성능을 향상시킴.
비동기식 입출력
- 작업 완료를 기다리지 않는 입출력을 요청한다.
- 스레드를 사용하지 않고 동시에 입출력 부하가 많은 작업을 처리할 경우 유용함
4.1 벡터 입출력
- 한번의 시스템 콜을 사용해서 여러개의 버퍼 벡터에 쓰거나, 여러 개의 버퍼 벡터로 읽어 들일 때 사용하는 입출력 메서드
- 2장의 표준 읽기와 쓰기는
선형 입출력
이라고 함.
- 2장의 표준 읽기와 쓰기는
- 벡터 입출력의 장점
- 좀 더 자연스러운 코딩 패턴 - 미리 정의된 구조체의 여러 필드에 걸쳐서 데이터가 분리되어 있는 경우, 벡터 입출력을 사용하면 직관적인 방법으로 조작할 수 있음
- 효율 - 한번의 사용으로 여러번의 선형 입출력 연산을 대체할 수 있음
- 성능 - 시스템 콜 호출 횟수 ⬇️, 내부적으로 최적화된 구현을 제공
- 원자성 - 벡터 입출력 연산 중에 다른 프로세스가 끼어들 수 없음
4.1.1 readv() 와 writev()
readv()
함수는 fd에서 데이터를 읽어서 count 개수만큼 iov 버퍼에 저장한다.
1
ssize_t readv (int fd, const struct iovec *iov, int count);
writev()
함수는 count 개수만큼 iov 버퍼에 있는 데이터를 fd에 기록함
1
ssize_t writev(int fd, const struct iovec *iov, int count);
- readv()와 writev() 함수는 여러 개의 버퍼를 사용한다는 점에서 read(), write()와 구분됨
iovec
구조체는 세그먼트라고 하는 독립적으로 분리된 버퍼를 나타낸다.
1
2
3
4
struct iovec{
void *iov_base; // 버퍼의 시작 포인터
size_t iov_len; // 버퍼 크기 (바이트)
}
- 이런 세그먼트의 집합을
벡터
라고 한다. - 벡터의 각 세그먼트에는 데이터를 기록하거나 읽어올 메모리 공간의 주소와 크기가 저장되어 있다.
- 두 함수는 각 버퍼에 iov_len 바이트만큼 데이터를 채우거나 쓴 다음, 다음 버퍼로 넘어간다.
- 두 함수 모드 iov[0] 부터 시작해서 iov[1], 그리고 iov[count-1]까지 세그먼트 순서대로 동작한다.
반환값
- 두 함수는 호출이 성공했을 때 읽거나 쓴
바이트 개수
를 반환함- 반환값은 반드시 count * iov_len 값과 같아야함.
- 에러 발생 시
-1
을 반환, errno 를 설정 각각 read(), write() 시스템 콜에서 발생 가능한 모든 종류의 에러가 발생할 수 있음
추가로 두 가지 에러 상황을 정의하고 있음
- 반환값의 자료형이
ssize_t
이기 때문에, 만약 count * iov_len 값이SSIZE_MAX
보다 큰 경우에는 데이터가 전송되지 않고 -1을 반환하며 errno 는EINVAL
로 설정됨 POSIX에서는 count가 0보다 크고
IOV_MAX
(리눅스에서는 현재 1024로 정의하고 있음) 와 같거나 작아야 한다고 명시하고 있는데, 만약 count가 0이라면 readv()와 writev()는 0을 반환한다.만약 count 값이
IOV_MAX
보다 크다면 데이터는 전송되지 않고 -1을 반환하며 errno는EINVAL
로 설정됨
- 반환값의 자료형이
최적 count 찾기
- 벡터 입출력 작업을 할 때 리눅스 커널에서는 각 세그먼트를 위해 내부 데이터 구조체를 반드시 할당하게 됨!
- 근데 이 할당은 count의 크기에 따라 동적으로 일어난다.
- 만약 count값이 크지 않다면 스택에 미리 만들어둔 작은 세그먼트 배열을 사용해서, 동적 할당이 일어나지 않도록한다. →
성능 개선
! (아주 효율적으로 동작함) - 그러니깐 벡터 입출력 연산을 사용할 때 세그먼트의 개수의 감이 오지 않는다면 8 이하로 시도~~
예제
writev()
예제- 3개의 벡터 세그먼트에 데이터를 쓰는 예제
- 각각 크기가 다른 문자열을 담고 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int main(){
struct iovec iov[3];
ssize_t nr;
int fd, i;
char *buf[] = {
"aaa",
"bbbb",
"cccccc"
};
fd = open("buccaneer.txt", O_WRONLY | OCREAT | O_TRUNC);
if (fd == -1)
// error
return 1;
// 세 iovec 구조체 값을 채운다
for (i = 0; i<3;i++){
iov[i].iov_base = buf[i];
iov[i].iov_len = strlen(buf[i]) + 1;
}
// 단 한번의 호출로 세 iovec 내용을 모두 쓴다
nr = writev (fd, iov, 3);
if (nr == -1 )
//error
return 1;
printf("wrote %d bytes\n", nr);
if (close(fd))
//error
return 1;
return 0
}
readv()
예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/uio.h>
int main ()
{
char foo[48], bar[51], baz[49];
struct iovec iov[3];
ssize_t nr;
int fd, i;
fd = open ("buccaneer.txt", O_RDONLY);
if (fd == −1) {
perror ("open");
return 1;
}
/* set up our iovec structures */
iov[0].iov_base = foo;
iov[0].iov_len = sizeof (foo);
iov[1].iov_base = bar;
iov[1].iov_len = sizeof (bar);
iov[2].iov_base = baz;
iov[2].iov_len = sizeof (baz);
/* read into the structures with a single call */
nr = readv (fd, iov, 3);
if (nr == −1) {
perror ("readv");
return 1;
}
for (i = 0; i < 3; i++)
printf ("%d: %s", i, (char *) iov[i].iov_base);
if (close (fd)) {
perror ("close");
return 1;
}
return 0;
}
- 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <sys/uio.h>
ssize_t naive_writev (int fd, const struct iovec *iov, int count)
{
ssize_t ret = 0;
int i;
for (i = 0; i < count; i++) {
ssize_t nr;
errno = 0;
nr = write (fd, iov[i].iov_base, iov[i].iov_len);
if (nr == −1) {
if (errno == EINTR)
continue;
ret = −1;
break;
}
ret += nr;
}
return ret;
}
- readv()와 writev()는 사용자 영역에서 단순 루프를 사용해서 구현할 수 있음!
- 사실 리눅스 커널 내부의 모든 입출력은 벡터 입출력이다.
- read, write 구현 역시 하나짜리 세그먼트를 가지는 벡터 입출력으로 구현되어 있음.
4.2 epoll
- poll과 select 의 한계에 대해서 인지하면서 커널 2.6버전에서는
epoll
(event poll) 이라는 기능이 추가되었음 poll과 select
- 실행할 때마다 전체 fd를 요구함
- → 커널은 검사해야 할 모든 파일 리스트를 다 살펴봐야함.
- → fd 리스트의 크기가 수백 ~ 수천까지 커지면 병목현상이 발생
- 실행할 때마다 전체 fd를 요구함
epoll
은 실제로 검사하는 부분과 검사할 fd를 등록하는 부분을 분리해서 위의 문제를 해결함epoll
은 세 가지 System call로 동작함- epoll 컨텍스트를 초기화
- 검사해야 할 fd를 epoll 컨텍스트에 등록하거나 삭제함
- 실제 이벤트를 기다리도록 동작
4.2.1 새로운 epoll 인스턴스 생성하기
- epoll 컨텍스트는
epoll_create1()
을 통해서 생성됨
1
2
3
4
5
6
#include <sys/epoll.h>
int epoll_create1 (int flags);
/* deprecated. use epoll_create1() in new code. */
int epoll_create (int size);
- 호출이 성공하면 새로운 epoll 인스턴스를 생성하고 그 인스턴스와 연관된 fd (epoll fd) 를 반환한다.
- 요 fd는 실제 파일과는 아무런 관계가 없고 epoll 기능을 사용하는 다음 호출에 사용되는 핸들일 뿐임.
flag
인자는 epoll 동작을 조정하기 위한 것0
을 쓰면 size 인자가 없어졌다는 점을 빼면epoll_create()
과 동일함!- epoll_fd 의 크기정보를 전달했었음
- 현재는
EPOLL_CLOSEXEC
만 유효함 - 새 프로세스가 실행될 때 이 파일을 자동적으로 닫아준다.
에러가 발생하면 -1을 반환, errno를 설정
EINVAL
- 잘못된 flags 인자
EMFILE
- 사용자의 최대 파일 초과
ENFILE
- 시스템의 최대 파일 초과
ENOMEM
- 메모리 부족
사용예제
1 2 3 4 5
int epfd; epfd = epoll_create1 (0); if (epfd < 0) perror ("epoll_create1");
- epoll_create1()에서 반환하는 fd는 폴링이 끝난 뒤에 반드시 close()로 닫아줘야한다.
4.2.2 epoll 제어
epoll_ctl()
시스템 콜은 주어진 epoll 컨텍스트에 fd를 추가하거나 삭제할 때 사용한다.1 2 3 4 5 6
#include <sys/epoll.h> int epoll_ctl (int epfd, int op, int fd, struct epoll_event *event);
1 2 3 4 5 6 7 8 9
struct epoll_event { __u32 events; /* events */ union { void *ptr; int fd; __u32 u32; __u64 u64; } data; };
- epoll_ctl() 호출이 성공하면 해당 epoll 인스턴스는 epfd 파일 디스크립터와 연결된다.
epfd
- 이전에 epoll_create1() 로 생성한 epoll fd
fd
- 등록할 fd
op
인자는 fd가 가리키는 파일에 대한 작업을 명시한다.- 어떤 변경을 할지 결정하는 값
EPOLL_CTL_ADD
- epfd와 연관된 epoll 인스턴스가 fd와 연관된 파일을 감시하도록
추가
하며, 각 이벤트는 event 인자로 정의한다.
- epfd와 연관된 epoll 인스턴스가 fd와 연관된 파일을 감시하도록
EPOLL_CTL_DEL
- epfd와 연관된 epoll 인스턴스에 fd를 감시하지 않도록
삭제
한다.
- epfd와 연관된 epoll 인스턴스에 fd를 감시하지 않도록
EPOLL_CTL_MOD
- 기존에 감시하고 있는 fd에 대한 이벤트를 event에 명시된 내용으로
갱신
한다.
- 기존에 감시하고 있는 fd에 대한 이벤트를 event에 명시된 내용으로
event
인자는 그 작업의 동작에 대한 설명을 담고 있다.- 이벤트 유형
- epoll_event 구조체의 events 필드는 주어진 fd에서 감시할 이벤트의 목록을 담고 있음
- 여러가지 이벤트를 OR로 묶을 수 있다.
1 2 3 4 5 6 7 8 9 10
enum Events { EPOLLIN, //수신할 데이터가 있다. EPOLLOUT, //송신 가능하다. EPOLLPRI, //중요한 데이터(OOB)가 발생. EPOLLRDHUD,//연결 종료 or Half-close 발생 EPOLLERR, //에러 발생 EPOLLET, //엣지 트리거 방식으로 설정 EPOLLONESHOT, //한번만 이벤트 받음 }
- 여러가지 이벤트를 OR로 묶을 수 있다.
- epoll_event 구조체의
data 필드
는 사용자 데이터를 위한 필드이다.- 이 필드에 담긴 내용은 요청한 이벤트가 발생해서 사용자에게 반환될 때 함께 반환됨.
- 일반적인 사용 예
- event.data.fd를 fd로 채워서 이벤트가 발생했을 때 어떤 fd를 들여다 봐야 하는지 확인하는 용도
- 성공 시 0을 반환하고 실패 시 -1을 반환, errno 설정
예제 코드
1 2 3 4 5 6 7 8 9
struct epoll_event event; int ret; event.data.fd = fd; /* return the fd to us later (from epoll_wait) */ event.events = EPOLLIN | EPOLLOUT; ret = epoll_ctl (epfd, EPOLL_CTL_ADD, fd, &event); if (ret) perror ("epoll_ctl");
epfd와 연관된 fd에 설정된 기존 구독 이벤트를 변경하려면 아래와 같이 작성하면 됨
1 2 3 4 5 6 7 8 9
struct epoll_event event; int ret; event.data.fd = fd; /* return the fd to us later */ event.events = EPOLLIN; ret = epoll_ctl (epfd, EPOLL_CTL_MOD, fd, &event); if (ret) perror ("epoll_ctl");
반대로 epoll 인스턴스 epfd에 등록된 fd에 연관된 기존 이벤트를 삭제하려면 아래와 같이!
1 2 3 4 5 6
struct epoll_event event; int ret; ret = epoll_ctl (epfd, EPOLL_CTL_DEL, fd, &event); if (ret) perror ("epoll_ctl");
- op 값이
EPOLL_CTL_DEL
인 경우 이벤트 마스크가 없기 때문에event
값이NULL
이 될 수도 있음.- 하지만, 호환성 문제 떄문에 유효한 포인터를 넘겨야함.
4.2.3 epoll로 이벤트 기다리기
epoll_wait()
시스템 콜은 epoll 인스턴스와 연관된 fd에 대한 이벤트를 기다린다.
1
2
3
4
5
6
7
#include <sys/epoll.h>
int epoll_wait( int efpd, //epoll_fd
struct epoll_event* event, //event 버퍼의 주소
int maxevents, //버퍼에 들어갈 수 있는 구조체 최대 개수
int timeout //select의 timeout과 동일 단위는 1/1000
);
epoll_wait
를 호출하면 timeout 밀리 초 동안 epoll 인스턴스인 epfd와 연관된 파일의 이벤트를 기다린다.- 성공할 경우
events
에는 발생한 해당 이벤트 (파일이 읽어나 쓰기가 가능한 상태인지를 나타내는 epoll_event 구조체에 대한 포인터) 가 기록된다. 발생한 이벤트 개수를 반환- events의
data 필드
에는 사용자가epoll_ctl()
을 호출하기 전에 설정한 값이 담겨 있다.따라서 모든 fd에 대해 순회하면서 체크할 필요가 없음! 이벤트가 있는 fd들이 배열에 담겨오고 그 개수를 알 수 있으니 꼭 필요한 event 만 순회하면서 처리할 수 있다는 장점!
- events의
- 에러가 발생할 경우 -1을 반환하고 errno 를 설정
timeout
- 0이면 epoll_wait()는 이벤트가 발생하지 않아도 즉시 0을 반환함.
- -1이면 이벤트가 발생할 때까지 해당 호출은 반환되지 않음
- 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define MAX_EVENTS 64
struct epoll_event *events;
int nr_events, i, epfd;
events = malloc (sizeof (struct epoll_event) * MAX_EVENTS);
if (!events) {
perror ("malloc");
return 1;
}
nr_events = epoll_wait (epfd, events, MAX_EVENTS, −1);
if (nr_events < 0) {
perror ("epoll_wait");
free (events);
return 1;
}
for (i = 0; i < nr_events; i++) {
printf ("event=%ld on fd=%d\n",
events[i].events,
events[i].data.fd);
/*
* We now can, per events[i].events, operate on
* events[i].data.fd without blocking.
*/
}
free (events);
4.2.4 에지 트리거와 레벨 트리거
epoll_ctl()
로 전달하는 event 인자의 events 필드를EPOLLET
로 설정하면 fd에 대한 이벤트 모니터가레벨 트리거
가 아닌에지 트리거
로 동작한다.- 유닉스 파이프 통신 입출력 예시
출력
하는 쪽에서 파이프에 1KB만큼의 데이터를 씀입력
을 받는 쪽에서는 파이프에 대해서epoll_wait()
를 수행하고 파이프에 데이터가 들어와서 읽을 수 있는 상태가 되기를 기다림레벨 트리거
일 경우- 2단계의
epoll_wait()
호출은 즉시 반환하며 파이프가 읽을 준비가 되었음을 알려줌
- 2단계의
에지 트리거
일 경우- 1단계가 완료될 때까지 호출이 반환되지 않음.
- 즉, epoll_wait()를 호출하는 시점에 파이프를 읽을 수 있는 상황이더라도 파이프에 데이터가 들어오기 전까지는 결과 반환 안함.
- 기본 동작 방식은 레벨 트리거
- poll()과 select()의 동작방식도 동일
4.3 메모리에 파일 맵핑하기
- 리눅스 커널은 표준 파일 입출력의 대안으로 애플리케이션이 파일을 메모리에 맵핑할 수 있는 인터페이스를 제공한다.
- 메모리 주소와 파일의 단어가 일대일 대응이 된다는 것을 의미
- → 개발자가 메모리를 통해 파일에 직접 접근이 가능함.
- → 메모리 주소에 직접 쓰는 것만으로 디스크에 있는 파일에 기록할 수 있음
4.3.1 mmap()
mmap()
을 호출하면 fd가 가리키는 파일의 offset 위치에서 len 바이트만큼 메모리에 맵핑하도록 커널에 요청한다.
1
2
3
4
5
6
7
8
#include <sys/mman.h>
void * mmap (void *addr,
size_t len,
int prot,
int flags,
int fd,
off_t offset);
addr
- addr가 포함되면 메모리에서 해당 주소를 선호한다고 커널에 알려줌
- 그저 힌트일 뿐이며 대부분
0
을 넘겨줌
len
- fd 가 가리키는 파일의
offset
위치에서len
바이트만큼 메모리에 맵핑하도록 커널에 요청함.
- fd 가 가리키는 파일의
prot
- 접근권한을 지정
- 맵핑에 원하는 메모리 보호 정책을 명시
PROT_NONE
: 접근 불가PROT_READ
: 읽기 가능PROT_WRITE
: 쓰기 가능PROT_EXEC
: 실행 가능
flag
- 맵핑의 유형과 그 동작에 관한 몇 가지 요소를 명시
MAP_FIXED
: mmap()의 addr 인자를 힌트가 아니라 요구사항으로 취급하도록 함MAP_PRIVATE
: 맵핑이 공유되지 않음을 명시. 파일은 copy-on-write 로 맵핑됨.MAP_SHARED
: 같은 파일을 맵핑한 모든 프로세스와 맵핑을 공유MAP_SHARED
와MAP_PRIVATE
를 함께 지정하면 안됨.
- 반환
- 메모리 맵핑의 실제 시작 주소를 반환한다.
- fd를 맵핑하면 해당 파일의 참조 카운터가 증가한다. → 따라서 파일을 맵핑한 후에 fd를 닫더라도 프로세스는 여전히 맵핑된 주소에 접근할 수 있다.
예시
- fd가 가리키는 파일의 첫 바이트부터 len 바이트까지를 읽기 전용으로 맵핑한다.
1 2 3 4 5
void *p; p = mmap (0, len, PROT_READ, MAP_SHARED, fd, 0); if (p == MAP_FAILED) perror ("mmap");
- mmap() 에 전달하는 인자가 맵핑하는 과정
페이지 크기
페이지
는 메모리 관리 유닛 (MMU)에서 사용하는 최소 단위이다.- 별도의 접근 권한과 동작 방식을 따르는 가장 작은 메모리 단위라고 할 수 있음.
- 메모리 맵핑을 구성하는 블록이자 프로세스 주소 공간을 구성하는 블록
mmap()
시스템 콜은 페이지를 다루기 때문에addr
과offset
인자는 페이지 크기 단위(페이지 크기의 정수배)로 정렬되어야 한다.- 만약 len인자가 페이지 크기 단위로 정렬되지 않았다면 다음 크기의 페이지 정수배로 확장됨
- 마지막 유효 바이트와 맵핑의 끝 사이에 추가된 메모리는 0으로 채워짐
페이지 크기를 얻을 수 있는 표준 메서드는
sysconf()
이다.1 2 3
#include <unistd.h> long sysconf (int name);
POSIX는 페이지 크기를 바이트 단위로
_SC_PAGESIZE
(or_SC_PAGE_SIZE
) 로 정의함.- 런타임의 페이지 크기를 구하는 방법은 아래와 같다.
1
long page_size = sysconf (_SC_PAGESIZE);
리눅스는 바이트 단위의 페이지 크기를 반환하는
getpagesize()
를 제공함1 2 3
#include <unistd.h> int getpagesize (void);
1
int page_size = getpagesize ();
- PAGE_SIZE 매크로를 통해서도 페이지 크기를 구할 수 있는데, 런타임이 아닌 컴파일 시점에 시스템의 페이지 크기를 가져온다.
1
int page_size = PAGE_SIZE;
반환값과 에러
mmap()
호출이 성공하면 맵핑된 주소를 반환한다.- 실패하면
MAP_FAILED
(-1) 를 반환하고 errno를 설정한다. - 절대 0을 반환하지 않음.
관련 시그널
SIGBUS
- 프로세스가 더 이상 유효하지 않은 맵핑 영역에 접근하려고 할 때 발생함.
- 맵핑된 후에 파일이 잘렸을 경우에 이 시그널이 발생함.
SIGSEGV
- 프로세스가 읽기 전용으로 맵핑된 영역에 쓰려고 할 때 발생
4.3.2 munmap()
mmap()
으로 생성한 맵핑을 해제하기 위한munmap()
시스템 콜을 제공함.
1
2
3
#include <sys/mman.h>
int munmap (void *addr, size_t len);
- 페이지 크기로 정렬된
addr
에서 시작해서len
바이트만큼 이어지는 프로세스 주소 공간에 존재하는 페이지를 포함하는 맵핑을 해제함.- 맵핑 해제하고 다시 접근하면
SIGSEGV
시그널이 발생함.
- 맵핑 해제하고 다시 접근하면
- 성공 시 0을 반환, 실패 시 -1을 반환하고 errno 설정
- 예제
[addr, addr+len]
사이에 포함된 페이지를 담고 있는 메모리 영역에 대한 맵핑을 해제함.1 2
if (munmap (addr, len) == −1) perror ("munmap");
4.3.3 맵핑 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
// 인자로 파일 이름을 받음
int main (int argc, char *argv[])
{
struct stat sb;
off_t len;
char *p;
int fd;
if (argc < 2) {
fprintf (stderr, "usage: %s <file>\n", argv[0]);
return 1;
}
fd = open (argv[1], O_RDONLY); // 인자로 넘겨받은 파일을 연다
if (fd == −1) {
perror ("open");
return 1;
}
if (fstat (fd, &sb) == −1) { // fstat : 주어진 파일에 대한 정보 반환
perror ("fstat");
return 1;
}
if (!S_ISREG (sb.st_mode)) { // 주어진 파일이 디바이스 파일이나 디렉터리가 아닌 일반 파일인지 점검
fprintf (stderr, "%s is not a file\n", argv[1]);
return 1;
}
p = mmap (0, sb.st_size, PROT_READ, MAP_SHARED, fd, 0); // 맵핑 수행
if (p == MAP_FAILED) {
perror ("mmap");
return 1;
}
if (close (fd) == −1) {
perror ("close");
return 1;
}
for (len = 0; len < sb.st_size; len++)
putchar (p[len]);
if (munmap (p, sb.st_size) == −1) {
perror ("munmap");
return 1;
}
return 0;
}
4.3.4 mmap()의 장점
read()
와write()
시스템 콜을 사용하는 것보다mmap()
을 이용해서 파일을 조작하는 것이 좀 더 유용하다.
- read, write 시스템 콜 사용할 때 발생하는 불필요한 복사를 방지할 수 있음.
- 사용자 영역의 버퍼로 데이터를 읽고 써야 하기 때문에 추가적인 복사가 발생함.
- (잠재적인 페이지 폴트 가능성을 제외하면) 시스템 콜 호출이나 컨텍스트 스위칭 오버헤드가 발생하지 않음
- 여러 개의 프로세스가 같은 객체를 메모리에 맵핑한다면 데이터는 모든 프로세스 사이에서 공유된다.
lseek()
같은 시스템 콜을 사용하지 않고도 맵핑영역 탐색 가능
4.3.5 mmap()의 단점
- 메모리 맵핑은 항상 페이지 크기의 정수배만 가능하다.
- 메모리 맵핑은 반드시 프로세스의 주소 공간에 딱 맞아야한다.
- 다양한 사이즈의 맵핑이 있다면
파편화
가 일어남
- 다양한 사이즈의 맵핑이 있다면
- 메모리 맵핑과 관련 자료구조를 커널 내부에서 생성, 유지하는데 오버헤드가 발생한다.
- 이중 복사 제거 방법으로 방지할 수 있음
읽기 요청마다 표준 입출력 버퍼를 가리키는 포인터를 반환하는 대체 구현을 통해 데이터를 표준 입출력 버퍼에서 직접 읽을 수 있음 → 불필요한 복사 피함
- 이중 복사 제거 방법으로 방지할 수 있음
4.3.6 맵핑 크기 변경하기
- 리눅스는 주어진 메모리 맵핑 영역의 크기를 확장하거나 축소하기 위한
mremap()
시스템 콜을 제공함.
1
2
3
4
5
6
#define _GNU_SOURCE
#include <sys/mman.h>
void * mremap (void *addr, size_t old_size,
size_t new_size, unsigned long flags);
- mrepap()은
[addr, addr + old_size)
에 맵핑된 영역을new_size
만큼의 크기로 변경한다. flag
- 0
MREMAP_MAYMOVE
: 크기 변경 요청을 수행하는데 필요하다면 맵핑의 위치를 이동해도 괜찮다고 커널에 알려준다.- 맵핑 위치를 이동시킬 수 있다면 큰 크기 변경 요청이 성공할 가능성이 높아짐
- 성공 시 조정된 메모리 맵핑의 시작 주소를 반환함.
- 실패할 경우
MAP_FAILED
를 반환하며 errno 를 설정 예제
- glibc 같은 라이브러리는 malloc()으로 할당한 메모리의 크기를 변경하기 위한 realloc()을 효율적으로 구현하기 위해
mremap()
을 자주 사용함
1 2 3 4 5 6 7 8 9 10
void * realloc (void *addr, size_t len) { size_t old_size = look_up_mapping_size (addr); void *p; p = mremap (addr, old_size, len, MREMAP_MAYMOVE); if (p == MAP_FAILED) return NULL; return p; }
- glibc 같은 라이브러리는 malloc()으로 할당한 메모리의 크기를 변경하기 위한 realloc()을 효율적으로 구현하기 위해
4.3.7 맵핑의 보호 모드 변경하기
- POSIX는 기존 메모리 영역에 대한 접근 권한을 변경할 수 있는
mprotect()
인터페이스를 저으히함
1
2
3
4
5
#include <sys/mman.h>
int mprotect (const void *addr,
size_t len,
int prot);
[addr, addr+len)
영역내에 포함된 메모리 페이지의 보호 모드를 변경한다.prot
- mmap()에 사용한 prot 와 같은 값을 사용할 수 있다.
- 즉, 메모리 영역이 읽기가 가능한 상태에서 prot로
PROT_WRITE
를 설정한다면 쓰기만 가능해짐!
- 어떤 시스템에서는
mmap()
으로 생성한 메모리 맵핑에 대해서만mprotect()
를 쓸 수 있지만, 리눅스에서는 어떤 메모리 영역에도 사용할 수 있다. - 성공 시 0반환, 실패 시 -1 반환하고 errno 설정
4.3.8 파일과 맵핑의 동기화
- POSIX 는 2장에서 살펴본
fsync()
시스템 콜의 메모리 맵핑 버전인msync()
를 제공한다.
1
2
3
#include <sys/mman.h>
int msync (void *addr, size_t len, int flags);
msync()
는 mmap()으로 맵핑된 파일에 대한 변경 내용을 디스크에 기록하여 파일과 맵핑을 동기화한다.- 구체적으로 살펴보면 메모리 주소 addr에서부터 len 바이트 만큼 맵핑된 파일이나 파일 일부를 디스크로 동기화함.
- 이때 addr 값은 반드시 페이지 크기로 정렬되어야 한다. 보통은 mmap()에서 반환한 값을 사용함
- msync()를 호출하지 않으면 맵핑이 해제되기 전까지는 맵핑된 메모리에 쓰여진 내용이 디스크로 반영된다는 보장을 할 수가 없다.
- 쓰기 과정 중에 갱신된 버퍼를 디스크에 쓰도록 큐에 밀어넣는
write()
와는 동작방식이 다름
- 쓰기 과정 중에 갱신된 버퍼를 디스크에 쓰도록 큐에 밀어넣는
flag
MS_SYNC
: 디스크에 모든 페이지를 기록하기 전까지 msync()는 반환하지 않는다.MS_ASYNC
: 비동기 방식으로 동기화한다.MS_INVALIDATE
: 맵핑의 캐시 복사본을 모두 무효화한다.- OR로 명시할 수 있지만,
MS_SYNC
와MS_ASYNC
중 하나는 반드시 해야함. (둘을 함께하는 것은 안됨)
- 예제
[addr, addr+len)
영역에 맵핑된 파일을 디스크로 동기화한다.- fsync()에 비해서 10배 빠름 (메모리라서)
1 2
if (msync (addr, len, MS_ASYNC) == −1) perror ("msync");
- 성공하면 0 반환, 실패하면 -1반환하고 errno 설정
4.3.9 맵핑의 사용처 알려주기
- 리눅스는 프로세스가 맵핑을 어떻게 사용할 것인지 커널에 알려주는
madvise()
시스템 콜을 제공한다. - 커널이 이를 통해 얻는
힌트
를 사용해서 최적화가 가능함. 부하가 걸리는 상황에서 필요한 캐시와 미리 읽기 방식을 확실히 보장할 수 있게 된다.
1
2
3
4
5
#include <sys/mman.h>
int madvise (void *addr,
size_t len,
int advice);
- addr 로 시작해서 len 바이트의 크기를 가지는 메모리 맵핑 내의 페이지와 관련된 동작 방식에 대한 힌트를 커널에 제공함.
len
- 0 이라면 커널은 addr에서 시작하는 전체 맵핑에 힌트를 적용한다.
advice
MADV_NORMAL
: 이 메모리 영역에 대한 특별한 힌트를 제공하지 않는다.MADV_RANDOM
: 이 영역의 페이지는 랜덤하게 접근한다.MADV_SEQUENTIAL
: 이 영역의 페이지는 낮은 주소에서 높은 주소로 순차적으로 접근한다.MADV_WILLNEED
: 이 영역의 페이지는 곧 접근한다.MADV_DONTNEED
: 이 영역의 페이지는 당분간 접근하지 않는다.
POSIX는 힌트에 대한 의미만 정의하고 있다. 리눅스 커널 2.6 버전 부터는 각 힌트에 대해 조금 다르게 대응한다.
madvise 예시
[addr, addr + len)
메모리 영역을 순차적으로 접근할 것이라고 커널에 알려줌
1 2 3 4 5
int ret; ret = madvise (addr, len, MADV_SEQUENTIAL); if (ret < 0) perror ("madvise");
- 성공하면 0을 반환, 실패 시 -1을 반환하고 errno 설정
4.4 일반 파일 입출력에 대한 힌트
- 위에서는 메모리 맵핑을 사용하는데 힌트를 제공하는 방법에 대해서 알아봤음.
- 4.4 에서는 커널에 일반적인 파일 입출력에 대한 힌트를 제공하는 방법에 대해서 알아본다.
4.4.1 posix_fadvise() 시스템 콜
1
2
3
4
5
6
#include <fcntl.h>
int posix_fadvise (int fd,
off_t offset,
off_t len,
int advice);
fd
의[offset, offset + len)
범위에 대한 힌트를 커널에 제공한다.len
- 0이면 파일 전체인 [offset, 파일 길이] 에 적용된다.
- len과 offset을 0으로 넘기면 전체 파일에 대한 힌트제공
- 0이면 파일 전체인 [offset, 파일 길이] 에 적용된다.
advise
- madvise와 유사함. 한 가지 설정만 가능하다.
POSIX_FADV_NORMAL
: 힌트 제공 안함POSIX_FADV_RANDOM
: 데이터에 랜덤하게 접근POSIX_FADV_SEQUENTIAL
: 낮은 주소에서 높은 주소로 순차적POSIX_FADV_WILLNEED
: 곧 접근POSIX_FADV_NOREUSE
: 한번만 접근POSIX_FADV_DONTNEED
: 당분간 접근안함- madvise와 동일하게 커널이 이런 힌트에 대응하는 방법은 구현에 따라 다른다. (심지어는 커널 버전에 따라 다르게 동작함.)
예제
- 커널에게 fd가 가리키는 전체 파일에 랜덤하게 접근하겠다고 알려줌
1 2 3 4 5
int ret; ret = posix_fadvise (fd, 0, 0, POSIX_FADV_RANDOM); if (ret == −1) perror ("posix_fadvise");
- 성공하면 0을 반환, 실패하면 -1 반환하고 errno 설정
4.4.2 readahead() 시스템 콜
POSIX_FADV_WILLNEED
힌트와 동일한 동작 방식을 제공하기 위해 사용- 리눅스 전용 인터페이스이다.
1
2
3
4
5
6
7
#define _GNU_SOURCE
#include <fcntl.h>
ssize_t readahead (int fd,
off64_t offset,
size_t count);
- fd가 가리키는 파일의
[offset, offset+count)
영역의 페이지 캐시를 생성한다. - 성공하면 0 반환, 실패 시 -1반환하고 errno 설정
4.4.3 부담 없이 힌트를 사용하자 !!
- 일반적으로 애플리케이션에서 발생하는 일부 부하는 커널에 힌트를 제공함으로써 쉽게 개선할 수 있음!
- 힌트는 입출력의 부하를 완화시킨다.
- 파일 조각을 읽기 전에
POSIX_FADV_WILLNEED
(곧 접근) 힌트를 제공하여 커널이 읽으려는 파일을 페이지 캐시에 밀어 넣을 수 있음- 입출력은 백그라운드에서 비동기식으로 일어남. 애플리케이션이 최종적으로 파일에 접근하면 입출력을 블록킹하지 않고 원하는 작업을 완료할 수 있다.
- 많은 데이터를 연속적으로 디스크에 기록하는 경우
POSIX_FADV_DONTNEED
(당분간 접근 X) 힌트를 제공하면 파일 조각을 페이지 캐시에서 제거할 수도 있다.- 다시 접근하지 않으면, 불필요한 데이터로 가득 차있을 수 있기 때문에 주기적으로 캐시에서 스트림 데이터를 제거하는것이 합리적
- 파일 전체를 읽을 때는
POSIX_FADV_SEQUENTIAL
(순차적) 힌트를 사용해서 커널에 미리읽기를 공격적으로 수행하도록 할 수 있다. - 파일을 랜덤하게 접근하거나 파일의 이곳 저곳을 읽어야 한다면
POSIX_FADV_RANDOM
(랜덤하게 접근) 힌트를 사용해서 불필요한 미리읽기를 방지할 수 있음
4.5 동기화, 동기식, 비동기식 연산
동기식
(Synchronous)과동기화
(Synchroinized)는 크게 다르지 않음- 동기식
쓰기 연산
- 동기식 쓰기 연산은 최소한 쓰고자 하는 데이터가 커널의 버퍼 캐시에 기록되기 전까지는 반환되지 않는다.
- 비동기식 쓰기 연산은 데이터가 사용자 영역에 머무르고 있을지라도 즉시 반환될 수 있다.
읽기 연산
- 동기식 읽기 연산은 읽고자 하는 데이터가 애플리케이션에서 제공하는 사용자 영역의 버퍼에 저장되기 전까지는 반환되지 않는다.
- 비동기식 읽기 연산은 읽으려는 데이터가 미처 준비되기도 전에 반환될 수 있다.
- 비동기식 연산은 나중을 위해 요청을 큐에 넣을 뿐 실제로 요청된 작업을 수행하지 않음!
동기화 연산
은 단순 동기식 연산보다 좀 더 제약적이지만 더 안전하다.- 동기화 쓰기 연산은 데이터를 디스크에 기록해서 커널 버퍼에 있던 데이터와 디스크에 기록된 데이터가 동기화되도록 보장한다.
- 동기화 읽기 연산은 항상 데이터의 최신 복사본을 반환하며 이 복사본은 디스크에서 읽어낼 가능성이 높다.
- → 동기식과 비동기식이라는 용어는 입출력 연산이 반환하기 전에 데이터 저장과 같은 이벤트를
기다리는지의 여부
를 나타냄 → 동기화와 비동기화는 데이터를 디스크에 기록하는 것과 같은
정확한 이벤트
가 발생해야 함을 나타냄- 보통 유닉스의 쓰기 연산은
동기식
이자비동기화
연산임- 특징들의 모든 가능한 조합으로 동작이 가능함 | | 동기화 | 비동기화 | | ——– | ————————————————————————————— | ——————————————————————————————————– | | 동기식 | 데이터를 디스크에 다 비우기 전에는 반환되지 않음. O_SYNC 플래그 명시했을 때 이렇게 동작 | 데이터가 커널 버퍼에 저장되기 전까지 반환되지 않음. 일반적인 동작 | | 비동기식 | 요청이 큐에 들어가자마자 반환됨. 최종적으로 쓰기 연산이 실행되어야 디스크에 기록된다. | 요청이 큐에 들어가자마자 반환됨. 최종적으로 쓰기 연산이 실행되어야 적어도 데이터가 커널 버퍼에 저장된다. |
- 읽기 연산은
동기식
이면서동기화
연산이다.- 오랜 데이터를 읽는 것이 의미가 없으므로 항상 동기화 방식으로 동작함 | | 동기화 | | ——– | ——————————————————————————————– | | 동기식 | 최신 데이터가 제공된 버퍼로 읽어오기 전에는 반환하지 않는다. 일반적 동작 | | 비동기식 | 요청이 큐에 들어가자마자 반환된다. 하지만 최종적으로 연산이 실행되어야 최신 데이터를 반환함. |
4.5.1 비동기식 입출력
- 비동기식 입출력을 수행하려면 커널의 최하위 레벨에서부터 지원이 필요하다.
aio
인터페이스가 정의되어 있으며 리눅스에서 구현하고 있다.- 이는 비동기식 입출력을 요청하고 작업이 완료되면 알림을 받는 함수를 제공함.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <aio.h>
/* asynchronous I/O control block */
struct aiocb {
int aio_fildes; /* file descriptor */
int aio_lio_opcode; /* operation to perform */
int aio_reqprio; /* request priority offset */
volatile void *aio_buf; /* pointer to buffer */
size_t aio_nbytes; /* length of operation */
struct sigevent aio_sigevent; /* signal number and value */
/* internal, private members follow... */
};
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);
int aio_suspend (const struct aiocb * const cblist[],
int n,
const struct timespec *timeout);
4.6 입출력 스케줄러와 성능
- 디스크 성능을 가장 떨어뜨리는 부분은
seek
이라고 하는 하드 디스크에서 데이터를 읽고 쓰는 헤드를 이동시키는 과정이다.- 프로세스의 사이클 하나보다
25,000,000배
나 더 오래 걸리는 시간. - 입출력 요청을 순서대로 디스크로 보내는 방식은 비효율적임
- 프로세스의 사이클 하나보다
- → 입출력 스케줄러를 통해서 디스크 탐색 횟수를 최소화 함.
4.6.1 디스크 주소 지정 방식
- 하드 디스크는
실린더
,헤드
,섹터
또는CHS 주소 지정방식
을 사용함 - 하드 디스크는 플래터 여러 장으로 구성되어 있으며, 각 플래터는 하나의 디스크, 스핀들, 그리고 read/write 헤더로 구성되어 있다.
- 플래터를 CD로 생각할 수 있다
- 각각의 플래터는 CD 처럼 원형의 트랙으로 나뉘어져 있다. 그 트랙들은
정수 개의 섹터
로 나뉘어져 있음
- 특정 데이터가 저장되어 있는 디스크의 위치를 찾을 때 하드 디스크는
실린더
,헤드
,섹터 값
을 필요로 함.- 어떤 플래터의 어느 트랙, 어느 섹터에 데이터가 있는지 알아야 함
- 실린더 값 : 데이터가 위치한 트랙을 나타냄
- 헤드 값 : 요청한 읽기/쓰기 헤드(정확한 플래터)의 정확한 값을 구분함
- 섹터 값 : 트랙에 위치한 정확한 섹터
- 요즘 HD는 유일한 블록 번호를 맵핑해서 하나의 블록이 특정 섹터에 대응되도록 한다.
- 반면 파일 시스템은 소프트웨어로만 존재함.
논리 블록
이라는 독자적인 단위를 사용해서 동작함.- 파일 시스템의 논리 블록은 디스크의 하나 이상의 물리 블록에 맵핑되어 있다.
4.6.2 입출력 스케줄러의 동작 방식
- 입출력 스케줄러는
병합
과정렬
이라는 두 가지 기본 동작을 수행한다.병합
- 둘 이상의 인접한 입출력요청을 단일 요청으로 합치는 과정
- 예시 : 하나는 5번을 읽으려 하고, 하나는 6,7번까지 읽으려고 할 때 합쳐서 수행함. 연산 횟수는 절반으로 줄어듬
정렬
- 대기 중인 입출력 요청을 블록 순서의 오름차순으로 정렬하는 것이다.
- 예시 : 52, 109, 7에 대한 입출력 연산이 들어오면 입출력 스케줄러는 이 요청을 7, 52, 109 순서대로 정렬함.
- 만약 81번 블록에 대한 새로운 요청이 들어오면 52번과 109번 연산 요청 사이에 끼워넣음
- 선형적인 방법으로 부드럽게 이동시킬 수 있게 하여서 디스크의 헤드 움직임을
최소화
한다.
4.6.3 읽기 개선
읽기 요청
은 반드시 최신 데이터를 반환해야 함- 따라서 요청한 데이터가 페이지 캐시에 존재하지 않으면 디스크에서 데이터를 읽어올 때까지 블록되어야 하며 시간이 오래 걸릴 수 있음
- → 읽기 Latency 라고 한다.
- 읽기의 경우 나중에 들어온 요청은 앞선 요청의 완료에 의존적이다.
- 이에 반해
쓰기 요청
은 디스크 성능에 방해가 되지 않는 스트림을 사용하는데, 이는 커널과 디스크의 주의를 독차지 할 수 있다. → 이렇게 되면 읽기 문제가 복잡해지는데 이를 “Writes-starving-reads problem
”이라고 한다 만약 입출력 스케줄러가 항상 요청이 들어온 순서에 따라 새로운 요청을 끼워 넣는다면 멀리 떨어진 블록에 대한 요청을 무기한으로 굶겨 죽일 수 있음.
리누스 엘리베이터
같은 단순한 접근 방식은 큐에 충분히 오래된 요청이 있다면 삽입-정렬 기능을 멈춘다.- 전체 성능을 희생하여 요청에 대한 공정석을 유지하고 읽기 요청인 경우 레이턴시를 개선한다.
- 문제는 이 휴리스틱이 너무 단순하다는 것
데드라인 입출력 스케줄러
- 전통적인 엘리베이터 알고리즘의 일반적인 문제를 해결하기 위해 도입되었다.
- 리누스 엘리베이터는 대기 중인 입출력 요청을 정렬된 목록(
큐
)으로 유지한다. 데드라인 입출력 스케줄러
는 이 큐를 유지하고읽기 FIFO 큐
와쓰기 FIFO 큐
라는 두 가지 추가 큐를 도입해서 문제를 해결한다.- 각 큐에 들어있는 각 요청은
만료기간
이 할당되어 있음. - 읽기 500밀리초, 쓰기 5초
- 각 큐에 들어있는 각 요청은
- 새로운 입출력 요청이 들어오면 표준 큐에 삽입-정렬되고, 읽기 or 쓰기 FIFO 큐의 끝 부분에 위치한다.
- 일반적으로 표준 큐가 블록 번호로 정렬되어 있으므로 탐색을 최소화하여 전체 처리량을 최대로 높임
만약 읽기 쓰기 FIFO 큐 앞부분에 있는 아이템이 해당 표준 큐의 만료기간보다 오래되면 입출력 스케줄러는 포준 큐에서 입출력 요청을 처리하지 않고 해당 FIFO 큐에서 요청을 처리하기 시작함.
- 입출력 요청에 대해서 말랑한 데드라인을 강제한다.
- 비록 만료전에 처리된다고 보장할 수는 없지만, 이반적으로 거의 요청 만료시간 안에 처리함.
- 읽기 요청의 만료시간이 좀 더 짧기 떄문에 쓰기가 읽기를 굶겨 죽이는 문제도 최소화 한다.
예측 입출력 스케줄러
데드라인 입출력 스케줄러
의 문제점- 연속된 읽기 요청이 계속 들어올 경우, 정렬된 큐의 요청을 처리하기 위해서 앞뒤로 계속 왔다 갔다함.
- 새로운 읽기 요청은 앞선 요청이 반환되어야만 처리되는데, 그렇게 되면 데이터를 읽어서 서비스 하는 데 한번, 다시 되돌리는데 한번해서 총 두번의 탐색을 낭비함.
- 위의 문제점들을 해결하기 위해서
예측 입출력 스케줄러
는 데드라인 입출력 스케줄러에다가예측 매커니즘
을 추가하였다. - 예측 입출력 스케줄러는 읽기 요청이 들어오면 평소처럼 만료시간 내에 처리한다. 하지만 요청을 처리하고 아무것도 하지 않고 6밀리 초까지 기다림.
- → 6밀리 초는 애플리케이션이 파일 시스템의 동일한 부분에 대한 새로운 읽기를 요청할 충분한 시간이다.
- 6밀리 초 까지 요청이 없다면 예측이 잘못되었음을 인정하고 이전 작업 내용을 반환한다.
- 대부분의 읽기는 의존적이므로 에측을 통해 시간을 많이 아낄 수 있음
CFQ 입출력 스케줄러
- Complete Fair Queuing
- 프로세스마다
독자적인 큐
를 할당하고, 각 큐는 시간을 할당받는다. Round Robin
방식으로 각 큐를 순회하면서 큐의 허락된 시간이 다 지나거나, 요청이 남아 있지 않을 때 큐에 있는 요청을 처리함.- 시간이 남았지만, 더이상 요청이 큐에 없다면 CFQ 스케줄러는 짧은 시간 동안 (default = 10밀리초) 그 큐의 새로운 요청을 기다림.
- 예측이 맞으면 탐색을 피하고, 틀리면 다음 프로세스의 큐로 간다.
- 시간이 남았지만, 더이상 요청이 큐에 없다면 CFQ 스케줄러는 짧은 시간 동안 (default = 10밀리초) 그 큐의 새로운 요청을 기다림.
- 프로세스의 개별 큐 안에서 동기화된 요청(읽기 요청…) 은 동기화되지 않은 요청보다 더 높은 우선순위를 가짐.
- → 읽기 요청을 배려해서 쓰기 요청이 읽기를 굶겨 죽이는 문제를 회피한다.
- 대부분의 업무 부하에 적합하며 가장 먼저 고려해볼 만하다.
Noop 입출력 스케줄러
- 가장 기본적인 스케줄러
- 정렬을 수행하지 않고 병합만 수행함.
- → 정렬할 필요가 없거나, 정렬을 하지 않는 장치에 특화된 스케줄러
4.6.4 입출력 스케줄러 선택과 설정
- 기본 입출력 스케줄러는 부팅 시 커널 명령행 인자인
iosched
를 통해서 선택할 수 있다. - 유효한 값으로는
cfq
,deadline
,noop
이 있다. - 실행 중에도 각 장치에 대해
/sys/block/[device]/queue/scheduler
값을 변경해서 선택할 수 있음- device : 블록 디바이스를 의미
- 입출력 스케줄러 설정 예시
1
# echo cfq > /sys/block/hda/queue/scheduler
4.6.5 입출력 성능 최적화
- 디스크 입출력은 많이 느리기 떄문에 성능 극대화는 매우 중요함
- 여러가지 기법들
- 자잘한 연산을 묶어 몇 개로 합쳐서 연산 최소화 하기
- 입출력을 블록 크기에 정렬되도록 수행하기
- 사용자 버퍼링을 사용하기
- 벡터 입출력
- 위치를 지정한 입출력
- 비동기식 입출력
사용자 영역에서 입출력 스케줄링하기
- 엄청난 입출력을 처리해야 하는 애플리케이션은 입출력 요청을 정렬하고 병합해서 조금이라도 더 성능을 높여야함.
- (입출력이 많지 않은 애플리케이션에서 정렬하는 것은 어리석은 짓)
- 만약 입출력 요청이 계속 들어오고 있는 상황에서 중간에 정렬을 하는 것은 비효율적이다. 따라서 요청을 제출하기 전에 정렬을 해주면 원하는 순서대로 수행이 가능함.
경로로 정렬하기
파일 경로
로 정렬하는 방법은 가장 쉽지만, 효과는 적은 방법이다. (블록 단위 정렬을 흉내내는 방식)- 대부분의 파일시스템의 배치 알고리즘에 의해 디렉터리 내의 파일 혹은 부모 디렉터리를 공유하는 디렉터리들은 디스크에서 인접하는 경향이 있음.
- → 파일의 물리적인 위치를 얼추 비슷하게 맞출 수 있다.
- 장점
- 적어도 모든 파일 시스템에 적용 가능한 방법
- 일시적인 지역성 덕분에 중간 정도의 정확도를 기대할 수 있음.
- 구현하기 쉬움
- 단점
파편화
를 고려하지 않았음
inode로 정렬하기
inode
는 개별 파일과 관련된 메타데이터를 담고 있는 유닉스의 구성 요소이다.- 파일의 데이터가 물리 디스크 블록을 여러개 점유하고 있다고 해도, 하나의 inode만을 가짐.
- inode 는 유일한 번호가 할당됨.
1
2
3
4
5
파일 i의 inode 번호 < 파일 j의 inode 번호
==
파일 i의 물리블록 < 파일 j의 물리블록
- inode의 번호는
stat()
시스템 콜을 통해서 얻을 수 있음 주어진 파일의 inode 번호 출력 프로그램 예시
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> /* * get_inode - returns the inode of the file associated * with the given file descriptor, or −1 on failure */ int get_inode (int fd) { struct stat buf; int ret; ret = fstat (fd, &buf); if (ret < 0) { perror ("fstat"); return −1; } return buf.st_ino; } int main (int argc, char *argv[]) { int fd, inode; if (argc < 2) { fprintf (stderr, "usage: %s <file>\n", argv[0]); return 1; } fd = open (argv[1], O_RDONLY); if (fd < 0) { perror ("open"); return 1; } inode = get_inode (fd); printf ("%d\n", inode); return 0; }
- inode 정렬 장점
- inode번호는 쉽게 얻을 수 있고 정렬도 쉬움
- 물리적인 파일 배치를 추측할 수 있는 좋은 지표
- 단점
- 파편화에 따라 추측이 틀릴 수 있음
- 유닉스 파일 시스템이 아닌 경우 정확도가 떨어짐
- 사용자 영역에서 입출력 요청을 스케줄링하기 위해서 가장 흔히 사용되는 방법
물리 블록으로 정렬하기
- 최적의 방법은 물리적인 디스크 블록으로 정렬하는 것임
- 각 파일은 파일 시스템에서 가장 작은 할당 단위인
논리 블록 단위
로 쪼개짐.- (논리 블록 크기는 파일 시스템 마다 다르다.)
- 각각의 논리 블록은 하나의 물리 블록에 맵핑되어 있다.
커널은 파일의 논리 블록에서 물리 디스크 블록을 알아내는 메서드를 제공한다.
1 2 3
ret = ioctl (fd, FIBMAP, &block); if (ret < 0) perror ("ioctl");
block
- 찾고 싶은 물리 블록에 대한 논리 블록
- block은 0부터 시작하는 파일에 상대적인 값.
- 성공하면 block 은 물리 블록 번호로 바뀐다.
- 논리 블록과 물리 블록의 맵핑을 찾으려면 2단계가 필요함.
- 주어진 파일의 블록 개수를 구함
stat()
시스템 콜로 구할 수 있다.
- 각 논리 블록을 가지고 ioctl()을 통해 이에 상응하는 물리 블록을 구한다.
예제
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <linux/fs.h> /* * get_block - for the file associated with the given fd, returns * the physical block mapping to logical_block */ int get_block (int fd, int logical_block) { int ret; ret = ioctl (fd, FIBMAP, &logical_block); if (ret < 0) { perror ("ioctl"); return −1; } return logical_block; } /* * get_nr_blocks - returns the number of logical blocks * consumed by the file associated with fd */ int get_nr_blocks (int fd) { struct stat buf; int ret; ret = fstat (fd, &buf); if (ret < 0) { perror ("fstat"); return −1; } return buf.st_blocks; } /* * print_blocks - for each logical block consumed by the file * associated with fd, prints to standard out the tuple * "(logical block, physical block)" */ void print_blocks (int fd) { int nr_blocks, i; nr_blocks = get_nr_blocks (fd); if (nr_blocks < 0) { fprintf (stderr, "get_nr_blocks failed!\n"); return; } if (nr_blocks == 0) { printf ("no allocated blocks\n"); return; } else if (nr_blocks == 1) printf ("1 block\n\n"); else printf ("%d blocks\n\n", nr_blocks); for (i = 0; i < nr_blocks; i++) { int phys_block; phys_block = get_block (fd, i); if (phys_block < 0) { fprintf (stderr, "get_block failed!\n"); return; } if (!phys_block) continue; printf ("(%u, %u) ", i, phys_block); } putchar ('\N'); } int main (int argc, char *argv[]) { int fd; if (argc < 2) { fprintf (stderr, "usage: %s <file>\n", argv[0]); return 1; } fd = open (argv[1], O_RDONLY); if (fd < 0) { perror ("open"); return 1; } print_blocks (fd); return 0; }
- 장점
- 정확히 정렬하고 싶은 대상인 파일이 실제 존재하는 물리 디스크 블록을 반환한다
- 단점
- root 권한이 필요함.
- ioctl의
FIBMAP
이 root 권한이 필요한CAP_SYS_RAWIO
기능을 요구함