Programming
What are the differences between poll and select
In the realm of concurrent programming and network communication, efficiently handling multiple input/output (I/O) operations is paramount for building robust and responsive applications. Developers often encounter the challenge of managing numerous open connections or file descriptors without blocking the entire program while waiting for a single operation to complete. This is where I/O multiplexing techniques come into play, with select() and poll() being two long-standing system calls provided by Unix-like operating systems. While both serve the fundamental purpose of allowing a program to monitor multiple file descriptors for readiness, their underlying mechanisms, limitations, and performance characteristics exhibit significant poll and select differences. Understanding these distinctions is crucial for optimizing application performance and scalability, especially in server-side development or real-time systems where efficient resource management is key.
Understanding I/O Multiplexing: The Core Need
At its heart, I/O multiplexing is a technique that enables a single process or thread to monitor multiple I/O streams simultaneously. Without it, a program would typically have to dedicate a separate thread or process to each connection, or it would block indefinitely on a single I/O operation while other connections remain unserviced. Consider a web server handling thousands of client connections; waiting for data from one client before attending to another would lead to abysmal performance and unresponsiveness. This is precisely the problem that I/O multiplexing aims to solve, allowing a program to efficiently manage a large number of concurrent I/O events.
The traditional approach to handling I/O without multiplexing involves blocking system calls. When a program calls read() or write() on a socket, it pauses execution until the operation completes. If multiple sockets are involved, this blocking behavior becomes problematic. I/O multiplexing system calls like select() and poll() provide a mechanism to ask the kernel which file descriptors are ready for reading or writing without blocking. This non-blocking inquiry allows the application to respond to events as they occur across various connections, leading to more efficient resource utilization and improved throughput for applications that manage numerous concurrent I/O streams, such as chat servers, proxy servers, or database systems.
select(): The Traditional Approach
The select() system call is one of the oldest and most widely supported mechanisms for I/O multiplexing across Unix-like operating systems, dating back to 4.2BSD. It allows a program to monitor multiple file descriptors, waiting until one or more of them become “ready” for some I/O operation (e.g., read, write, or error conditions). To use select(), you pass three sets of file descriptors—one for reading, one for writing, and one for exceptional conditions—along with a timeout value. The kernel then modifies these sets to indicate which descriptors are ready.
A significant characteristic of select() is its use of fd_set, which is typically implemented as a bitmask. Each bit in the fd_set corresponds to a file descriptor. This bitmask approach imposes a fundamental limitation: the maximum number of file descriptors that select() can monitor is fixed by FD_SETSIZE, a compile-time constant (often 1024). While this limit can sometimes be increased by recompiling the kernel or application, it’s generally not a flexible solution for applications requiring thousands of concurrent connections. Furthermore, every time select() is called, the entire fd_set must be copied between user space and kernel space, and the kernel must iterate through all bits in the set to determine readiness, leading to performance degradation as the number of monitored file descriptors grows.
When comparing poll and select differences, a key point is their underlying mechanism for managing file descriptors. While select() relies on bitmasks within fixed-size fd_set structures, enforcing a strict limit on the number of concurrent file descriptors that can be monitored (typically 1024), poll() uses a flexible array of pollfd structures. This fundamental design choice gives poll() a significant advantage in terms of scalability, as it avoids the inherent FD_SETSIZE limitation of select(), making it more suitable for applications that need to handle a very large number of open connections.
poll(): A More Scalable Alternative
Introduced as part of System V Release 3, poll() emerged as an alternative to select(), designed to address some of its inherent limitations. Instead of using bitmasks, poll() operates on an array of struct pollfd. Each element in this array specifies a file descriptor to be monitored and the events of interest (e.g., POLLIN for read readiness, POLLOUT for write readiness). When poll() returns, the revents member of each pollfd structure is updated to indicate the actual events that occurred.
The primary advantage of poll() over select() lies in its scalability. Since it uses an array of structures, there is no fixed compile-time limit on the number of file descriptors it can monitor, other than available system memory and resources. This makes poll() much more suitable for high-performance servers and applications that need to manage thousands, or even tens of thousands, of concurrent connections. The array-based approach also simplifies file descriptor management, as you don’t need to manually manipulate bitmasks or clear them after each call, as is often the case with select().
Despite its advantages, poll() still involves copying the entire array of pollfd structures between user space and kernel space on each call. Additionally, the kernel still needs to iterate through the entire array to check for ready file descriptors. While more flexible than select(), for extremely high-concurrency scenarios (e.g., web servers handling millions of connections), even poll() can become a bottleneck. This led to the development of even more advanced mechanisms like epoll on Linux and kqueue on FreeBSD, which offer edge-triggered event notification and more efficient kernel-side management of monitored descriptors.
Key Differences Between poll and select
Question & Answer :
I am referring to the POSIX standard select and poll system C API calls.
The select() call has you create three bitmasks to mark which sockets and file descriptors you want to watch for reading, writing, and errors, and then the operating system marks which ones in fact have had some kind of activity; poll() has you create a list of descriptor IDs, and the operating system marks each of them with the kind of event that occurred.
The select() method is rather clunky and inefficient.
- There are typically more than a thousand potential file descriptors available to a process. If a long-running process has only a few descriptors open, but at least one of them has been assigned a high number, then the bitmask passed to
select()has to be large enough to accomodate that highest descriptor — so whole ranges of hundreds of bits will be unset that the operating system has to loop across on everyselect()call just to discover that they are unset. - Once
select()returns, the caller has to loop over all three bitmasks to determine what events took place. In very many typical applications only one or two file descriptors will get new traffic at any given moment, yet all three bitmasks must be read all the way to the end to discover which descriptors those are. - Because the operating system signals you about activity by rewriting the bitmasks, they are ruined and are no longer marked with the list of file descriptors you want to listen to. You either have to rebuild the whole bitmask from some other list that you keep in memory, or you have to keep a duplicate copy of each bitmask and
memcpy()the block of data over on top of the ruined bitmasks after eachselect()call.
So the poll() approach works much better because you can keep re-using the same data structure.
In fact, poll() has inspired yet another mechanism in modern Linux kernels: epoll() which improves even more upon the mechanism to allow yet another leap in scalability, as today’s servers often want to handle tens of thousands of connections at once. This is a good introduction to the effort:
http://scotdoyle.com/python-epoll-howto.html
While this link has some nice graphs showing the benefits of epoll() (you will note that select() is by this point considered so inefficient and old-fashioned that it does not even get a line on these graphs!):
http://lse.sourceforge.net/epoll/index.html
Update: Here is another Stack Overflow question, whose answer gives even more detail about the differences: