深入理解python中的select模組

pythontab發表於2017-04-25

簡介

Python中的select模組專注於I/O多路複用,提供了select  poll  epoll三個方法(其中後兩個在Linux中可用,windows僅支援select),另外也提供了kqueue方法(freeBSD系統)

select方法

程式指定核心監聽哪些檔案描述符(最多監聽1024個fd)的哪些事件,當沒有檔案描述符事件發生時,程式被阻塞;當一個或者多個檔案描述符事件發生時,程式被喚醒。

當我們呼叫select()時:

  1、上下文切換轉換為核心態

  2、將fd從使用者空間複製到核心空間

  3、核心遍歷所有fd,檢視其對應事件是否發生

  4、如果沒發生,將程式阻塞,當裝置驅動產生中斷或者timeout時間後,將程式喚醒,再次進行遍歷

  5、返回遍歷後的fd

  6、將fd從核心空間複製到使用者空間

fd:file descriptor 檔案描述符

fd_r_list, fd_w_list, fd_e_list = select.select(rlist, wlist, xlist, [timeout])

引數: 可接受四個引數(前三個必須)

rlist: wait until ready for reading

wlist: wait until ready for writing

xlist: wait for an “exceptional condition”

timeout: 超時時間

返回值:三個列表

select方法用來監視檔案描述符(當檔案描述符條件不滿足時,select會阻塞),當某個檔案描述符狀態改變後,會返回三個列表

    1、當引數1 序列中的fd滿足“可讀”條件時,則獲取發生變化的fd並新增到fd_r_list中

    2、當引數2 序列中含有fd時,則將該序列中所有的fd新增到 fd_w_list中

    3、當引數3 序列中的fd發生錯誤時,則將該發生錯誤的fd新增到 fd_e_list中

    4、當超時時間為空,則select會一直阻塞,直到監聽的控制程式碼發生變化

   當超時時間 = n(正整數)時,那麼如果監聽的控制程式碼均無任何變化,則select會阻塞n秒,之後返回三個空列表,如果監聽的控制程式碼有變化,則直接執行。


例項:

利用select實現一個可併發的服務端

import socket
import select
 
s = socket.socket()
s.bind(('127.0.0.1',8888))
s.listen(5)
r_list = [s,]
num = 0
while True:
 rl, wl, error = select.select(r_list,[],[],10)
 num+=1
 print('counts is %s'%num)
 print("rl's length is %s"%len(rl))
 for fd in rl:
  if fd == s:
   conn, addr = fd.accept()
   r_list.append(conn)
   msg = conn.recv(200)
   conn.sendall(('first----%s'%conn.fileno()).encode())
  else:
   try:
    msg = fd.recv(200)
    fd.sendall('second'.encode())
   except ConnectionAbortedError:
    r_list.remove(fd)
 
s.close()


import socket
 
flag = 1
s = socket.socket()
s.connect(('127.0.0.1',8888))
while flag:
 input_msg = input('input>>>')
 if input_msg == '0':
  break
 s.sendall(input_msg.encode())
 msg = s.recv(1024)
 print(msg.decode())
 
s.close()

在服務端我們可以看到,我們需要不停的呼叫select, 這就意味著:

  1  當檔案描述符過多時,檔案描述符在使用者空間與核心空間進行copy會很費時

  2  當檔案描述符過多時,核心對檔案描述符的遍歷也很浪費時間

  3  select最大僅僅支援1024個檔案描述符

poll與select相差不大,本文不作介紹

epoll方法:

epoll很好的改進了select:

  1、epoll的解決方案在epoll_ctl函式中。每次註冊新的事件到epoll控制程式碼中時,會把所有的fd複製進核心,而不是在epoll_wait的時候重複複製。epoll保證了每個fd在整個過程中只會複製一次。

  2、epoll會在epoll_ctl時把指定的fd遍歷一遍(這一遍必不可少)併為每個fd指定一個回撥函式,當裝置就緒,喚醒等待佇列上的等待者時,就會呼叫這個回撥函式,而這個回撥函式會把就緒的fd加入一個就緒連結串列。epoll_wait的工作實際上就是在這個就緒連結串列中檢視有沒有就緒的fd

  3、epoll對檔案描述符沒有額外限制


