Nio相关的系统调用

本文介绍了Linux系统调用在Nio中的应用,包括select、poll和epoll三种方式。select系统调用受限于1024个文件描述符,存在位图拷贝和O(n)遍历的问题;poll虽然解决了最大连接数限制,但仍存在遍历开销;epoll通过用户态和内核态共享fd集合,降低了拷贝开销,实现了O(1)复杂度的效率提升。

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

首先,每个客户端连接在Linux系统下,都有一个文件描述符fd与之对应,文件描述符有一个编号,不同的编号表示不同的连接。

1、select系统调用

select系统调用有一个重要参数,为fd文件描述符集合,即你要监听哪些文件描述符(哪些连接),这个文件描述符集合rset用一个bitmap位图表示,位图大小为1024,即最多只能监听1024个客户端连接。

当发起系统调用时,会将rset拷贝到内核态,然后内核态监听有没有数据可以处理,监听的所有文件描述符都没有数据的话会一直阻塞,直到有数据时,将有数据的fd索引置一,然后返回给用户态

Select缺点:

  • 位图大小默认1024,有上限。

  • 每次都需要创建一个文件描述符位图并拷贝到内核态。

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
​
  1. nfds:要检测的文件描述符数量,最大文件描述符加1。

  2. readfds:指定了被读监控的文件描述符集;

  3. writefds:指定了被写监控的文件描述符集;

  4. exceptfds:指定了被例外条件监控的文件描述符集;

  5. timeout:超时时间。

readfds是个长度为1024的bitmap。我们都知道fd文件描述符有一个序号,

如果现在我监听3,6,8号的fd,那么位图就是:

...10100100

那么select的具体流程是什么呢?

1、应用程序创建socket,生成文件描述符,并生成bitmap,使用hash的方式将bitmap的对应位置置一。

2、执行系统调用,将bitmap拷贝至内核空间,根据bitmap遍历对应的文件描述符,一旦有事件产生就返回。

3、用户程序遍历文件描述符,处理请求。

4、应用程序不停的调用select即可。

select模型已经很不错了,但是依然有不足的地方:

  1. bitmap位图上限是1024,所以能监控的fd最多也就这么多。

  2. fset位图不可重用,每次赋值全部清零,状态全部丢失。

  3. fset位图需要不断的进行用户空间到内核空间的拷贝。

  4. 每次查找时间复杂度都是O(n)。

说句实话,如果没有更好的选择方案,这都不是问题。

2、Poll系统调用

Poll工作原理与Select基本相同,不同的只是将位图数组改成数组,也有资料说是链表,没有了最大连接数1024的限制,依然有fd集合的拷贝和O(n)的遍历过程。

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

这个系统调用的

  1. fds:存放需要被检测状态的套接字描述符,与select不同(select在调用之后会清空这个数组),每当调用这个数组,系统不会清空这个数组,而是存放revents状态变化描述符变量,这样才做起来很方便。

  2. nfds:用于标记数组fd中struct pollfd结构元素的总数量。

  3. timeout:是超时时间。

  4. 返回值大于零表示成功,返回满足条件的文件描述符的个数

    返回值等于零,表示超时。

    返回值等于-1 发生错误,比如描述符不合法,接受到中断信号,内存不足

被检测的套接字使用结构体封装,如下:

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};
​

pollfd

  1. fd 文件描述符

  2. events 请求的事件

  3. revents 返回的事件

事件的类型比如:

  1. pollin表示文件有数据来、文件描述符可读

  2. pollout表示文件可写

  3. pollerr表示错误发生

poll的优势:

1、大量的 fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

2、 可重用

3、Epoll系统调用

为解决fd集合拷贝的问题,epoll采用用户态和内核态共享epoll_fds集合。当调用epoll_wait系统调用时,内核态会去检查有哪些fd有事件,检查完毕后会将共享的epoll_fds集合重排序,将有事件的fd放在前面,并返回有事件的fd个数。

客户端收到返回的个数,就不需要全部遍历,而是直接处理fd。

1、int epoll_create(int size);
​
#注意:size参数只是告诉内核这个 epoll对象会处理的事件大致数目,而不是能够处理的事件的最大个数。在 Linux最新的一些内核版本的实现中,#这个 size参数没有任何意义。
​
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
​
#epoll的事件注册函数,epoll_ctl向 epoll对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返回–1,此时需要根据errno错误码##判断错误类型。
​
#它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
​
​
3、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
​
epoll_wait方法返回的事件必然是通过 epoll_ctl添加到 epoll中的。
#第一个参数是epoll_create()的返回值,
#第二个参数表示动作,用三个宏来表示:
#EPOLL_CTL_ADD:注册新的fd到epfd中;
#EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
#EPOLL_CTL_DEL:从epfd中删除一个fd;
#第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事
 

处理流程大致如下:

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
​
/* Code to set up listening socket, 'listen_sock',
              (socket(), bind(), listen()) omitted */

epollfd = epoll_create1(0);
if (epollfd == -1) {
    perror("epoll_create1");
    exit(EXIT_FAILURE);
}
​
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
​

if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
}
​
for (;;) {
    nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        perror("epoll_wait");
        exit(EXIT_FAILURE);
    }
​
    for (n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {
            conn_sock = accept(listen_sock,
                               (struct sockaddr *) &addr, &addrlen);
            if (conn_sock == -1) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            setnonblocking(conn_sock);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                          &ev) == -1) {
                perror("epoll_ctl: conn_sock");
                exit(EXIT_FAILURE);
            }
        } else {
            do_use_fd(events[n].data.fd);
        }
    }
}
 
​
​
  1. 重排相当于置位,每次会把有事件发生的fd排在前边

  2. 没有靠背开销,共享内存。

  3. o(1)复杂度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值