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复用函数. 这 ...
随机推荐
- Appium WebView控件定位
背景 移动应用可以粗分为三种:原生应用(native app), 网页应用(web app,或HTML5 app),以及它们的混血儿--混合模式移动应用(hybrid app). 什么是Hybrid ...
- AVS 端能力之音频播放模块
功能简介 音频播放 音频流播放 URL文件播放 播放控制 播放 暂停 继续 停止 其它功能(AVS服务器端实现) 支持播放列表 支持上一首下一首切换 支持电台 事件指令集 AudioPlayer 端能 ...
- 基于SpringBoot+Mybatis plus+React.js实现条件选择切换搜索功能
笔记/朱季谦 在写React前端逻辑时,经常遇到可以切换不同条件的列表查询功能,例如下边截图这样的,其实,这块代码基本都一个逻辑,可以一次性将实现过程记录下来,待以后再遇到时,直接根据笔记复用即可. ...
- vector 的交换技巧
面试被问到如何解决 vector 有过多空闲内存的问题. 假定先有一 vector 容器 vec,它的容量是 10000,大小是 3. vector 的内存增长问题 vector 申请的是连续内存空间 ...
- mysql从零开始之MySQL 创建数据库
MySQL 创建数据库 我们可以在登陆 MySQL 服务后,使用 create 命令创建数据库,语法如下: CREATE DATABASE 数据库名; 以下命令简单的演示了创建数据库的过程,数据名为 ...
- 初步认识HCIA,什么是计算机网络,拓扑,网络的发展,交换机,路由器,IP,光纤,带宽,广播,ARP......
HCIA ---- 华为认证初级网络工程师 云技术 --- 云存储 云计算 计算机技术 : --- 抽象语言 -- 电线号的转换 抽象语言 -- 编码 ---- 应用层 编码 --- 二进制 -- ...
- 洛谷4455 [CQOI2018]社交网络 (有向图矩阵树定理)(学习笔记)
sro_ptx_orz qwq算是一个套路的记录 对于一个有向图来说 如果你要求一个外向生成树的话,那么如果存在一个\(u\rightarrow v\)的边 那么\(a[u][v]--,a[v][v] ...
- C#开发BIMFACE系列47 IIS部署并加载离线数据包
BIMFACE二次开发系列目录 [已更新最新开发文章,点击查看详细] 在前两篇博客<C#开发BIMFACE系列45 服务端API之创建离线数据包>与<C#开发BIMFACE系 ...
- 基本的bash shell命令
目录 基本的bash shell命令 启动shell shell提示符 基本的bash shell命令 启动shell GNU bash shell 能提供对Linux系统的交互式访问.它是作为普通程 ...
- VUE中v-for更新检测
口诀: 数组变更方法,就会导致 v-for 更新,页面更新 数组非变更方法:返回新数组,就不会导致 v-for 更新,更新值检测不到可采用覆盖或者 this.$set() 数组变更方法如下: 1. a ...