仅一个线程、进程处理并发
IO多路转接(复用)之select
跨平台适用linux,windows 底层:线性表
IO多路转接(复用)之poll
适用linux 底层:线性表
IO多路转接(复用)之epoll
适用linux 底层:红黑树(效率高)
多线程/多进程并发和IO多路转接的并发处理流程进行对比(服务器端):
多线程/多进程并发
- 主线程/父进程:调用 accept()监测客户端连接请求
- 如果没有新的客户端的连接请求,当前线程/进程会阻塞
- 如果有新的客户端连接请求解除阻塞,建立连接
- 子线程/子进程:和建立连接的客户端通信
- 调用 read() / recv() 接收客户端发送的通信数据,如果没有通信数据,当前线程/进程会阻塞,数据到达之后阻塞自动解除
- 调用 write() / send() 给客户端发送数据,如果写缓冲区已满,当前线程/进程会阻塞,否则将待发送数据写入写缓冲区中
IO多路转接并发
- 使用IO多路转接函数委托内核检测服务器端所有的文件描述符(通信和监听两类),这个检测过程会导致进程/线程的阻塞,如果检测到已就绪的文件描述符阻塞解除,并将这些已就绪的文件描述符传出
- 根据类型对传出的所有已就绪文件描述符进行判断,并做出不同的处理
- 监听的文件描述符:和客户端建立连接
- 此时调用accept()是不会导致程序阻塞的,因为监听的文件描述符是已就绪的(有新请求)
- 通信的文件描述符:调用通信函数和已建立连接的客户端通信
- 调用 read() / recv() 不会阻塞程序,因为通信的文件描述符是就绪的,读缓冲区内已有数据
- 调用 write() / send() 不会阻塞程序,因为通信的文件描述符是就绪的,写缓冲区不满,可以往里面写数据
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
select
函数原型
#include<sys/select.h>
struct timeval {time_t tv_sec; /* seconds */suseconds_t tv_usec; /* microseconds */
};int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds, struct timeval * timeout)
返回值:三个集合所有满足条件的文件描述符的个数
函数参数:
nfds:委托内核要检测检测的文件描述符中最大的一个 + 1
线性表实现,这个值是循环结束的条件,不知道就写最大的1024
在Window中这个参数是无效的,指定为-1即可
readfds: 文件描述符的集合,文件描述符对应的读缓冲区
writefds: 文件描述符的集合,文件描述符对应的写缓冲区
exceptfds:文件描述符的集合,文件描述符是否有异常状态
timeout:指定select函数检测的时长,用来强制解除select()函数的阻塞的
NULL:函数检测不到就绪的文件描述符会一直阻塞。
等待固定时长(秒):函数检测不到就绪的文件描述符,在指定时长之后强制解除阻塞,函数返回0
0:不等待,函数不会阻塞。
初始化fd_set类型的参数
// 将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);
内核实现细节
在select()函数中第2、3、4个参数都是fd_set类型,它表示一个文件描述符的集合,类似于信号集 sigset_t,这个类型的数据有128个字节,也就是1024个标志位,和内核中文件描述符表中的文件描述符个数是一样的。
这块内存中的每一个bit 和 文件描述符表中的每一个文件描述符是一一对应的关系
首先,将要检测的集合在内核中做了一份拷贝,内核基于拷贝的集合进行检测
下图fd_set中存储了要委托内核检测读缓冲区的文件描述符集合。
- 如果集合中的标志位为0代表不检测这个文件描述符状态
如果集合中的标志位为1代表检测这个文件描述符状态
内核在遍历这个读集合的过程中,如果被检测的文件描述符对应的读缓冲区中没有数据,内核将修改这个文件描述符在读集合fd_set中对应的标志位,改为0,如果有数据那么这个标志位的值不变,还是1。然后将结果更新到送检的读集合: