〇 操作系统概述
1. 用户空间与内核空间
内核空间用于运行操作系统核心组件,比如内存管理组件,IO交互组件,文件管理、中断管理组件等,同时驱动程序(Driver)也运行在内核空间。
用户空间,用于运行普通应用程序。用户空间是无法直接调用内核空间,只能通过内核提供的接口来调用内核空间。
🐟当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。用户态使用户隔离了内核,防止错误导致系统崩溃。区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。
🐟从用户空间进入内核空间通常有三种方式:系统调用,软中断(如定时器等),硬件中断。
一、进程
进程是操作系统的核心。进程是对正在运行程序的一个抽象。**一个进程就是一个正在执行的程序实例,包括程序计数器,寄存器和变量的当前值。**每个进程有一个地址空间和一个控制线程。
1.1 进程概念
💗 1.1.1 进程的专用名词解释
守护进程:停留在后台处理的进程。
进程表:包含了进程状态信息的结构数组,包括进程状态转换时必须保存的信息。
竞争条件:两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序。
互斥:以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做出同样的动作。
临界区:对共享内存访问的程序片段。
信号量 Semaphore:一个用来累计进程唤醒次数的整形变量。
互斥量:信号量的简化版本
原子操作:一组相关联的操作要么不间断的地执行,要么都不执行。
时间片:每个进程被分配的一个时间段
进程切换/上下文切换:一个进程切换到另一个进程时,保存和装入寄存器值及内存镜像,更新各种表,重新调入内存等操作。
1.2 进程运算过程
进程作为一个程序实例,其运算过程分为四个部分:进程创建,进程终止,进程调度,进程间通信。
💗 1.2.1 进程的创建
以下
4
个事件会导致进程的创建,
但是所有情形中,新进程都是由于一个已存在的进程执行了一个用于创建进程的系统调用而创建的。
① 系统初始化
② 正在运行的程序进行创建进程的系统调用
③ 用户请求创建一个新进程
④ 一个批处理作业的初始化
💗 1.2.2 进程的终止
以下4个事件会导致进程的终止:
① 正常退出(自愿)
② 出错退出(自愿) – 程序运行的某些条件不存在
③ 严重错误(非自愿) – 引用不存在的内存,非法指令
④ 被其他进程杀死(非自愿)
💗 1.2.3 进程的调度
由于计算机的CPU数量有限,但是计算机中有很多个进程,这就会造成多个进程会同时竞争CPU,因此这就需要 进程调度程序选择下一个要运行的进程。进程的调度通常伴随着进程状态的改变,因此首先介绍进程的状态关系。
1. 进程的状态
进程的状态通常分为三种: 运行态, 就绪态, 阻塞态。状态切换②和③是由进程调度程序引起的。 进程的调度是操作系统的一部分。
因为 进程的调度需要从用户态切换到内核态,并保存当前的状态,如果进程切换过于频繁,会耗费大量CPU时间。因此进程的调度需要考虑 进程的计算方式、 何时进行进程调度和 采用什么样的进程调度算法的问题。
(1).进程计算方式
进程计算方式分为两类:
– 计算密集型:CPU的运算时间要占大多数。
– I/O密集型:进程在等待I/O上花费大多数时间。
(2).何时进行进程调度
通常在4种情形中需要进程调度:
– 在创建新进程后,决定是运行父进程还是子进程。
– 在进程退出时需要进程调度。
– 在进程阻塞时需要进程调度。
– 在I/O中断发生时需要进程调度。
(3).进程调度算法
💗 1.2.4 进程间通信
在不同的进程之间经常需要相互通信,这就形成了进程间通信(Inter Process Commucation,ICP) 。进程间通信需要考虑是三个个问题:
Q1:一个进程如何把信息传递给另一个进程?– 通信方法
Q2:如何确保多个进程在通信过程中不存在交叉?-- 互斥
Q3:如何保证进程之间的执行顺序? – 同步
进程间通信方式主要有:
共享内存就是允许两个或多个进程共享一定的存储区。将 共享内存进行访问的程序片段称作临界区。因为数据不需要在客户机和服务器端之间复制,数据直接写到内存,不用若干次数据拷贝,所以这是 最快的一种IPC。
共享内存中没有任何互斥和同步机制。
2.进程的互斥与信号量
当有两个或多个进程读写共享数据时,会出现 进程竞争的现象。解决进程竞争,就是要阻止多个进程同时读写共享的数据( 互斥)。为了解决共享内存中出现的进程竞争和进程顺序问题,提出了信号量。 信号量的本质就是一个计数器,实现了进程之间的互斥和同步。 在介绍信号量之前,先介绍进程的互斥。
(1) 进程的互斥
要满足进程的互斥需要满足4个条件:
① 对任何两个进程不能同时处于临界区
② 不应对CPU的速度和数量做任何假设
③ 临界区外运行的进程不得阻塞其他进程
④ 不得是进程无限期等待进入临界区
针对以上4个条件,提出了几种实现互斥的方法:
信号量是一个整型变量来累计唤醒次数。 一个信号量的取值可以为0或者为正值。信号量有两种操作:down和up。 信号量实现了进程之间的互斥和同步。一个信号量操作开始后,在该操作完成或阻塞前,其他进程不允许访问该信号量。具体步骤如下:
Step1:测试控制该资源的信号量。
Step2:若此信号量的值为正,则允许进行使用该资源。进程将进号量减1。
Step3:若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入Step1。
Step4:当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
(3) 互斥量
互斥量是信号量的简化版本,去除了信号量的计数能力。互斥量常使用一个整形量,0表示 解锁,其他所有的值表示 加锁。 互斥量只实现了进程之间的互斥,不能保证进程之间的同步,因此,只使用互斥量可能会导致进程的死锁。
当一个进程如果想进入临界区,它首先尝试锁住相关的互斥锁,如果互斥锁没有加锁,那么这个进程可以立即进入,并且该互斥量被自动锁定以防止其他进程进入。如果互斥量已经被加锁,则调用进程被阻塞,直到该互斥量被解锁。
注意:条件变量不会存在在内存中,如果一个信号量传递给一个没有线程在等待的条件变量,那么这个信号就会丢失。
3. 管道
一个进程连接到另一个进程的一个数据流称为管道。管道的本质是内核的一块缓存,由两个文件描述符引用,一个表示读端,一个表示写端,数据从写端流入,从读端流出。管道通信分为 匿名管道和 命名管道。 匿名管道只能在有亲缘关系的进程间使用,命名管道可在同一台计算机的不同进程之间或在跨越一个网络的不同计算机的不同进程之间使用。
管道通信共有特点如下:
① 一个进程只能控制读端或者写端,不能同时自己读写
② 管道中的数据不可反复读取
③ 管道采用半双工通信方式,数据只能在一个方向上流动
(1) 匿名管道
(2) 命名管道
命名管道本质上是一个 管道文件,通过“文件”来传递信息。
4. 消息队列
消息队列是由 消息的链表, 存放在内核中并由 消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列提供了一种从一个进程向另一个进程发送一个 数据块的方法。消息队列通过 发送消息来避免命名管道的同步和阻塞问题, 其每个数据块都有一个最大长度的限制。
二、死锁
1.1 资源循环及死锁的产生
一个计算机的资源是有限的,多个进程在计算机中会对资源产生竞争,如果一个进程集合中每个进程都在等待只能有该进程集合中的其他进程才能引发的时间,那么,该进程集合就是死锁。 如下图所示:
死锁的产生有四个必要的条件:
① 互斥条件 – 每个资源要么已经分配,要么是可用的。
② 占有和等待条件 – 已经得到了某个资源的进程可以再请求新的资源。
③ 不可抢占条件 – 已经分配给一个进程资源不能强制被抢占,只能显式的释放。
④ 环路等待条件 – 死锁发生时,系统存在进程环路。
只有当4个条件同时发生时,才会发生死锁。
为了避免和消除死锁,有4个策略:
① 忽略该问题
② 检测死锁并恢复
③ 对资源进行分配,避免死锁
④ 破坏引起死锁的的四个条件之一。
2.2 死锁问题解决
针对4个策略,提出了解决死锁的方法:
💗 2.2.1 死锁检测
1. 每种类型一个资源的死锁检测 --基于树的资源死锁检测
依次将每一个节点作为一棵数的根节点,并进行深度优先搜索,如果碰到已经遇到的节点,那么就发生了死锁。如果任何节点都被穷举了,那么回溯到前面的节点,如果不包含任何环,则说明没有死锁发生。
2. 每种类型多个资源的死锁检测 – 基于向量的资源死锁检测
E表示现有资源向量,A表示可用资源向量,C表示当前分配矩阵,R表示请求矩阵。
💗 2.2.2 死锁恢复
1. 利用抢占恢复
将某一资源从一个进程抢占,分配给另一个进程使用。
2. 利用回滚恢复
周期性的对进程设立检查点检查,当发生死锁时,将进程恢复到更早的检查点,并重新分配资源。
3. 通过杀死进程恢复
杀掉环中的一个进程或者选择环外的一个进程,释放其进程资源。
💗 2.2.3 死锁避免
要避免死锁就要保证资源的分配处于安全状态。 安全状态就是当所有进程突然请求对资源的最大需求,仍然存在某种调度能够使每一个进程运行完毕。
💗 2.2.4 死锁预防
三、内存管理
内存是计算机中的重要资源,现在计算机的内存解决方式是分层存储器体系,如下图所示:
在内存管理中,主要要解决三个问题:
① 内存地址的查找
② 内存超载问题
③ 内存置换技术
3.1 内存地址的查找
在无抽象的内存管理中,内存地址查找的是绝对地址物理地址,但绝对地址会影响操作系统的运行。因此提出了抽象内存管理—地址空间。地址空间是一个进程可用于寻址内存的一套地址集合,每个进程都有一个自己的地址空间。
3.2 内存超载问题
通常情况下,程序的进程所需空间要远大于内存空间,这就造成了内存的超载。解决内存超载主要有两种方式:交换技术和虚拟内存。
💗 3.2.1 交换技术
交换技术,即把进程完整调入内存,使该进程运行一段时间,然后把它存回磁盘。
1. 交换技术的内存空间分配
进程运行其空间还会继续增长,这就需要在调入内存时,额外的分配一些内存,同时为堆栈分配内存空间。其空间分配如下图:
要实现交换技术的内存空间分配,需要对空闲内存进行跟踪管理。有两种方法来进行内存的跟踪:
① 使用位图的内存管理
使用位图方法,内存被划分为多个分配单元,每个分配单元对应位图中的一位,0表示空闲, 1表示占用。
维护一个记录已分配内存段和空闲内存段的链表,链表中每一个结点包含:空闲区(H)或进程§标志,起始地址,长度,和指向下一结点的指针。
虚拟内存的基本思想是: 每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称为页面,每一页有连续的地址范围,这些页被映射到物理内存。 若该页面没有被映射,则产生缺页中断。
1. 页面技术
页面是虚拟内存的基础, 虚拟地址空间按照固定的大小划分成页面若干个单元,在物理内存中对应的单元称为叶框,页面和叶框的大小通常是一样的。
为了 使虚拟页面映射为页框,提出了 页表。在大内存的情况下,传统的单级页表会占用很大的内存,为了解决大内存页表过大问题,提出了 多级页表。其原理如下图所示:
3.3 内存置换问题
当虚拟内存发生缺页中断时,需要在内存中选择一个页面将其换出内存,为即将调入的页面腾出空间。常用页面置换算法如下:
四、文件管理
内存提供的是快速的,少量的信息和数据存储,用于进程运行过程中信息和数据保存。若要将大量数据长时间保存就需要将数据保存到磁盘当中。
在文件管理中,主要要解决几个问题:
① 如何找到文件存储的位置
4.1 文件存储位置 - 目录
文件系统通过目录或文件夹来记录文件的存储位置。现在操作系统中都是层次目录系统:
信号量与互斥量的区别**:
互斥量用于进/线程的互斥,互斥是指某一资源只允许一个访问者进行访问,具有唯一性和排它性,但互斥无法保证进/线程对资源的有序访问。互斥量只能是0或1
信号量用于进/线程的同步,同步时指在互斥的基础上实现进/线程对资源的有序访问,线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。同步是必定互斥的。信号量可以是非负整数。
信号量和互斥量使用时,必须要先使用互斥量,确保只有一个线程进入临界区,然后再使用信号量进行同步
(6).使用互斥量和条件变量解决线程生产者-消费者问题:
如果多线程中只使用了互斥量,而没有条件变量,则会造成线程的死锁。其中,条件变量解决的是线程同步的问题,保证线程的执行顺序,防止死锁。
例如,有消费者线程A
,生产者线程B
和共享变量n
,消费者线程A
必须等到 n>0
才能接着往下执行,如果 n==0
,那么 消费者线程A
将一直等待。生产者线程B
进入临界区,修改 n
的值,使得 n >0
,当 n > 0
时,B
通知等待 n > 0
的消费者线程A。消费者线程A
被 生产者线程B
通知之后就可以接着往下执行了。
但是,当 消费者线程A
进入临界区时,其他线程不能进入临界区,生产者线程B
没有机会去修改 n
, n
的值一直为 0,不满足A
继续执行的条件(n > 0),消费者线程A
只能一直等待,从而陷入死循环(死锁)。
#include<stdio.h>
#include<pthread.h>
#define MAX 10 //缓冲区的大小
/********线程共享内存************/
pthread_mutex_t the_mutex;
pthread_cond_t condc; //消费者条件变量
pthread_cond_t condp; //生产者条件变量
int buffer=0;
/*******************************/
void *producer(void *ptr){
int i;
for(i=1;i<=MAX;i++){
pthread_mutex_lock(&the_mutex);
//buffer!=0说明加锁,缓冲区中有产品,阻塞生产者线程
while(buffer!=0) {
pthread_cond_wait(&condp,&the_mutex); //条件等待,等待condp的信号出现解除阻塞
}
sleep(1);
printf(" -- %u %u Producer produce item %d\r\n",getpid(),pthread_self(),i);
buffer=i;
pthread_cond_signal(&condc); //发送信号condc
pthread_mutex_unlock(&the_mutex);
}
pthread_exit(0);
}
void *consumer(void *ptr){
int i;
for(i=1;i<=MAX;i++){
pthread_mutex_lock(&the_mutex);
//buffer==0说明解锁,解锁后缓存区没有东西,消费者线程阻塞
while(buffer==0) {
pthread_cond_wait(&condc,&the_mutex); //条件等待,等待condc的信号出现解除阻塞
}
sleep(1);
printf(" -- %u %u Consumer consume item %d\r\n",getpid(),pthread_self(),i);
buffer=0;
pthread_cond_signal(&condp); //发送信号condp
pthread_mutex_unlock(&the_mutex);
}
pthread_exit(0);
}
int main(){
pthread_t pro,con; //con消费者线程,pro生产者线程
pthread_mutex_init(&the_mutex,0);
pthread_cond_init(&condc,0);
pthread_cond_init(&condp,0);
pthread_create(&con,0,consumer,0);
pthread_create(&pro,0,producer,0);
pthread_join(pro,0); //以阻塞的方式等待thread指定的线程结束
pthread_join(con,0); //以阻塞的方式等待thread指定的线程结束
pthread_cond_destroy(&condc);
pthread_cond_destroy(&condp);
pthread_mutex_destroy(&the_mutex);
sleep(3);
return 0;
}
2.线程
💗2.1 线程的必要性:
① 一个进程中的多线程拥有共享同一地址空间个所有可用数据的能力。
② 线程的创建和销毁效率更高
③ 多线程会使同一进程中的大量计算或大量I/O处理重叠进行,加快应用程序的执行速度。
💗2.2 线程模型:
每个线程有自己的堆栈,保存自己的运行状态。线程也有4个状态:运行态,阻塞态,就绪或终止态
💗 2.3 线程的实现方式:
① 在用户空间中实现线程: ② 在内核中实现线程:
🐟用户空间中实现线程:在用户空间实现线程,线程运行在运行时系统中(run-time system),内核对此一无所知。在用户空间实现线程时,每一个进程针对自己的线程维护了一个线程表(Thread Table),该表保存了线程运行的各种变量,比如寄存器,PC,状态等等,线程表用进程的运行时系统来维护,当一个线程被block,她的当前运行状态会被保存在线程表中,当再次启动时,也会读取线程表中已经保存的状态,从该状态进行再次运行。
🐟在内核中实现线程:内核线程同样有线程表(Thread table),不过这个线程表是保存在内核中,其功能和用户空间线程表的功能一样,都是用于保存线程的数据。线程的调度由操作系统内核来实现。
💗2.4 线程间通信: 见进程间通信
💗2.4 死锁的避免-银行家算法
首先,在死锁避免中有两种状态:
① 安全状态:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序,能够使得每一个进程运行完毕,则该状态为安全的。
② 不安全状态: 在所有进程突然请求对资源的最大需求时,没有一个线程可以运行完成,则该状态为不安全的。不安全状态并不是死锁,系统仍能运行一段时间。
银行家算法:
为保证资金的安全,银行家规定:
(1) 当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;
(2) 顾客可以分期贷款,但贷款的总数不能超过最大需求量;
(3) 当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;
(4) 当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金.
操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程本次申请的资源数是否超过了该资源所剩余的总量。若超过则拒绝分配资源,若能满足则按当前的申请量分配资源,否则也要推迟分配。
💗2.5 死锁的预防
针对死锁发生的4个必要条件,有4中对应的死锁预防方法:
① 互斥条件:使用假脱机技术
② 占有和等待条件:在进程开始时就请求全部资源。
③ 不可抢占条件:抢占资源
④ 环路等待条件:对资源按顺序编号。
三、内存管理
1.基本概念介绍
无内存抽象存储器:在无内存抽象的计算机中,程序直接访问存储器物理地址。当有两个或两个以上的程序时,由于两个程序都是基于绝对物理地址,被装入内存中时,地址会发生冲突,从而程序发生崩溃。
地址空间一个进程可用于寻址内存的一套地址集合,每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间。
交换技术:用于处理实际内存小于需要内存的内存超载问题,有两种方法:1.交换技术,2.虚拟内存
页框:物理内存对应的单元。
虚拟页面:虚拟内存对应的单元
缺页中断:虚拟页面没有对应的映射到虚拟页框
页表:将虚拟页面映射到页框的一个函数。输入为虚拟页面,输出为物理页框号。
颠簸:每执行几条指令程序就发生一次缺页中断。
工作集:一个进程当前正在使用的页面集合。
工作集模型:确保在进程运行以前,进程的工作集就存在内存中,减少缺页的中断率。
2.虚拟内存
虚拟内存的基本思想是:每个程序拥有自己的地址空间,这个空间被分割成多个块,每个块称作页面(Page),每一页有连续的地址范围,这些页被映射到物理内存。虚拟内存的本质是创造一个对物理内存的抽象-地址空间,虚拟内存的实现是将虚拟地址空间分解成页,并将每一页映射到物理内存的某个页框或者解除映射。内存和磁盘之间的传输是依靠页进行的,一次传输一个页
3.页面置换
当发生缺页中断时,操作系统必须在内存中选择一个页面将其置换出内存,为即将调入的页面腾出空间,如果要换出的页面在内存中被修改过,就必须把它写回磁盘,以更新该页面在磁盘上的副本,如果该页面没有被修改过,那么他在磁盘上的副本不需要更新,也就不需要回写,直接用调入的页面覆盖被淘汰的页面即可。
页面置换算法:
算法 | 说明 | 注释 |
---|---|---|
最优算法 | 每次淘汰时,找一个未来最长时间才会被访问的页面进行淘汰 | 不可实现 |
NRU(最近未使用)算法 | LRU的近似 | |
FIFO(先入先出)算法 | FIFO算法是根据页面进入内存的时间先后选择调度页面,该算法实现时需要将页面按照进入的时间先后组成一个队列,每次优先淘汰队首页面。 | 可能抛弃重要页面 |
第二次机会算法 | 比FIFO有较大改善 | |
时钟算法 | 为每一页设置访问位,将内存中所有页面通过连接指针接成循环队列,当页面被访问时访问位置1,每次淘汰时,从指针当前位置开始循环遍历,将访问位为1的置为0,找到第一个访问位为0的将其淘汰。 | 现实的 |
LRU(最近最少使用)算法 | 首先淘汰最长时间未被使用的页面 | 很难实现 |
NFU(最不经常使用)算法 | 淘汰一定时期内被访问次数最少的页面 | LRU的近似 |
老化算法 | 近似LRU的有效算法 | |
工作集算法 | 实现开销大 | |
工作集时钟算法 | 好的有效算法 |
四、输入/输出
1.基本概念介绍
I/O设备分为两类:块设备和字符设备
块设备:把信息存储在固定大小的块中,每个块有自己的地址,所有传输以一个或多个完整的块为单位。
字符设备:以字符为单位发送或接收一个字符流,不考虑任何块结构。
2. I/O硬件原理
💗 2.1 直接存储器 DMA
DMA 传输将数据从一个地址空间复制到另外一个地址空间。当CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成。DMA传输前,CPU要把总线控制权交给DMA控制器,而在结束DMA传输后,DMA控制器应立即把总线控制权再交回给CPU。一个完整的DMA传输过程必须经过DMA请求、DMA响应、DMA传输、DMA结束4个步骤。
DMA的出现使使IO设备可以通过DMA控制器直接访问内存,与此同时,CPU可以继续执行程序,这就需要两者对内存的使用进行分配,因为对应内存,同一时间只能有DMA或CPU进行访问,不能同时访问。
① 在DMA访问内存时停止CPU访问内存:DMA读取内存时,CPU没有总线控制权,CPU不能运行。
② DMA与CPU交替访问内存:DMA读取内存的同时,CPU可以同时继续工作。
💗 2.2 中断
在中断发生时,中断服务程序需要保存程序计数器,所有可见的寄存器和内部寄存器也需要保存。
精确中断与不精确中断:
(a).精确中断有4个特性:
① PC(程序计数器)保存在一个已知的地方
② PC所指向的指令之前的所有指令已经完全执行
③ PC所指向的指令之后的所有指令都没有执行。
④ PC所指向的指令的执行状态是已知的。
(b).除了精确中断剩下的都为不精确中断
3. I/O软件原理
💗 3.1 IO的三种实现方式:
IO有三种实现方式:程序控制IO,中断驱动IO,使用DMA的IO
🐟 程序控制IO:程序控制IO会占用CPU的全部时间,直到全部I/O完成,CPU要不断地查询设备以了解它是否就绪(称为轮询或忙等待)。
🐟 中断驱动IO:在IO设备接收到数据后后,设备开始运行,此时CPU调用调度程序,启动其他的进程进行运行,当IO设备运行完成后产生一个中断,CPU阻塞进程,应答中断。之后返回到中断之前正在运行的进程继续运行。
🐟 使用DMA的IO
💗 3.2 IO系统层次:
五、网络通信
1. 在Linux建立高并发epoll+线程池服务器
epoll
是 Linux 内核为处理大批量文件描述符而作了改进的poll
,是 Linux 下`多路复用 IO接口 select/poll 的增强版本。
💗1.1 传统服务器
最简单的服务器是单进程,单线程的TCP/UDP服务器,这种服务器只能与单个客户端进行相连,效率很低。为了提升服务器效率,提出了单进程,多线程服务器,但是不断的进行线程的创建会造成资源的大量浪费。由于网络数据的传输实际上一种I/O的操作,因此可以通过I/O复用来提高效率-select+poll服务器