深入理解Linux网络笔记(三):内核和用户进程协作之epoll

news2024/11/18 9:29:17

本文为《深入理解Linux网络》学习笔记,使用的Linux源码版本是3.10,网卡驱动默认采用的都是Intel的igb网卡驱动

Linux源码在线阅读:https://elixir.bootlin.com/linux/v3.10/source

2、内核是如何与用户进程协作的(二)

3)、内核和用户进程协作之epoll

IO多路复用:

在IO多路复用模型中,会有一个内核线程不断去轮询多个socket的状态,只有当真正读写事件发生时,才真正调用实际的IO读写操作。因为在IO多路复用模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有读写事件进行时,才会使用IO资源,所以它大大减少了资源占用

IO多路复用机制可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。这里的复用指的是对进程的复用

在Linux上多路复用方案有select、poll、epoll。它们三个中的epoll的性能表现是最优秀的,能支持的并发量也最大

epoll的简单示例如下:

int main() {
    listen(lfd, ...);
    cfd1 = accept(...);
    cfd2 = accept(...);
    efd = epoll_create(...);

    epoll_ctl(efd, EPOLL_CTL_ADD, cfd1, ...);
    epoll_ctl(efd, EPOLL_CTL_ADD, cfd2, ...);
    epoll_wait(efd, ...);
}

其中和epoll相关的函数是如下三个:

  • epoll_create:创建一个epoll对象
  • epoll_ctl:向epoll对象添加要管理的连接
  • epoll_wait:等待其管理的连接上的IO事件
1)epoll内核对象的创建

在用户进程调用epoll_create时,内核会创建一个struct eventpoll内核对象,并把它关联到当前进程的已打开文件列表中,如下图所示:

对于struct eventpoll对象,更详细的结构如下图所示:

poll_create源码如下:

// fs/eventpoll.c
SYSCALL_DEFINE1(epoll_create1, int, flags)
{
	int error, fd;
	struct eventpoll *ep = NULL;
	...
	// 创建一个eventpoll对象
	error = ep_alloc(&ep);
	...
}

struct eventpoll的定义如下:

// fs/eventpoll.c
struct eventpoll {
	...
	// sys_epollo_wait用到的等待队列
	wait_queue_head_t wq;
	...
	// 接收就绪的描述符都会放到这里
	struct list_head rdllist;

	// 每个epollo对象中都有一棵红黑树
	struct rb_root rbr;
	...
}

eventpoll这个结构体的几个成员的含义如下:

  • wq:等待队列链表。软中断数据就绪的时候会通过wq来找到阻塞在epoll对象上的用户进程
  • rbr:一棵红黑树。为了支持对海量连接的高效查找、插入和删除,eventpoll内部使用了一棵红黑树。通过这棵树来管理用户进程添加进来的所有socket连接
  • rdllist:就绪的描述符的链表。当有连接就绪的时候,内核会把就绪的连接放到rdllist链表里。这样应用进程只需要判断链表就能找出就绪连接,而不用去遍历整棵树

eventpoll初始化工作在ep_alloc中完成:

// fs/eventpoll.c
static int ep_alloc(struct eventpoll **pep)
{
	...
	struct eventpoll *ep;
	...
	// 申请eventpoll内存
	ep = kzalloc(sizeof(*ep), GFP_KERNEL);
	...
	// 初始化等待队列头
	init_waitqueue_head(&ep->wq);
	init_waitqueue_head(&ep->poll_wait);
	// 初始化就绪队列
	INIT_LIST_HEAD(&ep->rdllist);
	// 初始化红黑树指针
	ep->rbr = RB_ROOT;
	...
}
2)为epoll添加socket

理解这一步是理解整个epoll的关键。为了简单起见,这里只考虑使用EPOLL_CTL_ADD添加socket,先忽略删除和更新

假设现在和客户端的多个连接的socket都创建好了,也创建好了epoll内核对象。在使用epoll_ctl注册每一个socket的时候,内核会做如下三件事情:

  1. 分配一个红黑树节点对象epitem
  2. 将等待事件添加到socket的等待队列中,其回调函数是ep_poll_callback
  3. 将epitem插入epoll对象的红黑树

