码迷,mamicode.com
首页 > 编程语言 > 详细

python网络编程04 IO模型

时间:2021-06-25 16:53:53      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:关注   输出   大量   nal   也会   基于   intel   end   另一个   

1、IO模型的基础概念

  • 在学习IO模型前先介绍几个概念。
    • 用户空间和内核空间
    • 进程切换
    • 进程的阻塞
    • 文件描述符
    • 缓存 I/O

1、用户空间和内核空间

  • Linux操作系统和驱动程序运行在内核空间,应用程序运行在用户空间
  • os分配给每个进程一个独立的、连续的、虚拟的地址内存空间,该大小一般是4G(32位操作系统,即2的32次方),其中将高地址值的内存空间分配给os占用,linux os占用1G,window os占用2G;其余内存地址空间分配给进程使用。
  • 通常32位Linux内核虚拟地址空间划分0~3G为用户空间,3~4G为内核空间(注意,内核可以使用的线性地址只有1G)。注意这里是32位内核地址空间划分,64位内核地址空间划分是不同的。

技术图片

    • 进程寻址空间0~4G
    • 进程在用户态只能访问0~3G,只有进入内核态才能访问3G~4G
    • 进程通过系统调用进入内核态
    • 每个进程虚拟空间的3G~4G部分是相同的
    • 进程从用户态进入内核态不会引起CR3的改变但会引起堆栈的改变
  • Linux内核高端内存
    • 若x86架构机器中安装8G物理内存,那么内核就只能访问前1G物理内存,后面7G物理内存将会无法访问,因为内核的地址空间已经全部映射到物理内存地址范围0×0~0×40000000。即使安装了8G物理内存,那么物理地址为0×40000001的内存,内核该怎么去访问呢?代码中必须要有内存逻辑地址的,0xc0000000~0xffffffff的地址空间已经被用完了,所以无法访问物理地址0×40000000以后的内存。
    • 因此x86架构中将内核地址空间划分三部分:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。ZONE_HIGHMEM即为高端内存,这就是内存高端内存概念的由来。
    • 当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存。如下图。

技术图片

  • 用户空间(进程)是否有高端内存概念?
    • 用户进程没有高端内存概念。只有在内核空间才存在高端内存。用户进程最多只可以访问3G物理内存,而内核进程可以访问所有物理内存。
  • 64位内核中有高端内存吗?
    • 目前现实中,64位Linux内核不存在高端内存,因为64位内核可以支持超过512GB内存。若机器安装的物理内存超过内核地址空间范围,就会存在高端内存。
  • 用户进程能访问多少物理内存?内核代码能访问多少物理内存?
    • 32位系统用户进程最大可以访问3GB,内核代码可以访问所有物理内存。
    • 64位系统用户进程最大可以访问超过512GB,内核代码可以访问所有物理内存。
  • 高端内存和物理地址、逻辑地址、线性地址的关系?
    • 高端内存只和逻辑地址有关系,和逻辑地址、物理地址没有直接关系。
  • 为什么需要区分内核空间与用户空间
    • 在CPU的指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。
    • 所以CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。比如Intel的CPU将特权等级分为4个级别:Ring0~Ring3。
    • 其实Linux系统只使用了Ring0和Ring3两个运行级别(Windows系统也是一样的)。当进程运行在Ring3级别时被称为运行在用户态,而运行在Ring0级别时被称为运行在内核态。
  • 内核态与用户态
    • 当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。
    • 内核态下,进程运行在内核地址空间中,此时CPU可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。
    • 用户态下,进程运行在用户地址空间中,被执行的代码要受到CPU的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中I/O许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。
  • 区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。

