Linux下I/O多路複用select, poll, epoll 三種模型的Python使用

Linux下I/O多路複用select, poll, epoll 三種模型

select, poll, epoll本質上都是同步的I/O,由於它們都是在讀寫事件就緒後本身負責進行讀寫,這個讀寫的過程是阻塞的。html

select, poll, epoll 都是一種 I/O 複用的機制。它們都是經過一種機制(由系統提供的)來監視多個描述符,一旦某個描述符就緒了,就能通知程序進行相應的讀寫操做。python

select

select的原理

select 是經過系統調用來監視着一個由多個文件描述符(file descriptor)組成的數組,當select()返回後,數組中就緒的文件描述符會被內核修改標記位(其實就是一個整數),使得進程能夠得到這些文件描述符從而進行後續的讀寫操做。select飾經過遍歷來監視整個數組的,並且每次遍歷都是線性的。linux

select的缺點

  1. 每次調用select,都須要把fd集合從用戶態拷貝到內核態,這個開銷在fd不少的時候會很大
  2. 單個進程可以監視的fd數量存在最大限制,在linux上默認爲1024(能夠經過修改宏定義或者從新編譯內核的方式提高這個限制)
  3. 而且因爲select的fd是放在數組中,而且每次都要線性遍歷整個數組,當fd不少的時候,開銷也很大

在Python中調用select

Python中,select,poll,epoll和unix的kqueue()都在模塊select中。數組

調用select的函數爲select.select(rlist, wlist, xlist[, timeout]),前三個參數都分別是三個數組,數組中的對象均爲waitable object:均是整數的文件描述符(file descriptor)或者一個擁有返回文件描述符方法fileno()的對象;app

  • rlist: 等待讀就緒的list
  • wlist: 等待寫就緒的list
  • xlist: 等待「異常」的list

這三個list能夠是一個空的list,可是接收3個空的list是依賴於系統的(在Linux上是能夠接受的,可是在window上是不能夠的)。socket

timeout參數是接受一個 float 的數字,單位是。當缺省timeout時,select會一直阻塞之道至少有一個文件描述符(fd)準備就緒。若是timeout設爲0時,則select不會阻塞。函數

函數的返回值是返回三個準備就緒的list: 對應者rlist, wlist, xlist這三個list的子集。若是timeout,會返回3個空的list。unix

在list中能夠接受Ptython的的file對象(好比sys.stdin,或者會被open()os.open()返回的object),socket object將會返回socket.socket()。也能夠自定義類,只要有一個合適的fileno()的方法(須要真實返回一個文件描述符,而不是一個隨機的整數)。code

Python的簡單示例

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import select, socket

response = b"hello world"

#建立一個server socket
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('localhost', 8080))
serversocket.listen(1)
serversocket.setblocking(0)

inputs = [serversocket, ]

while True:
    rlist, wlist, xlist = select.select(inputs, [], [])
    for sock in rlist:
        # server socket讀就緒
        if sock == serversocket:
            con, addr = serversocket.accept()
            #將這個connection添加到讀就緒中
            inputs.append(con)
        else:
            data = sock.recv(1024)
            if data:
                sock.send(response)
                #從讀就緒的list中刪除
                inputs.remove(sock)
                sock.close()

poll

poll的原理

poll本質上和select沒有區別,只是沒有了最大鏈接數(linux上默認1024個)的限制,緣由是它基於鏈表存儲的。server

poll的缺點

poll除了沒有了最大鏈接數的缺點,其餘都和select同樣

在Python中調用poll

  • select.poll(),返回一個poll的對象,支持註冊和註銷文件描述符。

  • poll.register(fd[, eventmask])註冊一個文件描述符,註冊後,能夠經過poll()方法來檢查是否有對應的I/O事件發生。fd能夠是i 個整數,或者有返回整數的fileno()方法對象。若是File對象實現了fileno(),也能夠看成參數使用。

  • eventmask是一個你想去檢查的事件類型,它能夠是常量POLLIN, POLLPRIPOLLOUT的組合。若是缺省,默認會去檢查全部的3種事件類型。

事件常量 意義
POLLIN 有數據讀取
POLLPRT 有數據緊急讀取
POLLOUT 準備輸出:輸出不會阻塞
POLLERR 某些錯誤狀況出現
POLLHUP 掛起
POLLNVAL 無效請求:描述沒法打開
  • poll.modify(fd, eventmask) 修改一個已經存在的fd,和poll.register(fd, eventmask)有相同的做用。若是去嘗試修改一個未經註冊的fd,會引發一個errnoENOENTIOError
  • poll.unregister(fd)從poll對象中註銷一個fd。嘗試去註銷一個未經註冊的fd,會引發KeyError
  • poll.poll([timeout])去檢測已經註冊了的文件描述符。會返回一個可能爲空的list,list中包含着(fd, event)這樣的二元組。 fd是文件描述符, event是文件描述符對應的事件。若是返回的是一個空的list,則說明超時了且沒有文件描述符有事件發生。timeout的單位是milliseconds,若是設置了timeout,系統將會等待對應的時間。若是timeout缺省或者是None,這個方法將會阻塞直到對應的poll對象有一個事件發生。

Python簡單示例

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import select, socket

response = b"hello world"

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('192.168.199.197', 8080))
serversocket.listen(1)
serversocket.setblocking(0)

