UNIX网络编程——select函数的并发限制和 poll 函数应用举例

本文探讨了在UNIX网络编程中,select函数的并发限制主要受到FD_SETSIZE(通常为1024)的限制,可以通过修改内核配置扩大限制。实验显示服务器端最多接受1020个连接。相比之下,poll函数的并发数量受限于系统允许的最大文件描述符数,通过ulimit命令可以提升到2048,服务器端成功接收了2044个连接,展示了更高的并发能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、用select实现的并发服务器,能达到的并发数,受两方面限制


       1、一个进程能打开的最大文件描述符限制。这可以通过调整内核参数。可以通过ulimit -n来调整或者使用setrlimit函数设置, 但一个系统所能打开的最大数也是有限的,跟内存大小有关,可以通过cat /proc/sys/fs/file-max 查看

       2、select中的fd_set集合容量的限制(FD_SETSIZE,一般为1024) ,这需要重新编译内核。


可以写个测试程序,只建立连接,看看最多能够建立多少个连接,客户端程序如下:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)


int main(void)
{
    int count = 0;
    while(1)
    {
        int sock;
        if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        {
            sleep(4);
            ERR_EXIT("socket");
        }

        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

        if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("connect");

        struct sockaddr_in localaddr;
        socklen_t addrlen = sizeof(localaddr);
        if (getsockname(sock, (struct sockaddr *)&localaddr, &addrlen) < 0)
            ERR_EXIT("getsockname");

        printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
        printf("count = %d\n", ++count);

    }

    return 0;
}
服务器的代码serv.c

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>



#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)


