(超详细)Linux Select多路复用与实例


阻塞与非阻塞

阻塞IO

  • 阻塞:进程会一直阻塞,直到数据拷贝完成。应用程序调用一个I/O函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,I/O函数返回成功指示。这个是linux系统默认的I/O下操作模式,也是最常见的I/O操作模式。也就是说,如果你创建了一个套接字,想要使用非阻塞模式,那么你需要进行设置,因为你默认的是阻塞模式。下面会详细讲到。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

当你创建了套接字,bind和listen后,以及使用accept连接上后就会使用这个recv函数去等待数据到来,等对方通过socket将数据发送到你的内核,你的内核就会通知你有数据到来,此时你这个函数就会返回,返回的是你接受数据的字节数,否则一直会在阻塞状态等待数据的到来。

非阻塞IO

  • 非阻塞当我们告诉内核,如果数据没有到来,你立马给我返回,不用等待数据了。设置成非阻塞的方法如下。
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);   //设置成非阻塞模式;

其实非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的CPU资源。

举个例子:

  • 阻塞相当于你去饭店吃饭,点菜以后需要等待菜做好。如果你选择坐到旁边等待,此时相当于阻塞;
  • 如果你不选择等而是取一边玩手机或者逛其他商店,那么你就要每隔一段时间来饭店看看菜到底做好没,这就相当于非阻塞。

多路复用技术

  • 阻塞模式是最常用的一种I/O模式,从socket的角度出发,一个client去连接连接一个server端,server端往往需要等待客户端的访问,那么如果此时我们使用阻塞模式的话,如果只使用一个线程的话,如果此时第一个连接过来了,那么你调用了recv()函数,阻塞在等待消息这里,此时如果第二个客户端连接的话,因为你的程序一直在这里等着,无法处理这个连接请求,那么此时你的连接数量是1.那么如果我们使用对线程机制,对每个客户端都使用一个线程,那么如果有大量的客户连接的话,服务端就要创建大量的线程,linux操作系统本身对文件描述符的个数有限制,即使没有限制,大量的线程创建和套接字创建也会消耗linux的资源。此时多路复用就诞生了,在我理解,多路复用就是可以在一个线程中监测多个套接字,比如select,poll,epoll,当这些套接字(文件描述符)中的任意一个进入有数据到来,以上三个函数就会返回,之后进入数据处理状态。

select函数

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

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

参数说明、

  • maxfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。在linux系统中,select的默认最大值为1024。设置这个值的目的是为了不用每次都去轮询这1024个fd,假设我们只需要几个套接字,我们就可以用最大的那个套接字的值加上1作为这个参数的值,当我们在等待是否有套接字准备就绪时,只需要监测maxfd+1个套接字就可以了,这样可以减少轮询时间以及系统的开销。
  • readfds:首先需要明白,fd_set是什么数据类型,有一点像int,又有点像struct,其实,fd_set声明的是一个集合,也就是说,readfs是一个容器,里面可以容纳多个文件描述符,把需要监视的描述符放入这个集合中,当有文件描述符可读时,select就会返回一个大于0的值,表示有文件可读;writefds和readfs类似,表示有一个可写的文件描述符集合,当有文件可写时,select就会返回一个大于0的值,表示有文件可写;fd_set* errorfds同上面两个参数的意图,用来监视文件错误异常文件。
  • timeout:这个参数一出来就可以知道,可以选择阻塞,可以选择非阻塞,还可以选择定时返回。当将timeout置为NULL时,表明此时select是阻塞的;当将tineout设置为timeout->tv_sec = 0,timeout->tv_usec = 0时,表明这个函数为非阻塞;当将timeout设置为非0的时间,表明select有超时时间,当这个时间走完,select函数就会返回。从这个角度看,个人觉得可以用select来做超时处理,因为你如果使用recv函数的话,你还需要去设置recv的模式,麻烦的很。
struct timeval{      
	long tv_sec;   /*秒 */long tv_usec;  /*微秒 */   
}

void FD_ZERO(fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);

对fd_set的理解:fd_set可以理解为一个二进制位图集合,那么集合就会有一个数量,在<sys/select.h>总定义了一个常量FD_SETSIZE,默认为1024,也就是说在这个集合内默认最多有1024个文件描述符,但是通常你用不了这么多,你通常只是关心maxfds个描述符。也就是说你现在有maxfds个文件描述符在这个集合里,那么我怎么知道集合里的哪个文件描述符有消息来了呢?你可以将fd_set中的集合看成是二进制bit位,一位代表着一个文件描述符。0代表文件描述符处于睡眠状态,没有数据到来;1代表文件描述符处于准备状态,可以被应用层处理。我觉得select函数可以分下面几步进行理解

