Libevent R4: Working with events
Overview
원문은 이곳<ref>http://www.wangafu.net/~nickm/libevent-book/Ref4_event.html</ref>에서 확인할 수 있다.
Libevent의 기본 실행 단위는 event(이벤트)이다. 모든 이벤트는 상태를 가지게 되는데 이는 다음과 같다.
- 파일 디스크립터의 read/write 준비 완료
- 파일 디스크립터의 read/write 준비 중(Egde-triggered IO Only)
- timout 만료
- signal 발생
- 사용자-정의 trigger
이벤트들은 비슷한 주기를 가지게 되는데, 한번 이벤트를 생성하게 되면 initialized 상태가 된다. 이후 event_base 에 등록하게 되면 pending 상태가 된다. 이벤트가 pending 상태에서 특정 조건을 만족하여 trigger(readable/writable, timeout expire) 되게 되면, active 상태가 되면서 등록된 callback 함수가 실행된다. 만약 이벤트가 persistent 로 설정되었다면, 다시 pending 상태로 돌아간다. 만약 persistent 가 아니라면, pending으로 돌아가지 않는다. 이벤트 delete 를 함으로써 pending 상태에서 non-pending 상태로 바꿀 수 있으며, add 를 이용해서 non-pending 에서 pending으로 변경할 수도 있다.
Constructing event objects
새로운 이벤트를 생성하기 위해서는 event_new() 인터페이스를 사용하면 된다.
- Interface
<source lang=c>
- define EV_TIMEOUT 0x01
- define EV_READ 0x02
- define EV_WRITE 0x04
- define EV_SIGNAL 0x08
- define EV_PERSIST 0x10
- define EV_ET 0x20
typedef void (*event_callback_fn)(evutil_socket_t, short, void *);
struct event *event_new(struct event_base *base,
evutil_socket_t fd, short what, event_callback_fn cb, void *arg);
void event_free(struct event *event); </source>
event_new() 함수는 같이 넘겨지는 인자 *base 에서 동작하는 이벤트를 생성한다. 그리고 what 인자는 flag 셋인데, 사용가능한 flag 가 위쪽에 나와있다. 만약 fd 가 양수 값으로 설정됐다면, read/write 하고자 하는 파일 디스크립터 값으로 간주된다. 이벤트가 active 되면, 이벤트는 여기서 등록된 cb 함수를 다음의 인자 값과 함께 실행시킨다(fd : 파일 디스크립터, arg: 이벤트 생성시 넘겨준 arg 값)
오류 발생시, event_new() 는 NULL 을 리턴한다.
모든 이벤트는 생성 직후 initialized 상태 혹은 non-pending 상태가 된다. pedning 상태로 만들기 위해서는 event_add() 를 호출해야 한다.(아래에서 설명함)
이벤트를 삭제(메모리 해제)하고 한다면 event_free() 를 사용해야 하며, 해당 이벤트가 pending/active 상태 중에서도 사용 가능하다.(해당 이벤트가 non-pending/inactive 상태일 때 삭제된다)
- Example
<source lang=c>
- include <event2/event.h>
void cb_func(evutil_socket_t fd, short what, void *arg) {
const char *data = arg; printf("Got an event on socket %d:%s%s%s%s [%s]", (int) fd, (what & EV_TIMEOUT)? " timeout" : "", (what & EV_READ)? " read" : "", (what & EV_WRITE)? " write" : "", (what & EV_SIGNAL)? " signal" : "", data);
}
void main_loop(evutil_socket_t fd1, evutil_socket_t fd2) {
struct event *ev1, *ev2; struct timeval five_seconds = {5, 0}; struct event_base *base = event_base_new(); /* The caller has already set up fd1, fd2 somehow, and make them * nonblocking. */ ev1 = event_new(base, fd1, EV_TIMEOUT|EV_READ|EV_PERSIST, cb_func, (char*)"Reading event"); ev2 = event_new(base, fd2, EV_WRITE|EV_PERSIST, cb_func, (char*)"Writing event"); event_add(ev1, &five_seconds); event_add(ev2, NULL); event_base_dispatch(base);
} </source>
The event flags
- EV_TIMEOUT
- 같이 설정한 시간뒤에 active 된다.
- EV_READ
- 등록한 파일 디스크립터가 Read 가능한 상태일 때 active 된다.
- EV_WRITE
- 등록한 파일 디스크립터가 Write 가능한 상태일 때 active 된다.
- EV_SIGNAL
- 등록한 시그널이 감지될 때 active 된다.
- EV_PERSIST
- 해당 이벤트는 "영구적"임을 나타낸다.
- EV_ET
- event_base 의 백엔드가 Edge-trigger 를 지원할 때, 해당 이벤트가 Edge-triggered 를 사용한다는 것을 나타낸다. EV_READ/EV_WRITE 의 Edge-trigger 버전이다.
About Event Persistance
기본적으로 이벤트가 pending 상태에서 active 상태가 되면, 등록된 callback 함수가 실행된 직후, non-pending 상태가 된다. 따라서 만약 해당 이벤트를 다시금 pending 상태로 만들고 싶다면, 콜백 함수 내에서 event_add() 통해 다시금 이벤트를 등록시켜주어야 한다.
하지만 만약 EV_PERSIST 플래그가 세팅되어 있다면, 이벤트는 영구적이 된다. 무슨 말이냐면, 이벤트가 active 상태가 되어 콜백을 실행시킨 이후에 다시 pending 상태로 돌아온다는 뜻이다. 만약 non-pending 상태로 만들고 싶다면, event_del() 를 사용하면 된다.
timeout과 persistent 옵션을 같이 사용하게 되면, timout 와 trigger 를 공유하는 형태가 된다. 예를 들어 EV_READ|EV_PERSIST 와 timeout 5초를 함께 설정하게 되면 다음과 같이 된다.
- 소켓이 read 가능하면 active된다.
- 마지막으로 active 된 지 매 5 초 후마다 active 된다.
Creating an event as its own callback argument
때때로, 콜백 함수 자신을 인자값으로 처리해야 하는 경우가 있을 수 있다. 하지만 이벤트 자체를 그냥 포인터로 넘겨줄 수는 없다. 왜냐하면 아직 생성되지 않았기 때문이다. 실제로 이벤트가 active 되고 실행하는 순간 callback 메모리가 할당되기 때문이다. 따라서 이를 위해서는 별도의 event_self_cbarg() 함수를 사용해야 한다.
- Interface
<source lang=c> void *event_self_arg(); </source>
event_self_cbarg()는 콜백 함수를 가리키는 "매직 포인터"를 리턴해준다.
- Example
<source lang=c>
- include <event2/event.h>
static int n_calls = 0;
void cb_func(evutil_socket_t fd, short what, void *arg) {
struct event *me = arg; printf("cb_func called %d times so far.\n", ++n_calls); if(n_calls > 100) { event_del(me); }
}
void run(struct event_base *base) {
struct timeval one_sec = {1, 0}; struct event *ev; /* We're going to set up a repeating timer to get called called 100 * times. */ ev = event_new(base, -1, EV_PERSIST, cb_func, event_self_cbarg()); event_add(ev, &one_sec); event_base_dispatch(base);
} </source> event_new(), evtimer_new(), evsignal_new(), event_assign(), evtimer_assign, evsignal_assign() 등과 함께 사용할 수 있다.
Timeout-only events
evtimer_ 로 시작하는 매크로 들이 있다. event_ 로 시작하는 함수와 timer 관련 옵션을 함께 설정할때 사용가능한데, 이를 사용하면 번거로운 작업을 줄일 수 있다.
- Interface
<source lang=c>
- define evtimer_new(base, callback, arg) \
event_new(base), -1, 0, (callback), (arg))
- define evtimer_add(ev, tv) \
event_add((ev), (tv))
- define evtimer_Del(ev) \
event_del(ev)
- define evtimer_pending(ev, tv_out) \
event_pending((ev), EV_TIMEOUT, (tv_out))
</source>
Constructing signal events
Libevent는 POSIX 시그널도 감지가 가능하다.
- Interface
<source lang=c>
- define evsignal_new(base, signum, cb, arg) \
event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)
</source> event_new 사용법과 같은데, 파일 디스크립터 대신 시그널 번호만 넘겨주는 것이 다르다.
- Example
<source lang=c> struct event *hup_event; struct event_base *base = event_base_new();
/* call sighup_function on a HUP signal */ hup_event = evsignal_new(base, SIGHUP, sighup_function, NULL); </source> 등록한 시그널 콜백은 시그널이 발생될때 이벤트 루프에서 호출된다. 따라서 시그널 콜백 사용시, POSIX 시그널 핸들러에 아무처리를 하지않도록 따로 설정해놓는 것이 안전하다.
signal event 등록은 위한 매크로 역시 존재한다.
- Interface
<source lang=c>
- define evsignal_add(ev, tv) \
event_add((ev), (tv))
- define evsignal_del(ev) \
event_del(ev)
- define evsignal_pending(ev, what, tv_out) \
event_pending((ev), (what), (tv_out))
</source>
- Caveats when working with signals
- Libevent 에서 시그널을 등록해서 사용할 때 주의해야할 점이 있다. 프로세스당 오직 하나의 event_base 에서만 시그널을 수신할 수 있다는 것이다. 만약 하나의 프로세스에서 여러개의 event_base 를 생성하고 각각의 event_base 마다 시그널을 등록했을 경우, 오직 하나의 event_base 에서만 시그널을 수신하게 된다는 것이다. 시그널의 번호가 모두 달라도 안된다.
- 단, kqueue 에서는 제약이 없다.
Setting up events without heap-allocation
성능, 혹은 다른 이유로 힙영역 이외의 다른 곳에 이벤트 메모리를 관리할 수 있다. 이 기능을 사용하게 되면 다음의 문제들을 관리할 수 있다.
- 작은 양의 메모리를 heap 영역에 할당하게 되면 오버헤드가 발생한다.
- struct event pointer 를 역 조회하게되면 time overhead 가 발생한다.
- 만약 이벤트가 캐시에 없는 경우, 이를 위한 time-overhead 가 발생할 수 있다.
이를 위해서는 event_assign() 을 사용하면 된다. 하지만 이를 사용하게 되면 다른 버전의 Libevent 와의 바이너리 호환성을 깨트릴 수 있다. 왜냐하면 이벤트 구조체의 사이즈가 달라질 수 있기 때문이다.
위에 나열한 문제들은 굉장히 작은 문제들이다. 그리고 거의 대부분의 어플리케이션에서는 문제되지 않는다. 이벤트 메모리를 다른 곳에 관리함으로써 생기는 문제점들에 대해 정확히 알고있지 않은 한, event_new() 를 사용하길 바란다. event_assign()을 사용하게 되면 다른 버전의 Libevent(만약 이벤트 구조체 크기가 달라졌다면)와 연동했을 경우, 에러를 찾기 힘들어진다.
- Interface
<source lang=c> int event_assign(struct event *event,
struct event_base *base, evutil_socket_t fd, short what, void (*callback)(evutil_socket_t, short, void *), void *arg);
</source> event 인자값을 제외한, 모든 인자값은 event_new() 에서 사용하는 것과 같다. 성공시 0, 실패시 -1을 리턴한다.
- Example
<source lang=c>
- include <event2/event.h>
/* Watch out! Including event_struct.h means that your code will not
* be binary-compatible with future versions of Libevent. */
- include <event2/event_struct.h>
- include <stdlib.h>
struct event_pair {
evutil_socket_t fd; struct event read_event; struct event write_event;
}; void readcb(evutil_socket_t, short, void *); void writecb(evutil_socket_t, short, void *); struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd) {
struct event_pair *p = malloc(sizeof(struct event_pair)); if(!p) return NULL; p->fd = fd; event_assign(&p->read_event, base, fd, EV_READ|EV_PERSIST, readcb, p); event_assign(&p->write_event, base, fd, EV_WRITE|EV_PERSIST, writecb, p); return p;
} </source>
- Warning
절대로 pending 중인 이벤트에 대해 event_assign() 를 사용하면 안되다. 만약 그런 일을 하게되면 정말로 오류가 발생해도 원인을 찾을 수 없게 된다. 만약 이미 Initialized 됐고, pending 상태의 이벤트라면, event_del() 을 먼저 사용하고, event_assign()을 사용해야 한다.
assign 과 관련된 매크로 함수들 역시 존재한다.
- Interface
<source lang=c>
- define evtimer_assign(event, base, callback, arg) \
event_assign(event, base, -1, 0, callback, arg)
- define evsignal_assign(event, base, signum, callback, arg) \
event_assign(event, base, signum, EV_SIGNAL|EV_PERSIST, callback arg)
</source>
event_assign() 을 사용하면서도 추후의 Libevent 라이브러리와 바이너리 호환성을 유지해야 한다면, 사용중인 라이브러리의 struct event 크기를 확인할 수 있다.
- Interface
<source lang=c> size_t event_get_struct_event_size(void); </source>
이 함수는 현재 사용중인 이벤트 구조체의 바이트 크기를 리턴한다. This function returns the number of bytes you need to set aside for a struct event. As before, you should only be using this function if you know that heap-allocation is actually a significant problem in your program, since it can make your code much harder to read and write.
Note that event_get_struct_event_size() may in the future give you a value smaller than sizeof(struct event). If this happens, it means that any extra bytes at the end of struct event are only padding bytes reserved for use by a future version of Libevent.
Here’s the same example as above, but instead of relying on the size of struct event from event_struct.h, we use event_get_struct_size() to use the correct size at runtime.
- Example
<source lang=c>
- include <event2/event.h>
- include <stdlib.h>
/* When we allocate an event_pair in memory, we'll actually allocate
* more space at the end of the structure. We define some macros * to make accessing those events less error-prone. */
struct event_pair {
evutil_socket_t fd;
};
/* Macro: yield the struct event 'offset' bytes from the start of 'p' */
- define EVENT_AT_OFFSET(p, offset) \
((struct event*) (((char*)(p)) + (offset))
/* Macro: yield the read event of an event_pair */
- define READEV_PTR(pair) \
EVENT_AT_OFFSET((pair), sizeof(struct event_pair))
/* Macro: yield the write event of an event_pair */
- define WRITEEV_PTR(pair) \
EVENT_AT_OFFSET((pair), sizeof(struct event_pair) + event_get_struct_event_size())
/* Macro: yield the actual size to allocate for an event_pair */
- define EVENT_PAIR_SIZE() \
(sizeof(struct event_pair) + 2 * event_get_struct_event_size())
void readcb(evutil_socket_t, short, void *); void writecb(evutil_socket_t, short, void *); struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd) {
struct event_pair *p = malloc(EVENT_PAIR_SIZE()); if (!p) return NULL; p->fd = fd; event_assign(READEV_PTR(p), base, fd, EV_READ|EV_PERSIST, readcb, p); event_assign(WRITEEV_PTR(p), base, fd, EV_WRITE|EV_PERSIST, writecb, p); return p;
} </source>
Making events pending and non-pending
한번 이벤트를 생성하면, 이벤트를 루프에 추가해서 pending 상태로 만들기 전까지는 아무것도 하지 않는다. 루프에 추가하기 위해서는 event_add() 를 사용한다.
- Interface
<source lang=c> int event_add(struct event *ev, const struct timeval *tv); </source>
event_add() 를 호출하면 이벤트를 event_base 루프에 추가해서 pending 상태로 만들 수 있다. 성공시 0, 실패시 -1을 리턴한다. timeout 을 지정하고 싶지 않다면, tv를 NULL 로 입력하면 된다. timeout은 마이크로 초 단위까지 설정할 수 있다.
만약 이미 pending 상태인 이벤으를 다시 event_add()로 추가한다면, pending 상태가 해제되고, 새로 설정된 timout 으로 다시 스케쥴링 된다. 만약 timeout 을 NULL 로 설정하여 재추가하게 되면, 아무런 일도 일어나지 않는다.
- Note
tv 변수에 timeout 목표 시간을 입력하면 안된다. 예를 들어 "tv->tv_sec = time(NULL)+10;"이라고 입력했다면 입력한 시간으로부터 10초 뒤가 아닌, 40년 뒤에 타이머가 작동될 것이다.
- Interface
<source lang=c> int event_remove_timer(struct event *ev); </source> 드디어 타이머 이벤트를 삭제할 수 있는 함수가 나왔다. 만약 이벤트가 이벤트가 timeout pending 상태이며, IO 컴포넌트, 시그널 컴포넌트가 없는 상태일 때 사용가능하며, event_del() 함수와 같은 역할을 수행한다. 성공시 0, 실패시 -1을 리턴한다.
Events with priorities
여러개의 이벤트들이 같이 trigger 되었을 때, Libevent는 어떤 콜백이 먼저 수행되어야 하는지 정해진 규칙이 없다. 하지만 중요한 콜백들이 실행 우선권을 가질 숭 씨도록 우선 순위를 지정할 수 있다.
앞서 설명했듯이 event_base는 하나 이상의 우선순위를 설정할 수 있고, 초기화 직후, 이벤트를 추가하기 전에 설정 가능하다.
- Interface
<source lang =c> int event_priority_set(struct event *event, int priority); </source> 이벤트에 부여 가능한 우선순위 번호는 0부터 event_base 에 설정한 우선순위 갯수 - 1 까지이다. 성공시 0, 실패시 -1을 리턴한다.
여러개의 우선순위로 이루어진 이벤트들이 동작할때, 낮은 우선 순위를 가진 이벤트는 동작하지 않는다. 대신에, Libevent는 높은 우선 순위를 가진 이벤트를 실행시킨다. 그리고 다시 이벤트를 검색한다. active 상태인 높은 우선순위 이벤트가 없을때 낮은 우선순위의 이벤트를 실행시킨다.
- Example
<source lang=c>
- include <event2/event.h>
void read_cb(evutil_socket_t, short, void *); void write_cb(evutil_socket_t, short, void *);
voie main_loop(evutil_socket_t fd) {
struct event *importatn, *unimportant; struct event_base *base; base = event_base_new(); event_base_priority_init(base, 2); /* Now base has priority 0, and priority 1 */ important = event_new(base, fd, EV_WRITE|EV_PERSIST, write_cb, NULL); unimportant = event_new(base, fd, EV_READ|EV_PERSIST, read_cb, NULL); event_priority_set(important, 0); event_priority_set(unimportant, 1); /* Now, whenever the fd is ready for writing, the write callback will * happen before the read callback. The read callback won't happen at * all until the write callback is no longer active. */
} </source>
별도로 우선순위를 설정하지 않았다면, 기본값으로 event_base 에서 사용가능한 우선순위 갯수 / 2 가 된다.
Inspecting event status
이벤트가 추가됐는지 혹은 이벤트가 어떤 상태인지 알고 싶을 때가 있다.
- Interface
<source lang =c> int event_pending(const struct event *ev, short what, struct timeval *tv_out);
- define event_get_signal(ev) /* ... */
evutil_socket_t event_get_fd(const struct event *ev); struct event_base *event_get_base(const struct event *ev); short event_get_events(const struct event *ev); event_callback_fn event_get_callback(const struct event *ev); void *event_get_callback_arg(const struct event *ev); int event_get_priority(const struct event *ev);
void event_get_assignment(const struct event *event,
struct event_base **base_out, evutil_socket_t *fd_out, short *events_out, event_callback_fn *callback_out, void **arg_out);
</source> event_pending() 함수는 현재 이벤트가 pending 상태인지 active 상태인지를 리턴한다. 만약 what 인자에 EV_READ, EV_WRITE, EV_SIGNAL, EV_TIMEOUT 이 설정되어 있었다면, 모든 플래그에 대해서 pending 인지 active 인지를 리턴한다. 만약 tv_out 인자에 값이 설정되고, what인자에 EV_TIMEOUT 이 설정되었고, 이벤트가 현재 timout 으로 pending/active 상태라면 tv_out 에는 이벤트가 언제 expire 되는지 입력된다.
event_get_fd(), event_get_signal() 함수는 각기 설정된 파일 디스크립터와 시그널 번호를 리턴한다. event_get_base() 함수는 이벤트가 설정되어 있는 event_base 를 리턴한다. 그리고 event_get_events() 함수는 이벤트에 설정된 플래그들(EV_READ, EV_WRITE 등)을 리턴한다. event_get_call(), event_get_callback_arg() 함수는 등록된 콜백 함수와 인자값을 돌려주며, event_get_priority() 함수는 설정된 우선순위 값을 돌려준다.
event_get_assignment() 함수는 이벤트에 설정된 모든 정보들을 포인터로 넘겨주는데, NULL로 인자값은 설정되어있다면 그냥 무시한다.
- Example
<source lang=c>
- include <event2/event.h>
- include <stdio.h>
/* Change the callback and callback_arg of 'ev', which must not be
* pending */
int replace_callback(struct event *ev, event_callback_fn new_callback,
void *new_callback_arg)
{
struct event_base *base; evutil_socket_t fd; short events; int pending; pending = event_pending(ev, EV_READ|EV_WRITE|EV_SIGNAL|EV_TIMEOUT, NULL); if(pending) { /* We want do to catch this here so that we do not re-assign a * pending event. That would be very very bad. */ fprintf(stderr, "Error! replace_callback called on a pending event!\n"); return -1; } event_get_assignment(ev, &base, &fd, &events, NULL, /* ignore old callback */ NULL /* ignore old callback argument */); event_assign(ev, base, fd, events, new_callback, new_callback_arg); return 0;
} </source>
Finding the currently running event
디버깅 혹은 다른 이유로 현재 동작중인 이벤트의 포인터가 필요할 때가 있다.
- Interface
<source lang=c> struct event *event_base_get_running_event(struct event_base *base); </source> 현재 루프 중인 event_base 에서만 유효하다는 것을 알아두자. 또한 다른 스레드에서 동작중인 event_base 의 정보를 가져오는 것에 대해서는 결과를 장담할 수 없다.
Configuring one-off events
둘 이상의 이벤트를 추가할 필요가 없거나 추가된 이벤트를 삭제하고 싶고, 영구적일 필요도 없다면 event_base_once() 를 사용하면 된다.
- Interface
<source lang=c> int event_base_once(struct event_base *, evutil_socket_t, short, void(*)(evutil_socket_t, short, void *), void *, const struct timeval *); </source> 이 함수에서 사용하는 인자 값들은 EV_SINGAL과 EV_PERSIST 를 지원하지 않는다는 것만 빼고 event_new() 와 같다. 추가된 이벤트는 기본 우선순위 값으로 한번만 스케쥴링 되며, 실행 이후에는 자체적으로 메모리 해제가 된다. 성공시 0, 실패시 -1을 리턴한다.
event_base_once()로 추가된 이벤트는 삭제되거나 수동으로 active 시킬 수 없다. 만약 이벤트를 삭제하거나 수동으로 active 시키고 싶다면 event_new() 혹은 event_assign() 을 사용하면 된다.
Manually activating an event
간혹, 수동으로 이벤트를 active 시켜야 할 때가 있다.
- Interface
<source lang=c> void event_active(struct event *ev, int what, short ncalls); </source> 이 함수는 이벤트를 what (EV_READ, EV_WRITE, EV_TIMEOUT) 인자값에 맞추어 active 시기는 기능을 한다. 이벤트가 반드시 pending/active 상태일 필요는 없다.
다만, 이 함수를 이벤트 내에서 재귀적으로 호출한다면 시스템 자원을 엄청나게 소모하는 현상이 발생한다. 다음의 예를 보자.
- Bad Example: making an infinite loop with event_active()
<source lang=c> struct event *ev;
static void cb(int sock, short which, void *arg) {
/* Whoops: Calling event_active on the same event unconditionally * from within its callback means that no other events might not get * run! */ event_active(ev, EV_WRITE, 0);
}
int main(int argc, char **argv) {
struct event_base *base = event_base_new(); ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL); event_add(ev, NULL); event_active(ev, EV_WRITE, 0); event_base_loop(base, 0); return 0;
} </source> 위의 코드는 이벤트 루프를 한번만 수행하고, 이후부터는 cb 콜백 함수는 무한정 호출 하는 코드이다.
- Example: Alternative solution to the above problem using times
<source lang=c>
- include <event2/event.h>
struct event *ev; struct timeval tv;
static void cb(int sock, short which, void *arg) {
if(!evtimer_pending(ev, NULL)) { event_del(ev); evtimer_add(ev, &tv); }
}
int main(int argc, char **argv) {
struct event_base *base = event_base_new(); tv.tv_sec = 0; tv.tv_usec = 0; ev = evtimer_new(base, cb, NULL); evtimer_add(ev, &tv); event_base_loop(base, 0); return 0;
} </source>
- Example: Alternative solution to the above problem using event_config_set_max_dispatch_interval()
<source lang=c> struct event *ev;
static void cb(int sock, short which, void *arg) {
event_active(ev, EV_WRITE, 0);
}
int main(int argc, char **argv) {
struct event_config *cfg = event_config_new(); /* Run at most 16 callbacks before checking for other events. */ event_config_set_max_dispatch_interval(cfg, NULL, 16, 0); struct event_base *base = event_base_new_with_config(cfg); ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL); event_add(ev, NULL); event_active(ev, EV_WRITE, 0); event_base_loop(base, 0); return 0;
} </source>
Optimizing common timeouts
현재의 Libevent 는 pending 중인 이벤트의 timout을 확인하기 위해 바이너리 힙 알고리즘을 사용한다. 바이너리 힙 알고리즘은 이벤트의 timeout 추가/삭제시에 O(lg n)의 복잡도를 가진다. 하지만 이 알고리즘은 랜덤하게 timout 값들이 분포되어 있는 경우에는 효율이 좋으나, 많은 수의 같은 timout 을 가진 이벤트들이 있는 경우에는 효율적이지 못하다.
예를 들어 10,000 개의 이벤트가 있다고 가정하고, 이벤트 timout 을 5초를 가진다고 해보자. 이런상황에서 더블 링크드 큐 알고리즘을 사용할 경우, O(1)의 복잡도가 나온다.
하지만 일반적으로, 큐를 사용할 수 밖에 없는 경우를 제외하고는, 이벤트들의 관리에 큐를 사용하는 것은 좋은 선택이 아니다. 만약 다른 timout 을 가지는 이벤트가 있다면, 다른 timeout 을 가진 이벤트의 갯수 만큼 큐 슬롯을 생성하고 저장될 것이다. 추가하는 데에만 O(n) 복잡도 시간이 걸릴 것이며, 이는 바이너리 힙 알고리즘보다 확실히 느린 것이다.
Libevent는 이런 문제를 해결하기 위해, 큐와 바이너리 힙을 구분해서 사용할 수 있도록 한다. 이를 위해서 큐로 관리하고자하는 timeout 시간을 위한 별도의 "common timeout"을 지정해야하고, 만약 엄청나게 많은 수의 이벤트가 같은 timeout을 가진다면, timeout 성능 향상을 위한 최적화를 해주어야 한다.
- Interface
<source lang=c> const struct timeval *event_base_init_common_timeout(struct event_base *base, const struct timeval *duration); </source>
이 함수는 event_base, duration, common timeout to initialize 인자값들을 가진다. 결과로 timeval 구조체를 리턴하는데, 이는 이벤트 timeout 추가시 O(lg n) 힙을 사용하는 것이 아닌 O(1) queue 를 사용하도록 해준다. 이벤트 등록 이후, timeval 구조체는 마음대로 복사/할당 해도 된다. 지정한 event_base 에서만 사용가능하다. <source lang=c>
- include <event2/event.h>
- include <string.h>
/* We're going to create a very large number of events on a given base,
* nearly all of which have a ten-second timeout. If initialize_timeout * is called, we'll tell Libevent to add the ten-second one to an O(1) * queue */
struct timeval ten_seconds = {10, 0};
void initialize_timeout(struct event_base *base) {
struct timeval tv_in = {10, 0}; const struct timeval *tv_out; tv_out = event_base_init_common_timeout(base, &tv_in); memcpy(&ten_seconds, tv_out, sizeof(struct timeval));
}
int my_event_add(struct event *ev, const struct timeval *tv) {
/* Note that ev must have the same event_base that we passed to * initialize_timeout */ if (tv && tv->tv_sec == 10 && tv->tv_usec == 0) { return event_add(ev, &ten_seconds); } else { return event_add(ev, tv); }
} </source> 정확히 어떤 부분에서 최적화를 하고자 하는지 알기 전까지는, common_timeout 함수를 사용지 않길 바란다.
Telling a good event apart from cleared memory
Libevent는 생성한 이벤트 메모리를 0으로 초기화하는 함수들을 제공한다.(calloc()의 기능과 비슷하다. memeset() 혹은 bzero())
- Interface
<source lang=c> int event_initialized(const struct event *ev);
- define evsignal_initialized(ev) event_initialized(ev)
- define evtimer_initialized(ev) event_initialized(ev)
</source>
- Warning
기본적으로 이 함수들은 이벤트 메모리가 초기화를 위한 것인지, 이미 데이터가 들어있는지를 구분하지 않는다. 따라서 이 함수들을 사용할 때는 어떤 데이터가 이벤트 메모리에 있는지를 확실히 알고 사용해야 한다.
- Example
<source lang=c>
- include <event2/event.h>
- include <stdlib.h>
struct reader {
evutil_socket_t fd;
}
- define READER_ACTUAL_SIZE() \
(sizeof(struct reader) + \ event_get_struct_event_size())
- define READER_EVENT_PTR(r) \
((struct event *) (((char*)(r)) + sizeof(struct reader)))
struct reader *allocate_reader(evutil_socket_t fd) {
struct reader *r = calloc(1, READER_ACTUAL_SIZE()); if(r) r->fd = fd; return r;
}
void readcb(evutil_socket_t, short, void *) int add_reader(struct reader *r, struct event_base *b) {
struct event *ev = READER_EVENT_PTR(r); if(!event_initialized(ev)) event_assign(ev, b, r->fd, EV_READ, readcb, r); return event_add(ev, NULL);
} </source>
References
<references />