【微信WXG面试】C++后端开发

简介: 当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一个描述符上面,另外的描述符虽然有数据但是不能读出来,

一、项目提问

1.1 epoll和select的区别

1.问题的引出

当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一个描述符上面,另外的描述符虽然有数据但是不能读出来,

2.解决方案

1.使用多进程或者多线程

但是这种方法会造成程序的复杂,而且对与进程与线程的创建维护也需要很多的开销。(Apache服务器是用的子进程的方式,优点可以隔离用户)

2.轮询

用一个进程,但是使用非阻塞的I/O读取数据,当一个I/O不可读的时候立刻返回,检查下一个是否可读,这种形式的循环为轮询(polling),这种方法比较浪费CPU时间,因为大多数时间是不可读,但是仍花费时间不断反复执行read系统调用。

3.异步I/O(asynchronous I/O)

当一个描述符准备好的时候用一个信号告诉进程,但是由于信号个数有限,多个描述符时不适用。

4.I/O多路转接(I/O multiplexing)(多路复用)

先构造一张有关描述符的列表(epoll中为队列),然后调用一个函数,直到这些描述符中的一个准备好时才返回,返回时告诉进程哪些I/O就绪。select和epoll这两个机制都是多路I/O机制的解决方案,select为POSIX标准中的,而epoll为Linux所特有的。

区别(epoll相对select优点):

(1)select的句柄数目有限,而epoll没有监听文件描述符的最大数量限制;

(2)epoll不会随着文件描述符FD的数目增大而降低效率,因为epoll将维护等待队列和阻塞进程分离开,epoll只会对"活跃"的socket进行操作;select相当于轮询且阻塞式地监视socket。

——epoll实现了一 个"伪"AIO。但是如果绝大部分的I/O都是“活跃的”,每个I/O端口使用率很高的话,epoll效率不一定比select高(可能是要维护队列复杂)。

(3)epoll用红黑树监视兴趣列表(装文件描述符),搜索速度极大提升。

(4)使用mmap加速内核与用户空间的消息传递。

无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。

1.2 epoll中et和lt的区别与实现原理:

epoll有2种工作方式:LT和ET。简单说:

水平触发:不断查询是否有可用的文件描述符,有的话,内核触发事件,如果数据没有处理完,内核接着触发事件(有数据就触发)

边缘触发:只有当I/O状态改变时,才触发事件,每次触发一次性把数据全部处理完,因为下一次处理要等I/O状态再次改变才可以(触发就全部处理完数据)

image.png

LT(level triggered)

缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。

如果你不作任何操作,内核还是会继续通知你 的,所以这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET (edge-triggered)

高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述 符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致 了一个EWOULDBLOCK 错误)。

但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用。

1.3 对套接字编程的理解,协议如何

1)socket通常称为“套接字”,用于描述IP地址和端口,是一个通信链的句柄。应用程序通过套接字向网络发出请求或应答网络请求。

服务器和客户端通过socket进行交互。服务器需要绑定在本机的某个端口号上,客户端需要声明自己连接哪个地址的哪个端口,这样服务器和客户端就能连接了。

2)根据连接启动的方式以及本地套接字要连接的目标,

套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。

(1)服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。

(2)客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

(3)连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

3)socket是对TCP/IP协议的封装和应用。

在TCP/IP协议中,TCP协议通过三次握手建立一个可靠的连接。

第一次握手:客户端尝试连接服务器,向服务器发送syn包(同步序列编号Synchronize Sequence Numbers),syn=j,客户端进入SYN_SEND状态等待服务器确认。

第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态。

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

服务器socket与客户端socket建立连接的部分其实就是“三次握手”。

1.4 阻塞&非阻塞

阻塞:程序在等某操作完成前,自身无法继续完成干别的事

非阻塞:程序在等某操作完成前,自身可以继续完成干别的事

1.5 文件描述符

(1)文件描述符:

文件指针数组的索引;默认0是输入,1是输出,2是错误。

每个进程被创建时,files文件指针数组的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。

栗子:C语言的printf函数是向命令行打印字符,但从进程的角度看,就是向files[1]写入数据,scanf函数就是进程向file[0]这个文件中读取数据。

image.png

(2)一般计算机中,输入流是键盘,输出流是显示器,错误流也是显示器,即上图的进程、内核和键盘和显示器“连了”3根线——硬件由内核管理,进程通过系统调用让内核进程访问硬件资源。

Linux中一切都被抽象为文件,即设备也是文件(可进行读和写)。

(3)栗子:打开一个文件进行读写,通过系统调用,内核把文件打开后该文件被放到files文件指针数组的文件描述符3位置,如下图:

image.png

(4)输入重定向

程序想读取数据时就去files[0]读取,所以只要把files[0]指向一个文件,那程序就能从这个文件中读取数据,此时的数据来源就不是一开始说的键盘了。——如上图

(5)输出重定向

把files[1]指向一个文件,程序的输出就不会写入到显示器,而是写入到该文件中。

1.6 项目的这个断点续传咋实现

(1)场景

文件较小时可以直接将文件转为字节流上传到服务器,但文件较大时还这么做,可能文件上传到一半后中断了,就又得重新开始下载了。

(2)秒传

用户在客户端上传文件到服务器,服务器会先做MD5校验,如果服务器上已经有相同的文件了,服务器就直接给你一个新地址,别人下载的都是服务器的同一个文件。

想不秒传,只要让MD5改变,如一个文本文件,修改几个字后,文件的MD5也会改变,就不会秒传。

秒传的核心逻辑:

a、利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位。

b、当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为false,则说明还没上传完成,此时需要在调用set的方法,保存块号文件记录的路径,其中key为上传文件md5加一个固定前缀,value为块号文件记录路径。

(3)分片上传

定义:将上传的文件分割成多个数据块,上传后再由服务端将所有上传的文件“拼”起来。

场景:大文件上传,网络环境不好,有重传风险。

(4)断点续传

断点续传:在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。

场景:【断点续传】可以看做是【分片上传】的一个衍生,因为可以使用分片上传的场景,都可以使用断点续传。

断点续传的核心逻辑:

在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。

为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。

断点续传的实现过程:

a、方案一,常规步骤

将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;

初始化一个分片上传任务,返回本次分片上传唯一标识;

按照一定的策略(串行或并行)发送各个分片数据块;

发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。

b、方案二:

客户端需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小

服务端创建conf文件用来记录分块位置,conf文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤)

服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。

小结:Linux中一切皆文件

不管是设备、另一个进程、socket 套接字还是真正的文件,全部都可以读写,统一装进一个简单的files数组,进程通过简单的文件描述符访问相应资源,具体细节交于操作系统,有效解耦

参考资料

二、C++基础

1.STL 容器相关实现

2.C++新特性的了解

3.多态和虚函数的实现

4.指针的使用

2.1 成员变量,虚函数表指针的位置是怎么排布?

如果一个类带有虚函数,那么该类实例对象的内存布局如下:

首先是一个虚函数指针,

接下来是该类的成员变量,按照成员在类当中声明的顺序排布,整体对象的大小由于内存对齐会有空白补齐。

其次如果基类没有虚函数但是子类含有虚函数:

此时内存子类对象的内存排布也是先虚函数表指针再各个成员。

如果将子类指针转换成基类指针此时编译器会根据偏移做转换。在visual studio,x64环境下测试,下面的Parent p = Child();是父类对象,由子类来实例化对象。

#include <iostream>
using namespace std;
class Parent{
public:
    int a;
    int b;
};
class Child:public Parent{
public:
    virtual void test(){}
    int c;
};
int main() {
    Child c = Child();
    Parent p = Child();
    cout << sizeof(c) << endl;//24
    cout << sizeof(p) << endl;//8
    Child* cc = new Child();
    Parent* pp = cc;
    cout << cc << endl;//0x7fbe98402a50
    cout << pp << endl;//0x7fbe98402a58
  cout << endl << "子类对象abc成员地址:" << endl;
    cout << &(cc->a) << endl;//0x7fbe98402a58
    cout << &(cc->b) << endl;//0x7fbe98402a5c
    cout << &(cc->c) << endl;//0x7fbe98402a60
  system("pause");
    return 0;
}

结果如下:

24
8
0000013AC9BA4A40
0000013AC9BA4A48
子类对象abc成员地址:
0000013AC9BA4A48
0000013AC9BA4A4C
0000013AC9BA4A50
请按任意键继续. . .

(1)第一行24为子类对象的大小,首先是虚函数表指针8B,然后是2个继承父类的int型数值,还有1个是该子类本身的int型数值,最后的4是填充的。

(2)第二行的8为父类对象的大小,该父类对象由子类初始化,含有2个int型成员变量。

(3)子类指针cc指向又new出来的子类对象(第三个),然后父类指针pp指向这个子类对象,这两个指针的值:

父类指针pp值:0000013AC9BA4A48

子类指针cc值:0000013AC9BA4A40

即发现如之前所说的:如果将子类指针转换成基类指针此时编译器会根据偏移做转换。我测试环境是64位,所以指针为8个字节。转换之后pp和cc相差一个虚表指针的偏移。

(4)&(cc->a)的值即 0000013AC9BA4A48,和pp值是一样的,注意前面的 0000013AC9BA4A40到0000013AC9BA4A47其实就是子类对象的虚函数表指针了。

2.2 必须使用初始化列表的时候

除了性能问题之外,有些时候合初始化列表是不可或缺的,以下几种情况时必须使用初始化列表

const常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面

引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面

没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化

class Test1{
public:
  Test1(int a):i(a){}
  int i;
};
class Test2{
public:
  Test1 test1 ;
  Test2(Test1 &t1)
  {test1 = t1 ;}
};

以上代码无法通过编译,因为Test2的构造函数中test1 = t1这一行实际上分成两步执行:

调用 Test1 的默认构造函数来初始化 test1

由于Test1没有默认的构造函数,所以1 无法执行,故而编译错误。

正确的代码如下,使用初始化列表代替赋值操作:

class Test2{
public:
  Test1 test1 ;
  Test2(int x):test1(x){}
}

2.3 智能指针

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

auto_ptr智能指针:当对象拷贝或者赋值后,前面的对象就悬空了。

unique_ptr智能指针:防止智能指针拷贝和复制。

shared_ptr智能指针:通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

weak_ptr智能指针:可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用记数的增加或减少。

c++11出来之前,只有1种智能指针,就是auto_ptr,c++11出来之后补充了3个智能指针,分别是unique_ptr shared_ptr weak_ptr。

强调一点,不是每一种智能指针都可以增加内存的引用计数。智能指针分为两类,一种是可以使用多个智能指针管理同一块内存区域,每增加一个智能指针,就会增加1次引用计数,另一类是不能使用多个智能指针管理同一块内存区域,通俗来说,当智能指针2来管理这一块内存时,原先管理这一块内存的智能指针1只能释放对这一块指针的所有权。按照这个分类标准,auto_ptr unique_ptr weak_ptr属于后者,shared_ptr属于前者。

对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。

更多参考:https://blog.csdn.net/weixin_44826356/article/details/107540457

三、408基础

3.1 进程的死锁

下列关于死锁的论述中哪个是不正确的____(北京邮电大学2011)答案:B

A. 可以采用一次性请求所有资源来预防死锁

B. 可以采用强占其它进程已占资源的方法来预防死锁

C. 可以通过提高进程优先级的方法解除死锁

D. 可以采用资源分配拒绝策略(如银行家算法)避免死锁

解析:B选项:抢占其他进程资源是一种解除死锁的策略,而不是预防死锁。

A选项:死锁的预防是指破坏产生死锁的四个必要条件中(互斥条件、不剥夺条件、请求并保持条件、循环等待条件)的一个或多个;一次性请求所有资源即破坏请求并保持条件。

C选项:死锁的解除主要方法有:资源剥夺法、撤销进程法、进程回退法。

其中撤销的原则可以按进程优先级和撤销进程代价的高低进行。

(死锁的检测和解除中,在系统为进程分配资源时不采取任何措施——只提供死锁的检测和解除手段)

D选项:死锁的避免并不是事先采取某种限制措施,而是在资源动态分配过程中,防止系统进入【不安全状态】(不能用于判断系统是否处于死锁),常用银行家算法。

image.png

3.2 为啥3次握手4次挥手

(1)为什么是三次握手,关闭是四次?

答:四次是因为客户端发送FIN请求释放后,服务器端还可能继续发送数据,所以第一个是先回复客户端的FIN,等发送完所有数据服务器端再发送FIN。

而三次握手是因为客户端发送连接请求后,客户端可以直接发送SYN+ACK报文,而四次挥手是只是先发送ACK报文回应客户端的FIN报文已被接收。

(2)为啥四次挥手要等待2MSL?

答:防止客户端最后一次发送给服务器的确认ACK在网络中丢失,以至于客户端关闭了而服务端未关闭(如果服务端没有收到ACK则会不断发送FIN报文,即客户端不能立马关闭)。

2MSL即一个发送和一个回复的最大时间。

(3)为啥不用两次握手?四次握手?

答:TCP是可靠传输的,面向字节即对每个字节的数据分配一个序号。

1)因为如果两次握手,只有服务器对客户端的起始序列号做出确认,但客户端却没有对服务器的起始序列号做确认,不能保证TCP运输可靠性;

2)而四次握手没必要(第二三步可以合并,提高连接的速度和效率)。

3.3 虚拟内存有啥用

虚存是【时间换空间】

——解决容量问题(虚实地址转换,多次访存增加延时,降低计算机速度;使用一部分辅存);

Cache是【金钱换时间】

——解决速度问题(缓和CPU与主存的速度不匹配)。

四、算法题

4.1 说一下各种排序算法,稳定性如何?快排能写吗

4.2 合法的ip地址

4.2 表达式等价

4.3 虚拟内存的换页机制(lru)

五、Linux

5.1 linux常用命令

1.cd进入对应目录

2.ls列出当前文件夹内的内容

3.pwd显示目前当前工作目录的完整路径

4.tar用于压缩解压

5.mkdir创建目录

6.ifconfig查看自己IP、网络设备状态

7.grep用于查找文件里面符合条件的字符串

——linux中三大文本处理工具:awk、sed、grep。

5.2 孤儿进程&守护进程

(1)孤儿进程

1.什么是 孤儿进程

如果一个子进程的父进程先于子进程 结束, 子进程就成为一个孤儿进程,它由 init 进程收养,成为init进程的子进程。

2.那么如何让一个进程变为一个孤儿进程呢?

我们可以先创建一个进程,然后杀死其父进程,则其就变成了孤儿进程。

pid =  fork();
if(pid > 0) {
  exit(0);
}

(2)守护进程

1 . 什么是守护进程呢?

( daemon) 是指在后台运行,没有控制终端与之相连的进程。它独立于控制终端,通常周期性地执行某种任务 。

2. 为什么要引入守护进程:

由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。

但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他地变化而受到影响,那么就必须把这个进程变成一个守护进程。

更多学习请参考。

六、回顾socket编程

超级好文-https的故事

image.png

1.Socket

基础概念

(0)文件描述(file description表示一个打开文件的上下文信息,如文件的大小、内容、编码等),如同一个抽屉,该文件描述由内核管理,而文件描述和文件描述符不同——回顾OS笔记第23点可知open()系统调用后,内核分配给用户空间一个文件描述符fd——即抽屉的把手(h a n d l e handlehandle,翻译为句柄,fd其实就是一个整数)。

一个抽屉可以有多个把手(即文件描述可对应多个fd);只有多个fd都关了,内核才知道该文件描述没人用了(可回顾硬链接),所以就回收。

(1)socket是一种特殊接口(也是一种文件描述符fd),如插座端口上的孔,端口不能被其他进程占用。Socket即为实现操作某个IP地址上的某个端口达到点到点通信的目的,需要绑定到某个具体的进程中和端口中。

(2)客户端和服务器之间的通信都需要唯一的socket,每个socket都由 {undefined协议、本地地址、本地端口} 表示,一个完整的套接字则由{协议、本地地址、本地端口、远程端口}表示。

(3)socket也有类似打开文件的函数调用,该函数返回一个整型的socket描述符。

struct sockaddr 
{
  unsigned short sa_family; /*地址族*/
  char sa_data[14]; /*14 字节的协议地址,包含该 socket 的 IP 地址和端口号。*/
};
struct sockaddr_in 
{
  short int sin_family; /*地址族*/
  unsigned short int sin_port; /*端口号*/
  struct in_addr sin_addr; /*IP 地址*/
  unsigned char sin_zero[8]; /*填充 0 以保持与 struct sockaddr 同样大小*/
};
struct in_addr
{
  unsigned long int s_addr; /* 32 位 IPv4 地址,网络字节序 */
};

分类

(1)流式socket——TCP通信;

(2)数据报socket——UDP通信;

(3)原始socket。

2.接收data流程

1)网卡接收数据

最终就是网卡读取数据后,存入内存中,该过程用到DMA、IO通路:

(1)网卡收到网线传来的数据;

(2)硬件电路传输;

(3)将数据写入到内存中的某个地址上。

2)如何知接收到data

当网卡将data写入内存后,网卡向CPU发出一个中断信号,CPU从而知道有新数据到来,从而CPU执行中断处理程序。

3)进程阻塞和CPU关系

阻塞是进程调度的关键一环,指进程在等待某件事(如接收到网络数据)前的等待状态,方法有recv、select、epoll等。

服务器端的简化流程:

//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);   
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)

可以回顾OS的进程状态转换图:

image.png

等待态即阻塞态,从简化流程中看出,一开始创建socket语句,即创建一个由文件系统管理的socket对象(该对象包含发送缓冲区、接收缓冲区与等待队列等,其中等待队列指向所有需要等待该socket事件的进程)

上面的recv是一个阻塞方法,当运行到recv时会一直等待,直到接收到数据为止;

image.png

当socket接收到数据后,OS将socket等待队列上的进程重新放回到工作队列,即该进程由【阻塞态】变为【就绪态】,之后变为【运行态】。

socket的接收缓冲区已经有了data,recv返回接收到的数据。

中断程序作用:

(1)先将网络数据写入到对应socket的接收缓冲区里面;

(2)之后唤醒在等待队列中的进程,重新将该进程放入到工作队列中。

而唤醒进程后,OS如何知道网络数据对应哪个socket?

答:网络数据报的首部字段包含IP和端口,而socket和端口号是一一对应关系。

4.同时监视多个socket

回顾多路复用的“复用”和分用:

image.png

而最后一个概念就提到I/O多路复用。现在服务器端要管理多个客户端连接,而recv只能监视一个socket,为了解决这个问题先后出现了select和epoll。

1.select

1)设计思想

一个fds数组存放所有需要监视的socket,然后调用select——如果fds中所有socket都没有数据,select会阻塞,直到有一个socket接收到数组,select返回,唤醒进程。

用户可以遍历fds数组,通过FD_ISSET判断具体哪个socket收到数据,然后做出处理。

int s = socket(AF_INET, SOCK_STREAM, 0);  
bind(s, ...);
listen(s, ...);
int fds[] =  存放需要监听的socket;
while(1){
    int n = select(..., fds, ...)
    for(int i=0; i < fds.count; i++){
        if(FD_ISSET(fds[i], ...)){
            //fds[i]的数据处理
        }
    }
}

若进程A同时监视三个socket(sock1、2、3),调用select后A进程加入到这3个socket的等待队列,而如果此时sock2收到了数据后,利用中断处理程序,进程被唤醒从而离开等待队列,重新调入工作队列中(如下图),程序只需遍历一遍socket列表,就可以得到就绪的socket。

image.png

上面的例子是只有一个socket接收缓冲区有数据,如果有多个socket接收缓冲区有数据,则select直接返回,不会阻塞,select的返回值也就可能大于0。

2)select的缺点

(1)每次调用 select 都需要将进程加入到所有监视 socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历;

而且每次都要将整个 fds 列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定 select 的最大监视数量,默认只能监视 1024 个 socket。

(2)进程被唤醒后,程序并不知道哪些 socket 收到数据,还需要遍历一次。

2.epoll特点

为了减少遍历次数。

1)功能分离

image.png

多数情况下,需要监视的socket相对固定,因此epoll将【维护等待队列】和【阻塞进程】分离——先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程。

int s = socket(AF_INET, SOCK_STREAM, 0);   
bind(s, ...)
listen(s, ...)
int epfd = epoll_create(...);//(1)create创建了一个epoll对象epfd
epoll_ctl(epfd, ...); //(2)将所有需要监听的socket添加到epfd中
while(1){
    int n = epoll_wait(...)//(3)等待数据
    for(接收到数据的socket){
        //处理
    }
}

