select / poll / epoll 区别与应用场景

本文详细对比了select、poll及epoll等I/O多路复用技术的特点与应用场景,探讨了它们在不同场景下的优缺点,并给出了选择建议。

select / poll / epoll: practical difference for system architects

January 7, 2014George

When designing a high performance networking application with non-blocking socket I/O, the architect needs to decide which polling method to use to monitor the events generated by those sockets. There are several such methods, and the use cases for each of them are different. Choosing the correct method may be critical to satisfy the application needs.

This article highlights the difference among the polling methods and provides suggestions what to use.

 

Contents [hide]

· 1 Polling with select()

· 2 Polling with poll()

· 3 Polling with epoll()

· 4 Polling with libevent

Polling with select()

Old, trusted workforce from the times the sockets were still called Berkeley sockets. It didn’t make it into the first specification though since there were no concept of non-blocking I/O at that moment, but it did make it around eighties, and nothing changed since that in its interface.

To use select, the developer needs to initialize and fill up several fd_set structures with the descriptors and the events to monitor, and then call select(). A typical workflow looks like that:

fd_set fd_in, fd_out;

struct timeval tv;

 

// Reset the sets

FD_ZERO( &fd_in );

FD_ZERO( &fd_out );

 

// Monitor sock1 for input events

FD_SET( sock1, &fd_in );

 

// Monitor sock2 for output events

FD_SET( sock2, &fd_out );

 

// Find out which socket has the largest numeric value as select requires it

int largest_sock = sock1 > sock2 ? sock1 : sock2;

 

// Wait up to 10 seconds

tv.tv_sec = 10;

tv.tv_usec = 0;

 

// Call the select

int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );

 

// Check if select actually succeed

if ( ret == -1 )

    // report error and abort

else if ( ret == 0 )

    // timeout; no event detected

else

{

    if ( FD_ISSET( sock1, &fd_in ) )

        // input event on sock1

 

    if ( FD_ISSET( sock2, &fd_out ) )

        // output event on sock2

}

When the select interface was designed and developed, nobody probably expected there would be multi-threaded applications serving many thousands connections. Hence select carries quite a few design flaws which make it undesirable as a polling mechanism in the modern networking application. The major disadvantages include:

§ select modifies the passed fd_sets so none of them can be reused. Even if you don’t need to change anything – such as if one of descriptors received data and needs to receive more data – a whole set has to be either recreated again (argh!) or restored from a backup copy via FD_COPY. And this has to be done each time theselect is called.

§ To find out which descriptors raised the events you have to manually iterate through all the descriptors in the set and call FD_ISSET on each one of them. When you have 2,000 of those descriptors and only one of them is active – and, likely, the last one – you’re wasting CPU cycles each time you wait.

§ Did I just mention 2,000 descriptors? Well, select cannot support that much. At least on Linux. The maximum number of the supported descriptors is defined by the FD_SETSIZE constant, which Linux happily defines as 1024. And while some operating systems allow you to hack this restriction by redefining the FD_SETSIZEbefore including the sys/select.h, this is not portable. Indeed, Linux would just ignore this hack and the limit will stay the same.

§ You cannot modify the descriptor set from a different thread while waiting. Suppose a thread is executing the code above. Now suppose you have a housekeeping thread which decided that sock1 has been waiting too long for the input data, and it is time to cut the cord. Since this socket could be reused to serve another payingworking client, the housekeeping thread wants to close the socket. However the socket is in the fd_set whichselect is waiting for.
Now what happens when this socket is closed? man select has the answer, and you won’t like it. The answer is, “If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”.

§ Same problem arises if another thread suddenly decides to send something via sock1. It is not possible to start monitoring the socket for the output event until select returns.

§ The choice of the events to wait for is limited; for example, to detect whether the remote socket is closed you have to a) monitor it for input and b) actually attempt to read the data from socket to detect the closure (readwill return 0). Which is fine if you want to read from this socket, but what if you’re sending a file and do not care about any input right now?

§ select puts extra burden on you when filling up the descriptor list to calculate the largest descriptor number and provide it as a function parameter.

Of course the operating system developers recognized those drawbacks and addressed most of them when designing the poll method. Therefore you may ask, is there is any reason to use select at all? Why don’t just store it in the shelf of the Computer Science Museum? Then you may be pleased to know that yes, there are two reasons, which may be either very important to you or not important at all.

