📜 ⬆️ ⬇️

How to use linux epoll in python

The article describes:

Introduction


Since version 2.6, Python includes an API for working with the epoll Linux library. This article briefly demonstrates this API with Python 3 code examples.

From the translator.
I tried not to abuse English terms as much as possible. So “register / unregister” became “subscription / unsubscribe”, “print to the console” - “console output”. “Production server” decided to translate as “loaded server”, since nothing better than “production server” does not occur to me. “Thread” translated as “stream”, not “thread”.
The names of events, modes and flags decided not to translate at all, giving only a one-time example of a possible translation.
Although it is written that the code is for Python 3, everything works fine on Python 2.6.

Examples of using blocking sockets


The first example is a simple Python 3.0 server that listens on port 8080 for incoming HTTP requests, displays them on the console, and sends an HTTP response message to the client.

The official HOWTO contains a more detailed description of socket programming in Python.
')
Example 1
Copy Source | Copy HTML
  1. import socket
  2. EOL1 = b '\ n \ n'
  3. EOL2 = b '\ n \ r \ n'
  4. response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
  5. response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
  6. response + = b 'Hello, world!'
  7. serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
  8. serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
  9. serversocket.bind (( '0.0.0.0' , 8080 ))
  10. serversocket.listen ( 1 )
  11. connectiontoclient, address = serversocket.accept ()
  12. request = b ''
  13. while EOL1 is not in request and EOL2 is not in request:
  14. request + = connectiontoclient.recv ( 1024 )
  15. print (request.decode ())
  16. connectiontoclient.send (response)
  17. connectiontoclient.close ()
  18. serversocket.close ()

Example 2 adds a loop in line 15 to re-process client connections that are performed before a user interrupt (for example, from the keyboard). This more clearly shows that the server socket is never used to exchange data with the client. Rather, it only accepts the connection from the client and creates a new socket, which is already used for communication.
The finally block in lines 23-24 is needed so that the listening server socket is closed in any case, even if errors occur.

Example 2
Copy Source | Copy HTML
  1. import socket
  2. EOL1 = b '\ n \ n'
  3. EOL2 = b '\ n \ r \ n'
  4. response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
  5. response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
  6. response + = b 'Hello, world!'
  7. serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
  8. serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
  9. serversocket.bind (( '0.0.0.0' , 8080 ))
  10. serversocket.listen ( 1 )
  11. try :
  12. while true:
  13. connectiontoclient, address = serversocket.accept ()
  14. request = b ''
  15. while EOL1 is not in request and EOL2 is not in request:
  16. request + = connectiontoclient.recv ( 1024 )
  17. print ( '-' * 40 + '\ n' + request.decode () [: - 2 ])
  18. connectiontoclient.send (response)
  19. connectiontoclient.close ()
  20. finally :
  21. serversocket.close ()


Benefits of asynchronous sockets and linux epoll


The sockets shown in Example 2 are called blocking sockets, because the Python program pauses its execution until the event arrives. The accept () call on line 16 is blocked until the connection is received from the client. The recv () call on line 19 is blocked until receiving data from the client (or until there is no data to receive). The send () call on line 21 is blocked before all data sent to the client is added to the Linux send queue.

When a program uses blocking sockets, it often uses a separate thread (or even process) to perform interaction with each of these sockets. The main program flow contains a listening server socket, which accepts incoming connections from clients. It accepts these connections one at a time, passing the newly created client socket to a separate thread that will interact with the client. Since each of these flows is associated with only one client, it is acceptable that network locks occur in some places. These locks do not prevent other threads from performing their tasks.

Using blocking sockets with multiple threads leads to simple code, but is associated with a series of flaws . It is difficult to be sure of the correct sharing of streams to shared resources. And this programming style is not very effective on computers with a single CPU.

Issue C10K discusses alternative processing options for multiple concurrent sockets. One of them is to use asynchronous sockets. Such sockets are not blocked until the event arrives. On the contrary, the program performs an action on an asynchronous socket and immediately receives a notification of success or error. This information allows the program to decide how to proceed. Since asynchronous sockets are non-blocking, there is no need for multiple execution threads. All work can be done in a single thread. Such a single-threaded approach has its own problems and is not a good choice for many programs. But it can be combined with a multi-threaded approach: asynchronous sockets applied in a single stream can be used for the network component of the server, and streams can be used to access external blocking resources, such as databases.

