今天又碰到一个epoll相关的问题,花了一些时间解决,在这里记录下。
首先还是看段测试代码:
#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;
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) {
err = shutdown(e->data.fd, SHUT_WR);
assert(!err);
}
}
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方法。
该方法最主要的目的是,当客户端建立tcp连接到服务端时,服务端立即调用shutdown方法,关闭其send方向。
最开始我认为服务端socket会连续两次触发epollout事件,所以也会连续两次输出epollout。
第一次是tcp连接建立后,在把服务端的socket加入到epoll的监听队列时,会触发一次epollout事件,第二次是shutdown系统调用里会触发的epollout事件,这个上篇文章有说过原因。
但执行测试程序后,epollout居然连续输出了三次,也就是说明,服务端socket连续三次触发epollout事件。
这个我就不明白了。
想了好久原因,也用了各种方式测试,最终在又一次翻内核源码时找到了答案。
我们知道,在服务端socket调用shutdown方法发送fin包之后,其会进入TCP_FIN_WAIT1状态,对方收到fin包,会发送一个ack包,告诉服务端socket,它已经收到。
所以,最有可能再次触发epollout事件的地方就是这个ack包的处理逻辑部分。
我们来直接看下内核源码:
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
...
switch (sk->sk_state) {
...
case TCP_FIN_WAIT1: {
...
tcp_set_state(sk, TCP_FIN_WAIT2);
sk->sk_shutdown |= SEND_SHUTDOWN;
...
if (!sock_flag(sk, SOCK_DEAD)) {
// 下面这个方法触发epollout事件
sk->sk_state_change(sk);
break;
}
...
}
...
}
...
}
EXPORT_SYMBOL(tcp_rcv_state_process);
果然如我们猜测的那样。
服务端socket在收到ack包后,会调用sk->sk_state_change方法,告知epoll,服务端的socket有新事件发生,而这个事件正是epollout事件。
口说无凭,如何证明一下真的是这个ack包导致的呢?
我们继续看。
首先我们先改下handle_events的代码,在shutdown之前,先sleep几秒,让我们有足够的时间做些其他事。
更改的结果如下:
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) {
printf("5秒钟之后将发送fin包给客户端,请于此请期间设置iptables规则\n");
sleep(5);
err = shutdown(e->data.fd, SHUT_WR);
assert(!err);
}
}
既然我们猜测是ack包导致的,那如果我们能让服务端socket无法收到这个ack包,理论上来说就不会有第三次epollout输出。
那怎么什么过滤这个ack包呢?用非常强大的iptables工具。
同时,我们也会辅以tcpdump的输出,来帮我们进一步确认结果。
我们先把实验过程的各个终端所有输出展示下。
下面是服务端输出:
$ gcc server.c && ./a.out
sockfd 5: EPOLLOUT
5秒钟之后将发送fin包给客户端,请于此请期间设置iptables规则
sockfd 5: EPOLLOUT
sockfd 5: EPOLLOUT
下面是客户端输出:
$ ncat -4 localhost 9999
下面是iptables输出:
$ sudo iptables -A INPUT -p tcp --dport 9999 --tcp-flags ACK ACK -j DROP && sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-A INPUT -p tcp -m tcp --dport 9999 --tcp-flags ACK ACK -j DROP
$ sudo iptables -F && sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
下面是tcpdump的输出:
$ sudo tcpdump -i any -n# -tttt port 9999
1 2019-08-02 18:20:57.715515 IP 127.0.0.1.58646 > 127.0.0.1.9999: Flags [S], seq 2234765919, win 65495, options [mss 65495,sackOK,TS val 3534452729 ecr 0,nop,wscale 7], length 0
2 2019-08-02 18:20:57.715528 IP 127.0.0.1.9999 > 127.0.0.1.58646: Flags [S.], seq 1561670940, ack 2234765920, win 65483, options [mss 65495,sackOK,TS val 3534452729 ecr 3534452729,nop,wscale 7], length 0
3 2019-08-02 18:20:57.715540 IP 127.0.0.1.58646 > 127.0.0.1.9999: Flags [.], ack 1, win 512, options [nop,nop,TS val 3534452729 ecr 3534452729], length 0
4 2019-08-02 18:21:02.716015 IP 127.0.0.1.9999 > 127.0.0.1.58646: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 3534457730 ecr 3534452729], length 0
5 2019-08-02 18:21:02.718586 IP 127.0.0.1.58646 > 127.0.0.1.9999: Flags [.], ack 2, win 512, options [nop,nop,TS val 3534457732 ecr 3534457730], length 0
6 2019-08-02 18:21:02.918653 IP 127.0.0.1.9999 > 127.0.0.1.58646: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 3534457932 ecr 3534452729], length 0
7 2019-08-02 18:21:02.918672 IP 127.0.0.1.58646 > 127.0.0.1.9999: Flags [.], ack 2, win 512, options [nop,nop,TS val 3534457932 ecr 3534457932,nop,nop,sack 1 {1:2}], length 0
# 省略大量的fin包及ack包的输出
20 2019-08-02 18:21:28.911939 IP 127.0.0.1.9999 > 127.0.0.1.58646: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 3534483926 ecr 3534452729], length 0
21 2019-08-02 18:21:28.911962 IP 127.0.0.1.58646 > 127.0.0.1.9999: Flags [.], ack 2, win 512, options [nop,nop,TS val 3534483926 ecr 3534483926,nop,nop,sack 1 {1:2}], length 0
实验的执行流程如下:
1. 开启tcpdump,监听客户端及服务端之间的tcp包。
2. 编译并启动服务端程序,此时服务端终端无任何输出,tcpdump也无任何输出。
3. 用ncat模拟客户端连接服务端,此时服务端终端输出第一次epollout,并紧接着输出上面的5秒提示那句话,同时tcpdump终端输出前三行,表示tcp三次握手成功,连接建立。
4. 在5秒钟之内,在iptables终端执行第一条命令,命令的大概意思就是丢弃到9999端口的tcp的ack包,也就是到服务端的ack包,该命令的输出内容可以参考上面。
5. 在5秒钟过后,服务端输出第二次epollout,证明shutdown方法确实通知了epoll,服务端socket有新epollout事件,而第三次epollout并没有输出。同时,tcpdump终端输出大量了fin和ack包,如上面的tcpdump的4到7行。
产生该行为的原因是,我们用iptables规则丢弃了客户端到服务端的ack包,服务端socket收不到这个ack包,就会重新发送fin包,所以就这样一直持续下去。
6. 等一段时间,在确定第三次epollout真的不会输出后,我们在iptables终端执行第二条命令,清除掉所有的iptables过滤规则。
过一段时间后,tcpdump终端会再一次输出服务端socket的fin重试包,以及客户端的ack包。 由于此次ack包不会被iptables过滤,所以服务端socket正常收到这个ack包,进而执行上面tcp_rcv_state_process方法内的逻辑,在该方法中,内核通知了epoll,服务端socket有新epollout事件。
也是在几乎同时,服务端终端输出第三次epollout。
实验结束。
该实验完美证实了,服务端socket的第三次epollout事件,就是由fin的ack包导致的。
不能再完美!
最后,在这里再推荐下我之前写的几篇相关文章:
完。
本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!