当前位置: 首页 > news >正文

客户都不愿意做网站百度图片搜索引擎入口

客户都不愿意做网站,百度图片搜索引擎入口,wordpress用户管理解决方案,网站怎样做推广文章目录 概要一、多路复用I/O模型的诞生1.1 多线程或进程方式1.2 通过数组,链表等方式保存socket fd,不断轮询 二、select三、poll四、epoll五、小结六、参考 概要 在Unix五种I/O模型一文中,提到了I/O多路复用模型,其在Linux下有…

文章目录

    • 概要
    • 一、多路复用I/O模型的诞生
        • 1.1 多线程或进程方式
        • 1.2 通过数组,链表等方式保存socket fd,不断轮询
    • 二、select
    • 三、poll
    • 四、epoll
    • 五、小结
    • 六、参考

概要

在Unix五种I/O模型一文中,提到了I/O多路复用模型,其在Linux下有3种实现方式:select、poll、epoll,本文主要深入介绍下它们各自特点。

事先说明:I/O多路复用模型,select和poll核心就是【轮询+内核I/O事件就绪通知】,epoll的核心是内核I/O事件就绪通知。

多路:多个socket连接(即多个客户端连接)
复用:允许内核监听多个socket描述符,一旦发现进程指定的一个或多个scoket的I/O事件就绪(TCP三次握手成功、可读,可写),就通知该进程

要想更好的了解,最好根据代码来说,下面是代码的基本框架:

void main(int argc, char **argv)
{int listenfd, connfd;struct sockaddr_in srv_addr;//创建socket套接字if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);return;}//设置绑定地址的内容memset(&srv_addr, 0, sizeof(srv_addr));srv_addr.sin_family = AF_INET; //ipv4srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//ip 0.0.0.0srv_addr.sin_port = htons(8888); //端口//绑定地址if (bind(listenfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) == -1){printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);return;}//listenif (listen(listenfd, SOMAXCONN) == -1) //指定监听的套接字描述符、TCP半连接和全连接队列大小{printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);return;}//【在这个区域分别使用阻塞,多线程,select,poll,epoll等多种方式实现连接】close(listenfd); //关闭listen socketreturn;
}

一、多路复用I/O模型的诞生

之所以诞生多路复用I/O模型,肯定是旧的I/O模型无法满足需要了,首先回顾下基础的阻塞I/O模型:

代码如下:

    char buff[MAXLNE];int n;struct sockaddr_in cli_addr;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&cli_addr, &len)) == -1){printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return;}while(1){n = recv(connfd, buff, MAXLNE, 0); //阻塞if (n > 0){//加上字符串的尾部,以便显示和转发buff[n] = '\0';printf("recv msg: %s \n", buff);send(connfd, buff, n, 0);}else if (n == 0){close(connfd); //关闭client socket连接}else //n==-1{printf("recv errno: %d\n", errno);}}

此时while在accept API之后,那么只能处理一个client,并维持长连接。
那么while在accept API之前会如何呢,显而易见,此时能处理多个client,但只能处理每个client一条消息,不能未出长连接。

如果想与多个client维持长连接,该如何做呢?于是基础阻塞I/O模型有了以下两种方式:

  • 多线程或进程;
  • 通过数组,链表等方式保存socket fd,不断轮询;
1.1 多线程或进程方式

代码如下(以多进程为例):

	signal(SIGCHLD, sig_child); //注册子进程退出处理函数pid_t child_pid;while(1){struct sockaddr_in cli_addr;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&cli_addr, &len)) == -1){printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return;}if ((child_pid = fork()) == 0) //为每个client派生一个子进程处理{ close(listenfd);str_echo(connfd);exit(0);}}

子进程处理客户端请求函数

void str_echo(int connfd)
{int n;char buff[MAXLNE];again:while ((n = recv(connfd, buf, MAXLINE)) > 0){printf("recv msg: %s \n", buff);send(sockfd, buf, n);}if (n < 0 && errno == EINTR){goto again;}	else if (n == 0){close(connfd); //结束return;  //退出进程}else //n==-1{printf("recv errno: %d\n", errno);}
}

子进程退出函数

void sig_child(int signo)
{pid_t pid;int stat;//等待所有子进程退出while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)printf("child %d exit\n", pid);return;
}

但是这方式有个弊端,每个client由一个进程/线程去处理,系统开销相当大,很难维持大量客户端。

1.2 通过数组,链表等方式保存socket fd,不断轮询

这种模式是将客户端socket fd通过数组,链表等方式保存下来,然后不断地轮询,如果客户端太多,轮询也是很慢的。

伪代码如下:

int client_fds[FD_SETSIZE];
int sockfd;
while(1)
{int connfd = accept() //阻塞for (i = 0; i < FD_SETSIZE; i++) { if (client[i] < 0) {client_fds[i] = connfd;break;}}for (i = 0; i < FD_SETSIZE; i++) {if ((sockfd = client_fds[i]) <= 0){continue; }n = recv(sockfd, buf, MAXLINE)//阻塞if(n > 0){printf("recv msg: %s \n", buff);send(sockfd, buf, n);}else if (n < 0 ){printf("recv fd:%d, errno: %d\n", sockfd, errno);}	else// n == 0{close(connfd); //结束client_fds[i] = 0; //标记一下}	}
}  

可以看到通过client_fds数组将客户端连接的描述符保存下来,后续对其进行轮询,来达到与多个client维持长连接的目的。
但accept,recv等函数都是阻塞的,如果此时I/O事件(比如accept的TCP三次握手成功,recv的可读,send的可写)没就绪,那岂不永远卡死了。所以我们需要一种机制,告诉我们client_fds数组和监听socket listenfd中哪些socket有I/O就绪事件,基于此多路复用I/O模型诞生了,没错,该模型本质就是告诉进程哪些socket有I/O就绪事件,然后我们基于此去轮询那些有I/O就绪事件的scoket,这样就不会卡住了。

PS:这种方式是没有实际应用的,主要是为了引出多路复用I/O模型。

那select、poll、epoll是如何告诉进程哪些socket有I/O就绪事件呢?下面依次探究下吧。

二、select

select api函数(还有个pselect函数,不是很常用,不过二者核心逻辑是一样的):

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

参数:
nfds:最大的文件描述符数量加1的值
readfds:监听可读集合
writefds:监听可写集合
exceptfds:监听异常集合
timeout:超时时间
返回值:
大于0,表示I/O就绪事件的socket描述符个数;
等于0表示没有描述符有状态变化,并且调用超时;
小于0表示出错,此时全局变量errno保存错误码。

我们使用Glibc库select或pselect函数时,都会走Linux内核do_select函数,可以看到本质就是轮询readfds、writefds、exceptfds这三个集合,每次调用会轮询两次:

  1. 第一次会将当前进程(本质是I/O事件就绪时的回调函数)加入到readfds、writefds、exceptfds这三个集合中socket的等待队列中(socket有I/O事件就会触发回调函数),然后就通过poll_schedule_timeout函数挂起;
  2. 一旦有某个socket描述符I/O事件就绪,会立即通知进程,就会开始第二次轮询,本次轮询会确定readfds、writefds、exceptfds这三个集合中到底是哪些socket描述符有就绪的I/O事件;
  3. 明明两次,为啥会有三呢,这里主要是在do_select最后会调用一次poll_freewait函数,该函数会将当前进程从readfds、writefds、exceptfds这三个集合中socket的等待队列中移除。

从上面描述就可以看出,select主要工作:维护所有socket的监听(添加【步骤1】和移除【步骤三】)、判定是否有I/O事件就绪的socket
该方式虽相比基于阻塞I/O的多进程/线程方式能更便捷的实现与多个客户端维持长连接了,但缺点多多:

  1. 由于无法准确识别哪些socket描述符I/O事件就绪,所以会进行无差别轮询,时间复杂度O(N);
  2. Linux下readfds、writefds、exceptfds这三个集合大小默认1024,所以维持长连接的客户端数量是有限的,源码如下:
    typedef __kernel_fd_set fd_set
    __kernel_fd_set
#undef __FD_SETSIZE
#define __FD_SETSIZE	1024typedef struct {unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

可以看到无论32位还是64位操作系统下fds_bits大小都是1024位,注意fd_set集合是通过位运算标识第几个scoket描述符有无I/O事件。

  1. 每次调用select函数,都需要把所有fd_set从用户空间拷贝到内核空间,如果fd_set比较大,对性能影响就非常大;

优点:

  1. 相比基于阻塞I/O的多进程/线程方式,更便捷的实现与多个客户端维持长连接;
  2. 相比非阻塞I/O,主动轮询socket是否有I/O事件,调整为等待内核通知,这样一次系统调用就实现多个client事件的管理,更有优势。

综合来看相比基于阻塞I/O的多进程/线程方式优势并不大。

三、poll

poll api函数:

struct pollfd {int   fd;        //要监听的文件描述符short events;    //要监听的事件short revents;   //事件结果
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数:
fds:这是一个数组,每一个数组元素表示要监听的文件描述符以及相应的事件
nfds:数组的个数
timeout:超时时间
返回值:
大于0:表示结构体数组fds中有fd描述符的状态发生变化,或可以读取、或可以写入、或出错。并且返回的值表示这些状态有变化的socket描述符的总数量;此时可以对fds数组进行遍历,以寻找那些revents不空的描述符,然后判断这个里面有哪些事件以读取数据。
等于0:表示没有描述符有状态变化,并且调用超时。
小于0:此时表示有错误发生,此时全局变量errno保存错误码。

我们使用Glibc库poll函数时,会走Linux内核do_sys_poll和do_poll函数,可以看到:

  • do_sys_poll函数会将fds数组转化为struct poll_list链表,根据代码可知结构如下图:
    poll_list结构图
  • do_poll函数被 do_sys_poll调用,其核心逻辑与do_select差不多,每次被调用会轮询两次:
    1):第一次会将当前进程(本质是I/O事件就绪时的回调函数)加入到struct poll_list链表中socket的等待队列中(socket有I/O事件就会触发回调函数),然后就通过poll_schedule_timeout函数挂起;
    2):一旦有某个socket描述符I/O事件就绪,会立即通知进程,就会开始第二次轮询,本次轮询会确定struct poll_list链表中到底是哪些socket描述符有就绪的I/O事件。
  • do_sys_poll调用do_poll函数结束后,还有一次循环,即poll_freewait函数,此时会将当前进程从struct poll_list链表中socket的等待队列中移除。

从上面描述就可以看出,相比select,唯一改进的地方在于没有了最大连接数的限制,但相应的也需要关注随着客户端连接数增加,轮询的效率和会急剧下降的问题。

四、epoll

epoll全名event poll,其api函数有:

  1. epoll_create,创建一个epoll实例struct eventpoll,其实其返回一个int值,可以称之为epoll描述符,Linux内核会管理该值与具体epoll实例的映射关系。对应内核源码do_epoll_create
//epoll 结构体,即epoll实例
struct eventpoll {/* 等待队列头,被sys_epoll_wait使用 */wait_queue_head_t wq;/* 保准准备就绪的文件描述符的一个链表 */struct list_head rdllist;/* 红黑树节点,epoll使用红黑树存储事件信息 */struct rb_root rbr;...
};
int epoll_create(int size);

参数:
size:一个int值,实际没有任何用,只要不小于等于0即可;
返回值:
小于0表示错误,此时全局变量errno保存错误码

  1. epoll_ctl,添加、删除、修改事件。对应内核源码do_epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数:
epfd:epoll_create函数的返回值,即epoll描述符;
op:事件类型,EPOLL_CTL_ADD(添加事件)、EPOLL_CTL_MOD(修改事件)、EPOLL_CTL_DEL(删除事件);
fd: socket描述符;
event:告诉内核需要监听哪个事件。
返回值:
小于0表示错误,此时全局变量errno保存错误码

  1. epoll_wait,等待I/O事件。对应内核源码do_epoll_wait
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

参数:
epfd:epoll_create函数的返回值,即epoll描述符;
events:表示准备就绪的事件数组;
maxevents: 要等待的最大事件数;
timeout:超时时间。
返回值:
大于0:表示事件就绪的socket个数。
等于0:表示没有描述符有状态变化,并且调用超时。
小于0:此时表示有错误发生,此时全局变量errno保存错误码。

根据api就可以观察出,相比select和poll,epoll拆成了三个,实际上是两个epoll_ctl和epoll_wait。即select和poll是将维护监听scoket描述符等待I/O事件合为一体的,epoll拆开了,epoll_ctl来维护监听scoket描述符,epoll_wait来等待I/O事件`。

为什么要这样做呢,在分析select和epoll缺点时,其实主要集中两点:

1:轮询:每次调用select或poll时都需要将进程加入到所有socket的等待队列中,等到socket有I/O事件立马唤醒进程,此时会轮询整个socket列表以确定哪些socket有I/O事件,最后会将进程从每个socket等队列中移除。这里涉及到对socket列表的三次轮询,随着socket列表的增加,会造成性能急剧下降的。
2:复制:每次调用select或poll时都要将整个socket列表从用户区复制到内核区,也是有一定开销的。

知道了缺点,该如何解决呢?

1:针对每次调用需要将将进程加入到所有socket的等待队列中,最后将进程从每个socket等队列中移除的操作,这部分交由epoll_ctl处理,按需对某个socket进行EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL操作;
2:针对每次调用都要将整个socket列表从用户区复制到内核区的问题,这部分交由epoll_create创建的epoll实例替代,epoll_ctl通过EPOLL_CTL_ADD操作,会被插入到epoll实例的红黑树中,也就是说只会被复制一次;
3:针对进程被唤醒后,还需要轮询整个socket列表以确定哪些socket有I/O事件的问题(即不知道哪些socket的I/O事件就绪,只能一个个遍历),调整成每个socket有I/O事件时会将该socket加入到epoll实例的rdllink双向链表中,进程调用epoll_wait时只需判断rdllink双向链表长度即可,大于0立即返回,等于0就挂起进程,等待被某个socketI/O事件唤醒。

所以说epoll通过对select/poll功能的拆分,解决了前两者的缺点,相对于前两者优点如下:

  1. 没有最大并发连接的限制,当然了还受操作系统限制,比如资源、配置等等;
  2. 性能高,没有了轮询,不会随着socket数量的增加而导致性能下降。
  3. epoll支持边缘触发(EPOLLET)和水平触发(EPOLLLT),前两者仅支持水平触发。

缺点:
目前缺点就是进程去读写I/O事件就绪的socket时,还需要将数据从内核区复制到用户区,当然了select/poll也有该问题,这一点需要异步I/O去解决了。目前epoll是够用的,Nginx,Redis都是基于epoll的,足以应对处理C10K,C100K问题。

LT(level triggered) 是 缺省 的工作方式 ,并且同时支持 block 和 no-block I/O。 在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,下次内核还会继续通知你的,所以,这种模式编程出错误可能性要小一点。 select/poll 都是这种模型的代表。
ET(edge-triggered) 是高速工作方式 ,只支持 no-block I/O 。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了 ( 比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个 EWOULDBLOCK 错误)。但是请注意,如果一直不对这个 socket做 IO 操作 ( 从而使它再次变成未就绪 ) ,内核不会发送更多的通知 (only once), 不过在 TCP 协议中, ET 模式的加速效用仍需要更多的 benchmark 确认。
Epoll 工作在 ET 模式的时候,必须使用非阻塞I/O,以避免由于一个文件句柄的阻塞读 / 阻塞写操作把处理多个文件描述符的任务饿死。

五、小结

select&poll&epoll区别
从整体来看,epoll的实现性能是比select/poll更好的,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调,一般情况下选epoll就对了。

本人研究Redis相关源码时,观察到其在Linux下就是通过epoll+EPOLLET+非阻塞I/O+Reactor设计模式组合来处理I/O事件,是其高性能的一个关键点。

六、参考

1]:Linux下实现单客户连接的tcp服务端
2]:从网络I/O模型到Netty,先深入了解下I/O多路复用
3]:epoll的本质
4]:深入了解epoll模型
5]:Linux epoll内核源码剖析

http://www.dinnco.com/news/38456.html

相关文章:

  • 唐山建设工程信息网站河南关键词优化搜索
  • 华为官方网站手机商城首页拉人注册给佣金的app
  • 邢台如何做企业网站网站流量统计查询
  • 盐城网站建设方案网站信息组织优化
  • 网站开发技术有什么痘痘该如何去除效果好
  • 产品展示网站源码个人如何注册网站
  • 网站建设鼠标移动变颜色app推广兼职是诈骗吗
  • 开天猫旗舰店网站建设seo全国最好的公司
  • 制作自己专属头像关键词优化推广公司
  • 网站建设申请书苏州百度推广代理商
  • ps如何做音乐网站seo的定义是什么
  • 网站用空间还是服务器管理培训机构
  • 做导购网站赚钱医院线上预约
  • 青岛如何做网站seo外贸网站搭建推广
  • 做水果苹果大的网站最新全国疫情消息
  • 牡丹江到林口外贸网站优化推广
  • 禁用wordpress编辑器seo搜索引擎优化方法
  • 建设什么网站网络销售有哪些
  • 怎样做网站api接口域名查询工具
  • 网站开发合作保密协议短信广告投放软件
  • 如东做网站公司全网搜索指数查询
  • 怎样进入国外网站地推的60种方法
  • 小蘑菇网站开发360关键词推广
  • 重庆做营销型网站建设公司申请一个网站需要多少钱
  • 药品推广方案策划北京seo软件
  • web后端是做网站的么网站如何seo推广
  • 怎么样宣传自己的网站seo推广策略
  • 长沙设计网站排名一句简短走心文案
  • 服务器可以吧网站做跳转吗吸引人的营销标题
  • 政府网站建设工作情况汇报软文台