Linux 2.6 has a number of mechanisms for managing asynchronous sockets, three of which are represented in the Python API through select , poll and epoll. epoll and poll are better than select , because the Python program does not need to keep track of all events of interest in the socket. Instead, you can rely on the operating system to report which events occurred on which sockets. And epoll, in turn, is better than poll , because it does not require the operating system to check all sockets for events of interest every time it is requested by the Python program. Rather, when requesting from Python, Linux checks if these events have occurred, and returns a list of events. So, epoll is a more efficient and scalable mechanism for a large number (thousands) of simultaneous connections, as shown in these graphs .

Examples of asynchronous use of sockets through epoll


Programs using epoll often work on the following principle:
  1. An epoll object is created.
  2. The epoll object is indicated to monitor certain events on certain sockets.
  3. The epoll object is requested on which sockets the specified events occurred since the previous poll
  4. Some actions are performed on these sockets.
  5. The epoll object is indicated to change the list of sockets and / or observable events.
  6. Repeat steps 3 through 5 to completion.
  7. The epoll object is destroyed.

Example 3 repeats the functionality of Example 2 using asynchronous sockets. The program is more complicated because one thread interacts with multiple clients in turn.

Example 3
Copy Source | Copy HTML
  1. import socket, select
  2. EOL1 = b '\ n \ n'
  3. EOL2 = b '\ n \ r \ n'
  4. response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
  5. response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
  6. response + = b 'Hello, world!'
  7. serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
  8. serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
  9. serversocket.bind (( '0.0.0.0' , 8080 ))
  10. serversocket.listen ( 1 )
  11. serversocket.setblocking ( 0 )
  12. epoll = select .epoll ()
  13. epoll.register (serversocket.fileno (), select .EPOLLIN)
  14. try :
  15. connections = {}; requests = {}; responses = {}
  16. while true:
  17. events = epoll.poll ( 1 )
  18. for fileno, event in events:
  19. if fileno == serversocket.fileno ():
  20. connection, address = serversocket.accept ()
  21. connection.setblocking ( 0 )
  22. epoll.register (connection.fileno (), select .EPOLLIN)
  23. connections [connection.fileno ()] = connection
  24. requests [connection.fileno ()] = b ''
  25. responses [connection.fileno ()] = response
  26. elif event & select .EPOLLIN:
  27. requests [fileno] + = connections [fileno] .recv ( 1024 )
  28. if EOL1 in requests [fileno] or EOL2 in requests [fileno]:
  29. epoll.modify (fileno, select .EPOLLOUT)
  30. print ( '-' * 40 + '\ n' + requests [fileno] .decode () [: - 2 ])
  31. elif event & select .EPOLLOUT:
  32. byteswritten = connections [fileno] .send (responses [fileno])
  33. responses [fileno] = responses [fileno] [byteswritten:]
  34. if len (responses [fileno]) == 0 :
  35. epoll.modify (fileno, 0 )
  36. connections [fileno] .shutdown ( socket .SHUT_RDWR)
  37. elif event & select .EPOLLHUP:
  38. epoll.unregister (fileno)
  39. connections [fileno] .close ()
  40. del connections [fileno]
  41. finally :
  42. epoll.unregister (serversocket.fileno ())
  43. epoll.close ()
  44. serversocket.close ()

epoll has two modes of operation, called edge-triggered and level-triggered. In the edge-triggered mode, a call to epoll.poll () will return an event only after a read or write event occurs on the socket. The caller must process all the data associated with this event without having to call epoll.poll () again . When data from a particular event is exhausted, additional attempts to work with the socket will lead to exceptions. On the contrary, in the level-triggered mode, repeated calls to epoll.poll () will give repeated notifications about the events of interest until all the data associated with the events has been processed. No exceptions occur during normal operation in the level-triggered mode.

For example, suppose a server socket was signed in the epoll object for read events. In the edge-triggered mode, the program should call accept () to accept new connections until a socket.error exception occurs . In level-triggered mode, a single accept () call can be made, and then an epoll object can be requested again for the next event in the queue.

Example 3 uses level-triggered mode, which is the default mode. Example 4 demonstrates how to use edge-triggered mode. In lines 25, 36 and 45, cycles are entered that I work until an exception occurs (or it becomes known that all data has been processed). Lines 32, 38 and 48 catch exceptions. Finally, lines 16, 28, 41, and 51 add an EPOLLET mask, which defines an edge-triggered mode.