int main(void)
{
    
    signal(SIGPIPE, SIG_IGN);
    int listenfd; //被动套接字(文件描述符),即只可以accept, 监听套接字
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
//  listenfd = socket(AF_INET, SOCK_STREAM, 0)  
        ERR_EXIT("socket error");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 
    /* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
    /* inet_aton("127.0.0.1", &servaddr.sin_addr); */
    
    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt error");

    if (bind(listenfd, (struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
        ERR_EXIT("bind error");

    if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前
        ERR_EXIT("listen error");
    
    struct sockaddr_in peeraddr; //传出参数
    socklen_t peerlen = sizeof(peeraddr); //传入传出参数,必须有初始值
    
    int conn; // 已连接套接字(变为主动套接字,即可以主动connect)
    int i;
    int client[FD_SETSIZE];
    int maxi = 0; // client数组中最大不空闲位置的下标
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;

    int nready;
    int maxfd = listenfd;
    fd_set rset;
    fd_set allset;
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

	int count = 0;
    while (1) {
        rset = allset;
        nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
        if (nready == -1) {
            if (errno == EINTR)
                continue;
            ERR_EXIT("select error");
        }

        if (nready == 0)
            continue;

        if (FD_ISSET(listenfd, &rset)) {
        
            conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);  //accept不再阻塞
            if (conn == -1)
                ERR_EXIT("accept error");
            printf("count = %d\n", ++count);
            for (i = 0; i < FD_SETSIZE; i++) {
                if (client[i] < 0) {
                    client[i] = conn;
                    if (i > maxi)
                        maxi = i;
                    break;
                } 
            }
            
            if (i == FD_SETSIZE) {
                fprintf(stderr, "too many clients\n");
                exit(EXIT_FAILURE);
            }

            printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
                ntohs(peeraddr.sin_port));

            FD_SET(conn, &allset);
            if (conn > maxfd)
                maxfd = conn;

            if (--nready <= 0)
                continue;
        }

        for (i = 0; i <= maxi; i++) {
            conn = client[i];
            if (conn == -1)
                continue;

            if (FD_ISSET(conn, &rset)) {
                
                char recvbuf[1024] = {0};
                int ret = read(conn, recvbuf, 1024);
                if (ret == -1)
                    ERR_EXIT("read error");
                else if (ret  == 0) { //客户端关闭 
                    printf("client close \n");
                    FD_CLR(conn, &allset);
                    client[i] = -1;
                    close(conn);
                }
        
                fputs(recvbuf, stdout);
                write(conn, recvbuf, strlen(recvbuf));
                
                if (--nready <= 0)
                    break; 
            }
        }


    }
        
    return 0;
}

/* select所能承受的最大并发数受
 * 1.一个进程所能打开的最大文件描述符数,可以通过ulimit -n来调整
 *   但一个系统所能打开的最大数也是有限的,跟内存有关,可以通过cat /proc/sys/fs/file-max 查看
 * 2.FD_SETSIZE(fd_set)的限制,这个需要重新编译内核                                                                          
 */


huangcheng@ubuntu:~$ ./serv  
count = 1  
recv connect ip=127.0.0.1 port=48370  
count = 2  
recv connect ip=127.0.0.1 port=48371  
count = 3  
recv connect ip=127.0.0.1 port=48372  
count = 4  
recv connect ip=127.0.0.1 port=48373  
....................................  
recv connect ip=127.0.0.1 port=49389  
count = 1020  
recv connect ip=127.0.0.1 port=49390  



accept error: Too many open files  
[cpp] view plaincopyprint?
huangcheng@ubuntu:~$ ./cli  
ip=127.0.0.1 port=46327  
count = 1  
ip=127.0.0.1 port=46328  
count = 2  
ip=127.0.0.1 port=46329  
count = 3  
ip=127.0.0.1 port=46330  
count = 4  
ip=127.0.0.1 port=46331  
count = 5  
ip=127.0.0.1 port=46332  
count = 6  
ip=127.0.0.1 port=46333  
.......................  
ip=127.0.0.1 port=47345  
count = 1020  
ip=127.0.0.1 port=47346  
count = 1021  
socket: Too many open files  


输出太多条目,上面只截取最后几条,从中可以看出对于客户端,最多只能开启1021个连接套接字,因为总共是1024个,还得除去0、1、2。而服务器端只能accept 返回1020个已连接套接字,因为除了0、1、2之外还有一个监听套接字,客户端某一个套接字(不一定是最后一个)虽然已经建立了连接,在已完成连接队列中,但accept 返回时达到最大描述符限制,返回错误,打印提示信息。


       也许有人会注意到上面有一行 sleep(4);当客户端调用socket准备创建第1022个套接字时,如上所示也会提示错误,此时socket函数返回-1出错,如果没有睡眠4s后再退出进程会有什么问题呢?如果直接退出进程,会将客户端所打开的所有套接字关闭掉,即向服务器端发送了很多FIN段,而此时也许服务器端还一直在accept ,即还在从已连接队列中返回已连接套接字,此时服务器端除了关心监听套接字的可读事件,也开始关心前面已建立连接的套接字的可读事件,read 返回0,所以会有很多 client close 字段 参杂在条目的输出中,还有个问题就是,因为read 返回0,服务器端会将自身的已连接套接字关闭掉,那么也许刚才说的客户端某一个连接会被accept 返回,即测试不出服务器端真正的并发容量。

  1. huangcheng@ubuntu:~$ ./serv  
  2. count = 1  
  3. recv connect ip=127.0.0.1 port=50413  
  4. count = 2  
  5. ....................................  
  6. client close  
  7. client close  
  8. client close  
  9. client close  
  10. ...................................  
  11. recv connect ip=127.0.0.1 port=51433  
  12. client close  
  13. count = 1021  
  14. recv connect ip=127.0.0.1 port=51364  
  15. client close  
  16. client close  
      可以看到输出参杂着client close,且这次的count 达到了1021,原因就是服务器端前面已经有些套接字关闭了,所以accept 创建套接字不会出错,服务器进程也不会因为出错而退出,可以看到最后接收到的一个连接端口是51364,即不一定是客户端的最后一个连接。

二、poll 函数应用举例


  1. #include <poll.h>  
  2. int poll(struct pollfd *fds, nfds_t nfds, int timeout);  
参数1:结构体数组指针

  1. struct pollfd {  
  2.     int   fd;         /* file descriptor */  
  3.     short events;     /* requested events */  
  4.     short revents;    /* returned events */  
  5. };  

结构体中的fd 即套接字描述符,events 即感兴趣的事件,如下图所示,revents 即返回的事件。


参数2:结构体数组的成员个数,即文件描述符个数。

参数3:即超时时间,若为-1,表示永不超时。


       poll 跟 select 还是很相似的,比较重要的区别在于poll 所能并发的个数跟FD_SETSIZE无关,只跟一个进程所能打开的文件描述符个数有关,可以在select 程序的基础上修改成poll 程序,在运行服务器端程序之前,使用ulimit -n 2048 将限制改成2048个,注意在运行客户端进程的终端也需更改,因为客户端也会有所限制,这只是临时性的更改,因为子进程会继承这个环境参数,而我们是在bash命令行启动程序的,故在进程运行期间,文件描述符的限制为2048个。

使用poll 函数的服务器端程序如下:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>
#include<poll.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)


int main(void)
{
    int count = 0;
    signal(SIGPIPE, SIG_IGN);
    int listenfd; //被动套接字(文件描述符),即只可以accept, 监听套接字
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        //  listenfd = socket(AF_INET, SOCK_STREAM, 0)
        ERR_EXIT("socket error");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
    /* inet_aton("127.0.0.1", &servaddr.sin_addr); */

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt error");

    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind error");

    if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前
        ERR_EXIT("listen error");

    struct sockaddr_in peeraddr; //传出参数
    socklen_t peerlen = sizeof(peeraddr); //传入传出参数,必须有初始值

    int conn; // 已连接套接字(变为主动套接字,即可以主动connect)
    int i;

    struct pollfd client[2048];
    int maxi = 0; //client[i]最大不空闲位置的下标

    for (i = 0; i < 2048; i++)
        client[i].fd = -1;

    int nready;
    client[0].fd = listenfd;
    client[0].events = POLLIN;

    while (1)
    {
        /* poll检测[0, maxi + 1) */
        nready = poll(client, maxi + 1, -1);
        if (nready == -1)
        {
            if (errno == EINTR)
                continue;
            ERR_EXIT("poll error");
        }

        if (nready == 0)
            continue;

        if (client[0].revents & POLLIN)
        {

            conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen); //accept不再阻塞
            if (conn == -1)
                ERR_EXIT("accept error");

            for (i = 1; i < 2048; i++)
            {
                if (client[i].fd < 0)
                {
                    client[i].fd = conn;
                    if (i > maxi)
                        maxi = i;
                    break;
                }
            }

            if (i == 2048)
            {
                fprintf(stderr, "too many clients\n");
                exit(EXIT_FAILURE);
            }

            printf("count = %d\n", ++count);
            printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
                   ntohs(peeraddr.sin_port));

            client[i].events = POLLIN;

            if (--nready <= 0)
                continue;
        }

        for (i = 1; i <= maxi; i++)
        {
            conn = client[i].fd;
            if (conn == -1)
                continue;
            if (client[i].revents & POLLIN)
            {

                char recvbuf[1024] = {0};
                int ret = read(conn, recvbuf, 1024);
                if (ret == -1)
                    ERR_EXIT("readline error");
                else if (ret  == 0)   //客户端关闭
                {
                    printf("client  close \n");
                    client[i].fd = -1;
                    close(conn);
                }

                fputs(recvbuf, stdout);
                write(conn, recvbuf, strlen(recvbuf));

                if (--nready <= 0)
                    break;
            }
        }


    }

    return 0;
}

