在Linux环境下select函数的初体验

运维 系统运维 Linux
在linux中, 主要的 IO复用方式中, 有epoll, poll 和select, 这次先来学习下select。select 能够同时监视多个文件描述符的变法, 也支持超时返回。

[[184930]]

select介绍

在linux中, 主要的 IO复用方式中, 有epoll, poll 和select, 这次先来学习下select.

select 能够同时监视多个文件描述符的变法, 也支持超时返回.

先来看下select函数的定义

/* /usr/include/sys/select.h */ 
extern int select (int __nfds,               // 最大文件描述符+1 
           fd_set *__restrict __readfds,     // 读状态文件集 
           fd_set *__restrict __writefds,    // 写状态文件集 
           fd_set *__restrict __exceptfds,   // 异常状态文件集 
           struct timeval *__restrict __timeout);  // 超时时间  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

如上图函数声明所示, 不管我们关注什么状态, 我们都应该把同一类状态的文件描述符存到同一个fd_set集合,以便select能够相应的位置打上标签, 以便后续我们来判断该文件描述符是否已经准备好

这些传递给select函数的参数, 将告诉内核:

  • 我们需要监听的文件描述符
  • 对于每个文件描述符, 我们所关心的状态 (读/写/异常)
  • 我们要等待多长时间 (无限长/超时返回)

而内核也会通过select的返回, 告知我们一些信息:

  • 已经准备好的文件描述符个数
  • 那三种状态分别是哪些文件描述符

我们可以通过以下方式将关注的文件描述符加入相应的文件集:

int socket_test; 
socket_test = socket(...);      //创建socket文件描述符 
connent(socket_test,..);        //连接服务端 
FD_SET(socket_test, &rdfds);    //加入读状态文件集 
FD_SET(socket_test, &wdfds);    //加入写状态文件集 
....  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

select原理

select函数执行顺序是: SYSCALL_DEFINE5 (sys_select) -> core_sys_select -> do_select

我们都知道, select 支持监听三个文件集: 读文件集, 写文件集, 异常文件集;

在我们调用FD_SET(socket_test, &rdfds)时, 实际上执行的操作是: 在rdfds成员数组中, 将__FDELT (d)位置的值 设成 __FDMASK (d), 直接说会有点疑惑, 先看下相关的函数,宏定义是怎样定义的吧:

/* 取自: /usr/include/sys/select.h */ 
#define FD_SET(fd, fdsetp)  __FD_SET (fd, fdsetp) 
typedef long int __fd_mask; 
 
/* 取自: /usr/include/bits/select.h  */ 
 
#define __NFDBITS   (8 * (int) sizeof (__fd_mask)) 
#define __FDELT(d)  ((d) / __NFDBITS) 
#define __FDMASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS)) 
 
#define __FD_SET(d, set)    (__FDS_BITS (set)[__FDELT (d)] |= __FDMASK (d)) 
 
typedef struct 
  { 
    /* XPG4.2 requires this member name.  Otherwise avoid the name 
       from the global namespace.  */ 
#ifdef __USE_XOPEN 
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; 
# define __FDS_BITS(set) ((set)->fds_bits) 
#else 
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; 
# define __FDS_BITS(set) ((set)->__fds_bits) 
#endif 
  } fd_set; 
 
/* /usr/include/linux/posix_types.h */ 
#define __FD_SETSIZE    1024  
  • 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.

举个栗子, 假设 fd=3, 当我们执行FD_SET(fd, &rdfds)时:

  1. 算出 __FDELT(d) 和 __FDMASK(d)的值, 通过上面的宏定义, 可以分别得出结果: 3/(8*8), 1<<3%(8*8), 也就是0 和 二进制的 0000 0100
  2. 然后分别将值存入 rdfds.__fds_bits第0个位置, 值为十进制的8
  3. 我们可以将__fds_bit的每个索引看成是一个聚合的过程, 每个值8字节, 也就是有64位, 可以存64个fd, 在我的系统上, 算出数组的长度是__FD_SETSIZE / __NFDBITS = 1024/8=128个, 也就是大概能容纳 128*64=8192(如果理解错误请指出)

