IO、同期、非同期マルチプレクサ

クリエイティブコモンズライセンス 著作権:帰属、紙ベースを作成するために他人を許可し、(同じライセンスで元のライセンス契約に基づいて用紙配布する必要がありますクリエイティブコモンズ

1.キーコンセプト

1.1同期、非同期

関数またはメソッドは、呼び出し元最終結果かどうかが呼び出されたとき。直接最終的な結果に、それは同期呼び出しで、直接、最終的な結果を得ることはありません、それは非同期呼び出しです。

1.2ブロッキング、ノンブロッキング

関数やメソッドの呼び出した場合、返す、返すすぐに呼び出すかどうかのようなコールがすぐに返されていないのブロッキング、ノンブロッキングです。

同期、非同期、および障害物に関連していない、ブロックは、同期、非同期のかどうか、最終的な結果かどうか、ブロック、非ブロック、時間が強調されていることを強調した待機すること。

同期および非同期の違い:呼び出し側が希望する最終的な結果を得たかどうか。同期は、最終的な結果を返すために行わなければならないれ、非同期直接リターンが、リターンは、最終的な結果ではありません。呼び出し側がこの呼び出しの結果を得ることができない、他の当事者は、呼び出し側が必要とされ、最終的な結果を取得するには、呼び出し側の式を通知すること。ブロッキングと非ブロッキングの違いは、発信者が、他のものが可能であるかどうかです。ブロックされ、呼び出し側だけ待つことができます。ノンブロッキング、呼び出し側は、他の多忙に行くだろう、私はいつも待ってはいけません。

1.3オペレーティングシステムの知識

X86 CPUは、4つの動作レベルがあります。

リング0は、特権命令を実行することができ、データのすべてのレベルへのアクセスは、あなたがIOデバイスにアクセスすることができます。RING3レベル、最低レベル、このレベルではデータのみをアクセスすることができます。カーネルコードはリング0を実行して、ユーザーコードは、リング3を実行します。

オペレーティングシステム、カーネルの独立した、より高い特権レベルで実行され、彼らはすべての権利は、ハードウェアへのアクセス権を持っているために、保護されるようにメモリ空間内に存在し、このメモリはカーネル空間(カーネルモード)と呼ばれています

ユーザ空間(ユーザーモード)で実行されている一般的なアプリケーション。アプリケーションは、一部のハードウェアリソースが提供するオペレーティングシステムが必要とされているアクセスしようと、システムコールをシステムコールは、その後、特権命令を使用して、カーネル空間で実行され、カーネルモードの動作に処理することができます。システムコールが完了すると、プロセスは、ユーザ空間のコードを実行するユーザモードに戻ります。

1.4同期IO、非同期IO、IOマルチプレクサ

1.4.1 IO 2段階

  1. データ準備段階。デバイスは、カーネル空間にカーネルバッファからデータを読み込み、
  2. バックユーザー空間カーネルスペースバッファ段プロセスへのコピー

それは時にIOを発生します。

  1. IOデバイスからのカーネルデータを読みます
  2. カーネルからデータをコピーするプロセス

1.5 IOモデル

1.5.1同期IO

同期IOモデルはIO、非ブロックIO、IO多重化を遮断含み

IOをブロックします:

プロセスは、書き込みが完了するまで(ブロッキング)を待ちます。(フル待機)

IOをノンブロッキング:

IOデバイスは、プロセスがブロックされていない、すぐにエラーを返す準備ができていない場合、プロセスは、のrecvfrom操作を呼び出します。ユーザーは、システムコールを再起動することができます(ポーリングすることができます)。カーネルの準備ができているならば、それはブロックされ、その後、ユーザ空間にデータをコピーします。

データの最初のフェーズは、あなたが別の忙しいを開始することができ、そしてそのデータが非ブロッキングを処理する準備ができているかどうかを確認するために、見ていきます、準備ができていません。第二段階は、ユーザ空間とカーネル空間との間、すなわち、コピーデータがブロックされ、ブロックされています。

IO多重化:

IO多重いわゆるまたは同時に複数のIOを監視し、準備があり、我々はIOを処理する能力を向上させ、治療を開始するのを待つ必要はありません。選択し、ほぼすべてのオペレーティングシステムプラットフォームは、アップグレードを選択するために、アンケートをサポートしています。epoll、Linuxカーネル2.5+は、サーベイランスに基づいて、選択コールバックメカニズムを高めるために、アップグレードや世論調査をサポートするために始めました。BSD、Macプラットフォームのkqueueのは、WindowsがIOCPを持っています。

 

選択するには、例えば、IO操作に焦点を当てますが、選択機能、コールのブロッキングプロセス、カーネル「モニター」ファイルディスクリプタfdの注目を告げ、どんな注意が準備ができて、選択戻りIOデータを対応するFDです。そして、ユーザプロセスに読み出さコピーデータを使用します。

一般情况下,select最多能监听1024个fd,但是由于select采用轮询的方式,当管理的IO多了,每次都要 遍历全部fd,效率低下。epoll没有管理的fd上限,且是回调机制,不需遍历,效率很高。

信号驱动IO:

进程在IO访问时,先通过sigaction系统调用,提交一个信号处理函数,立即返回,进程不阻塞。当内核准备好数据后,产生一个SIGIO信号并投递给信号处理函数,可以在此函数中调用recvfrom函数操作数据从内核空间复制到用户空间,这段过程阻塞。

异步IO:  (注意:回调是被调用者做得,不是调用者)

进程发起异步IO请求,立即返回。内核完成IO的两个阶段,内核给进程发一个信号。在整个过程中,进程都可以忙别的,等好了再过来。

 

Linux的aio的系统调用,内核从版本二2.6开始支持:

1.6 python中的IO多路复用

 

IO多路复用:

  • 大多数操作系统都支持select和poll
  • Linux2.5+支持epoll
  • BSD、Mac支持kqueue
  • Solaris实现了/dev/poll
  • WindowsDE IOCP

python的select库实现了select、poll系统调用,这个基本上操作系统都支持。部分实现了epoll,它是底层的额IO多路复用模块。

开发中的选择:

  1. 完全跨平台,使用select、poll。但是性能较差。
  2. 针对不同操作系统自行选择支持的技术,这样做会提高IO处理的性能。

select维护一个文件描述符数据结构,单个进程使用有上限,通常是1024,线性扫面这个数据结构,效率低。poll和select的区别是内部数据结构使用链表,没有这个最大限制,但是依然要遍历才能知道哪个设备就绪了。epoll、使用事件通知机制,使用回调机制提高效率。select、poll还要从内核空间复制数据到用户空间,而epoll通过内核空间和用户空间共享一块内存来减少复制。

1.6.1 selectors库

poython3.4提供了selectors库,高级的IO复用库。

类层次结构:

selectors.DefaultSelector返回当前平台最有效、性能最高的实现。但是没有实现Windows下的IOCP,所以,Windows下只能退化为select。

# 在selects模块源码最下面有如下代码
# Choose the best implementation, roughly:
# epoll|kqueue|devpoll > poll > select.
# select() also can't accept a FD > FD_SETSIZE (usually around 1024)
if 'KqueueSelector' in globals():
    DefaultSelector = KqueueSelector
elif 'EpollSelector' in globals():
    DefaultSelector = EpollSelector
elif 'DevpollSelector' in globals():
    DefaultSelector = DevpollSelector
elif 'PollSelector' in globals():
    DefaultSelector = PollSelector
else:
    DefaultSelector = SelectSelector

事件注册:

class SelectSelector(BaseselctorImpol):
    """Select-based selector."""
    def register(fileobj, events, data=None) -> SelectorKey: 
        pass
  • selector注册一个文件对象,监视它的IO事件,返回SelectorKey对象。
  • fileobj 被监视文件对象,例如socket对象
  • events 事件,该文件对象必须等待的事件
  • data 可选的与此文件对象相关联的不透明数据,例如,关联用来存储每个客户端的会话ID,关联方法。通过这个 参数在关注的事件产生后让selector干什么事。

EVENT_READ =  (1 << 0)

EVENT_WRITE =  (1 << 1)

这样定义常量的好处是便于合并

selectors.SelectorKey有4个属性:

  1. fileobj注册的文件对象
  2. fd文件描述符
  3. events等待上面的文件描述符的文件对象的事件
  4. data注册时关联的数据

 

IO多路复用实现TCP Server:

import selectors
import socket


s = selectors.DefaultSelector()  # 1拿到selector

# 准备类文件对象
server = socket.socket()
server.bind(('127.0.0.1', 9997))
server.listen()

# 官方建议采用非阻塞IO
server.setblocking(False)


def accept(sock: socket.socket, mas: int):
    conn, r_address = sock.accept()
    # print(conn)
    # print(r_address)
    print(mas)
    # pass
    conn.setblocking(False)
    key1 = s.register(conn, selectors.EVENT_READ, rec)
    print(key1)


def rec(conn: socket.socket, mas: int):
    print(mas)
    data = conn.recv(1024)
    print(data)

    msg = 'Your msg = {} form {}'.format(data.decode(), conn.getpeername())
    conn.send(msg.encode())


# 2注册关注的类文件对象和其事件们
key = s.register(server, selectors.EVENT_READ, accept)  # socket fileobject
print(key)

while True:
    events = s.select()  # epoll select,默认是阻塞的
    # 当你注册时的文件对象们,这其中的至少一个对象关注的事件就绪了,就不阻塞了
    print(events)  # 获得了就绪的对象们,包括就绪的事件,还会返回data

    for key, mask in events:  # event =>key, mask
        # 每一个event都是某一个被观察的就绪的对象
        print(type(key), type(mask))   # key, mask
        # <class 'selectors.SelectorKey'> <class 'int'>
        print(key.data)
        # <function accept at 0x0000000001EA3A60>
        key.data(key.fileobj, mask)  # mask为掩码

server.close()
s.close()

 

IO多路复用实现群聊:

# IO多路复用,实现TCP版本的群聊
import socket
import threading
import selectors
import logging


FORMAT = "%(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)


class ChatServer:

    def __init__(self, ip='127.0.0.1', port=9992):
        self.sock = socket.socket()
        self.address = ip, port
        self.event = threading.Event()

        self.selector = selectors.DefaultSelector()

    def start(self):
        self.sock.bind(self.address)
        self.sock.listen()
        self.sock.setblocking(False)

        key = self.selector.register(self.sock, selectors.EVENT_READ, self.accept)  # 只有一个
        logging.info(key)  # 只有一个
        # self.accept_key = key
        # self.accept_fd = key.fd

        threading.Thread(target=self.select, name='select', daemon=True).start()

    def select(self):

        while not self.event.is_set():
            events = self.selector.select()  # 阻塞
            for key, _ in events:
                key.data(key.fileobj)  # select线程

    def accept(self, sock: socket.socket):  # 在select线程中运行的
        new_sock, r_address = sock.accept()
        new_sock.setblocking(False)
        print('~' * 30)

        key = self.selector.register(new_sock, selectors.EVENT_READ, self.rec)  # 有n个
        logging.info(key)

    def rec(self, conn: socket.socket):  # 在select线程中运行的
        data = conn.recv(1024)
        logging.info(data.decode(encoding='cp936'))

        if data.strip() == b'quit' or data.strip() == b'':
            self.selector.unregister(conn)  # 关闭之前,注销,理解为之前的从字典中移除socket对象
            conn.close()
            return

        for key in self.selector.get_map().values():
            s = key.fileobj
            # if key.fileobj is self.sock:  # 方法一
            #     continue
            # if key == self.accept_key:  # 方法二
            #     continue
            # if key.fd == self.accept_fd:  # 方法三
            #     continue
            # msg = 'Your msg = {} form {}'.format(data.decode(encoding='cp936'), conn.getpeername())
            # s.send(msg.encode(encoding='cp936'))
            # print(key.data)
            # print(self.rec)
            # print(1, key.data is self.rec)  # False
            # print(2, key.data == self.rec)  # True
            if key.data == self.rec:  # 方法四
                msg = 'Your msg = {} form {}'.format(data.decode(encoding='cp936'), conn.getpeername())
                s.send(msg.encode(encoding='cp936'))

    def stop(self):  # 在主线程中运行的
        self.event.set()
        fs = set()
        for k in self.selector.get_map().values():
            fs.add(k.fileobj)
        for f in fs:
            self.selector.unregister(f)  # 相当于以前的释放资源
            f.close()
        self.selector.close()


if __name__ == "__main__":
    cs = ChatServer()
    cs.start()

    while True:
        cmd = input(">>>").strip()
        if cmd == 'quit':
            cs.stop()
            break
        logging.info(threading.enumerate())
        logging.info(list(cs.selector.get_map().keys()))
        # for fd, ke in cs.selector.get_map().items():
        #     logging.info(fd)
        #     print(ke)
        #     print()

 

总结:

使用IO多路复用 + (select、epoll)并不一定比多线程+ 同步阻塞性能好,其最大的优势是可以处理更多的连接。多线程+同步阻塞IO模式,开辟太多的线程,线程开辟、销毁开销还是较大,倒是可以使用线程池;线程多,线程自己使用的内存也很可观,多线程切换时,要保护现场和恢复现场,线程过多,切换回占用大量的时间 。

IO +モードブロッキング少ない接続、マルチスレッド同期は効率が低くない、適切です。接続が同時IOはまだ比較的高く、スレッドの多くを開くために、この時間は、実際には良い取引ではありませんサービス側で非常に多くの場合、この時間IO多重化は、より良い選択かもしれません。

 

 

おすすめ

転載: blog.csdn.net/sqsltr/article/details/92762279