/* poll 只受一个进程所能打开的最大文件描述符限制,这个可以使用ulimit -n调整 */

参照前面对 select 函数 的解释不难理解上面的程序,就不再赘述了。来看一下输出:


  1. root@ubuntu:/home/huangcheng# ulimit -n 2048  
  2. root@ubuntu:/home/huangcheng# su - huangcheng  
  3. huangcheng@ubuntu:~$ ulimit -n  
  4. 2048  
  5. huangcheng@ubuntu:~$ ./serv  
  6. ...........................  
  7. count = 2042  
  8. recv connect ip=127.0.0.1 port=54499  
  9. count = 2043  
  10. recv connect ip=127.0.0.1 port=54500  
  11. count = 2044  
  12. recv connect ip=127.0.0.1 port=54501  
  13. accept error: Too many open files  
  1. root@ubuntu:/home/huangcheng# ulimit -n 2048  
  2. root@ubuntu:/home/huangcheng# su - huangcheng  
  3. huangcheng@ubuntu:~$ ulimit -n  
  4. 2048  
  5. huangcheng@ubuntu:~$./cli  
  6. ..........................  
  7. ip=127.0.0.1 port=54499  
  8. count = 2043  
  9. ip=127.0.0.1 port=54500  
  10. count = 2044  
  11. ip=127.0.0.1 port=54501  
  12. count = 2045  
  13. socket: Too many open files  
       可以看到现在最大的连接数已经是2045个了,虽然服务器端有某个连接没有accept 返回。即poll 比 select 能够承受更多的并发连接,只受一个进程所能打开的最大文件描述符个数限制。可以通过ulimit -n  修改,但一个系统所能打开的文件描述符个数也是有限的,这跟系统的内存大小有关系,所以说也不是可以无限地并 发,可以查看一下本机的容量:

  1. huangcheng@ubuntu:~$ cat /proc/sys/fs/file-max  
  2. 101598  
本机是虚拟机,内存2G,能够打开的文件描述符个数大约在10w个左右。





