Linux的I/O复用技术:select

select:
select系统调用的用途是:在一段指定时间内,监听用户所感兴趣的文件描述符上的可读、可写和异常事件
缺点:
1.所能监视的文件描述符的数量有限制,sizeof(fd_set)=128,说明能监视的描述符的最大值为128*8=1024个;
2.同时每次调用select都需要在内核遍历传递进来的所有fd,当fd很多时性能会下降;
3.由于当有事件发生时,select返回后会修改三个事件集,所以,每次都需要把fd集合从用户区拷贝到内核区,当需要监视的fd数量增多时,性能会下降;
适用场景:
适用于所监视的文件描述符数量较少的场景。

select系统调用的原型如下:

#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
/*
  nfds参数指定被监听的文件描述符的总数。它通常被设置为 select监听的所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的。
  readfds、 writefds和 exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用 select函数时,通过这3个参数传入自己感兴趣的文件描述符。
  select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。
  fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。 fd_set能容纳的文件描述符数量由 FD_SETSIZE指定,
  这就限制了select能同时处理的文件描述符的总量,最大值是1024。

  应该使用下面的一系列宏来访问 fd_set结构体中的位:
  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是否被设置

  timeout 参数用来设置 select 函数的超时时间。它是一个 timeval 结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序 select 等待了多久。
  timeval结构的定义如下:
  struct timeval
  {
    long tv_sec; //秒数
    long tv_usec; // 微秒数
  };
  如果给 timeout 的两个成员都是 0,则 select 将立即返回。如果 timeout 传递NULL,则 select 将一直阻塞,直到某个文件描述符就绪。

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

/*
  一、文件描述符的就绪条件:
  哪些情况下文件描述符可以被认为是可读、可写或者出现异常,对于select的使用非常关键。
  
  在网络编程中,下列情况下socket可读:
  (1) socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RCVLOWAT,此时我们可以无阻塞地读该socket,并且读操作返回的字节数大于0。
  (2) socket通信的对方关闭连接。此时对该socket的读操作将返回0。
  (3) 监听socket上有新的连接请求。
  (4) socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

  下列情况下socket可写:
  (1) socket内核发送缓存区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0。
  (2) socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
  (3) socket使用非阻塞connect连接成功或者失败(超时)之后。
  (4) socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。
*/
网络程序中,select 能处理的异常情况只有一种:socket 上接收到带外数据(紧急数据)。
selectserver.cpp:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
    if(argc <= 2)
    {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );
    printf("ip is %s and port is %d\n", ip, port);

    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);
        printf("select one\n");
        if (ret < 0)
        {
            printf( "selection failure\n" );
            break;
        }
        
        if (FD_ISSET(connfd, &read_fds))   //对于可读事件,采用普通的recv函数读取数据
        {
            ret = recv(connfd, buf, sizeof(buf)-1, 0);
            if(ret <= 0)
            {
                break;
            }
            printf("get %d bytes of normal data: %s\n", ret, buf);
        }
        else if(FD_ISSET(connfd, &exception_fds)) //对于异常事件,采用带MSG_OOB标志的recv函数读取带外数据
        {
            ret = recv( connfd, buf, sizeof(buf)-1, MSG_OOB);
            if(ret <= 0)
            {
                break;
            }
            printf("get %d bytes of oob data: %s\n", ret, buf);
        }

    }

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

selectclient.cpp:

#include<sys/types.h>
#include<sys/msg.h>
#include<sys/ipc.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<iostream>
#include<sys/wait.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<sys/ipc.h>
#include<errno.h>
#include<sys/shm.h>
#include<fcntl.h>
#include<semaphore.h>
#include<arpa/inet.h>
#include<iostream>
#include<assert.h>
#include<ctype.h>
#include<time.h>
using namespace std;
#define MS(a,b) memset(a,b,sizeof(a))

int main(int argc, char* argv[])
{
    if (argc <= 2)
    {
      printf("argc error!\n");
      return -1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    int ret = 0;
    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &addr.sin_addr);
    addr.sin_port = htons(port);
    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd >= 0);
    printf("select client fd: %d\n", listenfd);
    ret = connect(listenfd, (struct sockaddr*)&addr, sizeof(addr));
    assert(ret != -1);
    
     
    while(1)
    {
        char buf[128] = {0x00};
        int i = 0,ret;
        sprintf(buf, "hanyufengloveliuyifei: %d",++i);
        ret = send(listenfd, buf, strlen(buf), 0);
        printf("ret: %d\n", ret);
        assert(ret >= 0);
        MS(buf,0);
        ssize_t len = recv(listenfd, buf, sizeof(buf)-1, 0);
        assert(len >= 0);
        printf("select client recv data: [%s]\n", buf);
        sleep(2);
    }

    return 0;
}

 

posted @ 2023-06-03 20:29  韓さん  阅读(38)  评论(0编辑  收藏  举报