Fork me on GitHub

python自动化编程-第十天 IO、进程、协程

python自动化编程-第十天 IO、进程、协程


多进程

多进程实际上就是主进程的拷贝;
多进程提供本地和远程并发性,通过使用子进程代替线程,有效地绕过全局解释器锁。能够适应CPU密集型的操作。

import multiprocessing, threading

import time


def run(name):
    time.sleep(2)
    print('hello', name)
    t = threading.Thread(target=thread_run)
    t.start()

if __name__ == '__main__':

    for i in range(10):
        p = multiprocessing.Process(target=run, args=('bob%s'%i,))
        p.start()
    # p.join()

显示进程id和子进程id,

from multiprocessing import Process
import os


def info(title):
    print(title)
    print('module name:', __name__)
    print('parent process:', os.getppid())  # 父进程的id
    print('process id:', os.getpid())  # 自己的id
    print("\n\n")


def f(name):
    info('\033[31;1mcalled from child process function f\033[0m')
    print('hello', name)


if __name__ == '__main__':
    info('\033[32;1mmain process line\033[0m')
    p = Process(target=f, args=('bob',))
    p.start()
    p.join()

进程间通信

不同进程间内存是不共享的,要想实现两个进程间的数据交换,需要使用Queue、Pipe、Manager等方法

Queue

进程的Queue与threading的queue使用方法差不多
但是只能让父进程与子进程进行交互;或者同属于同一父进程下的多个子进程进行交互

from multiprocessing import Process, Queue

def f(q):
    q.put([42, None, 'hello'])


if __name__ == '__main__':
    q = Queue()
    p = Process(target=f, args=(q,))
    p.start()
    print(q.get())  # prints "[42, None, 'hello']"
    p.join()

原理实际上是 子进程拷贝了一份主进程的队列;

Pipe

Pipe是一个管道,一般情况下是双向的;
pipe()返回的两个连接对象表示管道的两端。每个连接对象都有send()和recv()方法。注意,如果两个进程(或线程)试图同时读取或写入管道的同一端口,那么管道中的数据可能会被损坏。当然,在同时使用不同端口就不会导致数据损坏。因此一个管道口只有一个进程对其操作;

from multiprocessing import Process, Pipe


def f(conn):
    conn.send([42, None, 'hello from child'])
    conn.send([42, None, 'hello from child2'])
    print('from parent:',conn.recv())
    conn.close()


if __name__ == '__main__':
    parent_conn, child_conn = Pipe() # 将管道的两端分配给父进程和子进程
    p = Process(target=f, args=(child_conn,))
    p.start()
    print(parent_conn.recv())  # prints "[42, None, 'hello from child']"
    print(parent_conn.recv())  # prints "[42, None, 'hello from child2']"
    parent_conn.send("zhangyang kehao")
    p.join()

managers

manager object 返回 manager()控制的一个包含Python对象的服务器进程,并允许其他进程使用代理来操作它们。

manager 返回的manager()支持类型列表、命令、名称空间、锁、RLock、信号量、BoundedSemaphore、条件、事件、Barrier、队列、值和数组。

from multiprocessing import Process, Manager
import os

def f(d, l):
    d[os.getpid()] = os.getpid()
    l.append(os.getpid())
    print(l)


if __name__ == '__main__':
    with Manager() as manager:  # manager = Manager()  ,内部自己有锁,
        d = manager.dict()    # 生成一个字典,可在多个进程间共享和传递

        l = manager.list(range(5))  # 生成一个列表,可在多个进程间共享和传递
        p_list = []   #进程列表
        for i in range(10):
            p = Process(target=f, args=(d, l))
            p.start()
            p_list.append(p)
        for res in p_list:  #等待结果
            res.join()

        print(d)
        print(l)

进程间实现数据的共享:

Queue/Pipe 只是实现进程间数据的传递,中间存在拷贝操作(pickle)
Manager 实现进程间数据的共享,即整个进程可以修改同一份数据,无拷贝操作;

