Socket套接字(客户端,服务端)和IO多路复用

Socket套接字(客户端,服务端)

socket是什么

Socket(套接字) 是一种用于在网络中进行进程间通信(一般是不同主机之间进程)的编程接口。它提供了一种标准的方法,使得不同计算机上的应用程序能够相互通信。

一、在客户端

通过套接字(SOCKET)在客户端和服务器之间传输数据是网络编程中的一种常见方式。下面是详细步骤,包括相关的内核态和用户态切换的说明。

1. 创建套接字
  • 用户态
    在客户端使用系统调用 socket() 创建一个套接字。
  • 内核态
    系统调用进入内核态,内核为套接字分配资源并返回文件描述符。
2. 设置服务器地址
  • 用户态
    定义服务器的地址结构并设置相关信息,如 IP 地址和端口号。
3. 连接到服务器
  • 用户态
    使用 connect() 系统调用发起连接。
  • 内核态
    连接请求被发送到服务器,内核处理 TCP 握手(SYN、SYN-ACK、ACK)以建立连接。
4. 发送数据
  • 用户态
    使用 send()write() 系统调用发送数据

  • 内核态
    数据被复制到内核缓冲区,内核负责将数据打包并通过网络协议栈发送到服务器。

5. 接收数据
  • 用户态
    使用 recv()read() 系统调用接收来自服务器的数据。
  • 内核态
    内核检查网络接口接收缓冲区,若有数据则将数据复制到用户空间的缓冲区,并返回接收到的字节数。
6. 关闭连接
  • 用户态
    使用 close() 系统调用关闭套接字。
  • 内核态
    内核释放与套接字相关的资源,处理 TCP 的四次挥手(FIN、FIN-ACK、ACK)以关闭连接。

二、内核态与用户态切换

  • 当用户态程序调用系统调用(如 socket()connect()send()recv() 等)时,CPU 从用户态切换到内核态,执行内核中的相关函数。
  • 完成内核任务后,内核会将控制权返回给用户态程序,继续执行。
  • 系统调用作为软中断:系统调用本质上是一种软件中断(也称为“软中断”)

三、系统调用与上下文切换的关系

系统调用确实涉及到 CPU 上下文切换

  1. 用户态与内核态

    • 在现代操作系统中,CPU 有两种运行模式:用户态和内核态。用户态是应用程序运行的状态,而内核态是操作系统内核执行的状态。
    • 系统调用通常发生在用户态到内核态的切换。
  2. 发起系统调用

    • 当应用程序需要访问硬件资源或执行特权操作(如文件读写、网络通信等)时,它会通过特定的系统调用接口发起请求。这一请求会触发一个软中断,或者直接调用一个特殊的指令(如 syscallint 指令)。
  3. 上下文切换步骤

    • 保存当前上下文:在发起系统调用时,操作系统需要保存当前用户程序的执行状态(包括寄存器内容、程序计数器等),以便在返回时能够继续执行。
    • 切换到内核态:操作系统会将 CPU 模式从用户态切换到内核态,并开始执行相应的系统调用处理程序。
    • 执行系统调用:内核根据系统调用的类型执行具体操作。
    • 恢复上下文:系统调用完成后,操作系统将之前保存的用户程序上下文恢复,并将 CPU 模式从内核态切换回用户态。
    • 继续执行:最后,控制权返回给用户程序,继续其执行。

四、在服务端

1. 创建 Socket (用户态)
2. 绑定
3. 监听
4. 接受连接

服务端通过 accept 方法阻塞等待客户端的连接。

当有客户端尝试连接时:
内核态会处理 TCP 三次握手。
连接建立,accept() 方法将返回一个新的 socket 对象(用于与客户端通信)以及客户端的地址信息。

socket返回一个fd(文件描述符),对fd的操作就是对io文件流的操作。

对于多个客户端的连接,往往采用IO多路复用的方法,监控返回的多个socket对象。

准备 fd_set(select方法特有),在使用 select() 之前,你需要创建并初始化一个 fd_set 结构体,来表示你希望监控的文件描述符(FD)。这个结构体可以包含多个 socket 描述符
除了select()使用的 fd_set(位图),还可能是数组(poll),红黑树/链表(epoll)

调用 select() 函数
一旦你设置好 fd_set,就可以调用 select() 函数。这个函数会阻塞,直到至少有一个 socket 变为就绪状态。

处理就绪的 Socket
当 select() 返回后,需要检查哪个 socket 已经就绪。这可以通过再次遍历你的 fd_set 来完成

5. 接收消息 (用户态 -> 内核态)

服务端使用 recv 方法接收数据。

用户态发起 recv 调用,请求从 socket 中读取数据。
控制权切换到内核态,内核检查是否有可读的数据。
如果有数据,内核将数据复制到进程的地址空间(用户态),然后返回给用户态。

五、IO多路复用