经过上面的运算, 我们将需要关注的文件描述关联到 rdfds文件集了, 对于写文件集, 异常文件集都是同样的运算, 等这些步骤都进行完了, 接下来就是进入core_sys_select函数了:

  1. 执行到 core_sys_select 时, 定义一个fd_set_bits结构体: fds.
  2. 分别为fds的成员(in, out, ex, res_in, res_out, res_ex)申请内存
  3. 将我们传给select的 rdfds, wrfds, exfds分别赋值给 in, out, ex, 这样fds就能记录三个集合的结果了
  4. 初始化那个三个成员之后, 将执行do_select(n, &fds, end_time)
  5. 在do_select中, 函数将进入死循环,其中还有两个循环, 分别是针对 "最大文件描述符数" 和 fd_set_bits数组中单个值位数. 从上面我们已经知道, 在fd_set_bits每个值都代表所关注的文件描述符, 每个值是__NFDBITS(8 *8字节)大小,也就是64位, 所以在上面循环内, 还要再循环64次
  6. 看到这里其实大家都应该有个底了, 为什么要循环那么多次, 因为我们需要通过每个文件描述符对应的file_operations结构体的接口f_op->poll来得知是否已经准备好了

简单介绍 file_operations

我们都知道,当我们打开一些设备或者文件时, 总是返回一个文件描述符, 其实通过这个文件描述符, 我们通过fget_light 来获得对应的file结构体, 为什么还要反查这个file, 因为通过这个file结构体可以得到: file_operations结构体

file_operations结构体: 用来存储驱动内核模块提供的对 设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的 事务的函数的地址。

/* linux-2.6.32/include/linux/fs.h */ 
struct file_operations { 
   ... 
    unsigned int (*poll) (struct file *, struct poll_table_struct *);    // select通过这个来获取状态 
   ...(其他接口忽略)  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

do_select 循环体源码:

/* select.c/do_select() */ 
for (;;) { 
        unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp; 
 
        inp = fds->in; outp = fds->out; exp = fds->ex; 
        rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex; 
 
        for (i = 0; i < n; ++rinp, ++routp, ++rexp) {       
            unsigned long inout, ex, all_bits, bit = 1, mask, j; 
            unsigned long res_in = 0, res_out = 0, res_ex = 0; 
            const struct file_operations *f_op = NULL
            struct file *file = NULL
 
            in = *inp++; out = *outp++; ex = *exp++; 
            all_bits = in | out | ex; 
            if (all_bits == 0) { 
                i += __NFDBITS; 
                continue
            } 
 
            for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {   // 遍历64位 
                int fput_needed; 
                if (i >= n) 
                    break; 
                if (!(bit & all_bits)) 
                    continue
 
                //在当前进程的struct files_struct中根据所谓的用户空间文件描述符fd来获取文件描述符 
                file = fget_light(i, &fput_needed);             
                if (file) { 
                    f_op = file->f_op;          // file_operations结构体 
                    mask = DEFAULT_POLLMASK; 
                    if (f_op && f_op->poll) { 
                        wait_key_set(wait, inoutbit); 
                        mask = (*f_op->poll)(file, wait); 
                    } 
                    fput_light(file, fput_needed); 
                    if ((mask & POLLIN_SET) && (in & bit)) {    //判断读状态 
                        res_in |= bit
                        retval++; 
                        wait = NULL
                    } 
                    if ((mask & POLLOUT_SET) && (out & bit)) {  //判断写状态 
                        res_out |= bit
                        retval++; 
                        wait = NULL
                    } 
                    if ((mask & POLLEX_SET) && (ex & bit)) {    //判断异常状态 
                        res_ex |= bit
                        retval++; 
                        wait = NULL
                    } 
                } 
            } 
            if (res_in) 
                *rinp = res_in; 
            if (res_out) 
                *routp = res_out; 
            if (res_ex) 
                *rexp = res_ex; 
            cond_resched(); 
        } 
        wait = NULL
        if (retval || timed_out || signal_pending(current)) 
            break; 
        if (table.error) { 
            retval = table.error; 
            break; 
        } 
 
        /* 
         * If this is the first loop and we have a timeout 
         * given, then we convert to ktime_t and set the to 
         * pointer to the expiry value. 
         */ 
        if (end_time && !to) { 
            expire = timespec_to_ktime(*end_time); 
            to = &expire; 
        } 
 
        if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, 
                       to, slack)) 
            timed_out = 1; 
    }  
  • 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.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.

当select经历完上面的流程, 将会有以下结果:

  • >0: 准备好的文件描述符个数
  • 0: 超时
  • -1: 出错或者接收到信号

那我们接下来要做的就是,

  • 通过行FD_ISSET()判断之前绑定的文件fd, 如果为真, 则进行相应操作
  • 因为select返回后, 之前存好的rdfds, wdfds, exfds都会被清空, 所以需要用FD_SET()重新加入

select实战

上面已经学习到关于select的相关知识, 那么我们应该要来实战下:

这次我们需要实现的目标是:

一个程序, 同时连接3个socket_server, 并且将socket_server发送的消息打印出来(不需要响应, 也不需要交互)

程序代码:

/* filename: test_select.c */ 
 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/socket.h> 
#include <sys/select.h> 
#include <sys/types.h> 
#include <netinet/in.h> 
#include <unistd.h> 
#include <fcntl.h> 
void main() 

    // socket1 
    int socketd; 
    char buffer[1025];  
    struct sockaddr_in seraddr; 
    socketd = socket(AF_INET, SOCK_STREAM, 0); 
    seraddr.sin_family = AF_INET; 
    seraddr.sin_port = htons(9997); 
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr); 
    if (connect(socketd, (struct sockaddr *) &seraddr, sizeof(seraddr))<0) 
    { 
        printf("socketd1 connect failed\n"); 
        exit(3); 
    } 
 
    // socket2 
    int socketd2; 
    char buffer2[1025]; 
    struct sockaddr_in seraddr2; 
    socketd2 = socket(AF_INET, SOCK_STREAM, 0); 
    seraddr2.sin_family = AF_INET; 
    seraddr2.sin_port = htons(9998); 
    inet_pton(AF_INET, "127.0.0.1", &seraddr2.sin_addr); 
    if (connect(socketd2, (struct sockaddr *) &seraddr2, sizeof(seraddr))<0) 
    { 
        printf("socketd2 connect failed\n"); 
        exit(3); 
    } 
 
    // scoket3 
    int socketd3; 
    char buffer3[1025]; 
    struct sockaddr_in seraddr3; 
    socketd3 = socket(AF_INET, SOCK_STREAM, 0); 
    seraddr3.sin_family = AF_INET; 
    seraddr3.sin_port = htons(9999); 
    inet_pton(AF_INET, "127.0.0.1", &seraddr3.sin_addr); 
    if (connect(socketd3, (struct sockaddr *) &seraddr3, sizeof(seraddr))<0) 
    { 
        printf("socketd3 connect failed\n"); 
        exit(3); 
    } 
 
    int maxfdp;   
    fd_set fds;                      // select需要的文件描述符集合  
    maxfdp = socketd3 + 1;           // select 第一个形参就是打开的最大文件描述符+1 
    struct timeval timeout = {3, 0}; // 超时设置 
    while(1) 
    { 
        FD_ZERO(&fds);               // 初始化文件描述符集合 
        FD_SET(socketd, &fds);       // 分别添加以上三个需要监听的文件描述符 
        FD_SET(socketd2, &fds); 
        FD_SET(socketd3, &fds); 
        select(maxfdp, &fds, NULLNULL, &timeout); 
        // 通过FD_ISSET 来分别判断 监听的文件描述符在fds有没有被设置成1 
        if (FD_ISSET(socketd, &fds)) 
        { 
            read(socketd, buffer, 1024); 
            printf("1 %s\n",buffer); 
        } 
        if(FD_ISSET(socketd2, &fds)) 
        { 
            read(socketd2, buffer2, 1024); 
            printf("2 %s\n",buffer2); 
             
        } 
        if(FD_ISSET(socketd3, &fds)) 
        { 
            read(socketd3, buffer3, 1024); 
            printf("3 %s\n",buffer3); 
             
        } 
    } 
 
  • 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.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.

为了快速建立简单的测试服务端, 所以用python实现简单socket_server:

# socket1.py 
 
