Monday, March 31, 2008

단순한 디자인

추가할 것이 더 이상 없을 때가 아니라 제거할 것이 더이상 없을 때, 디자이너는 완벽함에 도달했다는 것을 알게 된다.

Antoine de Saint-Exupery, 프랑스 소설가, 항공기 디자이너

Optimizing I/O Performance

간단하다. User space에서 할 수 있는 최적화란, 물리적인 블럭 순으로 I/O 작업을 요청하는 것이다. 이렇게 하기 위해 세가지 방법이 있는데,
  • 파일 경로를 보고 정렬해주는 방법 (단순하나, 별로...)
  • inode 번호를 보고 정렬해주기 (fragmentation 경우 별로이나, 대부분 이 방식을 쓴다.)
  • physical block 순으로 정렬하기 (다 좋은데 root 권한이 있어야 한다.)

inode 번호 대로 정렬하는 것은 stat() system call을 통해 사용할 수 있다.



#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int get_inode(int fd)
{
struct stat buf;
int ret;

ret = fstat(fd, &buf);

if (ret < 0) {
perror("stat");
return -1;
}

return buf.st_ino;
}

physical block 순으로 정렬하기 위해서는 ioctl() system call을 사용한다. 아래처럼 FIBMAP을 주면 해당하는 논리 블럭의 physical block 번호를 block에 넣어준다.


#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/fs.h>

int get_inode(int fd)
{
struct stat buf;
int ret;

ret = fstat(fd, &buf);

if (ret < 0) {
perror("stat");
return -1;
}

return buf.st_blocks;
}

int get_block(int fd, int logical_block)
{
if (ioctl(fd, FIBMAP, &logical_block) < 0) {
perror ("ioctl"); return -1;
}

return logical_block;
}

앞서 봤던 inode 값을 리턴하는 함수에서 buf.st_ino를 buf.st_blocks로 바꾸면 논리 블럭 갯수를 반환한다. 예를 들어 어떤 파일이 8개의 논리 블로으로 구성되어 8을 리턴했으면 이 파일은 0~7번의 논리 블럭을 가지고 있는 것이다. 그래서 이 0~7번을 ioctl 함수를 통해 각각에 대한 physical block의 번호를 받을 수 있고 이 값을 가지고 I/O 요청을 최적화할 수 있다.

I/O scheduler 고르기

/sys/block/device/queue/scheduler 에 아래와 같이 사용하고픈 scheduler를 등록하면 된다.

> echo cfq > /sys/block/hda/queue/scheduler

각 scheduler 들의 tuning 값들은 /sys/block/device/queue/iosched에 있다.

Saturday, March 29, 2008

I/O Schedulers

하드 디스크는 느리다. 프로세서는 빠르다. 그래서 느린 I/O가 bottle neck이 되지 않게 하기 위한 스케줄러가 필요하다. 리눅스에서 사용해온 스케줄러는 아래와 같다.
  • Linus Elevator
  • Deadline I/O Scheduler
  • The Anticipatory I/O Scheduler
  • The CFQ I/O Scheduler

CHS Addressing 은 Cylinder, Header, Sector 를 의미하는 것으로 하드 디스크의 데이터를 접근할 때 사용하는 물리 주소이다. 오늘 날의 디스크 들은 이러한 물리적 주소가 physical/device block address로 연결되어 있다. (몰라도 된다는 ...)

I/O Scheduler의 기능 은 두가지다. I/O 요청들의 mergingsorting.

Writes-starving-reads 2.4커널 이전에 있었던 현상인데, 연속적인 데이터를 write혹은 read한다고 치자. write 는 일단 버퍼 캐시에 쓰고 바로 리턴하기 때문에 실제로 연속적인 데이터 write가 일어난다. 하지만 read는 어떨까 write를 하고 있는 동안 연속적인 데이터의 read가 일어난다고 해보자. I/O 스케줄러가 들어온 요청 순서대로 일을 처리한다고 하면, read는 연속적으로 이루어지지 않는다. 한번의 요청에 일부분의 데이터를 읽어오고 또 다른 요청에 일부를 읽어오고,. 하는 식으로 동작하기 때문에 매우 느려진다. 요게 writes-starving-read 란다. (아,. 확실하진 않음 ㅡ.ㅡ;)

Linus Elevator 가 그래서 등장했다. 좀 Heuristic한 방법이긴 한데, 큐에서 요청을 (block number 순으로) Insertion sort 하다가 age가 오래 된 요청이 있으면 그거부터 처리하는 방식이다. 단순한 만큼 개선점이 많아 2.6커널 부터는 사라졌다.

Deadline I/O Scheduler 는 앞의 리누스 엘리베이터 에 작업 큐외에 별도의 read/write 큐를 둔다. 작업 큐에는 요청이 block 번호 순서로 정렬되어 있고, read/write큐는 FIFO로 (요청 들어온 순서대로) 들어간다. I/O Scheduler는 작업 큐에서 정렬된 요청들을 가지고 작업을 하다가 expiration time에 넘은 요청이 발견되면 그 요청을 가지고 있는 read(혹은 write)큐에가서 작업을 진행한다. 일반적으로 read 큐는 expiration time이 500ms로 짧고 write 큐는 5초정도로 준다.

Anticipatory I/O Scheduler 한번의 read 작업이 끝나고 다음 read작업이 일어나는데 방금 전 읽은 부분의 다음 부분이라고 가정해 보자. 디스크의 헤더는 이미 다른 위치로 이동해 있는 상태에서 다시 이전의 위치로 이동하여 작업을 수행하게 되는 낭비가 있다. 때문에 연속적인 read를 위해 한번의 read가 마쳐지면 최대 6ms 까지 아무일도 수행하지 않고 (헤드의 위치를 이동하지 않고) 대기해서 연속적인 read작업시 시간을 아끼는 방식의 스케쥴링을 anticipatory I/O scheduler라고 한다.

CFQ I/O Scheduler 가장 성능이 좋은 스케쥴러 인것으로 알고 있다. Complete Fair Queuing 의 약자 인데, 각 프로세스 마다 작업 큐를 가지고 있고 이것들이 round robin방식으로 돌며 정해진 time slice 내에서 작업을 수행하게 된다. time slice안에 작업을 모두 끝내게 되어도 10ms 정도를 추가로 기다리며 혹시나 있을 I/O 작업을 대기하다가 안들어오면 다른 프로세스의 큐로 이동한다. 각 큐에서는 synchronous 요청이 asynchronous보다 우선순위를 가지고 진행되어 writes-starving-reads 문제를 해결한다.

Noop I/O Scheduler 블럭 번호 순으로 정렬은 하지 않고 merging만 하는 스케쥴러 이다. storage종류에 따라 정렬이 필요하지 않은 경우가 있는데 이따 사용한다.

Thursday, March 27, 2008

When you build your own asynchronous I/O

직접 비동기 I/O를 구현해야하는 경우는 성능이나 특별히 수행해야하는 작업이 있을 때 인데, 참 고루한 작업이 되겠지만, 왠만하면 안하는게 좋겠지만, 어쩔 수 없이 해야한다면 아래와 같은 순서로 하자
  1. 모든 I/O 를 처리할 worker thread를 만든다.
  2. work queue에 I/O 작업을 할당할 수 있는 interface를 만든다.
  3. 각각의 thread가 큐에서 작업을 가져와 수행할 수 있도록 해준다.
  4. 작업이 마쳐지면 result queue에 결과를 얹어 놓게 한다.
  5. result queue에서 상태를 가져와 요청 루틴에 결과값을 반환하는 interface를 만든다.

synchronous, asynchronous, synchronized and nonsynchronized

미국사람들도 혼동스러워하는 위 네 단어의 뜻을 짚고 넘어가자. synchronous, asynchronous는 kernel buffer cache에 대해 동기화가 되냐 안되냐의 얘기이고, synchronized, nonsynchronized는 disk에 대해 동기화가 되냐 안되냐의 얘기이다.

read의 경우는 항상 synchronized이다(읽어야 보여주니까..). 그래서 read의 경우에는 buffer cache에 올라온 상태에서 리턴하냐 아니냐 (block이냐 non block이냐)에 따라서 synchronous, asynchronous 두가지 상태로 나눌 수 있다.

posix_fadvise(), readahead() - Advice for File I/O

File I/O 에 대해서는 posix_fadvise()를 통해 r/w 매커니즘을 효율적으로 사용하도록 커널에 hint를 줄 수 있다.

#include <fcntl.h>

int posix_fadvice (int fd, off_t offset, off_t len, int advice);

대부분 madvise()의 flag과 비슷하나 몇가지가 살짝 다른데,

  • POSIX_FADV_NORMAL

  • POSIX_FADV_RANDOM

  • POSIX_FADV_SEQUENTIAL : from low to high address

  • POSIX_FADV_WILLNEED

  • POSIX_FADV_NOREUSE : 한번만 쓰인다는 얘긴데, 지금은 WILLNEED와 동일하게 동작한다.

  • POSIX_FADV_DONTNEED


이 함수는 커널 2.6이후에 도입되었고 사실 리눅스에서는 그전에 readahead()로 같은 기능을 제공하고 있었다(이 함수는 POSIX_FADV_WILLNEED와 같은 기능을 한다).

#include <fcntl.h>

size_t readahead(int fd, off64_t offset, ssize_t count);

stream video 같은 경우에는 한번 display한 데이터는 캐시가 안되도 되기 때문에 POSIX_FADV_DONTNEED를 주면 좋다.

madvice()

느린 디스크 접근을 보완하기 위해 커널은 여러 매커니즘을 사용한다. mmap된 영역을 어떻게 사용할 것인지에 대해 커널에 미리 알려주면 이러한 매커니즘을 통해 성능을 향상하는데 큰도움이 된다. 이 때 사용하는 함수로 madvice()가 있다.

#include <sys/mman.h>

int madvice (void *addr, size_t len, int advice);

len에 0을 주면 addr로 맵핑된 모든 영역에 대해 적용되며 사용가능한 advice로 flag에 설정하는 값은 아래와 같다. (cf. read ahead window)

  • MADV_NORMAL : no specific advice

  • MADV_RANDOM : will be read random (read ahead - turned off)

  • MADV_SEQUENTIAL : read ahead will be maximized

  • MADV_WILLNEED : the pages will be cached

  • MADV_DONTNEED : the pages will be freed


비슷하게 posix_fadvise도 있는데, 이건 따로...

msync()

mmap된 영역을 디스크에 sync하는 함수이다. 인자로는 간단하게 주소, 크기, flag을 받는다. 이 함수와 함께 비교해볼 함수로 fsync()가 있다. write()함수 호출시에는 disk의 write queue에 들어가고 dirty 영역이 언젠가는 디스크에 쓰여지지만, mmap의 경우에는 msync를 호출하기 전에는 언제 dirty가 sync될지는 가늠할 수 없다. flag으로 MS_ASYNC/MS_SYNC를 줄 수 있고, MS_INVALIDATE를 주면 이 영역을 물고 있는 모든 cache를 invalidate시켜서 다음번 접근 부터는 sync 된 데이터를 사용할 수 있게 해준다.

mprotect()

이름에서 알 수 있다시피 특정 영역의 권한을 변경하는 함수이다.

#include <sys/mman.h>

int mprotect(const void *addr, size_t len, int prot);

linux에서는 mmap되지 않은 다른 공간에 대해서도 이 함수로 옵션을 줄 수 있으며, 옵션에는 PROT_NONE, PROT_READ, PROT_WRITE 가 있다.

mremap()

mmap된 영역의 크기를 변경하고 싶을 때는 mremap()을 사용하는데, 아래와 같다.

#include <unistd.h.>
#include <sys/mman.h>

void * mremap (void *addr, size_t old_size, size_t new_size, unsigned log flags);

glibc의 realloc()도 mremap을 사용하여 구현하는 경우가 있다. flag에는 0이나 MREMAP_MAYMOVE를 줄 수 있는데, MAYMOVE를 주면 맵핑된 주소를 커널이 옮겨도 된다는 의미이다(큰파일의 경우 효율을 위해 옮기는 것이 유리할 때가 있다.)

Wednesday, March 26, 2008

mmap()

파일을 메모리에 올려놓고 사용할 때 쓴느 mmap()은 아래와 같이 생겼다.

#include <sys/mman.h>
void * mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);

fd가 물고 있는 파일의 offset 주소부터 len만큼 메모리의 addr에 맵핑하는데, prot/flags 옵션을 준다..라고 생각하면된다. prot는 실행/읽기/쓰기 권한을 지정하는 것이고, flags는 메모리를 프로세스간 공유할 것인지 혹은 '꼭' addr 주소에 mmap 되야함을 알려주는 역할을 한다. mmap을 하게 되면 fd의 reference count가 한개 올라간다. 즉 mmap을 위한 fd가 생긴다. 때문에 mmap할 때 열었던 fd는 닫아도 된다. 새로 생성된 fd는 munmap하거나 프로세스가 종료하면 해제된다. 실제 사용예는 아래와 같다.

void *p;
p = mmap(0, len, PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) perror("mmap");

mmap은 페이지 길이 단위로 할당되기 때문에 주소가 이 align에 맞아야 한다. 길이가 page size의 정수배가 아닌경우 한 페이지 정도의 공간을 더 할당하고 남은 공간에 0을 채워준다(이 공간은 나중에 file에 sync되지 않는다) 페이지 길이를 확인하는 방법은 sysconf(), getpagesize(), PAGE_SIZE가 있는데 두번째 것은 리눅스에서만 지원하고 세번째는 컴파일 타임에 얻어오는 것이라 여러 아키텍쳐에서 돌릴 수 없다. 그러니 sysconf(_SC_PAGESIZE)로 값을 얻어오자. mmap()의 반환 값은 man을 참조하기로 하자. 이 놈은 더이상 유효하지 않은 영역에 접근하거나 읽기전용인 곳에 쓰려고 할때 시그널을 준다.

munmap()은 할당된 mmap을 해제할 때 쓰는데 아래와 같이 간단하다.

if(mmunmap(addr, len) == -1) perror("munmap");

간단히 mmap()의 장점을 요약하면 아래와 같다.

  • avoid extraneous copy (disk -> kernel buffer -> user buffer ...)

  • does not incur system call

  • mmaped data can be shared

  • use pointer instead of lseek()


반대로 mmap()의 단점을 요약하면 아래와 같다.

  • 페이지 단위로 맵핑되므로 작은 파일에 대해서는 공간 낭비가 있다. (4kb for 7 bytes file!)

  • fragmentation of large file in process address space at 32bit architecture

  • creating & maintaining overhead (can be reduced by double copy elimination)

Tuesday, March 25, 2008

epoll()

epoll()은 multiplexed I/O 를 구현한 함수 중의 하나로 모니터할 fd의 수가 많아질 때 이전에 제공되던 select, poll 보다 성능이 좋은 함수이다. epoll()은 이전 함수들과는 달리 create, control, monitor의 세 단계로 함수를 나누어 제공한다. 먼저 이벤트 구조체에 대해 알아놓을 필요가 있는데,

# include <sys/epoll.h>
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __attribute__ ((__packed__));

위 처럼 되어 있어 events에 EPOLLIN, EPOLLOUT 등 여러개의 모니터할 이벤트를 걸어놓을 수 있다. Union 으로 되어 있는 epoll_data에는 임의의 데이터를 넣을 수 있는데, 주로 관련 fd를 넣어 발생한 이벤트가 어떤 fd로 부터 였는지 확인할 때 사용한다. 이렇게 구조체를 사용하여 epoll은 아래의 세 함수를 통해 사용할 수 있다.

# include <sys/epoll.h>
// size는 fd 갯수
int epoll_create(int size);

// op = EPOLL_CTL_ADD, EPOLL_CTL_DEL 로 fd 추가,삭제
int epoll_ctl (int epfd,
int op,
int fd,
int struct epoll_event *event);

int epoll_wait (int epfd,
struct epoll_event *event,
int maxevents,
int timeout);
// epfd는 나중에 close()되어야 한다.

그래서 epoll 예제를 사용하는 예제를 하나 보면 아래와 같다.

# include <sys/epoll.h>

// 1. epoll create
int epfd;
// 대략 ~100개의 fd를 관찰할 계획
epfd = epoll_create(100);
if (epfd<0) perror("epoll_create");

// 2. add fd with events
struct epoll_event event;
int ret;
event.data.fd = fd; //모니터할 fd
event.events = EPOLLIN | EPOLLOUT;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
if (ret<0) perror("epoll_ctl");

// 3. monitoring
// 발견된 event들이 담길 배열의 포인터
struct epoll_event *events;
#define MAX_EVENTS 64
// 위에서 사용한 epfd를 쓰는 것인지 모르겠음
int nr_events, i, epfd2;
events = malloc(sizeof(struct epoll_event) * MAX_EVENTS);
if (!events) perror ("malloc");
// 무한 타임아웃
nr_events = epoll_wait(epfd2, events, MAX_EVENTS, -1);
if (nr_events<0) perror("epoll_wait");
for (i=0; i<nr_events; i++)
{
printf("event=%ld on fd=%d\n",
events[i].events,
events[i].data.fd);
}

Monday, March 24, 2008

Scatter gather I/O

Scatter Gather I/O는 vector를 사용하는 read/write이다. 일반적으로 사용되는 메모리 공간들은 하나의 연속적인 큰 공간이기 보다는 작지만 리스트 처럼 서로 연결되어 있는 공간인 경우가 많다. 이러한 공간에 read, write 작업을 수행할 때 각 단편화된(fragmented) 메모리에 대한 정보를 벡터를 통해 넘겨주어 한번의 read, write 작업으로 끝낼 수 있다. 넘겨주는 벡터의 모양은 아래와 같다.

#include <sys/uio.h>

struct iovec {
void *iov_base;
size_t iov_len;
};

이러한 벡터 iovec의 배열을 넘겨주어 Scatter Gather I/O를 수행하는데, 이때의 readv(), writev() 함수는 아래와 같은 식으로 호출한다.

#include <sys/uio.h>

ssize_t readv(int fd,
const struct iovec *iov,
int count);

ssize_t writev(int fd,
const struct iovec *iov,
int count);

실제 Scatter Gather I/O는 PCI 콘트롤러의 Scatter Gather DMA Mode를 통해 이루어진다(CPU의 도움없이 PCI 콘트롤러 상에서 벡터의 주소를 따라가며 read, write 작업을 수행해준다.) 때문에 프로세서의 자원 절약 및 성능 개선의 여러 효과가 있다.

Wednesday, March 19, 2008

함수 포인터 그리고 (*(void(*)())0)();

(*(void(*)())0)();

자,. 이게 무슨 얘기일까. 일단 함수 포인터에 대한 이해가 있어야 한다. int *f(); 는 int *를 리턴하는 함수를 의미한다. 하지만 int (*f)(); 는 정수를 리턴하는 함수에 대한 포인터 이다. 같은 식으로 void (*f)()는 리턴이 void인 함수의 포인터이다. 그럼 이런 함수 포인터로 캐스팅하려면? (void (*)()) 처럼 쓰면된다. 그래서 주소0을 반환 값이 void인 함수의 포인터로 캐스팅 하면 (void(*)())0 이 된다. 포인터 p의 실제 값은 *p가 되듯이 이 함수 포인터를 통해 실제 함수를 호출하려면 마찬가지로 (*(void(*)())0)(); 하면 된다.