The first reason is portability. select has been around for ages, and you can be sure that every single platform around which has network support and nonblocking sockets will have a working select implementation while it might not have poll at all. And unfortunately I’m not talking about the tubes and ENIAC here;poll is only available on Windows Vista and above which includes Windows XP – still used by the whooping 34% of users as of Sep 2013 despite the Microsoft pressure. Another option would be to still use poll on those platforms and emulate it with select on those which do not have it; it is up to you whether you consider it reasonable investment.

The second reason is more exotic, and is related to the fact that select can – theoretically – handle the timeouts withing the one nanosecond precision, while both poll and epoll can only handle the one millisecond precision. This is not likely to be a concern on a desktop or server system, which clocks doesn’t even run with such precision, but it may be necessary on a realtime embedded platform while interacting with some hardware components. Such as lowering control rods to shut down a nuclear reactor – in this case, please, use select to make sure we’re all stay safe!

The case above would probably be the only case where you would have to use select and could not use anything else. However if you are writing an application which would never have to handle more than a handful of sockets (like, 200), the difference between using poll and select would not be based on performance, but more on personal preference or other factors.

Polling with poll()

poll is a newer polling method which probably was created immediately after someone actually tried to write the high performance networking server. It is much better designed and doesn’t suffer from most of the problems which select has. In the vast majority of cases you would be choosing between poll and epoll/libevent.

To use poll, the developer needs to initialize the members of struct pollfd structure with the descriptors and events to monitor, and call the poll(). A typical workflow looks like that:

// The structure for two events

struct pollfd fds[2];

 

// Monitor sock1 for input

fds[0].fd = sock1;

fds[0].events = POLLIN;

 

// Monitor sock2 for output

fds[1].fd = sock2;

fds[1].events = POLLOUT;

 

// Wait 10 seconds

int ret = poll( &fds, 2, 10000 );

// Check if poll actually succeedif ( ret == -1 )    // report error and abortelse if ( ret == 0 )    // timeout; no event detectedelse{    // If we detect the event, zero it out so we can reuse the structure    if ( pfd[0].revents & POLLIN )        pfd[0].revents = 0;        // input event on sock1     if ( pfd[1].revents & POLLOUT )        pfd[1].revents = 0;        // output event on sock2}

poll was mainly created to fix the pending problems select had, so it has the following advantages over it:

§ There is no hard limit on the number of descriptors poll can monitor, so the limit of 1024 does not apply here.

§ It does not modify the data passed in the struct pollfd data. Therefore it could be reused between the poll() calls as long as set to zero the revents member for those descriptors which generated the events. The IEEE specification states that “In each pollfd structure, poll() shall clear the revents member, except that where the application requested a report on a condition by setting one of the bits of events listed above, poll() shall set the corresponding bit in revents if the requested condition is true“. However in my experience at least one platform did not follow this recommendation, and man 2 poll on Linux does not make such guarantee either (man 3p poll does though).

§ It allows more fine-grained control of events comparing to select. For example, it can detect remote peer shutdown without monitoring for read events.

There are a few disadvantages as well, which were mentioned above at the end of the select section. Notably, pollis not present on Microsoft Windows older than Vista; on Vista and above it is called WSAPoll although the prototype is the same, and it could be defined as simply as:

#if defined (WIN32)

static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); }

#endif

And, as mentioned above, poll timeout has the 1ms precision, which again is very unlikely to be a concern in most scenarios. Nevertheless poll still has a few issues which need to be kept in mind:

§ Like select, it is still not possible to find out which descriptors have the events triggered without iterating through the whole list and checking the revents. Worse, the same happens in the kernel space as well, as the kernel has to iterate through the list of file descriptors to find out which sockets are monitored, and iterate through the whole list again to set up the events.

§ Like select, it is not possible to dynamically modify the set or close the socket which is being polled (see above).

Please keep in mind, however, that those issues might be considered unimportant for most client networking applications – the only exception would be client software such as P2P which may require handling of thousands of open connections. Those issues might not be important even for some server applications. Therefore pollshould be your default choice over select unless you have specific reasons mentioned above. More, poll should be your preferred method even over epoll if the following is true:

§ You need to support more than just Linux, and do not want to use epoll wrappers such as libevent (epoll is Linux only);

§ Your application needs to monitor less than 1000 sockets at a time (you are not likely to see any benefits from using epoll);