import socket 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
s.bind(('localhost', 9997)) 
s.listen(2) 
rint 'Socket1 is on ready!' 
client, info = s.accept() 
print info 
while 1: 
    message = raw_input('input: '
    client.send(message) 
s.close() 
 
------------------------------- 
# socket2.py 
 
import socket 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
s.bind(('localhost', 9998)) 
s.listen(2) 
rint 'Socket2 is on ready!' 
client, info = s.accept() 
print info 
while 1: 
    message = raw_input('input: '
    client.send(message) 
s.close() 
 
------------------------------- 
# socket3.py 
 
import socket 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
s.bind(('localhost', 9999)) 
s.listen(2) 
rint 'Socket3 is on ready!' 
client, info = s.accept() 
print info 
while 1: 
    message = raw_input('input: '
    client.send(message) 
s.close()  
  • 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.

分别运行 socket1.py, socket2.py, socket3.py将会看到如下结果:

# 运行 socket1.py 
[root@iZ23pynfq19Z ~]# python socket1.py  
Socket1 is on ready! 
 
---------------------- 
# 运行 socket1.py 
[root@iZ23pynfq19Z ~]# python socket2.py  
Socket2 is on ready! 
 
---------------------- 
# 运行 socket1.py 
[root@iZ23pynfq19Z ~]# python socket3.py  
Socket3 is on ready!  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

当我们编译 test_select.c 并运行时, 将会看到三个服务端都出现了相应的响应:

# 运行 socket1.py 
[root@iZ23pynfq19Z ~]# python socket1.py  
Socket1 is on ready! 
('127.0.0.1', 55951)  # 客户端连接的信息, 端口不一定相同 
input: 
 
---------------------- 
# 运行 socket1.py 
[root@iZ23pynfq19Z ~]# python socket2.py  
Socket2 is on ready! 
('127.0.0.1', 55921) 
input: 
 
---------------------- 
# 运行 socket1.py 
[root@iZ23pynfq19Z ~]# python socket3.py  
Socket3 is on ready! 
('127.0.0.1', 55933) 
input:  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

那么我们来尝试三个服务端分别发送消息到select程序吧:

# socket1.py 
[root@iZ23pynfq19Z ~]# python socket1.py  
Socket1 is on ready! 
('127.0.0.1', 55951)  # 客户端连接的信息, 端口不一定相同 
input: asd 
input: qwe 
input: as 
input:  
---------------------- 
# socket1.py 
[root@iZ23pynfq19Z ~]# python socket2.py  
Socket2 is on ready! 
('127.0.0.1', 55921) 
input: asd 
input: asd 
input: asd 
input: as 
input: s 
input:  
 
---------------------- 
# socket1.py 
[root@iZ23pynfq19Z ~]# python socket3.py  
Socket3 is on ready! 
('127.0.0.1', 55933) 
input: asd 
input: qwe 
input: a 
input:   
  • 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.

将看到select程序都能输出三个socket_server发出的消息:

 

 

 

 

需要注意的是:

  • 前面的数字是socket_server的编号, 因为server发送消息的顺序是乱的, 所以输出的编号也是乱的
  • 这次只为验证select, 所以并没对程序的健壮性作较好的设计, 所以如果服务端/客户端刷屏了, 直接ctrl-c终止吧

经过上述的实验, 我们应该能够简单的了解select的用法和效果, 通过select实现IO多路复用, 可以让我们一定程度上避免多线程/多进程的繁琐, 在我们日常工作上, 有必要的话尝试这种方式也不失一种偷懒的方法. 

责任编辑:庞桂玉 来源: Lin_R的博客
相关推荐

2011-06-20 14:58:53

QT BasicExcel

2013-12-12 11:33:31

iOS 7API

2010-03-23 15:24:45

Linux shell

2009-02-16 17:10:17

OpenSolarisLinux 挑战

2009-03-09 15:12:39

XenServer安装

2009-08-01 09:06:35

UbuntuOneLinux开源操作系统

2010-09-17 11:01:05

Java运行环境

2010-11-22 10:31:17

Sencha touc

2011-05-30 15:12:10

App Invento 初体验

2023-07-15 08:01:38

2010-03-11 10:26:15

Ubuntu的初体验

2010-06-28 15:38:01

MeeGo

2010-04-09 13:44:38

Ubuntu 10.0

2011-01-14 11:27:02

Linux制作网页

2017-09-05 05:55:24

AWS ES集群大数据

2011-09-05 10:20:21

Sencha ToucAPP

2009-07-21 13:08:08

iBATIS DAO

2013-05-28 10:22:03

2023-07-17 08:34:03

RocketMQ消息初体验

2024-12-23 07:00:00

FastExcelEasyExcel开源框架
点赞
收藏

51CTO技术栈公众号