准备的基础知识 (五)

操作系统中的内存结构

在这里插入图片描述

【在内存结构中,自底向上依次是 : 代码区 已初始化数据区data段 未初始化数据区bss段 堆区 栈区

一个程序本质上是由未初始化数据区bss段、已初始化区数据data段、代码区text段组成的。
一个可执行程序在存储(没有调入内存)时分为代码段、已初始化数据区和未初始化数据区三部分

bss 段 (未初始化数据区)通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域
data 段(已初始化数据区)存放程序中已初始化的全局变量的一块内存区域
text 代码段(代码区)存放程序执行代码的一块内存区域

【可执行程序在运行时多出两个区域:堆区和栈区

栈区由编译器自动分配和释放,存放函数的一些参数值、返回值和局部变量等
每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用
栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。

堆区由程序员手动分配和释放,用于动态分配内存,位于bss和栈中间的地址区域,主要存放一些全局变量
堆是从低地址位向高地址位增长,采用链式存储结构。频繁的malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多


操作系统中的缺页中断

malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。 通过页表机制为虚拟地址匹配到合适的物理内存。
缺页中断在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。

缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
1、保护CPU现场
2、分析中断原因
3、转入缺页中断处理程序进行处理
4、恢复CPU现场,继续执行


深入浅出—静态链接和动态链接

源文件--预处理--编译--汇编--链接目标文件--生成可执行文件

预处理, 展开头文件/宏替换/去掉注释/条件编译 生成预处理文件 (test.i main .i)
编译, 词法分析/语法分析 生成汇编文件 ( test.s main .s)
汇编, 汇编代码转换机器码 生成二进制的目标文件 (test.o main.o)
链接 , 将目标文件链接到一起生成可执行程序 a.out


详细步骤:
编译分为3步
首先对源文件进行预处理,这个过程主要是处理一些#号定义的命令或语句(如宏、#include、预编译指令#ifdef等),生成*.i文件;
然后进行编译,这个过程主要是进行词法分析、语法分析和语义分析等,生成*.s的汇编文件;
最后进行汇编,这个过程比较简单,就是将对应的汇编指令翻译成机器指令,生成可重定位的二进制目标文件。以上就是编译的过程

【链接 - 静态链接 - 动态链接】
动态链接和静态链接主要解决的是在生成可执行程序前,将各个目标文件依次链接的顺序的问题;
静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的执行则是在程序执行时
通俗点讲,静态链接会在形成可执行程序前,将所有可能用到的目标文件全部链接一遍,然后等着程序调用即可(空间浪费、更新困难,但是速度快)
动态链接是在生成可执行程序的过程中,用到哪个目标文件就将哪个文件链接,依次加载需要的文件,对于那些不需要的文件就不会加载(节约内存,但是动态查找比较耗时)


静态库和动态库

1、静态链接
1)为什么进行静态链接
在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个*.c文件会形成一个*.o文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接
2)静态链接的实现原理
3)静态链接的优缺点

缺点
一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;
另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序
优点:在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

2、动态链接
动态链接出现的原因就是为了解决静态链接中提到的两个问题,一方面是空间浪费,另外一方面是更新困难。
1)实现机制
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
2)优缺点
缺点:动态查找的过程速度比较慢,会消耗较多的时间;
优点:节约内存,不会存在不必要的副本;更新方便,更换时,只需替换原来的目标文件,而无需将所有的程序重新链接一遍;


计算机 内核态和用户态

内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态。因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;
虚拟内存被操作系统划分成两块:内核空间和用户空间,内核空间是内核代码运行的地方,用户空间是用户程序代码运行的地方。当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态,为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响;

线程上下文切换就会涉及到用户态到内核态的切换通过一些调用API进行系统调用就可以实现从用户态到内核态的切换;

内核态:运行操作系统程序、操作硬件;
用户态:运行用户程序;

用户模式&&内核模式

