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.
References
<references />