I/O复用——select系统调用

本文详细介绍了I/O复用技术在处理多任务网络编程中的应用,包括非阻塞的Connect技术、聊天室程序、TCP服务器处理、服务器处理多种请求和监听多个端口的情况。重点讲解了Linux实现的I/O复用系统调用select、poll和epoll,以及如何使用这些系统调用监听文件描述符的可读、可写和异常事件。还特别强调了如何处理带外数据并区分普通数据和带外数据的就绪状态。

      I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。通常网络程序在下列情况下需要使用I/O复用技术。

    1、客户端程序要同时处理多个socket。比如本章要讨论的非阻塞的Connect 技术。

    2、客户端程序要同时处理用户输入和网络连接,比如本章将要讨论的聊天室程序。

    3、TCP服务器要同时处理监听socket和连接socket。这里I/O复用使用最多的场合。

    4、服务器要同时处理TCP请求和UDP请求。比如回射服务器。

    5、服务器要同时监听多个端口,或者处理多种服务。

   需要指出的是,I/O复用虽然能同时监听多个文件描述,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外措施,程序就只能顺序依次处理其中的每个文件描述符,这使用服务器程序想串行工作。如果要实现并发,只能使用多进程等编程手段。

   Linux实现的I/O复用的系统调用主要有select、poll和epoll。

1、select系统调用。

      select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。本节先介绍select系统调用的API,然后讨论select判断文件描述符就绪动态条件,最后给出一个处理带外数据中的实际应用。

    一、select API

 C++ Code 
1
2
3
4
#include<sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set*exceptfds
           ,struct timeval* timeout);
    1)nfds参数指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的。

    2)readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通过这三个参数传入自己感兴趣的文件描述符。select调用返回时,内核将修改他们来通知应用程序那些文件描述符已经就绪。这3个参数是fd_set结构指针类型。fd_set结构体定义如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<typesizes.h>
#define __FD_SETSIZE 1024

#include<sys/select.h?
#define FD_SETSIZE __FD_ETSIZE
typedef long int __fd_mask;
#undef __NFDBITS
#define __NFDBITS (8*(int)sizeof(__fd_mask))

typedef struct
{
#ifdef __USE_XOPEN
    __fd_mask fd_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->fd_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FD_BITS(set) ((set)->__fds_bits)
#endif
} fd;
   由以上定义可见,fd_set结构体仅包含一个整形数组,该数组的每个元素的每一位bit标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理文件描述符的总量。

  由于位操作过于繁琐,我们应该使用下面一系列宏来访问fd_set结构体中的位。

   

 C++ Code 
1
2
3
4
5
#include<sys/select.h>
FD_ZERO(fd_set *fdset);  //清除fdset所有位
FD_SET(int fd, fd_set *fdset); //设置fdset的位fd
FD_CLR(int fd, fd_set *fdset); //清除fdset的位fd
int FD_ISSET(int fd, fd_set *fdset); //测试fdset的位fd是否被设置


    3)timeout参数用来设置select函数的超时时间,它是一个timeval结构类的指针,采用指针参数是因为内核将修改它告诉应用程序select等待了多久。不过我们不能完全信任select调用返回后的timeout值,比如调用失败时timeout值是不确定的。timeval结构体定义如下所示:

 C++ Code 
1
2
3
4
5
struct timeval
{
    long tv_sec; //秒数
    long tv_uset; //微秒数
};
    由以上定义课件,select给我们提供了一个微秒级的定时方式。如果给timeout变量的tv_sec成员和tv_usec成员的都传递0,则select将立即返回。如果给timeout传递NULL,则select将一直阻塞,知道某个文件描述符就绪。

    select成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0.select失败时返回-1,并设置errno。如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。

    

    二、文件描述符的就绪条件

    那些情况下文件描述符可以被认为可读、可写或者异常呢?对于select的使用非常关键。在网络编程中,下列情况下socket可读。

    1、socket内核接收缓冲区中的字节数大于或者等于低水平标记SO_RCVOWAT。此时我们可以无阻塞地读该socket,并且读该socket的读操作将返回0.

   2、socket通信的对方关闭连接。此时对该socket的读操作将返回0

   3、监听socket上新的连接请求。

   4、socket上有未处理的错误,此时我们可以使用getsockopt来读取和清楚该错误。

   5、socket内核发送缓存区中的可用字节数大于或者低于SO_SNDLOWAT。此时我们可以无阻塞的写socket,并且写操作返回的字节大于0.

    6、socket的写操作被关闭。对写操作被关闭的socket执行写操作出发一个SIGPIPE信号。

    7、socket使用非阻塞的Connect 连接成功或者失败之后。

    8、socket上有未处理的错误。此时我们可以使用getsocket来读取和清楚该错误。

    网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据。


三、处理带外数据

     上一小节提到,socket上接收到普通数据和带外数据使用select返回,但socket处于不同的就绪状态:前者处于可读状态,后者处于异常状态。代码如下是如何同时处理二者的。

 C++ Code 
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
86
87
88
89
90
91
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>

int main(int argc, char *argv[])
{
    if(argc <= 2)
    {
        printf("usage:%s ip_adderss prot_number\n", basename(argv[0]));
        return 1;
    }
    const char *ip = argv[1];
    int port = atoi(argv[2]);
    int ret = 0;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd > 0);
    ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
    assert(ret != -1);
    ret = listen(listenfd, 5);
    assert(ret != -1);

    struct sockaddr_in client_address;
    socklen_t client_addrlength = sizeof(client_address);
    int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
    if(connfd < 0)
    {
        printf("errno is: %d\n", errno);
        close(listenfd);
    }

    char buf[1024];
    fd_set read_fds;
    fd_set exception_fds;
    FD_ZERO(&read_fds);
    FD_ZERO(&exception_fds);

    while(1)
    {
        memset(buf, '\0'sizeof(buf));
        //每次调用select前都要重新在read_fds和exception_fds中
        //设置文件描述符connfd
        //因为事件发生之后,文件描述符被内核修改。
        FD_SET(connfd, &read_fds);
        FD_SET(connfd, &exception_fds);
        ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);//阻塞
        if(ret < 0)
        {
            printf("selection failure\n");
            break;
        }
        //对于可读事件,采用普通的recv函数读取数据
        if(FD_ISSET(connfd, &read_fds))
        {
            ret = recv(connfd, buf, sizeof(buf) - 10);
            if(ret <= 0)
            {
                break;
            }
            printf("get %d bytes of normal data:%s\n", ret, buf);
        }
        //对于异常事件,采用带MSG_OOB标志的recv函数读取带外数据
        else if(FD_ISSET(connfd, &exception_fds))
        {
            ret = recv(connfd, buf, sizeof(buf) - 1, MSG_OOB);
            if(ret <= 0)
            {
                break;
            }
            printf("get %d bytes of normal data:%s\n", ret, buf);
        }

    }
    close(connfd);
    close(listenfd);
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值