再谈Linux epoll惊群问题的原因和解决方案

2023-11-04

转自:https://blog.csdn.net/dog250/article/details/80837278

缘起

近期排查了一个问题,epoll惊群的问题,起初我并不认为这是惊群导致,因为从现象上看,只是体现了CPU不均衡。一共fork了20个Server进程,在请求负载中等的时候,有三四个Server进程呈现出比较高的CPU利用率,其余的Server进程的CPU利用率都是非常低。

中断,软中断都是均衡的,网卡RSS和CPU之间进行了bind之后依然如故,既然系统层面查不出个所以然,只能从服务的角度来查了。

自上而下的排查首先就想到了strace,没想到一下子就暴露了原形:

accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)

如果仅仅strace accept,即加上“-e trace=accept”参数的话,偶尔会有accept成功的现象:

accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, {sa_family=AF_INET, sin_port=htons(39306), sin_addr=inet_addr("172.16.1.202")}, [16]) = 19
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)

大量的CPU空转,进一步加大请求负载,CPU空转明显降低,这说明在预期的空转期间,新来的请求降低了空转率…现象明显偏向于这就是惊群导致的之判断!

本文将详细说一下关于epoll的细节。现在开始!


关于epoll的文章,我很早前写过一篇总结性的,可以参考这里:
Linux内核中网络数据包的接收-第二部分 select/poll/epollhttps://blog.csdn.net/dog250/article/details/50528373
不过这篇文章主要是原理性的介绍,对于一开始并不充分理解epoll机制的人来讲,可读性并不强,所以我准备写一篇稍微接地气的,比如带有一些“源码分析”的文章,虽然我并不是很喜欢源码分析,但有时对于快速理解为什么这样还是必要的。

题目中为什么是“再谈”,因为这个话题别人已经聊过很多了,我顺势继续下去而已。

简单介绍惊群和事件模型

关于什么是惊群,这里不再做概念上的解释,能搜到这篇文章的想必已经有所了解,如果仍有概念上的疑惑,自行百度或者谷歌。


惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理,这是通过以下的代码来实现的:

    list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
        unsigned flags = curr->flags;

        if (curr->func(curr, mode, wake_flags, key) &&
                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
    }

是的,添加了一个WQ_FLAG_EXCLUSIVE标记,告诉内核进行排他性的唤醒,即唤醒一个进程后即退出唤醒的过程,问题得以解决。

然而,没有哪个web服务器会傻到多个进程直接阻塞在accept上准备接收请求,在更高层次上,多路复用的需求让select,poll,epoll等事件模型更为受到欢迎,所谓的事件模型即阻塞在事件上而不是阻塞在事务上。内核仅仅通知发生了某件事,具体发生了什么事,则有处理进程或者线程自己来poll。如此一来,这个事件模型(无论其实现是select,poll,还是epoll)便可以一次搜集多个事件,从而满足多路复用的需求。

好了,基本原理就介绍到这里,下面我将来详细谈一下Linux epoll中的惊群问题,我们知道epoll在实际中要比直接accept实用性强很多,据我所知,除非编程学习或者验证性小demo,几乎没有直接accept的代码,所有的线上代码几乎都使用了事件模型。然而由于select,poll没有可扩展性,存在O(n)O(n)问题,因此在带宽越来越高,服务器性能越来越强的趋势下,越来越多的代码将收敛到使用epoll的情形,所以有必要对其进行深入的讨论。


Linux epoll惊群问题

知乎上有一个问题:
Linux 3.x 中epoll的惊群问题?https://www.zhihu.com/question/24169490/answers/created
建议先看一下,但不要看回答,因为知乎上上的很多回答往往会让事情变得更加混乱,除非你自己对这个问题已经有了自己的答案或者观点,否则还是不要去指望在诸多的答案中选一个自己满意的来用,还是要自己先思考。

下面我来就这个问题给一个答案,这也是我自己思考的答案:

  1. 在ep_poll的睡眠中加入WQ_FLAG_EXCLUSIVE标记,确实实实在在解决了epoll的惊群问题
  2. epoll_wait返回后确实也还有多个进程被唤醒只有一个进程能正确处理其他进程无事可做的情况发生,但这不是因为惊群,而是你的使用方法不对。

What?使用方法不对?

是的,使用方法不对。若想了解Why,则必须对epoll的实现细节以及其对外提供的API的语义有充分的理解,接下来我们就循着这个思路来撸个所以然。请继续阅读。

Linux epoll的实现机制

说起实现原理,很多人喜欢撸源码分析,我并不喜欢,我认为源码是自己看看就行了,搞这个行业的能看懂代码是一个最最基本的能力,我比较在意的是对某种机制内在逻辑的深入理解,而这个通过代码是体现不出来的,我一般会做下面几件事:

  • 运行起来并测得预期的数据
  • 看懂代码并画出原理图
  • 自己重新实现一版(时间精力允许的情况下)
  • 写个demo验证一些具体逻辑细节

不多说。

下面是我总结的一张关于Linux epoll的原理图:

这里写图片描述

要说代码实现上,其实也比较简单,大致有以下的几个逻辑:

  1. 创建epoll句柄,初始化相关数据结构
  2. 为epoll句柄添加文件句柄,注册睡眠entry的回调
  3. 事件发生,唤醒相关文件句柄睡眠队列的entry,调用其回调
  4. 唤醒epoll睡眠队列的task,搜集并上报数据

来,一个一个说

1.创建epoll句柄,初始化相关数据结构

这里主要就是创建一个epoll文件描述符,注意,后面操作epoll的时候,就是用这个epoll的文件描述符来操作的,所以这就是epoll的句柄,精简过后的epoll结构如下:

 struct eventpoll {
    // 阻塞在epoll_wait的task的睡眠队列
    wait_queue_head_t wq;
    // 存在就绪文件句柄的list,该list上的文件句柄事件将会全部上报给应用
    struct list_head rdllist;
    // 存放加入到此epoll句柄的文件句柄的红黑树容器
    struct rb_root rbr;
    // 该epoll结构对应的文件句柄,应用通过它来操作该epoll结构
    struct file *file;
};