§ Your application needs to monitor more than 1000 sockets at a time, but the connections are very short-lived (this is a close case, but most likely in this scenario you are not likely to see any benefits from using epoll because the speedup in event waiting would be wasted on adding those new descriptors into the set – see below)

§ Your application is not designed the way that it changes the events while another thread is waiting for them (i.e. you’re not porting an app using kqueue or IO Completion Ports).

Polling with epoll()

epoll is the latest, greatest, newest polling method in Linux (and only Linux). Well, it was actually added to kernel in 2002, so it is not so new. It differs both from poll and select in such a way that it keeps the information about the currently monitored descriptors and associated events inside the kernel, and exports the API to add/remove/modify those.

To use epoll, much more preparation is needed. A developer needs to:

§ Create the epoll descriptor by calling epoll_create;

§ Initialize the struct epoll structure with the wanted events and the context data pointer. Context could be anything, epoll passes this value directly to the returned events structure. We store there a pointer to our Connection class.

§ Call epoll_ctl( … EPOLL_CTL_ADD ) to add the descriptor into the monitoring set

§ Call epoll_wait() to wait for 20 events for which we reserve the storage space. Unlike previous methods, this call receives an empty structure, and fills it up only with the triggered events. For example, if there are 200 descriptors and 5 of them have events pending, the epoll_wait will return 5, and only the first five members of the pevents structure will be initialized. If 50 descriptors have events pending, the first 20 would be copied and 30 would be left in queue, they won’t get lost.

§ Iterate through the returned items. This will be a short iteration since the only events returned are those which are triggered.

A typical workflow looks like that:

// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.// The function argument is ignored (it was not before, but now it is), so put your favorite number hereint pollingfd = epoll_create( 0xCAFE );  if ( pollingfd < 0 ) // report error // Initialize the epoll structure in case more members are added in futurestruct epoll_event ev = { 0 }; // Associate the connection class instance with the event. You can associate anything// you want, epoll does not use this information. We store a connection class pointer, pConnection1ev.data.ptr = pConnection1; // Monitor for input, and do not automatically rearm the descriptor after the eventev.events = EPOLLIN | EPOLLONESHOT;

// Add the descriptor into the monitoring list. We can do it even if another thread is // waiting in epoll_wait - the descriptor will be properly addedif ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )    // report error // Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)struct epoll_event pevents[ 20 ]; // Wait for 10 secondsint ready = epoll_wait( pollingfd, pevents, 20, 10000 );

// Check if epoll actually succeedif ( ret == -1 )    // report error and abortelse if ( ret == 0 )    // timeout; no event detectedelse{    // Check if any events detected    for ( int i = 0; i < ret; i++ )    {        if ( pevents[i].events & EPOLLIN )        {            // Get back our connection pointer            Connection * c = (Connection*) pevents[i].data.ptr;            c->handleReadEvent();         }    }}

Just looking at the implementation alone should give you the hint of what are the disadvantages of epoll, which we will mention firs. It is more complex to use, and requires you to write more code, and it requires more library calls comparing to other polling methods.

However epoll has some significant advantages over select/poll both in terms of performance and functionality:

§ epoll returns only the list of descriptors which triggered the events. No need to iterate through 10,000 descriptors anymore to find that one which triggered the event!

§ You can attach meaningful context to the monitored event instead of socket file descriptors. In our example we attached the class pointers which could be called directly, saving you another lookup.

§ You can add sockets or remove them from monitoring anytime, even if another thread is in the epoll_waitfunction. You can even modify the descriptor events. Everything will work properly, and this behavior is supported and documented. This gives you much more flexibility in implementation.

§ Since the kernel knows all the monitoring descriptors, it can register the events happening on them even when nobody is calling epoll_wait. This allows implementing interesting features such as edge triggering, which will be described in a separate article.

§ It is possible to have the multiple threads waiting on the same epoll queue with epoll_wait(), something you cannot do with select/poll. In fact it is not only possible with epoll, but the recommended method in the edge triggering mode.

However you need to keep in mind that epoll is not a “better poll”, and it also has disadvantages when comparing to poll:

§ Changing the event flags (i.e. from READ to WRITE) requires the epoll_ctl syscall, while when using poll this is a simple bitmask operation done entirely in userspace. Switching 5,000 sockets from reading to writing withepoll would require 5,000 syscalls and hence context switches (as of 2014 calls to epoll_ctl still  could not be batched, and each descriptor must be changed separately), while in poll it would require a single loop over thepollfd structure.

