阻塞socket上read/write出现errno为EAGAIN的原因解密

一直以来,个人概念中只有非阻塞socket才会产生EAGAIN的错误,意思是当前不可读写,只要继续重试就好。当最近我们redis模块的一个报错纠正我的这个概念错误。

事件回顾:hiredis的redisConnectWithTimeout和redisContextSetTimeout接口会设置与redis-server连接的socket为阻塞模式,并且设置读写超时,我们项目中设置超时为50ms。接着在下面几天的日志中发现hiredis报错“Resource temporarily unavailable”,一开始非常奇怪,因为这个错误对应的就是EAGAIN,而这种情况在我知识概念中只

有非阻塞模式下才会报。下面介绍下我理清这个概念的过程:

1.查看redis的读写接口

/* Use this function to handle a read event on the descriptor. It will try
 * and read some bytes from the socket and feed them to the reply parser.
 *
 * After this function is called, you may use redisContextReadReply to
 * see if there is a reply available. */
int redisBufferRead(redisContext *c) {
    char buf[1024*16];
    int nread;

    /* Return early when the context has seen an error. */
    if (c->err)
        return REDIS_ERR;

    nread = read(c->fd,buf,sizeof(buf));
    if (nread == -1) {
        if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) {
            /* Try again later */
        } else {
            __redisSetError(c,REDIS_ERR_IO,NULL);
            return REDIS_ERR; </span>                                  //分支A1</span>
        }
    } else if (nread == 0) {
            __redisSetError(c,REDIS_ERR_EOF,"Server closed the connection");
            return REDIS_ERR;  </span>                                 //分支A2</span>
    } else {
        if (redisReaderFeed(c->reader,buf,nread) != REDIS_OK) {
            __redisSetError(c,c->reader->err,c->reader->errstr);
            return REDIS_ERR;
        }
    }
    return REDIS_OK;
}
/* Write the output buffer to the socket.
 *
 * Returns REDIS_OK when the buffer is empty, or (a part of) the buffer was
 * succesfully written to the socket. When the buffer is empty after the
 * write operation, "done" is set to 1 (if given).
 *
 * Returns REDIS_ERR if an error occured trying to write and sets
 * c->errstr to hold the appropriate error string.
 */
int redisBufferWrite(redisContext *c, int *done) {
    int nwritten;

    /* Return early when the context has seen an error. */
    if (c->err)
        return REDIS_ERR;

    if (sdslen(c->obuf) > 0) {
        nwritten = write(c->fd,c->obuf,sdslen(c->obuf));
        if (nwritten == -1) {
            if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) {
                /* Try again later */
            } else {
                __redisSetError(c,REDIS_ERR_IO,NULL);       //分支B1
                return REDIS_ERR;
            }
        } else if (nwritten > 0) {
            if (nwritten == (signed)sdslen(c->obuf)) {
                sdsfree(c->obuf);
                c->obuf = sdsempty();
            } else {
                sdsrange(c->obuf,nwritten,-1);
            }
        }
    }
    if (done != NULL) *done = (sdslen(c->obuf) == 0);
    return REDIS_OK;
}

注意,错误日志中显示hiredis设置错误为REDIS_ERR_IO,并且errstr为“Resource temporarily unavailable”,那么只可能是分支A1和分支B1,再往下追究

2.查看错误设置过程

void __redisSetError(redisContext *c, int type, const char *str) {
    size_t len;

    c->err = type;
    if (str != NULL) {
        len = strlen(str);
        len = len < (sizeof(c->errstr)-1) ? len : (sizeof(c->errstr)-1);
        memcpy(c->errstr,str,len);
        c->errstr[len] = '\0';
    } else {
<span style="color:#ff0000;">        /* Only REDIS_ERR_IO may lack a description! */
        assert(type == REDIS_ERR_IO);
        strerror_r(errno,c->errstr,sizeof(c->errstr));</span>
    }
}

分支中设置了c->errstr为“Resource temporarily unavailable”,从而反推errno为EAGAIN

3.为什么blocking的read和write会导致errno为EAGAIN?

1)我们对socket做了什么? 设置了超时时间

int redisContextSetTimeout(redisContext *c, const struct timeval tv) {
    if (setsockopt(c->fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)) == -1) {
        __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(SO_RCVTIMEO)");
        return REDIS_ERR;
    }
    if (setsockopt(c->fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)) == -1) {
        __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(SO_SNDTIMEO)");
        return REDIS_ERR;
    }
    return REDIS_OK;
}