2.为epoll句柄添加文件句柄,注册睡眠entry的回调

这个步骤中其实有两个子步骤:
1). 添加文件句柄
将一个文件句柄,比如socket添加到epoll的rbr红黑树容器中,注意,这里的文件句柄最终也是一个包装结构,和epoll的结构体类似:

struct epitem {
    // 该字段链接入epoll句柄的红黑树容器
    struct rb_node rbn;
    // 当该文件句柄有事件发生时,该字段链接入“就绪链表”,准备上报给用户态
    struct list_head rdllink;
    // 该字段封装实际的文件,我已经将其展开
    struct epoll_filefd {
        struct file *file;
        int fd;
    } ffd;
    // 反向指向其所属的epoll句柄
    struct eventpoll *ep;
};

以上结构实例就是epi,将被添加到epoll的rbr容器中的逻辑如下:

struct eventpoll *ep = 待加入文件句柄所属的epoll句柄;
struct file *tfile = 待加入的文件句柄file结构体;
int fd = 待加入的文件描述符ID;

struct epitem *epi = kmem_cache_alloc(epi_cache, GFP_KERNEL);
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
...
ep_rbtree_insert(ep, epi);

2). 注册睡眠entry回调并poll文件句柄
在第一个子步骤的代码逻辑中,我有一段“…”省略掉了,这部分比较关键,所以我单独抽取了出来作为第二个子步骤。
我们知道,Linux内核的sleep/wakeup机制非常重要,几乎贯穿了所有的内核子系统,值得注意的是,这里的sleep/wakeup依然采用了OO的思想,并没有限制睡眠的entry一定要是一个task,而是将睡眠的entry做了一层抽象,即:

struct __wait_queue {
    unsigned int flags;
    // 至于这个private到底是什么,内核并不限制,显然,它可以是task,也可以是别的。
    void *private;
    wait_queue_func_t func;
    struct list_head task_list;
};

以上的这个entry,最终要睡眠在下面的数据结构实例化的一个链表上:

struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
};

显然,在这里,一个文件句柄均有自己睡眠队列用于等待自己发生事件的entry在没有发生事件时来歇息,对于TCP socket而言,该睡眠队列就是其sk_wq,通过以下方式取到:

static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
    return &rcu_dereference_raw(sk->sk_wq)->wait;
}

我们需要一个entry将来在发生事件的时候从上述wait_queue_head_t中被唤醒,执行特定的操作,即将自己放入到epoll句柄的“就绪链表”中。下面的函数可以完成该逻辑的框架:

// 此处的whead就是上面例子中的sk_sleep返回的wait_queue_head_t实例。
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
                 poll_table *pt)
{
    struct epitem *epi = ep_item_from_epqueue(pt);
    struct eppoll_entry *pwq;

    if (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL)) {
        // 发生事件即调用ep_poll_callback回调函数,该回调函数会将自己这个epitem加入到epoll的“就绪链表”中去。
        init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
        // 是否排他唤醒取决于用户的配置,有些IO是希望唤醒所有entry来处理,有些则不必。注意,这里是针对文件句柄IO而言的,并不是针对epoll句柄的。
        if (epi->event.events & EPOLLEXCLUSIVE)
            add_wait_queue_exclusive(whead, &pwq->wait);
        else
            add_wait_queue(whead, &pwq->wait);

    } 
}

至于说什么时候调用上面的函数,Linux的poll机制仍然是采用了分层抽象的思想,即上述函数会作为另一个回调在相关文件句柄的poll函数中被调用。即:


static inline unsigned int ep_item_poll(struct epitem *epi, poll_table *pt)
{
    pt->_key = epi->event.events;
    return epi->ffd.file->f_op->poll(epi->ffd.file, pt) & epi->event.events;
}

对于TCP socket而言,其file_operations的poll回调即:

unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
    unsigned int mask;
    struct sock *sk = sock->sk;
    const struct tcp_sock *tp = tcp_sk(sk);
    // 此函数会调用poll_wait->wait._qproc
    // 而wait._qproc就是ep_ptable_queue_proc
    sock_poll_wait(file, sk_sleep(sk), wait);
    ...
}

现在,我们可以把子步骤1中的逻辑补全了:

struct eventpoll *ep = 待加入文件句柄所属的epoll句柄;
struct file *tfile = 待加入的文件句柄file结构体;
int fd = 待加入的文件描述符ID;

struct epitem *epi = kmem_cache_alloc(epi_cache, GFP_KERNEL);
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
// 这里会将wait._qproc初始化成ep_ptable_queue_proc
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
// 这里会调用wait._qproc即ep_ptable_queue_proc,安排entry的回调函数ep_poll_callback,并将entry“睡眠”在socket的sk_wq这个睡眠队列上。
revents = ep_item_poll(epi, &epq.pt);
ep_rbtree_insert(ep, epi);
// 如果刚才的ep_item_poll取出了事件,随即将该item挂入“就绪队列”中,并且wakeup阻塞在epoll_wait系统调用中的task!
if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
    list_add_tail(&epi->rdllink, &ep->rdllist);
    if (waitqueue_active(&ep->wq))
        wake_up_locked(&ep->wq);
}

3.事件发生,唤醒相关文件句柄睡眠队列的entry,调用其回调

上面已经很详细地描述了epoll的基础设施了,现在我们假设一个TCP Listen socket上来了一个连接请求,已经完成了三次握手,内核希望通知epoll_wait返回,然后去取accept。
内核在wakeup这个socket的sk_wq时,最终会调用到ep_poll_callback回调,这个函数我们说了好几次了,现在看看它的真面目:

static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    unsigned long flags;
    struct epitem *epi = ep_item_from_wait(wait);
    struct eventpoll *ep = epi->ep;
    // 这个lock比较关键,操作“就绪链表”相关的,均需要这个lock,以防丢失事件。
    spin_lock_irqsave(&ep->lock, flags);
    // 如果发生的事件我们并不关注,则不处理直接返回即可。
    if (key && !((unsigned long) key & epi->event.events))
        goto out_unlock;

    // 实际将发生事件的epitem加入到“就绪链表”中。
    if (!ep_is_linked(&epi->rdllink)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);
    }
    // 既然“就绪链表”中有了新成员,则唤醒阻塞在epoll_wait系统调用的task去处理。注意,如果本来epi已经在“就绪队列”了,这里依然会唤醒并处理的。
    if (waitqueue_active(&ep->wq)) {
        wake_up_locked(&ep->wq);
    }

out_unlock:
    spin_unlock_irqrestore(&ep->lock, flags);
    ...
}

没什么好多说的。现在“就绪链表”已经有epi了,接下来就要唤醒epoll_wait进程去处理了。

4.唤醒epoll睡眠队列的task,搜集并上报数据
这个逻辑主要集中在ep_poll函数,精简版如下:

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, long timeout)
{
    unsigned long flags;
    wait_queue_t wait;

    // 当前没有事件才睡眠
    if (!ep_events_available(ep)) {
        init_waitqueue_entry(&wait, current);
        __add_wait_queue_exclusive(&ep->wq, &wait);
        for (;;) {
            set_current_state(TASK_INTERRUPTIBLE);
            ...// 例行的schedule timeout
        }
        __remove_wait_queue(&ep->wq, &wait);
        set_current_state(TASK_RUNNING);
    }
    // 往用户态上报事件,即那些epoll_wait返回后能获取的事件。
    ep_send_events(ep, events, maxevents);
}

其中关键在ep_send_events,这个函数实现了非常重要的逻辑,包括LT和ET的逻辑,我不打算深入去解析这个函数,只是大致说下流程:

ep_scan_ready_list()
{
    // 遍历“就绪链表”
    ready_list_for_each() {
        // 将epi从“就绪链表”删除
        list_del_init(&epi->rdllink);
        // 实际获取具体的事件。
        // 注意,睡眠entry的回调函数只是通知有“事件”,具体需要每一个文件句柄的特定poll回调来获取。
        revents = ep_item_poll(epi, &pt);
        if (revents) {
            if (__put_user(revents, &uevent->events) ||
                __put_user(epi->event.data, &uevent->data)) {
                // 如果没有完成,则将epi重新加回“就绪链表”等待下次。
                list_add(&epi->rdllink, head);
                return eventcnt ? eventcnt : -EFAULT;
            }
            // 如果是LT模式,则无论如何都会将epi重新加回到“就绪链表”,等待下次重新再poll以确认是否仍然有未处理的事件。这也符合“水平触发”的逻辑,即“只要你不处理,我就会一直通知你”。
            if (!(epi->event.events & EPOLLET)) {
                list_add_tail(&epi->rdllink, &ep->rdllist);
            }
        }
    }
    // 如果“就绪链表”上仍有未处理的epi,且有进程阻塞在epoll句柄的睡眠队列,则唤醒它!(这将是LT惊群的根源)
    if (!list_empty(&ep->rdllist)) {
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);
    }
}

这里的代码逻辑的分析过程就到此为止了。以对这个代码逻辑的充分理解为基础,接下来我们就可以看具体的问题细节了。

下面一小节先从LT(水平触发模式)以及ET(即边沿触发模式)开始。

epoll的LT和ET以及相关细节问题

简单点解释:

  • LT水平触发
    如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你。
  • ET边沿触发
    如果事件来了,不管来了几个,你若不处理或者没有处理完,除非下一个事件到来,否则epoll将不会再通知你。

理解了上面说的两个模式,便可以很明确地展示可能会遇到的问题以及解决方案了,这将非常简单。

LT水平触发模式的问题以及解决

下面是epoll使用中非常常见的代码框架,我将问题注释于其中:

// 否则会阻塞在IO系统调用,导致没有机会再epoll
set_socket_nonblocking(sd);
epfd = epoll_create(64);
event.data.fd = sd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sd, &event);
while (1) {
    epoll_wait(epfd, events, 64, xx);
    ... // 危险区域!如果有共享同一个epfd的进程/线程调用epoll_wait,它们也将会被唤醒!
    // 这个accept将会有多个进程/线程调用,如果并发请求数很少,那么将仅有几个进程会成功:
    // 1. 假设accept队列中有n个请求,则仅有n个进程能成功,其它将全部返回EAGAIN (Resource temporarily unavailable)
    // 2. 如果n很大(即增加请求负载),虽然返回EAGAIN的比率会降低,但这些进程也并不一定取到了epoll_wait返回当下的那个预期的请求。
    csd = accept(sd, &in_addr, &in_len); 
    ...
}

这一切为什么会发生?

我们结合理论和代码一起来分析。

再看一遍LT的描述“如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你。”,显然,epoll_wait刚刚取到事件的时候的时候,不可能马上就调用accept去处理,事实上,逻辑在epoll_wait函数调用的ep_poll中还没返回的,这个时候,显然符合“仍然有未处理的事件”这个条件,显然这个时候为了实现这个语义,需要做的就是通知别的同样阻塞在同一个epoll句柄睡眠队列上的进程!在实现上,这个语义由两点来保证:

  1. 保证1:在LT模式下,“就绪链表”上取出的epi上报完事件后会重新加回“就绪链表”;
  2. 保证2:如果“就绪链表”不为空,且此时有进程阻塞在同一个epoll句柄的睡眠队列上,则唤醒它。