#
poll = select.poll()
poll.register(serversocket.fileno(), select.POLLIN)

connections = {}
while True:
    for fd, event in poll.poll():
        if event == select.POLLIN:
            if fd == serversocket.fileno():
                con, addr = serversocket.accept()
                poll.register(con.fileno(), select.POLLIN)
                connections[con.fileno()] = con
            else:
                con = connections[fd]
                data = con.recv(1024)
                if data:
                    poll.modify(con.fileno(), select.POLLOUT)
        elif event == select.POLLOUT:
            con = connections[fd]
            con.send(response)
            poll.unregister(con.fileno())
            con.close()

epoll

epoll的原理及改進

在linux2.6(準確來講是2.5.44)由內核直接支持的方法。epoll解決了select和poll的缺點。

  • 對於第一個缺點,epoll的解決方法是每次註冊新的事件到epoll中,會把全部的fd拷貝進內核,而不是在等待的時候重複拷貝,保證了每一個fd在整個過程當中只會拷貝1次。
  • 對於第二個缺點,epoll沒有這個限制,它所支持的fd上限是最大能夠打開文件的數目,具體數目能夠cat /proc/sys/fs/file-max查看,通常來講這個數目和系統內存關係比較大。
  • 對於第三個缺點,epoll的解決方法不像select和poll每次對全部fd進行遍歷輪詢全部fd集合,而是在註冊新的事件時,爲每一個fd指定一個回調函數,當設備就緒的時候,調用這個回調函數,這個回調函數就會把就緒的fd加入一個就緒表中。(因此epoll實際只須要遍歷就緒表)。

epoll同時支持水平觸發和邊緣觸發:

  • 水平觸發(level-triggered):只要知足條件,就觸發一個事件(只要有數據沒有被獲取,內核就不斷通知你)。e.g:在水平觸發模式下,重複調用epoll.poll()會重複通知關注的event,直到與該event有關的全部數據都已被處理。(select, poll是水平觸發, epoll默認水平觸發)
  • 邊緣觸發(edge-triggered):每當狀態變化時,觸發一個事件。e.g:在邊沿觸發模式中,epoll.poll()在讀或者寫event在socket上面發生後,將只會返回一次event。調用epoll.poll()的程序必須處理全部和這個event相關的數據,隨後的epoll.poll()調用不會再有這個event的通知。

在Python中調用epoll

  • select.epoll([sizehint=-1])返回一個epoll對象。

  • eventmask

事件常量 意義
EPOLLIN 讀就緒
EPOLLOUT 寫就緒
EPOLLPRI 有數據緊急讀取
EPOLLERR assoc. fd有錯誤狀況發生
EPOLLHUP assoc. fd發生掛起
EPOLLRT 設置邊緣觸發(ET)(默認的是水平觸發)
EPOLLONESHOT 設置爲 one-short 行爲,一個事件(event)被拉出後,對應的fd在內部被禁用
EPOLLRDNORM 和 EPOLLIN 相等
EPOLLRDBAND 優先讀取的數據帶(data band)
EPOLLWRNORM 和 EPOLLOUT 相等
EPOLLWRBAND 優先寫的數據帶(data band)
EPOLLMSG 忽視
  • epoll.close()關閉epoll對象的文件描述符。
  • epoll.fileno返回control fd的文件描述符number。
  • epoll.fromfd(fd)用給予的fd來建立一個epoll對象。
  • epoll.register(fd[, eventmask])在epoll對象中註冊一個文件描述符。(若是文件描述符已經存在,將會引發一個IOError
  • epoll.modify(fd, eventmask)修改一個已經註冊的文件描述符。
  • epoll.unregister(fd)註銷一個文件描述符。
  • epoll.poll(timeout=-1[, maxevnets=-1])等待事件,timeout(float)的單位是秒(second)。

Ptython示例

epoll的示例就直接引用這篇出名的blog

import socket, select

EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
response += b'Hello, world!'

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
serversocket.setblocking(0)

epoll = select.epoll()
epoll.register(serversocket.fileno(), select.EPOLLIN)

try:
   connections = {}; requests = {}; responses = {}
   while True:
      events = epoll.poll(1)
      for fileno, event in events:
         if fileno == serversocket.fileno():
            connection, address = serversocket.accept()
            connection.setblocking(0)
            epoll.register(connection.fileno(), select.EPOLLIN)
            connections[connection.fileno()] = connection
            requests[connection.fileno()] = b''
            responses[connection.fileno()] = response
         elif event & select.EPOLLIN:
            requests[fileno] += connections[fileno].recv(1024)
            if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
               epoll.modify(fileno, select.EPOLLOUT)
               print('-'*40 + '\n' + requests[fileno].decode()[:-2])
         elif event & select.EPOLLOUT:
            byteswritten = connections[fileno].send(responses[fileno])
            responses[fileno] = responses[fileno][byteswritten:]
            if len(responses[fileno]) == 0:
               epoll.modify(fileno, 0)
               connections[fileno].shutdown(socket.SHUT_RDWR)
         elif event & select.EPOLLHUP:
            epoll.unregister(fileno)
            connections[fileno].close()
            del connections[fileno]
finally:
   epoll.unregister(serversocket.fileno())
   epoll.close()
   serversocket.close()
相關文章
相關標籤/搜索