2)就绪列表

为了避免像select一样程序不知道哪些socket收到数据而需要逐个遍历,epoll就在内核维护一个就绪列表(引用收到数据的socket)。只要获取该就绪列表rdlist的内容,就能知道哪些socket收到数据。

image.png

5.epoll详解

1)创建epoll对象

当某个进程调用epoll_create后,内核会创建一个eventpoll对象(和socket一样会等待队列)。内核要维护就绪列表等数据,就绪列表可以作为eventpoll的成员。

image.png

2)维护监视列表(epoll_ctl)

创建了eventpoll对象后,利用epoll_ctl添加或删除所要监听的socket。

如下图通过epoll_ctl添加sock1、2、3的监听,内核会将eventpoll添加到这三个socket的等待队列中。

image.png

添加所要监听的socket

3)接收数据

当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程.

当socket接收到数据后,中断程序会给eventpoll对象的就绪列表添加socket的引用,如下图红线。

eventpoll对象相当于socket和进程之间的中介,socket的数据接收后不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。

image.png

4)阻塞和唤醒进程

当程序执行到epoll_wait时,如果rdlist已经引用了socket,那么epoll_wait直接返回,如果rdlist为空,阻塞进程。

假如计算机中正在运行进程 A 和进程 B,在某时刻进程 A 运行到了 epoll_wait 语句。如下图所示,内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。

image.png

当 socket 接收到数据,中断程序一方面修改 rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态(如下图)。也因为 rdlist 的存在,进程 A 可以知道哪些 socket 发生了变化。

image.png

6.eventpoll的DS

eventpoll包含了lock、mtx、wq(等待队列)与rdlist等成员,其中rdlist就绪列表和 rbr兴趣列表最重要。

从《Linux/Unix系统编程手册》中提到,epoll实例实现了2个目的:

(1)记录了在进程中声明过的刚兴趣的文件描述符列表——i n t e r e s t . l i s t interest.listinterest.list(兴趣列表),其实就是上面介绍的eventpoll添加所要监听的socket。

(2)维护了处于I/O就绪态的文件描述符列表——r e a d y . l i s t ready.listready.list(就绪列表),其实就是上面介绍rdlist列表。

1)就绪列表的结构

eventpoll的就绪列表r e a d y . l i s t ready.listready.list用双向链表实现,为啥呢——就绪列表引用着就绪的socket,需要能够快速插入数据,即利用epoll_ctl实现监听socket,也要能够快速删除。

2)兴趣列表的索引结构

epoll使用【红黑树】作为索引结构,追踪当前监听的所有文件描述符——即兴趣列表用了红黑树。

epoll将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的 socket,至少要方便地添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N))。

PS:

因为操作系统要兼顾多种功能,以及由更多需要保存的数据,rdlist 并非直接引用 socket,而是通过 epitem 间接引用,红黑树的节点也是 epitem 对象。同样,文件系统也并非直接引用着 socket。

