(超详细)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;
}
  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
目录 第1章 Linux快速入门 1 1.1 嵌入式Linux基础 1 1.1.1 Linux发展概述 1 1.1.2 Linux作为嵌入式操作系统的优势 2 1.1.3 Linux发行版本 3 1.1.4 如何学习Linux 4 1.2 Linux安装 5 1.2.1 基础概念 5 1.2.2 硬件需求 7 1.2.3 安装准备 7 1.2.4 安装过程 8 1.3 Linux文件及文件系统 11 1.3.1 文件类型及文件属性 11 1.3.2 文件系统类型介绍 13 1.3.3 Linux目录结构 14 1.4 实验内容——安装Linux操作系统 17 本章小结 17 思考与练习 18 第2章 Linux基础命令 19 2.1 Linux常用操作命令 19 2.1.1 用户系统相关命令 20 2.1.2 文件目录相关命令 27 2.1.3 压缩打包相关命令 38 2.1.4 比较合并文件相关命令 40 2.1.5 网络相关命令 45 2.2 Linux启动过程详解 50 2.2.1 概述 51 2.2.2 内核引导阶段 51 2.2.3 init阶段 52 2.3 Linux系统服务 54 2.3.1 独立运行的服务 55 2.3.2 xinetd设定的服务 56 2.3.3 设定服务命令常用方法 56 2.4 实验内容 57 2.4.1 在Linux下解压常见软件 57 2.4.2 定制Linux系统服务 58 本章小结 60 思考与练习 60 第3章 Linux下的C编程基础 61 3.1 LinuxC语言编程概述 61 3.1.1 C语言简单回顾 61 3.1.2 LinuxC语言编程环境概述 62 3.2 进入Vi 63 3.2.1 Vi的模式 63 3.2.2 Vi的基本流程 63 3.2.3 Vi的各模式功能键 65 3.3 初探Emacs 66 3.3.1 Emacs的基本操作 67 3.3.2 Emacs的编译概述 70 3.4 Gcc编译器 71 3.4.1 Gcc编译流程解析 71 3.4.2 Gcc编译选项分析 74 3.5 Gdb调试器 77 3.5.1 Gdb使用流程 78 3.5.2 Gdb基本命令 81 3.6 Make工程管理器 86 3.6.1 Makefile基本结构 86 3.6.2 Makefile变量 87 3.6.3 Makefile规则 90 3.6.4 Make管理器的使用 91 3.7 使用autotools 92 3.7.1 autotools使用流程 92 3.7.2 使用autotools所生成的Makefile 96 3.8 实验内容 98 3.8.1 Vi使用练习 98 3.8.2 用Gdb调试有问题的程序 99 3.8.3 编写包含多文件的Makefile 101 3.8.4 使用autotools生成包含多文件的Makefile 103 本章小结 105 思考与练习 105 第4章 嵌入式系统基础 106 4.1 嵌入式系统概述 106 4.1.1 嵌入式系统简介 106 4.1.2 嵌入式系统发展历史 107 4.1.3 嵌入式系统的特点 108 4.1.4 嵌入式系统的体系结构 108 4.1.5 几种主流嵌入式操作系统分析 109 4.2 ARM处理器硬件开发平台 111 4.2.1 ARM处理器简介 111 4.2.2 ARM体系结构简介 113 4.2.3 ARM9体系结构 113 4.2.4 S3C2410处理器详解 116 4.3 嵌入式软件开发流程 121 4.3.1 嵌入式系统开发概述 121 4.3.2 嵌入式软件开发概述 122 4.4 实验内容——使用JTAG烧写NAND Flash 128 本章小结 131 思考与练习 132 第5章 嵌入式Linux开发环境的搭建 133 5.1 嵌入式开发环境的搭建 133 5.1.1 嵌入式交叉编译环境的搭建 133 5.1.2 级终端和Minicom配置及使用 135 5.1.3 下载映像到开发板 142 5.1.4 编译嵌入式Linux内核 145 5.1.5 Linux内核目录结构 149 5.1.6 制作文件系统 149 5.2 U-Boot移植 153 5.2.1 Bootloader介绍 153 5.2.2 U-Boot概述 155 5.2.3 U-Boot源码导读 156 5.2.4 U-Boot移植主要步骤 163 5.2.5 U-Boot常见命令 164 5.3 实验内容——移植Linux内核 164 本章小结 165 思考与练习 165 第6章 文件I/O编程 166 6.1 Linux系统调用及用户编程接口(API) 166 6.1.1 系统调用 166 6.1.2 用户编程接口(API) 167 6.1.3 系统命令 167 6.2 Linux中文件及文件描述符概述 168 6.3 不带缓存的文件I/O操作 168 6.3.1 open和close 168 6.3.2 read、write和lseek 170 6.3.3 fcntl 173 6.3.4 select 178 6.4 嵌入式Linux串口应用开发 183 6.4.1 串口概述 183 6.4.2 串口设置详解 184 6.4.3 串口使用详解 191 6.5 标准I/O开发 194 6.5.1 打开和关闭文件 194 6.5.2 文件读写 197 6.5.3 输入输出 198 6.6 实验内容 201 6.6.1 文件读写及上锁 201 6.6.2 多路复用式串口读写 204 本章小结 207 思考与练习 207 第7章 进程控制开发 208 7.1 Linux下进程概述 208 7.1.1 进程相关基本概念 208 7.1.2 Linux下的进程结构 210 7.1.3 Linux下进程的模式和类型 210 7.1.4 Linux下的进程管理 211 7.2 Linux进程控制编程 212 7.3 Linux守护进程 224 7.3.1 守护进程概述 224 7.3.2 编写守护进程 224 7.3.3 守护进程的出错处理 229 7.4 实验内容 232 7.4.1 编写多进程程序 232 7.4.2 编写守护进程 235 本章小结 238 思考与练习 239 第8章 进程间通信 240 8.1 Linux下进程间通信概述 240 8.2 管道通信 241 8.2.1 管道概述 241 8.2.2 管道创建与关闭 242 8.2.3 管道读写 244 8.2.4 标准流管道 246 8.2.5 FIFO 249 8.3 信号通信 253 8.3.1 信号概述 253 8.3.2 信号发送与捕捉 255 8.3.3 信号的处理 258 8.4 共享内存 264 8.4.1 共享内存概述 264 8.4.2 共享内存实现 265 8.5 消息队列 267 8.5.1 消息队列概述 267 8.5.2 消息队列实现 268 8.6 实验内容 272 8.6.1 管道通信实验 272 8.6.2 共享内存实验 275 本章小结 277 思考与练习 278 第9章 多线程编程 279 9.1 Linux下线程概述 279 9.1.1 线程概述 279 9.1.2 线程分类 280 9.1.3 Linux线程技术的发展 280 9.2 Linux线程实现 281 9.2.1 线程基本操作 281 9.2.2 线程访问控制 288 9.3 实验内容——“生产者消费者”实验 298 本章小结 302 思考与练习 303 第10章 嵌入式Linux网络编程 304 10.1 TCP/IP协议概述 304 10.1.1 OSI参考模型及TCP/IP参考模型 304 10.1.2 TCP/IP协议族 305 10.1.3 TCP和UDP 306 10.2 网络基础编程 308 10.2.1 socket概述 308 10.2.2 地址及顺序处理 309 10.2.3 socket基础编程 314 10.3 网络高级编程 322 10.4 ping源码分析 326 10.4.1 ping简介 326 10.4.2 ping源码分析 327 10.5 实验内容——NTP协议实现 345 本章小结 352 思考与练习 352 第11章 嵌入式Linux设备驱动开发 353 11.1 设备驱动概述 353 11.1.1 设备驱动简介及驱动模块 353 11.1.2 设备文件分类 354 11.1.3 设备号 355 11.1.4 驱动层次结构 355 11.1.5 设备驱动程序与外界的接口 355 11.1.6 设备驱动程序的特点 356 11.2 字符设备驱动编写 356 11.3 LCD驱动编写实例 363 11.3.1 LCD工作原理 363 11.3.2 LCD驱动实例 365 11.4 块设备驱动编写 374 11.4.1 块设备驱动程序描述符 374 11.4.2 块设备驱动编写流程 375 11.5 中断编程 381 11.6 键盘驱动实现 382 11.6.1 键盘工作原理 382 11.6.2 键盘驱动综述 383 11.6.3 键盘驱动流程 384 11.7 实验内容——skull驱动 394 本章小结 398 思考与练习 399 第12章 Qt图形编程 400 12.1 嵌入式GUI简介 400 12.1.1 Qt/Embedded 401 12.1.2 MiniGUI 401 12.1.3 Microwindows、Tiny X等 402 12.2 Qt/Embedded开发入门 402 12.2.1 Qt/Embedded介绍 402 12.2.2 Qt/Embedded信号和插槽机制 405 12.2.3 搭建Qt/Embedded开发环境 409 12.2.4 Qt/Embedded窗口部件 410 12.2.5 Qt/Embedded图形界面编程 414 12.2.6 Qt/Embedded对话框设计 416 12.3 实验内容——使用Qt编写“Hello,World”程序 420 本章小结 428
下面是一个简单的 Linux `select` 使用实例,用于监听标准输入和标准输出是否有数据可读写。 ```c #include <stdio.h> #include <stdlib.h> #include <sys/select.h> #include <unistd.h> int main() { fd_set rfds, wfds; // 用于存储文件描述符集合的 fd_set 结构体 int maxfd = STDIN_FILENO + 1; // 待监视的最大文件描述符值加一 struct timeval timeout = {5, 0}; // 时时间为 5 秒 while (1) { FD_ZERO(&rfds); // 将读文件描述符集合清零 FD_ZERO(&wfds); // 将写文件描述符集合清零 FD_SET(STDIN_FILENO, &rfds); // 将标准输入加入到读文件描述符集合 FD_SET(STDOUT_FILENO, &wfds); // 将标准输出加入到写文件描述符集合 // 调用 select 函数进行 I/O 多路复用 int ret = select(maxfd, &rfds, &wfds, NULL, &timeout); if (ret < 0) { perror("select error"); exit(1); } else if (ret == 0) { printf("timeout\n"); } else { // 判断标准输入是否可读 if (FD_ISSET(STDIN_FILENO, &rfds)) { char buf[1024]; int n = read(STDIN_FILENO, buf, sizeof(buf)); if (n < 0) { perror("read error"); exit(1); } else if (n == 0) { printf("EOF\n"); break; } else { printf("read: %s", buf); } } // 判断标准输出是否可写 if (FD_ISSET(STDOUT_FILENO, &wfds)) { char buf[1024] = "hello world\n"; int n = write(STDOUT_FILENO, buf, sizeof(buf)); if (n < 0) { perror("write error"); exit(1); } else { printf("write: %s", buf); } } } } return 0; } ``` 该程序通过 `FD_SET` 函数将标准输入和标准输出加入到读和写文件描述符集合中,然后调用 `select` 函数进行 I/O 多路复用。当标准输入可读或标准输出可写时,`select` 函数会返回,程序通过 `FD_ISSET` 函数判断哪个文件描述符可读或可写,并进行相应的读写操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

VioletEvergarden丶

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

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

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

打赏作者

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

抵扣说明:

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

余额充值