这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
前面我们比较深入切直白的分析了io到nio的演变,其中提到了epoll多路复用,今天主要结合着redis、nginx来重点了解下epoll这个玩意儿
首先我们要搞清楚什么是多路复用,简单理解就是:一个服务端进程可以同时处理多个套接字描述符。
- 多路:多个客户端连接(连接就是套接字描述符)
- 复用:使用单进程就能够实现同时处理多个客户端的连接
废话不多说,直接干吧(我在机器上安装了redis和nginx,如果你想操练下可以按照下面的步骤走一遍)
nginx
启动并追踪
通过如下命令启动并且追踪nginx
strace -ff -o ./nginxout /usr/local/nginx/sbin/nginx
执行上面命令后发现生成了如下三个文件,对应的是三个进程
通过ps -ef|grep nginx可以发现Nginx 实际上是有两个进程master-14607和worker-14608(配置文件默认只有一个worker),那14606是啥玩意呢
过渡进程
看下nginxout.14606这个文件,如下:
mkdir("/usr/local/nginx/scgi_temp", 0700) = -1 EEXIST (File exists)
stat("/usr/local/nginx/scgi_temp", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
openat(AT_FDCWD, "/usr/local/nginx/logs/access.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 4
fcntl(4, F_SETFD, FD_CLOEXEC) = 0
openat(AT_FDCWD, "/usr/local/nginx/logs/error.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 5
fcntl(5, F_SETFD, FD_CLOEXEC) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 6
setsockopt(6, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
ioctl(6, FIONBIO, [1]) = 0
bind(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(6, 511) = 0
listen(6, 511) = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=1024*1024, rlim_max=1024*1024}) = 0
mmap(NULL, 384, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0) = 0x7fe7f8911000
rt_sigaction(SIGHUP, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGUSR1, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGWINCH, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGTERM, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGQUIT, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGUSR2, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGALRM, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGINT, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGIO, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGCHLD, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGSYS, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGPIPE, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe7f8907a10) = 14607
exit_group(0) = ?
+++ exited with 0 +++
~
可以看到这个文件的最后一行显示退出了,但是在退出之前它还在做了它该做的,截取如下:
// 创建socket
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 6
// 绑定端口
bind(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
//监听
listen(6, 511)
是不是有点通了呢,不要急我们继续看
其实它是一个过渡的程序,为啥这么说呢,仔细看上面的图片你会发现有个clone操作,也就是说14606生出了14607
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe7f8907a10) = 14607
出现了socket..、bind..、listen.. 但是并没有发现epoll操作,我们再去matser(14607)进程中看下
master进程
set_robust_list(0x7fe7f8907a20, 24) = 0
getpid() = 14607
setsid() = 14607
umask(000) = 022
openat(AT_FDCWD, "/dev/null", O_RDWR) = 7
dup2(7, 0) = 0
dup2(7, 1) = 1
close(7) = 0
openat(AT_FDCWD, "/usr/local/nginx/logs/nginx.pid", O_RDWR|O_CREAT|O_TRUNC, 0644) = 7
pwrite64(7, "14607\n", 6, 0) = 6
close(7) = 0
dup2(5, 2) = 2
close(3) = 0
rt_sigprocmask(SIG_BLOCK, [HUP INT QUIT USR1 USR2 ALRM TERM CHLD WINCH IO], NULL, 8) = 0
socketpair(AF_UNIX, SOCK_STREAM, 0, [3, 7]) = 0
ioctl(3, FIONBIO, [1]) = 0
ioctl(7, FIONBIO, [1]) = 0
ioctl(3, FIOASYNC, [1]) = 0
fcntl(3, F_SETOWN, 14607) = 0
fcntl(3, F_SETFD, FD_CLOEXEC) = 0
fcntl(7, F_SETFD, FD_CLOEXEC) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe7f8907a10) = 14608
rt_sigsuspend([], 8
然后你会发现master进程中的内容很少,因为master其实是不干活的,master中直接就是一个clone,创建了一个worker进程
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe7f8907a10) = 14608
worker进程
所以我们还是要接着往下找,看下worker进程干了哪些事(由于内容比较多,我这边copy了主要的内容并加了一些注释)
// 开辟了一块内核空间
epoll_create(512) = 8
// 将上面提到的6 放到这块内核空间中(6、7分别是ipv4、ipv6的)
epoll_ctl(8, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLRDHUP, {u32=4169998352, u64=140634284171280}}) = 0
close(3) = 0
epoll_ctl(8, EPOLL_CTL_ADD, 7, {EPOLLIN|EPOLLRDHUP, {u32=4169998560, u64=140634284171488}}) = 0
// 等待就绪
epoll_wait(8,
对于上图的6、7可以直接到主进程的fd看下(cd /proc/14607/fd)
然后再看下关于nginx中关于80端口的监听情况
以上是关于nginx中epoll的情况,
redis
启动并追踪
strace -ff -o ./redisout /software/redis-5.0.10/src/redis-server
日志一直在变大
主进程
进入主进程看看,注意下面这个图包含的信息量较大
重要的内容提取下
epoll_create(1024) = 5
socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP) = 6
bind(6, {sa_family=AF_INET6, sin6_port=htons(6379), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::", &sin6_addr), sin6_scope_id=0}, 28) = 0
listen(6, 511) = 0
fcntl(6, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 7
setsockopt(7, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(7, { =AF_INET, sin_port=htons(6379), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(7, 511) = 0
fcntl(7, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK) = 0
gettimeofday({tv_sec=1628522184, tv_usec=972588}, NULL) = 0
gettimeofday({tv_sec=1628522184, tv_usec=972618}, NULL) = 0
gettimeofday({tv_sec=1628522184, tv_usec=972647}, NULL) = 0
gettimeofday({tv_sec=1628522184, tv_usec=972677}, NULL) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 6, {EPOLLIN, {u32=6, u64=6}}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 7, {EPOLLIN, {u32=7, u64=7}}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 3, {EPOLLIN, {u32=3, u64=3}}) = 0
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
至于它为什么一直变大,可以通过tail -f redisout.14719命令,可以看到它一直在轮询,是不是发现有点不对啊
同样是使用epoll,我们前面介绍nginx是阻塞,但是redis怎么是轮询呢?其实仔细思考下应该会有一些想法
- redis是单线程,这一个线程除了读写还要去做其他事情(fork线程做LRU、RDB、AOF…),所以需要轮询,在循环中做io以及其他操作
- nginx也是单线程的,和redis一样使用了epoll,不同点在于nginx多进程,提高了稳定性和并发能力
IO是个细活,最近几篇文章主要是聚焦于如何高效的处理io,epoll多路复用解决就是io消息事件的事情,它可以通过事件驱动的方式告诉工作线程那是连接准备就绪了(可读/可写);工作线程拿到这些连接再去做读/写。IO很难一下子搞清楚,需要从多个方面去思考,文章主要是从实际操作日志的角度分析了多路复用的一些逻辑,希望对你有点帮助,先到这里吧,希望大家越来越好!
待得秋来九月八,我花开时百花杀 - 黄巢