完整的问题是:
当read方法返回0,即我们收到了对方发给我们的fin包,使我们的socket处于RCV_SHUTDOWN状态,此后,该socket还会有epollin事件发生吗?
同理,我们调用shutdown方法,关闭了send端,使我们的socket处于SEND_SHUTDOWN状态,此后,还会有epollout事件吗?
其实,对认真读过之前几篇文章的同学来说,这个问题已经很简单了,答案就是会。
因为,当有任意epoll事件发生时,内核只是把该socket放到epoll的事件就绪队列里,等我们下次调用epoll_wait方法时,epoll内部会再调用这个队列里的各个socket的tcp_poll方法,检查该socket此时所有就绪的事件,然后将这些事件返回给用户。
也就是说,即使内核通知epoll,该socket有epollin事件,epoll内部还是会检查该socket是否还有其他事件,epoll会把所有就绪事件收集好之后,一起返回给用户。
tcp/epoll体系中关键的tcp_poll方法我们之前的文章已经分析过了,这里再拿来看下:
// net/ipv4/tcp.c
__poll_t tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
__poll_t mask;
struct sock *sk = sock->sk;
const struct tcp_sock *tp = tcp_sk(sk);
int state;
...
state = inet_sk_state_load(sk);
...
mask = 0;
...
// 该socket的既是RCV_SHUTDOWN,又是SEND_SHUTDOWN,或者状态是TCP_CLOSE
// 对应的epoll事件都是EPOLLHUP
if (sk->sk_shutdown == SHUTDOWN_MASK || state == TCP_CLOSE)
mask |= EPOLLHUP;
// 该socket是RCV_SHUTDOWN,比如对方用shutdown(sockfd, SHUT_WR)方法
// 关闭它的SEND_SHUTDOWN,也就是关闭了我们的RCV_SHUTDOWN
// 又比如,我们用shutdown(sockfd, SHUT_RD)方法,关闭我们自己的RCV_SHUTDOWN
// 在此模式下,epoll事件为EPOLLIN
if (sk->sk_shutdown & RCV_SHUTDOWN)
mask |= EPOLLIN | EPOLLRDNORM | EPOLLRDHUP;
// 当我们的socket处于TCP_ESTABLISHED等状态时
if (state != TCP_SYN_SENT &&
(state != TCP_SYN_RECV || tp->fastopen_rsk)) {
...
// 如果我们的socket里有可读字节,epoll对应的事件就是EPOLLIN
if (tcp_stream_is_readable(tp, target, sk))
mask |= EPOLLIN | EPOLLRDNORM;
if (!(sk->sk_shutdown & SEND_SHUTDOWN)) {
// 如果我们的socket有可写空间,epoll事件就是EPOLLOUT
if (sk_stream_is_writeable(sk)) {
mask |= EPOLLOUT | EPOLLWRNORM;
} else {
...
}
} else
// 如果我们的socket关闭了SEND_SHUTDOWN,epoll事件就是EPOLLOUT
mask |= EPOLLOUT | EPOLLWRNORM;
...
} else if (state == TCP_SYN_SENT && inet_sk(sk)->defer_connect) {
...
}
...
// 如果我们的socket发生错误了,epoll事件就是EPOLLERR
if (sk->sk_err || !skb_queue_empty(&sk->sk_error_queue))
mask |= EPOLLERR;
return mask;
}
EXPORT_SYMBOL(tcp_poll);
该方法就是epoll检查socket有哪些就绪事件时调用的方法。
由该方法可见,只要socket处于RCV_SHUTDOWN状态,就一直有epollin事件,只要socket处于SEND_SHUTDOWN状态,就一直有epollout事件。
写段代码来证明下,先看epollin事件:
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define PORT 9999
#define MAX_EVENTS 10
static int tcp_listen() {
int lfd, opt, err;
struct sockaddr_in addr;
lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
assert(lfd != -1);
opt = 1;
err = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
assert(!err);
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
err = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
assert(!err);
err = listen(lfd, 8);
assert(!err);
return lfd;
}
static void epoll_ctl_add(int epfd, int fd, int evts) {
struct epoll_event ev;
ev.events = evts;
ev.data.fd = fd;
int err = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
assert(!err);
}
static void handle_events(struct epoll_event *e, int epfd) {
int err, m;
char buf[8192];
static int n;
printf("sockfd %d: ", e->data.fd);
if (e->events & EPOLLIN) {
printf("EPOLLIN");
e->events &= ~EPOLLIN;
m = read(e->data.fd, buf, 1);
assert(m == 0);
printf("(read返回0) ");
}
if (e->events & EPOLLOUT) {
printf("EPOLLOUT ");
e->events &= ~EPOLLOUT;
n++;
}
if (e->events & EPOLLHUP) {
printf("EPOLLHUP ");
e->events &= ~EPOLLHUP;
}
if (e->events & EPOLLERR) {
printf("EPOLLERR ");
e->events &= ~EPOLLERR;
}
assert(e->events == 0);
printf("\n");
if (n == 1) { // 连接建立成功后直接关闭receive端
err = shutdown(e->data.fd, SHUT_RD);
assert(!err);
}
while (n > 1) {
err = write(e->data.fd, buf, sizeof(buf) / sizeof(buf[0]));
if (err == -1) {
assert(errno == EAGAIN);
break;
}
}
}
int main(int argc, char *argv[]) {
int epfd, lfd, cfd, err, n;
struct epoll_event events[MAX_EVENTS];
epfd = epoll_create1(0);
assert(epfd != -1);
lfd = tcp_listen();
epoll_ctl_add(epfd, lfd, EPOLLIN);
for (;;) {
n = epoll_wait(epfd, events, MAX_EVENTS, -1);
assert(n != -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd != lfd) {
handle_events(&events[i], epfd);
continue;
}
cfd = accept(lfd, NULL, NULL);
assert(cfd != -1);
err = fcntl(cfd, F_SETFL, O_NONBLOCK);
assert(!err);
epoll_ctl_add(epfd, cfd, EPOLLIN | EPOLLOUT | EPOLLET);
}
}
return 0;
}
和之前一样,这里我们只要重点关注handle_events方法就好。
执行该程序后,用ncat对其进行连接,该程序所在终端的输出如下:
$ gcc server.c && ./a.out
sockfd 5: EPOLLOUT
sockfd 5: EPOLLIN(read返回0) EPOLLOUT
sockfd 5: EPOLLIN(read返回0) EPOLLOUT
sockfd 5: EPOLLIN(read返回0) EPOLLOUT
sockfd 5: EPOLLIN(read返回0) EPOLLOUT
# 一直输出上面相同行 #
可以看到,当我们用write方式一直触发epollout事件时,epollin事件也在同时发生。
所以,即使我们read返回0,也不能保证之后不会发生epollin事件。
我们再来看下epollout事件是否也是这样。
先将handle_events方法改成下面这样:
static void handle_events(struct epoll_event *e, int epfd) {
int err;
static int n;
printf("sockfd %d: ", e->data.fd);
if (e->events & EPOLLIN) {
printf("EPOLLIN ");
e->events &= ~EPOLLIN;
}
if (e->events & EPOLLOUT) {
printf("EPOLLOUT ");
e->events &= ~EPOLLOUT;
n++;
}
if (e->events & EPOLLHUP) {
printf("EPOLLHUP ");
e->events &= ~EPOLLHUP;
}
if (e->events & EPOLLERR) {
printf("EPOLLERR ");
e->events &= ~EPOLLERR;
}
assert(e->events == 0);
printf("\n");
if (n == 1) { // 连接建立成功后直接关闭send端
err = shutdown(e->data.fd, SHUT_WR);
assert(!err);
}
}
运行该程序后,用ncat对其建立tcp连接,然后一直在ncat终端输入数据,你会看到运行我们程序的终端有如下输出:
$ gcc server.c && ./a.out
sockfd 5: EPOLLOUT
sockfd 5: EPOLLOUT
sockfd 5: EPOLLOUT
sockfd 5: EPOLLIN EPOLLOUT
sockfd 5: EPOLLIN EPOLLOUT
sockfd 5: EPOLLIN EPOLLOUT
# 一直输出上面相同行 #
由上可见,即使我们关闭了send端,epollout事件还是会返回。
但如果我们不想要这种结果呢?比如说,当read返回0后,就不要再返回epollin事件,这怎么做呢?
其实说来也简单,你只要把你不想要的事件从epoll注册中移除就好了。
虽然epoll还是会调用tcp_poll方法,返回的socket事件还是包含所有的就绪事件,但它在返回给用户时,会过滤掉我们不感兴趣的事件。
所以,当read返回0时,你只要把epollin事件从epoll注册中取消,以后就再也不会有这个事件发生了。
本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!