多路复用I/O-select

多路复用IO

第一章 高性能服务器技术栈 (select)
第二章 高性能服务器技术栈 (epool/poll)



前言

现在网络技术越来月普及,网络充斥了我们的生活当中,网络场景越来越复杂。在网络开发中,高并发的情景是在后端开发中司空见惯的场景。掌握并发技术是后端开发中的基本能力。目前实现并发的技术,主要有借助于协程,进程实现;多路复用I/O(select, poll, epool)等。
多路复用(IO multiplexing) 在网络层通常是指select/epoll。 但是有些地方
也称这种IO 方式为事件驱动IO(event driven IO)。


一、并发基础

实现并发的技术主要有select,poll ,epoll 等技术,它们各有优缺点(它们在内核中的实现其它章节讲解)。select 技术是通过轮询遍历的方式处理网路就绪IO。epool 是通过网络事件的方式处理网络就绪IO 。具体比较其它章节讲解

1.1 select 技术

select 引入了集合(fd_set)概念Select采用一个bit表,每个fd对应表中的一个bit位,采用此方式存储放入的fd.通过轮询检测集合中的句柄。等待检测的fd 准备就绪了(fd 已经是非阻塞状态)。然后返回就绪的文件描述符的个数。在进行进一步处理。IO有三种状态,可读,可写,异常。
接口函数

/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

/**常用的select 函数*/
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

函数接口分别分析*

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

参数解析

maxfd:文件描述符的范围,比监听的最大文件描述符加1。select最多同时监听描述符 数量有个上限,FD_SETSIZE(1024);
readfds:可读:它是指向fd_set结构的指针,fd_set是一个描述符集合。这个集合中是要监控的读类型的文件的描述符。
writefds:可写:它也是指向fd_set结构的指针,这个集合是要监控的写类型的文件的描述符。
errorfds:异常:它是用来监视文件错误异常的文件描述符的集合。
timeout:它是select函数的超时时间,这个参数至关重要,它可以使select处于三种状态。
	  1,timeout=NULL 阻塞: 若将NULL以形参传入,即不传入时间结构,则select一直置于阻塞状态,直到监控到文件描述符集合中某个文件描述符发生变化为止;
	  2,timeout所指向的结构,时间设为0 非阻塞 :若将时间值设为0秒0毫秒,就变成一个纯粹的 非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;	 
	  3,timeout所指向的结构设为非零时间: timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后返回0。

函数的返回值:
1.在正常情况下返回就绪的文件描述符个数;
2.时间超时返回0;
3.出错或者select被某个信号中断返回-1;

NODE

  1. 每次调用select后,都需要重新清空描述符集并重新添加感兴趣的文件描述符。
    2.select返回时会将剩余时间填充到timeout参数中,因此重新调用select的时候也要重新初始化该时间参数;
  2. 添加到fd_set中的fd的数目必须小于FD_SETSIZE,否则就会越界。

检测越界工具:

使用valgrind和purify等内存检测工具。

API 接口

void FD_CLR(inr fd, fd_set *fdset);用来清除描述符集合fdset中的描述符fd

int FD_ISSET(int fd,f d_set *fdset);用来检测描述符集合fdset中的描述符fd是否存在, 返回值: 存在返回1,不存在返回0

void FD_SET(int fd, fd_set *fdset);用来将描述符fd添加到描述符集合fdset中

void FD_ZERO(fd_set *fdset);用来清除描述符集合fdset,把集合置0;

详解:
fd_set 定义的位表中,有两类fd, listenfd 和 rwfd;

1.2 select 结构体分析

fdset 是一个 fd 的位图,采用数组的形式来存储。每个 bit 位代表一个 fd,0表示该 fd 的事件未就绪,1表示该 fd 事件的已就绪

// fd_set 里面文件描述符的数量,可以使用 ulimit -n 进行查看,默认1024
#define FD_SETSIZE __FD_SETSIZE
// fd_set 的成员是一个长整型的结构体
typedef long int __fd_mask;

// fd_set 记录要监听的 fd 集合,及其对应状态
typedef struct {
    // fds_bits是long类型数组,用来实现位图,长度: 1024/32=32
    // 共1024个bit位,每个bit位代表一个fd,0表示未就绪,1表示就绪
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
} fd_set;

1.3 select 使用流程

