libev使用方法

joker8發表於2020-11-29

1. libev簡介

libev是個高效能跨平臺的事件驅動框架,支援io事件,超時事件,子程式狀態改變通知,訊號通知,檔案狀態改變通知,還能用來實現wait/notify機制。libev對每種監聽事件都用一個ev_type型別的資料結構表示,如ev_io, ev_timer, ev_child, ev_async分別用來表示檔案監聽器, timeout監聽器, 子程式狀態監聽器, 同步事件監聽器.

libev支援優先順序, libev一次loop收集的事件按優先順序先排序, 優先順序高的事件回撥先執行, 優先順序低的後執行, 相同優先順序則按事件到達順序執行. libev優先順序從[-2, 2], 預設優先順序為0,libev註冊watcher的流程如下:

static void type_cb(EV_P_ ev_type *watcher, int revents)
{
	// callback
}

static void ev_test()
{
#ifdef EV_MULTIPLICITY
	struct ev_loop *loop;
#else
	int loop;
#endif
	ev_type *watcher;

	loop = ev_default_loop(0);
	watcher = (ev_type *)calloc(1, sizeof(*watcher));
	assert(loop && watcher);

	ev_type_init(watcher, type_cb, ...);
	ev_start(EV_A_ watcher);

	ev_run(EV_A_ 0);

	/* 資源回收 */
	ev_loop_destroy(EV_A);
	free(watcher);
}

libev註冊watcher可以分為四個步驟:

  1. 建立一個loop和watcher
  2. 初始化watcher,主要設定callback函式和定義watcher的引數
  3. 啟用watcher
  4. 啟動libev,開始loop收集事件

2. ev_io

libev內部使用後端select, poll, epoll(linux專有), kqueue(drawin), port(solaris10)實現io事件監聽, 使用者可以指定作業系統支援的後端或者由libev自動選擇使用哪個後端,如linux平臺上使用者可以強制指定libev使用select作為後端。libev支援單例模式和多例模式, 假設我們連結的是多例模式的libev庫, 且watcher使用預設優先順序0.

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>

#include <ev.h>

static void io_cb(struct ev_loop *loop, ev_io *watcher, int revents)
{
	char buf[1024] = {0};

	/* 引數watcher即註冊時的watcher */
	read(watcher->fd, buf, sizeof(buf) - 1);
	fprintf(stdout, "%s\n", buf);

	ev_break(loop, EVBREAK_ALL);
}

static void io_test()
{
	struct ev_loop *loop;
	ev_io *io_watcher;

	/* 指定libev使用epoll機制,關閉環境變數對libev影響 */
	loop = ev_default_loop(EVFLAG_NOENV | EVBACKEND_EPOLL);
	io_watcher = (ev_io *)calloc(1, sizeof(*io_watcher));
	assert(("can not alloc memory", loop && io_watcher));

	/* 設定監聽標準輸入的可讀事件和回撥函式 */
	ev_io_init(io_watcher, io_cb, STDIN_FILENO, EV_READ);
	ev_io_start(loop, io_watcher);

	/* libev開啟loop */
	ev_run(loop, 0);

	/* 資源回收 */
	ev_loop_destroy(loop);
	free(io_watcher);
}

int main(void)
{
	io_test();

	return 0;
}

ev_io_init(ev, cb, fd, event)

  • ev:ev_io
  • cb: 回撥函式
  • fd:socket,pipe等控制程式碼
  • event:事件型別(EV_READ/EV_WRITE)

Makefile

target := main
CC := clang
CFLAGS += -g -Wall -fPIC
LDFLAGS += -lev

src += \
	main.c
obj := $(patsubst %.c, %.o, $(src))

$(target):$(obj)
	$(CC) -o $@ $^ $(LDFLAGS)

%.o:%.c
	$(CC) -o $@ -c $< $(CFLAGS)

.PHONY:clean

clean:
	@rm -rf $(target) *.o

