linux select 函数详解

I/O多路复用之select()系统调用

为什么要使用I/O多路复用

应用程序常常需要在多于一个文件描述符上阻塞:例如响应键盘输入(stdin)、进程间通信以及同时操作多个文件。

在不使用线程(怎么理解线程的存在呢?我么可以举一个例子。当我们运行qq这个进程的时候,是可以执行不同的任务的。例如,我们可以在使用qq发送消息的同时来收发文件。而这两个不同的任务就是利用线程来完成的),尤其是独立处理每一个文件的情况下,进程无法在多个文件描述符上同时阻塞。如果文件都处于准备好被读写的状态,同时操作多个文件描述符是没有问题的。但是,一旦在该过程中出现一个未准备好的文件描述符(就是说,如果一个read()被调用,但没有读入数据),则这个进程将会阻塞,不能再操作其他文件。可能阻塞只有几秒钟,但是应用无响应也会造成不好的用户体验。然而,如果文件描述符始终没有任何可用数据,就可能一直阻塞下去。

如果使用非阻塞I/O,应用可以发起I/O请求并返回一个特别的错误,从而避免阻塞。但是,从两个方面来讲,这种方法效率较差。首先,进程需要以某种不确定的方式不断发起I/O操作,直到某个打开的文件描述符准备好进行I/O。其次,如果程序可以睡眠的话将更加有效,可以让处理器进行其他工作,直到一个或更多文件描述符可以进行I/O时再唤醒。

三种I/O多路复用方案

I/O多路复用允许应用在多个文件描述符上同时阻塞,并在其中某个可以读写时收到通知。这时I/O多路复用就成了应用的关键所在。

I/O多路复用的设计遵循一下原则:

1、I/O多路复用:当任何文件描述符准备好I/O时告诉我

2、在一个或更多文件描述符就绪前始终处于睡眠状态

3、唤醒:哪个准备好了?

4、在不阻塞的情况下处理所有I/O就绪的文件描述符

5、返回第一步,重新开始

Linux提供了三种I/O多路复用方案:selectpollepoll

先来说说select()

select()系统调用的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

/*
作用:通知执行了select()的进程哪个Socket或文件可读
返回值:负值:select错误,见ERRORS。 
       正值:某些文件可读写或出错  
       0:等待超时,没有可读写或错误的文件
*/

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

其中fd_setselect机制中提供的一种数据结构,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(不仅是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一个socket或文件发生了可读或可写事件

监测的文件描述符可以分为三类,分别等待不同的事件。监测readfds集合中的文件描述符,确认其中是否有可读数据(也就是说,确认好了的文件描述符的读操作可以无阻塞的完成)。监测writefds集合中的文件描述符,确认其中是否有一个写操作可以不阻塞地完成。监测exceptfds中的文件描述符,确认其中是否有出现异常发生或者出现带外数据(这种情况只适用于套接字)。指定的集合可能为空(NULL)。相应的,select()则不对此类事件进行监测。

成功返回时,每个集合只包含对应类型的I/O就绪的文件描述符。举个例子,readfds集合中有两个文件描述符:7和9.当调用返回时,如果7还在集合中,该文件描述符就准备好进行无阻塞I/O了。如果9已不在集合中,它可能在被读取时会发生阻塞。出现错误返回-1

第一个参数n,等于所有集合中文件描述符的最大值加1。这样,select()的调用者需要找到最大的文件描述符值,并将其加1后传给第一个参数。

timeout参数是一个指向timeval结构体的指针,定义如下:

1
2
3
4
5
#include <sys/time.h>
struct timeval {
    long tv_sec; /* seconds */
    long tv_usec; /* microseconds */
};

如果这个参数不是NULL,即使此时没有文件描述符处于I/O就绪状态,select()调用也将在tv_sec秒、tv_usec微秒后返回。即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回

如果时限中的两个值都是0,调用会立即返回,并报告调用时所有事件对应的文件描述符均不可用,且不等待任何后续事件。

若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止

举个select()的小例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define TIMEOUT 5 /* 选择超时秒数 */
#define BUF_LEN 1024 /* 读缓冲区字节 */

int main(int argc, char const *argv[])
{
    struct timeval tv;
    fd_set readfds;
    int ret;
     
    /* 等待输入 */
    FD_ZERO(&readfds); // 把writefds集合中的所有文件描述符移除
    FD_SET(STDIN_FILENO, &readfds); // 向writefds集合中添加文件描述符STDIN_FILENO。STDIN_FILENO就是标准输入设备(一般是键盘)的文件描述符。它的值为0

    /* 设置等待为5秒 */
    tv.tv_sec = TIMEOUT;
    tv.tv_usec = 0;

    /* 在指定的tv时间内阻塞 */
    ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv); // 通知执行了select()的进程哪个Socket或文件可读

    if (ret == -1) {
    	perror("select");
    	return 1;
    }
    else if (!ret) {
    	printf("%d 秒 已经过去了. \n", TIMEOUT);
    	return 0;
    }

    if (FD_ISSET(STDIN_FILENO, &readfds)) { // 测试给定的文件描述符在不在给定的集合中。检查fdset联系的文件句柄fd是否可读写,当>0表示可读写
    	char buf[BUF_LEN + 1];
    	int len;
    	/* 保证没有阻塞 */
    	len = read(STDIN_FILENO, buf, BUF_LEN);
    	if (len == -1) {
    		perror("read");
    		return 1;
    	}
    	if (len) {
    		buf[len] = '\0';
    		printf("read: %s\n", buf);
    	}
    	return 0;
    }
    fprintf(stderr, "This should not happen!\n");

	return 0;
}

