模型图
单线程处理
- 我们自己会怎样写单线程处理多个客户端连接呢?我们知道在linux里面中每个网络连接在内核中都是文件描述符(
Fd
)的形式存在,为了使大家看得明白,我们使用一段伪代码来编写一个单线程网络服务器,以下伪代码中我们需要用程序判断当前Fdx是否有数据,这个其实过程还是有些慢的,下面让我们看一下select/poll/epoll
的原代码是怎么写的。
while (1){
for(Fdx in (Fd1~Fd5)) { # Fdx 为当前遍历到的文件;Fd1~Fd5为这5个网络连接在内核中的文件描述符
if (Fdx) { # 是否有数据
# 读取Fdx并处理数据
}
}
}
select 函数源码
# 准备文件描述符的数组 Fds
# 创建socket服务端
sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr, 0, sizeof (addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd,(struct sockaddr*)&addr ,sizeof(addr));
listen (sockfd, 5);
# 创建了5个文件描述符
for (i=0;i<5;i++)
{
memset(&client, 0, sizeof (client));
addrlen = sizeof(client);
fds[i] = accept(sockfd,(struct sockaddr*)&client, &addrlen);
# 计算出一个最大的文件描述符
if(fds[i] > max)
max = fds[i];
}
# 通过以上程序我们准备好了一个文件描述符的集合假如是1 3 6 7 9 和 一个最大的文件描述符 9
while(1){
FD_ZERO(&rset);
for (i = 0; i< 5; i++ ) {
FD_SET(fds[i],&rset);
}
puts("round again");
# select参数依次含义:最大的文件描述符 + 1 (用于内核截取文件描述符); 读文件描述符集合; 写文件描述符集合;异常文件描述符集合; 超时时间;
# rset参数接受的并不是Fd集合,而是接受了FD_SET,FD_SET的类型是一个bitmap
# 假如我们文件描述符集合是 1 3 6 7 9,那么我们bitmap里面存的值就是0101001101000... (1024位01的位置坑位)。
# 无数据时select阻塞
select(max+1, &rset, NULL, NULL, NULL);
# 有数据时select会把Fd置位并返回向下执行。
for(i=0;i<5;i++) {
# 循环判断哪个Fd被置位了,然后读取数据并且处理
if (FD_ISSET(fds[i], &rset)){
memset(buffer,0,MAXBUF);
read(fds[i], buffer, MAXBUF);
puts(buffer);
}
}
}
- select 函数执行过程过程中会把用户态空间rset的bitmap类型的数据复制到内核态一份,由内核判断Fd是否有数据。我们知道用户态程序判断效率一定是低于内核去判断的,因为用户态判断也是在询问内核,它是有一个用户态到内核态的切换的,判断每一个Fd都需要用户态到内核态的切换。如果没数据的话,select函数是一个阻塞函数,会一直阻塞在select所在的行。如果有数据的话,内核会把有数据的Fd置位(可以理解为标识成有数据)并且返回,不在阻塞。
select 缺点
- rset 的 bitmap 类型有大小限制1024位,即FD_SET的大小限制为1024位
- FD_SET是不可重用,rset 的数据会被内核置位修改,每次while循环的时候都需要重新赋值
- 虽然是比我们在用户态判断的消耗好些,但是 rset 数据从用户态复制到内核态仍然是有很大的开销的
- select 函数返回的时候说明至少有一个Fd有数据(被置位了),但是我们并不知道是哪一个Fd或者那些Fd被置位了,需要我们再去遍历一边哪个被置位了,需要o(n)的复杂度的。
poll 函数源码
# 声明结构图pollfd
struct pollfd {
int fd;
short events;
short revents;
};
# 构造pollfd数据
for (i=0;i<5;i++)
{
memset(&client, 0, sizeof (client));
addrlen = sizeof(client);
pollfds[i].fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
# POLLIN 读事件
pollfds[i].events = POLLIN;
}
sleep(1);
while(1){
puts("round again");
# 传入pollfds 结构体
# 5 传入的数量
# 50000 超时时间
# 也是阻塞函数
poll(pollfds, 5, 50000);
for(i=0;i<5;i++) {
if (pollfds[i].revents & POLLIN){
pollfds[i].revents = 0;
memset(buffer,0,MAXBUF);
read(pollfds[i].fd, buffer, MAXBUF);
puts(buffer);
}
}
}
- 同select一样,poll函数也阻塞函数,只不过poll传入的是一个数组类型的结构体,poll函数也会把用户态数据复制到内核态置位,但是select置位的是bitmap(导致不可从重用),而poll置位的是结构体中的revents(初始化为0,有数据置位为POLLIN)字段。同理,无数据时poll堵塞,有数据时执行读操作并处理数据,并把revents设置为0,所以pollfd数据并没有发生改变可以复用。
poll函数解决了select的哪些问题
- pollfd这种结构体,解决了bitmap 1024位的限制
- 结构体的revents字段在有数据是被置位(POLLIN),我们遍历读取的时候重新设置为0,所以pollfds可以重用,解决了select中 FD_SET 不可重用的弊端。
epoll 函数源码
# 创建一个epfd,在执行epoll_wait时需要,epfd 就相当于一种空的数据格式
struct epoll_event events[5];
int epfd = epoll_create(10);
...
...
for (i=0;i<5;i++)
{
static struct epoll_event ev;
memset(&client, 0, sizeof (client));
addrlen = sizeof(client);
ev.data.fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
ev.events = EPOLLIN;
# 对 epfd配置,相当于poll声明的结构体,定义一种数据格式,epoll_ctl相当于构造epfd这种数据格式,大概是一个文件描述符对应的一个events事件 (fd-events)
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
}
# 准备好epfd执行下面程序
while(1){
puts("round again");
nfds = epoll_wait(epfd, events, 5, 10000);
for(i=0;i<nfds;i++) {
memset(buffer,0,MAXBUF);
read(events[i].data.fd, buffer, MAXBUF);
puts(buffer);
}
}
- epoll 也是阻塞函数,无数据时阻塞;有数据时,它也是置位,不过它的置位其实是一种变相的位,会把有数据的fd重拍到最前面位置,之前的select和poll都没有返回值,而epoll是有返回值的,它的返回值是有数据的个数。这样的话我们后续的遍历读取处理也简单了,遍历复杂度由 o(n)次简化为 o(1)次了。
Comment here is closed