select / poll 非阻塞原理

 应用程序通过调用 select / poll 函数,可以实现非阻塞编程,以下举例:

int main(int argc, char *argv[])
{
    int fd;
    int ret = 0;
    char *filename;
    struct pollfd fds;
    fd_set readfds;
    struct timeval timeout;
    unsigned char data;

    if (argc != 2) {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];
    fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞访问 */
    if (fd < 0) {
        printf("Can't open file %s\r\n", filename);
        return -1;
    }

#if 0
    /* 构造结构体 */
    fds.fd = fd;
    fds.events = POLLIN;

    while (1) {
        ret = poll(&fds, 1, 500);
        if (ret) { /* 数据有效 */
       if ( fds.revents & POLLIN ) {
  ret = read(fd, &data, sizeof(data));
       }   
if(ret < 0) {   /* 读取错误 */   } else {   if(data)   printf("key value = %d \r\n", data);   } } else if (ret == 0) { /* 超时 */ /* 用户自定义超时处理 */ } else if (ret < 0) { /* 错误 */ /* 用户自定义错误处理 */ } } #endif while (1) { FD_ZERO(&readfds); FD_SET(fd, &readfds); /* 构造超时时间 */ timeout.tv_sec = 0; timeout.tv_usec = 500000; /* 500ms */ ret = select(fd + 1, &readfds, NULL, NULL, &timeout); switch (ret) { case 0: /* 超时 */ /* 用户自定义超时处理 */ break; case -1: /* 错误 */ /* 用户自定义错误处理 */ break; default: /* 可以读取数据 */ if(FD_ISSET(fd, &readfds)) { ret = read(fd, &data, sizeof(data)); if (ret < 0) { /* 读取错误 */ } else { if (data) printf("key value=%d\r\n", data); } } break; } } close(fd); return ret; }

select() 使用的是 fd_set 类型变量记录文件句柄,从下面内核代码可知,最多只能记录1024个

typedef __kernel_fd_set        fd_set;

#define __FD_SETSIZE    1024

typedef struct {
    unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

 

 poll() 传入的是函数指针,存放文件句柄的数组位于应用程序,所以没有数量限制

 

POLL() 机制的内核代码详解

Linux APP系统调用,基本都可以在它的名字前加上“sys_”前缀,这就是它在内核中对应的函数。比如系统调用open、read、write、poll,与之对应的内核函数为:sys_open、sys_read、sys_write、sys_poll。 对于系统调用poll或select,它们对应的内核函数都是sys_poll。分析sys_poll,即可理解poll机制。

sys_poll函数

SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds,
        int, timeout_msecs)
{
    struct timespec64 end_time, *to = NULL;
    int ret;

    if (timeout_msecs >= 0) {
        to = &end_time;
        poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC,
            NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
    }

    ret = do_sys_poll(ufds, nfds, to);
……

SYSCALL_DEFINE3是一个宏,它定义于include/linux/syscalls.h,展开后就是sys_poll函数。

do_sys_poll函数

int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
        struct timespec64 *end_time)
{
……
    poll_initwait(&table);
    fdcount = do_poll(head, &table, end_time);
    poll_freewait(&table);
……
}

 poll_initwait() 中把当前进程变量存在变量 pwq 内,后面会把当前进程变量作为等待队列条目的一部分加入等待队列中,用于唤醒进程。

变量 pwq 记录了 __pollwait 函数指针,此函数最后会在驱动程序的 poll() ->poll_wait() 内被执行

do_poll函数

这是POLL机制中最核心的代码

① 从这里开始,将会导致驱动程序的poll函数被第一次调用。

 沿着②③④⑤,你可以看到:驱动程序里的 poll_wait()--->__pollwait()--->add_wait_queue(),把带有当前进程变量的等待队列条目加入到驱动程序创建的等待队列中。

当调用 wake_up_interruptible(等待队列),会执行等待队列的等待队列条目的 func() 函数,即 pollwake()--->__pollwake()--->default_wake_function()--->try_to_wake_up(),从而唤醒进程。
等待队列条目对应的结构体如下:

当执行完①之后,在⑥或⑦处,pt->_qproc被设置为NULL,所以第二次调用驱动程序的poll时,不会再次把当前进程放入等待队列里。

⑧ 如果驱动程序的poll返回有效值,则count非0,跳出循环;

⑨ 否则休眠一段时间;当休眠时间到,或是被中断唤醒时,会再次循环、再次调用驱动程序的poll。 

相关函数

等待事件

/* 
*    conditon:必须满足,否则阻塞
*    timeout和conditon相比,有更高优先级
*/
wait_event(wq, condition);
wait_event_timeout(wq, condition, timeout);
wait_event_interruptible(wq, condition) ;
wait_event_interruptible_timeout(wq, condition, timeout) ;

唤醒等待队列

//可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)

//只能唤醒处于TASK_INTERRUPTIBLE状态的进程
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

 

等待队列初始化

void init_waitqueue_head(wait_queue_head_t *q)

 

等待队列创建并初始化

DECLARE_WAITQUEUE(name, tsk)

 

等待队列 加入/删除 等待队列条目

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

void remove_wait_queue(wait_queue_head_t *q, wait_queue_t wait)

 

posted @ 2023-02-19 22:39  流水灯  阅读(137)  评论(0编辑  收藏  举报