通过epoll_ctl添加两个socket以后,这些内核数据结构最终在进程中的关系大致如下图所示:

来看看socket是如何添加到epoll对象里的,找到epoll_ctl的源码

// fs/eventpoll.c
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
		struct epoll_event __user *, event)
{
	...
	struct file *file, *tfile;
	struct eventpoll *ep;
	...
	// 根据epfd找到eventpoll内核对象
	file = fget(epfd);
	...
	// 根据socket句柄号,找到其file内核对象
	tfile = fget(fd);
	...
	ep = file->private_data;
	...
	switch (op) {
	case EPOLL_CTL_ADD:
		if (!epi) {
			epds.events |= POLLERR | POLLHUP;
			error = ep_insert(ep, &epds, tfile, fd);
		} else
			error = -EEXIST;
		clear_tfile_check_list();
		break;
	...
	}
	...
}

在epoll_ctl中首先根据传入fd找到eventpoll、socket相关的内核对象。对于EPOLL_CTL_ADD操作来说,会执行到ep_insert函数。所有的注册都是在这个函数中完成的

// fs/eventpoll.c
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
		     struct file *tfile, int fd)
{
	...
	struct epitem *epi;
	struct ep_pqueue epq;
	...
	// 1.分配并初始化epitem
	// 分配一个epi对象
	if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
		return -ENOMEM;
	...
	// 对分配的epi对象进行初始化
	INIT_LIST_HEAD(&epi->pwqlist);
	epi->ep = ep;
	// epi->ffd中存了句柄号和struct file对象地址
	ep_set_ffd(&epi->ffd, tfile, fd);
	...
	// 2.设置socket等待队列
	// 定义并初始化ep_pqueue对象
	epq.epi = epi;
	init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

	// 调用ep_ptable_queue_proc注册回调函数
	// 实际注入的函数为ep_poll_callback
	revents = ep_item_poll(epi, &epq.pt);
	...
	// 3.将epi插入eventpoll对象的红黑树中
	ep_rbtree_insert(ep, epi);
	...
}

分配并初始化epitem

对于每一个socket,调用epoll_ctl的时候,都会为之分配一个epitem。该结构的主要数据结构如下:

// fs/eventpoll.c
struct epitem {
	// 红黑树节点
	struct rb_node rbn;
	...
	// socket文件描述信息
	struct epoll_filefd ffd;
	...
	// 等待队列
	struct list_head pwqlist;

	// 所归属的eventpoll对象
	struct eventpoll *ep;
	...
}

对epitem进行一些初始化,首先在epi->ep = ep;这行代码中将其ep指针指向eventpoll对象。另外用要添加的socket的file、fd来填充epi->ffd。epitem初始化后的关联关系如下图所示:

其中使用到的ep_set_ffd函数如下:

// fs/eventpoll.c
static inline void ep_set_ffd(struct epoll_filefd *ffd,
			      struct file *file, int fd)
{
	ffd->file = file;
	ffd->fd = fd;
}

设置socket等待队列

在创建epitem并初始化之后,ep_insert中第二件事情就是设置socket对象上的等待任务队列,并把函数fs/eventpoll.c文件下的ep_poll_callback设置为数据就绪时候的回调函数,如下图所示:

先来看ep_item_poll

// fs/eventpoll.c
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;
}

这里调用了socket下的file->f_op->poll,这个函数实际上是sock_poll

// net/socket.c
static unsigned int sock_poll(struct file *file, poll_table *wait)
{
	struct socket *sock;
	sock = file->private_data;
	return sock->ops->poll(file, sock, wait);
}

sock->ops->poll指向的是tcp_poll

// net/ipv4/tcp.c
unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
	...
	struct sock *sk = sock->sk;
	...
	sock_poll_wait(file, sk_sleep(sk), wait);
	...
}

在sock_poll_wait的第二个参数传参前,先调用了sk_sleep函数。在这个函数里它获取了sock对象下的等待队列列表头wait_queue_head_t,稍后等待队列项就插到这里。这里稍微注意下,是socket的等待队列,不是epoll对象的。下面来看sk_sleep源码