§ Each accept()ed socket needs to be added to the set, and same as above, with epoll it has to be done by calling epoll_ctl – which means there are two required syscalls per new connection socket instead of one for poll. If your server has many short-lived connections which send or receive little traffic, epoll will likely take longer than poll to serve them.

§ epoll is exclusively Linux domain, and while other platforms have similar mechanisms, they are not exactly the same – edge triggering, for example, is pretty unique (FreeBSD’s kqueue supports it too though).

§ High performance processing logic is more complex and hence more difficult to debug, especially for edge triggering which is prone to deadlocks if you miss extra read/write.

Therefore you should only use epoll if all following is true:

§ Your application runs a thread poll which handles many network connections by a handful of threads. You would lose most of epoll benefits in a single-threaded application, and most likely it won’t outperform poll.

§ You expect to have a reasonably large number of sockets to monitor (at least 1,000); with a smaller number epoll is not likely to have any performance benefits over poll and may actually worse the performance;

§ Your connections are relatively long-lived; as stated above epoll will be slower than poll in a situation when a new connection sends a few bytes of data and immediately disconnects because of extra system call required to add the descriptor into epoll set;

§ Your app depends on other Linux-specific features (so in case portability question would suddenly pop up, epoll wouldn’t be the only roadblock), or you can provide wrappers for other supported systems. In the last case you should strongly consider libevent.

If all the items above aren’t true, you should be better served by using poll instead.

Polling with libevent

libebent is a library which wraps the polling methods listed in this article (and some others) in an uniform API.Its main advantage is that it allows you to write the code once and compile and run it on many operating systems without the need to change the code. It is important to understand that libevent it is just a wrapper built on top of the existing polling methods, and therefore it inherits the issues those polling methods have. It will not makeselect supporting more than 1024 sockets on Linux or allow epoll to modify the polling events without a syscall/context switch. Therefore it is still important to understand each method’s pros and cons.

Having to provide access to the functionality from the dramatically different methods, libevent has a rather complex API which is much more difficult to use than poll or even epoll. It is however easier to use libevent than to write two separate backends if you need to support FreeBSD (epoll and kqueue). Hence it is a viable alternative which should be considered if:

§ Your application requirements indicate that you must use epoll, and using just poll would not be enough (if pollwould satisfy your needs, it is extremely unlikely libevent would offer you any benefits)

§ You need to support other OS than Linux, or may expect such need to arise in future. Again, this depends on other features of your application – if it is tied up to many other Linux-specific things you’re not going to achieve anything by using libevent instead of epoll

 