ep_scan_ready_list()
{
    // 遍历“就绪链表”
    ready_list_for_each() {
        list_del_init(&epi->rdllink);
        revents = ep_item_poll(epi, &pt);
        // 保证1
        if (revents) {
            __put_user(revents, &uevent->events);
            if (!(epi->event.events & EPOLLET)) {
                list_add_tail(&epi->rdllink, &ep->rdllist);
            }
        }
    }
    // 保证2
    if (!list_empty(&ep->rdllist)) {
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);
    }
}

我们来看一个情景分析。

假设LT模式下有10个进程共享同一个epoll句柄,此时来了一个请求client进入到accept队列,我们发现上述的1和2是一个循环唤醒的过程:

1).假设进程a的epoll_wait首先被ep_poll_callback唤醒,那么满足1和2,则唤醒了进程B;
2).进程B在处理ep_scan_ready_list的时候,发现依然满足1和2,于是唤醒了进程C….
3).上面1)和2)的过程一直到之前某个进程将client取出,此时下一个被唤醒的进程在ep_scan_ready_list中的ep_item_poll调用中将得不到任何事件,此时便不会再将该epi加回“就绪链表”了,LT水平触发结束,结束了这场悲伤的梦!

问题非常明确了,但是怎么解决呢?也非常简单,让不同进程的epoll_waitI调用互斥即可。

但是且慢!

上面的情景分析所展示的是一个“惊群效应”吗?其实并不是!对于Listen socket,当然要避免这种情景,但是对于很多其它的I/O文件句柄,说不定还指望着大家一起来read数据呢…所以说,要说互斥也仅仅要针对Listen socket的epoll_wait调用而言。

换句话说,这里epoll LT模式下有进程被不必要唤醒,这一点并不是内核无意而为之的,内核肯定是知道这件事的,这个并不像之前accept惊群那样算是内核的一个缺陷。epoll LT模式只是提供了一种模式,误用这种模式将会造成类似惊群那样的效应。但是不管怎么说,为了讨论上的方便,后面我们姑且将这种效应称作epoll LT惊群吧。

除了epoll_wait互斥之外,还有一种解决问题的方案,即使用ET边沿触发模式,但是会遇到新的问题,我们接下来来描述。

ET边沿触发模式的问题以及解决

ET模式不满足上述的“保证1”,所以不会将已经上报事件的epi重新链接回“就绪链表”,也就是说,只要一个“就绪队列”上的epi上的事件被上报了,它就会被删除出“就绪队列”。

由于epi entry的callback即ep_poll_callback所做的事情仅仅是将该epi自身加入到epoll句柄的“就绪链表”,同时唤醒在epoll句柄睡眠队列上的task,所以这里并不对事件的细节进行计数,比如说,如果ep_poll_callback在将一个epi加入“就绪链表”之前发现它已经在“就绪链表”了,那么就不会再次添加,因此可以说,一个epi可能pending了多个事件,注意到这点非常重要!

一个epi上pending多个事件,这个在LT模式下没有任何问题,因为获取事件的epi总是会被重新添加回“就绪链表”,那么如果还有事件,在下次check的时候总会取到。然而对于ET模式,仅仅将epi从“就绪链表”删除并将事件本身上报后就返回了,因此如果该epi里还有事件,则只能等待再次发生事件,进而调用ep_poll_callback时将该epi加入“就绪队列”。这意味着什么?

这意味着,应用程序,即epoll_wait的调用进程必须自己在获取事件后将其处理干净后方可再次调用epoll_wait,否则epoll_wait不会返回,而是必须等到下次产生事件的时候方可返回。即,依然以accept为例,必须这样做:

// 否则会阻塞在IO系统调用,导致没有机会再epoll
set_socket_nonblocking(sd);
epfd = epoll_create(64);
event.data.fd = sd;
// 添加ET标记
event.events |= EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, sd, &event);
while (1) {
    epoll_wait(epfd, events, 64, xx);
    while ((csd = accept(sd, &in_addr, &in_len)) > 0) {
        do_something(...);
    } 
    ...
}

好了,解释完了。


以上就是epoll的LT,ET相关的两个问题和解决方案。接下来的一节,我将用一个小小的简单Demo来重现上面描述的理论和代码。

测试demo

是时候给出一个实际能run的代码了:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netdb.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <time.h>
#include <signal.h>

#define COUNT 1

int mode = 0;
int slp = 0;

int pid[COUNT] = {0};
int count = 0;

void server(int epfd) 
{
    struct epoll_event *events;
    int num, i;
        struct timespec ts;

    events = calloc(64, sizeof(struct epoll_event));

    while (1) {
        int sd, csd;
        struct sockaddr in_addr;

        num = epoll_wait(epfd, events, 64, -1);
        if (num <= 0) {
            continue;
        }
        /*
        ts.tv_sec = 0;
        ts.tv_nsec = 1;
        if(nanosleep(&ts, NULL) != 0) {
            perror("nanosleep");
            exit(1);
        }
        */
        // 用于测试ET模式下丢事件的情况
        if (slp) {
            sleep(slp);
        }

        sd = events[0].data.fd;
        socklen_t in_len = sizeof(in_addr);

        csd = accept(sd, &in_addr, &in_len);
        if (csd == -1) {
            // 打印这个说明中了epoll LT惊群的招了。
            printf("shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:%d\n", getpid()); 
            continue;
        }
        // 本进程一共成功处理了多少个请求。
        count ++;
        printf("get client:%d\n", getpid()); 
        close(csd);
    }
}

static void siguser_handler(int sig)
{
    // 在主进程被Ctrl-C退出的时候,每一个子进程均要打印自己处理了多少个请求。
    printf("pid:%d  count:%d\n", getpid(), count);
    exit(0);
}