// include/net/sock.h
static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
	BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
	return &rcu_dereference_raw(sk->sk_wq)->wait;
}

接着真正进入sock_poll_wait

// include/net/sock.h
static inline void sock_poll_wait(struct file *filp,
		wait_queue_head_t *wait_address, poll_table *p)
{
	...
		poll_wait(filp, wait_address, p);
	...
}
// include/linux/poll.h
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
	if (p && p->_qproc && wait_address)
		p->_qproc(filp, wait_address, p);
}

这里的qproc是个函数指针,它在前面的init_poll_funcptr调用时设置成了ep_ptable_queue_proc函数,ep_ptable_queue_proc源码如下:

// fs/eventpoll.c
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 (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
		// 初始化回调方法
		init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
		pwq->whead = whead;
		pwq->base = epi;
		// 将ep_poll_callback放入socket等待带队列whead(注意不是epollo等待队列)
		add_wait_queue(whead, &pwq->wait);
		list_add_tail(&pwq->llink, &epi->pwqlist);
		epi->nwait++;
	} else {
		epi->nwait = -1;
	}
}

在ep_ptable_queue_proc函数中,新建了一个等待队列项,并注册其回调函数为ep_poll_callback函数,然后再将这个等待项添加到socket的等待队列中

在前面介绍阻塞式的系统调用recvfrom时,由于需要在数据就绪的时候唤醒用户进程,所以等待对象项的private会设置成当前用户进程描述符current。而这里的socket是交给epoll来管理的,不需要在一个socket就绪的时候就唤醒进程,所以这里的q->private没有什么用就设置成了NULL

// include/linux/wait.h
static inline void init_waitqueue_func_entry(wait_queue_t *q,
					wait_queue_func_t func)
{
	q->flags = 0;
	q->private = NULL;
	q->func = func;
}

如上,等待队列项中仅将回调函数q->func设置为ep_poll_callback。后面讲到“数据来了”时,软中断将数据收到socket的接收队列后,会通过注册的这个ep_poll_callback函数来回调,进而通知epoll对象

插入红黑树

分配完epitem对象后,紧接着把它插入红黑树。一个插入了一些socket描述符的epoll里的红黑树示意图如下图所示:

这里使用红黑树是为了让epoll在查找效率、插入效率、内存开销等多个方法比较均衡

3)epoll_wait之等待接收

epoll_wait做的事情不复杂,当它被调用时它观察eventpoll->rdllist链表里有没有数据。有数据就返回,没有数据就创建一个等待队列项,将其添加到eventpoll的等待队列上,然后把自己阻塞掉

其源代码如下:

// fs/eventpoll.c
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
		int, maxevents, int, timeout)
{
	...
	error = ep_poll(ep, events, maxevents, timeout);
	...
}

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
		   int maxevents, long timeout)
{
	...
	wait_queue_t wait;
	...
fetch_events:
	spin_lock_irqsave(&ep->lock, flags);
	// 1.判断就绪队列上有没有事件就绪
	if (!ep_events_available(ep)) {
		// 2.定义等待事件关联当前进程
		init_waitqueue_entry(&wait, current);
		// 3.把新waitqueue添加到epoll->wq链表
		__add_wait_queue_exclusive(&ep->wq, &wait);

		for (;;) {
			// 4.让出CPU,主动进入睡眠状态
			set_current_state(TASK_INTERRUPTIBLE);
			if (ep_events_available(ep) || timed_out)
				break;
			if (signal_pending(current)) {
				res = -EINTR;
				break;
			}

			spin_unlock_irqrestore(&ep->lock, flags);
			if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
				timed_out = 1;

			spin_lock_irqsave(&ep->lock, flags);
		}
		__remove_wait_queue(&ep->wq, &wait);

		set_current_state(TASK_RUNNING);
	}
...
}

判断就绪队列上有没有事件就绪

首先调用ep_events_available来判断就绪链表中是否有可处理的事件

// fs/eventpoll.c
static inline int ep_events_available(struct eventpoll *ep)
{
	return !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;
}

定义等待事件关联当前进程

假设确实没有就绪的连接,那接着会进入init_waitqueue_entry中定义等待任务,并把current(当前进程)添加到waitqueue上

当没有IO事件的时候,epollo也会阻塞调当前进程,因为没有事情可做了占着CPU也没什么意义。epoll本身是阻塞的,但一般会把socket设置成非阻塞

// include/linux/wait.h
static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
{
	q->flags = 0;
	q->private = p;
	q->func = default_wake_function;
}

注意这里的回调函数名称是default_wake_function。后面讲到“数据来了”时将会调用该函数

添加到等待队列

// include/linux/wait.h
static inline void __add_wait_queue_exclusive(wait_queue_head_t *q,
					      wait_queue_t *wait)
{
	wait->flags |= WQ_FLAG_EXCLUSIVE;
	__add_wait_queue(q, wait);
}

在这里把定义的等待事件添加到了epoll对象的等待队列中

让出CPU主动进入睡眠状态

通过set_current_state把当前进程设置为可打断。调用schedule_hrtimeout_range让出CPU,主动进入睡眠状态

// kernel/hrtimer.c
int __sched schedule_hrtimeout_range(ktime_t *expires, unsigned long delta,
				     const enum hrtimer_mode mode)
{
	return schedule_hrtimeout_range_clock(expires, delta, mode,
					      CLOCK_MONOTONIC);
}

int __sched
schedule_hrtimeout_range_clock(ktime_t *expires, unsigned long delta,
			       const enum hrtimer_mode mode, int clock)
{
	...
		schedule();
	...
}

在schedule中选择下一个进程调度

// kernel/sched/core.c
static void __sched __schedule(void)
{
	...
	next = pick_next_task(rq);
	...
		context_switch(rq, prev, next);
	...
}
4)数据来了

在前面epoll_ctl执行的时候,内核为每一个socket都添加了一个等待队列项。在epoll_wait运行完的时候,又在event poll对象上添加了等待队列元素

  • socket->sock->sk_data_ready设置的就绪处理函数是sock_def_readable
  • 在socket的等待队列中,其回调函数是ep_poll_callback,private指向的是空指针null
  • 在eventpoll的等待队列项中,其回调函数是default_wake_function,private指向的是等待该事件的用户进程

将数据接收到任务队列

从TCP协议栈的处理入口函数tcp_v4_rcv开始说起:

// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
	...
    // 获取TCP头
	th = tcp_hdr(skb);
	// 获取IP头
	iph = ip_hdr(skb);
	...
	// 根据数据包头中的IP、端口信息查找到对应的socket
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
	...
	// socket未被用户锁定
	if (!sock_owned_by_user(sk)) {
		...
		{
			if (!tcp_prequeue(sk, skb))
				ret = tcp_v4_do_rcv(sk, skb);
		}
		...
}

在tcp_v4_rcv中首先根据收到的网络包的header里的source和dest信息在本机上查找对应的socket。找到以后,直接接入接收的主体函数tcp_v4_do_rcv

// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
	...
	if (sk->sk_state == TCP_ESTABLISHED) {
		...
        // 执行连接状态下的数据处理  
		if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
			rsk = sk;
			goto reset;
		}
		return 0;
	}
	// 其他非ESTABLISH状态的数据包处理
	...
}

假设处理的是ESTABLISH状态下的包,这样就又进入tcp_rcv_established函数中进行处理了

// net/ipv4/tcp_input.c
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
			const struct tcphdr *th, unsigned int len)
{
  			    ...
				// 将数据接收到队列中
				eaten = tcp_queue_rcv(sk, skb, tcp_header_len,
						      &fragstolen);
			}
			...
            // 数据准备好,唤醒socket上阻塞掉的进程  
			sk->sk_data_ready(sk, 0);
			...
}

在tcp_rcv_established中通过调用tcp_queue_rcv函数完成了将接收数据放到socket的接收队列上,如下图所示:

// net/ipv4/tcp_input.c
static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen,
		  bool *fragstolen)
{
	...
    // 把接收到的数据放到socket的接收队列的尾部 
	if (!eaten) {
		__skb_queue_tail(&sk->sk_receive_queue, skb);
		skb_set_owner_r(skb, sk);
	}
	return eaten;
}

查找就绪回调函数

调用tcp_queue_rcv完成接收之后,接着再调用sk_data_ready来唤醒在socket上等待的用户进程。在“socket的直接创建”中讲到的sock_init_data函数,已经把sk_data_ready设置成了sock_def_readable函数了。它是默认的数据就绪处理函数

当socket上数据就绪时,内核将以sock_def_readable这个函数为入口,找到epoll_ctl添加socket时在其上设置的回调函数ep_poll_callback,如下图所示:

// net/core/sock.c
static void sock_def_readable(struct sock *sk, int len)
{
	struct socket_wq *wq;

	rcu_read_lock();
	wq = rcu_dereference(sk->sk_wq);
    // 判断等待队列不为空
	if (wq_has_sleeper(wq))
        // 执行等待队列项上的回调函数
		wake_up_interruptible_sync_poll(&wq->wait, POLLIN | POLLPRI |
						POLLRDNORM | POLLRDBAND);
	sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
	rcu_read_unlock();
}

重点看wake_up_interruptible_sync_poll,看一下内核是怎么找到等待队列项里注册的回调函数的

// include/linux/wait.h
#define wake_up_interruptible_sync_poll(x, m)				\
	__wake_up_sync_key((x), TASK_INTERRUPTIBLE, 1, (void *) (m))
// kernel/sched/core.c
void __wake_up_sync_key(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, void *key)
{
	unsigned long flags;
	int wake_flags = WF_SYNC;

	if (unlikely(!q))
		return;

	if (unlikely(!nr_exclusive))
		wake_flags = 0;

	spin_lock_irqsave(&q->lock, flags);
	__wake_up_common(q, mode, nr_exclusive, wake_flags, key);
	spin_unlock_irqrestore(&q->lock, flags);
}

接着进入__wake_up_common

// kernel/sched/core.c
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key)
{
	wait_queue_t *curr, *next;

	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;
	}
}

__wake_up_common中,选出等待队列里注册的某个元素curr,回调其curr->func。之前调用ep_insert的时候,把这个func设置成ep_poll_callback了

执行socket就绪回调函数

找到了socket等待队列项里注册的函数ep_poll_callback,接着软中断就会调用它

// fs/eventpoll.c
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
	...
    // 获取wait对应的epitem
	struct epitem *epi = ep_item_from_wait(wait);
    // 获取epitem对应的eventpoll结构体
	struct eventpoll *ep = epi->ep;
	...
	if (!ep_is_linked(&epi->rdllink)) {
        // 1.将当前epitem添加到eventpoll的就绪队列中
		list_add_tail(&epi->rdllink, &ep->rdllist);
		ep_pm_stay_awake_rcu(epi);
	}
	// 2.查看eventpoll的等待队列上是否有等待
	if (waitqueue_active(&ep->wq))
		wake_up_locked(&ep->wq);
	...
}

在ep_poll_callback中根据等待任务队列上额外的base指针可以找到epitem,进而也可以找到eventpoll对象

它做的第一件事就是把自己的epitem添加到epoll的就绪队列中。接着它又会查看eventpoll对象上的的等待队列里是否有等待项(epoll_wait执行的时候会设置)。如果没有等待项,软中断的事情就做完了。如果有等待项,那就找到等待项里设置的回调函数,如下图所示:

依次调用wake_up_locked() => __wake_up_locked() => __wake_up_common

// kernel/sched/core.c
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key)
{
	wait_queue_t *curr, *next;

	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;
	}
}

__wake_up_common离,调用curr->func。这里的func是在epoll_wait时传入的default_wake_function函数

执行epoll就绪通知

在default_wake_function中找到等待队列项里的进程描述符,然后唤醒它,如下图所示:

// kernel/sched/core.c
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags,
			  void *key)
{
	return try_to_wake_up(curr->private, mode, wake_flags);
}

等待队列项curr->private指针是在对象上等待而被阻塞掉的进程

将epoll_wait进程推入可运行队列,等待内核重新调度进程。当这个进程重新运行后,从epoll_wait阻塞时暂停的代码处继续执行。把rdlist中就绪的事件返回给用户进程