进程锁

主要是屏幕输出,如果不加锁,则很容易弄混淆;

from multiprocessing import Process, Lock

# 控制屏幕输出,防止输出错误;

def f(l, i):
    l.acquire()
    try:
        print('hello world', i)
    finally:
        l.release()

if __name__ == '__main__':
    lock = Lock()

    for num in range(10):
        Process(target=f, args=(lock, num)).start()

进程池

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。

进程池的目的是限制进程的数量;进程与线程不同,线程的开销小,如果多了,系统会变慢,因为cpu切换太频繁;而进程多了,系统就瘫痪了。

进程池中有两个方法:
apply 并行
apply_async 串行

from multiprocessing import Process, Pool   # 如果是windows,此处要加上 freeze_support
import time
import os


def Foo(i):
    time.sleep(2)
    print('in process',os.getpid())
    return i + 100


def Bar(arg):
    print('-->exec done:', arg,os.getpid())


if __name__ == '__main__':   #为了区分主动运行脚本,还是被其他人当做模块调用,
    #freeze_support()

    pool = Pool(processes=5)  #允许进程池中同时放入5个进程
    print(os.getpid())
    for i in range(10):
        # pool.apply_async(func=Foo, args=(i,), callback=Bar)  # callback = 回调,是主进程执行的
        pool.apply(func=Foo, args=(i,))   # 同步执行,串行
        # pool.apply_async(func=Foo, args=(i,))   # 并行

    print('end')
    pool.close()
    pool.join()  # 进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。

进程池需要注意的是:
一定要先关闭掉进程池,然后在join()。否则会报错。
callback是回调函数,回调函数都是主进程执行的。

协程

协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:

协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

协程的好处:

无需线程上下文切换的开销
无需原子操作锁定及同步的开销
  "原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
方便切换控制流,简化编程模型
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

缺点:

无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

使用yield实现协程操作例子

import time
import queue


def consumer(name):
    print("--->starting eating baozi...")
    while True:
        new_baozi = yield
        print("[%s] is eating baozi %s" % (name, new_baozi))
        # time.sleep(1)


def producer():
    r = con.__next__()
    r = con2.__next__()
    n = 0
    while n < 5:
        n += 1
        con.send(n)   #
        con2.send(n)
        print("\033[32;1m[producer]\033[0m is making baozi %s" % n)


if __name__ == '__main__':
    con = consumer("c1")
    con2 = consumer("c2")
    p = producer()

协程的定义

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 一个协程遇到IO操作自动切换到其它协程

协程只要遇到io操作就切换,io操作完成了在切换回来,问题是怎么知道io操作完成了,所以说yeid并不算真正的协程。

Greenlet

greenlet是一个用C实现的协程模块,相比与python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator

from greenlet import greenlet

# greenlet 是手动切换,每次切换都会回到上次停止时的位置,

def test1():
    print(12)
    gr2.switch()
    print(34)
    gr2.switch()


def test2():
    print(56)
    gr1.switch()
    print(78)


gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

Gevent

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

import gevent
 
def func1():
    print('\033[31;1m11111...\033[0m')
    gevent.sleep(2)
    print('\033[31;1m222222...\033[0m')
 
def func2():
    print('\033[32;1maaaaaa...\033[0m')
    gevent.sleep(1)
    print('\033[32;1mbbbbbbb...\033[0m')

def func3():
    print('\033[33;1m年月日...\033[0m')
    gevent.sleep(1)
    print('\033[32;1m日月年...\033[0m') 
 
gevent.joinall([
    gevent.spawn(func1),
    gevent.spawn(func2),
    gevent.spawn(func3),
])

func1()打印第一句以后,进入sleep状态,2秒钟,
然后切换到func2()打印一句,也进入sleep,1秒钟
切换到func1(),发现是sleep状态,则切换到func3(),
再打印func3()第一句后,同样进入sleep,1秒钟,
此时 gevent就会在3个函数中来回不断的切换,直到某一个sleep结束,则继续打印这个函数的第二句,然后继续切换。。。。。

