我是靠谱客的博主 雪白小土豆,最近开发中收集的这篇文章主要介绍「网络」套接字 (Socket),觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

  • 内核态与用户态

    1. 内核态: 可以访问系统资源, 比如CPU, 内存, 网络, 外设

    2. 用户态: 只能访问进程自己的资源, 无法访问系统资源

    3. 用户态访问系统资源时, 需通过系统调用, 即CPU切到内核态, 读取资源后再切回用户态. 中间涉及堆栈上下文的切换, 为避免频繁切换, 有了"用户缓冲区"和"系统缓冲区"

    4. 当用户进程需要从"磁盘/网络"中读取数据时, 系统会将"系统缓冲区"的数据复制到"用户缓冲区". 若"系统缓冲区"中没有对应数据, 系统会将当前进程挂起, 处理其他进程. 等数据到达"系统缓冲区"后, 系统将数据拷贝至"用户缓冲区", 然后才会通知进程, 不同IO模型通知时机不同

    5. 调用

      1. 系统调用

        1. 系统调用指运行在用户态的程序向操作系统内核请求系统资源

        2. 系统调用是用户程序与操作系统之间的接口

        3. 系统调用由操作系统内核提供, 运行在内核态

      2. 函数调用

        1. 函数调用由库函数或用户提供, 运行在用户态

        2. 主要通过压栈操作来进行函数调用

  • 套接字 (socket)

    1. 定义: 进程间通信流的端点, 是一种进程间通信机制, 由五元组(本地IP+本地端口+远程IP+远程端口+协议)唯一标识套接字, 一个套接字对应着一条连接

    2. 位于应用层和传输层之间的一个抽象层, 便于应用程序更方便的将数据经由传输层传输

    3. 每个套接字在"系统缓冲区"中都有"发送缓冲区"和"接收缓冲区", 并提供有send和receive方法

    4. 数据发送: 由"用户缓冲区"拷贝到"发送缓冲区", 拷贝的过程通过客户端套接字的send方法完成, 需要CPU参与. 最后数据将从"发送缓冲区"拷贝到网卡中, 从内存到网卡通过DMA(类似协处理器)拷贝, 不需要CPU参与.

    5. 数据接收: 通过"网卡"接收, 数据通过DMA拷贝至"接收缓冲区". 再由"接收缓冲区"通过CPU拷贝至"用户缓冲区", 该过程通过服务端套接字的receive方法完成.

    6. 当服务器网卡收到的数据校验成功后, 会向客户端发送ACK, 客户端再清除客户端套接字"发送缓冲区"里的数据

  • 五种IO模型, IO即磁盘/网络读写

    1. c10k 一台计算机实现10000台客户端的连接, 并处理对应的请求. 一个进程处理一个连接, 会消耗过多资源. 所以要一个进程处理多个连接, 即IO多路复用, 目前通过IO多路复用已实现c10k.

    2. 五种IO模型 (参考自https://zhuanlan.zhihu.com/p/54580385)

      1. 所有的IO模型都分为两阶段, 前四个模型, 第二阶段复制数据阶段都是阻塞的.

        1. 等待系统将数据准备好, 即等待数据从网卡/磁盘复制到"系统缓冲区", 需要DMA参与

        2. 将数据从"系统缓冲区"复制到"用户缓冲区", 需要CPU参与

      2. 阻塞式IO, 第一阶段"请求进程"调用receive, 一直阻塞至第二阶段完成

      3. 非阻塞IO, 第一阶段"请求进程"不停调用receive, receive立即返回异常. 直至"数据已备好"后, "请求进程"再调用receive会阻塞至第二阶段完成

      4. IO多路复用(即事件驱动IO), 第一阶段"请求进程"调用select, 并阻塞至"数据准备好". 第二阶段"请求进程"发起receive, 阻塞至拷贝完成. 虽然看起来"请求进程"都是阻塞, 但是服务器内核可以同时监听select负责的多个套接字, 服务器效率非常高

      5. 信号驱动IO, 第一阶段"请求进程"向内核注册信号后, 不阻塞. 当"数据准备好"后, 内核通过信号通知"请求进程", "请求进程"调用receive, 阻塞取得数据

      6. 异步IO, 第一阶段"请求进程"发起receive, 不阻塞. 当第二阶段数据拷贝完后, 内核会通过回调通知"请求进程".

    3. nginx是事件驱动服务器的代表, 适合IO密集型服务, 使用的是epoll; Apache是线程服务器的代表, 适合计算密集型服务(apache存疑)

    4. I/O多路复用 (select, poll, epoll)

      1. select

        1. select函数

          1. 定义

            select用来监视多个文件描述符, 当文件描述符所对应事件状态不改变时, select会阻塞. 当某个文件描述符所对应事件状态改变后, select会返回

          2. Python案例

            readable_list, writeable_list, execable_list = select.select(read_list, write_list, exce_list, timeout) 当read_list的fd满足可读条件时, 则将发生变化的fd添加到readable_list中 当write_list的fd满足可写条件时, 则将发生变化的fd添加到writeable_list中 当exce_list的fd发生错误时, 则将该发生错误的fd添加到execable_list中

        2. 原理

          1. select通过内核来监视一个由多个文件描述符(fd)组成的数组. 当select返回后, 数组中就绪的文件描述符会被内核修改标记位, 进程便可以通过遍历数组, 来获得这些文件描述符并从而进行后续的读写操作

          2. 进程会指定内核监听哪些文件描述符的哪些事件, 当没文件描述符所监听的事件发生时, 进程被阻塞, 当一个或者多个文件描述符事件发生时, 进程则被唤醒

          3. 文件描述符数组: 位图(bitmap), 最大一般为1024位, 即最大可监听1024个文件描述符. 当有事件发生时, 对应的位会被标记为1. 当下一次监听时, 位图需全部置为0

        3. select被调用时的过程

          1. 上下文切换, 将用户态切换为内核态, 并将fd数组从用户空间复制到内核空间

          2. 内核遍历fd数组, 查看这些文件描述符对应事件是否发生. 如果这些文件描述符无对应事件发生, 进程将阻塞. 当设备驱动产生中断或者timeout时间后, 进程唤醒并再次进行遍历. 持续该过程直至有事件发生

          3. 内核标记发生事件的fd, 并将fd从内核空间复制到用户空间, 并从内核态切换为用户态

        4. select缺点

          1. 单个进程能够监视的fd数量存在限制, 在linux上为1024

          2. 当fd数组很大时, 因每次调用select都需要把fd数组从用户空间拷贝到内核空间, 所以开销很大

          3. 当fd数组很大时, 内核对fd数组的遍历浪费时间

          4. 下一次监听时, 位图需全部置为0

      2. poll

        1. 原理

          1. 结构 struct pollfd { int fd; # 文件描述符 short events; # 该文件描述符注册的事件集合 short revents; # 该文件描述符事件状态发生变化的事件集合 }

          2. poll通过内核监视多个pollfd组成的数组, 当任一监听的pollfd变化时, revents会被标记并返回. 且下一次监听时, events不用重新置位. poll本质上和select无区别, 只是没有最大连接数的限制, 原因是它基于链表存储

        2. poll缺点

          1. 当fd数组很大时, 因为每次调用poll都需要把pollfd数组从用户空间拷贝到内核空间, 所以开销很大

          2. 当fd数组很大时, 内核对pollfd数组的遍历浪费时间

      3. epoll

        1. 在freeBSD系统上没有epoll, 请使用kqueue. kqueue与epoll的不同之处: kqueue的一个fd的read/write事件需要分开注册, 而epoll则是一个fd一次注册read/write事件

        2. 结构

          struct epollfd {

          int fd; # 文件描述符

          short events; # 该文件描述符注册的事件集合 }

        3. 函数

          1. epoll_create: 内核空间中建立红黑树, 双向就绪链表

          2. epoll_ctl

            1. 向内核空间的红黑树中塞入epollfd

            2. 为该事件文件描述符注册一个回调函数. 当该事件文件描述符监听的事件就绪后, 该回调函数会把就绪的epollfd加入到双向就绪链表中

          3. epoll_wait: 检查双向就绪链表中是否有元素, 没有就睡眠, 有就将epollfd从内核空间拷贝至用户空间, 并返回

        4. 高效理由

          1. 对于select缺点一, epoll所支持的最大fd数量是系统中可打开文件的最大数目, 一般该数目和内存有较大关系

          2. 对于select缺点二, epoll_ctl会将epollfd从用户空间拷贝到内核空间, 之后每次调用epoll_wait只是检查就绪链表, 返回已就绪的, 不像select, poll返回所有的

          3. 对于select缺点三, 因为注册了回调函数, 故不需要内核遍历

        5. 通知模型 (参考自https://blog.csdn.net/fengxinlinux/article/details/75331567)

          1. 水平触发(LT)

            1. 文件描述符上有数据变化时, 触发一个事件. 可以只读该事件有关的一部分数据, 未读完则下次检查时会再触发事件来通知

            2. 优点是稳定可靠, 缺点是当就绪的文件描述符过多时效率低

          2. 边缘触发(ET)

            1. 文件描述符上有数据变化时, 触发一个事件. 需要一次读完该事件有关的数据, 否则之后不会再触发事件来通知

            2. 优点是高效, 缺点是不可靠, 实现复杂

          3. select, poll仅支持水平触发; epoll支持水平触发和边缘触发, 默认采用水平触发; nginx采用的是边缘触发

 

最后

以上就是雪白小土豆为你收集整理的「网络」套接字 (Socket)的全部内容,希望文章能够帮你解决「网络」套接字 (Socket)所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(36)

评论列表共有 0 条评论

立即
投稿
返回
顶部