2、进程切换

  • 操作系统为了控制进程的执行,必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行,这种行为被称为进程切换,任务切换或上下文切换。
  • 进行进程切换就是从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。
    • 从某个进程收回处理器,实质上就是把进程存放在处理器的寄存器中的中间数据存放到进程的私有堆栈,从而把处理器的寄存器腾出来让其他进程使用。
    • 让进程来占用处理器,实质上是把某个进程存放在私有堆栈中寄存器的数据(前一次本进程被中止时的中间数据)再恢复到处理器的寄存器中去,并把待运行进程的断点送入处理器的程序指针PC,于是待运行进程就开始被处理器运行了,也就是这个进程已经占有处理器的使用权了。
  • 在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用处理器时,进程的上下文是存储在进程的私有堆栈中的。
  • 进程上下文切换由以下4个步骤组成:
    • (1)决定是否作上下文切换以及是否允许作上下文切换。包括对进程调度原因的检查分析,以及当前执行进程的资格和CPU执行方式的检查等。在操作系统中,上下文切换程序并不是每时每刻都在检查和分析是否可作上下文切换,它们设置有适当的时机。
    • (2)保存当前执行进程的上下文。这里所说的当前执行进程,实际上是指调用上下文切换程序之前的执行进程。如果上下文切换不是被那个当前执行进程所调用,且不属于该进程,则所保存的上下文应是先前执行进程的上下文,或称为“老”进程上下文。显然,上下文切换程序不能破坏“老”进程的上下文结构。
    • (3)使用进程调度算法,选择一处于就绪状态的进程。
    • (4)恢复或装配所选进程的上下文,将CPU控制权交到所选进程手中。
  • 进程切换注意事项
    • 保存处理器PC寄存器的值到被中止进程的私有堆栈;
    • 保存处理器PSW寄存器的值到被中止进程的私有堆栈;
    • 保存处理器SP寄存器的值到被中止进程的进程控制块;
    • 保存处理器其他寄存器的值到被中止进程的私有堆栈;
    • 自待运行进程的进程控制块取SP值并存入处理器的寄存器SP;
    • 自待运行进程的私有堆栈恢复处理器各寄存器的值;
    • 自待运行进程的私有堆栈中弹出PSW值并送入处理器的PSW;
    • 自待运行进程的私有堆栈中弹出PC值并送入处理器的PC。

3、进程的阻塞

  • 正在运行的进程由于提出系统服务请求(如I/O操作),但因为某种原因未得到操作系统的立即响应,或者需要从其他合作进程获得的数据尚未到达等原因,该进程只能调用阻塞原语把自己阻塞(进程的阻塞是进程自身的主动行为),等待相应的事件出现后才被唤醒。
  • 正在进行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态(当进程进入阻塞状态时,不占用CPU),亦即进程的执行受到阻塞,我们把这种暂停状态叫进程阻塞,有时也成为等待状态或封锁状态。通常这种处于阻塞状态的进程也排成一个队列。有的系统则根据阻塞原因的不同而处于阻塞状态进程排成多个队列。
  • 进程阻塞不会消耗CPU资源,但会消耗其他系统资源(内存、磁盘IO等)。
  • 进程的五种状态及转换

技术图片

    • 创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态
    • 就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行
    • 执行状态:进程处于就绪状态被调度后,进程进入执行状态
    • 阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用。(操作系统不会去尝试运行被阻塞的进程,而是由对象等待某种“刺激”,将其变为就绪态。)
    • 终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行。

4、文件描述符

  • 内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
  • 每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。
    具体概况,需要查看由内核维护的3个数据结构:
    1.进程级的文件描述符表;
    2.系统级的打开文件描述符表;
    3.文件系统的i-node表。

  • 文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统
  • 习惯上,标准输入(standard input)的文件描述符是0,标准输出(standard output)是1,标准错误(standard error)是2。尽管这种习惯并非Unix内核的特性,但是因为一些shell和很多应用程序都使用这种习惯,因此,如果内核不遵循这种习惯的话,很多应用程序将不能使用。
  • POSIX定义了STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO来代替0、1、2。这三个符号常量的定义位于头文件unistd.h。
  • 文件描述符的有效范围是0到OPEN_MAX。一般来说,每个进程最多可以打开64个文件(0—63)。对于FreeBSD 5.2.1、Mac OS X 10.3和Solaris9来说,每个进程最多可以打开文件的多少取决于系统内存的大小,int 的大小,以及系统管理员设定的限制。Linux 2.4.22强制规定最多不能超过1,048,576。
  • 文件描述符的好处主要有两个:
    • 基于文件描述符的I/O操作兼容POSIX标准。
    • 在UNIX、Linux的系统调用中,大量的系统调用都是依赖于文件描述符。
  • 文件描述符的概念存在两大缺点:
    • 在非UNIX/Linux操作系统上(如Windows NT),无法基于这一概念进行编程。
    • 由于文件描述符在形式上不过是个整数,当代码量增大时,会使编程者难以分清哪些整数意味着数据,哪些意味着文件描述符。因此,完成的代码可读性也就会变得很差。
  • 与文件描述符相关的部分操作
    • 文件描述符的生成
      • open(),creat()
      • socket()
      • pipe()
    • 与单一文件描述符相关的操作
      • read(), write()
      • recv(), send()
    • 与复数文件描述符相关的操作
      • select(), pselect()
      • poll()
    • 与文件描述符表相关的操作
      • close()
      • dup()
    • 改变进程状态的操作
      • fchdir()
      • mmap()
    • 与文件加锁的操作
      • flock()
      • lockf()
    • 与套接字相关的操作
      • connect()
      • bind()
      • listen()
      • accept()
      • shutdown()

5、缓存 I/O

  • 缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
  • 缓存I/O的优点:
    • 缓存I/O使用了操作系统内核缓冲区,在一定程度上分离了应用程序空间和实际的物理设备;
    • 缓存I/O可以减少读盘的次数,提高性能;
  • 缓存1/0的缺点:
    • 数据在传输过程中需要在应用程序地址空间和内核空间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是丰常大的。