遇到io阻塞时自动切换任务

import gevent
from urllib.request import urlopen
from gevent import monkey

monkey.patch_all() #把当前程序的所有io操作单独做上标记

def f(url):
    print('GET: %s' % url)
    resp = urlopen(url)
    data = resp.read()
    # f = open('url.html', 'rw')
    # f.write(data)
    # f.close()
    print('%d bytes received from %s.' % (len(data), url))


gevent.joinall([
    gevent.spawn(f, 'https://www.python.org/'),
    gevent.spawn(f, 'https://www.yahoo.com/'),
    gevent.spawn(f, 'https://github.com/'),
])

gevent不知道urllib的io操作,这就需要gevent知道urllib的io操作,则需要打上monkey.patch_all这个补丁,就可以让协程自动完成切换操作;

使用gevent实现单线程下的多socket并发

server code


import sys
import socket
import time
import gevent

from gevent import socket, monkey

monkey.patch_all()


def server(port):
    s = socket.socket()
    s.bind(('0.0.0.0', port))
    s.listen(500)
    while True:
        cli, addr = s.accept()
        gevent.spawn(handle_request, cli)


def handle_request(conn):
    try:
        while True:
            data = conn.recv(1024)
            print("recv:", data)
            conn.send(data)
            if not data:
                conn.shutdown(socket.SHUT_WR)  #关闭客户端连接

    except Exception as  ex:
        print(ex)
    finally:
        conn.close()


if __name__ == '__main__':
    server(8001)

client code

import socket
 
HOST = 'localhost'    # The remote host
PORT = 8001           # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
    msg = bytes(input(">>:"),encoding="utf8")
    s.sendall(msg)
    data = s.recv(1024)
    #print(data)
 
    print('Received', repr(data))
s.close()

并发client连接

import socket
import sys

messages = [ b'This is the message. ',
             b'It will be sent ',
             b'in parts.',
             ]
server_address = ('localhost', 10000)

# Create a TCP/IP socket
socks = [ socket.socket(socket.AF_INET, socket.SOCK_STREAM) for i in range(100)]

# Connect the socket to the port where the server is listening
print('connecting to %s port %s' % server_address)
for s in socks:
    s.connect(server_address)

for message in messages:

    # Send messages on both sockets
    for s in socks:
        print('%s: sending "%s"' % (s.getsockname(), message) )
        s.send(message)

    # Read responses on both sockets
    for s in socks:
        data = s.recv(1024)
        print( '%s: received "%s"' % (s.getsockname(), data) )
        if not data:
            print(sys.stderr, 'closing socket', s.getsockname() )

通过列表生成式一次生成多个连接,通过for循环连接服务器,实现并发效果;

事件驱动模型

通常,我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新的进程,来处理该请求;
(2)每收到一个请求,创建一个新的线程,来处理该请求;
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求
上面的几种方式,各有千秋,
第(1)中方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。
第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。
第(3)种方式,在写应用程序代码时,逻辑比前面两种都复杂。
综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式

在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢?
方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:

  1. CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?
  2. 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
  3. 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
    所以,该方式是非常不好的。

方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:

  1. 有一个事件(消息)队列;
  2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
  3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
  4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。

让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。


在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。

在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。

在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。

当我们面对如下的环境时,事件驱动模型通常是一个好的选择:

程序中有许多任务,而且…
任务之间高度独立(因此它们不需要互相通信,或者等待彼此)而且…
在等待事件到来时,某些任务会阻塞。
当应用程序需要在任务间共享可变的数据时,这也是一个不错的选择,因为这里不需要采用同步处理。

网络应用程序通常都有上述这些特点,这使得它们能够很好的契合事件驱动编程模型。

IO模型

此处讨论的背景是Linux环境下的network IO。

概念说明

用户空间和内核空间

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

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。

从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:

  1. 保存处理机上下文,包括程序计数器和其他寄存器。

  2. 更新PCB信息。

  3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。

  4. 选择另一个进程执行,并更新其PCB。

  5. 更新内存管理的数据结构。

  6. 恢复处理机上下文。

总而言之就是很耗资源,具体的可以参考这篇文章:进程切换

注:进程控制块(Processing Control Block),是操作系统核心中一种数据结构,主要表示进程状态。其作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位或与其它进程并发执行的进程。或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。 PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息

进程的阻塞

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

文件描述符

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

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

缓存 I/O

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

缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

IO复用模型

阻塞 I/O(blocking IO)

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

blocking IO,从发送请求,到内核接受到数据,并将数据copy到用户空间,而后发送给程序,这真个过程全部是阻塞状态;

非阻塞 I/O(nonblocking IO)


当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。也就是说第一阶段不是阻塞的,但是内核接收到数据,并copy到用户空间这个过程是阻塞的

I/O 多路复用( IO multiplexing)


当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

多路复用 IO和blocking IO比其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

异步 I/O(asynchronous IO)


用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

IO模型比较

I/O 多路复用之select、poll、epoll详解

select

select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。

select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。

另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

poll

poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。

poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

epoll

直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。

epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

区别

socket中的缓冲区,就是为了减少从内核态转变成用户态的次数,

网络数据都是先发送到内核态的缓冲区,在拷贝到用户态的内存空间中;
用户空间没有办法访问内核空间,硬件都是在内核中;

select epool pool 都只是在检测socket连接。

select最多只能1024个socket连接,就是默认能够打开的文件数,是linux中limit中的参数

I/O定义多路复用,在返回时不会告诉是哪个socket的连接,因此需要循环去找,这样就会浪费资源;

poll和epool都没有连接数的限制。

epool:nginx在用,centos6之后才只支持,(linux)kernel-2.6才支持,widnows不支持,windows只支持select,而poll不确定

epool:会将有数据连接返回给用户;
水平触发:如果没有取数据,下次在循环时,还是会告诉用户程序有数据,
边缘触发:如果数据没有及时取出来,那么就取不回来了;

python selcet socket server code

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# author:czlan
# Email:czlan91@live.cn
# Date: 2018/5/1

import select
import socket
import queue

# 单线程下多路复用,不需要多线程

msg_dic = {}  # 每个连接一个队列,因此弄一个字典,

server = socket.socket()

server.bind(('localhost', 9000))

server.listen(1000)

server.setblocking(False)  # 0就是false,不阻塞

inputs = [server, ]
outputs = []

while True:
    readable, writeable, exceptional = select.select(inputs, outputs, inputs)  # 检测的连接,,检测错误连接
    # 需要检测是否有数据接收的连接,需要检测是否有数据发送的连接,需要检测是否有异常连接

    print(readable, writeable, exceptional)

    for r in readable:
        if r is server:  # 代表来了一个新连接
            conn, addr = server.accept()
            print('来了个新连接', conn, addr)
            inputs.append(conn)  # 是应为这个新建立的连接还没发数据过来,现在就接收的话,由于不阻塞,所以程序就报错了,要想实现这个客户端发数据来时server端能知道,就需要让select再检测conn
            msg_dic[conn] = queue.Queue()  # 初始化一个队列,后面存要返回给这个客户端的数据
        else:
            data = r.recv(1024)
            if data:
                print('收到数据', data)
                msg_dic[r].put(data)
                if r not in outputs:
                    outputs.append(r) # 放入返回的连接队列里
            else:  # 如果收不到data代表什么呢? 代表客户端断开了呀
                print("客户端断开了", r)

                if r in outputs:
                    outputs.remove(r)  # 清理已断开的连接

                inputs.remove(r)  # 清理已断开的连接

                del msg_dic[r]  ##清理已断开的连接


    for w in writeable:  # 要返回给客户端的连接列表
        data_to_client = msg_dic[w].get()
        w.send(data_to_client)  # 返回给客户端源数据
        outputs.remove(w)  # 确保下一次循环的时候writeable,不反悔这个已经处理完的连接

    for e in exceptional:  # 客户端断开了,
        if e in outputs:  # 判断是否在要发送数据的连接列表中;
            outputs.remove(e)
        inputs.remove(e)  # 删除监听的连接
        e.close()
        del msg_dic[e]  # 删除队列