与多进程和多线程技术相比,IO 多路复用技术的最大优势是系统开销小,系统不必创建进程或线程,也不必维护这些进程,从而大大减小了系统的开销。
select()、poll() 和 epoll() 都是用于实现 I/O 多路复用的系统调用

主要区别
特性select()poll()epoll()
文件描述符限制限制为 FD_SETSIZE(通常为1024)没有限制没有限制
数据结构位图数组红黑树/链表
性能性能随监视的文件描述符数量增加而降低能够处理较多的文件描述符性能稳定,尤其在监视大量文件描述符时
灵活性不灵活,需每次调用前设置相对灵活,能够处理

在这里插入图片描述

1. select()
  • 用户进程需要监控某些资源 fds,在调用 select 函数后会阻塞,操作系统会将用户线程加入这些资源的等待队列中。
  • select() 的内部实现通常使用一个时间轮询机制,通过不断检查每个文件描述符的状态来判断其是否就绪。
  • 直到有fd就绪(有数据可读、可写或有 except异常)或超时(timeout 指定等待时间,如果立即返回设为 null即可),函数返回。
  • select 函数返回后,中断程序唤起用户线程。用户可以遍历 fds,通过 FD_ISSET 判断具体哪个 fd收到数据,并做出相应处理。

优点

  • 简单,易于理解和使用。
  • 标准化,几乎所有 UNIX-like 操作系统都支持。

缺点

  • 文件描述符数量受到限制,32 位系统最多能监听 1024 个 fd,64 位最多监听 2048 个。
  • 每次调用 select 都需要将进程加入到所有监视 fd 的等待队列(每个fd有自己的等待队列,里面放的需要此fd资源的进程),每次唤醒都需要从每个队列中移除。 这里涉及了两次遍历,而且每次都要将整个 fd_set 列表传递给内核,有一定的开销。
  • 当函数返回时,系统会将就绪描述符写入 fd_set 中,并将其拷贝到用户空间。进程被唤醒后,用户线程并不知道哪些 fd 收到数据,还需要遍历一次。
  • 对于大量的文件描述符,性能会显著降低。
2. poll()
  • 数组结构:poll() 使用一个结构体数组(struct pollfd)来存储要监视的文件描述符及其事件类型。这使得它能够监视更多的文件描述符,而不受 FD_SETSIZE 的限制。
  • 事件标志:每个文件描述符都可以设置多个事件类型,如可读、可写等,并且可以通过事件结果获取哪些事件发生了。

优点

  • 没有文件描述符数量的硬性限制(受限于系统资源)。
  • 适合处理大量的文件描述符。

缺点

  • 和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。
  • 事件检测偏向线性搜索,对于高负载场景性能较差。
3. epoll()

步骤:

  1. 创建 epoll 实例:调用 epoll_create() 或 epoll_create1() 来创建一个 epoll 实例,这将返回一个用于后续操作的文件描述符。
  2. 注册文件描述符
  3. 等待事件:当注册的文件描述符上发生事件时,可以调用 epoll_wait() 阻塞等待事件的发生。该函数会填充就绪的事件信息。
  4. 处理事件:根据 epoll_wait() 返回的就绪事件,进行相应的处理。每个事件包含了发生事件的文件描述符和事件类型,应用程序可以据此决定如何响应。
  5. 修改或删除事件:在事件处理过程中,如果需要修改现有的事件或从 epoll 中删除某个文件描述符,可以再次调用 epoll_ctl()。
  6. 清理资源

epoll 主要基于事件通知机制,它允许用户注册需要监控的文件描述符,并在这些文件描述符上发生特定事件时得到通知。与 select() 和 poll() 的线性扫描不同,epoll 使用了一种基于事件的模型,可以显著提高性能。

epoll 使用一个文件描述符管理多个描述符,将用户进程监控的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间只需拷贝一次。

所有 FD 集合采用红黑树存储,就绪 FD 集合使用链表存储。这是因为就绪 FD 都需要处理,业务优先级需求,最好的选择便是线性数据结构。
优点

  • 高效,尤其适合大量文件描述符的情况,性能不会随着监视的文件描述符数量增加而明显下降
  • 支持边缘触发和水平触发模式,可以更灵活地管理事件。

缺点

  • 仅在 Linux 环境下可用
epoll工作模式

水平触发(Level Triggered, LT) 默认模式
特点:只要文件描述符满足条件(如数据可读或可写),就会被反复通知,直到你处理了该事件。
适用场景:适用于大多数传统的阻塞式 I/O 模型。

边缘触发(Edge Triggered, ET)
要求更加严格,只有当文件描述符的状态从“无事件”变为“有事件”时,epoll 会通知一次。
特点:只会在状态发生变化的边缘时触发通知,如果你没有在第一次通知时完全处理事件,它不会再次通知你,除非事件再次变化。
适用场景:通常用于高性能、非阻塞 I/O 模型,适合减少不必要的通知,优化性能。

为何高效
1) epoll 精巧的使用了 3 个方法来实现 select 方法要做的事,分清了频繁调用和不频繁调用的操作。