<think>好的,我现在需要回答用户关于Unix环境下阻塞IO与非阻塞IO的区别及用法的问题。首先,我要回顾一下用户提供的参考资料,确保我的回答准确。 根据引用[1],同步IO异步IO的区别在于数据从内核拷贝到用户空间时是否阻塞进程,而阻塞IO与非阻塞IO的区别在于发起IO请求时是否阻塞。引用[4]提到在网络编程中,阻塞IO非阻塞IO的概念涉及socket的处理方式。 首先,我需要明确两者的基本区别。阻塞IO在数据未就绪时会一直等待,线程被挂起,直到操作完成。而非阻塞IO会立即返回一个错误码,如EAGAIN或EWOULDBLOCK,线程可以继续执行其他任务,需要轮询检查状态。 接下来是实现的区别。用户可能需要知道如何设置非阻塞模式,比如使用fcntl函数设置O_NONBLOCK标志,或者使用selectpoll、epoll等IO多路复用技术来管理多个非阻塞socket。这部分需要结合Unix系统调用来解释。 然后是关于使用场景。阻塞IO适合简单的应用程序,逻辑简单但并发能力差。非阻塞IO适合高并发场景,如网络服务器,但代码复杂度高。这里可以举例说明,比如Web服务器如何处理多个连接。 还需要注意两者的性能差异。非阻塞IO通过避免线程阻塞,提高了资源利用率,但需要更多的系统调用复杂的错误处理。而阻塞IO虽然效率低,但编程模型简单。 最后,用户可能需要知道如何在实际中实现这两种模式。比如,在socket编程中,创建socket时默认是阻塞的,如何更改为非阻塞,并配合多路复用技术使用。同时,要提到常见的函数如fcntl、select等。 检查是否有遗漏点,比如是否需要提到异步IO(但用户主要问阻塞非阻塞)。不过引用[1]提到了同步异步的区别,可以适当关联,但重点在阻塞与非阻塞。 总结结构:先区别定义,再实现方式,然后使用场景,最后注意事项。确保引用正确,比如引用[1][4]中的内容,自然地插入引用标识。同时避免使用Markdown格式,保持自然的中文口语化。</think>### Unix 环境下阻塞 IO 与非阻塞 IO 的区别及实现方式 #### 一、核心区别 1. **阻塞 IO** - **定义**:当进程发起 IO 操作时,若数据未就绪(例如网络数据未到达或磁盘未响应),进程会被挂起(阻塞),直到数据就绪并完成拷贝到用户空间后才会继续执行[^1][^4]。 - **特点**:编程模型简单,但会占用线程资源,导致并发能力受限。 2. **非阻塞 IO** - **定义**:进程发起 IO 操作时,若数据未就绪,内核立即返回错误码(如 `EAGAIN` 或 `EWOULDBLOCK`),进程可继续执行其他任务,需通过轮询或事件通知机制主动检查数据状态[^1][^4]。 - **特点**:提高了资源利用率,但需要更复杂的逻辑处理(如重试、多路复用)。 #### 二、实现方式 1. **阻塞 IO 的实现** - **默认模式**:Unix 中大多数文件描述符(如 socket)默认是阻塞模式。 - **示例代码**(TCP 读取): ```c char buffer[1024]; ssize_t n = read(socket_fd, buffer, sizeof(buffer)); // 阻塞直到数据到达或出错 ``` 2. **非阻塞 IO 的实现** - **设置非阻塞标志**:通过 `fcntl` 函数为文件描述符添加 `O_NONBLOCK` 标志。 ```c int flags = fcntl(socket_fd, F_GETFL, 0); fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK); ``` - **配合多路复用技术**:使用 `select`、`poll` 或 `epoll` 监听多个非阻塞描述符的就绪状态。 ```c fd_set read_fds; FD_ZERO(&read_fds); FD_SET(socket_fd, &read_fds); select(socket_fd + 1, &read_fds, NULL, NULL, NULL); // 等待数据就绪 ``` #### 三、使用场景 - **阻塞 IO**:适合简单、低并发的场景(如命令行工具)。 - **非阻塞 IO**:适合高并发服务(如 Web 服务器、实时通信系统),通过多路复用技术(如 `epoll`)管理海量连接。 #### 四、注意事项 - **性能权衡**:非阻塞 IO 减少了线程阻塞时间,但增加了系统调用次数(如轮询)代码复杂度。 - **错误处理**:非阻塞 IO 需处理 `EAGAIN` 等错误,并设计重试逻辑。 - **与异步 IO 的区别**:非阻塞 IO 仍是同步操作(需主动检查状态),而异步 IO(如 `aio_read`)由内核通知完成,无需轮询[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值