libev內部使用一個大的迴圈來收集各種watcher註冊的事件,如果沒有註冊ev_timer和ev_periodic,則libev內部使用的後端採用59.743s作為超時事件,如果select作為後端,則select的超時設定為59.743s,這樣可以降低cpu佔用率,對一個fd可以註冊的watcher數量不受限(或者說只受記憶體限制),比如可以對標準輸入的可讀事件註冊100個watcher,當有使用者輸入時所有100個watcher的回撥都能執行(當然回撥中還是隻有一個read操作成功)。

3. ev_timer

ev_timer可以用來實現定時器, ev_timer不受牆上時間影響,如設定一個1小時定時器,把當前系統時間調快1小時不能讓定時器立刻超時,超時依舊發生在1小時後,如下是一個簡單的例子:

static void timer_cb(struct ev_loop *loop, ev_timer *w, int revents)
{
	fprintf(stdout, "%fs timeout\n", w->repeat);
	ev_break(loop, EVBREAK_ALL);
}

static void timer_test()
{
	struct ev_loop *loop;
	ev_timer *timer_watcher;

	loop = ev_default_loop(EVFLAG_NOENV);
	timer_watcher = calloc(1, sizeof(*timer_watcher));
	assert(("can not alloc memory", loop && timer_watcher));

	ev_timer_init(timer_watcher, timer_cb, 0., 3600.);
	ev_timer_start(loop, timer_watcher);

	ev_run(loop, 0);

	ev_loop_destroy(loop);
	free(timer_watcher);
}

ev_timer_init(ev, cb, ofs, iva)

  • ev: ev_timer
  • cb: 回撥函式
  • ofs,iva: 超時事件為(now + ofs + ival * N), now為當前時間,N為正整數

如果ival引數為0,則timer是一次性的定時器,超時後libev自動stop timer
可以在回撥中重新設定timer超時,並重新啟動timer。

static void timer_cb(struct ev_loop *loop, ev_timer *watcher, int revents)
{
	fprintf(stdout, "%fs timeout\n", watcher->repeat);
	watcher->repeat += 5.;
	ev_timer_again(loop, watcher);
}

上面介紹了libev是在一個大的迴圈中監聽所有watcher的事件,只有ev_io型別的watcher時,libev後端以59.743s作為超時(如select超時),這時使用者註冊一個3s的timer,那麼libev會不會因為後端超時太長導致定時器檢測非常不準呢?答案是不會,libev保證後端超時時間不大於定時器超時時間,註冊一個3s timer,則libev自動調整到3s以內loop一次,這樣保證timer超時能及時被檢測到,同時也帶來更高的cpu佔用率。

4. ev_periodic

ev_timer做為定時器很方便,但是對應指定到某時刻發生超時就比較困難,比如每天00:00:00觸發超時,每天08:00開燈,18:00關燈等等,ev_periodic可以很好的應付這種場景,ev_periodic基於牆上時間,所以受牆上時間影響,如註冊1小時後超時的ev_peroidic,同時系統時間調快1小時,ev_periodic立馬能超時。如下例子指定每天凌晨發生超時:

static void periodic_cb(struct ev_loop *loop, ev_periodic *watcher, int revents)
{
	fprintf(stdout, "00:00:00 now, time to sleep");
}

ev_tstamp my_schedule(ev_periodic *watcher, ev_tstamp now)
{
	time_t cur;
	struct tm tm;

	time(&cur);
	localtime_r(&cur, &tm);

	tm.tm_wday += 1;
	tm.tm_hour = 0;
	tm.tm_sec = 0;
	tm.tm_min = 0;
	tm.tm_mon -= 1;
	tm.tm_year -= 1900;

	return mktime(&tm);
}

static void periodic_test()
{
	struct ev_loop *loop;
	ev_periodic *periodic_watcher;

	loop = ev_default_loop(0);
	periodic_watcher = (ev_periodic *)calloc(1, sizeof(*periodic_watcher));
	assert(("can not alloc memory", loop && periodic_watcher));

	ev_periodic_init(periodic_watcher, periodic_cb, 0, 0, my_schedule);
	ev_periodic_start(loop, periodic_watcher);

	ev_run(loop, 0);

	ev_loop_destroy(loop);
	free(periodic_watcher);
}