Example 4
Copy Source | Copy HTML
  1. import socket, select
  2. EOL1 = b '\ n \ n'
  3. EOL2 = b '\ n \ r \ n'
  4. response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
  5. response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
  6. response + = b 'Hello, world!'
  7. serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
  8. serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
  9. serversocket.bind (( '0.0.0.0' , 8080 ))
  10. serversocket.listen ( 1 )
  11. serversocket.setblocking ( 0 )
  12. epoll = select .epoll ()
  13. epoll.register (serversocket.fileno (), select .EPOLLIN | select .EPOLLET)
  14. try :
  15. connections = {}; requests = {}; responses = {}
  16. while true:
  17. events = epoll.poll ( 1 )
  18. for fileno, event in events:
  19. if fileno == serversocket.fileno ():
  20. try :
  21. while true:
  22. connection, address = serversocket.accept ()
  23. connection.setblocking ( 0 )
  24. epoll.register (connection.fileno (), select .EPOLLIN | select .EPOLLET)
  25. connections [connection.fileno ()] = connection
  26. requests [connection.fileno ()] = b ''
  27. responses [connection.fileno ()] = response
  28. except socket .error:
  29. pass
  30. elif event & select .EPOLLIN:
  31. try :
  32. while true:
  33. requests [fileno] + = connections [fileno] .recv ( 1024 )
  34. except socket .error:
  35. pass
  36. if EOL1 in requests [fileno] or EOL2 in requests [fileno]:
  37. epoll.modify (fileno, select .EPOLLOUT | select .EPOLLET)
  38. print ( '-' * 40 + '\ n' + requests [fileno] .decode () [: - 2 ])
  39. elif event & select .EPOLLOUT:
  40. try :
  41. while len (responses [fileno])> 0 :
  42. byteswritten = connections [fileno] .send (responses [fileno])
  43. responses [fileno] = responses [fileno] [byteswritten:]
  44. except socket .error:
  45. pass
  46. if len (responses [fileno]) == 0 :
  47. epoll.modify (fileno, select .EPOLLET)
  48. connections [fileno] .shutdown ( socket .SHUT_RDWR)
  49. elif event & select .EPOLLHUP:
  50. epoll.unregister (fileno)
  51. connections [fileno] .close ()
  52. del connections [fileno]
  53. finally :
  54. epoll.unregister (serversocket.fileno ())
  55. epoll.close ()
  56. serversocket.close ()

For all the similarities, the level-triggered mode is often used when porting applications using select or poll mechanisms, while the edge-triggered mode can be used by the programmer in the case when there is no need for such support for managing the state of events from the operating system.

In addition to these two modes, sockets can also be signed in the epoll to an EPOLLONESHOT event. When using this option, the event is correct only for a single call to epoll.poll (), after which it is automatically removed from the list of observed events.

Performance issues


The length of the server connection queue

The 12th line of all examples shows the call to the serversocket.listen () method. The parameter for this method is the length of the server connection queue (listen backlog). It tells the operating system about the maximum accepted number of TCP / IP connections that can be placed in the system queue before the Python program accepts them. Each time a Python program calls accept () on a server socket, one of the connections is removed from the queue and the space freed up can be used for another incoming connection. When the queue is full, new incoming connections are silently ignored, resulting in unnecessary delays on the client side. A busy server typically handles hundreds and thousands of simultaneous connections, so a value of 1 will be inadequate. As an example, using ab for load testing the above examples with hundreds of simultaneous HTTP 1.0 clients, the queue length of less than 50 can sometimes lead to a dramatic drop in performance.

TCP settings

The TCP_CORK option can block (bottle up) sending data until it is ready. This option, illustrated in lines 34 and 40 of Example 5, can be useful for an HTTP server using the HTTP / 1.1 pipeline.