为了使操作系用内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。
  当设置了模式位时,进程就运行在内核模式中,即超级用户模式。
  一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中任何存储器位置。
  没有设置模式位时,进程就运行在用户模式中用户模式中的进程不允许执行特权指令,比如停止处理器,改变模式位,或者发起一个I/O操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
  运行应用程序代码的进程初始时是在用户模式中的。
  进程从用户模式变为内核模式的唯一方法是通过诸如中断,故障或者陷入系统调用这样的异常,当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式
  处理程序运行在内核模式中,当他返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。


进程互斥和同步

进程管理的方式
创建进程(调用fork()函数) 获取进程信息 设置进程属性 执行新代码 退出进程 和跟踪进程

进程同步机制:对多个相关进程在执行次序上进行协调,使并发执行的各进程按照一定规则共享系统资源
(生产者-消费者问题是典型的进程同步问题)

进程互斥机制:指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。一个时间段内只允许一个进程访问资源。
(临界区:进程中正在访问临界资源代码段)

进程互斥、同步的概念是并发进程下存在的概念,有了并发进程,就产生了资源的竞争与协作,从而就要通过进程的互斥、同步、通信来解决资源的竞争与协作问题。
在多道程序设计系统中,同一时刻可能有许多进程,这些进程之间存在两种基本关系:竞争关系和协作关系。
进程的互斥、同步、通信都是基于这两种基本关系而存在的。

为了解决进程间竞争关系(间接制约关系)而引入进程互斥;
为了解决进程间松散的协作关系( 直接制约关系)而引入进程同步;
为了解决进程间紧密的协作关系而引入进程通信。

科普

  1. 临界资源临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。
  2. 临界区每个进程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程进入。临界区是一种轻量级的同步机制,与互斥和事件这些内核同步对象相比,临界区是用户态下的对象,即只能在同一进程中实现线程互斥。因无需在用户态和核心态之间切换,所以工作效率比较互斥来说要高很多。虽然临界区同步速度很快,但却只能用来同步本 进程内的线程,而不可用来同步多个进程中的线程。

进程同步的实现工具:信号量(锁)机制
新的进程同步工具:管程
(代表共享资源的数据结构以及对该共享数据结构实施操作的一组过程所组成的资源管理程序共同构成了一个操作系统的资源管理模块)
(一个管程定义了一个数据结构和能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据)

Linux下常见的进程同步的方法:

1、信号量 2、管程 3、互斥锁(基于共享内存的快速用户态 ) 4、文件锁(通过 fcntl 设定,针对文件)

互斥量(mutex)包含的几个操作原语
CreateMutex() 创建一个互斥量
OpenMutex() 打开一个互斥量
ReleaseMutex() 释放互斥量
WaitForMultipleObjects() 等待互斥量对象

信号量操作
P操作 申请资源 ;V操作 释放资源
两进程协作完成一件事,我们叫同步。
设有a,b两进程共用一个变量x,a负责算数得到x的结果,然后把结果传给b,b负责把结果x打印出来。这里我们需要两个信号量S1,S2,初值为1,0。(S1给a用,S2给b用)
要完成这次任务
执行P(S1)→得到结果→V(S2)→P(S2)→打印结果→V(S1)

P(S1)表示a进程要工作了,把它对应的信号量S1置0,表示a进程不可用正在工作中。
得到结果后,V(S2)把S2置1,表示b可以开始工作。
P(S2)表示b进程要工作了,把它对应的信号量S2置0。
打印出结果后,V(S1)讲S1置1,表示任务完成,a进程可以开始下一轮的工作了。

条件变量
在C++11中,我们可以使用条件变量(condition_variable)实现多个线程间的同步操作;当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作

一个线程因等待"条件变量的条件成立"而挂起;
另外一个线程使"条件成立",给出信号,从而唤醒被等待的线程。


线程同步机制:

互斥锁 pthread_mutex ;读写锁 pthread_rwlock;条件变量 pthread_cond;信号量;自旋锁

1、互斥锁mutex:
通过锁机制实现线程间的同步
1)初始化锁。在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对它进行初始化。
静态分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态分配:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr);
2)加锁。对共享资源的访问,要对互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞,直到互斥量被解锁。