// fs/eventpoll.c
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
		   int maxevents, long timeout)
{
		...
		__remove_wait_queue(&ep->wq, &wait);

		set_current_state(TASK_RUNNING);
	}
check_events:
	eavail = ep_events_available(ep);

	spin_unlock_irqrestore(&ep->lock, flags);

	// 用给用户进程返回就绪事件
	if (!res && eavail &&
	    !(res = ep_send_events(ep, events, maxevents)) && !timed_out)
		goto fetch_events;

	return res;
}

从用户角度来看,epoll_wait只是多等了一会儿而已,但执行流程还是顺序的

5)小结

epoll的整个工作流程总结如下图所示:

其中软中断回调时的回调函数调用关系整理如下:

sock_def_readable: sock对象初始化时设置的
	=> ep_poll_callback: 调用epll_ctl时添加到socket上的
		=> default_wake_function: 调用epoll_wait时设置到epoll上的

总结一下,epoll相关的函数里内核运行环境分两部分:

  • 用户进程内核态。调用epoll_wait等函数时会将进程写入内核态来执行。这部分代码负责查看接收队列,以及负责把当前进程阻塞掉,让出CPU
  • 硬、软中断上下文。在这些组件中,将包从网卡接收过来进行处理,然后放到socket的接收队列。对于epoll来说,再找到socket关联的epitem,并把它添加到epoll对象的就绪链表中。这个时候再捎带检查一下epoll上是否有被阻塞的进程,如果有唤醒它

在实践中,只要活儿足够多,epoll_wait根本不会让进程阻塞。用户进程会一直干活儿,一直干活儿,直到epoll_wait里实在没活儿可干的时候才主动让出CPU。这就是epol高效的核心原因所在

4)、总结

1)阻塞到底是怎么一回事?

阻塞其实说的是进程因为等待某个事件而主动让出CPU挂起的操作。在网络IO中,当进程等待socket上的数据时,如果数据还没有到来,那就把当前进程状态从TASK_RUNNING修改为TASK_INTERRUPTIBLE,然后主动让出CPU。由调度器来调度下一个就绪状态的进程来执行

所以,在分析某个技术方案是不是阻塞的时候,关键要看进程有没有放弃CPU。如果放弃了,那就是阻塞。如果没放弃,那就是非阻塞。事实上,recvfrom也可以设置成非阻塞。在这种情况下,如果socket上没有数据到达,调用直接返回空,而不是挂起等待

2)同步阻塞IO都需要哪些开销?

同步阻塞IO的开销主要有以下这些:

  • 进程通过recv系统调用接收一个socket上的数据时,如果数据没有到达,进程就被从CPU上拿下来,然后再换上另一个进程。这导致一次进程上下文切换的开销
  • 当连接上的数据就绪的时候,睡眠的进程又会被唤醒,又是一次进程切换的开销
  • 一个进程同时只能等待一条连接,如果有很多并发,则需要很多进程。每个进程都将占用大于几MB的内存

3)多路复用epoll为什么就能提高网络性能?

epoll高性能最根本的原因是极大程度地减少了无用的进程上下文切换,让基础呢很难过更专注地处理网络请求

在内核的硬、软中断上下文中,包从网卡接收过来进行处理,然后放到socket的接收队列。再找到socket关联的epitem,并把它添加到epoll对象的就绪链表中

在用户进程中,通过调用epoll_wait来查看就绪链表中是否有事件到达,如果有,直接走进行处理。处理完毕再次调用epoll_wait。在高并发的实践中,主要活儿足够多,epoll_wait根本不会让进程阻塞。用户进程会一直干活儿,一直干活儿,直到epoll_wait里实在没活儿可干的时候才主动让出CPU。这就是epoll高效的核心原因所在

至于红黑树,仅仅是提高了epoll查找、添加、删除socket时的效率而已,不算epoll在高并发场景高性能的根本原因

4)epoll也是阻塞的?

例如,一个epoll对象下添加了一万个客户端连接的socket。假设所有这些socket上都还没有数据到达,这个时候进程调用epoll_wait发现没有任何事情可干。这种情况下用户进程就会被阻塞掉,而这种情况是完全正常的,没有工作需要处理,那还占着CPU是没有道理的

