一个文章让你彻底弄懂poll(晓lin的linux百炼之旅day1更新)

poll 函数的底层机制和用法详解

为了帮助你更好地理解 poll 函数,我们将从其底层机制开始,逐步深入到高级用法。以下是详细的解析:


1. 什么是 poll

poll 是一个系统调用,用于监视多个文件描述符(file descriptors),以确定它们是否已准备好进行 I/O 操作(如读、写)。它提供了一种非阻塞的方式,可以同时处理多个 I/O 操作,而不需要为每个操作创建单独的线程或进程。


2. poll 的底层机制
2.1 文件描述符与事件

在 Linux 系统中,所有 I/O 资源(如文件、网络套接字、管道等)都可以通过文件描述符来访问。poll 函数允许你指定一组文件描述符,并告诉内核你关心哪些类型的事件(例如:可读、可写、错误等)。当这些事件发生时,内核会通知应用程序。

2.2 事件驱动模型

poll 使用的是事件驱动模型,即应用程序告诉内核它感兴趣的事件类型,然后等待内核的通知。这种方式避免了频繁轮询(busy-waiting),从而提高了效率。

2.3 内核态与用户态切换

当调用 poll 时,程序会进入阻塞状态,直到有事件发生或超时。在此期间,CPU 可以执行其他任务,因为进程处于睡眠状态。一旦内核检测到某个文件描述符上的事件,它会唤醒进程并返回结果。


3. poll 的基本用法
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • 参数说明

    • struct pollfd *fds:指向一个 pollfd 结构体数组,每个结构体包含一个文件描述符及其感兴趣的事件。
    • nfds_t nfds:数组中的元素数量。
    • int timeout:等待的时间(毫秒),如果设置为 -1 则无限期等待,0 表示立即返回。
  • 返回值

    • 如果有事件发生,返回就绪的文件描述符数量。
    • 如果超时,返回 0
    • 如果出错,返回 -1
3.1 pollfd 结构体
struct pollfd {
    int fd;         // 文件描述符
    short events;   // 关心的事件类型
    short revents;  // 实际发生的事件类型
};
  • fd:要监视的文件描述符。
  • events:指定你关心的事件(如 POLLIN 表示可读,POLLOUT 表示可写)。
  • revents:内核返回的实际发生的事件。

4. 常见的事件类型
  • POLLIN:文件描述符可读。
  • POLLPRI:有紧急数据可读(如带外数据)。
  • POLLOUT:文件描述符可写。
  • POLLERR:发生错误。
  • POLLHUP:挂起(通常表示对端关闭连接)。
  • POLLNVAL:无效的文件描述符。

5. poll 的高级用法
5.1 多文件描述符监视

你可以同时监视多个文件描述符。例如,如果你有一个服务器程序需要同时监听多个客户端连接,poll 可以让你在一个循环中处理所有连接,而不需要为每个连接创建单独的线程或进程。

struct pollfd fds[2];
fds[0].fd = socket_fd1;
fds[0].events = POLLIN;

fds[1].fd = socket_fd2;
fds[1].events = POLLIN;

int ret = poll(fds, 2, -1);
5.2 超时控制

poll 允许你设置超时时间,以便在没有事件发生时不会无限期等待。这对于实现定时任务非常有用。

int ret = poll(fds, 1, 5000);  // 等待 5 秒
if (ret == 0) {
    printf("Timeout occurred\n");
}
5.3 边缘触发 vs. 水平触发

poll 默认使用水平触发(Level Triggered),这意味着只要文件描述符仍然处于就绪状态,poll 就会继续返回该描述符。如果你想使用边缘触发(Edge Triggered),则需要结合其他机制(如 epoll),因为 poll 本身不支持边缘触发。

5.4 非阻塞 I/O

虽然 poll 本身是阻塞的,但你可以将文件描述符设置为非阻塞模式(如 O_NONBLOCK),并在 poll 返回后尝试读取或写入数据。如果数据不可用,读取或写入操作会立即返回错误(通常是 EAGAINEWOULDBLOCK)。


6. poll 的局限性

尽管 poll 是一个多用途的 I/O 多路复用工具,但它也有一些局限性:

  • 性能问题:对于大量文件描述符,poll 的性能可能会下降,因为它每次调用都需要遍历整个 pollfd 数组。
  • 不支持边缘触发:如前所述,poll 只支持水平触发,这在某些情况下可能不够灵活。

7. 替代方案

对于更高效的 I/O 多路复用,Linux 提供了 epoll,它解决了 poll 的一些性能问题,并且支持边缘触发。如果你的应用程序需要处理大量的文件描述符,建议考虑使用 epoll


总结

poll 是一个强大的系统调用,适用于需要同时监视多个文件描述符的应用程序。它通过事件驱动的方式提高了 I/O 操作的效率,避免了频繁轮询带来的资源浪费。虽然它有一些局限性,但在大多数场景下仍然是一个非常有用的工具。

从源码角度深入解析 poll 的底层机制和实现

为了更深入地理解 poll 的底层机制,我们可以从 Linux 内核的源码出发,逐步分析其工作原理。虽然你没有提供具体的内核源码,但我将基于常见的 Linux 内核版本(如 v5.x)进行解释。

1. 用户空间调用 poll

在用户空间中,poll 函数的调用最终会通过系统调用接口进入内核。我们先来看看用户空间的 poll 系统调用是如何定义的:

#include <poll.h>

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

当用户程序调用 poll 时,它会触发一个系统调用,进入内核模式。

2. 系统调用入口

在 Linux 内核中,poll 系统调用的入口通常是 sys_poll 或类似的函数。这个函数负责处理用户传递的参数,并调用内核中的核心逻辑。

SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds, long, timeout_msecs)
{
    return do_poll(ufds, nfds, timeout_msecs);
}

这里,SYSCALL_DEFINE3 宏用于定义一个带有三个参数的系统调用。do_poll 是实际处理 poll 操作的核心函数。

3. 核心逻辑:do_poll 函数

do_poll 函数是 poll 系统调用的核心部分,它负责设置文件描述符的监视条件,并等待事件的发生。

static long do_poll(struct pollfd __user *ufds, unsigned int nfds, long timeout)
{
    struct poll_wqueues table;
    ktime_t expire, *to = NULL;
    int res;

    if (timeout >= 0) {
        to = &expire;
        *to = ktime_add_ns(timespec64_to_ktime(current_kernel_time64()),
                           (u64)timeout * NSEC_PER_MSEC);
    }

    res = do_pollfd(ufds, nfds, &table, to);

    free_poll_wqueues(&table);
    return res;
}
  • 超时处理:如果设置了超时时间(timeout >= 0),则计算到期时间。
  • 调用 do_pollfd:这是真正执行轮询的地方,后面会详细介绍。
  • 清理资源:释放与轮询相关的资源。
4. 轮询文件描述符:do_pollfd 函数

do_pollfd 函数遍历用户提供的文件描述符数组,并为每个文件描述符设置监视条件。

static int do_pollfd(struct pollfd __user *ufds, unsigned int nfds,
                     struct poll_wqueues *pwait, ktime_t *to)
{
    int fdcount = 0;
    int error;
    int i;

    for (;;) {
        fdcount = 0;
        error = 0;

        for (i = 0; i < nfds; i++) {
            struct pollfd tfd;
            int ready;

            if (__get_user(tfd.fd, &ufds[i].fd) ||
                __get_user(tfd.events, &ufds[i].events)) {
                error = -EFAULT;
                break;
            }

            ready = do_poll_one_fd(tfd.fd, &tfd, pwait);
            if (ready > 0) {
                fdcount++;
                if (__put_user(tfd.revents, &ufds[i].revents))
                    error = -EFAULT;
            }
        }

        if (error || fdcount || signal_pending(current))
            break;

        if (!pwait->cur_queue || !poll_schedule_timeout(pwait, TASK_INTERRUPTIBLE, to))
            break;
    }

    return error ? error : fdcount;
}
  • 获取用户数据:从用户空间读取文件描述符和感兴趣的事件。
  • 调用 do_poll_one_fd:对每个文件描述符进行轮询,检查是否有事件发生。
  • 更新用户空间:如果有事件发生,更新 revents 字段并返回。
  • 等待事件:如果没有事件发生且未超时,则让进程进入睡眠状态,等待事件或超时。
5. 单个文件描述符轮询:do_poll_one_fd 函数

do_poll_one_fd 函数负责检查单个文件描述符是否准备好进行 I/O 操作。

static inline int do_poll_one_fd(int fd, struct pollfd *req, struct poll_wqueues *pwait)
{
    struct file *file;
    wait_queue_entry_t *wait;
    poll_table *pt;
    int mask;

    file = fget(fd);
    if (!file)
        return POLLNVAL;

    pt = poll_get_wait_queue(file, pwait);
    mask = DEFAULT_POLLMASK;
    if (pt) {
        mask = file->f_op->poll(file, req, pt);
        poll_put_wait_queue(pt);
    }

    fput(file);
    return mask;
}
  • 获取文件对象:通过文件描述符获取对应的文件对象。
  • 调用文件操作的 poll 方法:每个文件类型(如套接字、管道等)都有自己的 poll 实现,用于检查文件描述符的状态。
  • 返回结果:返回文件描述符的就绪状态(如可读、可写等)。
6. 文件类型的 poll 实现

不同的文件类型有不同的 poll 实现。例如,对于套接字文件,sock_poll 函数会检查套接字的状态:

unsigned int sock_poll(struct file *file, poll_table *wait)
{
    struct socket *sock = file->private_data;
    unsigned int mask;

    poll_wait(file, sk_sleep(sock->sk), wait);

    mask = 0;
    /* Check for readable data */
    if (skb_peek(&sock->sk->sk_receive_queue) != NULL)
        mask |= POLLIN | POLLRDNORM;

    /* Check for writable data */
    if (sock_writeable(sock))
        mask |= POLLOUT | POLLWRNORM;

    return mask;
}
  • 等待队列:将当前进程加入到文件描述符的等待队列中,以便在有事件发生时唤醒。
  • 检查状态:根据文件描述符的具体类型,检查是否有可读或可写的数据。
7. 事件通知

当某个文件描述符上有事件发生时,内核会唤醒所有在该文件描述符上等待的进程。这通常通过等待队列(wait_queue)来实现。

void wake_up_interruptible_poll(wait_queue_head_t *q, unsigned int mode)
{
    wake_up_state(q, TASK_INTERRUPTIBLE);
}
  • 唤醒进程:当文件描述符的状态发生变化时,调用 wake_up_interruptible_poll 唤醒所有等待的进程。

总结

通过上述步骤,我们可以看到 poll 的底层机制涉及多个层次的协作:

  1. 用户空间调用:用户程序调用 poll,触发系统调用。
  2. 系统调用入口:进入内核模式,调用 sys_poll
  3. 核心逻辑do_poll 设置超时并调用 do_pollfd 遍历文件描述符。
  4. 文件描述符轮询do_poll_one_fd 检查每个文件描述符的状态。
  5. 文件类型特定实现:不同文件类型有不同的 poll 实现,如套接字的 sock_poll
  6. 事件通知:通过等待队列机制,当事件发生时唤醒等待的进程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值