ev_periodic_init(ev, cb, ofs, ival,schedule)

  • ev:ev_periodic
  • cb:回撥函式
  • ofs,ival:ofs + ceil((now - ofs) / ival) * ival // now表示當前時間戳
  • schedule:使用者自定義函式,該函式返回下一次ev_periodic超時時間
  1. 如果schedule不為空,則ev_periodic超時時間為:ofs + ceil((now - ofs) / ival) * ival,表示從當前時間開始,經過ofs時間後所有能被ival整數的點,註冊ev_periodic,ifs = 1, ival = 10, 當前時間為1604649458(2020-10-615:57:38),則ev_periodic第一次經過2s就發生超時了。
  2. 如果schedule存在,則ofs,ival引數被忽略,ev_periodic超時時間由schedule()返回值指定

5. ev_child

libev支援監聽子程式狀態變化, 如子程式退出, 內部用waitpid去實現, libev限制只能用default loop去監聽子程式狀態變化, 如果以ev_loop_new()建立的loop則不行, 通過ev_default_loop()建立default loop時libev內部自動註冊了SIGCHILD訊號處理函式, 需要在自己程式碼處理SIGCHILD的話, 可以在ev_default_loop()之後註冊SIGCHIL處理以覆蓋libev中的預設處理, 如下是一個簡單的例子:

static void child_cb(struct ev_loop *loop, ev_child *watcher, int revents)
{
	fprintf(stdout, "pid:%d exit, status:%d\n", watcher->rpid, watcher->rstatus);	
}

static void child_test()
{
	pid_t pid;
	struct ev_loop *loop;
	ev_child *child_watcher;

	switch (pid = fork()) {
		case 0:
			sleep(5);
			fprintf(stdout, "child_pid:%d\n", getpid());
			exit(EXIT_SUCCESS);
		default:
			{
				loop = ev_default_loop(0);
				child_watcher = (ev_child*)calloc(1, sizeof(*child_watcher));
				assert(("can not alloc memory", loop && child_watcher));

				ev_child_init(child_watcher, child_cb, 0, 1);
				ev_child_start(loop, child_watcher);

				ev_run(loop, 0);

				/* 資源回收 */
				ev_loop_destroy(loop);
				free(child_watcher);
			}
	}
}

ev_child_init(ev, cb, pid, trace)

  • ev:ev_child
  • cb:回撥函式
  • pid:子程式pid
  • trace:設定為1
    個人覺得libev的default loop預設註冊SIGCHILD處理並不好,最好還是在自己程式碼中做處理。

6. ev_async

可以通過libev的async來實現wait/notify機制, 使用者註冊多個ev_async監聽器, 在其他地方呼叫ev_async_send()即可觸發ev_async註冊的回撥, libev內部用eventfd(linux平臺)和pipe(win32)實現, 個人覺得linux平臺上直接用eventfd更完美, 如下是簡單例子.

static void *routine(void *args)
{
	static size_t count = 0;
	ev_async *watcher = (ev_async *)args;
	struct ev_loop *loop = (struct ev_loop *)watcher->data;

	while (count++ < 10) {
		ev_async_send(loop, watcher);
		sleep(1);
	}

	return NULL;
}

static void async_cb(struct ev_loop *loop, ev_async *watcher, int revents)
{
	fprintf(stdout, "get the order, start move...\n");
}

static void async_test()
{
	pthread_t pid;
	struct ev_loop *loop;
	ev_async *async_watcher;

	loop = ev_default_loop(0);
	async_watcher = (ev_async *)calloc(1, sizeof(*async_watcher));
	assert(("can not alloc memory", loop && async_watcher));

	ev_async_init(async_watcher, async_cb);
	ev_async_start(loop, async_watcher);

	async_watcher->data = loop;
	pthread_create(&pid, NULL, routine, async_watcher);

	ev_run(loop, 0);

	/* 資源回收 */
	ev_loop_destroy(loop);
	free(async_watcher);
}