static void sigint_handler(int sig)
{
    int i = 0;
    // 给每一个子进程发信号,要求其打印自己处理了多少个请求。
    for (i = 0; i < COUNT; i++) {
        kill(pid[i], SIGUSR1);
    }
}

int main (int argc, char *argv[])
{
    int ret = 0;
        int listener;
    int c = 0;
    struct sockaddr_in saddr;
    int port;
    int status;
        int flags;
    int epfd;
    struct epoll_event event;


    if (argc < 4) {
        exit(1);
    }

    // 0为LT模式,1为ET模式
    mode = atoi(argv[1]);
    port = atoi(argv[2]);
    // 是否在处理accept之前耽搁一会儿,这个参数更容易重现问题
    slp = atoi(argv[3]);

    signal(SIGINT, sigint_handler);

    listener = socket(PF_INET, SOCK_STREAM, 0);

    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(port);
    saddr.sin_addr.s_addr = INADDR_ANY;

    bind(listener, (struct sockaddr*)&saddr, sizeof(saddr));
    listen(listener, SOMAXCONN);

    flags = fcntl (listener, F_GETFL, 0);
    flags |= O_NONBLOCK;
    fcntl (listener, F_SETFL, flags);


    epfd = epoll_create(64);
    if (epfd == -1) {
        perror("epoll_create");
        abort();
    }

    event.data.fd = listener;
    event.events = EPOLLIN;
    if (mode == 1) {
        event.events |= EPOLLET;
    } else if (mode == 2) {
        event.events |= EPOLLONESHOT;
    } 

    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listener, &event);
    if (ret == -1) {
        perror("epoll_ctl");
        abort();
    }


    for(c = 0; c < COUNT; c++) {
        int child;
            child = fork();
            if(child == 0) {
                    // 安装打印count值的信号处理函数
                    signal(SIGUSR1, siguser_handler);
                    server(epfd);
            }
        pid[c] = child;
        printf("server:%d  pid:%d\n", c+1, child);
        }
    wait(&status);
    sleep(1000000);
    close (listener);
}

编译之,为a.out。

测试客户端选用了简单webbench,首先我们看一下LT水平触发模式下的问题:

[zhaoya@shit ~/test]$ sudo ./a.out 0 112 0
server:1  pid:9688
server:2  pid:9689
server:3  pid:9690
server:4  pid:9691
server:5  pid:9692
server:6  pid:9693
server:7  pid:9694
server:8  pid:9695
server:9  pid:9696
server:10  pid:9697

另起一个终端运行webbench,并发10,测试5秒:

[zhaoya@shit ~/test]$ webbench -c 10 -t 5 http://127.0.0.1:112/        
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.

Benchmarking: GET http://127.0.0.1:112/
10 clients, running 5 sec.

而a.out的终端有以下输出:

...
get client:9690
get client:9688
get client:9691
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9693
get client:9692
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9689
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9697
get client:9691
get client:9696
get client:9690
get client:9690
get client:9695
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9697
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9689
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9692
get client:9696
get client:9688
get client:9695
get client:9693
get client:9689
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9691
get client:9695
get client:9691
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9692
get client:9690
get client:9694
get client:9693
...

所有的“shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:”的行均是被epoll LT惊群不必要唤醒的进程打印的。

接下来用ET模式运行:

[zhaoya@shit ~/test]$ sudo ./a.out 1 112 0

对应的输出如下:

...
get client:14462
get client:14462
get client:14464
get client:14464
get client:14462
get client:14462
get client:14467
get client:14469
get client:14468
get client:14468
get client:14464
get client:14467
get client:14467
get client:14469
get client:14469
get client:14469
get client:14464
get client:14464
get client:14466
get client:14466
get client:14469
get client:14469
...

没有任何一行是shit,即没有被不必要唤醒的惊群现象发生。

以上两个case确认了epoll LT模式的惊群效应是可以通过改用ET模式来解决的,接下来我们确认ET模式非循环处理会丢失事件

用ET模式运行a.out,这时将slp参数设置为1,即在epoll_wait返回和实际accept之间耽搁1秒,这样可以让一个epi在被加入到“就绪链表”中之后,在其被实际accept处理之前,积累更多的未决事件,即未处理的请求,而我们实验的目的则是,epoll ET会丢失这些事件。

webbench的参数依然如故,a.out的输出如下:

[zhaoya@shit ~/test]$ sudo ./a.out 1 114 1   
server:1  pid:31161
server:2  pid:31162
server:3  pid:31163
server:4  pid:31164
server:5  pid:31165
server:6  pid:31166
server:7  pid:31167
server:8  pid:31168
server:9  pid:31169
server:10  pid:31170
get client:31170
get client:31170
get client:31167
get client:31169
get client:31166
get client:31165
get client:31170
get client:31167
get client:31169
get client:31165
get client:31168
get client:31170
get client:31167
get client:31165
get client:31169
get client:31170
get client:31167
get client:31169
get client:31170
get client:31167
get client:31169

^Cpid:31170  count:6
pid:31169  count:5
pid:31163  count:0
pid:31168  count:1
pid:31167  count:5
pid:31165  count:3
pid:31166  count:1
pid:31161  count:0
pid:31162  count:0
pid:31164  count:0
User defined signal 1

同样的webbench参数,仅仅处理了十几个请求,可见大多数都丢掉了。如果我们用LT模式,同样在sleep 1秒导致事件挤压的情况下,是不是会多处理一些呢?我们的预期应该是肯定的,因为LT模式在事件被处理完之前,会一直促使epoll_wait返回继续处理,那么让我们试一下:

[zhaoya@shit ~/test]$ sudo ./a.out 0 115 1  
server:1  pid:363
server:2  pid:364
server:3  pid:365
server:4  pid:366
server:5  pid:367
server:6  pid:368
server:7  pid:369
server:8  pid:370
server:9  pid:371
server:10  pid:372
get client:372
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:371
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:365
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:366
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:363
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:367
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:369
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:364
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:368
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:370
get client:370
get client:364
get client:367
get client:368
get client:369
get client:365
get client:371
get client:372
get client:363
get client:366
get client:370
get client:367
get client:364
get client:369
get client:371
get client:368
get client:366
get client:363
get client:365
get client:372
get client:370
get client:367
get client:364
get client:371
get client:369
get client:366
get client:368
get client:363
get client:365
get client:372
get client:370
get client:367
get client:371
get client:364
get client:369
get client:366
get client:365
get client:368
get client:363
get client:372
get client:370
get client:364
get client:371
get client:367
get client:366
get client:369
get client:365
get client:363
get client:368
get client:372
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:371
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:370
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:364
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:367
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:366
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:369
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:365
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:363
^Cpid:363  count:5
pid:368  count:5
pid:372  count:6
pid:369  count:5
pid:366  count:5
pid:370  count:5
pid:367  count:5
pid:371  count:5
pid:365  count:5
pid:364  count:5
User defined signal 1

是的,多处理了很多,但是出现了LT惊群,这也是意料之中的事。

最后,让我们把这个Demo代码小改一下,改成循环处理,依然采用ET模式,sleep 1秒,看看情况会怎样。修改后的代码如下:

void server(int epfd)
{
        struct epoll_event *events;
        int num, i;
        struct timespec ts;

        events = calloc(64, sizeof(struct epoll_event));

        while (1) {
                int sd, csd;
                struct sockaddr in_addr;

                num = epoll_wait(epfd, events, 64, -1);
                if (num <= 0) {
                        continue;
                }

                if (slp)
                        sleep(slp);

                sd = events[0].data.fd;
                socklen_t in_len = sizeof(in_addr);
                // 这里循环处理,一直到空。
                while ((csd = accept(sd, &in_addr, &in_len)) > 0) {
                        count ++;
                        printf("get client:%d\n", getpid());
                        close(csd);
                }
        }
}

改完代码后,再做同样参数的测试,结果大大不同:

[zhaoya@shit ~/test]$ sudo ./a.out 0 116 1
...
get client:3640
get client:3645
get client:3640
get client:3641
get client:3641
get client:3641
^Cpid:3642  count:14
pid:3647  count:33531
pid:3646  count:21824
pid:3648  count:22
pid:3644  count:32219
pid:3645  count:94449
pid:3641  count:8
pid:3640  count:85385
pid:3643  count:13
pid:3639  count:10
User defined signal 1  

可以看到,大多数的请求都得到了处理,同样的逻辑,epoll_wait返回后的循环读和一次读结果显然不同。

问题和解决方案都很明确了,可以结单了吗?我想是的,但是在终结这个话题之前,我还想说一些结论性的东西以供备忘和参考。

结论

曾经,为了实现并发服务器,出现了很多的所谓范式,比如下面的两个很常见:

  • 范式1:设置多个IP地址,多个IP地址同时侦听相同的端口,前端用4层负载均衡或者反向代理来对这些IP地址进行请求分发;
  • 范式2:Master进程创建一个Listen socket,然后fork出来N个worker进程,这N个worker进程同时侦听这个socket。

第一个范式与本文讲的epoll无关,更多的体现一种IP层的技术,这里不谈,这里仅仅说一下第二个范式。


为了保证元组的唯一性以及处理的一致性,很长时间以来对于服务器而言,是不允许bind同一个IP地址和端口对的。然而为了可以并发处理多个连接请求,则必须采用某种多处理的方式,为了多个进程可以同时侦听同一个IP地址端口对,便出现了create listener+fork这种模型,具体来讲就是:

sd = create_listen_socket();
for (i = 0; i < N; i++) {
    if (fork() == 0) {
        // 继承了父进程的文件描述符
        server(sd);
    }
}

然而这种模式仅仅是做到了进程级的可扩展性,即一个进程在忙时,其它进程可以介入帮忙处理,底层的socket句柄其实是同一个!简单点说,这是一个沙漏模型:
这里写图片描述
这种模型在处理同一个socket的时候,必须互斥,同时内核必须防止潜在的惊群效应,因为互斥的要求,有且仅有一个进程可以处理特定的请求。这就对编程造成了极大的干扰。

以本文所描述的case为例,如果不清楚epoll LT模式和ET模式潜在的问题,那么就很容易误用epoll导致比较令人头疼的后果。

非常幸运,reuseport出现后,模型彻底变成了桶状:
这里写图片描述

于是乎,使用了reuseport,一切都变得明朗了:

  • 不再依赖mem模型
  • 不再担心惊群

为什么reuseport没有惊群?首先我们要知道惊群发生的原因,就是同时唤醒了多个进程处理一个事件,导致了不必要的CPU空转。为什么会唤醒多个进程,因为发生事件的文件描述符在多个进程之间是共享的。而reuseport呢,侦听同一个IP地址端口对的多个socket本身在socket层就是相互隔离的,在它们之间的事件分发是TCP/IP协议栈完成的,所以不会再有惊群发生。

所以,结论是什么?

结论就是全部统一采用reuseport的方式吧,彻底解决惊群问题。

后记

--------------------- 本文来自 dog250 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/dog250/article/details/80837278?utm_source=copy

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