int pthread_mutex_lock(pthread_mutex *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);

3)解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

4)销毁锁。锁在是使用完成后,需要进行销毁以释放资源。

int pthread_mutex_destroy(pthread_mutex *mutex);
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
#include "iostream"
using namespace std;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int tmp;
void* thread(void *arg)
{
	cout << "thread id is " << pthread_self() << endl;
	pthread_mutex_lock(&mutex);
	tmp = 12;
	cout << "Now a is " << tmp << endl;
	pthread_mutex_unlock(&mutex);
	return NULL;
}
int main()
{
	pthread_t id;
	cout << "main thread id is " << pthread_self() << endl;
	tmp = 3;
	cout << "In main func tmp = " << tmp << endl;
	if (!pthread_create(&id, NULL, thread, NULL))
	{
		cout << "Create thread success!" << endl;
	}
	else
	{
		cout << "Create thread failed!" << endl;
	}
	pthread_join(id, NULL);
	pthread_mutex_destroy(&mutex);
	return 0;
}
//编译:g++ -o thread testthread.cpp -lpthread

2、条件变量
与互斥锁不同,条件变量是用来等待而不是用来上锁的。
条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。条件变量分为两部分: 条件和变量。
条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。条件变量使我们可以睡眠等待某种条件出现。
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。 条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

1、初始化条件变量。
静态初始化,pthread_cond_t cond = PTHREAD_COND_INITIALIER;
动态初始化,int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
2、等待条件成立。
释放锁,同时阻塞等待条件变量为真才行。timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);

3、激活条件变量 pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有线程的阻塞

4清除条件变量无线程等待,否则返回EBUSY

int pthread_cond_destroy(pthread_cond_t *cond);

3 信号量
如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。信号量函数的名字都以"sem_"打头。线程使用的基本信号量函数有四个。

1)信号量初始化。 int sem_init (sem_t *sem , int pshared, unsigned int value);
这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux 只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE。
2)等待信号量。给信号量减1,然后等待直到信号量的值大于0。 int sem_wait(sem_t *sem);
3)释放信号量信号量值加1。并通知其他等待线程。 int sem_post(sem_t *sem);
4)销毁信号量我们用完信号量后都它进行清理。归还占有的一切资源。 int sem_destroy(sem_t *sem);


IO多路复用模型

selcet poll epoll都是IO多路复用的机制;
所谓IO多路复用,就是通过一种机制可以监听多个文件描述符fd,一旦某个描述符就绪(读就绪或写就绪),能够通知程序进行相应的读写程序
但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。


同步异步、阻塞与非阻塞

在这里插入图片描述在这里插入图片描述
同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的
阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞
阻塞是使用同步机制的结果,非阻塞则是使用异步机制的结果

1、同步机制
发送方发送请求之后,需要等接收方接收响应后才接着发
2、异步机制
发送方发送一个请求之后不等待接收方响应这个请求,就继续发送下个请求
3、阻塞调用
调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回,该线程在此过程中不能进行其他处理;
可以这样理解:一个线程想要获取某个请求,在得到这个请求之前,即使没有响应它也会一直等待获得请求,然后再去执行别的操作
4、非阻塞调用
调用结果不能马上返回,当前线程也不会被挂起,而是立即返回执行下一个调用。(网络通信中主要指的是网络套接字Socket的阻塞和非阻塞方式,而soket 的实质也就是IO操作)
可以这样理解:一个线程想要获得某种请求,但是暂时没有得到响应,那么这个线程会先去执行别的操作,过段时间再来看看是否可以得到这个请求
5、同步阻塞方式
发送方发送请求之后一直等待响应。接收方处理请求时进行的IO操作如果不能马上等到返回结果,就一直等到返回结果后,才响应发送方,期间不能进行其他工作
6、同步非阻塞方式
发送方发送请求之后,一直等待响应,接受方处理请求时进行的IO操作如果不能马上的得到结果,就立即返回,取做其他事情。但是由于没有得到请求处理结果,不响应发送方,发送方一直等待。一直等到IO操作完成后,接收方获得结果响应发送发后,接收方才进入下一次请求过程。(实际不应用)
7、异步阻塞方式
发送方向接收方请求后,不等待响应,可以继续其他工作,接收方处理请求时进行IO操作如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他操作。 (实际不应用)
异步非阻塞方式发送方向接收方请求后,不等待响应,可以继续其他工作,接收方处理请求时进行IO操作如果不能马上得到结果,也不等待,而是马上返回取做其他事情。当IO操作完成以后,将完成状态和结果通知接收方,接收方在响应发送方。(效率最高)