阻塞不会导致低性能,过多过频繁的阻塞才会。epoll的阻塞和它的高性能并不冲突

5)为什么Redis的网络性能很突出?

Redis在网络IO性能上表现非常突出,单进程的服务器在极限情况下可以达到10万的QPS

Redis的事件循环可以简化到用如下伪代码来表示

void aeMain(aeEventLoop *eventLoop) {
    job = epollo_wait(...);
    do_job();
}

Redis的主要业务逻辑就是在本机内存上的数据结构的读写,几乎没有网络IO和磁盘IP,单个请求处理起来很快。所以它把主服务器程序干脆就做成了单进程的,这样省去了多进程之间协作的负担,也很大程序减少了进程切换。进程主要的工作过程就是调用epoll_wait等待事件,有了事件以后处理,处理完之后再调用epoll_wait。一直工作,一直工作,直到实在没有请求需要处理,或者进程时间片到的时候才让出CPU。工作效率发挥到了极致

推荐阅读:

Linux五种I/O模型:带你彻底理解Linux五种I/O模型

红黑树:图解:什么是红黑树?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1065206.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Godot 官方2D游戏笔记(1):导入动画资源和添加节点

前言 Godot 官方给了我们2D游戏和3D游戏的案例,不过如果是独立开发者只用考虑2D游戏就可以了,因为2D游戏纯粹,我们只需要关注游戏的玩法即可。2D游戏的美术素材简单,交互逻辑简单,我们可以把更多的时间放在游戏的玩法…

苍穹外卖

1、基础知识扫盲 项目从0到1 需求分析->设计->编码->测试->上线运维 角色 项目经理:对整个项目负责,任务分配,把控进度 产品经理:进行需求调研,输出需求调研文档,产品原型 UI设计师&…

【java计算机毕设】 留守儿童爱心捐赠管理系统 springboot vue html mysql 送文档ppt

1.项目视频演示 【java计算机毕设】留守儿童爱心捐赠管理系统 springboot vue html mysql 送文档ppt 2.项目功能截图 3.项目简介 后端:springboot,前端:vue,html,数据库:mysql,开发软件idea 留…

Springboot使用Aop保存接口请求日志到mysql

1、添加aop依赖 <!-- aop日志 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency> 2、新建接口保存数据库的实体类RequestLog.java package com.example…

volatile关键字使用总结

先说结论 1. volatile关键字可以让编译器层面减少优化&#xff0c;每次使用时必须从内存中取数据&#xff0c;而不是从cpu缓存或寄存器中获取 2. volatile关键字不能完全禁止指令重排&#xff0c;准确地说是两个volatile修饰的变量之间的命令不会进行指令重排 3. 使用volati…

BLE协议栈1-物理层PHY

从应届生开始做ble开发也差不读四个月的时间了&#xff0c;一直在在做上层的应用&#xff0c;对蓝牙协议栈没有过多的时间去了解&#xff0c;对整体的大方向概念一直是模糊的状态&#xff0c;在开发时也因此遇到了许多问题&#xff0c;趁有空去收集了一下资料来完成了本次专栏&…

毕业设计选题之Android基于移动端的线上订餐app外卖点餐安卓系统源码 调试 开题 lw

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人七年开发经验&#xff0c;擅长Java、Python、PHP、.NET、微信小程序、爬虫、大数据等&#xff0c;大家有这一块的问题可以一起交流&#xff01; &#x1f495;&…

【gcc】RtpTransportControllerSend学习笔记

本文是对大神 webrtc源码分析(8)-拥塞控制(上)-码率预估 的学习笔记。看了啥也没记住,所以跟着看代码先。CongestionControlHandler 在底层网络可用的时候,会触发RtpTransportControllerSend::OnNetworkAvailability()回调,这里会尝试创建CongestionControlHandler创建后即刻…

在VS Code中优雅地编辑csv文件

文章目录 Rainbow csv转表格CSV to Tablecsv2tableCSV to Markdown Table Edit csv 下面这些插件对csv/tsv/psv都有着不错的支持&#xff0c;这几种格式的主要区别是分隔符不同。 功能入口/使用方法Rainbow csv按列赋色右键菜单CSV to Table转为ASCII表格指令CSV to Markdown …

混合网状防火墙的兴起如何彻底改变网络安全

数字环境在不断发展&#xff0c;随之而来的是日益复杂的网络威胁。 从复杂、持续的攻击到对非传统设备的秘密尝试&#xff0c;网络犯罪分子不断完善他们的策略。 除了这些日益严峻的挑战之外&#xff0c;各组织还在努力应对物联网 (IoT)&#xff0c;尽管大量联网设备收集和传…

Leetcode 1492.n的第k个因子

给你两个正整数 n 和 k 。 如果正整数 i 满足 n % i 0 &#xff0c;那么我们就说正整数 i 是整数 n 的因子。 考虑整数 n 的所有因子&#xff0c;将它们 升序排列 。请你返回第 k 个因子。如果 n 的因子数少于 k &#xff0c;请你返回 -1 。 示例 1&#xff1a; 输入&#…

使用华为eNSP组网试验⑸-访问控制

今天练习使用华为sNSP模拟网络设备上的访问控制&#xff0c;这样的操作我经常在华为的S7706、S5720、S5735或者H3C的S5500、S5130、S7706上进行&#xff0c;在网络设备上根据情况应用访问控制的策略是一个网管必须熟练的操作&#xff0c;只是在真机上操作一般比较谨慎&#xff…

FFmpeg 基础模块:AVIO、AVDictionary 与 AVOption

目录 AVIO AVDictionary 与 AVOption 小结 思考 我们了解了 AVFormat 中的 API 接口的功能&#xff0c;从实际操作经验看&#xff0c;这些接口是可以满足大多数音视频的 mux 与 demux&#xff0c;或者说 remux 场景的。但是除此之外&#xff0c;在日常使用 API 开发应用的时…

cpp primer笔记090-动态内存

shared_ptr和unique_ptr都支持的操作&#xff0c;加上shared_ptr独有的操作 每个shared_ptr都有一个关联的计数器&#xff0c;通常称其为引用计数&#xff0c;当调用了shared_ptr的构造函数时就会递增&#xff0c;当调用析构函数时就会递减&#xff0c;一旦一个shared_ptr的计…

【2023年11月第四版教材】第19章《配置与变更管理》(合集篇)

第19章《配置与变更管理》&#xff08;合集篇&#xff09; 1 章节内容2 配置管理3 变更管理4 项目文档管理 1 章节内容 【本章分值预测】本章内容90%和第三版教材内容一样的&#xff0c;少部分有一些变化&#xff0c;特别是变更涉及的人员及职责&#xff0c;预计选择题考3分&a…

Python如何实现数据驱动的接口自动化测试

大家在接口测试的过程中&#xff0c;很多时候会用到对CSV的读取操作&#xff0c;本文主要说明Python3对CSV的写入和读取。下面话不多说了&#xff0c;来一起看看详细的介绍吧。 1、需求 某API&#xff0c;GET方法&#xff0c;token,mobile,email三个参数 token为必填项mobil…

Connect to 127.0.0.1:1080 [/127.0.0.1] failed: Connection refused: connect

报错信息 A problem occurred configuring root project CourseSelection. > Could not resolve all artifacts for configuration :classpath.> Could not resolve com.android.tools.build:gradle:3.6.1.Required by:project :> Could not resolve com.android.tool…

力扣第101题 c++ 递归 迭代 双方法 +注释 ~

题目 101. 对称二叉树 简单 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false提示&a…

点读笔背后的神秘力量,究竟是如何实现即时识别的?

点读笔是一种智能学习工具&#xff0c;通过与印刷物或电子设备配合使用&#xff0c;将文字、图片或声音转化为可听、可读、可学习的内容。它的核心功能是识别并解析特定标识&#xff08;如二维码、条形码&#xff09;或区域内的信息&#xff0c;并提供相应的语音、文字或图像反…

docker swarm安装指导

SWARM部署DOCKER集群 1. 简介............................................................................................................................ 3 2. 部署准备.........................................................................................…