相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
相关文章
|
3天前
|
存储 监控 API
构建高效微服务架构:后端开发的现代实践
【5月更文挑战第9天】 在本文中,我们将深入探讨如何在后端开发中构建一个高效的微服务架构。通过分析不同的设计模式和最佳实践,我们将展示如何提升系统的可扩展性、弹性和维护性。我们还将讨论微服务架构在处理复杂业务逻辑和高并发场景下的优势。最后,我们将分享一些实用的工具和技术,以帮助开发者实现这一目标。
|
5天前
|
API 持续交付 开发者
构建高效微服务架构:后端开发的新视角
【5月更文挑战第8天】 随着现代软件开发的演变,微服务架构已经成为了企业追求敏捷、可扩展和灵活部署的重要解决方案。本文将深入探讨如何构建一个高效的微服务架构,包括关键的设计原则、技术栈选择以及持续集成与部署的最佳实践。我们还将讨论微服务带来的挑战,如数据一致性、服务发现和网络延迟,并提出相应的解决策略。通过本文,后端开发者将获得构建和维护微服务系统所需的深度知识,并了解如何在不断变化的技术环境中保持系统的健壮性和可维护性。
41 8
|
2天前
|
Kubernetes API 开发者
构建高效微服务架构:后端开发的新范式
【5月更文挑战第11天】 在现代软件开发的快速演变中,微服务架构已成为企业追求敏捷性、可扩展性和技术多样性的关键解决方案。本文旨在探讨如何构建高效的微服务架构,并分析其对后端开发的影响。我们将通过一系列最佳实践和策略,展示如何优化服务的独立性、弹性和性能,同时确保系统的整体稳定性和安全性。文章还将介绍容器化、API网关、服务发现和分布式追踪等关键技术的应用,为后端开发者提供一份全面的微服务实施指南。
|
2天前
|
设计模式 监控 API
构建高效的微服务架构:后端开发的新范式
【5月更文挑战第11天】 在当今的软件开发领域,微服务架构已经成为一种流行的设计模式。它通过将应用程序分解为一组小型、松散耦合的服务来提供高度可扩展和灵活的解决方案。本文将探讨如何构建一个高效的微服务架构,包括选择合适的技术栈、设计原则以及应对常见挑战的策略。我们将深入讨论如何确保系统的可维护性、可靠性和性能,同时考虑到安全性和监控的需求。
|
3天前
|
Web App开发 数据采集 移动开发
开发uniapp过程中对app、微信小程序与h5的webview调试
开发uniapp过程中对app、微信小程序与h5的webview调试
|
3天前
|
监控 持续交付 开发者
构建高效微服务架构:后端开发的新范式
【5月更文挑战第10天】在现代软件开发领域,微服务架构已经成为一种流行的设计模式,它通过将大型应用程序拆分为一组小型、独立和松散耦合的服务来提供更高的可伸缩性和灵活性。本文深入探讨了微服务架构的设计理念、实施步骤以及面临的挑战,并提出了一套实用的策略和最佳实践,帮助后端开发者构建和维护高效的微服务系统。
|
5天前
|
Kubernetes 持续交付 开发者
构建高效微服务架构:后端开发的新趋势
【5月更文挑战第8天】 随着现代软件开发的不断演进,微服务架构已成为众多企业解决复杂系统问题的首选方案。本文深入探讨了微服务架构的核心概念、设计原则以及实施策略,旨在为后端开发者提供一种清晰、高效的技术路径。通过分析微服务的优势与挑战,结合具体的应用实例,文章将展示如何通过容器化、服务网格和持续集成/持续部署(CI/CD)等先进技术手段,实现后端服务的高可用性、可扩展性和敏捷性。
|
5天前
|
消息中间件 监控 Java
构建高效微服务架构:后端开发的新趋势
【5月更文挑战第8天】随着现代软件开发的复杂性日益增加,传统的单体应用架构逐渐难以满足快速迭代和灵活部署的需求。微服务架构作为一种新的解决方案,以其模块化、独立性强和易于扩展的特点,正在成为后端开发领域的重要趋势。本文将深入探讨如何构建一个高效的微服务架构,并分析其对后端开发实践的影响。
|
5天前
|
敏捷开发 持续交付 API
构建高效微服务架构:后端开发的现代实践
【5月更文挑战第8天】 在数字化转型的浪潮中,微服务架构已成为企业追求敏捷开发、持续交付和系统弹性的关键解决方案。本文将深入探讨微服务的核心概念,包括其设计原则、优缺点以及如何在后端开发中实现高效的微服务架构。我们将通过实际案例分析,展示微服务如何帮助企业快速适应市场变化,同时保持系统的可维护性和扩展性。
|
6天前
|
设计模式 Kubernetes 数据库
构建高效可靠的微服务架构:后端开发的新范式
【5月更文挑战第7天】在现代软件开发的浪潮中,微服务架构已经成为一种流行的设计模式。它通过将应用程序分解为一组小的、独立的服务来提高系统的可维护性和扩展性。本文深入探讨了微服务架构的核心概念、优势以及如何利用最新的后端技术构建一个高效且可靠的微服务体系。我们将讨论关键的设计原则,包括服务的独立性、通信机制、数据一致性和容错性,并展示如何在云环境中部署和管理这些服务。
19 3