让我们执行这段代码之后,等待5秒:

可以看到,在这5秒内,进程是处于阻塞状态的(因为文件描述符的状态没有发生变化)

现在让我们执行完这段代码后输入一些内容:

上面这个例子虽然只是检测了一个文件描述符(因此不是多路复用),但是对于select()这个系统调用的用法已经很清晰了。

select函数及fd_set介绍

1. select函数

1. 用途

       在编程的过程中,经常会遇到许多阻塞的函数,好像read和网络编程时使用的recv, recvfrom函数都是阻塞的函数,当函数不能成功执行的时候,程序就会一直阻塞在这里,无法执行下面的代码。这时就需要用到非阻塞的编程方式,使用select函数就可以实现非阻塞编程。
       select函数是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。

2. 大致原理

       select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。详细的原理请看这里

3. 函数定义

       该函数声明如下

1

int select(int nfds,  fd_set* readset,  fd_set* writeset,  fe_set* exceptset,  struct timeval* timeout);

  

参数:

       nfds           需要检查的文件描述字个数
       readset     用来检查可读性的一组文件描述字。
       writeset     用来检查可写性的一组文件描述字。
       exceptset  用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
       timeout      超时,填NULL为阻塞,填0为非阻塞,其他为一段超时时间

返回值:

       返回fd的总数,错误时返回SOCKET_ERROR

. fd_set结构体

       上面select函数中需要用到两个fd_set形参,这个结构体到底做什么用的呢

       fd_set其实这是一个数组的宏定义,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读。

       系统提供了FD_SETFD_CLRFD_ISSETFD_ZERO进行操作,声明如下:

1

2

3

4

FD_SET(int fd, fd_set *fdset);       //将fd加入set集合

FD_CLR(int fd, fd_set *fdset);       //将fd从set集合中清除

FD_ISSET(int fd, fd_set *fdset);     //检测fd是否在set集合中,不在则返回0

FD_ZERO(fd_set *fdset);              //将set清零使集合中不含任何fd

  

       下面写一段程序探究一下这几个宏的工作:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

#include <WINSOCK2.H> 

int main()

{   

    fd_set fdset;   

    FD_ZERO(&fdset);   

    FD_SET(1, &fdset);   

    FD_SET(2, &fdset);   

    FD_SET(3, &fdset);   

    FD_SET(7, &fdset);   

    int isset = FD_ISSET(3, &fdset);   

    printf("isset = %d\n", isset);   

    FD_CLR(3, &fdset);   

    isset = FD_ISSET(3, &fdset);   

    printf("isset = %d\n", isset);   

    return 0;

}

  

当使用FD_SET添加完1、2、3、7后,fdset的值如下:

               

       然后经过FD_CLR以后,fd_array[2]就被清除了,数组后面的数据依次往前提,即7被放到了fd_array[2]
       所以isset前后两次打印的值分别为1和0

3. 小结

       select的结果会对fd_set造成影响。例如,对于一个监听的socket:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

#include <WinSock2.h>

#include <stdio.h>

#pragma comment(lib,"WS2_32.lib")   

int main()

