在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。