linux select 函数详解

本文详细解析了Linux中I/O多路复用的select系统调用,探讨了为何使用I/O多路复用,以及select、poll、epoll之间的区别。文章通过代码示例解释了select函数的用途、大致原理、fd_set结构体,并分析了select的睡眠和唤醒过程,展示了如何在多并发服务器中使用select。
摘要由CSDN通过智能技术生成

 

 

 

 

 

 

 

 

 

 

 

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;
    }
    
  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值