在你开始监测这些描述符时,你先将这些文件描述符全部置为0
当你需要监测的描述符置为1
使用select函数监听置为1的文件描述符是否有数据到来
当状态为1的文件描述符有数据到来时,此时你的状态仍然为1,但是其他状态为1的文件描述如果没有数据到来,那么此时会将这些文件描述符置为0
当select函数返回后,可能有一个或者多个文件描述符为1,那么你怎么知道是哪个文件描述符准备好了呢?其实select并不会告诉你说,我哪个文件描述符准备好了,他只会告诉你他的那些bit为位哪些是0,哪些是1。也就是说你需要自己用逻辑去判断你要的那个文件描是否准备好了
理解了上面几步的话,下面这些宏就比较好理解了。

  • FD_ZERO:将指定集合里面所有的描述符全部置为0,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的
  • FD_SET:用于在文件描述符集合中增加一个新的文件描述符,将相应的位置置为1
  • FD_CLR:用来清除集合里面的某个文件描述符
  • FD_ISSET:用来检测指定的某个描述符是否有数据到来。- 那么假如在我们的程序中有5个客户端已经连接上了服务器,这个时候突然有一条数据过来了。select返回了,但是此时你并不知道是哪个客户发过来的消息,因为你每个客户发过来的消息都是一样重要的。所以你没法去只针对一个套接字使用FD_ISSET,你需要做的是用一个循环去检测(FD_ISSET)到底是哪一个客户发过来的消息,因为如果此时你监测一个套接字的话,其他客户的信息你会丢失。这个也是select的一个缺点,你需要去检测所有的套接字,看看这个套接字到底是谁来的数据。

select模型的缺点

  • 如果只对系统提供的事件集合FD_SET做遍历,当监听集合很多但是只有个别几个有事件发生时,遍历哪个文件描述符有事件的过程会消耗很多无用资源
  • 但是这不是select相比于poll,epoll效率就低
  • epoll适用于连接较多,活动数量较少的情况。
    epoll为了实现返回就绪的文件描述符,维护了一个红黑树和好多个等待队列,内核开销很大。如果此时监听了很少的文件描述符,底层的开销会得不偿失;
    epoll中注册了回调函数,当有时间发生时,服务器设备驱动调用回调函数将就绪的fd挂在rdllist上,如果有很多的活动,同一时间需要调用的回调函数数量太多,服务器压力太大。
  • select适用于连接较少的情况。
    当select上监听的fd数量较少,内核通知用户现在有就绪事件发生,应用程序判断当前是哪个fd就绪所消耗的时间复杂度就会大大减小。

select服务器代码流程

(参考黑马,实现客户端写,服务端读并把小写转大写,再写回客户端的功能)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>

#define SERV_PORT 6666

int main(int argc, char *argv[])
{
    int listenfd, connfd;
    char buf[BUFSIZ], str[INET_ADDRSTRLEN];

    struct sockaddr_in clie_addr, serv_addr;
    socklen_t clie_addr_len;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //SO_REUSEADDR允许重用本地地址端口

    bzero(&serv_addr, sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(SERV_PORT);

    bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    listen(listenfd, 128);

    fd_set rset, allset;
    int ret, maxfd = 0;
    maxfd = listenfd;
    FD_ZERO(&allset); //集合全部设为0
    FD_SET(listenfd, &allset);

    while (1)
    {
        rset = allset;
        ret = select(maxfd + 1, &rset, NULL, NULL, NULL); //返回有事件发生的个数
        if (FD_ISSET(listenfd, &rset))
        {
            clie_addr_len = sizeof(clie_addr);
            connfd = accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
            printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)), ntohs(clie_addr.sin_port));

            FD_SET(connfd, &allset); //将一个集合中的事件“加入”
            if (maxfd < connfd)
                maxfd = connfd;
            if (ret == 1)
                continue; //说明select只返回一个,并且只是listenfd,无需后续执行
        }
        int n = 0;//read读到的字节数
        for (int i = listenfd + 1; i < maxfd + 1; ++i)
        {
            if (FD_ISSET(i, &rset)) //判断文件描述符是否在集合中
            {
                n = read(i, buf, sizeof(buf));
                if (n == 0)
                {
                    close(i);
                    FD_CLR(i, &allset); //将一个集合中的事件“拿走”
                }
                else if (n == -1)
                {
                }
                else
                    for (int j = 0; j < n; ++j)
                        buf[j] = toupper(buf[j]);
                write(i, buf, n);
                write(STDOUT_FILENO, buf, n);
            }
        }
    }

    close(listenfd);
    return 0;
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VioletEvergarden丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值