再谈Linux epoll惊群问题的原因和解决方案 的相关文章

  • LINUX:如何锁定内存中进程的页面

    我有一个 LINUX 服务器 运行一个具有大量内存占用的进程 某种数据库引擎 该进程分配的内存太大 需要将其中一部分换出 换出 我想做的是将所有其他进程 或正在运行的进程的子集 的内存页面锁定在内存中 以便只有数据库进程的页面被换出 例如
  • Linux无法删除文件

    当我找到文件时 我在删除它们时遇到问题 任务 必须找到带有空格的文件并将其删除 我的尝试 rm find L root grep i 但我有错误 rm cannot remove root test No such file or dire
  • Ubuntu Python shebang 线不工作

    无法让 shebang 线在 Ubuntu 中为 python 脚本工作 我每次只收到命令未找到错误 test py usr bin env python print Ran which python usr bin python 在 sh
  • ssh 连接超时

    我无法在 git 中 ssh 到 github bitbucket 或 gitlab 我通常会收到以下错误消息 如何避免它 输出 ssh T email protected cdn cgi l email protection i ssh
  • 为什么 Linux 原始套接字的 RX 环大小限制为 4GB?

    背景 我试图mmap 我的原始套接字的 RX 环形缓冲区64 bitLinux 应用程序 我的环由 4096 个块组成 每个块大小为 1MB 总共 4GB 请注意 每个 1MB 块中可以有许多帧 如果您好奇 请参阅此文档了解背景信息 htt
  • 执行命令而不将其保留在历史记录中[关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 在进行软件开发时 经常需要在命令行命令中包含机密信息 典型示例是将项目部署到服务器的凭据设置为环境变量 当我不想将某些命令存储在命令历史记
  • SSH,运行进程然后忽略输出

    我有一个命令可以使用 SSH 并在 SSH 后运行脚本 该脚本运行一个二进制文件 脚本完成后 我可以输入任意键 本地终端将恢复到正常状态 但是 由于该进程仍在我通过 SSH 连接的计算机中运行 因此任何时候它都会登录到stdout我在本地终
  • 相当于Linux中的导入库

    在 Windows C 中 当您想要链接 DLL 时 您必须提供导入库 但是在 GNU 构建系统中 当您想要链接 so 文件 相当于 dll 时 您就不需要链接 为什么是这样 是否有等效的 Windows 导入库 注意 我不会谈论在 Win
  • FileOutputStream.close() 中的设备 ioctl 不合适

    我有一些代码可以使用以下命令将一些首选项保存到文件中FileOutputStream 这是我已经写了一千遍的标准代码 FileOutputStream out new FileOutputStream file try BufferedOu
  • 我们真的应该使用 Chef 来管理 sudoers 文件吗?

    这是我的问题 我担心如果 Chef 破坏了 sudoers 文件中的某些内容 可能是 Chef 用户错误地使用了说明书 那么服务器将完全无法访问 我讨厌我们完全失去客户的生产服务器 因为我们弄乱了 sudoers 文件并且无法再通过 ssh
  • python获取上传/下载速度

    我想在我的计算机上监控上传和下载速度 一个名为 conky 的程序已经在 conky conf 中执行了以下操作 Connection quality alignr wireless link qual perc wlan0 downspe
  • 如何使用 Cloud Init 挂载未格式化的 EBS 卷

    Context 我正在使用https wiki jenkins io display JENKINS Amazon EC2 Plugin https wiki jenkins io display JENKINS Amazon EC2 Pl
  • Linux 上的静态 Qt5 构建:部署时如何处理字体?

    我使用这些配置选项创建了 Qt 5 2 0 库的静态版本 Ubuntu 12 04 开源 确认许可 force pkg config 发布 静止的 前缀 home juzzlin qt5 无icu opengl桌面 无油嘴滑舌 辅助功能 n
  • GMail 421 4.7.0 稍后重试,关闭连接

    我试图找出为什么它无法使用 GMail 从我的服务器发送邮件 为此 我使用 SwiftMailer 但我可以将问题包含在以下独立代码中
  • 使用非规范地址检索内存数据会导致 SIGSEGV 而不是 SIGBUS

    我无法使用以下汇编代码产生 总线错误 这里我使用的内存地址不是合法的 规范地址 那么 我怎样才能触发该错误呢 我在带有 NASM 2 14 02 的 Ubuntu 20 04 LTS 下运行这段代码 但它会导致负载出现 SIGSEGV 分段
  • 如何让R使用所有处理器?

    我有一台运行 Windows XP 的四核笔记本电脑 但查看任务管理器 R 似乎一次只使用一个处理器 如何让 R 使用全部四个处理器并加速我的 R 程序 我有一个基本系统 我使用它在 for 循环上并行化我的程序 一旦您了解需要做什么 此方
  • 从 Xlib 转换为 xcb

    我目前正在将我的一个应用程序从 Xlib 移植到 libxcb 但在查找有关我有时使用的 XInput2 扩展的信息时遇到了一些麻烦 libxcb 中有 XInput2 实现吗 如果是的话 在哪里可以找到文档 目前我在使用此功能时遇到问题
  • 内核的panic()函数是否完全冻结所有其他进程?

    我想确认内核的panic 功能和其他类似kernel halt and machine halt 一旦触发 保证机器完全冻结 那么 所有的内核和用户进程都被冻结了吗 是panic 可以被调度程序中断吗 中断处理程序仍然可以执行吗 用例 如果
  • 配置tomat的server.xml文件并自动生成mod_jk.conf

    我在用apache 2 2 15 and tomcat6 6 0 24 on CentOS 6 4并希望使用 tomcat 服务器的功能 通过添加以下内容自动生成 mod jk conf 文件
  • Linux 为一组进程保留一个处理器(动态)

    有没有办法将处理器排除在正常调度之外 也就是说 使用sched setaffinity我可以指示线程应该在哪个处理器上运行 但我正在寻找相反的情况 也就是说 我想从正常调度中排除给定的处理器 以便只有已明确调度的进程才能在那里运行 我还知道

随机推荐

  • 【CMake】CMake官方教程

    CMake CMake官方教程 很好的一个官方教程翻译文档 CMake简介 CMake是一个跨平台的 开源的构建工具 cmake是makefile的上层工具 它们的目的正是为了产生可移植的makefile 并简化自己动手写makefile时
  • 太阳能发电板的规格尺寸_光伏组件(太阳能电池板)规格表

    光伏组件 太阳能电池板 规格表 峰值 型号 材料 功率Pm watt 峰值电压Vmp V 峰值电流Imp A 开路电压Voc V 短路电流Isc A 尺寸 mm APM18M5W27x27 APM36M5W27x27 APM18P5W27x
  • 链表累加求和

    给定程序是建立一个带头结点的单向链表 函数fun的功能是将单向链表结点 不包括头结点 数据域为偶数的值累加起来 并且做为函数值返回 include
  • LeetCode--初级算法--回文链表

    题目 请判断一个链表是否为回文链表 示例 1 输入 1 gt 2 输出 false 示例 2 输入 1 gt 2 gt 2 gt 1 输出 true 进阶 你能否用 O n 时间复杂度和 O 1 空间复杂度解决此题 解题方法 其实 链表的题
  • redhat 安装 java_Redhat Linux安装JDK 1.7

    本篇主要介绍在Redhat Linux Red Hat Enterprise Linux Server release 5 7 Tikanga 系统上安装JDK 1 7 其它Linux平台安装也大同小异 可能略有差别 步骤1 下载JDK 1
  • 纪念2015年最后的10天

    至今天为止 我已经在现在的公司也是我的第一家公司待了一年零4个月整 经历了一段历程之后 忽然自己发问为什么这只菜鸟还是飞不高 飞不远 仔细想来 原因可能有以下几点 1 过分的依赖于网络查询 而忽略了个人总结 虽然笔记记了一大堆 但是每次用的
  • Compound Types: Enums and Structs___CH_10

    10 1 Introduction to program defined user defined types What are user defined program defined types Defining program def
  • ROS2 Humble如何使用串口驱动?(Serial)

    目录 1 串口库 Serial Library 简介 2 源码及主页 3 国内git仓库 ROS2 humble已测试可用 4 可能出现的问题
  • 抖音小程序怎么赚钱,都有哪些变现玩法技巧。

    流量 钱 在抖音小程序变现这个项目中不一定成立 A 第一个视频爆了100多万观看 连怼了一次爆到900万 照理说 那么大的曝光率 挂小程序应该赚很多钱 答 没有多少收益 为什么 因为A发的视频内容和挂的小程序不相关 爆的是搞笑的视频 而挂的
  • pikachu xss攻击模块 信息安全 xss漏洞 详细分析

    XSS攻击 跨站脚本漏洞测试流程 反射型xss get 反射型xss post 存储型xss 实例 xss钓鱼 DOM型xss Dom型xss x Xss盲打 Xss之过滤 Xss之htmlspecialchars Xss之href输出 X
  • 第四节:论文种类分类-学习笔记

    任务说明 学习主题 论文分类 数据建模任务 利用已有数据建模 对新论文进行类别分类 学习内容 使用论文标题完成类别分类 学习成果 学会文本分类的基本方法 TF IDF等 数据处理步骤 在原始arxiv论文中论文都有对应的类别 而论文类别是作
  • Python读取文本文件到数组

    支持的格式有txt dat csv mat等 读入的数据存为float类型 可以自定义数据之间的间隔符 可以自定义列数cols import numpy as np import linecache import os filename d
  • MinIO verify 接口敏感信息泄露漏洞分析(CVE-2023-28432)

    MinIO verify 接口敏感信息泄露漏洞 简介 漏洞描述 MinIO 是一种开源的对象存储服务 它兼容 Amazon S3 API 可以在私有云或公有云中使用 MinIO 是一种高性能 高可用性的分布式存储系统 它可以存储大量数据 并
  • 【转载】Java中将InputStream读取为String, 各种方法的性能对比

    Java中将InputStream读取为String 各种方法的性能对比 原文地址 http www cnblogs com milton p 6366916 html 如下 一共存在11种实现方式及其对应的性能测试结果 1 使用IOUti
  • 在wpf中利用异步lambda编程,模拟数据库连接,防止界面假死

    参考 图解C 第20章异步编程第五节程序 解决在wpf中连接数据库 界面假死问题 public partial class MainWindow Window private bool isConnected false public Ma
  • 山洪灾害监测预警系统解决方案

    一 方案背景 近几年我国频繁发生山洪灾害现象 造成大量的人员伤亡 使得洪涝灾害死亡总人数呈上升趋势 群死群伤事件时有发生 为了提高山洪灾害监测预警能力 加强灾害发生时的快速反应能力 我司研发出了山洪灾害监测预警系统设备 它具有技术先进 功能
  • 职工管理系统_201026(思路详解版-第四步修改职工)

    12 修改职工 功能描述 能够按照职工的编号对职工信息进行修改并保存 12 1 修改职工函数声明 在workerManager h中添加成员函数 void Mod Emp 修改职工 void Mod Emp 12 2 修改职工函数实现 在w
  • 使用PAM保障开发运营安全

    硬编码凭据和 DevOps 系统中缺乏凭据安全性是组织的巨大漏洞 以明文形式访问凭据的恶意内部人员可以在 IT 中建立和扩展其立足点 基础设施 构成巨大的数据被盗风险 什么是PAM 特权访问管理 PAM 是指一组 IT 安全管理原则 可帮助
  • Markdown 技能树(2):段落及强调

    Markdown 技能树 2 段落及强调 在 Markdown 中 段落是由一个以上相连接的行句组成 而一个以上的空行则会切分出不同的段落 一般的段落不需要用空白或换行缩排 空行的定义是显示上看起来像是空行 便会被视为空行 比如 若某一行只
  • 再谈Linux epoll惊群问题的原因和解决方案

    转自 https blog csdn net dog250 article details 80837278 缘起 近期排查了一个问题 epoll惊群的问题 起初我并不认为这是惊群导致 因为从现象上看 只是体现了CPU不均衡 一共fork了