{  

    FD_SET   ReadSet;  

    FD_ZERO(&ReadSet); 

    WSADATA   wsaData; 

    WSAStartup(MAKEWORD(2, 2), &wsaData);         //初始化

    SOCKET  ListenSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);  //定义一个监听套接字    

    //bind等操作这里省略....   //.....    

    FD_SET(ListenSocket, &ReadSet);      //将套接字加入ReadSet集合中    

    int isset = FD_ISSET(ListenSocket, &ReadSet);         //这里并没有通过select对fd_set进行筛选   

    printf("Before select, isset = %d\n", isset);         //所以这里打印结果为1 

    struct timeval tTime;  

    tTime.tv_sec = 10; 

    tTime.tv_usec = 0; 

    select(0, &ReadSet, NULL, NULL, &tTime);       //通过select筛选处于就绪状态的fd                                                  

    //这时,刚才的ListenSocket并不在就绪状态(没有连接连入),那么就从ReadSet中去除它    

    isset = FD_ISSET(ListenSocket, &ReadSet);  

    printf("After select, isset = %d\n", isset);     //所以这里打印的结果为0 

    system("pause");   

    return 0;

}

  

       所以可以使用select以及fd的操作来完成异步的网络消息处理,具体的实现请看这里的例子

select 系统调用

#include <sys/select.h>

int select (int maxfd + 1,fd_set *readset,fd_set *writeset,

fd_set *exceptset,const struct timeval * timeout);

参数一:最大的文件描述符加1。

参数二:用于检查可读性,

参数三:用于检查可写性,

参数四:用于检查带外数据

参数五:一个指向timeval结构的指针,用于决定select等待I/o的最长时间。如果为空将一直等待。timeval结构的定义:struct timeval{

long tv_sec; // seconds

long tv_usec; // microseconds

}

两种poll

poll 函数   此函数在系统调用select内部被使用

unsigned int (*poll)(struct file * fp, struct poll_table_struct * table)

此函数在系统调用select内部被使用,作用是把当前的文件指针挂到设备内部定义的等待

队列中。这里的参数table可以不考虑,是在select函数实现过程中的一个内部变量。

函数具体实现时:

wait_queue_head_t t = ((struct mydev *)filp->private_data)->wait_queue;

poll_wait(filp, t, table);

return mask;

这里mask可以是:

POLLIN | POLLRDNORM

POLLOUT | POLLWRNORM

等等。

poll()函数 系统调用

poll()函数:这个函数是某些Unix系统提供的用于执行与select()函数同等功能的函数,下面是这个函数的声明:

#include <poll.h>

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

参数说明:

fds:是一个struct pollfd结构类型的数组,用于存放需要检测其状态的Socket描述符;每当调用这个函数之后,系统不会清空这个数组,操作起来比较方便;特别是对于socket连接比较多的情况下,在一定程度上可以提高处理的效率;这一点与select()函数不同,调用select()函数之后,select()函数会清空它所检测的socket描述符集合,导致每次调用select()之前都必须把socket描述符重新加入到待检测的集合中;因此,select()函数适合于只检测一个socket描述符的情况,而poll()函数适合于大量socket描述符的情况;

nfds:nfds_t类型的参数,用于标记数组fds中的结构体元素的总数量;

timeout:是poll函数调用阻塞的时间,单位:毫秒;

返回值:

>0:数组fds中准备好读、写或出错状态的那些socket描述符的总数量;

==0:数组fds中没有任何socket描述符准备好读、写,或出错;此时poll超时,超时时间是timeout毫秒;换句话说,如果所检测的socket描述符上没有任何事件发生的话,那么poll()函数会阻塞timeout所指定的毫秒时间长度之后返回,如果timeout==0,那么poll() 函数立即返回而不阻塞,如果timeout==INFTIM,那么poll() 函数会一直阻塞下去,直到所检测的socket描述符上的感兴趣的事件发生是才返回,如果感兴趣的事件永远不发生,那么poll()就会永远阻塞下去;

-1: poll函数调用失败,同时会自动设置全局变量errno;

select、poll、epoll之间的区别(搜狗面试)

(1)select==>时间复杂度O(n)

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

(2)poll==>时间复杂度O(n)

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

(3)epoll==>时间复杂度O(1)

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。  

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现

select:

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

      一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

       当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。                   

2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll:

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

epoll为什么要有EPOLLET触发模式?

如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符

epoll的优点:

1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
select、poll、epoll 区别总结:

1、支持一个进程所能打开的最大连接数

select

单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll

poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的

epoll

虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接

2、FD剧增后带来的IO效率问题

select

因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

poll

同上

epoll

因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3、 消息传递方式

select

内核需要将消息传递到用户空间,都需要内核拷贝动作

poll

同上

epoll

epoll通过内核和用户空间共享一块内存来实现的。

