✍个人博客:https://blog.csdn.net/Newin2020?type=blog
📣专栏地址:http://t.csdnimg.cn/fYaBd
📚专栏简介:在这个专栏中,我将会分享 C++ 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
160. SO_LINGER 选项
cpp
#include<sys/socket.h>
struct linger
{
int l_onoff; //开启(非0)还是关闭(0)该选项
int l_linger; //滞留时间
};
根据 linger 结构体中两个成员变量的不同值,close 系统调用可能产生如下 3 种行为之一:
- l_onoff 等于 0 (关闭):此时 SO_LINGER 选项不起作用,close 用默认行为来关闭 socket。
- l_onoff 不为 0 (开启),l_linger 等于 0:此时 close 系统调用立即返回,TCP 模块将丢弃被关闭的 socket 对应的 TCP 发送缓冲区中残留的数据,同时给对方发送一个复位报文段(RST)。因此,这种情况给服务器提供了异常终止一个连接的方法。
- l_onoff 不为 0 (开启),l_linger 大于 0:此时 close 的行为取决于两个条件:一是被关闭的 socket 对应的 TCP 发送缓冲区是否还有残留的数据;二是该 socket 是阻塞的,还是非阻塞的。对于阻塞的 socket,close 将等待一段长为 l_linger 的时间,直到 TCP 模块发送完所有残留数据并得到对方的确认。如果这段时间内 TCP 模块没有发送完残留数据并得到对方的确认,那么 close 系统调用将返回 -1 并设置 errno 为 EWOULDBLOCK。如果 socket 是非阻塞的,close 将立即返回,此时我们需要根据其返回值和 errno 来判断残留数据是否已经发送完毕。
161. 什么是 I/O 多路复用?
最基础的 TCP 的 Socket 编程,它是阻塞 I/O 模型,基本上只能一对一通信,那为了服务更多的客户端,我们需要改进网络 I/O 模型。
比较传统的方式是使用多进程/线程模型,每来一个客户端连接,就分配一个进程/线程,然后后续的读写都在对应的进程/线程,这种方式处理 100 个客户端没问题,但是当客户端增大到 10000 个时,10000 个进程/线程的调度、上下文切换以及它们占用的内存,都会成为瓶颈。
阻塞等待
BIO模型
非阻塞,忙轮询
NIO 模型
162. I/O 多路复用的 API
为了解决上面这个问题,就出现了 I/O 的多路复用,可以只在一个进程里处理多个文件的 I/O,Linux 下有三种提供 I/O 多路复用的 API,分别是:select、poll、epoll。
select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0 ~ 1023 的文件描述符。
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。
epoll 通过两个方面解决了 select/poll 的问题。
1、epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。
2、epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。
cpp
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...)
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
while(1) {
int n = epoll_wait(...);
for(接收到数据的socket){
//处理
}
}
而且,epoll 支持边缘触发和水平触发的方式,而 select/poll 只支持水平触发,一般而言,边缘触发的方式会比水平触发的效率高。
简单总结一下:
- select:
- select 使用一个包含所有文件描述符的位图数据结构(通常是 fd_set)。这个位图的大小通常由 FD_SETSIZE 宏定义决定,这个宏的默认值是 1024。
- 当调用 select 时,内核会遍历位图中的每个文件描述符,检查是否有可读、可写、或异常事件。这个遍历是线性的,因此性能受到文件描述符数量的限制。
- select 是水平触发,它会一直通知应用程序,直到所有就绪事件被处理。
- poll:
- poll 使用一个存放在用户态的数组来注册文件描述符。数组中的每个元素都包含了文件描述符和等待的事件。
- 当调用 poll 时,内核会遍历这个数组,检查每个文件描述符是否有就绪事件。和 select 一样,这个遍历也是线性的,因此性能受到文件描述符数量的限制。
- epoll:
- epoll 使用一个事件表(event table)来注册文件描述符,事件表是内核空间的数据结构。
- 当调用 epoll_create 时,内核会创建一个事件表,并返回一个文件描述符,应用程序可以使用这个文件描述符来操作事件表。
- epoll 使用事件通知机制。当文件描述符就绪时,内核会将这个事件添加到事件表中,然后将一个就绪队列中的事件通知应用程序。
- 应用程序可以调用 epoll_wait 来等待事件就绪,并获得就绪的事件列表,这个操作是阻塞的,直到有事件就绪。
- epoll 可以使用水平触发或边缘触发模式。在边缘触发模式下,只有在状态发生变化时才会通知应用程序。