linux網路程式設計中的errno處理

2023-03-13 18:01:20

在Linux網路程式設計中,errno是一個非常重要的變數。它記錄了最近發生的系統呼叫錯誤程式碼。在編寫網路應用程式時,合理處理errno可以幫助我們更好地瞭解程式出現的問題並進行偵錯。

通常,在Linux網路程式設計中發生錯誤時,errno會被設定為一個非零值。因此,在進行系統呼叫之後,我們應該始終檢查errno的值。我們可以使用perror函數將錯誤資訊列印到標準錯誤輸出中,或者使用strerror函數將錯誤程式碼轉換為錯誤資訊字串。

在網路程式設計中,處理網路連線、連線收發資料等經常會涉及到errno的處理。經過查閱了很多資料,發現沒有一個系統的講解,在不同階段會遇到哪些errno,以及對這些errno需要如何處理。因此,本文將分為三個部分來講解。

1. 接受連線(accept)

這一階段發生在 accept 接收 tcp 連線中。

在accept接收tcp連線的過程中,可能會遇到以下errno:

  • EAGAIN或EWOULDBLOCK:表示當前沒有連線可以接受,非阻塞模式下可以繼續嘗試接受連線
  • ECONNABORTED:表示連線因為某種原因被終止,可以重新嘗試接受連線
  • EINTR:表示系統呼叫被中斷,可以重新嘗試接受連線
  • EINVAL:表示通訊端不支援接受連線操作,需要檢查通訊端是否正確

其中 EINTR、EAGAIN與EWOULDBLOCK,表示可能遇到了系統中斷,需要對這些errno忽略,如果是其他錯誤,則需要執行錯誤回撥或者直接處理錯誤。

在 libevent 為這些需要忽略的errno定義了宏 EVUTIL_ERR_ACCEPT_RETRIABLE,宏裡定義了上面三個需要忽略的訊號,在 accept 處理時會判斷如果遇到這些訊號則進行忽略,下次重試就好。

/* True iff e is an error that means a accept can be retried. */
#define EVUTIL_ERR_ACCEPT_RETRIABLE(e)			\
	((e) == EINTR || EVUTIL_ERR_IS_EAGAIN(e) || (e) == ECONNABORTED)

// libevent accept 處理程式碼
static void listener_read_cb(evutil_socket_t fd, short what, void *p)
{
	struct evconnlistener *lev = p;
	int err;
	evconnlistener_cb cb;
	evconnlistener_errorcb errorcb;
	void *user_data;
	LOCK(lev);
	while (1) {
		struct sockaddr_storage ss;
		ev_socklen_t socklen = sizeof(ss);
		evutil_socket_t new_fd = evutil_accept4_(fd, (struct sockaddr*)&ss, &socklen, lev->accept4_flags);
		if (new_fd < 0)
			break;
		if (socklen == 0) {
			/* This can happen with some older linux kernels in
			 * response to nmap. */
			evutil_closesocket(new_fd);
			continue;
		}
    ..........
	}
	err = evutil_socket_geterror(fd);
	if (EVUTIL_ERR_ACCEPT_RETRIABLE(err)) {
		UNLOCK(lev);
		return;
	}
	if (lev->errorcb != NULL) {
		++lev->refcnt;
		errorcb = lev->errorcb;
		user_data = lev->user_data;
		errorcb(lev, user_data);
		listener_decref_and_unlock(lev);
	} else {
		event_sock_warn(fd, "Error from accept() call");
		UNLOCK(lev);
	}
}

2. 建立連線(connect )

這一階段發生在 connect 連線中。

在connect連線的過程中,可能會遇到以下errno:

  • EINPROGRESS:表示連線正在進行中,需要等待連線完成
  • EALREADY:表示通訊端非阻塞模式下連線請求已經傳送,但連線還未完成,需要等待連線完成
  • EISCONN:表示通訊端已經連線,無需再次連線
  • EINTR:表示系統呼叫被中斷,可以重新嘗試連線
  • ENETUNREACH:表示網路不可達,需要檢查網路連線是否正常

其中 EINPROGRESS、EALREADY、EINTR 表示連線正在進行中,需要等待連線完成或重新嘗試連線。如果是其他錯誤,則需要執行錯誤回撥或者直接處理錯誤。

一般情況下,我們需要通過 select、poll、epoll 等 I/O 多路複用函數來等待連線完成,或者使用非阻塞的方式進行連線,等待連線完成後再進行下一步操作。

