"就让我是一道微光,能让你拥有灿烂的锋芒"
IO=等待 + 数据拷贝
高效IO:减少"等待"花费的单位时间,尽可能提高IO效率!
阻塞 IO顾名思义:
阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待.
举个钓鱼的例子,再鱼没咬钩之前,死盯着杆子。一旦鱼咬钩,就立马拉杆。
显然,非阻塞IO就和阻塞IO完全对立。
如果内核还未将数据准备好, 系统调用仍然会直接返回。并且返回EWOULDBLOCK错误码。
因此,使用非阻塞IO必然会对程序员提一些要求。
阻塞IO,一旦数据好了就会立马读取,但在此期间不能做任何事情。
非阻塞IO,需要反复对一个文件描述符尝试读写("轮询“),一旦成功也就不会返回错误码(errno)"EWOULDBLOCK"。
内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
由内核在数据拷贝完成时, 通知应用程序。
同信号驱动IO不同的是(何时可以开始拷贝数据);
多路转接:最核心在于 能够同时等待多个文件描述符的就绪状态.
可以看出,在多路转接中,等待 和 拷贝数据 被分割成了两部分。“等待” 单独由select函数执行等待,
一旦"等事件就绪” 就可以供用户层进行读取。
但什么是"等事件就绪"呢?
同步通信:所谓同步,也就是指,当调用这进行调用时,如果得不到结果,那么则这个调用也不会进行返回。换句话说,就是由调用者主动等待这个调用的结果;
异步通信:反观异步,调用在发出之后,这个调用就直接返回了,所以没有返回结果。
换句话,如果调用者需要得到调用后的结果,需要被调用者自己,去通知调用者。(可以使用回调函数....来解决)。
这两个完全是不相干的概念。
同步与互斥:
解决的是进程/线程 对访问临界资源的制约。
阻塞与非阻塞都是 对待调用的结果的方式而区别的。
阻塞调用:指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
非阻塞调用:不能立刻得到结果之前,该调用不会阻塞当前线程.
非阻塞IO,纪录锁,系统V流机制,
I/O多路转接(也叫I/O多路复用),
readv和writev函数以及存储映射IO(mmap)
本篇着重讨论IO的多路转接
#include
#includeint fcntl(int fd, int cmd, ... /* arg */ );
传入cmd的值不同,后面追加的参数也不同
fcntl函数有5种功能:
①复制一个现有的描述符 (cmd=F_DUPFD)
②获得/设置文件描述符标记 (cmd=F_GETFD或F_SETFD).
③获得/设置文件状态标记 (cmd=F_GETFL或F_SETFL).
④获得/设置异步I/O所有权 (cmd=F_GETOWN或F_SETOWN)
⑤获得/设置记录锁 (cmd=F_GETLK,F_SETLK或F_SETLKW).
通过 fcntl 设置一个非阻塞的fd。
非阻塞:
此时进程不再等待 用户输入,而是非阻塞的。一旦用户输入数据 也会立马读取数据。
从上述的例子可以看出,read/recv/write/send也可以进行 等待。但他始终只能等待一个。
系统提供select函数,是实现多路IO的复用输入输出的模型。
性质:
select系统调用是用来让我们的程序监视 多个文件描述符的状态变化的。程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。
select函数原型:
int select(int nfds,fd_set *readfds, fd_set *writefds,fd_set *exceptfds,
struct timeval*timeout);
nfd:需要监视的最大文件描述符+1
readfds\writefds\exceptfds:需要监视的事件
timeout:select()等待时间
//查看fd_set 结构体
vim /usr/include/sys/select.h
因此,select如何进行等待事件就绪,本质就是由用户传入fd_set位图结构,并由select进行对应的fd监管。一旦 受监管的fd上的事件就绪,就会立马在fd_set位图结构上进行标识。
1.用户告知内核,需要对那些fd上的事件,进行监管。
2.内核告知用户,关心的fd上的哪些事件已经就绪。
这两个功能,是理解多路转接的关键一步。
timeout取值:
NULL:select将一直被阻塞,直到某个文件描述符上发生了事件。(阻塞)
0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。(非阻塞)
特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。timeout返回:
1.只要不就绪,不返回。
2.不就绪,立马返回。
3.设置deadline,dealine内 ,deadline外。
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
执行成功则返回文件描述词状态已改变的个数
如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
当有错误发生时则返回-1,错误原因存于errno。
下面代码仅作演示。展现select如何 进行读事件就绪
为什么用select需要额外定义数组?
fd_array初始化 以及 设置监听fd
对select返回值判断:
select就绪事件:
select就绪事件读取:
最后也就是这样。可以看出,我们此时就仅仅用一个单进程,就可以对接 之前用多进程、多线程才能做的,同时接收多条链接请求。
特点
1.fd_set:不同编译器下,fd_set的大小是不定的,比如说刚刚我的linux下,fd_set=128(字节);
因为1比特位标识 一个文件描述符信息,因此支持的最大文件描述符的个数为
: 128*8
2.fd_array:
将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,
缺点
1.每次需要重新设置,进行遍历(fd_set本质上是 输入输出型参数)
2.能够同时进行检测的fd_set 是有上限的。3.需要经过OS进行轮询检测fd上就绪的事件。
4.数据拷贝的代价体现在,不同从用户态 之间 内核态的来回切换。
严格来说,poll并没有多重要。实际应用中也较少。更多的会去使用epoll来代替。但是仍然对初学者而言,有一定的学习意义。加深理解。
函数原型:
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);struct pollfd *fds:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};nfds:fds数组的长度
timeout:超时时间
在上面也讲了多路转接的核心是要找到:
用户---->内核
内核---->用户。
//查询 poll
vim /usr/include/sys/poll.h
可以看出,poll结构体内的 用户与内核 已经分开了。不再是作用于同一份 fd_set。
那么对于poll而言,是怎么设置fd 所关心的事件的?
事件 | 描述 | 是否可输入 | 是否可输出 |
POLLIN | 数据可读 | 是 | 是 |
POLLOUT | 数据可写 | 是 | 是 |
POLLHUP | 表示对应的文件描述符被挂断 | 否 | 是 |
EPOLLERR | 表示对应的文件描述符发生错误 | 否 | 是 |
当然不止上面这四种,当然还有很多。
poll函数的返回值也和select一样也就不再多言
按照man手册的说法: 是为处理 大批量句柄而作了 改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.
同样,多路转接核心的 要义,对于epoll同样适用
int epoll_create(int size);
创建一个epoll句柄
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
epfd:epoll返回值的句柄
op:epoll事件函数
EPOLL_CTL_ADD (注册)
EPOLL_CTL_MOD (修改)
EPOLL_CTL_DEL (删除)
fd:需要监听的fdevent:告诉内核需要 监听fd 的事件是什么
epoll_event结构:
int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout);
event: epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
maxevents:告诉epoll events有多大
timeout:超时时间
因此对于epoll而言:
epoll_ctl:用户告知内核
epoll_wait:内核告知用户
首先对套接字注册事件。
等待数据就绪。
读事件
可能你会对epoll到底是怎样做到 让epoll_wait 后的就绪依次 放入你给定的数组里。
那么我们就不得不谈及epoll的原理。
想象一下,两种情况。
马上要吃饭了。你妈妈喊你吃饭。但是你有事情要做。
第一种:你妈妈一直喊你,直到你坐到座位上 把饭吃了。(水平触发) LT
第二种:你妈妈只喊你一次,之后再也不喊你了。 (边缘触发) ET
LT:只要通知后,没有任何作为,则会一直通知。直到事情了。
ET:有且通知一次,再也不会有任何通知。
ET vs LT
LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数.
但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完。
另一方面 ET模式代码复杂程度也就更高。对程序员 提出了更高的要求。
此时我们将读屏蔽掉,也就是默认情况下(LT)下,让epoll一直通知。
设置为ET;
这不仅仅体现在通知机制策略上,而是让上层应用更快地把数据拿走,TCP由此可以更新出更大的滑动窗口大小,提高底层发送数据的效率,更好的支持TCP诸如延迟应答等策略!
讲了好几个多路转接的函数。并没有特例地应用到一些场景。
我们自己基于epoll,写一个简易的计算器。
反应堆:
server端:
Accepter链接管理器:
Recver:
Sender:
Errorer:
测试:
①常见的五种IO模型:阻塞、非阻塞、异步、多路转接、信号
②IO的本质:等+拷贝数据
③多路转接的核心:
用户告知内核,你需要帮我监听哪些fd
内核告知用户,哪些设置为监听的fd上的事件 就绪。
④select的核心在于:fd_set 内核用户共用一份
⑤poll的核心在于:strcut pollfd,里面实现了用户 与 内核的分开
⑥epoll的核心在于:epoll_ctl \ epoll_wait
⑦为什么基于epoll下的ET fd需要设置为非阻塞?
以上就是本章的所有内容,感谢你的阅读啊
祝你好运!