epoll详解

辉哥给的WebServer-master项目中用到epoll机制

参考资料

什么是epoll

epoll是什么?
按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法


epoll相关的系统调用

epollepoll-create、epoll_ctl、epoll_wait三个系统调用

1、 int epoll_create(int size);
作用是创建一个epoll的句柄。
自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

2、int epoll_ctl(int epfd,int op,int fd,struct epoll_event*event);
epoll的事件注册函数
它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型;

第一个参数是epoll_create()的返回值;
第二个参数表示动作,用三个宏表示;
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是要监听的fd
第四个参数是告诉内核需要监听什么事件,struct epoll_event的结构如下:

//保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;
 //感兴趣的事件和被触发的事件
struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

3、int epoll_wait(int epfd,struct epoll_event*events,int maxevents,int timeout)
作用是收集epoll监控的事件中已经发送的事件;
参数events是分配好的epoll_event结构体数组,epoll会把发生的事件赋值到events数组中;
maxevents告诉内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size
参数timeout是超时时间;


epoll工作原理

epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。


Epoll的两种工作方式-水平触发LT和边缘触发ET ;

Level Triggered 工作模式

相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

LT(level triggered)是epoll缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET (edge-triggered)是高速工作方式,只支持no-block socket,它效率要比LT更高
ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。

因此,LT模式下开发基于epoll的应用要简单些,不太容易出错。而在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。

图示说明:
在这里插入图片描述
Nginx默认采用ET模式来使用epoll


epoll的优点

【总结–重点:】
因为不论是select/poll还是epoll都需要将socket句柄从内核传递给用户
select/poll每次调用的时候都会将所有的socket描述符同从内核空间拷贝到用户空间,这样在大量的fd的情况下是非常低效的。
而epoll的调用不是对socket进行复制,而是调用epoll_wait()函数,它传递给用户的不是具体的文件描述符,而是一个代表文件描述符数量的值,然后再根据这个值去查找对应的文件就好了。这样提高了IO效率。


1、支持一个进程打开大数目的socket描述符(fd)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

2、IO效率不随fd数目的增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降
但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

3、使用mmap加速内核与用户空间的消息传递
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的

4、内核微调
这一点其实不算epoll的优点了,而是整个linux平台的优点。
也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小— 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。


linux下如何实现高效处理百万句柄

大家都明白epoll是一种IO多路复用技术,可以非常高效的处理数以百万计的socket句柄,比起以前的select和poll效率高大发了。我们用起epoll来都感觉挺爽,确实快,那么,它到底为什么可以高速处理这么多并发连接呢?
**首先调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄,多个这个最大数时内核不保证效果;
**epoll_ctl可以操作上面建立的epoll. 例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。

**epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程;

epoll比select和poll的优越之处
因为后者每次调用时都要传递你所要监控的所有socket给select/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。而我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。
所以,实际上在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。

当一个进程调用epoll_creaqte方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关:


epoll的使用方法

通过在包含一个头文件#include <sys/epoll.h> 以及几个简单的API将可以大大的提高你的网络服务器的支持人数。
首先通过create_epoll(int maxfds)来创建一个epoll的句柄。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。

之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为: nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。
max_events是当前需要监听的所有socket句柄数。 最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件返回,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则返回。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。
epoll_wait返回之后应该是一个循环,遍历所有的事件;

