LV06-03-网络编程-07-多路复用IO-01-select
本文主要是网络编程——多路复用IO中的select的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
一、select相关函数
1. select()
1.1 函数说明
在linux
下可以使用man 2 select
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于执行I/O
多路复用操作,能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
【函数参数】
nfds
:int
类型,通常表示需要监视的最大文件描述符编号值加1
。readfds
:fd_set
类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符读是否就绪(是否可读)。select()
返回后,readfds
将清除除准备读取的文件描述符外的所有文件描述符。writefds
:fd_set
类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符写是否就绪(是否可写)。select()
返回后,writefds
将清除除准备写入的文件描述符外的所有文件描述符。exceptfds
:fd_set
类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符是否出现异常,需要注意异常情况并不是在文件描述符上出现了一些错误。select()
返回后,exceptfds
将清除除了发生异常条件的文件描述符以外的所有文件描述符。timeout
:struct timeval
类型结构体指针变量,用于设定select()
阻塞的时间上限,控制select
的阻塞行为。
点击查看 timeout 参数说明
在使用man
手册的时候,会有该结构体的说明,该结构体的成员如下:
1 | struct timeval |
(1)我们可将timeout
参数设置为NULL
,表示select()
将会一直阻塞、直到某一个或多个文件描述符成为就绪态;
(2)如果参数timeout
指向一个struct timeval
结构体对象,并且这个结构体对象中的两个成员变量都为0
,那么此时select()
函数不会阻塞,它只是简单地轮训指定的文件描述符集合,看看其中是否有就绪的文件描述符并立刻返回。
(3)若参数timeout
将为select()
指定一个等待(阻塞)时间的上限值,如果在阻塞期间内,文件描述符集合中的某一个或多个文件描述符成为就绪态,将会结束阻塞并返回;如果超过了阻塞时间的上限值,select()
函数将会返回。
【返回值】int
类型,返回值可能会有以下三种情况:
- 返回
-1
:表示有错误发生,并且会设置errno
。可能的错误码包括EBADF
、EINTR
、EINVAL
、EINVAL
以及ENOMEM
,其中EBADF
表示readfds
、writefds
或exceptfds
中有一个文件描述符是非法的;EINTR
表示该函数被信号处理函数中断了,其他更详细的我们可以使用man
命令查看帮助手册去详细了解。 - 返回
0
:表示在任何文件描述符成为就绪态之前select()
调用已经超时,在这种情况下,readfds
,writefds
以及exceptfds
所指向的文件描述符集合都会被清空。 - 返回正整数:表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符的个数,在这种情况下,每个返回的文件描述符集合都需要检查,通过
FD_ISSET()
宏进行检查,以此查找发生的I/O
事件是什么。如果同一个文件描述符在readfds
,writefds
以及exceptfds
中同时被指定,且它有多个I/O
事件都处于就绪态的话,那么这个文件描述符就会被统计多次,换句话说,也就是说select()
返回三个集合中被标记为就绪态的文件描述符的总数。
【使用格式】后边还有几个知识点,到最后查看使用实例即可。
1 | /* 需要的一些头文件 */ |
【注意事项】
(1)select()
函数将阻塞直到有以下事情发生:
readfds
、writefds
或exceptfds
指定的文件描述符中至少有一个成为就绪态(变为可读、可写或者发生了异常)。- 该调用被信号处理函数中断。
- 参数
timeout
中指定的时间上限已经超时。
(2)理论上说,nfds
参数应该不会超过1025
,对于我现在使用的Ubuntu
来说,之前有查询过一个进程最多可以打开1024
个文件描述符,所以这里应该不会超过1025
。
(3)一般情况,我们会使用读集合。写集合填NULL
,这是因为我们写的时候一般都是读取完时候后给到一个反馈,然后会写入数据,所以这里写集合我们一般不怎么关心。异常集合一般也是是填NULL
,有些特殊的情况可能会用到,但是吧,我还没遇到过,就先这样吧,后边遇到了再补充。
(4) select()
函数里面的各个文件描述符fd_set
集合的参数在select()
前后发生了变化:
前:表示关心的文件描述符集合。
后:有数据的集合(如不是在超时还回情况下)。谁改变了这个集合?答案是
kernel
,就是内核。
(5)调用select()
函数之后,select()
函数会清空它所检测的socket
描述符集合,导致每次调用select()
之前都必须把socket
描述符重新加入到待检测的集合中。
1.2 使用实例
暂无。
2. pselect()
在我们使用man
手册查询select()
函数相关用法的时候,一定看到过这个函数,下边是这个函数的一些相关笔记。
2.1 函数说明
在linux
下可以使用man 2 pselect
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于执行I/O
多路复用操作,该函数是一个 防止信号干扰的增强型 select()
函数,它也能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
【函数参数】
nfds
:int
类型,通常表示需要监视的最大文件描述符编号值加1
。readfds
:fd_set
类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符读是否就绪(是否可读)。select()
返回后,readfds
将清除除准备读取的文件描述符外的所有文件描述符。writefds
:fd_set
类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符写是否就绪(是否可写)。select()
返回后,writefds
将清除除准备写入的文件描述符外的所有文件描述符。exceptfds
:fd_set
类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符是否出现异常,需要注意异常情况并不是在文件描述符上出现了一些错误。select()
返回后,exceptfds
将清除除了发生异常条件的文件描述符以外的所有文件描述符。timeout
:struct timespec
类型结构体指针变量,用于设定pselect()
阻塞的时间上限,与select()
函数的timeout
参数作用相同,只是时间精度不同。
点击查看 timeout 参数说明
在使用man
手册的时候,会有该结构体的说明,该结构体的成员如下:
1 | struct timespec |
这个参数与select()
的timeout
参数作用相同,只是pselect()
函数使用的是struct timespec
结构体,可以指定超时时间到纳秒级
sigmask
:sigset_t
类型指针变量,表示信号掩码,指定一个需要屏蔽的信号集。
【返回值】int
类型,返回值与select()
函数相同,可能会有以下三种情况:
- 返回
-1
:表示有错误发生,并且会设置errno
。可能的错误码包括EBADF
、EINTR
、EINVAL
、EINVAL
以及ENOMEM
,其中EBADF
表示readfds
、writefds
或exceptfds
中有一个文件描述符是非法的;EINTR
表示该函数被信号处理函数中断了,其他更详细的我们可以使用man
命令查看帮助手册去详细了解。 - 返回
0
:表示在任何文件描述符成为就绪态之前select()
调用已经超时,在这种情况下,readfds
,writefds
以及exceptfds
所指向的文件描述符集合都会被清空。 - 返回正整数:表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符的个数,在这种情况下,每个返回的文件描述符集合都需要检查,通过
FD_ISSET()
宏进行检查,以此查找发生的I/O
事件是什么。如果同一个文件描述符在readfds
,writefds
以及exceptfds
中同时被指定,且它有多个I/O
事件都处于就绪态的话,那么这个文件描述符就会被统计多次,换句话说,也就是说select()
返回三个集合中被标记为就绪态的文件描述符的总数。
【使用格式】暂时还没有用到过,后边遇到了再补充。
【注意事项】none
2.2 sigmask
参数说明
sigmask
参数指定了一个应该在pselect()
函数调用期间被阻塞的信号的集合,它会在调用期间覆盖当前的信号掩码,当函数返回后恢复之前的信号掩码。如果sigmask
为NULL
,信号掩码在pselect()
调用期间不会被修改。(信号掩码是啥?进程通信部分里边关于信号阻塞的部分内容)。在man
手册中,有以下与使用select()
等价的写法:
1 | ready = pselect(nfds, &readfds, &writefds, &exceptfds, timeout, &sigmask); |
首先,我们在前边的学习中知道在Linux
下按下Ctrl+C
会产生SIGINT
信号,该信号的编号为2
。所以我们以该信号为例,下进行一个说明:
(1)我们定义了一个信号集 origmask ,目前还没有向里边添加任何需要被阻塞的信号,这个时候不会有信号被阻塞;
(2)我们将 SIGINT 信号添加到这个信号集中去,这个时候这个信号就会在某种情况下被阻塞,也就是该信号不会被传递给进程;
(3)我们调用了 pselect() 函数,在该函数调用期间,SIGINT 信号将会一直被则色,该信号无法被传递给当前进程;
(4)当pselect()函数返回后,会恢复之前的信号掩码,SIGINT 阻塞状态被取消,此时我们按下 Ctrl + C 便可以正常向进程传递 SIGINT 信号了。
2.3 与select()
的不同
使用man
手册的时候,我们会看到以下说明:
1 | The operation of select() and pselect() is identical, other than these three differences: |
意思就是,select()
和pselect()
的操作是相同的,但会有以下三个不同的地方:
select()
使用timeval
结构(带有秒和微秒)的超时,而pselect()
使用timepec
结构(带有秒和纳秒)。select()
可以更新timeout
参数来表示剩余时间,而pselect()
不会改变这个参数。select()
没有sigmask
参数,在pselect()
调用时,若将sigmask
设为NULL
,此时这两个函数是等价的。
二、select
使用流程图
需要读取数据时,大概的流程如下:
- (1)把关心的文件描述符添加到文件描述符集合中;
- (2)调用
select()/poll()
函数来监控文件描述符集合,然后会阻塞等待集合中一个或者多个文件描述符有数据; - (3)当有数据时,函数返回;
- (4)依次判断哪个文件描述符有数据;
- (5)依次处理有数据的文件描述符的数据。
三、文件描述符集操作函数
1. 数据类型:fd_set
上边,我们介绍了select()
函数的各个参数,下边来了解一些这个文件描述符集,也就是这个fd_set
类型的数据。这个数据类型定义在sys/select.h
文件中,关于这个数据类型的定义如下:
1 | /* fd_set for select and pselect. */ |
会发现,fd_set
是一个结构体,它的成员是一个__fd_mask
类型的数组,数组的大小为__FD_SETSIZE / __NFDBITS
。
还是在这个文件中,我们会找到如下定义:
1 | /* The fd_set member is required to be an array of longs. */ |
从这里我们知道__fd_mask
实际上为long int
类型,在Ubuntu21.04
的64
位版中,该类型占8
个字节。
点击查看常见有关 int 的数据类型所占字节数
1 | (short int) (int) (long) (long long) |
__FD_SETSIZE / __NFDBITS
是多少呢?我自己是没有找到这两个到底是定义在哪里,后边找到了再补充到这里把。
总之来说,fd_set就是一个long类型数组,数组中所有元素按照二进制位排列,每一位都对应一个文件描述符)。大概就是下图这个样子,集合的每一个元素代表了一个文件描述符的状态,准备好了就置1
。
在使用select()
函数之前,需要先了解一下文件描述符的相关操作,这里一共有四个函数:
1 | void FD_CLR(int fd, fd_set *set); |
2. FD_CLR()
2.1 函数说明
在linux
下可以使用man 2 FD_CLR
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于从文件描述符集中移除一个文件描述符。
【函数参数】
fd
:int
类型,表示要移除的文件描述符。set
:fd_set
类型,表示已经定义好的文件描述符集。
【返回值】none
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.2 使用实例
暂无。
3. FD_ISSET()
3.1 函数说明
在linux
下可以使用man 2 FD_ISSET
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于测试指定的文件描述符是否还存在于指定的文件描述符集。
【函数参数】
fd
:int
类型,表示要测试的文件描述符。set
:fd_set
类型,表示已经定义好的文件描述符集。
【返回值】int
类型,文件描述符fd
在set
中存在,FD_ISSET()
返回非0
值,如果不存在则返回0
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.2 使用实例
暂无。
4. FD_SET()
4.1 函数说明
在linux
下可以使用man 2 FD_SET
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于向文件描述符集中添加一个我们关心的文件描述符。
【函数参数】
fd
:int
类型,表示要添加的文件描述符。set
:fd_set
类型,表示已经定义好的文件描述符集。
【返回值】none
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
4.2 使用实例
暂无。
5. FD_ZERO()
5.1 函数说明
在linux
下可以使用man 2 FD_ZERO
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于将一个文件描述符集合初始化为空。
【函数参数】
set
:fd_set
类型,表示需要初始化为空的文件描述符集。
【返回值】none
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
5.2 使用实例
暂无。
四、select()
使用实例
在下边的实例中,我们使用TCP
协议的客户端和服务器端进行测试,在下边的测试源码中,我们的客户端和服务器端接收的数据打印的时候都没有换行,其实看前边的几篇笔记中的一些例子就会发现,我们打印出来的数据间隔大部分都是两行左右,间距太大了,这是因为从标准输入的时候将enter
也一起读取了,所以发送出去的数据本身就有一个换行符。
1. server
服务器端
点击查看实例
1 | /** ===================================================== |
2. client
客户端
点击查看实例
1 | /** ===================================================== |
3. Makefile
由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make
来进行。
点击查看 Makefile 文件
1 | ## ===================================================== |
4. 测试结果
我们执行以下命令编译链接程序,生成两个可执行文件:
1 | make |
然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server
和客户端程序client
:
1 | gcc -g -O2 -Wall client.c -o client |
对于服务器端,我们执行以下命令启动服务器进程:
1 | ./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口 |
对于客户端,我们执行以下命令启动客户端进程:
1 | ./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5002 |
然后我们就会看到如下现象:
可以发现我们发送的数据,在客户端都被打印了出来,说明客户端与服务器端可以进行正常通信。