select.epoll(sizehint=-1, flags=0) 建立epoll物件

 

epoll.close()

Close the control file descriptor of the epoll object.關閉epoll物件的檔案描述符

 

epoll.closed

True if the epoll object is closed.檢測epoll物件是否關閉

 

epoll.fileno()

Return the file descriptor number of the control fd.返回epoll物件的檔案描述符

 

epoll.fromfd(fd)

Create an epoll object from a given file descriptor.根據指定的fd建立epoll物件

 

epoll.register(fd[, eventmask])

Register a fd descriptor with the epoll object.向epoll物件中註冊fd和對應的事件

 

epoll.modify(fd, eventmask)

Modify a registered file descriptor.修改fd的事件

 

epoll.unregister(fd)

Remove a registered file descriptor from the epoll object.取消註冊

 

epoll.poll(timeout=-1, maxevents=-1)

Wait for events. timeout in seconds (float)阻塞,直到註冊的fd事件發生,會返回一個dict,格式為:{(fd1,event1),(fd2,event2),……(fdn,eventn)}

事件:


EPOLLIN Available for read 可讀 狀態符為1

EPOLLOUT Available for write 可寫 狀態符為4

EPOLLPRI Urgent data for read

EPOLLERR Error condition happened on the assoc. fd 發生錯誤 狀態符為8

EPOLLHUP Hang up happened on the assoc. fd 掛起狀態

EPOLLET Set Edge Trigger behavior, the default is Level Trigger behavior 預設為水平觸發,設定該事件後則邊緣觸發

EPOLLONESHOT Set one-shot behavior. After one event is pulled out, the fd is internally disabled

EPOLLRDNORM Equivalent to EPOLLIN

EPOLLRDBAND Priority data band can be read.

EPOLLWRNORM Equivalent to EPOLLOUT

EPOLLWRBAND Priority data may be written.

EPOLLMSG Ignored.

水平觸發和邊緣觸發:

Level_triggered(水平觸發,有時也稱條件觸發):當被監控的檔案描述符上有可讀寫事件發生時,epoll.poll()會通知處理程式去讀寫。如果這次沒有把資料一次性全部讀寫完(如讀寫緩衝區太小),那麼下次呼叫 epoll.poll()時,它還會通知你在上沒讀寫完的檔案描述符上繼續讀寫,當然如果你一直不去讀寫,它會一直通知你!!!如果系統中有大量你不需要讀寫的就緒檔案描述符,而它們每次都會返回,這樣會大大降低處理程式檢索自己關心的就緒檔案描述符的效率!!! 優點很明顯:穩定可靠

Edge_triggered(邊緣觸發,有時也稱狀態觸發):當被監控的檔案描述符上有可讀寫事件發生時,epoll.poll()會通知處理程式去讀寫。如果這次沒有把資料全部讀寫完(如讀寫緩衝區太小),那麼下次呼叫epoll.poll()時,它不會通知你,也就是它只會通知你一次,直到該檔案描述符上出現第二次可讀寫事件才會通知你!!!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒檔案描述符!!!缺點:某些條件下不可靠

epoll例項:

import socket
import select
 
s = socket.socket()
s.bind(('127.0.0.1',8888))
s.listen(5)
epoll_obj = select.epoll()
epoll_obj.register(s,select.EPOLLIN)
connections = {}
while True:
 events = epoll_obj.poll()
 for fd, event in events:
  print(fd,event)
  if fd == s.fileno():
   conn, addr = s.accept()
   connections[conn.fileno()] = conn
   epoll_obj.register(conn,select.EPOLLIN)
   msg = conn.recv(200)
   conn.sendall('ok'.encode())
  else:
   try:
    fd_obj = connections[fd]
    msg = fd_obj.recv(200)
    fd_obj.sendall('ok'.encode())
   except BrokenPipeError:
    epoll_obj.unregister(fd)
    connections[fd].close()
    del connections[fd]
 
s.close()
epoll_obj.close()
import socket
 
flag = 1
s = socket.socket()
s.connect(('127.0.0.1',8888))
while flag:
 input_msg = input('input>>>')
 if input_msg == '0':
  break
 s.sendall(input_msg.encode())
 msg = s.recv(1024)
 print(msg.decode())
 
s.close()


相關文章