总结:

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善 

关于这三种IO多路复用的用法,前面三篇总结写的很清楚,并用服务器回射echo程序进行了测试。连接如下所示:

select:IO多路复用之select总结

poll:O多路复用之poll总结

epoll:IO多路复用之epoll总结

我们来看下select函数的参数。参数n指定需要测试的描述符的数目,测试的描述符范围从0到n-1。第二个参数fdset指定需要测试的可读描述符集合。当fdset集合中有描述符可读,或者经历了timeout时间时,select将返回。当select返回时,作为一个副作用,select修改了参数fdset指向的描述符集合,这时fdset变成由读集合中准备好可以读了的描述符组成。select函数的返回值则指明了就绪集合的基数。值得注意的是,由于这个副作用,我们必须每次在调用select时都更新读集合。

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    int listenfd, connfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    fd_set readfds, testfds;

    // 创建套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    // 命名套接字
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(6240);
    server_len = sizeof(server_address);
    bind(listenfd, (struct sockaddr*)&server_address, server_len);

    // 创建套接字队列
    listen(listenfd, 5);

    FD_ZERO(&readfds);
    FD_SET(listenfd, &readfds);

    // 等待客户请求
    while (1) {
        char ch;
        int fd;
        int nread;

        // 同时检查监听套接字和已连接套接字
        testfds = readfds;
        printf("server waiting\n");
        int result = select(FD_SETSIZE, &testfds, (fd_set*)0, (fd_set*)0, (struct timeval*)0);
        if (result < 1) {
            perror("select error");
            exit(1);
        }

        for (fd = 0; fd < FD_SETSIZE; fd++) {
            // 检查哪个描述符可读
            if (FD_ISSET(fd, &testfds)) {
                // 如果是一个新的客户连接请求
                if (fd == listenfd) {
                    client_len = sizeof(client_address);
                    connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_len);
                    FD_SET(connfd, &readfds);
                    printf("adding client on fd %d\n", connfd);
                }
                // 如果是旧的客户活动
                else {
                    ioctl(fd, FIONREAD, &nread);
                    // 如果客户断开连接
                    if (nread == 0) {
                        close(fd);
                        FD_CLR(fd, &readfds);
                        printf("removing client on fd %d\n", fd);
                    }
                    // 客户请求数据到达
                    else {
                        read(fd, &ch, 1);
                        sleep(5);
                        printf("serving client on fd %d\n", fd);
                        ch++;
                        write(fd, &ch, 1);
                    }
                }
            }
        }
    }
}

上面的代码展示了如何使用select来编写多并发服务器的过程。服务器可以让select调用同时检查监听套接字和已连接套接字。一旦select指示有活动发生,就可以用FD_ISSET来遍历所有可能的文件描述符,以检查是哪个描述符上面有活动发生。
如果是监听套接字可读,这说明正有一个客户试图建立连接,此时就可以调用accept创建一个客户的已连接套接字而不用担心阻塞。如果是某个客户描述符准备好,这说明该描述符上有一个客户请求需要我们读取处理。如果读操作返回零字节,这表示有一个客户进程已结束,这时我们可以关闭该套接字并把它从描述符集合中删除

 

Select函数实现原理分析

 转载至:http://blog.chinaunix.net/uid-20643761-id-1594860.html

select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。
 
下面我们分两个过程来分析select:
 

1. select的睡眠过程

 
支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的BLOCK或NONBLOCK操作。当应用程序通过设备驱动访问该设备时(默认为BLOCK操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。
 
select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看select睡眠的详细过程。
 
select会循环遍历它所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。
 
下面分析一下代码是如何实现的。
select的调用path如下:sys_select -> core_sys_select -> do_select

其中最重要的函数是do_select, 最主要的工作是在这里, 前面两个函数主要做一些准备工作。do_select定义如下:

2.  select的唤醒过程

前面介绍了select会循环遍历它所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。
一个典型的驱动程序poll函数实现如下:
(摘自《Linux Device Drivers – ThirdEdition》Page 165)

参考:

https://www.cnblogs.com/wuyepeng/p/9745573.html

https://huanghantao.github.io/2017/09/13/I-O%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8%E4%B9%8Bselect-%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8/

https://www.cnblogs.com/aspirant/p/9166944.html

https://blog.csdn.net/lihao21/article/details/66097377

https://blog.csdn.net/liitlefrogyyh/article/details/52104120

猜你喜欢

转载自blog.csdn.net/shanandqiu/article/details/111657355
今日推荐