3.13 学网络编程 io阻塞

1.事件驱动和py无关,编程思想

def author('yanga11ang'):
#第一种...傻逼式while
while True:
    鼠标监听
#事件驱动
'''
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
    有一个事件(消息)队列;
    鼠标按下时,往这个队列中增加一个点击事件(消息);
    有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
    事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数; 
'''

2.io多复路

def time('3.13'):
def time('3.13'):
基础
#用户空间和内核空间 
#进程切换
#进程的阻塞
#文件描述符
#缓存 I/O

用户空间与内核空间

    现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(232次方)。 
    操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。 
    为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。 
    针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC00000000xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x000000000xBFFFFFFF),供各个进程使用,称为用户空间。 

#进程切换
    为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。
    这种行为被称为进程切换,这种切换是由操作系统来完成的。
    因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。 
    从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
    保存处理机上下文,包括程序计数器和其他寄存器。
    更新PCB信息。
    把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
    选择另一个进程执行,并更新其PCB。
    更新内存管理的数据结构。
    恢复处理机上下文。 
    注:总而言之就是很耗资源的

#进程的阻塞

    正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,
    则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。
    可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。
    当进程进入阻塞状态,是不占用CPU资源的。

#文件描述符fd
    文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。 
    文件描述符在形式上是一个非负整数。
    实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
    当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
    在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。
    但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

#缓存 I/O
    缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。
    在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,
    也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
    用户空间没法直接访问内核空间的,内核态到用户态的数据拷贝 

对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,
一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。
当一个read操作发生时,它会经历两个阶段:
 1 等待数据准备 (Waiting for the data to be ready)
 2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
记住这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况。
#blocking IO (阻塞IO) 6.1
    conn,addr=sk.accept() 等待数据阻塞
    等待对方复制数据阻塞;     两次堵塞,进程也被堵塞
#non-blocking IO(非阻塞IO)6.2
    询问,没有的话先做自己的,过一会儿在询问 (不能即使拿到,如果对方在询问后才准备好数据)
    等待对方复制数据的阻塞
#IO multiplexing(IO多路复用)6.3
    事件驱动模型制作
    整个进程都在阻塞
    唯一的好处是,select可以等待多个
#Asynchronous I/O(异步IO)6.5
    进程不堵塞
    内部实现复杂
#select poll epoll IO多路复用介绍 6.6

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

3.select poll epoll


#select 
    select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,
    当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,
    使得进程可以获得这些文件描述符从而进行后续的读写操作。 
    select目前几乎在所有的平台上支持 
    select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。 
    另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。
    同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

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

#epoll 
    直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll。被公认为Linux2.6下性能最好的多路I/O就绪通知方法。windows不支持 
    没有最大文件描述符数量的限制。 
    比如100个连接,有两个活跃了,epoll会告诉用户这两个两个活跃了,直接取就ok了,而select是循环一遍。 
    (了解)epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,
    它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),
    理论上边缘触发的性能要更高一些,但是代码实现相当复杂。 

    另一个本质的改进在于epoll采用基于事件的就绪通知方式。
    在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,
    而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,
    内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。 
#阻塞io
import socket
sk=socket.socket()
sk.bin('127.0.0.1',8080)
sk.listen(3)
while 1:
    conn addr=sk.accept()
#非阻塞io
import socket
sk=socket.socket()
sk.bin('127.0.0.1',8080)
sk.listen(3)
sk.setblocking(False)
while 1:
    try:
        conn addr=sk.accept()
    except Exception:
        print('OK')
        time.sleep(3)


#io多复用
#----select--------
import socket
import select
sk1=socket.socket()
sk1.bin('127.0.0.1',8080)
sk1.listen(3)

sk2=socket.socket()
sk2.bin('127.0.0.1',8081)
sk2.listen(3)
while 1:
    r,w,e=select.select([sk1,sk2],[],[],5) #链接不断开,就一直存在,最后可选参数,最多等待时间s
    for obj in r: #[sk1]
        conn,addr=obj.accept()
        conn.sed('hello'.encode('utf8'))
        print('hello')
def a():
def b():
def c():
def d():
def e():
def f():
def g():
def h():

# 首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。
# 不管是文件,还是套接字,还是管道,我们都可以把他们看作流。
# 之后我们来讨论I/O的操作,通过read,我们可以从流中读入数据;通过write,我们可以往流写入数据。现在假
# 定一个情形,我们需要从流中读数据,但是流中还没有数据,(典型的例子为,客户端要从socket读如数据,但是
# 服务器还没有把数据传回来),这时候该怎么办?
# 阻塞。阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干
# (或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话
# (假定一定能叫醒你)。
# 非阻塞忙轮询。接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂
# 个电话:“你到了没?”
# 很明显一般人不会用第二种做法,不仅显很无脑,浪费话费不说,还占用了快递员大量的时间。
# 大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,
# 就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。
#
# 为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为
# 了减少频繁I/O操作而引起频繁的系统调用(你知道它很慢的),当你操作一个流时,更多的是以缓冲区为单位进
# 行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。
# 假设有一个管道,进程A为管道的写入方,B为管道的读出方。
# 假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变
# 到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。
# 但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写
# 入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候
# 会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。
# 假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从
# 长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”
# 也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告
# 诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。
# 这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四
# 个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是
# 什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。
#
# 然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多
# 个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。
# 于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞
# 模式再此不予讨论):
# while true {
# for i in stream[]; {
# if i has data
# read until unavailable
# }
# }
# 我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为
# 如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻
# 塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。
#
# 为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不
# 过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻
# 塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可
# 以把“忙”字去掉了)。代码长这样:
# while true {
# select(streams[])
# for i in streams[] {
# if i has data
# read until unavailable
# }
# }
# 于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知
# 道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,
# 找出能读出数据,或者写入数据的流,对他们进行操作。
# 但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。再次
# 说了这么多,终于能好好解释epoll了
# epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我
# 们。此时我们对这些流的操作都是有意义的。
# 在讨论epoll的实现细节之前,先把epoll的相关操作列出:
# epoll_create 创建一个epoll对象,一般epollfd = epoll_create()
# epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件
# 比如
# epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回
# epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
# epoll_wait(epollfd,...)等待直到注册的事件发生
# (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。
# 而epoll只关心缓冲区非满和缓冲区非空事件)。
# 一个epoll模式的代码大概的样子是:
# while true {
# active_stream[] = epoll_wait(epollfd)
# for i in active_stream[] {
# read or write till unavailable
# }
# }


# 举个例子:
#    select:
#          班里三十个同学在考试,谁先做完想交卷都要通过按钮来活动,他按按钮作为老师的我桌子上的灯就会变红.
#          一旦灯变红,我(select)我就可以知道有人交卷了,但是我并不知道谁交的,所以,我必须跟个傻子似的轮询
#          地去问:嘿,是你要交卷吗?然后我就可以以这种效率极低地方式找到要交卷的学生,然后把它的卷子收上来.
#
#
#    epoll:
#         这次再有人按按钮,我这不光灯会亮,上面还会显示要交卷学生的名字.这样我就可以直接去对应学生那收卷就
#         好了.当然,同时可以有多人交卷.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值