校招后端面经——网络IO

校招后端面经--网络I/O

        • 1. select
          • wakeup callback机制
          • 过程
          • 问题
        • 2. poll
        • 3. epoll
          • 改进方案
          • 两种模式
            • ET(边沿触发)
            • LT(水平触发)
            • 性能比较:
        • 4. socket连接过程
          • 1. 背景
            • TCP协议维护两个socket的缓冲区:发送缓冲和接收缓冲
            • 两种套接字:监听套接字和已连接套接字
          • 2. 具体过程

1. select

wakeup callback机制

Linux通过socket的睡眠队列来管理所有等待socket的某个事件的进程,同时通过wakeup机制来异步唤醒整个睡眠队列上等待事件的进程,通知进程相关事件的发生。当socket的事件发生的时候,其会顺序遍历socket睡眠队列上的每个进程节点,调用每个进程节点上的回调函数启动进程去读socket上的数据。

过程

当进程需要读取N个socket上的数据时,它会被插入到N个socket的睡眠队列中等待任意一个socket数据可读,当其中任意一个socket有数据可读时,会唤醒挂在上面的进程,进程会遍历自己监控的所有的socket,挨个查找是否有数据可读,若有数据可读,则读取数据

问题
  1. 被监控的socket的文件描述符的集合太小,限制为1024,
  2. 文件描述符集合需要从用户空间拷贝到内核空间
  3. 当某一个socket有数据可读时,会遍历所有的socket,而不是只对其中可读的socket进行遍历

2. poll

只是解决了select的问题一,改变文件描述符集合的描述方式,增大了集合的容量

3. epoll

使用了中间件解决了select遗留的问题

改进方案
  1. 引入epoll_ctl的系统调用,把高频调用的epoll_wait和低频的epoll_ctl隔离开,使用epoll_ctl来对fds集合进行增删改查。
  2. epoll通过内核与用户空间mmap(内存映射)同一块内存地址,减少用户态和内核态的数据交换
  3. 引入中间层,即一个双向链表(准备队列),一个单独的睡眠队列。刚开始时进程会先到准备队列上查找是否有相关的socket有事件可读,若没有,等待socket事件的进程直接插到单独睡眠的睡眠队列中,而不是socket上的睡眠队列。同时设置一个wait_entry_sk与socket密切相关,wait_entry_sk放在socket的睡眠队列上,wait_entry_sk有一个回调函数,作用是当socket上有数据可读时,把当前的socket插入到准备队列中,并唤醒单独睡眠队列的相关进程。单独睡眠队列上的进程节点的回调函数的作用为,遍历准备队列上的所有socket,挨个收集socket上的数据。
两种模式
ET(边沿触发)

socket的接收缓冲区状态发生变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件

socket的发送缓冲区状态发生变化时触发写事件,即满的缓冲区刚空出来时触发写事件

原理:遍历epoll的准备队列,将socket从准备队列中移除,然后收集这个socket上的数据,不管数据有没有全部读完,都不会再放在准备队列中,直到socket上有新的数据可读

LT(水平触发)

socket接收缓冲区不为空,有数据可读,则读事件一直触发

socket发送缓冲区不满可以继续写入数据,则写事件一直触发

原理:遍历epoll的准备队列,将socket从准备队列中移除,然后收集这个socket上的数据,若数据没有读完还有剩余,则重新放回准备队列中

性能比较:

当服务端有海量活跃的socket时,LT模式下,epoll_wait返回时,会有海量的socket放入准备队列中,但有些socket上的数据是不需要的,这就导致会遍历到一些没有用的socket节点,影响性能。

但如果有海量活跃socket并不多的,LT模式下,对socket的遍历便不再是多余的

4. socket连接过程

1. 背景
TCP协议维护两个socket的缓冲区:发送缓冲和接收缓冲

通过TCP连接发送出的数据都会先拷贝到发送缓冲,可能是从用户空间的app buffer拷入,也可能是从内核的kernel buffer拷入,拷入过程通过send()函数完成;接收时也是先接收到接收缓冲中,再从接收缓冲拷入app buffer或者kernel buffer

校招后端面经——网络IO_第1张图片

两种套接字:监听套接字和已连接套接字

当服务进程从配置文件中解析出要监听的地址和端口,然后通过socket()函数创建监听套接字,通过bind()函数将该套接字绑定到对应的地址和端口上。随后进程或者线程就可以通过listen()函数来监听这个端口。

当TCP三次握手成功后,通过accept()函数返回套接字,后续进程或者线程就可以通过这个套接字和客户端进行TCP通信

2. 具体过程

校招后端面经——网络IO_第2张图片

  1. 进程或者线程在监听的过程中,它阻塞在select()或poll()上,直到有数据(SYN信息)写入它所监听的socket中,内核被唤醒,并将SYN数据拷贝到内核的缓冲中进行处理,并准备SYN+ACK数据,这个数据需要从内核缓冲拷贝到发送缓冲中,再拷入网卡传送出去。
  2. 这时,会在连接未完成队列中为这个连接创建一个新节点,并设置为SYN_RECV状态。
  3. 然后再次使用select或者poll函数监听这个套接字,直到有数据写入,内核再次被唤醒,如果写入的是ACK的信息,表示客户端对服务端内核发送的SYN的回应,于是把数据拷入内核缓冲处理完成后,把连接未完成队列中对应的项目移入连接已完成的队列当中,并设置为完成建立的状态。
  4. 然后accept函数处理已完成对列中的连接数据,创建一个已连接套接字,并从队列中删除这个数据。这样,两边的TCP连接顺利完成。

你可能感兴趣的:(面经)