Linux网络编程 ——Select机制

一、 select的工作机制

select,是基于内核函数sys_poll实现的,有文件描述符(1024)的限制大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,开销随着文件描述符数量的增加而线性增大。(大量并发,少量活跃率较低)
应用层与内核的交互如下图:
Linux网络编程 ——Select机制_第1张图片
select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。
1. select的睡眠过程
支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的BLOCK或NONBLOCK操作。当应用程序通过设备驱动访问该设备时(默认为BLOCK操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。

select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看select睡眠的详细过程。

select会循环遍历它所监测的fd_set(一组文件描述符(fd)的集合)内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。(该函数会阻塞)

2.select的唤醒
唤醒进程的过程通常是在所监测文件的设备驱动内实现的,驱动程序维护了针对自身资源读写的等待队列。当设备驱动发现自身资源变为可读写并且有进程睡眠在自身资源的等待队列上时,就会唤醒自身资源等待队列上的进程。

3.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);  //把文件描述符集合里fd清0
	int FD_ISSET(int fd,fd_set *set); //测试文件描述符集合里fd是否置1
	void FD_SET(int fd,fd_set *set); //把文件描述符集合里fd位置1
	void FD_ZERO(fd_set *set);  //把文件描述符集合里所有位清0
参数:
	nfds:最大文件描述符+1
	readfds:需要监听的读的文件描述符存放集合
	writefds:需要监听的写的文件描述符存放集合  
	exceptfds:需要监听的异常的文件描述符存放集合 NULL
	timeout:多长时间监听一次  固定的时间,限时等待 NULL永久监听
	struct timeval{
			long tv_sec; //秒
			long tv_usec; //微妙
			};
	返回值:返回的是变化的文件描述符的个数
	注意:变化的文件描述符会存放在监听的集合中,未变化的文件描述符会被删除

二、select的编程

程序实例:select实现的并发服务器,接收客户端发送的内容并显示,并且在返回给客户端

#define MAX_SIZE 1024
int main(int argc,char *argv[])
{
	char buf[MAX_SIZE];   int n;
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    sockaddr_in addr;
    memset(&addr,0,sizeof(addr));
    addr.sin_addr.s_addr=0;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(8888);
    bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
    listen(sockfd,128);

	fd_set rfds,rset,wfds,wset;
    FD_ZERO(&rfds);
    FD_SET(sockfd,&rfds);
    FD_ZERO(&wfds);
    int max_fd=sockfd;
    while (1)
    {
        rset=rfds;
        wset=wfds;
        int ready=select(max_fd+1,&rset,&wset,NULL,NULL);
        
        if(FD_ISSET(sockfd,&rset))
        {
            struct sockaddr_in clientaddr;
            socklen_t len=sizeof(clientaddr);
            int cfd=accept(sockfd,(struct sockaddr *)&clientaddr,&len);
            FD_SET(cfd,&rfds);
            /**/
            if(cfd>max_fd)  max_fd=cfd;
            if(--ready==0)  continue;
        }

        for(int i=sockfd+1;i<=max_fd;i++)
        {

            if(FD_ISSET(i,&rset))
            {
                
                 n=recv(i,buf,MAX_SIZE,0);
                if(n>0)
                {
                    buf[n]='\0';
                    printf("recv msg from client: %s\n",buf);
                    FD_SET(i,&wfds);
                }
                else if(n==0)
                {
                    close(i);
                    FD_CLR(i,&rfds);
                    continue;
                }
                if(--ready==0)  break;
            }
            else if(FD_ISSET(i,&wset))
            {
                printf("n的个数 %d\n",n);
                send(i,buf,n,0);
                **FD_CLR(i,&wfds);**//非常重要
               // FD_SET(i,&rfds);
            }
        }

    }
	close(sockfd);
    return 0;
}

此程序中把recv()和send()进行分开,读和写不是在一次select监听后统一执行的啊,是分别调用一次select各自执行的啊。读和写事件的发生是调用了两次select啊(即select的执行次数一定为偶数),注意服务器send完数据后一定要把fd_set集合的文件描述符清0FD_CLR(i,&wfds); (select监听可写事件时,只要连接未断开,文件描述符开着,监听到的文件描述符就一直可写。当select函数返回的时候,writefds将清除其中不可写的文件描述符,只留下可写的文件描述符。如果send完数据,不把写集合里面的文件描述符清0,就一直可写,一直send,陷入了死循环)

三、select的优缺点

优点:跨平台
缺点:
文件描述符1024的限制 由于FD_SETSIZE的限制
只是返回变化的文件描述符的个数,具体那个变化需要遍历**(集合的文件描述符用完(断开连接)会被回收,因此集合文件描述符不一定是按着顺序依次递增)**
每次都需要将需要监听的文件描述符集合由应用层拷贝到内核
大量并发,少量活跃率低

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

你可能感兴趣的:(linux,网络)