2)socket设置SO_RCVTIMEO和SO_SNDTIMEO对read/write有什么影响?看man怎么说

SO_RCVTIMEO and SO_SNDTIMEO

Specify the receiving or sending timeouts until reporting an error. The argument is a struct timeval. If an input or output function blocks for this period of time, and data has been sent or received, the return value of that function will be the amount of data transferred; if no data has been transferred and the timeout has been reached then -1 is returned witherrno set to EAGAIN or EWOULDBLOCK, or EINPROGRESS (for connect(2)) just as if the socket was specified to be nonblocking. If the timeout is set to zero (the default) then the operation will never timeout. Timeouts only have effect for system calls that perform socket I/O (e.g., read(2), recvmsg(2), send(2), sendmsg(2)); timeouts have no effect for select(2), poll(2), epoll_wait(2), and so on.

终于清晰了:SO_RCVTIMEO和SO_SNDTIMEO会导致read/write函数返回EAGAIN


2023-09-15补充:实测结果,阻塞socket设置超时而不可写时就会返回EAGAIN,跟是否设置TCP_NODELAY没有关系。而且,write接口返回的是-1,不是0,特此更正。
 

在 Unix 和类 Unix 系统中,当一个进程尝试在已经关闭的管道(pipe)上执行读取或写入操作时,会触发 `io: read/write on closed pipe` 错误。该错误通常出现在使用管道、套接字或 FIFO 文件进行进程间通信(IPC)的场景中。以下是该错误的常见原因及对应的解决方法。 ### 常见原因 1. **读端或写端提前关闭** 管道是单向通信机制,分为读端和写端。如果写端在仍有读端尝试读取数据时关闭,则读端会收到 EOF;如果读端在仍有写端尝试写入时关闭,则写端会收到 `SIGPIPE` 信号,若未处理该信号,则程序会终止并提示 `io: read/write on closed pipe` 错误[^1]。 2. **异步 I/O 操作未正确同步** 在使用异步 I/O 模型(如事件循环、协程)时,如果某个协程或回调函数在管道关闭后仍尝试访问该管道,也可能导致该错误。 3. **资源泄漏或未正确释放** 如果程序中未能正确关闭所有打开的文件描述符,或者在多线程/多进程环境中未能正确管理管道生命周期,也可能导致在无效的文件描述符上进行读写操作。 ### 解决方法 1. **正确处理 `SIGPIPE` 信号** 可以通过忽略 `SIGPIPE` 信号来避免程序因写入关闭的管道而崩溃: ```c #include <signal.h> signal(SIGPIPE, SIG_IGN); ``` 这样即使尝试写入已关闭的管道,程序也不会终止,而是返回错误码(如 `EPIPE`),可以通过检查错误码进行进一步处理。 2. **确保管道两端正确同步关闭** 在关闭管道之前,确保所有读写操作已完成,并且所有相关的文件描述符都被正确关闭。可以使用 `close()` 显式关闭描述符,并在关闭前使用 `shutdown()` 来优雅地结束连接。 3. **使用 `poll()` 或 `select()` 监控描述符状态** 在进行 I/O 操作前,使用 `poll()` 或 `select()` 检查管道是否仍然有效。例如: ```c struct pollfd fds; fds.fd = pipe_fd; fds.events = POLLIN; int ret = poll(&fds, 1, timeout); if (ret > 0 && (fds.revents & POLLHUP)) { // 管道已被关闭 } ``` 4. **使用异步 I/O 框架的生命周期管理机制** 如果使用如 Tornado 的 `IOLoop` 模块进行事件驱动编程,应确保在关闭描述符时取消所有相关的回调和定时器,防止在关闭后仍被触发。 5. **日志记录与异常处理** 在捕获到 I/O 错误时,记录详细的上下文信息,有助于排查问题根源。可以结合 `errno` 获取具体的错误代码并进行分类处理。 --- ### 示例代码:忽略 `SIGPIPE` 并处理写入关闭的管道 ```c #include <stdio.h> #include <unistd.h> #include <signal.h> #include <errno.h> int main() { int pipefd[2]; pipe(pipefd); // 忽略 SIGPIPE 信号 signal(SIGPIPE, SIG_IGN); // 关闭写端 close(pipefd[1]); // 尝试写入已关闭的管道 ssize_t bytes_written = write(pipefd[1], "hello", 5); if (bytes_written == -1 && errno == EPIPE) { printf("Caught EPIPE, pipe is closed.\n"); } return 0; } ``` --- ###
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值