/*
 171 * This structure is stored inside the "private_data" member of the file
 172 * structure and represents the main data structure for the eventpoll
 173 * interface.
 174 */
 
 175struct eventpoll {
 
 176        /* Protect the access to this structure */
 
 177        spinlock_t lock;
 
 178
 
 179        /*
 180         * This mutex is used to ensure that files are not removed
 181         * while epoll is using them. This is held during the event
 182         * collection loop, the file cleanup path, the epoll file exit
 183         * code and the ctl operations.
 184         */
 
 185        struct mutex mtx;
 
 186
 
 187        /* Wait queue used by sys_epoll_wait() */
 
 188        wait_queue_head_t wq;
 190        /* Wait queue used by file->poll() */
 
 191        wait_queue_head_t poll_wait;
 
 192
 
 193        /* List of ready file descriptors */
 
 194        struct list_head rdllist;
 
 195
 
 196        /* RB tree root used to store monitored fd structs */
 
 197        struct rb_root rbr;//红黑树根节点,这棵树存储着所有添加到epoll中的事件,也就是这个epoll监控的事件
 198
 199        /*
 200         * This is a single linked list that chains all the "struct epitem" that
 201         * happened while transferring ready events to userspace w/out
 202         * holding ->lock.
 203         */
 204        struct epitem *ovflist;
 205
 206        /* wakeup_source used when ep_scan_ready_list is running */
 207        struct wakeup_source *ws;
 208
 209        /* The user that created the eventpoll descriptor */
 210        struct user_struct *user;
 211
 212        struct file *file;
 213
 214        /* used to optimize loop detection check */
 215        int visited;
 216        struct list_head visited_list_link;//双向链表中保存着将要通过epoll_wait返回给用户的、满足条件的事件
 217};

每一个epoll对象都有一个独立的eventpoll结构体,这个结构体会在内核空间中创造独立的内存,用于存储使用epoll_ctl方法向epoll对象中添加进来的事件。这样,重复的事件就可以通过红黑树而高效的识别出来。

在epoll中,对于每一个事件都会建立一个epitem结构体:

/*
 130 * Each file descriptor added to the eventpoll interface will
 131 * have an entry of this type linked to the "rbr" RB tree.
 132 * Avoid increasing the size of this struct, there can be many thousands
 133 * of these on a server and we do not want this to take another cache line.
 134 */
 135struct epitem {
 136        /* RB tree node used to link this structure to the eventpoll RB tree */
 137        struct rb_node rbn;
 138
 139        /* List header used to link this structure to the eventpoll ready list */
 140        struct list_head rdllink;
 141
 142        /*
 143         * Works together "struct eventpoll"->ovflist in keeping the
 144         * single linked chain of items.
 145         */
 146        struct epitem *next;
 147
 148        /* The file descriptor information this item refers to */
 149        struct epoll_filefd ffd;
 150
 151        /* Number of active wait queue attached to poll operations */
 152        int nwait;
 153
 154        /* List containing poll wait queues */
 155        struct list_head pwqlist;
 156
 157        /* The "container" of this item */
 158        struct eventpoll *ep;
 159
 160        /* List header used to link this item to the "struct file" items list */
 161        struct list_head fllink;
 162
 163        /* wakeup_source used when EPOLLWAKEUP is set */
 164        struct wakeup_source __rcu *ws;
 165
 166        /* The structure that describe the interested events and the source fd */
 167        struct epoll_event event;
 168      };

此外,epoll还维护了一个双链表,用户存储发生的事件。当epoll_wait调用时,仅仅观察这个list链表里有没有数据即eptime项即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!

那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。

如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

几乎所有的epoll程序都使用下面的框架:

 for( ; ; )
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd) //有新的连接
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
                ev.data.fd=connfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
            }

            else if( events[i].events&EPOLLIN ) //接收到数据,读socket
            {
                n = read(sockfd, line, MAXLINE)) < 0    //读
                ev.data.ptr = md;     //md为自定义类型,添加数据
                ev.events=EPOLLOUT|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
            }
            else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
            {
                struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取数据
                sockfd = md->fd;
                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //发送数据
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
            }
            else
            {
                //其他的处理
            }
        }
    }

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.liang呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值