ev_async_init(ev,cb)

  • ev:ev_async
  • cb:回撥函式

7. ev_prepare/ev_idle

libev每次loop收集各種事件之前都會先呼叫ev_prepare的回撥函式(如果有的話), 如果存在比ev_idle優先順序更高的監聽有事件待處理, 則ev_idle的事件不會處理, 如存在優先順序1,2的事件待處理, 則優先順序為1的ev_idle的事件不會被處理, 只有在優先順序1,2的所有事件都處理完後才會把ev_idle的事件新增到帶處理的事件佇列中去.

static void idle_cb(struct ev_loop *loop, ev_idle *watcher, int revents)
{
	fprintf(stdout, "no one has higher priority than me now\n");
	ev_idle_stop(loop, watcher);
}

static void prepare_cb(struct ev_loop *loop, ev_prepare *watcher, int revents)
{
	fprintf(stdout, "prepare_cb\n");
}

static void ev_test()
{
	struct ev_loop *loop;
	ev_io *io_watcher;
	ev_idle *idle_watcher;
	ev_prepare *prepare_watcher;

	loop = ev_default_loop(0);
	prepare_watcher = (ev_prepare *)calloc(1, sizeof(*prepare_watcher));
	idle_watcher = (ev_idle *)calloc(1, sizeof(*idle_watcher));
	io_watcher = (ev_io *)calloc(1, sizeof(*io_watcher));
	assert(("can not alloc memory", loop && prepare_watcher && io_watcher && idle_watcher));

	ev_io_init(io_watcher, io_cb, STDIN_FILENO, EV_READ);
	ev_prepare_init(prepare_watcher, prepare_cb);
	ev_idle_init(idle_watcher, idle_cb);

	ev_prepare_start(loop, prepare_watcher);
	ev_io_start(loop, io_watcher);
	ev_idle_start(loop, idle_watcher);

	ev_run(loop, 0);

	ev_loop_destroy(loop);
	free(io_watcher);
	free(idle_watcher);
	free(prepare_watcher);
}

ev_prepare(ev, cb)/ev_idle(ev, cb)

  • ev:ev_prepare/ev_idle
  • cb:回撥函式

可以看到每次輸入前都先有ev_prepare的回撥,只有不存在優先順序別idle高的時間待處理時才會處理idle的回撥。

8. ev_stat

不建議使用,還不如用個定時器自己去檢測檔案是否改動

9. ev_fork

註冊ev_fork,在libev自動檢測到fork呼叫(開啟了EVFLAG_FORKCHECK),或者使用者呼叫ev_loop_fork()通知libev有fork呼叫時ev_fork回撥被觸發

10. ev_cleanup

註冊ev_cleanup的watcher,在libev銷燬時呼叫ev_cleanup的回撥,用來做一些清理工作

11. 簡單回顯伺服器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <assert.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#include <ev.h>

/* client number limitation */
#define MAX_CLIENTS 1000

/* message length limitation */
#define MAX_MESSAGE_LEN (256)

#define err_message(msg) \
	do {perror(msg); exit(EXIT_FAILURE);} while(0)

/* record the number of clients */
static int client_number;

static int create_serverfd(char const *addr, uint16_t u16port)
{
	int fd;
	struct sockaddr_in server;

	fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0) err_message("socket err\n");

	server.sin_family = AF_INET;
	server.sin_port = htons(u16port);
	inet_pton(AF_INET, addr, &server.sin_addr);

	if (bind(fd, (struct sockaddr *)&server, sizeof(server)) < 0) err_message("bind err\n");

	if (listen(fd, 10) < 0) err_message("listen err\n");

	return fd;
}

static void read_cb(EV_P_ ev_io *watcher, int revents)
{
	ssize_t ret;
	char buf[MAX_MESSAGE_LEN] = {0};

	ret = recv(watcher->fd, buf, sizeof(buf) - 1, MSG_DONTWAIT);	
	if (ret > 0) {
		write(watcher->fd, buf, ret);

	} else if ((ret < 0) && (errno == EAGAIN || errno == EWOULDBLOCK)) {
		return;

	} else {
		fprintf(stdout, "client closed (fd=%d)\n", watcher->fd);
		--client_number;
		ev_io_stop(EV_A_ watcher);
		close(watcher->fd);
		free(watcher);
	}
}

