openEuler epoll模型

  1. 背景:以往存在的模型存在的问题

    • PPC和TPC模型
      • PPC(Process Per Connection)模型和TPC(Thread Pe人Connection)模型:为每个链接分配一个独立的进程或者线程进行服务。
      • 缺点:不仅需要耗费大量的时间和空间资源,而且因为管理连接较多时,切换开销大,所以也不可能接受大量的连接。
    • select 模型
      • 最大并发数限制:一个进程可以打开的文件描述符fd是有限的,由FD_SETSIZE限制,默认值是1024/2048。
      • 效率:每次select都会线性扫描全部的fd集合,O(n)的时间复杂度还是很高的。
      • 内存拷贝机制:select在将fd消息传送给用户空间时,使用的是内存拷贝机制。我们都知道,内存拷贝的开销很高,有时候也可以采用写时复制一类的技术。
  2. 什么是epoll

    • epoll和select都采用IO多路复用(IO multiplexing)技术

    • epoll是为处理大批量句柄而进行改进改进的poll

  3. epoll模型如何解决其他模型存在的问题

    • 支持一个进程打开大量文件描述符

      • 传统方案有两种方式可以解决这个问题:

        1. 修改决定可开启fd数量的FD_SETSIZE后重新编译内核,

          缺点:fd数量的提升会带来网络性能的下降。

        2. 使用多进程的方案,缺点:进程的创建存在开销,同时,进程之间数据同步的代价较高。

      • epoll如何实现

        1. epoll摆脱了文件描述符的限制。它支持一个进程开启整个系统可以开启的最大的文件描述符的数目的文件描述符fd。
    • IO效率不会因为文件描述符的增加而线性下降:

      • epoll只对活跃的socket进行操作。主要是因为epoll是根据每个fd上的callback函数实现的。这就决定了只有活跃的socket会调用其fd上的callback函数。
    • 使用mmap加速内核与用户空间的信息传递

      • 将用户地址空间映射到内核地址空间中要发送消息的部分。
  4. epoll中重要的数据结构

    1. eventpoll

      [/fs/eventpoll.c]
      //epoll的核心实现对应于一个epoll描述符  
      struct eventpoll {
        //保证文件被epoll使用时不被移动
      	struct mutex mtx;
      
      	/* Wait queue used by sys_epoll_wait() */
      	wait_queue_head_t wq;
      
      	/* Wait queue used by file->poll() */
      	wait_queue_head_t poll_wait;
      
      	/* List of ready file descriptors */
      	struct list_head rdllist;
      
      	/* RB tree root used to store monitored fd structs */
      	struct rb_root_cached rbr;
      
        //链接struct epitem的链表
      	struct epitem *ovflist;
      
      	/* wakeup_source used when ep_scan_ready_list is running */
      	struct wakeup_source *ws;
      
      	/* The user that created the eventpoll descriptor */
      	struct user_struct *user;
      
      	struct file *file;
      
      	/* used to optimize loop detection check */
      	int visited;
      	struct list_head visited_list_link;
      
      #ifdef CONFIG_NET_RX_BUSY_POLL
      	/* used to track busy poll napi_id */
      	unsigned int napi_id;
      #endif
      };
      
    2. epitem

      [/fs/eventpoll.c]
      // 对应于一个加入到epoll的文件  
      struct epitem {
      	union {
      		// 挂载到eventpoll 的红黑树节点  
      		struct rb_node rbn;
      		//用于释放epitem
      		struct rcu_head rcu;
      	};
      	// 挂载到eventpoll.rdllist 的节点  
      	struct list_head rdllink;
      	// 连接到ovflist 的指针  
      	struct epitem *next;
      	//该item涉及到的文件描述符信息
      	struct epoll_filefd ffd;
      	//与poll operation有关的active的waitqueue的数量
      	int nwait;
      	//包含poll wait queues的列表
      	struct list_head pwqlist;
      	// 当前epitem 的所有者  
      	struct eventpoll *ep;
      	//用于将该item连接到struct file 列表的列表头
      	struct list_head fllink;
      	//当EPOLLWAKEUP被设置时 wakeup_source将会被使用
      	struct wakeup_source __rcu *ws;
      	//描述监视的event和source 的fd
      	struct epoll_event event;
      };
      
  5. epoll如何使用

    epoll API包括:epoll_create, epoll_ctl和epoll_wait三个。

    • epoll_create

      • 该函数用于创建一个epoll描述符。要在使用完后,就调用close释放。
      [/fs/eventpoll.c]
      //判断size是否大于0,如果大于0就调用epoll_create1,否则就调用epoll_create
      SYSCALL_DEFINE1(epoll_create1, int, flags)
      {
      	return do_epoll_create(flags);
      }
      SYSCALL_DEFINE1(epoll_create, int, size)
      {
      	if (size <= 0)
      		return -EINVAL;
      	return do_epoll_create(0);
      }
      

      P.S.:

      1. SYSCALL_DEFINE1是一个宏,用来定义有一个参数的系统调用函数。

      2. 以下两个函数展开后为int epoll_create1(int flags)和int epoll_create(int size)。这两个就是我们常见的入口。

      3. 使用宏的原因在于:系统调用的参数个数,传参方式都有限制。

      • do_epoll_create函数

        [/fs/eventpoll.c]
        //创建一个epoll描述符
        static int do_epoll_create(int flags)
        {
        	int error, fd;
        	struct eventpoll *ep = NULL;	//主描述符
        	struct file *file;
        	...
        	//分配一个struct eventpoll
        	error = ep_alloc(&ep);
        	//创建设置一个eventpoll文件所需要的各个items,如:file structure和free file descriptor
        	fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
        	...
          //创建匿名fd
        	file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
        	...
        }
        
        
    • epoll_ctl

      • 在得到epoll描述符后,可以调用epoll_ctl以注册要监听的事件类型。

        [/fs/eventpoll.c]
        //实现epoll描述符的控制接口,支持对在epoll文件描述符中监听的文件描述符的修改,如插入/删除/修改,由参数op决定,
        //此外,fd是我们要监听的描述符,就是socket的,而event是我们感兴趣的事件
        SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd, struct epoll_event __user *, event)
        {
        	int error;
        	int full_check = 0;
        	struct fd f, tf;
        	struct eventpoll *ep;
        	struct epitem *epi;			//socket描述符 在 epoll 描述符中的映射
        	struct epoll_event epds;
        	struct eventpoll *tep = NULL;
        	...
        	//在红黑树中以tfile和fd为参数来查找文件对应的epitem
        	epi = ep_find(ep, tf.file, fd);
        
        	error = -EINVAL;
          //对每种操作分别进行处理
        	switch (op) {
        	case EPOLL_CTL_ADD:	//处理添加
        		if (!epi) {
        			epds.events |= EPOLLERR | EPOLLHUP;
              //进行添加的函数
        			error = ep_insert(ep, &epds, tf.file, fd, full_check);
        		} else
        			error = -EEXIST;
        		if (full_check)
        			clear_tfile_check_list();
        		break;
        	case EPOLL_CTL_DEL:	//处理删除
        		if (epi)
        			error = ep_remove(ep, epi);
        		else
        			error = -ENOENT;
        		break;
        	case EPOLL_CTL_MOD:	//处理修改
        		if (epi) {
        			if (!(epi->event.events & EPOLLEXCLUSIVE)) 			{
        				epds.events |= EPOLLERR | EPOLLHUP;
        				error = ep_modify(ep, epi, &epds);
        			}
        		} else
        			error = -ENOENT;
        		break;
        	}
        	...
        }
        
      • 在epoll_ctl中处理EPOLL_CTL_ADD事件时,会调用ep_insert函数。

        [/fs/eventpoll.c]
        //向epollfd中添加一个监听fd
        static int ep_insert(struct eventpoll *ep, const struct epoll_event *event,
        		     struct file *tfile, int fd, int full_check)
        {
          struct epitem *epi;  
        	...
        
          //使用queue的callback函数初始化poll table
        	epq.epi = epi;
        	init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
        
          //在文件对应的wait queue head上注册callback函数,并返回当前文件的状态
        	revents = ep_item_poll(epi, &epq.pt, 1);
        
        	//添加当前的epitem到文件的f_ep_links链表
        	spin_lock(&tfile->f_lock);
        	list_add_tail_rcu(&epi->fllink, &tfile->f_ep_links);
        	spin_unlock(&tfile->f_lock);
        
        	//将epi插入红黑树
        	ep_rbtree_insert(ep, epi);
        	...
          //如果文件就绪就把它插入到就绪列表中
        	if (revents && !ep_is_linked(epi)) {
        		list_add_tail(&epi->rdllink, &ep->rdllist);
        		ep_pm_stay_awake(epi);
        
            //提醒等待任务的事件已经就绪
        		if (waitqueue_active(&ep->wq))
              //通知epoll_wait, 调用callback函数唤醒等待进程
        			wake_up_locked(&ep->wq);
        		if (waitqueue_active(&ep->poll_wait))
        			pwake++;
        	}
        
        	spin_unlock_irq(&ep->wq.lock);
        	atomic_long_inc(&ep->user->epoll_watches);
        	//在没有锁的时候再通知epoll进程
        	if (pwake)
        		ep_poll_safewake(&ep->poll_wait);
        	return 0;
        
        	...
        }
        
      • 等待队列中的文件描述符调用的callback函数

        [/fs/eventpoll.c]
        //这是等待队列唤醒进程时的callback函数。在等待队列中的文件描述符希望被唤醒时会调用该函数
        static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
        {
        	int pwake = 0;
        	unsigned long flags;
          //获取epitem
        	struct epitem *epi = ep_item_from_wait(wait);
        	struct eventpoll *ep = epi->ep;
        	spin_lock_irqsave(&ep->wq.lock, flags);
        
        	//在callback函数调用时,如果epoll_wait函数返回了,此时进程可能在获取event
          //此时,内核将发生event的epitem用单独的链表链接,在下次epoll_wait时交付
        	if (ep->ovflist != EP_UNACTIVE_PTR) {
        		if (epi->next == EP_UNACTIVE_PTR) {
        			epi->next = ep->ovflist;
        			ep->ovflist = epi;
        			if (epi->ws) {
        				__pm_stay_awake(ep->ws);
        			}
        		}
        		goto out_unlock;
        	}
        	...
          //唤醒epoll wait list和the ->poll() wait list.
        	if (waitqueue_active(&ep->wq)) {
        		if ((epi->event.events & EPOLLEXCLUSIVE) &&
        					!(pollflags & POLLFREE)) {
        			switch (pollflags & EPOLLINOUT_BITS) {
        			case EPOLLIN:
        				if (epi->event.events & EPOLLIN)
        					ewake = 1;
        				break;
        			case EPOLLOUT:
        				if (epi->event.events & EPOLLOUT)
        					ewake = 1;
        				break;
        			case 0:
        				ewake = 1;
        				break;
        			}
        		}
        		wake_up_locked(&ep->wq);
        	}
          //如果epollfd也在被poll,则唤醒队列中所有成员
        	if (waitqueue_active(&ep->poll_wait))
        		pwake++;
        		...
        }
        
    • epoll_wait

      • 该函数用于获取在epoll监控下发生的那些被关注的事件
      • epoll将会发生的事件都放在events参数中(events不能是空指针,内核只负责赋值,不负责开辟空间)。
      • maxevents告诉内核events数组大小。
      • 如果函数调用成功,会返回IO上准备好的文件描述符数目,返回0表示超时。
      [/fs/eventpoll.c]
      SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
      		int, maxevents, int, timeout)
      {
      	return do_epoll_wait(epfd, events, maxevents, timeout);
      }
      
      
      [/fs/eventpoll.c]
       //为eventpoll描述符实现event wait接口。以下函数是用户空间的epoll_pwait(2)的kernel部分
      SYSCALL_DEFINE6(epoll_pwait, int, epfd, struct epoll_event __user *, events,
      		int, maxevents, int, timeout, const sigset_t __user *, sigmask,
      		size_t, sigsetsize)//该函数主要是调用下面的do_epoll_wait函数
       
      static int do_epoll_wait(int epfd, struct epoll_event __user *events,
      			 int maxevents, int timeout)
      {
      	int error;
      	struct fd f;
      	struct eventpoll *ep;
      	...
        //得到epoll文件描述符
      	f = fdget(epfd);
      	if (!f.file)
      		return -EBADF;
      	//获取eventpoll结构
      	ep = f.file->private_data;
      
      	/* Time to fish for events ... */
        //等待事件
      	error = ep_poll(ep, events, maxevents, timeout);
      	...
      }
      
      • 在等待事件时,将会调用ep_poll函数
      //该函数让执行epoll_wait的进程进入睡眠状态
      static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
      		   int maxevents, long timeout)
      {
      	int res = 0, eavail, timed_out = 0;
      	u64 slack = 0;
        //等待队列
      	wait_queue_entry_t wait;
      	ktime_t expires, *to = NULL;
      	...
      
      	if (!ep_events_available(ep)) {
      		...
            //目前没有足够的event可以返回给调用者,所以在此休眠。同时,将会在由event时,由ep_poll_callback()唤醒
      		init_waitqueue_entry(&wait, current);
      		__add_wait_queue_exclusive(&ep->wq, &wait);
      
      		for (;;) {
            //在ep_poll_back函数发送唤醒时,我们不希望还处于被挂起状态。因此我们在进行check event钱设置进程状态为TASK_INTERRUPTIBLE。即设置为可唤醒状态。
      			set_current_state(TASK_INTERRUPTIBLE);
            //允许进程在等待时间用完时退出。
      			if (fatal_signal_pending(current)) {
      				res = -EINTR;
      				break;
      			}
            //如果此时就绪队列中已经有了成员,或者睡觉时间用尽,则退出,结束睡眠
      			if (ep_events_available(ep) || timed_out)
      				break;
            //产生信号时,也退出睡眠
      			if (signal_pending(current)) {
      				res = -EINTR;
      				break;
      			}
      			//没有发生event,则解锁,进如睡眠
      			spin_unlock_irq(&ep->wq.lock);
      			if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
      				timed_out = 1;
      			//callback函数的调用时机是由被监听的fd具体实现的,例如在socket情况下,在等待队列头中决定,就是ep_insert中哪个。而epoll和当前进程做的就是等待。
      			spin_lock_irq(&ep->wq.lock);
      		}
      
      		__remove_wait_queue(&ep->wq, &wait);
          //运行
      		__set_current_state(TASK_RUNNING);
      	}
      check_events:
      	...
          //尝试向用户空间发送事件
      	if (!res && eavail &&
      	    !(res = ep_send_events(ep, events, maxevents)) && !timed_out)
      		goto fetch_events;
      	...
      }
      

参考文献:

  1. epoll分析过程

  2. openEuler源代码

猜你喜欢

转载自blog.csdn.net/weixin_43414275/article/details/106473450