一、多进程/多线程模型的不足
为每个请求分配一个进程或线程的方式会带来较大的资源开销。创建和切换进程/线程需要消耗系统资源,包括内存、CPU 时间等。例如,在一个大规模的服务器环境中,如果同时有数千个请求到来,为每个请求创建单独的进程或线程,可能会迅速耗尽系统的内存资源,导致系统性能下降甚至崩溃。而且,大量的进程/线程切换也会增加 CPU 的负担,降低整体的处理效率。
二、I/O 多路复用技术
I/O 多路复用技术允许一个进程同时监控多个 Socket 连接。虽然在某一时刻只能处理一个请求,但由于每个请求的处理时间极短,在单位时间内就能处理大量的请求。这种时分复用的方式类似于 CPU 对多个进程的并发处理。以一个在线游戏服务器为例,它可能同时接收来自众多玩家的连接请求,通过 I/O 多路复用,服务器能够高效地处理这些请求,而无需为每个玩家创建单独的进程或线程。
1、select、poll、epoll 内核提供的多路复用系统调用:
这些系统调用为进程提供了从内核获取多个事件的途径。进程可以通过调用相应的函数,将关注的连接(以文件描述符表示)传递给内核,内核会监控这些连接的状态变化,并将产生了事件的连接返回给进程。这使得进程能够集中处理有数据到达或可进行读写操作的连接,提高了系统的资源利用率和处理效率。
2、select、poll、epoll 获取网络事件的过程:
首先,应用程序将需要监控的所有连接的文件描述符传递给内核。内核会对这些连接进行监控。当有连接发生状态变化,比如有新数据到达、连接关闭等,内核会将这些产生了事件的连接的文件描述符返回给应用程序。应用程序在接收到这些信息后,就可以在用户态中对相应的连接进行处理,例如读取数据、发送响应等。
3、select、poll、epoll 能否实现 C10K:
- select:由于其存在文件描述符数量限制以及在每次调用时需要在用户态和内核态之间频繁复制大量文件描述符集合的问题,导致其在处理大规模并发连接时效率低下,较难实现 C10K。例如,在一个高并发的聊天应用中,如果使用 select 来处理大量的连接,可能会出现响应延迟、丢包等问题。
- poll:虽然没有了文件描述符数量的限制,但它在每次调用时仍需要复制大量的文件描述符集合,效率问题依然存在,要实现 C10K 也面临较大挑战。比如在一个大规模的电商网站中,大量用户同时发起请求,如果使用 poll 处理,可能会导致服务器性能下降,影响用户体验。
- epoll:采用了更高效的事件驱动方式,避免了上述的开销,能够轻松应对 C10K 甚至更高的并发连接需求。以一个热门的在线视频平台为例,使用 epoll 可以有效地处理大量观众同时观看视频时产生的并发连接,保证视频的流畅播放和用户的良好体验。
三、select 实现多路复用的方式
select 会把已连接的 Socket 放入一个文件描述符集合。这就像是把多个钥匙放在一个盒子里。当调用 select 函数时,会把这个装满钥匙(文件描述符)的盒子拷贝到内核中。内核就像一个检查官,通过逐个遍历盒子里的钥匙来检查是否有网络事件产生。一旦发现某个钥匙对应的 Socket 有事件,就给它做个可读或可写的标记。然后,又把整个盒子拷贝回用户态。回到用户态后,用户程序也得像内核那样,再次遍历这个盒子里的钥匙,找到有标记的、可读或可写的 Socket 进行处理。比如说,想象一个电商网站的服务器,同时处理大量的用户请求。select 就像是一个不太聪明的管理员,每次都要把所有的请求信息(文件描述符集合)搬进搬出内核,还得反复查看每个请求的状态。
1、select 的遍历和拷贝问题
对于 select 这种方式,需要进行两次对文件描述符集合的遍历。内核态里的遍历就像是在一个大仓库里逐个检查货物是否有问题;用户态里的遍历则像是在一堆已经检查过的货物中再次筛选出有问题的。而且,还会发生两次文件描述符集合的拷贝。这就好比把一箱子东西从一个房间搬到另一个房间,修改后又搬回来,这个过程既繁琐又耗时。以一个在线游戏的服务器为例,大量玩家同时在线,频繁的遍历和拷贝会导致服务器处理请求的速度变慢,玩家可能会感觉到卡顿。
2、select 的文件描述符限制
select 使用固定长度的 BitsMap 来表示文件描述符集合。这就好比一个固定大小的柜子,只能放一定数量的东西。在 Linux 系统中,它能处理的文件描述符个数由内核中的 FD_SETSIZE 限制,默认最大值为 1024,只能监听 0 到 1023 的文件描述符。假设我们有一个大型的金融交易系统,当并发连接数超过 1024 时,select 就无法有效处理,可能会导致部分交易请求被遗漏或延迟处理。
3、select服务端代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<netinet/ip.h>
#include<arpa/inet.h>
#define BUFSIZE 1024
#define SERROPT 8002
#define SERIP "192.168.117.129"
int main(int argc, char* argv[])
{
int lfd,cfd;
char buf[BUFSIZE];
socklen_t addrlen;
lfd= socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in seraddr, cliadd