static void accept_cb(EV_P_ ev_io *watcher, int revents)
{
	int connfd;
	ev_io *client;

	connfd = accept(watcher->fd, NULL, NULL);
	if (connfd > 0) {
		if (++client_number > MAX_CLIENTS) {
			close(watcher->fd);

		} else {
			client = calloc(1, sizeof(*client));
			ev_io_init(client, read_cb, connfd, EV_READ);
			ev_io_start(EV_A_ client);
		}

	} else if ((connfd < 0) && (errno == EAGAIN || errno == EWOULDBLOCK)) {
		return;

	} else {
		close(watcher->fd);
		ev_break(EV_A_ EVBREAK_ALL);
		/* this will lead main to exit, no need to free watchers of clients */
	}
}

static void start_server(char const *addr, uint16_t u16port)
{
	int fd;
#ifdef EV_MULTIPLICITY
	struct ev_loop *loop;
#else
	int loop;
#endif
	ev_io *watcher;

	fd = create_serverfd(addr, u16port);
	loop = ev_default_loop(EVFLAG_NOENV);
	watcher = calloc(1, sizeof(*watcher));
	assert(("can not alloc memory\n", loop && watcher));

	/* set nonblock flag */
	fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);

	ev_io_init(watcher, accept_cb, fd, EV_READ);
	ev_io_start(EV_A_ watcher);
	ev_run(EV_A_ 0);

	ev_loop_destroy(EV_A);
	free(watcher);
}

static void signal_handler(int signo)
{
	switch (signo) {
		case SIGPIPE:
			break;
		default:
			// unreachable
			break;
	}
}

int main(void)
{
	signal(SIGPIPE, signal_handler);
	start_server("127.0.0.1", 10009);

	return 0;
}

客戶端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <assert.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#include <pthread.h>

#define err_message(msg) \
	do {perror(msg); exit(EXIT_FAILURE);} while(0)

static int create_clientfd(char const *addr, uint16_t u16port)
{
	int fd;
	struct sockaddr_in server;

	fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0) err_message("socket err\n");

	server.sin_family = AF_INET;
	server.sin_port = htons(u16port);
	inet_pton(AF_INET, addr, &server.sin_addr);

	if (connect(fd, (struct sockaddr *)&server, sizeof(server)) < 0) perror("connect err\n");

	return fd;
}

static void *routine(void *args)
{
	int fd;
	char buf[128];

	fd = create_clientfd("127.0.0.1", 10009);

	for (; ;) {
		write(fd, "Hello", strlen("hello"));

		memset(buf, '\0', sizeof(buf));
		read(fd, buf, sizeof(buf) - 1);
		fprintf(stdout, "pthreadid:%ld %s\n", pthread_self(), buf);
		usleep(100 * 1000);
	}
}

int main(void)
{
	pthread_t pids[4];

	for (int i = 0; i < sizeof(pids)/sizeof(pthread_t); ++i) {
		pthread_create(pids + i, NULL, routine, 0);
	}

	for (int i = 0; i < sizeof(pids)/sizeof(pthread_t); ++i) {
		pthread_join(pids[i], 0);
	}

	return 0;
}

Makefile

all:server client

server_src += \
	server.c

server_obj := $(patsubst %.c, %.o, $(server_src))

client_src += \
	client.c

client_obj:= $(patsubst %.c, %.o, $(client_src))

CC := clang

CFLAGS += -Wall -fPIC

server:$(server_obj)
	$(CC) -o $@ $^ -lev
%.o:%.c
	$(CC) -o $@ -c $< $(CFLAGS)

client:$(client_obj)
	$(CC) -o $@ $^ -lpthread
%.o:%.c
	$(CC) -o $@ -c $< $(CFLAGS)

.PHONY:clean all

clean:
	@rm -rf server client *.o

相關文章