创建集合(集合的元素:文件描述符),把要等待的fd放入集合(监听)

  1. 创建fd_set集合
  2. 用FD_ZERO初始化集合(将位图所有位置0)
  3. 用FD_SET把要监听文件描述符fd加入集合(将位图所对应的位置为1)
  4. 把listenfd加入集合,监听到listenfd 获取connfd ,然后加入集合;
    用 select 系统调用,进程阻塞,当集合中任意一个FD准备就绪,解除阻塞
    用 FD_ISSET 检查 fd 是否就绪,就绪就解除阻塞

1.4 select 缺点

  1. 监听的 fd 的数量有限制,默认是1024, 此值可以修改
  2. 每次 select 都需要把监听的 fd 从用户态拷贝到内核态,返回后从内核态拷贝数据到用户态
  3. 轮询遍历所有的 fd 来判断就绪状态,效率低

二、实例

1.多线程多进程实例

代码如下(示例):借助于多线程实现,react模型。

void * routine(void *arg) {
    pthread_detach(pthread_self());

    int clientfd = *(int *)arg;
    unsigned char buff[BUFF_LENGTH];

    while (1) {

        int ret = recv(clientfd, buff, BUFF_LENGTH,0);
        printf("buffer:%s  ret:%dn",buff, ret);

        if (0 == ret) {
            close(clientfd);
            break;
        }
        send(clientfd, buff, ret,0);
    	printf("clientfd : %d",clientfd);
    }
}

    printf("threads method\n");

    while(1) {

    	struct sockaddr_in client;
    	socklen_t  len = sizeof(client);

    	int clientfd =  accept(listen_fd, (struct sockaddr *)&client, &len);

        pthread_t threadid;
        pthread_create(&threadid,NULL,routine,&clientfd);

    }

2.select 实例

代码如下(示例): 实现多个客户端连接服务端,通过requese-ack 模式实现。服务端借助于select 实现。

#if defined(PSELECT)
    printf("Slect method\n");

    fd_set rfds,wfds, rset, wset;  /** < fd_set 就是bit位,就是一个位表; rfds 是rset 的备份。
                                    定义两个变量主要是一个变量用来执行查询操作,一个用来置位操作 */
    int max_fd = listen_fd;
    int nready = 0;
    int conn_fd = 0;
    int nbytes = 0;
    int nsends = 0;
    char buff[BUFF_LENGTH] = {0};
    int n = 0;
	int i = 0;
    FD_ZERO(&rfds);
    FD_ZERO(&wfds);
    FD_SET(listen_fd, &rfds);

    while(1) {

        rset = rfds;
        wset = wfds;
        nready = select(max_fd+1, &rset, &wset, NULL, NULL);
        if (0 > nready ) {
            printf("select error\n");
            break;
        }else if(0 == nready) {
            continue;
        }

        if(FD_ISSET(listen_fd, &rset)) {
            struct sockaddr_in client_addr;
            socklen_t length = sizeof(client_addr);

            conn_fd = accept(listen_fd,(struct sockaddr *)&client_addr, &length);
            if (0 > conn_fd ) {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }

            FD_SET(conn_fd, &rfds);

            //max_fd = max_fd > conn_fd ? max_fd : conn_fd;
            if (conn_fd > max_fd) max_fd = conn_fd;
            if (--nready == 0) {
                continue;
            }
        }

        for(i = listen_fd+1; i < max_fd +1; i++) {
            if (FD_ISSET(i, &rset)){

                nbytes = recv(i, buff, BUFF_LENGTH, 0);
                if (nbytes > 0) {
                    buff[nbytes] = '\0';
                    printf("recv:%s \n",buff);
                    FD_SET(i, &wfds);
                }else if (0 == nbytes) {
                    FD_CLR(i,&rfds);
                    close(i);
                }

                if (--nready == 0) {
                    break;
                }
            }else if(FD_ISSET(i, &wset)) {
                    FD_SET(i, &rfds);
                n = send(i,buff,nbytes,0);
                if (n == nbytes) {
                    FD_CLR(i,&wfds);
                }

                if (--nready == 0) {
                    break;
                }
                printf("i: %d  maxid:%d listen_fd:%d\n",i,max_fd, listen_fd);
                //FD_CLR(i,&wfds);
            }
        }
    }


总结

例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。
本文主要讲解的是可以实现服务器并发方法,借助多线程实现并发,此种方法消耗资源比较大,相当于每一条会话都要借助于一个线程。linxu 系统的线程资源是有限的,此方法不太适合大量的并发。第二种方法借助于select 实现服务器并发,此种方法消耗资源较少。

推荐

[荐一个零声学院免费教程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值