<think>嗯,用户的问题是关于read系统调用和select/poll/epoll之间的区别。我需要先理清楚每个系统调用的基本功能和用途,然后找出它们的区别在哪里。首先,read是一个用于从文件描述符读取数据的系统调用。它通常是阻塞的,也就是说,如果数据没有准备好,进程会一直等待直到有数据可读。这可能在高并发场景下导致性能问题,因为每个read调用都可能阻塞一个线程,需要多线程或多进程来处理多个连接,这样资源消耗会很大。接下来是selectpollepoll,这些都属于I/O多路复用机制。它们的作用是允许一个进程同时监视多个文件描述符,看看哪些已经准备好进行I/O操作。这样,单个线程就可以管理多个连接,提高了效率。用户提到的引用里提到,这些机制都是同步I/O,因为它们需要应用自己处理读写,而异步I/O则是由内核负责数据拷贝的。所以,select/poll/epoll属于同步但非阻塞的模式? 需要比较它们的区别。首先,read是直接进行I/O操作,而多路复用机制是用于检测I/O就绪状态。也就是说,当使用select等函数时,一旦检测到某个fd就绪,应用程序才会调用read,这样read就不会阻塞,因为已经有数据准备好了。这可以减少阻塞时间,提高吞吐量。用户提供的引用中提到,selectpoll需要遍历所有fd来检查状态,而epoll通过事件驱动的方式,只通知就绪的fd。这可能涉及到效率问题,尤其是在大量连接的情况下,epoll的效率更高。比如引用[1]和[2]提到,selectpoll需要无差别轮询,而epoll会通知具体的事件,这样应用程序不需要遍历所有fd,节省了CPU资源。 另外,关于同步和异步的问题,引用[3]指出,select/poll/epoll都是同步I/O,因为实际的读写操作还是由应用程序自己完成的,并且这个过程是阻塞的。而异步I/O则是内核完成读写后通知应用,不需要应用自己处理。所以,虽然多路复用机制提高了检测就绪状态的效率,但实际的read/write仍然是同步的。需要把read和这些多路复用机制结合起来解释。比如,使用epoll时,程序先用epoll_wait等待事件,当某个fd就绪后,再调用read,这时read会立即返回数据,不会阻塞。这样,结合非阻塞的fd,可以实现高效的I/O处理。 另外,还要考虑使用场景。比如,read适用于简单的单连接情况,而多路复用适用于高并发的网络服务器,如Web服务器、数据库等,需要同时处理成千上万的连接。这时候使用多路复用机制可以显著减少线程或进程的数量,降低资源消耗。 可能用户的问题背后是想了解如何优化I/O性能,或者理解阻塞非阻塞、同步异步的区别。需要确保回答中涵盖这些概念,并结合具体的系统调用进行说明。总结一下,区别主要在以下几个方面:1.功能目的:read直接读取数据,而多路复用用于监控多个fd的就绪状态。 2.阻塞行为:read可能阻塞在数据等待,而多路复用阻塞在检测就绪状态。 3.效率:多路复用尤其是epoll在处理大量连接时更高效。4.使用模式:多路复用通常非阻塞I/O结合,减少线程资源消耗。 需要确保回答结构清晰,分点说明,并引用提供的资料来支持观点。比如引用[3]中的同步I/O解释,引用[2]的事件驱动机制等。</think>### read系统调用select/poll/epoll区别分析 #### 1. **功能定位** - **`read`系统调用**:直接用于从文件描述符(如文件、套接字)读取数据。若数据未就绪,默认会**阻塞**当前线程,直到数据到达或超时[^3]。 - **`select/poll/epoll`**:属于**I/O多路复用**机制,用于**监控多个文件描述符的状态**(如可读、可写、异常),并通知用户哪些描述符已就绪,但**不直接执行读写操作**[^3][^2]。 #### 2. **阻塞行为** - **`read`的阻塞**:若未设置非阻塞模式(O_NONBLOCK),`read`会一直等待数据就绪,导致线程无法处理其他任务。 - **多路复用的阻塞**:`select/poll/epoll`的阻塞发生在**等待描述符就绪阶段**,一旦检测到就绪的fd,后续的`read`操作可立即执行,避免无效等待[^3][^2]。 #### 3. **效率性能** - **`read`单线程模式**:需为每个fd创建独立线程/进程,资源消耗大,适用于低并发场景。 - **多路复用模式**: - **`select/poll`**:需遍历所有被监控的fd,时间复杂度为$O(n)$,性能随fd数量增加而下降[^1][^4]。 - **`epoll`**:基于事件驱动,仅关注活跃的fd,时间复杂度为$O(1)$,适合高并发场景(如万级连接)[^2][^3]。 #### 4. **使用模式** - **`read`直接操作数据流**: ```c char buffer[1024]; int bytes = read(fd, buffer, sizeof(buffer)); // 可能阻塞 ``` - **多路复用配合`read`**: ```c // 使用epoll_wait检测就绪fd int n = epoll_wait(epfd, events, MAX_EVENTS, timeout); for (int i = 0; i < n; i++) { if (events[i].events & EPOLLIN) { read(events[i].data.fd, buffer, sizeof(buffer)); // 非阻塞读取 } } ``` #### 5. **同步异步特性** - 两者均为**同步I/O**:`read`和多路复用均需用户程序自行处理数据读写,且`read`操作本身是阻塞的(即使fd已就绪)[^3]。 - **异步I/O对比**:异步I/O(如Linux的`io_submit`)由内核完成数据拷贝后通知用户,无需主动调用`read`[^3]。 --- ### 总结对比表 | 特性 | `read` | `select/poll` | `epoll` | |--------------------|-------------------------|------------------------|------------------------| | **核心功能** | 读取数据 | 监控多个fd状态 | 事件驱动监控 | | **时间复杂度** | 无监控阶段 | $O(n)$ | $O(1)$ | | **适用场景** | 单连接/简单I/O | 低并发(数百连接) | 高并发(万级连接) | | **资源消耗** | 需多线程/进程 | 全量遍历fd | 仅处理活跃fd | | **内核支持** | 所有系统 | 所有系统 | Linux特有 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值