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复用函数. 这 ...
随机推荐
- VMware虚拟机常见问题(针对目前我所学的而言,还会不断更新)
VMware虚拟机常见问题(针对目前我所学的而言,还会不断更新) 自己电脑的telnet Client是否打开 在控制面板->程序->打开或关闭Windows功能 虚拟机的telnet是否 ...
- 鸿蒙内核源码分析(信号消费篇) | 谁让CPU连续四次换栈运行 | 百篇博客分析OpenHarmony源码 | v49.04
百篇博客系列篇.本篇为: v49.xx 鸿蒙内核源码分析(信号消费篇) | 谁让CPU连续四次换栈运行 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁 ...
- sonarqube C# 单元测试覆盖率一栏总是0%解决办法
一.什么叫单元测试(unit testing)? 是指对软件中的最小可测试单元进行检查和验证.对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函 数,Java里 ...
- String(char[] value, boolean share) {
记录一下今天被蠢到 1. 在观察StringBuffer类的toString方法时,发现了个没见过的方法 return new String(toStringCache, true); @Overri ...
- MySQL学习总结:提问式回顾 undo log 相关知识
原文链接:MySQL学习总结:提问式回顾 undo log 相关知识 1.redo 日志支持恢复重做,那么如果是回滚事务中的操作呢,也会有什么日志支持么? 也回滚已有操作,那么就是想撤销,对应的有撤销 ...
- Node.js躬行记(12)——BFF
BFF字面意思是服务于前端的后端,我的理解就是数据聚合层.我们组在维护一个后台管理系统,会频繁的与数据库交互. 过去为了增删改查会写大量的对应接口,并且还需要在Model.Service.Router ...
- Spring动态代理的生成-如何判断是使用JDK动态代理还是CGlib代理
前言 在上一篇文章中讲到了Spring是如何获取对应的Bean的增强,然后本次主要讲解一下Spring如何在获取到增强后创建Spring代理的. 在步入正题之前先给大家看一下Spring创建代理的大致 ...
- Java基础之(十一):方法
Java方法详解 何谓方法(了解) System.out.println() // 类 对象 方法 方法是语句的集合,它们在一起执行一个功能. 方法是解决一类问题的步骤的有序集合 方法包含于类或对象中 ...
- 题解 2020.10.24 考试 T3 数列
题目传送门 题目大意 给出一个数 \(n\),你要构造一个数列,满足里面每个数都是 \(n\) 的因子,且每一个数与前面不互质的个数不超过 \(1\).问有多少种合法方案. 保证 \(n\) 的不同质 ...
- FastAPI 学习之路(八)路径参数和数值的校验
系列文章: FastAPI 学习之路(一)fastapi--高性能web开发框架 FastAPI 学习之路(二) FastAPI 学习之路(三) FastAPI 学习之路(四) FastAPI 学习之 ...