결국 이 얘기는 주소0에 있는 코드를 반환값이 없는 함수의 포인터로 캐스팅 후에 그 함수를 호출한다는 얘기다. 임베디드 시스템에서 부팅 후 제일 처음 수행되는 루틴을 C로 짜게되는 경우 위와같은 코드가 나오게 된다. 흐,. 괴물같다.

Tuesday, March 18, 2008

read ahread and write back

1. read ahead

간단하다. sequential locality 때문에 한번에 읽을 때 근처의 다른 데이터도 같이 읽어 메모리에 올려 놓는다. 리눅스에서는 read ahead의 양이 동적으로 변화 한다. 12kb~128kb 로 변화하는데 이를 read ahead window로 구현해 놓았다.

2. write back

write back이 발생하는 시점은 두가지 이다.
  1. 메모리가 부족하거나
  2. dirty page의 age가 너무 오래되었을때

이다. 모든 경우에 pdflush 라는 thread가 생겨나서 작업을 진행한다. (bdflush 시절에는 한번에 한 block device 만 진행할 수 있었음)

Write back에서 사용하는 버퍼는 커널의 buffer_head 구조체를 통해 구현되어 있다. 근데 이 버퍼가 사실은 dirty 정보 뿐만 아니라 실제 data에 대한 pointer도 가지고 있다.

그래서 2.4 커널 이후에서는 page cache와 buffer cache가 통합되었다.

Page Cache

시스템의 하드디스크는 매우느리다. 그래서 read/write 동작시 성능 저하를 가져올 수 있다. 그래서 디스크의 데이터 일부를 메모리에 올려놓고 사용하는 것을 페이지 캐시라고 생각하면 된다. 반대의 개념으로 메모리 swap을 생각할 수 있는데, 메모리 스왑은 사용되지 않고 있는 메모리의 데이터 일부를 디스크에 저장하는 것이다. 경험상 메모리 스왑은 엄청난 성능 저하를 가져온다. 시스템에서는 메모리에 접근하는 것으로 생각하고 작업을 진행하지만 사실을 매우 느린 디스크를 통해 데이터를 조작하기 때문이다. 리눅스에서는 이러한 페이지 캐시와 메모리 스왑의 비율을 /proc/sys/vm/swapiness 의 값을 통해 설정한다. 일반적으로 60정도 인데, 값이 높을 수록(100에 가까울 수록) 페이지 캐시/메모리 스왑을 많이 한다.

Virtual File System

리눅스에서 말하는 가상파일시스템은 여러 종류의 파일 시스템을 동일한 종류의 system call을 통해 접근하고 사용할 수 있도록 해주는 일종의 abstraction 이라고 생각하면된다. 실예로, 윈도우즈의 fat이나 ntfs등을 리눅스의 ext 파일 시스템과 동시에 사용할 수 있도록 해준다. Run time에서 해당 시스템콜이 어떤 파일 시스템에 접근하려고 하는지를 확인한 후 접근하려는 파일 시스템의 구현된 함수를 연결하여 작업을 수행해준다.

poll()

poll() 역시 Multiplexed I/O 구현할 수 있는 함수 중의 하나이다.

  • Data types
    struct pollfd {
    int fd;
    short events; // 감시할 이벤트
    short revents; // 그것들 중 발생한 이벤트
    }
  • Headers
    unistd.h, sys/poll.h
  • events에서 사용하는 flags 설명
    - read 관련
    POLLIN
    POLLPRI
    POLLRDNORM
    POLLRDBAND
    (POLLIN + POLLPRI = 셀렉트()의 read와 같다.)
    - write 관련 flag
    POLLOUT
    POLLWRBAND
    POLLWRNORM
    (POLLOUT + POLLWRBAND = 셀렉트()의 write 와 같다.)
  • poll()
    int poll(struct pollfd *fds, unsigned int nfds, int timeout);
    fds는 pollfd의 배열이고,
    nfds는 그것의 갯수.
  • 에러 값들은 ... man 페이지를 참조하자.
  • 참고로, Unix 시스템은 poll()을 지원하지 않는다.

셀렉트 함수

지난 번 셀렉트를 여엉 단어로 너무 많이 썼다가 스펨 블로그로 필터링되었으므로, 그냥 한글로 '셀렉트'로 표기한다 ㅠㅠ

  • 셀렉트 함수를 간단하게 설명하면, 여러개의 file descriptor를 non-block 하게 사용하기 위해서 만들어진 함수이고, fd_set 구조체로 된 fd 포인터 배열과 timeout 값을 인자로 넣어주면 조건에 만족하는 fd가 생기거나 timeout에 걸릴 때까지 sleep한다. (때문에 경우에 따라서는 sleep 으로 사용되는 경우도 있다)
  • pselect 라고 timespec 을 timeout 값으로 사용하는 함수도 있다. (timespec은 nano second값까지 설정할 수 있는 time 구조체이다.)

Wednesday, March 5, 2008

select()

select() 함수는 아래 처럼 사용되는데, 인자를 순서대로 설명하면, 등록하는 여러개의 fd들의 값중 제일 큰 값 + 1 이 첫번째 인자인데, select()가 fd의 처음부터(0아니면 1,..) 이값 n 사이의 fd를 check하도록 되어 있기 때문이다. 두번째 인자 부터는, 읽을 수 있는 fd 집합, 쓸 수 있는 fd 집합, 특정 exception이 일어나는 fd집합 그리고 timeout 값이다.


int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct time_val *timeout);

// Helper macros
FD_ISSET(int fd, fd_set *set); // fd가 set되 있는지
FD_SET(int fd, fd_set *set); // fd를 set시킴
FD_CLR(int fd, fd_set *set); // fd를 unset 시킴
FD_ZERO(fd_set *set); // 모든 fd를 unset 함


그래서 select()는 등록된 fd 중 조건에 만족하는 fd들중 제일 큰 값을 리턴하고, timeout에 걸린 경우 0을 리턴한다. 위 처럼 같이 제공되는 매크로를 사용하면 편리하다(제대로 코드를 짰다면 FD_CLR은 사용할 일이 없어야 한다). select()가 리턴되었을때 사용가능한 fd들을 사용하고 나서 다시 호출전에 timeout값을 다시 설정 해줘야한다. (리눅스에서는 리턴될때 timeout 값을 'timeout - elapsed time' 으로 설정해서 리턴해준다. 리눅스에서 동시에 사용할 수 있는 파일 수는 FD_SETSIZE 값과 같이 1,024개임도 유념하자. select()가 에러를 리턴하는 경우는 아래의 네가지 이다.
  1. EBADF - invalid fd
  2. EINTR - signal 받음
  3. EINVAL - n이 음수거나 timeout값 설정이 이상함
  4. ENOMEM - out of memory

Multiplexed I/O

file read/write처럼 device I/O를 사용하는 경우 이로 인해 프로그램이 block될 수 있다. 그렇다고 nonblocking I/O를 사용하면 매번 리턴되는 에러들을 확인해야하니 polling 이나 다름 없다(비효율적이다). 여러개의 I/O를 사용하는데 이중 하나가 활성화 되지 전까지 sleep할 수 있다면, 그 사이에 다른 프로세스들이 작업을 진행할 수 있으니 좋을 것이다. 그래서 이때 사용하는 것이 multiplexed I/O이다. 즉, 간단하게

  1. 특정 fd가 준비되면 알려달라고 설정하고,
  2. 그전까지는 sleep하는 것

이다. 대표적인 함수로 select(), poll() 이 있다.

새로운 블로그의 시작

네이버 블로그 화면이 너무 번잡스러워 나름 단순한 화면을 가진 구글 블로거 서비스를 사용한 새로운 블로깅을 시작했다. 이 블로그는 주로 컴퓨터 엔지니어링과 관련된 블로그를 올리는 것으로 사용할 계획이다.