场景:设计一个高性能的网络服务器,能够供多个客户端同时进行连接,并且能够处理这些客户端传上来的请求
应对并发,可以设计一个多线程的程序,每个传上来的请求都开一个线程。存在一个弊端,需要CPU上下文的切换,代价高
如何使用单线程解决问题?
每一个网络连接在内核中以文件描述符的形式存在
如果服务器正在处理A的请求,此时B发送一个请求,B的请求会被丢弃吗?不会,因为处理IO的设备不是CPU,而是专门的DMA
最简单粗暴的方法,缺点是仍然由CPU判断是否有数据
while(1){
for(fdx in (fda ~ fde)){
if(fdx有数据){
读fdx并处理;
}
}
}
select
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(socket, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);
for(i = 0; i < 5; i++){
memset(&client, 0, sizeof(client));
addrlen = sizeof(client);
fds[i] = accept(socket, (struct sockaddr*)&client, &addrlen);
if(fds[i] > max)
max = fds[i];
}
/*----------------------*/
while(1){
FD_ZERO(&rset);
for(i = 0; i < 5; i++)
FD_SET(fds[i], &rset);
puts("round again");
select(max + 1, &rset, NULL, NULL, NULL);
for(i = 0; i < 5; i++){
if(FD_ISSET(fds[i], &rset)){
memset(buffer, 0, MAXBUF);
read(fds[i], buffer, MAXBUF);
puts(buffer);
}
}
}
上一个部分主要是为了准备文件描述符的数组fds。首先创建了一个socket的服务端,然后创建了五个文件描述符
文件描述符是一些随机的不重复的数,将其中的最大值存到max中
select方法的参数:读文件描述符集合、rset、写文件描述符集合、异常描述符集合、超时时间
这里关心的是读文件描述符,因为要读取网络连接中的数据,将写文件描述符集合、异常描述符集合设置为NULL,超时时间NULL表示使用默认时间
rset是一个bitmap,用来表示哪一个文件描述符是被启用/监听的,bitmap有1024位,哪一位为1表示哪一个文件描述符被监听
select将rset从用户态拷贝到内核态,由内核态直接判断文件描述符是否有数据的操作;暴力方法判断时需要反复从用户态切换到内核态,效率更低
select函数的执行流程
- select是一个阻塞函数,当没有数据时,会一直阻塞在select函数那一行
- 当有数据时会将rset中对应的那一位置位
- select函数返回,不再阻塞
- 遍历文件描述符数组,判断哪个fd被置位了
- 读取数据,处理
select函数的缺点
- bitmap默认大小为1024,虽然可以调整但是有限度
- rset每次循环都需要重置,不可重复使用
- 尽管将rset从用户态拷贝到内核态由内核判断是否有数据,但还是有拷贝的开销
- 当有数据时select就会返回,但是select函数并不知道哪个文件描述符有数据了,后面还需要再次对文件描述符遍历
poll
struct pollfd{
int fd;
short events;
short revents;
}
/*----------*/
for(i = 0; i < 5; i++){
memset(&client, 0, sizeof(client));
addrlen = sizeof(client);
pollfds[i].fd = accept(socket, (struct sockaddr*)&client, &addrlen);
pollfds[I].events = POLLIN;
}
sleep(1);
/*----------*/
while(1){
puts("round again");
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);
}
}
}
poll函数的参数
- 自定义的结构体数组
- 数组的长度
- 超时时间
自定义结构体
- fd,文件描述符
- events,在意的事件是什么,读是POLLIN,写是POLLOUT,读和写都在意用或
- revents,对events的回馈,开始时为0,当有数据可读时就置为POLLIN,类似于上面的rset
poll函数的执行流程
- 将五个fd从用户态拷贝到内核态
- poll为阻塞方法,执行poll方法,如果有数据会将fd对应的revents置为POLLIN
- poll方法返回
- 循环遍历,查找哪个fd被置位为POLLIN了
- 将revents重置为0,便于复用
- 对置位的fd进行读取和处理
解决了select的哪些缺陷
- 使用数组,大小不止1024,解决了bitmap的大小限制
- 每次置位revents字段,revents可以恢复,解决了rset不可重用的情况
- 3和4的缺陷未被解决,因为两者原理相同
epoll
struct epoll_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(socket, (struct sockaddr*)&client, &addrlen);
ev.events = POLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
}
/*---------*/
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准备过程
- 利用epoll_create创建epfd,epfd相当于一个白板,用来作为epoll_wait的第一个参数,epoll_create的参数没有多大的实际意义,可以随意取值,用户态和内核态共享epfd
- 利用epoll_ctl对epfd进行配置,添加了五个fd-events的数据,没有revents
epoll的执行流程
- 当有数据的时候,会把相应的文件描述符置位,但是epoll没有revent标志位,并非真正的置位,而是将有数据的文件描述符放到队首
- epoll返回有数据的文件描述符的个数
- 根据返回的个数,读取前n个文件描述符即可
- 读取并处理