2、IO模型

  • 学习四种IO模型(本文讨论IO的背景是Linux环境下的network IO
    • 阻塞I/O(blocking IO)
    • 非阻塞I/O(nonblocking IO)
    • I/O多路复用( IO multiplexing)
    • 异步I/O(asynchronous IO)
    • 信号驱动I/O( signal driven IO)
  • 什么是IO?
    • 在linux(或unix)中一切皆文件,文件就是一串二进制流,不管是socket、FIFO、管道、还是终端,对我们来说都是文件、都是二进制流。
    • 信息交换的过程,就是对这些流进行数据的收发操作,简称为I/O操作(input and output)。从流中读出数据,系统调用read;写入数据、系统调用write。
    • 不过话说回来了,计算机里有这么多的流,怎么知道要操作哪个流呢?这就需要用到文件描述符fd了,一个fd就是一个非负整数,对这个非负整数的操作就是对这个文件(流)的操作。
  • IO涉及的对象和步骤
    • 对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process(或thread),另一个是系统内核(kernel)。
    • 当一个read操作发生时,它会经历两个阶段
      • 等待数据准备好(Waiting for the data to be ready)
      • 将数据从内核空间拷贝到用户空间(即进程)中(Copying the dato from the kernel to the process)
  • 通常用户进程中的一个完整IO分为两个阶段:
    • 用户空间 <---> 内核空间
    • 内核空间 <---> 设备空间

技术图片

 

 

技术图片

1、阻塞I/O(blocking IO)

  • 当用户进程发出IO请求时,就会查看内核空间中的数据是否就绪。
    • 如果内核空间的数据没有就绪,就等待数据就绪,而用户进程就会处于阻塞状态,用户进程交出CPU。
    • 当内核空间的数据就绪之后,就将数据从内核空间复制到用户空间,并返回结果给用户进程,用户进程才解除block状态。
  • blocking IO的特点就是在IO执行的两个阶段(等待数据和复制数据)都被阻塞。
  • 网络编程的listen()、send()、recv()等接口都是阻塞型的。
  • 典型应用:阻塞Socket, Java BIO
    • 进程阻塞挂起不消耗CPU资源、及时响应每个操作。
      实现难度低、开发应用较容易。
      适用并发量小的网络应用开发。
      不适用并发量大的应用、因为一个请求10会阻塞进程、所以、得为每请求分配一个处理进程(线程)以及时响应、系统开销大。

技术图片 

2、非阻塞I/O(nonblocking IO)

  • 当用户进程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。
    • 如果结果是一个error时,用户进程就知道数据还没有准备好,可以再次发送read操作。
    • 一旦内核中的数据准备好了,并且又再次收到了用户进程的请求,那么立即将数据拷贝到了用户空间,然后返回。
  • 在非阻塞IO模型中,用户进程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
    • 网络IO,非阻塞IO也会进行recrform系统调用,检查数据是否准备好,与阻塞IO不一样,"非阻塞IO将大的整片的阻塞时间分成N多的小的阻塞时间,所以进程不断地有机会被CPU光顾。即每次recrterm系统调用之间,CPU的权限还在进程手中,这段时间是可以做其他的事情"。
    • 也就是说非阻塞的recvform系续调用调用之后,进程并没有被阻塞,内核马上返回给进程。如果数据还没准备好,此时会返回一个error,在返回之后,可以干点别的事情,然后再发起recvform系续调用。重复上面的过程,循环往复的进行recvformu系统调用。这个过程通常被称之为轮询。轮调检查内核数据,直到数据准备好,再复制数据到用户空间。需要注意,复制数据的整个过程,进程仍然是属于阻塞的状态
  • 典型应用: 网络编程时将Socket设置为NONBLOCK
    • 进程轮询(重复)调用、消耗CPU的资源。
      实现难度低、开发应用相对阻塞IO模式较难。
      适用并发量较小、且不需要及时响应的网络应用开发。

技术图片

3、I/O多路复用( IO multiplexing)

  • 多个的进程的IO可以注册到一个复用器(select)上,当用户进程调用该select,select会监听所有注册进来的IO。
    • 如果select监听的所有IO在内核缓冲区都没有可读数据,select调用进程会被阻塞。
    • 当任一IO在内核缓冲区中有可读数据时,select调用就会返回。
    • 而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO,读取内核中准备好的数据。
  • IO复用模型和阻塞IO模型并没有太大的不同。
    • 事实上,IO复用还更差一些,因为IO复用需要两个系统调用(select和recvfrom),而阻塞IO模型只有一次系统调用(recvfrom)。
    • 但是,用select的优势在于它可以同时处理多个连接。所以如果处理的连接数不是很高,使用select/epoll的web server不一定比使用多线程加阻塞IO的web server性能更好、可能延迟还更大。
    • select/epoll的优势并不是对于单个连接能处理得更快、而是在于能处理更多的连接。
  • 在IO复用模型中,每一个socket一般都设置成为非阻塞。但是,整个用户的进程其实是一直被阻塞的,只不过进程是被select这个函数阻塞,而不是被socket IO给阻塞。
  • 典型应用: Java NIO, Nginx (epoll, poll, select)
    • 专一解决多个进程IO的阻塞问题、性能好、Reactor模式。
      实现、开发应用难度较大。
      适用高并发服务应用开发、一个进程/线程响应多个请求。

技术图片

4、异步I/O(asynchronous IO)

  • 在异步IO模型中,当用户进程发起read操作之后,立刻就可以开始去做其它的事。
    • 从内核的角度,当它受到一个asynchronous read后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户进程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户进程,当这一切都完成之后,内核会给用户进程发送一个信号,告诉它read操作完成了。
    • 也就说用户进程完全不需要关心实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
  • 在异步IO模型中,IO操作的两个阶段都不会阻塞用户进程。
  • 典型应用: Java 7 AIO、高性能服务器应用
    • 不阻塞、数据一步到位、Proactor模式
      需要操作系统的底层支持、LINUX 2.5版本内核首现、2.6版本产品的内核标准特性
      回调机制、实现、开发应用难度大
      非常适合高性能高并发应用

技术图片

5、信号驱动I/O( signal driven IO)

  • 在信号驱动IO模型中,当用户进程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户进程会继续执行,当内核数据就绪时会发送一个信号给用户进程,用户进程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。这个一般用于UDP中,对TCP套接口几乎是没用的,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情(不常用,所以不说明

技术图片

6、比较五种IO模型

技术图片

1、阻塞IO和非阻塞IO

  • 阻塞IO会一直阻塞住对应的进程直到IO操作完成。
  • 非阻塞IO在内核还没准备好数据时也会立刻返回。
  • 阻塞和非阻塞关注的是进程在等待调用结果时的状态。阻塞是当内核空间中无数据时,进程会一直等待数据,此时进程会被挂起让出CPU。非阻塞是当内核空间无数据时,也会返回一个error,不会阻塞当前进程。

2、同步IO和异步IO区别在哪?

  • 同步:I/O操作导致请求进程被阻塞,直到I/O操作完成。(IO操作一般是指将数据从内核空间复制到用户空间
  • 异步:I/O操作不会导致请求进程被阻塞。
  • 阻塞IO、非阻塞IO、IO多路复用、信号驱动IO都属于同步IO
    • 非阻塞IO和信号驱动IO,如果内核的数据没有准备好,这时候不会阻塞进程。但是,当内核中数据准备好的时候,recvfrom会将数据从内核拷贝到用户空间中,这个时候进程是被阻塞的。
  • 只有异步IO才是真正的异步IO
    • 当进程发起I异步IO操作后,就直接返回再也不理睬了,直到内核发送一个信号告诉进程说IO完成,在这整个过程中进程完全没有被阻塞。

3、信号驱动式IO和异步IO

  • 异步IO通知内核启动某个IO操作,并让内核在整个操作(包括数据从内核复制到用户缓冲区)完成时通知我们。也就是说,异步10是由内核通知我们IO操作何时完成,即实际的IO操作是异步的
  • 信号驱动IO是由内核通知我们何时可以启动一个IO操作,这个IO操作由用户自定义的信号函数来实现,实际的IO操作是同步的

3、select、poll和epoll

  • select()、poll()和epoll是IO多路复用的实现,可以用于实现单进程/单线程的并发服务。
  • select()、poll()和epoll是监控文件描述符状态的函数,它们可以监控一系列文件的一系列事件。
    • 事件大致分为3类:可读事件、可写事件和异常事件。

 1、select

  • select目前几乎在所有的平台上都被支持
  • select最早于1983年出现在4.2BSD中,它通过一个select()系统调用可以监视一个文件描述符的数组(包含多个文件描述符)。当进程调用select()后,select会在内核中轮询(遍历)所有的文件描述符,就绪的文件描述符便会被内核修改其返回值中的标志位,使进程可以获得哪些文件描述符已就绪,从而进行后续的读写操作。
  • select缺点
    • 单个进程能够监视的文件描述符的数量存在最大限制。在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
    • select()维护一个存储大量文件描述符的数据结构(文件描述符数组),每次调用select都要把它从用户空间拷贝到内核空间;每次调用select都需要在内核中遍历所有传递进来的文件描述符。随着文件描述符数量的增加,它们的开销也线性增长。

2、poll

  • poll和select在本质上没有多大差别。
  • poll没有最大文件描述符数量的限制
  • 一般不使用它,相当于过渡阶段。

3、epoll

  •  linux支持epoll,windows不支持
  • 直到Linux2.6才由内核直接支持。被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
  • 没有最大文件描述符数量的限制
  • epoll采用基于事件的就绪通知方式。事先通过epoll_ctl()来注册一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
#                                                                                                                   #

python网络编程04 IO模型

标签:关注   输出   大量   nal   也会   基于   intel   end   另一个   

原文地址:https://www.cnblogs.com/maiblogs/p/14890084.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!