缘由
昨夜再把深入理解nginx中epoll的部分读了一遍,这次主要关注epoll的原理。这篇博客也是用自己的话转述了书中的内容。预想场景
假设我们的服务器端的进程同时与100万个用户保持着TCP的链接,但是这100万个用户中并不是所有的用户都在发送请求。那么也就是说只有几十个用户是发送来了请求的。所以,实际上我们的进程只是处理这100万个链接中的几十个。为了接受这些响应,我们首先要做的就是从这100万个链接中找出这一时刻是哪几十个链接是发送来了数据。select与poll
原理
在linux版本内核2.4之前就是使用select与poll来完成这个过程的。思路很简单,那就是做一次遍历,对这100万个链接一个一个的询问。并且这个过程还所由操作系统来完成的,我们处理TCP链接的进程只能首先把这100万个socket的fd告诉操作系统,让操作系统知道这100万个是我们这次要监控的链接。然后再让操作系统去遍历。这样就有两个非常费时费力的过程:- 将这个100万个套接字的fd从用户态内存复制到内核态内存
- 遍历这100万个套接字,找出其中发生了满足要求的事件。对此就是遍历,没有什么多的说的了。
用户态与内核态
(摘自百度百科)
首先我们要明白用户态是什么,实际上用户态又分为CPU设计中的用户态,又可以指操作系统设计中的用户态。
对于操作系统中的用户态,指权限等级中的一种级别,与之相对的是管理员或者超级用户(类Unix系统中,名为“root”或“superuser”等)的特权级别。用户态启动的每个进程,根据运行该进程的用户,都被系统赋予特定的权限,举例而言,默认下的Unix系统中,运行在用户态的代码,不准通过侦听1024以下的端口号,以伪装成常见的服务,而超级用户运行的代码则有权这样做。
CPU设计中的用户态也所指一种状态,主要是指非特权状态。在此状态下,执行的代码被硬件限定,不能进行某些操作,比如写入其他进程的存储空间,以防止给操作系统带来安全隐患。
x86结构拥有四种级别,级别最高的是ring 0,也就是核心态。级别最低的是ring 3,也就是用户态。ring 1和ring 2设计成供驱动程式使用,但一般很少使用。
用户态进入内核态
如下图所示一个一般用户启动一个程序(一个进程),运行一些代码,如果只是实现用程序或者API函数,那么只是在用户态运行。当其执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0 级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。此时还需要注意,处理器总处于以下状态中的一种:
- 内核态,运行于进程上下文,内核代表进程运行于内核空间;
- 内核态,运行于中断上下文,内核代表硬件运行于内核空间;
- 用户态,运行于用户空间。
这就是书中所谓的将这个100万个套接字的fd从用户态内存复制到内核态内存的过程。
顺便说一下中断上下文:
硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。
简单的要点:
- 用系统调用时进入核心态。Linux 对硬件的操作只能在核心态,这可以通过写驱动程序来控制。在用户态操作硬件会造成core dump。
- 要注意区分系统调用和一般的函数。系统调用由内核提供,如read()、write()、open()等。而一般的函数由软件包中的函数库提供,如sin()、cos()等。在语法上两者没有区别。
- 一般情况:系统调用运行在核心态,函数运行在用户态。但也有一些函数在内部使用了系统调用(如fopen),这样的函数在调用系统调用是进入核心态,其他时候运行在用户态。
epoll的原理
epoll将原来poll的一个系统调用分为了三个部分:- epoll_create创建一个epoll对象
- epoll_ctl添加、删除需要监控的事件
- epoll_wait直接返回返回已经准备好了的事件。
epoll对象
它是一个eventpoll结构体,与epoll有关的部分主要是:struct eventpoll{
...
/*红黑树的根节点,书中有所有需要监控的事件,可被添加或者删除*/
struct rb_root rbr;
/*双向链表,保存着已经发生了的事件,一旦调用epoll_wait就会返回该双向链表的元素*/
struct list_head rdlist;
...
}
该结构体会在内核空间中创造独立的内存,用于储存epoll_ctl向该epoll对象中添加的进来的事件。
添加以及返回事件
添加进来的事件都会被放在红黑树的某个节点内,所以,重复添加是没有用的。注意由于是红黑树,所以添加、删除、查找的速度也非常快。当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback, 这个回调函数其实就所把这个事件添加到rdlist这个双向链表中。所以一旦发生了事件,最终的结果就是将该事件添加到双向链表中。那么当调用epoll_wait所做的事只是检查rdlist双向链表中是否有被添加的事件而已,所以速度非常快。这里也需要将发生了的事件复制到用户态内存中即可。