Example 5
Copy Source | Copy HTML
  1. import socket, select
  2. EOL1 = b '\ n \ n'
  3. EOL2 = b '\ n \ r \ n'
  4. response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
  5. response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
  6. response + = b 'Hello, world!'
  7. serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
  8. serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
  9. serversocket.bind (( '0.0.0.0' , 8080 ))
  10. serversocket.listen ( 1 )
  11. serversocket.setblocking ( 0 )
  12. epoll = select .epoll ()
  13. epoll.register (serversocket.fileno (), select .EPOLLIN)
  14. try :
  15. connections = {}; requests = {}; responses = {}
  16. while true:
  17. events = epoll.poll ( 1 )
  18. for fileno, event in events:
  19. if fileno == serversocket.fileno ():
  20. connection, address = serversocket.accept ()
  21. connection.setblocking ( 0 )
  22. epoll.register (connection.fileno (), select .EPOLLIN)
  23. connections [connection.fileno ()] = connection
  24. requests [connection.fileno ()] = b ''
  25. responses [connection.fileno ()] = response
  26. elif event & select .EPOLLIN:
  27. requests [fileno] + = connections [fileno] .recv ( 1024 )
  28. if EOL1 in requests [fileno] or EOL2 in requests [fileno]:
  29. epoll.modify (fileno, select .EPOLLOUT)
  30. connections [fileno] .setsockopt ( socket .IPPROTO_TCP, socket .TCP_CORK, 1 )
  31. print ( '-' * 40 + '\ n' + requests [fileno] .decode () [: - 2 ])
  32. elif event & select .EPOLLOUT:
  33. byteswritten = connections [fileno] .send (responses [fileno])
  34. responses [fileno] = responses [fileno] [byteswritten:]
  35. if len (responses [fileno]) == 0 :
  36. connections [fileno] .setsockopt ( socket .IPPROTO_TCP, socket .TCP_CORK, 0 )
  37. epoll.modify (fileno, 0 )
  38. connections [fileno] .shutdown ( socket .SHUT_RDWR)
  39. elif event & select .EPOLLHUP:
  40. epoll.unregister (fileno)
  41. connections [fileno] .close ()
  42. del connections [fileno]
  43. finally :
  44. epoll.unregister (serversocket.fileno ())
  45. epoll.close ()
  46. serversocket.close ()

On the other hand, the TCP_NODELAY option tells the system that any data passed to socket.send () should immediately be sent to the client without buffering by the operating system. This option, illustrated in line 14 of Example 6, can be useful for SSH clients and other real-time applications.

Example 6
Copy Source | Copy HTML
  1. import socket, select
  2. EOL1 = b '\ n \ n'
  3. EOL2 = b '\ n \ r \ n'
  4. response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
  5. response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
  6. response + = b 'Hello, world!'
  7. serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
  8. serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
  9. serversocket.bind (( '0.0.0.0' , 8080 ))
  10. serversocket.listen ( 1 )
  11. serversocket.setblocking ( 0 )
  12. serversocket.setsockopt ( socket .IPPROTO_TCP, socket .TCP_NODELAY, 1 )
  13. epoll = select .epoll ()
  14. epoll.register (serversocket.fileno (), select .EPOLLIN)
  15. try :
  16. connections = {}; requests = {}; responses = {}
  17. while true:
  18. events = epoll.poll ( 1 )
  19. for fileno, event in events:
  20. if fileno == serversocket.fileno ():
  21. connection, address = serversocket.accept ()
  22. connection.setblocking ( 0 )
  23. epoll.register (connection.fileno (), select .EPOLLIN)
  24. connections [connection.fileno ()] = connection
  25. requests [connection.fileno ()] = b ''
  26. responses [connection.fileno ()] = response
  27. elif event & select .EPOLLIN:
  28. requests [fileno] + = connections [fileno] .recv ( 1024 )
  29. if EOL1 in requests [fileno] or EOL2 in requests [fileno]:
  30. epoll.modify (fileno, select .EPOLLOUT)
  31. print ( '-' * 40 + '\ n' + requests [fileno] .decode () [: - 2 ])
  32. elif event & select .EPOLLOUT:
  33. byteswritten = connections [fileno] .send (responses [fileno])
  34. responses [fileno] = responses [fileno] [byteswritten:]
  35. if len (responses [fileno]) == 0 :
  36. epoll.modify (fileno, 0 )
  37. connections [fileno] .shutdown ( socket .SHUT_RDWR)
  38. elif event & select .EPOLLHUP:
  39. epoll.unregister (fileno)
  40. connections [fileno] .close ()
  41. del connections [fileno]
  42. finally :
  43. epoll.unregister (serversocket.fileno ())
  44. epoll.close ()
  45. serversocket.close ()

Source


The examples on this page are publicly available and can be downloaded here .

From translator


When a remote client closes a socket, an EPOLLIN event arrives on the local socket, but nothing will be received when recv is read. So moment
Copy Source | Copy HTML
elif event & select .EPOLLIN:
try :
while true:
requests [fileno] + = connections [fileno] .recv ( 1024 )

You can write this:
Copy Source | Copy HTML
elif event & select .EPOLLIN:
try :
while true:
data = connections [fileno] .recv ( 1024 )
if not data:
epoll.modify (fileno, select .EPOLLET)
connections [fileno] .shutdown ( socket .SHUT_RDWR)
else :
requests [fileno] + = data

In this case, there will be no looping when the connection is broken. Encountered a code where the gap does not occur immediately, but after several consecutive such idle positives, in order to eliminate the possibility of erroneous determination.

Source: https://habr.com/ru/post/121103/


All Articles