linux网络编程 IO多路复用 select epoll
本文以我的小型聊天室为例,对于服务器端的代码,做了三次改进,我将分别介绍阻塞式IO
,select
,epoll
.
一:阻塞式IO
对于聊天室这种程序,我们最容易想到的是在服务器端accept
之后,然后fork
一个进程或者pthread_create
创建一个线程去处理相应的连接,代码如下 :
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#define PORT 8888
#define MAX_QUEEN_LENGTH 5000
void my_err(const char* msg,int line)
{
fprintf(stderr,"line:%d",line);
perror(msg);
}
int main(int argc,char *argv[])
{
int i;
int conn_len;
int sock_fd,conn_fd;
struct sockaddr_in serv_addr,conn_addr;
char recv_buf[1024];
int pid;
if((sock_fd = socket(AF_INET,SOCK_STREAM,0)) == -1) { //第三个参数的意思为0表示自动选择与第二个参数对应的协议.
my_err("socket",__LINE__);
exit(1);
}
memset(&serv_addr,0,sizeof(struct sockaddr_in));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) == -1) {
my_err("bind",__LINE__);
exit(1);
}
if(listen(sock_fd,MAX_QUEEN_LENGTH) == -1) {
my_err("sock",__LINE__);
exit(1);
}
conn_len = sizeof(struct sockaddr_in);
while(1) {
conn_fd = accept(sock_fd,(struct sockaddr *)&conn_addr,&conn_len);
pid = fork();
if(pid == 0) {
//有关conn_fd的操作
}
}
return 0;
}
问题:
当连接变的较多时,我们创建进程或者线程的开销很大,并且系统在不同的线程之间切换处理也非常的耗费资源.因此我们想少开线程.于是,select
就是很好的选择,我们可以在一个线程内监控多个文件描述符的状态,下面我们来学习select.
二:select
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
//参数解释
nfds:是select监控的三个文件描述符集合中所包括的最大文件描述符加一
readfds:fd_set类型,用来检测输入是否就绪的文件描述符集合
writefds:同readfds,是用来检测输出是否就绪的文件描述符集合
exceptfds:检测异常情况是否发生文件描述符集合
timeout:设置select的等待时间,如果两个域都为0,则select不会阻塞.
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
//有关select监控文件描述符集合的操作
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd,fd_set *fdset); //将fd加入fdset集合中
void FD_CLR(int fd,fd_set *fdset); //将fd从fdset中清除
int FD_ISSET(int fd,fd_set *fdset); //判断fd是否在集合中
下面我们看有关select的代码
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#define PORT 8888
#define MAX_QUEEN_LENGTH 5000
void my_err(const char* msg,int line)
{
fprintf(stderr,"line:%d",line);
perror(msg);
}
int main(int argc,char *argv[])
{
int i;
int conn_len; //
int sock_fd,conn_fd;
struct sockaddr_in serv_addr,conn_addr;
char recv_buf[1024];
int pid;
struct timeval tv; //select第5个参数,可以确定轮询检查的时间
fd_set rfds; //select的检查集合
int conn_fd_array[MAX_QUEEN_LENGTH] = {0}; //客户端连接fd数组
int conn_amount = 0; //目前客户端连接的数量
int maxsock,ret; //maxsock是select的第一个参数
if((sock_fd = socket(AF_INET,SOCK_STREAM,0)) == -1) { //第三个参数的意思为0表示自动选择与第二个参数对应的协议.
my_err("socket",__LINE__);
exit(1);
}
memset(&serv_addr,0,sizeof(struct sockaddr_in));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) == -1) {
my_err("bind",__LINE__);
exit(1);
}
if(listen(sock_fd,MAX_QUEEN_LENGTH) == -1) {
my_err("sock",__LINE__);
exit(1);
}
conn_len = sizeof(struct sockaddr_in);
maxsock = sock_fd; //目前的maxsock就是listen socket
while(1) {
FD_ZERO(&rfds); //清空集合,因为select每次都需要重新检查,所以清空操作每次都需要
FD_SET(sock_fd,&rfds); //将listen socket加入rfds集合中
tv.tv_sec = 1; //设置时间为1s,即1s后无事件发生就返回
tv.tv_usec = 0;
for(i = 0;i < MAX_QUEEN_LENGTH;i++) {
if(conn_fd_array[i] != 0) {
FD_SET(conn_fd_array[i],&rfds); //将所有都设置
}
}
ret = select(maxsock+1,&rfds,NULL,NULL,&tv);
if(ret < 0) {
my_err("select",__LINE__);
exit(1);
}
for(i = 0;i < conn_amount;i++) { //conn_amount:连接数量
if(FD_ISSET(conn_fd_array[i],&rfds)) {
if(recv(conn_fd_array[i],recv_buf,1024,0) != 1024) {
close(conn_fd_array[i]); //关闭文件描述符
FD_CLR(conn_fd_array[i],&rfds); //从rfds中清除
conn_fd_array[i] = 0; //让fd数组中归零
} else {
printf("%s\n",recv_buf);
}
}
}
if(FD_ISSET(sock_fd,&rfds)) { //如果有新的连接
if((conn_fd = accept(sock_fd,(struct sockaddr *)&conn_addr,&conn_len)) <= 0 ) {
my_err("accept",__LINE__);
exit(1);
}
for(i = 0;i < MAX_QUEEN_LENGTH;i++) { //实现了conn_fd_array[i]中元素重复使用
if(conn_fd_array[i] == 0) {
conn_fd_array[i] = conn_fd;
break;
}
}
conn_amount++;
if(conn_fd > maxsock) { //始终保证传给select的第一个参数是maxsock+1
maxsock = conn_fd;
}
}
}
return 0;
}
问题:
select虽然减小了开销,但是由于文件描述符集合有一个最大容量限制,由常量FD_SETSIZE来限制,在linux上,此值一般为1024,我们无法轻易的修改它,因此select的连接限制一般就是1024以下,这显然是不够用的,因此对于需要大量连接,需要大量文件描述符的情况,epoll更加有用.
三:epoll
epoll的核心数据结构是epoll实例,它和一个打开的文件描述符相关联,这个文件描述符不是用来做IO操作的,它是内核数据结构的句柄.epoll主要有下面三个API
#include<epoll.h>
int epoll_create(int size);
参数:size指定了我们想要通过epoll案例来检查的文件描述符个数
返回值:代表创建的epoll实例的文件描述符
int epoll_ctl(int epfd,int op,int fd,struct epoll *ev);
epfd:是epoll_create函数返回的代表epoll实例的文件描述符
op:定义了需要执行的操作
EPOLL_CTL_ADD:将描述符fd添加到epoll实例epfd的兴趣列表中去.
EPOLL_CTL_MOD:修改描述符fd上设定的事件,需要用到由ev所指向的结构体的信息.
EPOLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中删除.
events:定义了下面的操作
EPOLLIN:读操作
EPOLLOUT:写操作
EPOLLRDHUP:套接字对端关闭
EPOLLLONESHOT:在完成事件通知之后禁用检查
//event的结构体
struct epoll_event {
uint32_t events;
epoll_data_t data;
};
typedef union epoll_data {
void *ptr;
int fd; //文件描述符
unit32_t u32;
unit64_t u64;
}epoll_data_t;
int epoll_wait(int epfd,struct epoll_event *evlist,int maxevents,int timeout);
epfd:是epoll_create函数返回的代表epoll实例的文件描述符
evlist:所指向的结构体数组中返回的是有关就绪状态文件描述符的信息
maxevents:最大的连接数
timeout:
-1:调用将一直被阻塞,直到兴趣列表中文件描述符上有事件发生.
0:执行一次非阻塞的检查,看兴趣列表中文件描述符上产生了哪个事件
>0:阻塞最多timeout毫秒,直到文件描述符上有事件发生.
下面我们看epoll的使用代码
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/epoll.h>
#define PORT 8888
#define MAX_LENGTH 1024
void my_err(const char* msg,int line)
{
fprintf(stderr,"line:%d",line);
perror(msg);
exit(1);
}
int main(int argc,char *argv[])
{
int i;
int listen_fd,conn_fd,sock_fd;
struct sockaddr_in serv_addr,conn_addr;
struct epoll_event ev,events[MAX_LENGTH] = {-1};
int nfds,epollfd;
int conn_len = sizeof(struct sockaddr_in);
char recv_buf[1024];
if((listen_fd = socket(AF_INET,SOCK_STREAM,0)) == -1) {
my_err("socket",__LINE__);
}
memset(&serv_addr,0,sizeof(struct sockaddr_in));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listen_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) == -1) {
my_err("bind",__LINE__);
}
if(listen(listen_fd,MAX_LENGTH) == -1) {
my_err("listen",__LINE__);
}
if((epollfd = epoll_create(MAX_LENGTH)) == -1) { //创建一个epoll实例
my_err("epoll",__LINE__);
}
ev.events = EPOLLIN; //设置事件为读
ev.data.fd = listen_fd; //设置文件描述符为监听套接字
epoll_ctl(epollfd,EPOLL_CTL_ADD,listen_fd,&ev); .//将监听套接字加入epollfd
while(1) {
nfds = epoll_wait(epollfd,events,1024,500); //event是一个数组
for(i = 0;i < nfds;i++) {
if(events[i].data.fd == listen_fd) { //连接请求
conn_fd = accept(listen_fd,(struct sockaddr *)&conn_addr,&conn_len);
printf("accept a new collection : %s\n",inet_ntoa(conn_addr.sin_addr));
ev.data.fd = conn_fd;
ev.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,conn_fd,&ev);
}
else if(events[i].events & EPOLLIN) { //读事件
if((sock_fd = events[i].data.fd) < 0 ) {
continue;
}
if((i = recv(sock_fd,recv_buf,1024,0)) != 1024) {
close(sock_fd);
events[i].data.fd = -1;
} else {
printf("%s\n",recv_buf);
}
ev.data.fd = sock_fd;
ev.events = EPOLLOUT;
epoll_ctl(epollfd,EPOLL_CTL_MOD,sock_fd,&ev);
}
else if(events[i].events & EPOLLOUT) { //写事件
sock_fd = events[i].data.fd;
ev.data.fd = sock_fd;
ev.events = EPOLLIN; epoll_ctl(epollfd,EPOLL_CTL_MOD,sock_fd,&ev);
}
}
}
return 0;
}
下面我们看下epoll和select的性能对比表(来自Linux/Unix 系统编程手册)
被监视的文件描述符数量 | select占用CPU时间(秒) | epoll占用CPU时间(秒) |
---|---|---|
10 | 0.73 | 0.41 |
100 | 3.0 | 0.42 |
1000 | 35 | 0.53 |
10000 | 930 | 0.66 |
因此我们不难看出epoll
的强大,它更加适合需要同时处理许多客户端的服务器,特别是需要监控的文件描述符数量巨大,但是大多数处于空闲状态,只有少部分处于就绪状态.
select和epoll性能差别原因:
- 每次调用select,内核都必须检查所有被指定的文件描述符,当大量检查时,耗费时间大.
- 每次调用select,程序都必须传递一个表示所有被检查的文件描述符到内核,内核通过检查文件描述符后,修改这个数据结构返回给程序,但是内核态和用户态之间切换的效率非常低.
- select完成之后,之后的程序必须检查返回的数据结构中的每一个元素.这样每次循环消耗非常大.
linux网络编程 IO多路复用 select epoll的更多相关文章
- python 网络编程 IO多路复用之epoll
python网络编程——IO多路复用之epoll 1.内核EPOLL模型讲解 此部分参考http://blog.csdn.net/mango_song/article/details/4264 ...
- python网络编程——IO多路复用之epoll
1.内核EPOLL模型讲解 此部分参考http://blog.csdn.net/mango_song/article/details/42643971博文并整理 首先我们来定义流的概念,一个流 ...
- python网络编程——IO多路复用select/poll/epoll的使用
转载博客: http://www.haiyun.me/archives/1056.html http://www.cnblogs.com/coser/archive/2012/01/06/231521 ...
- python网络编程——IO多路复用之select
1 IO多路复用的概念 原生socket客户端在与服务端建立连接时,即服务端调用accept方法时是阻塞的,同时服务端和客户端在收发数据(调用recv.send.sendall)时也是阻塞的.原生so ...
- Linux网络编程-IO复用技术
IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了.Linux中,提 ...
- linux网络编程IO模型
同步与异步: 同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成. 异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要 ...
- linux 网络编程 3---(io多路复用,tcp并发)
1,io模型: 阻塞io.非阻塞io.io多路复用,信号驱动io. 阻塞Io与非阻塞io的转换,可用fcntl()函数 #include<unistd.h> #include<fcn ...
- Linux 网络编程九(select应用--大并发处理)
//网络编程服务端 /* * 备注:因为客户端代码.辅助方法代码和epoll相同,所以select只展示服务器端代码 */ #include <stdio.h> #include < ...
- LINUX网络编程 IO 复用
参考<linux高性能服务器编程> LINUX下处理多个连接时候,仅仅使用多线程和原始socket函数,效率十分低下 于是就出现了selelct poll epoll等IO复用函数. 这 ...
随机推荐
- Docker系列(15)- Commit镜像
docker commit 提交容器成为一个新的副本,有点像套娃 # 命令和git原理类似 docker commit -m="提交的描述信息" -a="作者" ...
- javascript 数组 shuffle 洗牌 打乱顺序
* php shuffle 打乱数组顺序 Array.prototype.shuffle = function () { "use strict"; var a = [], b = ...
- centos7.X 系统初始化>>优化
1 修改网卡为eth0 cd /etc/sysconfig/network-scripts/ vim ifcfg-eno16777729TYPE=EthernetBOOTPROTO=staticIPA ...
- java 工具类 验证码
第一步: 引入工具类 工具类一: import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import jav ...
- MySQL&ES连接池
数据库的连接池建议放在类似settings.py的配置模块中,因为基本都是配置项,方便统一管理. 1) 连接池类#settings.py import os from DBUtils.PooledDB ...
- django url配置-反向解析-视图函数-HttpRequest对象-HttpResponse对象-cookies-session-redis缓存session
""" --视图概述:-- 作用:视图接受WEB请求,并响应WEB请求 本质:视图就是一个python中的函数 响应: 1.网页: 一.重定向 二.错误视图 400,50 ...
- display:flex;下的子元素width无效问题
因为flex属性默认值为flex:0 1 auto;其中 1 为 flex中的 flex-shrink 属性. 该属性介绍: 一个数字,规定项目将相对于其他灵活的项目进行收缩的量. 根据上述介绍可以理 ...
- P3645-[APIO2015]雅加达的摩天楼【bfs,根号分治】
正题 题目链接:https://www.luogu.com.cn/problem/P3645 题目大意 \(n\)个点,\(m\)条狗,第\(i\)条狗可以往左或者右跳恰好\(p_i\)步,开始是\( ...
- Windows下CMake编译安装OpenCV
Windows下CMake编译安装OpenCV 这是一个面向新手的在windows上运进opencv, helloword的教程. 在这里我们使用vs2019来编译opencv, 并运行一个hello ...
- 在CentOS 6中安装和配置OrientDB社区版
OrientDB概述: OrientDB是一个开源NoSQL非关系型数据库管理系统. NoSQL数据库提供了一种用于存储和检索引用除表式数据之外的数据(例如文档数据或图形数据)的NO关系或非关系数据的 ...