I/O 复 用与并 行行程序
并发与复 用 回顾之前的程序 ( 无论 UDP/TCP) 其 一 : 单个 Server, 单个 Client 其 二 : 单个 Server, 多个 Client 但客户端都是依次被服务器器端受理理并执 行行 函数是阻塞的, 怎么办
两种类型的服务器器端 第 一种 : 第 一个连接等待受理理时间为 0s, 第 50 个等待受理理时间为 50s, 第 100 个连接等待受理理时间为 100s, 依次类推 每个连接服务时间仅 1s 第 二种 : 所有连接请求的受理理时间不不超过 1s, 平均服务时间 越 2-3s 解决 方案 并发服务器器 多进程同时处理理客户端的连接请求 I/O 复 用的 方式轮询客户端的连接请求 [ 单进程!]
I/O 复 用 - 单进程的并发服务器器实现 用多进程并发服务器器的缺点 每次请求都需要创建新的进程, 创建 维护进程需 要消耗运算和内存空间 进程独 立存在, 可能涉及数据交换 实现多进程的技术 手段较复杂
不不创建进程 而实现并发服务器器 同时向多个客户端提供服务, 每个客户端不不阻塞其 它客户端 复 用 (Multiplexing) 技术 复 用的思维被 用在多个领域
不不创建进程 而实现并发服务器器 复 用 (Multiplexing) 技术 [ 通信 工程 ] 在同 一个通信信道离传递多个数据 [ 网络 ] 提 高物理理设备的效率, 用最少的物理理要素 传递最多的数据
通信中的多路路复 用 ( 复 用 ) 模型 时分多路路 频分多路路 码分多路路 空分多路路
纸杯电话 的复 用
Socket 服务器器上的复 用 服务器器端并发处理理多个客户端的连接 需要建 立多个进程 / 线程分别处理理
引 入复 用技术, 对服务器器端的应 用层程序进 行行复 用 实现单个进程处理理多个客户端的输 入请求 我们称之为 I/O 复 用
具体来说, 服务器器端的 I/O 复 用即 : 服务器器的应 用监测指定的客户端 若某客户端有请求产 生, 则处理理 若客户端 无请求产 生, 则略略过
I/O 复 用的 一个类 比 上课回答问题 方式 一 : 给每个学 生配备 一个 老老师, 一对 一, 一旦 某学 生有问题 老老师能 马上处理理, 但是该学 生没有问 题的时候 老老师必须等待 方式 二 : 一个聪明的 老老师, 多个学 生, 聪明的 老老师 周期性地扫视整个课堂, 有举 手的学 生则点他起来 处理理问题, 没有的话就继续保持观测
select() 函数 最具有代表性的实现复 用服务器器的 方法 常被程序员忽略略的 方法 winsock 和 BSD socket 等等都有该 方法 ( 或其扩展 )
select() 函数主要功能 将多个 文件描述符集中在 一起统 一监视 是否存在套接字有待接收数据? 是否存在套接字有 ( 无阻塞的 ) 待传输数据 是否存在套接字发 生了了异常
select() 函数的 用法
select() 函数 用法 - 步骤 一 设置 文件描述符 select() 可以同时监视多个 文件描述符 注意 文件描述符可以是 stdio 文件 设备 socket 等等 文件描述符被集中监视 维护 一张表, 表示同类的 文件描述符 监视项也是分类的 待接收 待传输 发 生异常
监视 文件描述符的表 (fd_set) 如何定义与初始化
设置 一个 文件描述符的表 fd_set, 结构如图 一个数组, 每 一位表示系统中的 一个 文件描述符, 如图第 一位表示 fd0, 第三位表示 fd2 每位的元素可为 0/1 若为 0, 表示 select 不不监视此 fd 若为 1,
如何改变表中的数值 我们命名这个表为 : fd_set set 利利 用宏 FD_ZERO(fd_set *); FD_SET(int, fd_set *); FD_CLR(int, fd_set *); FD_ISSET(int, fd_set *);
select() 函数 用法 - 步骤 二 设置监视的范围和超时时间 (timeout) select 函数监视不不同类型的 几组 文件描述符 ( 待接 收 待传输 发 生异常 ) select 函数监视时也发 生阻塞 通过设置 timeout value 来跳出阻塞
select 函数结构 int select( int maxfd, fd_set * readset, fd_set * writeset, fd_set * exceptset, const struct timeval * timeout );
因此, 使 用 select 函数最关键的两个输 入参数包括 文件描述符 (fd) 的监视范围 ( 即我们要监视的客 户端的集合 ) 超时的时间 通过 struct timeval 结构体来初始化并输 入
struct timeval { long tv_sec; long tv_usec; //sec //microsec } select 监视 fd, 如果 fd 们不不发 生变化则阻塞 过了了指定时间 timeval 后, 如果 fd 依然 无变化, 函数返回 0
执 行行 select 时,struct timeval * timeout 的变化 为什什么 timeout 是指针 timeout 会在 select 执 行行时改变
select() 函数 用法 - 步骤三 查看 select 的结果 如果 select 函数返回 大于 0 的数, 则说明部分监视 的 fd 发 生了了变化 例例如 : 经过查询, 有些客户端套接字产 生了了输 入 此时应检查三张 fd_set 表有什什么变化 若没有产 生变化, 原来为 1 的位被置为 0
不不归零码 曼彻斯特 差分曼彻斯特
返回结果后带来的困难? 下 一次 select 前需要重置 fd_set 们
select 函数 - 代码分析
此处设置 timeout 被注释掉了了 因为 timeout 在 select 过程中会改变, 在此处赋值不不科 学
注意传 入 select 的是变量量 temps 而不不是 reads 检查返回值也是检查变量量 temps 而不不是 reads
回声服务器器 - 再讨论 之前讨论的回声 (echo) 服务器器 回声 (echo) 服务器器, 满 足如下 ( 比较简化的 ) 设计要求 : 服务器器端同 一时刻只处理理 一个客户端请求 服务器器端依次向五个客户端提供服务, 然后退出 客户端发送 文本给服务器器, 服务器器回传 (echo back) 客户端输 入退出指令, 结束 一个客户端的请求
非 I/O 复 用的回声服务器器
基于 I/O 复 用的回声服务器器 Server
Server
Server
接 while 循环体 Server
客户端不不需要做出变化
总结 :select 模型 I/O 多路路复 用 (I/O Multiplexing) 操作系统赋予的 一个功能, 使得 socket( 或称 fd) 产 生变化时, 能返回 一个 通知 适 用于 非阻塞模式的 socket 适时地进 行行操作, 保证每次操作都能 有明确效果 监视多个 socket( 或称 fd) 在同 一个线程完成
socket 编程中 select 产 生监听事件的情况 可读 (readset): socket 内核接收缓冲区的内容 足够多 socket 通信对 方关闭连接 socket 上有新的连接请求 socket 上有未处理理的错误
socket 编程中 select 产 生监听事件的情况 可写 (writeset): socket 内核发送缓冲区的内容 足够多 socket 的读端关闭 socket 使 用 connect 连接成功 socket 上有未处理理的错误
select 的优点 兼容性 非常好 比较 传统 的 I/O 多路路复 用的 方法 多个操作系统都 支持 select 函数 实现逻辑简单, 适 用于服务器器端接 入少的情况
select 的缺点 select 遇到的问题 实践中, 接 入数百个客户端时性能降低直 至失效 部分操作系统对 select 的最 大监视容量量有限制 从原理理上会产 生性能瓶颈
select 函数的缺点分析 int select(, fd_set *readset, fd_set *writeset, fd_set *exceptset, ); 每次调 用 select 函数, 都必须将监视对象 (fd) 的 信息全部进 行行参数传递 由于 select 会修改各 fd_set, 因此每次调 用 select 都 需要复制各 fd_set 需要对 fd_set 监视的所有对象进 行行循环轮询
select 之外的 I/O 复 用 如何克服 select 的缺点 减少传递监视对象表的次数 监视对象的内容发 生变化时才产 生通知 因 I/O 复 用是操作系统层 面实现, 因此需要操作系统 的函数 支持 linux 下可以使 用 epoll
epoll 方法 epoll 的优点正好弥补了了 select 的不不 足 epoll 不不需要编写循环语句句 用于轮询所有 文件描述 符的状态变化 一旦有变化会返回变化的 文件描述符 调 用 epoll 系列列函数中查询 fd 变化情况的函数时, 不不 需要每次都向操作系统传递信息
epoll 方法的三个函数 epoll_create: 创建保存 epoll 文件描述符的空间 epoll_ctl: 向空间注册或注销 文件描述符 epoll_wait: 等待 文件描述符 fd 发 生变化并返回变化 的 fd 的集合 epoll 工作流程 创建 epoll 注册监视的 fd 等待发 生变化的 fd 消息
保存监视的 文件描述符 fd select 怎么实现的?fd_set 结构的变量量 epoll 保存在操作系统中 具体来说, 向操作系统请求创建保存 fd 的空间 ( 使 用 epoll_create) 保存监视的 fd 信息, 以及返回发 生事件变化的 fd 信息, 采 用 epoll_event 结构体
epoll_event
epoll 工作过程简介 epoll_create 函数 Linux 2.5.44 以上版本 #include <sys/epoll.h> int epoll_create(int size) 用 epoll_create 在操作系统中创建的 fd 保存空间叫做 epoll 例例程 size 决定 epoll 例例程 大 小, 但操作系统仅仅只参考!
epoll_ctl 函数 int epoll_ctl( int int int epfd, op, fd, struct epoll_event * event ); 成功时返回 0, 失败返回 -1
epoll_ctl 函数的例例 子 epoll_ctl(a, EPOLL_CTL_ADD, FD, EVENT) 向操作系统注册 文件描述符 FD, 主要 用于监测 EVENT 事件 epoll_ctl(a, EPOLL_CTL_DEL, FD, NULL) 从操作系统删除 文件描述符 FD
epoll_ctl 函数之 event 参数 event 参数是 一个 epoll_event 结构 监视的事件类型 ( 读取数据, 发送数据, ) 与第三个参数有部分内容重叠, 见示例例
标记的部分在第三项参数出现过
epoll_wait 函数 int epoll_wait( int epfd, struct epoll_event * events, int int maxevents, timeout ); 成功时返回发 生事件的 fd 数量量, 失败返回 -1
events 参数需要动态分配
基于 epoll 的回声服务器器端 - 代码
注意区别
epoll 第 一步 epoll 第 二步 注意 event 的 用法
处理理 clnt_sock 这个 fd 的事件 处理理 serv_sock 这个 fd 的事件
最后记得要关闭创建的 fd 们 关闭 socket( 服务器器和客户端 ) 关闭 epoll 例例程
多进程 网络程序设计 两种类型的服务器器端 第 一种 : 第 一个连接等待受理理时间为 0s, 第 50 个等待受理理时间为 50s, 第 100 个连接等待受理理时间为 100s, 依次类推 每个连接服务时间仅 1s 第 二种 : 所有连接请求的受理理时间不不超过 1s, 平均服务时间越 2-3s 解决 方案 并发服务器器 多进程同时处理理客户端的连接请求 I/O 复 用的 方式轮询客户端的连接请求 [ 单进程!]
进程和线程 - 基本概念 进程是表示资源分配的基本单位, 又是调度运 行行的基本 单位 例例如, 用户运 行行 自 己的程序, 系统就创建 一个进程, 并为它分配系统资源 该进程放 人进程的就绪队列列 进程调度程序选中它, 为它分配 CPU 以及其它有关资源, 该进程才真正运 行行 进程是系统中的并发执 行行的单位
在采 用微内核结构的操作系统中, 进程的功能发 生了了 变化 它只是资源分配的单位, 而不不再是调度运 行行的单位 在微内核系统中, 真正调度运 行行的基本单位是线程 真正实现并发功能的单位是线程
线程 线程是进程中执 行行运算的最 小单位, 亦即执 行行处理理 机调度的基本单位 如果把进程理理解为在逻辑上操作系统所完成的任务, 那么线程表示完成该任务的许多可能的 子任务之 一 线程可以在处理理器器上独 立调度执 行行, 在多处理理器器环 境下就允许 几个线程各 自在单独处理理器器上进 行行 操 作系统提供线程 方便便 而有效地实现这种并发性
线程的优点 易易于调度, 提 高并发性 创建开销少 利利于充分发挥多处理理器器的功能
进程和线程 - 联系 线程是指进程内的 一个执 行行单元, 也是进程内的可调度实体 一个线程只能属于 一个进程, 而 一个进程可以有多个线 程, 但 至少有 一个线程 进程独享分配的资源, 线程共享同 一进程的所有资源 CPU 核 心分给线程, 真正在 CPU 上运 行行的是线程 线程在执 行行过程中需要协作同步 不不同进程的线程间要 利利 用消息通信的办法实现同步
进程和线程 - 区别 调度 : 线程作为调度和分配的基本单位, 进程作为拥 有资源的基本单位 拥有资源 : 进程是拥有资源的 一个独 立单位, 线程不不 拥有系统资源, 但可以访问 隶属于进程的资源 系统开销 : 在创建或撤消进程时, 由于系统都要为之 分配和回收资源, 导致系统的开销明显 大于创建或撤 消线程时的开销
通信问题 - 进程间通信 管道通信 : 无名管道 用于具有亲缘关系的 父 子进程间的通信 有名管道除了了具有管道所具有的功能外, 它还允许 无亲缘关系进程间的通信 信号 (Signal): 软件层次上对中断机制的 一种模拟, 用于通知进程有某事件发 生, 进程收到信号与处理理器器 收到中断请求效果上 一致
消息队列列 (Message queue): 具有写权限得进程可以按照 一定得规则向消息队列列中添加新信息, 有读权限得进程则可以从消息队列列中读取信息 共享内存 (Shared memory): 多个进程可以访问同 一块内存空间, 需要依靠某种同步操作, 如互斥锁和信号量量等 信号量量 (Semaphore): 进程 / 线程之间得同步和互斥 手段 套接字 (Socket): 更更为 一般得进程间通信机制, 它可 用于 网络中不不同机器器之间的进程间通信, 应 用 非常 广泛
通信问题 - 线程间通信 信号量量 互斥型信号量量 二进制信号量量 整数型信号量量 记 录型信号量量 消息队列列 事件 (Event)
创建 一个进程 :fork 函数 #include <unistd.h> pid_t fork(void) 成功时返回 PID 失败返回 -1
fork 的 工作原理理 fork 创建当前进程的副本 fork 执 行行后才执 行行需要并 行行的代码 fork 函数通过 pid 来判断执 行行哪 一段代码
调 用 fork 函数返回的 pid 父进程 : 返回 子进程的 pid 子进程 : 返回 0 其中 父进程 : 调 用 fork 的主体, 原进程 子进程 : 父进程复制出的进程
完