python epool socket server code

import selectors   #默认用epool,如果没有epool那么就用select
import socket

sel = selectors.DefaultSelector()


def accept(sock, mask):
    conn, addr = sock.accept()  # Should be ready
    print('accepted', conn, 'from', addr)
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read)


def read(conn, mask):
    data = conn.recv(1024)  # Should be ready
    if data:
        print('echoing', repr(data), 'to', conn)
        conn.send(data)  # Hope it won't block
    else:
        print('closing', conn)
        sel.unregister(conn)
        conn.close()


sock = socket.socket()
sock.bind(('localhost', 10000))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)

while True:
    events = sel.select()   #这里写是写select,但是有可能是epool, 默认阻塞,有活动连接就返回活动的连接列表
    for key, mask in events:
        callback = key.data   # callback 相当于回调函数,就是accept
        callback(key.fileobj, mask)  # key.filobj = 文件句柄,就是还没有赋值的conn

selectors模块 ---封装后的select

默认用epool,如果机器上没有epool,那么就会用select,例如windows

This module allows high-level and efficient I/O multiplexing, built upon the select module primitives. Users are encouraged to use this module instead, unless they want precise control over the OS-level primitives used.

import selectors
import socket
 
sel = selectors.DefaultSelector()
 
def accept(sock, mask):
    conn, addr = sock.accept()  # Should be ready
    print('accepted', conn, 'from', addr)
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read)#新连接注册read回调函数
 
def read(conn, mask):
    data = conn.recv(1024)  # Should be ready
    if data:
        print('echoing', repr(data), 'to', conn)
        conn.send(data)  # Hope it won't block
    else:#客户端断开了
        print('closing', conn)
        sel.unregister(conn)# 取消注册
        conn.close()
 
sock = socket.socket()
sock.bind(('localhost', 10000))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)
 
while True:
    events = sel.select() #调用的可能是epool,也可能是select,默认是阻塞,有活动连接就返回活动的连接列表;
    for key, mask in events:
        callback = key.data #相当于调用accept
        callback(key.fileobj, mask)
        #(fileobj 就是文件句柄,还有建立好连接的socket;)

参数处理 optparse

optparse

parser = optparse.OptionParser()
parser.add_option("-s","--server", dest="server", help="ftp server ip_addr")
parser.add_option("-P","--port",type="int", dest="port", help="ftp server port")
parser.add_option("-u","--username", dest="username", help="username")
parser.add_option("-p","--password", dest="password", help="password")
self.options , self.args = parser.parse_args()
#定义好的参数在options中,以属性的形式存放
#没有定义好的参数在args中,以列表的形式存放

2.7 版本中使用的是optparse
3.2 版本中使用的是argparse

# argparse的使用
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument("-s","--server", dest="server", help="ftp server ip_addr")
parser.add_argument("-P","--port",type=int, dest="port", help="ftp server port")
parser.add_argument("-u","--username", dest="username", help="username")
parser.add_argument("-p","--password", dest="password", help="password")

self.args = parser.parse_args()
# print(self.args.accumulate(self.args.integers))
print(self.args)
#没有定义的参数就是 self.args[1:],通过反射的方式来找到对应的方法即可;

#定义好的存在args[0]中,存放在namespace,取数据时,以属性的形式取数据即可

select 版FTP

1、select中的FTP
所有的程序应该是在writeable 中写,但是也不能多次recv,只能一次接收recv;

posted @ 2018-06-01 18:07  耳_东  阅读(116)  评论(0)    收藏  举报