服务器编程
select poll epoll的区别与联系?
select
, poll
和epoll
都是I/O多路复用技术,它们用于同时处理多个I/O操作,特别是在高并发网络编程中。
select
select
是最早的I/O多路复用技术,它可以同时监视多个文件描述符(file descriptor, FD)的I/O状态(如可读、可写、异常等)。select
函数使用一个文件描述符集合(通常是一个位图)来表示要监视的文件描述符,当有I/O事件发生时,select
会返回对应的文件描述符集合。
select的主要限制如下:
- 文件描述符数量限制:
select
使用一个位图来表示文件描述符集合,这限制了它能够处理的文件描述符数量(通常是1024个)。 - 效率问题:当文件描述符数量较大时,
select
需要遍历整个文件描述符集合来查找就绪的文件描述符,这会导致较低的效率。 - 非实时性:每次调用
select
时,需要重新设置文件描述符集合,这会增加函数调用的开销。
poll
poll
是为了克服select
的限制而引入的一种I/O多路复用技术。poll
使用一个文件描述符数组(通常是一个结构体数组)来表示要监视的文件描述符。与select
类似,poll
可以监视多个文件描述符的I/O状态。
poll的优点如下:
- 文件描述符数量不受限制:由于
poll
使用一个动态数组来表示文件描述符,因此它可以处理任意数量的文件描述符。 - 效率相对较高:
poll
在查找就绪的文件描述符时,只需要遍历实际使用的文件描述符数组,而不是整个文件描述符集合。
然而,poll
仍然存在一些问题:
- 效率问题:尽管
poll
相对于select
具有较高的效率,但当文件描述符数量很大时,它仍然需要遍历整个文件描述符数组。 - 非实时性:与
select
类似,每次调用poll
时,需要重新设置文件描述符数组。
epoll
epoll
是Linux特有的一种高效I/O多路复用技术,它克服了select
和poll
的主要限制。epoll
使用一个事件驱动(event-driven)的方式来处理I/O操作,它只会返回就绪的文件描述符,而不是遍历整个文件描述符集合。
epoll的主要优点如下:
- 高效:
epoll
使用事件驱动的方式来处理I/O操作,因此它在处理大量文件描述符时具有很高的效率。当有I/O事件发生时,epoll
可以立即得到通知,而无需遍历整个文件描述符集合。这使得epoll
在高并发场景中具有更好的性能。 - 可扩展性:与
poll
类似,epoll
可以处理任意数量的文件描述符,因为它使用一个动态数据结构来表示文件描述符。 - 实时性:
epoll
使用一个内核事件表来记录要监视的文件描述符和事件,因此在每次调用epoll
时无需重新设置文件描述符集合。这可以减少函数调用的开销,并提高实时性。
epoll
具有诸多优点,但它目前仅在Linux平台上可用。对于其他平台,可能需要使用类似的I/O多路复用技术,如BSD中的kqueue
。
总结:select
是最早的I/O多路复用技术,但受到文件描述符数量和效率方面的限制。poll
克服了文件描述符数量的限制,但仍然存在一定的效率问题。epoll
是一种高效的I/O多路复用技术,尤其适用于高并发场景,但它仅在Linux平台上可用。一般来说,epoll的效率是要比select和poll高的,但是对于活动连接较多的时候,由于回调函数触发的很频繁,其效率不一定比select和poll高。所以epoll在连接数量很多,但活动连接较小的情况性能体现的比较明显。
系统调用 | select | poll | epoll |
---|---|---|---|
事件集合 | 用户通过三个参数分别传入感兴趣的可读可写以及异常等事件,内核通过对这些参数的在线修改来反馈其中就绪的事件。如果用户需要的话需要创建三个fdset以监听不同类型的事件。 | 统一处理所有的事件类型,因此只需要一个事件集参数。用户通过pollfd.events来传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件。 | 内核通过一个事件表直接管理用户感兴趣的所有事件。每次调用epoll_wait,内核直接在调用参数的events中注册就绪事件。 |
应用程序索引效率 | 采用轮询方式,O(n) | 采用轮询方式,O(n) | 采用回调方式,O(1) |
最大支持文件描述符数 | 一般1024 | 65535 | 65535 |
工作模式 | 条件触发 | 条件触发 | 条件触发或边缘触发 |
边缘触发与条件触发分别是什么?
边缘触发(Edge-triggered)和条件触发(Level-triggered)是两种常见的事件触发方式,主要应用于I/O多路复用和中断处理等场景。
边缘触发(Edge-triggered)
边缘触发是指在事件状态发生变化的时刻触发一次,例如从无事件变为有事件。在I/O多路复用中,边缘触发意味着当某个文件描述符发生I/O事件(如变为可读或可写)时,我们只会收到一次通知。当收到通知后,我们需要处理该文件描述符上的所有数据,直到数据全部处理完毕,否则不会再收到通知。
边缘触发的优点是只在事件状态改变时触发,可以减少事件通知的次数。然而,边缘触发的缺点是我们需要确保在收到通知后处理所有相关数据,否则可能会遗漏某些事件。
条件触发(Level-triggered)
条件触发是指只要事件状态保持满足某种条件,就会持续触发。在I/O多路复用中,条件触发意味着只要某个文件描述符的I/O事件状态满足条件(如可读或可写),我们就会不断收到通知。
条件触发的优点是它可以确保我们不会遗漏任何事件,因为只要条件满足,就会持续触发。然而,条件触发的缺点是它可能导致大量的事件通知,从而增加处理开销。
讲一讲client-server通信双方API调用过程?
Client-server 通信是客户端与服务器之间进行数据交互的一种常见方式。客户端和服务器分别调用各自的 API 来建立连接、发送请求、接收响应以及关闭连接。以下是典型的 Client-server 通信过程中的 API 调用:
服务器端 API 调用过程:
- 创建套接字(socket):服务器端首先创建一个套接字。在 C 语言中,可以使用
socket()
函数创建一个新的套接字。 - 绑定地址(bind):然后,服务器将套接字绑定到指定的 IP 地址和端口。这可以使用
bind()
函数完成。 - 监听连接(listen):服务器将套接字设置为监听模式,以便接受来自客户端的连接请求。这可以通过调用
listen()
函数实现。 - 接受连接(accept):当客户端发起连接请求时,服务器使用
accept()
函数接受该连接。accept()
函数返回一个新的套接字,用于与客户端进行通信。 - 接收数据(recv):服务器使用
recv()
或类似的函数从客户端接收数据。这些函数通常会阻塞,直到收到数据。 - 发送数据(send):服务器根据客户端的请求处理数据并生成响应。然后,服务器使用
send()
或类似的函数将响应数据发送回客户端。 - 关闭连接(close):完成通信后,服务器使用
close()
或类似的函数关闭与客户端的连接。服务器可以继续接受其他客户端的连接。
客户端 API 调用过程:
- 创建套接字(socket):与服务器类似,客户端使用
socket()
函数创建一个新的套接字。 - 连接服务器(connect):客户端使用
connect()
函数发起对服务器的连接请求。这需要指定服务器的 IP 地址和端口。 - 发送数据(send):连接建立后,客户端使用
send()
或类似的函数向服务器发送请求数据。 - 接收数据(recv):客户端使用
recv()
或类似的函数接收来自服务器的响应数据。这些函数通常会阻塞,直到收到数据。 - 关闭连接(close):通信完成后,客户端使用
close()
或类似的函数关闭与服务器的连接。
阻塞IO、非阻塞IO有什么区别?怎么判断写文件时Buffer已经写满?
阻塞IO和非阻塞IO是两种不同的I/O处理方式,它们主要区别在于I/O操作是否会导致调用者等待。
阻塞IO(Blocking IO)
在阻塞IO模式下,当一个I/O操作(如读或写)发起时,如果数据还没有准备好(例如,等待数据从磁盘读取或从网络接收),则调用者(通常是一个线程或进程)会被阻塞,直到数据准备好为止。在此期间,调用者无法执行其他任务,只能等待I/O操作完成。
阻塞IO的优点是编程简单,容易实现。然而,它的缺点是当I/O操作耗时较长时,会导致调用者的低效率和资源浪费,尤其在高并发场景中。
非阻塞IO(Non-blocking IO)
在非阻塞IO模式下,当一个I/O操作发起时,如果数据还没有准备好,调用者不会被阻塞,而是立即返回一个错误码(例如,表示资源不可用)。调用者可以继续执行其他任务,然后在适当的时间点再次尝试I/O操作。
非阻塞IO的优点是可以提高调用者的效率和资源利用率,尤其适用于高并发场景。然而,它的缺点是编程复杂度较高,需要使用I/O多路复用技术(如select
,poll
或epoll
)来处理多个I/O操作。
关于判断写文件时Buffer是否已经写满,通常是通过以下方式:
在阻塞IO模式下,当写入操作发起时,如果Buffer已满,调用者会被阻塞,直到Buffer有足够的空间容纳新的数据。在这种情况下不需要担心Buffer是否已满,因为操作系统会自动处理这个问题。
在非阻塞IO模式下,当写入操作发起时,如果Buffer已满,调用者会立即收到一个错误码(例如,表示资源不可用)。在这种情况下需要根据错误码来判断Buffer是否已满,并在适当的时间点再次尝试写入操作。通常,你可以结合I/O多路复用技术来监听文件描述符的可写事件,以便在Buffer有空间时得到通知。
同步与异步的区别,阻塞与非阻塞的区别?
同步和异步主要关注的是调用者与被调用者之间的关系。
- 同步(Synchronous):在同步操作中,调用者发起一个请求后,需要等待被调用者处理完毕并返回结果,期间调用者不能进行其他操作。换句话说,调用者与被调用者的执行是串行的。同步操作的典型例子是普通的函数调用。
- 异步(Asynchronous):在异步操作中,调用者发起一个请求后,无需等待被调用者处理完毕,可以继续执行其他操作。被调用者在处理完请求后,通常通过回调函数、事件或消息队列等方式通知调用者结果。换句话说,调用者与被调用者的执行是并行的。异步操作的典型例子是JavaScript中的Ajax请求。
阻塞和非阻塞主要关注的是I/O操作或系统调用的行为。
- 阻塞(Blocking):在阻塞操作中,如果数据还没有准备好(例如,等待数据从磁盘读取或从网络接收),则调用者(通常是一个线程或进程)会被阻塞,直到数据准备好为止。在此期间,调用者无法执行其他任务,只能等待I/O操作完成。阻塞I/O操作的典型例子是普通的文件读写。
- 非阻塞(Non-blocking):在非阻塞操作中,如果数据还没有准备好,调用者不会被阻塞,而是立即返回一个错误码(例如,表示资源不可用)。调用者可以继续执行其他任务,然后在适当的时间点再次尝试I/O操作。非阻塞I/O操作的典型例子是使用
select
,poll
或epoll
等I/O多路复用技术处理的网络通信。
总结:同步和异步关注的是调用者与被调用者之间的关系,同步操作需要等待结果,而异步操作可以立即返回。阻塞和非阻塞关注的是I/O操作的行为,阻塞操作会导致调用者等待,而非阻塞操作可以立即返回。这两者之间可以组合形成不同的操作模式,例如同步阻塞、同步非阻塞、异步阻塞和异步非阻塞。
讲一讲Rector模式与Proactor模式?
Reactor 模式和 Proactor 模式都是处理并发 I/O 事件的设计模式。它们各自的核心思想是将 I/O 操作与实际处理逻辑解耦,以便在高并发环境下更有效地处理请求。下面分别介绍这两种模式:
Reactor 模式
Reactor 模式基于事件驱动和异步 I/O 操作。其核心组件包括 Reactor、事件处理器和资源(如套接字)。Reactor 模式的工作原理如下:
- Reactor 负责监视多个资源(如套接字)上的 I/O 事件。当某个资源上发生 I/O 事件时,Reactor 将事件通知对应的事件处理器。
- 事件处理器负责处理这些 I/O 事件,如接受连接、读取数据、写入数据等。事件处理器将 I/O 操作与实际的业务逻辑解耦,使程序更易于管理和扩展。
- Reactor 模式通常使用非阻塞 I/O 操作。当资源不可用时,事件处理器不会阻塞,而是返回并允许 Reactor 继续监视其他资源。
Reactor 模式适用于 I/O 密集型应用,特别是当 I/O 操作可能导致阻塞时。其主要优点是简化了并发 I/O 处理,提高了程序的可扩展性和性能。
Proactor 模式
Proactor 模式是 Reactor 模式的扩展,采用异步 I/O 操作和操作系统级别的异步通知机制。Proactor 模式的核心组件包括 Proactor、异步操作处理器和资源(如套接字)。Proactor 模式的工作原理如下:
- Proactor 负责启动异步 I/O 操作(如读取、写入等)。异步 I/O 操作在后台进行,不会阻塞主程序的执行。
- 当异步 I/O 操作完成时,操作系统将通知 Proactor。Proactor 随后调用相应的异步操作处理器来处理已完成的操作。
- 异步操作处理器负责处理已完成的异步 I/O 操作。与 Reactor 模式类似,这些处理器将 I/O 操作与实际的业务逻辑解耦,使程序更易于管理和扩展。
Proactor 模式的主要优点是充分利用了操作系统的异步 I/O 功能,进一步简化了并发 I/O 处理,提高了程序的可扩展性和性能。但是,Proactor 模式对操作系统的支持程度不同,因此可能需要考虑跨平台兼容性。
如何调试服务器内存占用过高的问题?
服务器内存占用过高可能是由多种原因引起的,如内存泄漏、程序逻辑错误等。
- 监控内存使用情况:首先,使用操作系统提供的工具(如Linux上的
top
,htop
或free
命令)来实时监控服务器的内存使用情况。这可以帮助确定问题是否确实是由内存占用过高导致的,以及问题的严重程度。 - 确定问题进程:接下来,通过监控工具找出内存占用过高的进程。可以通过
top
或ps
命令查看进程的详细信息,例如进程ID、用户、CPU使用率、内存使用率等。 - 分析进程内存使用:使用进程内存分析工具(如
pmap
或smem
)来查看问题进程的内存使用情况。这可以帮助你了解进程的内存使用分布,例如堆、栈、共享库等。通过这些信息,你可以初步判断问题可能出在哪个模块或功能。 - 分析内存泄漏:如果怀疑问题是由内存泄漏导致的,可以使用内存泄漏检测工具(如
valgrind
或gperftools
)对进程进行分析。这些工具可以帮助你找出内存泄漏的位置,以及泄漏的详细信息。 - 分析程序代码:根据前面的分析结果,仔细审查程序代码,检查是否存在内存分配和释放不当、数据结构设计不合理等问题。这可能需要深入理解程序的逻辑和算法。
- 优化程序:针对发现的问题,优化程序代码,修复内存泄漏或逻辑错误。在修复后,重新监控内存使用情况,确保问题得到解决。
- 定期检查:即使问题得到解决,也建议定期检查服务器的内存使用情况,以便及时发现潜在的问题。可以通过编写脚本或使用第三方监控工具来自动化这一过程。
Linux下如何查到端口被哪个进程占用?
两个方法查看占用端口进程
- lsof -i:端口号
- netstat -tunlp|grep 端口号
Linux零拷贝的原理?
传统I/O工作流如下图所示
零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。通过减少用户态与内核态上下文切换和减少内存拷贝次数实现,通常实现方式有3种:mmap+write、sendfile、sendfile+DMA scatter/gather
- mmap+write: mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。包含2次系统调用,3次数据拷贝(2次DMA和1次CPU拷贝)
- 用户进程通过mmap方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
- CPU利用DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
- 上下文从内核态切换回用户态,mmap方法返回。
- 用户进程通过write方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
- CPU将内核缓冲区的数据拷贝到的socket缓冲区。
- CPU利用DMA控制器,把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write调用返回。
- sendfile:
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile()。它可以替代前面的 read() 和 write() 这两个系统调用,减少一次系统调用。包含1次系统调用,3次数据拷贝(2次DMA和1次CPU拷贝)
- 用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态。
- DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU将读缓冲区中数据拷贝到socket缓冲区。
- DMA控制器,异步把数据从socket缓冲区拷贝到网卡。
- 上下文(切换2)从内核态切换回用户态,sendfile调用返回。
- sendfile+DMA scatter/gather:
linux 2.4版本之后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次CPU拷贝。包含1次系统调用,2次数据拷贝(2次DMA拷贝)。
- 用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态。
- DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU把内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到socket缓冲区。
- DMA控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡。
- 上下文(切换2)从内核态切换回用户态,sendfile调用返回。
LVS的NAT、TUN、DR原理及区别?
LVS是Linux Virtual Server的简写,意即Linux虚拟服务器,是一个虚拟的服务器集群系统,使用负载均衡技术将多台服务器组成一个虚拟服务器。虚拟服务器的体系结构由一组服务器通过高速的局域网或者地理分布的广域网相互连接,在它们的前端有一个负载调度器(Load Balancer)。负载调度器能无缝地将网络请求调度到真实服务器上。
NAT(网络地址转换模式)
原理:就是把客户端发来的数据包的IP头的目的地址,在负载均衡器上换成其中一台RS的IP地址,并发至此RS来处理,RS处理完成后把数据交给经过负载均衡器,负载均衡器再把数据包的原IP地址改为自己的IP,将目的地址改为客户端IP地址即可。
TUN(IP隧道模式)
原理:隧道模式就是,把客户端发来的数据包,封装一个新的IP头标记(仅目的IP)发给RS,RS收到后,先把数据包的头解开,还原数据包,处理后,直接返回给客户端,不需要再经过负载均衡器。注意,由于RS需要对负载均衡器发过来的数据包进行还原,所以说必须支持IPTUNNEL协议,所以,在RS的内核中,必须编译支持IPTUNNEL这个选项。
各集群节点可以跨越不同的网络,不用在同一个VLAN。
调度器根据各个服务器的负载情况,动态地选择一台服务器,将请求报文封装在另一个 IP 报文中,再将封装后的 IP 报文转发给选出的服务器
服务器收到报文后,先将报文解封获得原来目标地址为 VIP 的报文,服务器发现 VIP地址被配置在本地的 IP 隧道设备上,所以就处理这个请求,然后根据路由表将响应报文直接返回给客户。
DR(直接路由模式)
原理:负载均衡器和RS都使用同一个IP对外服务,但只有DR对ARP请求进行响应,所有RS对本身这个IP的ARP请求保持静默,也就是说,网关会把对这个服务IP的请求全部定向给DR,而DR收到数据包后根据调度算法,找出对应的RS,把目的MAC地址改为RS的MAC(因为IP一致)并将请求分发给这台RS,这时RS收到这个数据包,处理完成之后,由于IP一致,可以直接将数据返给客户,则等于直接从客户端收到这个数据包无异,处理后直接返回给客户端。由于负载均衡器要对二层包头进行改换,所以负载均衡器和RS之间必须在一个广播域,也可以简单的理解为在同一台交换机上。
区别:
优点 | 缺点 | |
---|---|---|
NAT | 集群中的物理服务器可以使用任何支持TCP/IP操作系统它只需要一个 IP 地址配置在调度器上,服务器组可以用私有的 IP 地址 | 扩展性有限。当服务器节点(普通PC服务器)增长过多时,负载均衡器将成为整个系统的瓶颈,因为所有的请求包和应答包的流向都经过负载均衡器。当服务器节点过多时,大量的数据包都交汇在负载均衡器那,速度就会变慢。 |
TUN | 负载均衡器只负责将请求包分发给后端节点服务器,而RS将应答包直接发给用户。所以,减少了负载均衡器的大量数据流动,负载均衡器不再是系统的瓶颈,就能处理很巨大的请求量,这种方式,一台负载均衡器能够为很多RS进行分发。而且跑在公网上就能进行不同地域的分发。 | 隧道模式的RS节点需要合法IP,这种方式需要所有的服务器支持”IP Tunneling”(IP Encapsulation)协议,服务器可能只局限在部分Linux系统上。 |
DR | VS/DR跟 VS/TUN 方法相同,负载调度器中只负责调度请求,而服务器直接将响应返回给客户,可以极大地提高整个集群系统的吞吐量。 | 要求负载均衡器的网卡必须与物理网卡在一个物理段上 |
NAT模式 | IP TUN模式 | DR模式 | |
---|---|---|---|
对服务器要求 | 任何操作系统均支持 | 必须支持IP隧道协议,目前只有Linux支持 | 支持虚拟网卡,且可以禁用ARP响应 |
网络要求 | 局域网 | 局域网或广域网 | 局域网 |
支持的节点数 | 10~20个,视Director处理能力而定 | 可以支持到100个节点 | 可以支持到100个节点 |
安全性 | 较高,可隐藏real server | 较差,real server 容易暴露 | 较差,real server 容易暴露 |
IP要求 | 仅需要一个合法IP地址作为VIP | 除VIP外,每个服务器需要拥有合法IP地址可以直接路由至客户端 | 除VIP外,每个服务器需要拥有合法IP地址可以直接路由至客户端 |
拓展性 | 差 | 很好 | 好 |
特点 | 地址转换 | 封装IP | 修改MAC地址 |