在 libevent 中,為這些需要忽略的 errno 定義了宏 EVUTIL_ERR_CONNECT_RETRIABLE,宏裡定義了上面三個需要忽略的訊號,在 connect 處理時會判斷如果遇到這些訊號則進行忽略,下次重試就好。

/* True iff e is an error that means a connect can be retried. */
#define EVUTIL_ERR_CONNECT_RETRIABLE(e)			\\
	((e) == EINTR || (e) == EINPROGRESS || (e) == EALREADY)

// libevent connect 處理程式碼
/* XXX we should use an enum here. */
/* 2 for connection refused, 1 for connected, 0 for not yet, -1 for error. */
int evutil_socket_connect_(evutil_socket_t *fd_ptr, const struct sockaddr *sa, int socklen)
{
	int made_fd = 0;

	if (*fd_ptr < 0) {
		if ((*fd_ptr = socket(sa->sa_family, SOCK_STREAM, 0)) < 0)
			goto err;
		made_fd = 1;
		if (evutil_make_socket_nonblocking(*fd_ptr) < 0) {
			goto err;
		}
	}

	if (connect(*fd_ptr, sa, socklen) < 0) {
		int e = evutil_socket_geterror(*fd_ptr);
    // 處理忽略的 errno
		if (EVUTIL_ERR_CONNECT_RETRIABLE(e))
			return 0;
		if (EVUTIL_ERR_CONNECT_REFUSED(e))
			return 2;
		goto err;
	} else {
		return 1;
	}

err:
	if (made_fd) {
		evutil_closesocket(*fd_ptr);
		*fd_ptr = -1;
	}
	return -1;
}

3. 連線的讀寫

在 Linux 網路程式設計中,連線讀寫階段可能會遇到以下 errno:

  • EINTR:表示系統呼叫被中斷,可以重新嘗試讀寫
  • EAGAIN 或 EWOULDBLOCK:表示當前沒有資料可讀或沒有緩衝區可寫,需要等待下一次讀寫事件再嘗試讀寫,非阻塞模式下可以繼續嘗試讀寫
  • ECONNRESET 或 EPIPE:表示連線被重置或對端關閉了連線,需要重新建立連線
  • ENOTCONN:表示連線未建立或已斷開,需要重新建立連線
  • ETIMEDOUT:表示連線超時,需要重新建立連線
  • ECONNREFUSED:表示連線被拒絕,需要重新建立連線
  • EINVAL:表示通訊端不支援讀寫操作,需要檢查通訊端是否正確

其中 EINTR、EAGAIN 或 EWOULDBLOCK 表示可能遇到了系統中斷或當前沒有資料可讀或沒有緩衝區可寫,需要對這些 errno 忽略,如果是其他錯誤,則需要執行錯誤回撥或者直接處理錯誤。

在 libevent 中,為這些需要忽略的 errno 定義了宏 EVUTIL_ERR_RW_RETRIABLE,宏裡定義了 EINTR、EAGAIN 或 EWOULDBLOCK 需要忽略的訊號,在連線的讀寫處理時會判斷如果遇到這些訊號則進行忽略,下次重試就好。

/* True iff e is an error that means a read or write can be retried. */
#define EVUTIL_ERR_RW_RETRIABLE(e)				\\
	((e) == EINTR || EVUTIL_ERR_IS_EAGAIN(e))

// 連線讀寫處理程式碼例子
static void bufferevent_readcb(evutil_socket_t fd, short event, void *arg)
{
	struct bufferevent *bufev = arg;
	struct bufferevent_private *bufev_p = BEV_UPCAST(bufev);
	struct evbuffer *input;
	int res = 0;
	short what = BEV_EVENT_READING;
	ev_ssize_t howmuch = -1, readmax=-1;

	bufferevent_incref_and_lock_(bufev);

	if (event == EV_TIMEOUT) {
		/* Note that we only check for event==EV_TIMEOUT. If
		 * event==EV_TIMEOUT|EV_READ, we can safely ignore the
		 * timeout, since a read has occurred */
		what |= BEV_EVENT_TIMEOUT;
		goto error;
	}

	input = bufev->input;

	/*
	 * If we have a high watermark configured then we don't want to
	 * read more data than would make us reach the watermark.
	 */
	if (bufev->wm_read.high != 0) {
		howmuch = bufev->wm_read.high - evbuffer_get_length(input);
		/* we somehow lowered the watermark, stop reading */
		if (howmuch <= 0) {
			bufferevent_wm_suspend_read(bufev);
			goto done;
		}
	}
	readmax = bufferevent_get_read_max_(bufev_p);
	if (howmuch < 0 || howmuch > readmax) /* The use of -1 for "unlimited"
					       * uglifies this code. XXXX */
		howmuch = readmax;
	if (bufev_p->read_suspended)
		goto done;

	evbuffer_unfreeze(input, 0);
	res = evbuffer_read(input, fd, (int)howmuch); /* XXXX evbuffer_read would do better to take and return ev_ssize_t */
	evbuffer_freeze(input, 0);

	if (res == -1) {
		int err = evutil_socket_geterror(fd);
    // 處理需要忽略的errno
		if (EVUTIL_ERR_RW_RETRIABLE(err))
			goto reschedule;
		if (EVUTIL_ERR_CONNECT_REFUSED(err)) {
			bufev_p->connection_refused = 1;
			goto done;
		}
		/* error case */
		what |= BEV_EVENT_ERROR;
	} else if (res == 0) {
		/* eof case */
		what |= BEV_EVENT_EOF;
	}

	if (res <= 0)
		goto error;

	bufferevent_decrement_read_buckets_(bufev_p, res);

	/* Invoke the user callback - must always be called last */
	bufferevent_trigger_nolock_(bufev, EV_READ, 0);

	goto done;

 reschedule:
	goto done;

 error:
	bufferevent_disable(bufev, EV_READ);
	bufferevent_run_eventcb_(bufev, what, 0);

 done:
	bufferevent_decref_and_unlock_(bufev);
}

static void bufferevent_writecb(evutil_socket_t fd, short event, void *arg)
{
	struct bufferevent *bufev = arg;
	struct bufferevent_private *bufev_p = BEV_UPCAST(bufev);
	int res = 0;
	short what = BEV_EVENT_WRITING;
	int connected = 0;
	ev_ssize_t atmost = -1;

	bufferevent_incref_and_lock_(bufev);

	if (evbuffer_get_length(bufev->output)) {
		evbuffer_unfreeze(bufev->output, 1);
		res = evbuffer_write_atmost(bufev->output, fd, atmost);
		evbuffer_freeze(bufev->output, 1);
		if (res == -1) {
			int err = evutil_socket_geterror(fd);
		  // 處理需要忽略的 errno
			if (EVUTIL_ERR_RW_RETRIABLE(err))
				goto reschedule;
			what |= BEV_EVENT_ERROR;
		} else if (res == 0) {
			/* eof case
			   XXXX Actually, a 0 on write doesn't indicate
			   an EOF. An ECONNRESET might be more typical.
			 */
			what |= BEV_EVENT_EOF;
		}
		if (res <= 0)
			goto error;

		bufferevent_decrement_write_buckets_(bufev_p, res);
	}

	if (evbuffer_get_length(bufev->output) == 0) {
		event_del(&bufev->ev_write);
	}

	/*
	 * Invoke the user callback if our buffer is drained or below the
	 * low watermark.
	 */
	if (res || !connected) {
		bufferevent_trigger_nolock_(bufev, EV_WRITE, 0);
	}

	goto done;

 reschedule:
	if (evbuffer_get_length(bufev->output) == 0) {
		event_del(&bufev->ev_write);
	}
	goto done;

 error:
	bufferevent_disable(bufev, EV_WRITE);
	bufferevent_run_eventcb_(bufev, what, 0);

 done:
	bufferevent_decref_and_unlock_(bufev);
}

4. 總結

本文介紹了在 Linux 網路程式設計中處理 errno 的方法。在接受連線、建立連線和連線讀寫階段可能會遇到多種 errno,如 EINTR、EAGAIN、EWOULDBLOCK、ECONNRESET、EPIPE、ENOTCONN、ETIMEDOUT、ECONNREFUSED、EINVAL 等,需要對一些 errno 進行忽略,對於其他錯誤則需要執行錯誤回撥或者直接處理錯誤。在 libevent 中,為這些需要忽略的 errno 定義了宏,如 EVUTIL_ERR_ACCEPT_RETRIABLE、EVUTIL_ERR_CONNECT_RETRIABLE、EVUTIL_ERR_RW_RETRIABLE 等,方便開發者處理這些 errno。