epoll_ctrl 是不太频繁调用的,而 epoll_wait 是非常频繁调用的。而 epoll_wait 却几乎没有入参,这比 select 的效率高出一大截,而且,它也不会随着并发连接的增加使得入参越发多起来,导致内核执行效率下降。

2) mmap 的引入,将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。

3)红黑树将存储 epoll 所监听的 FD。高效的数据结构,本身插入和删除性能比较好,时间复杂度O(logN)。

select()开销
  1. 调用过程中的开销

当你调用 select() 时,通常需要进行以下几个步骤:

  • 设置文件描述符集合:在用户空间中,你需要创建并初始化一个 fd_set 结构,包含所有你希望监视的文件描述符。这一步需要你手动管理这个集合。

  • 进入系统调用:在进行 select() 系统调用时,整个 fd_set 集合会被传递给内核。因为 fd_set 是一个位图,表示一组文件描述符的信息,内核需要扫描这个集合来检查每个文件描述符的状态(可读、可写等)。这意味着内核会在进入系统调用时遍历你的 fd_set,检查每一个文件描述符的状态。

  • 注册等待:内核将进程加入到所有这些文件描述符的等待队列中,每个文件描述符都有自己的等待队列。(当多个进程或线程对同一文件描述符执行 I/O 操作时,可能会出现需要等待的情况,所以每个文件描述符都有自己的等待队列。)当某个文件描述符变为就绪状态时,内核会将进程从这个队列中移除,从而唤醒进程。这就涉及到了两次遍历:一次是检查所有文件描述符的状态,另一次则是在返回时将进程从对应的队列中移除。

  1. 返回结果时的开销

select() 函数返回时,内核会更新你的 fd_set 结构,标记哪些文件描述符已经就绪。此时,内核需要将这些状态信息拷贝回用户空间。这个过程也有几步:

  • 填充 fd_set:内核会根据它所监视的文件描述符状态,更新你的 fd_set 结构。这种更新是基于内核对文件描述符的实际状态的判断。

  • 拷贝到用户空间:更新完成后,内核需要将这个更新的 fd_set 结构拷贝到你的程序的用户空间,因为用户程序需要知道哪些文件描述符现在可以进行 I/O 操作。

  1. 用户层处理

一旦 select() 返回,用户线程接下来要处理的是已就绪的文件描述符。即便内核已经在 fd_set 中更新了状态,用户还是需要遍历这个 fd_set,找出哪些文件描述符已经准备好进行操作。这意味着:

  • 用户程序需要再次遍历 fd_set,这增加了额外的开销。
  • 这也使得处理逻辑更加复杂,因为用户需要解析哪些文件描述符是就绪的,并做相应的处理。
位图是什么

位图(Bitmap)是一种用于表示集合的高效数据结构,它使用一组位(binary digits,0 和 1)来表示某个元素是否存在于集合中。这种表示方式非常节省空间并且可以快速进行查找、插入和删除等操作。

位图的基本概念

  • 每个位表示一个元素:在位图中,每一个位置对应一个特定的元素。例如,如果你有一个最多包含 n 个元素的集合,那么可以用 n 个位来表示。
    • 如果某一位为 1,则表示该元素在集合中。
    • 如果某一位为 0,则表示该元素不在集合中。

示例

假设我们有一个整数集合 {0, 2, 3, 5},我们可以使用位图来表示这个集合。假设我们的集合元素范围是从 0 到 7(即 8 个可能的元素),那么我们就可以用 8 位来表示:

元素索引: 0  1  2  3  4  5  6  7
位图:     1  0  1  1  0  1  0  0

在这个例子中:

  • 位图的第 0 位为 1,表示 0 存在于集合中。
  • 位图的第 1 位为 0,表示 1 不在集合中。
  • 位图的第 2 位为 1,表示 2 存在于集合中。
  • 以此类推。

位图的优点

  1. 空间效率:相较于其他数据结构(如链表),位图在存储稀疏集合时占用更少的空间。
  2. 快速访问:可以通过简单的位运算快速检查元素是否存在、添加或删除元素,例如使用位与(AND)、位或(OR)等操作。
  3. 简化算法:位图可以使一些算法变得更加简单和高效,特别是在处理大规模数据和稀疏数组时。

位图的缺点

  1. 固定大小:位图需要预先定义范围,如果元素超出这个范围,将会导致无法再添加新的元素。
  2. 浪费空间:如果集合中的元素分布稀疏,位图可能会造成大量未使用的位,从而浪费空间。

应用场景

  • 集合操作:如求并集、交集、差集等。
  • 内存管理:用于表示内存块的使用情况,例如进程的页表。
  • 网络协议:用于标识已收到的数据包。
  • 图像处理:在某些情况下,可以用位图表示像素状态。

总结
位图是一种非常有效率的数据结构,尤其适用于那些元素范围固定且需要频繁查询的场合。它的优势在于极大的提升了元素存在性检测的速度,并且在某些应用中提供了显著的空间节约。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值