poll、ppoll 浅析
发布日期:2021-07-01 04:36:53 浏览次数:3 分类:技术文章

本文共 11860 字,大约阅读时间需要 39 分钟。

3 poll

3.1 poll 分析与应用

3.1.1 poll函数讲解

       当前对poll(); 函数的讲解参考《UNIX系统编程》P98 poll() 函数。

       poll(); 函数与select(); 函数类似,但是,它是用文件描述符而不是条件的类型来组织信息的。也就是说,一个文件描述符的可能事件都存储在 struct pollfd 结构中。与之相反,select 用事件的类型来组织信息,而且,读、写和出错的情况都有独立的描述符掩码。poll函数是POSIX::XSI扩展的一部分,它起源于UNIX System V

       poll(); 函数有三个按时,格式如下:

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

1 参数 fds --- 是一个struct pollfd[ ] 数组,用来表示文件描述符的监视信息。

2 参数nfds --- 给出了需要监视的描述符的数目。

3 参数 timeout --- 是一个用毫秒表示的时间,是poll在返回前没有接收事件时应该等待的时间。如果timeout的值为 -1poll 就永远都不会超时。如果整数值为32个比特,那么,最大的超时周期大约为30分钟。有如下设置:

timeout的值

解释

INFTIM

永远等待

0

立即返回,不阻塞

> 0

等待指定数目的毫秒数

       如果超时,poll函数返回0,如果成功,poll返回拥有事件的描述符的数目。如果不能够,poll返回 -1 并设置errno。下表列出了设置events监测的信号,以及设置到revents返回的信号。

/* These are specified by iBCS2 */

#define POLLIN       0x0001     //普通或优先级带数据可读

#define POLLPRI      0x0002     //高优先级数据可读

#define POLLOUT      0x0004     //普通数据可写

#define POLLERR      0x0008     //发生错误

#define POLLHUP      0x0010     //发生挂起

#define POLLNVAL  0x0020     //描述字不是一个打开的文件

 

/* The rest seem to be more-or-less nonstandard. Check them! */

#define POLLRDNORM   0x0040     //普通数据可读

#define POLLRDBAND   0x0080     //优先级带数据可读

#define POLLWRNORM   0x0100     //普通数据可写

#define POLLWRBAND   0x0200     //优先级带数据可写

#define POLLMSG      0x0400     //比较少用,查man 说该标记少用

#define POLLREMOVE   0x1000     //查man 找不到该标记说明,但是在网上查到: 在Solaris 7系统上,sun 引入了/dev/poll 设备,该标记就是从/dev/poll 列表中删除对应的fd。

       我们将这些标记分为三个部分:

1 处理输入的4个常值:POLLIN, POLLRDNORM, POLLRDBAND, POLLPRI

2 处理输出的3个常值:POLLOUT, POLLWRNORM, POLLWRBAND

3 处理错误的 3 个常值:POLLERR, POLLHUP, POLLNVAL

       poll识别三个类别的数据:普通(normal),优先级带(priority band)和高优先级(high priority),这些术语均出自基于流的实现。

       POLLIN可以被定义为POLLRDNORMPOLLRDBAND的逻辑或常值。POLLIN存在于SVR3的实现,它要早于SVR4中的优先级带,所以,此常值保持了向后兼容性。类似地,POLLOUT等效于POLLWRNORM,前者早于后者。

第一个参数 struct pollfd 结构的定义如下:

//该结构是应用层poll(); 函数监听文件描述符集合中用到。它存放了一个需要

//监听的文件描述符结构。

struct pollfd {

    int fd;              //需要监听的文件描述符

    short events;     //存放需要监听的事件

    short revents;       //存放产生的事件

};

       fd是文件描述符值,eventsrevents是通过对代表各种事件的标识符进行逻辑或运算构建而成的,上面列举了这些标记,表示相应的事件。设置events来包含要监视的事件,poll用已经发生的事件来填写revents

       poll函数通过revents中设置标识符POLLHUP, POLLERRPOLLNVAL来反映相关条件的存在。不需要在events中对与这些标识符相关的比特位进行设置。

       如果fd小于零,那么events字段被忽略。所以,如果我们要忽略该结构,就设置fd = -1,那么,events字段被忽略,而revents在调用poll(); 函数返回之后,就被设置为0

       标准中没有说明应该如何处理文件结束,文件结束可以通过revents的标识符POLLHUP或者返回0字节的常规读操作来传达。即使POLLINPOLLRDNORM指出还有数据要读,POLLHUP也可能会被设置。因此,应该在错误检验之前处理正常的读操作。

注意:

       select(); 调用对传递给它的文件描述符集进行了修改,所以,每次在调用select(); 函数的时候,都必须对其参数进行重新设置。poll(); 函数为输入和返回值使用了相互独立的变量,因此,不必在每次调用poll(); 之后重置被监视的描述符列表。poll(); 函数有很多优点,不需要在每次调用后重新设置掩码。

       就是说,当调用poll(); 函数返回之后,如果有信号要处理,就设置了监听结构的revents 成员,那么,当我们再次调用poll(); 函数的时候,不需要给revents清零。因为,每次调用poll(); 函数的时候,内核都会认为revents输入的是0,当有信号产生的时候,才会设置revents成员,如果没有信号产生,就revents的值就是0

select(); 不同,poll(); 函数将错误当作引起poll(); 返回的事件来处理。尽管参数timeout的范围受限,但它更容易使用,此外,poll(); 函数不需要使用max_fd参数。

3.1.2 poll函数的应用

       在上面“1.5.2 客户端的例子”的基础上增加如下成员函数:

#define MAX_SCAN_FDSET   10  //定义poll 监听的文件描述符集合的大小

#define POLL_WAIT_TIMEOUT   10*1000    //单位是毫秒

//=================================================================

//处理poll(); 模式

//=================================================================

void client_sock::work_poll()

{

    int i = 0, ret = 0;

    struct pollfd scan_fdset[MAX_SCAN_FDSET];

    int scan_fdset_num = 0;

    char recv_buf[BUF_LEN];

 

    bzero(recv_buf, BUF_LEN);

    bzero(scan_fdset, sizeof(struct pollfd)*MAX_SCAN_FDSET);

 

    scan_fdset[0].fd = fd;

    //注意,在《UNIX网络编程第一卷》P162中提到:POLLERR,POLLHUP,POLLNVAL 这些错误信号在events 中不能够设置,

    //当相应的条件触发的时候,它们会在revents 中返回。

    scan_fdset[0].events = POLLIN;  //设置需要监听的信号

 

    //数组中的其他元素设置为不可用

    for(i = 1; i < MAX_SCAN_FDSET; i++)

    {

       scan_fdset[i].fd = -1;

    }

 

    scan_fdset_num = 1;  //表示当前的scan_fdset[] 数组中只使用前面1 个元素存放需要监听的扫描符

 

    while(1)

    {

       ret = poll(scan_fdset, scan_fdset_num, POLL_WAIT_TIMEOUT);

       sleep(1);

       printf("scan_fdset[0].revents = %d \n", scan_fdset[0].revents);

       deal_select_ret(ret);    //处理返回值,与select 一样

 

       for(i = 0; i < MAX_SCAN_FDSET; i++)

       {

           if(scan_fdset[i].revents & POLLIN)

           {

              debug_printf("fd have signal!");

              ret = read(fd, recv_buf, BUF_LEN);

              if(-1 == ret)

              {

                  perror_printf("read err!");

                  continue;

              }

              show_data(recv_buf, BUF_LEN);

           }

       }//end of for

    }//end of while(1)

}

然后,在main(); 函数中调用该函数,如下:

int ret = 1;

client_sock client((char*)"127.0.0.1", 8861);

 

ret = client.init_client();

if(1 == ret)

{

    return 1;

}

client.work_poll();

运行的结果如下:

[weikaifeng@weikaifeng client]$ ./client

connect to server success! ip = 127.0.0.1, port = 8861

scan_fdset[0].revents = 1

client_sock.cpp(224)fd have signal!

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

scan_fdset[0].revents = 0

client_sock.cpp(218)select time out:: Success

scan_fdset[0].revents = 0

client_sock.cpp(218)select time out:: Success

3.1.3 poll函数被信号中断

       poll(); 函数如同select(); 函数一样,在阻塞等待的时候,能够被信号中断,具体的测试可以参考“2.1 select被信号中断”。

       所以,相应select(); 函数能够有 pselect(); 屏蔽信号,那么poll(); 函数也是由ppoll(); 函数屏蔽信号处理。

3.2 poll 内核追踪 VS select 函数 --- 比较效率

3.2.1 select 内核追踪

       本文分析的内核代码是参考2.6.12版本的内核。在 fs/select.c中有应用层select(); 函数在内核的入口地址 sys_select(); 函数。该函数的主要框架如下(具体的分析可以参考自己分析的2.6.12版本的内核源码):

asmlinkage long sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, struct timeval __user *tvp)

{

    .....

    /*

    n 是需要监听描述符集中,最大描述加1,然后,开始申请内存

    */

    ret = -ENOMEM;

    size = FDS_BYTES(n);

    bits = select_bits_alloc(size);

    if (!bits)

       goto out_nofds;

    //设置指针,指向特定的内存,所以,才可以往fds 中个指针填充数据

    fds.in      = (unsigned long *)  bits;

    fds.out     = (unsigned long *) (bits +   size);

    fds.ex      = (unsigned long *) (bits + 2*size);

    fds.res_in  = (unsigned long *) (bits + 3*size);

    fds.res_out = (unsigned long *) (bits + 4*size);

    fds.res_ex  = (unsigned long *) (bits + 5*size);

    //用用户空间获取相应的参数值

    if ((ret = get_fd_set(n, inp, fds.in)) ||

        (ret = get_fd_set(n, outp, fds.out)) ||

        (ret = get_fd_set(n, exp, fds.ex)))

       goto out;

    //清空需要返回给用户态的参数

    zero_fd_set(n, fds.res_in);

    zero_fd_set(n, fds.res_out);

    zero_fd_set(n, fds.res_ex);

 

    //这里就是重点调用do_select() 函数来轮询,该函数轮询结束之后,返回,

    //然后,就可以把fds 中相应的属性返回给应用程序。

    //do_select() 也是通过schedule_time() 调度返回的,那么休眠被唤醒之后,可能是被一些信号触发唤醒,

    //那么,下面就调用signal_pending(); 来检测信号是否异常。

    ret = do_select(n, &fds, &timeout);

    ......

};

最终调用 do_select(); 函数,该函数的主要框架如下:

int do_select(int n, fd_set_bits *fds, long *timeout)

{

    .....

    //此时,进入一个死循环,退出循环的条件有:

    //1 -- retval != 0,就是说,监听的文件描述符有信号可以处理。

    //2 -- timeout == 0, 超时了。

    //3 -- signal_pending(); 检测到当前的进程接受到信号处理。

    //4 -- wait->error 记录的错误事件不为,表示有错误处理。

    for (;;) {

       ......

       //设置当前的进程的可中断的休眠状态

       set_current_state(TASK_INTERRUPTIBLE);

       ......

       for (i = 0; i < n; ++rinp, ++routp, ++rexp) {

           ......

           for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {

                  .....

                  //调用fd 文件描述符对应的驱动程序的poll 函数指针指向的函数。

                  //如果fd 是一个网络套接,而且使用TCP 协议,那么,该函数指针就指向tcp_poll(); 函数。

                  mask = (*f_op->poll)(file, retval ? NULL : wait);

                  ......

              }

             

           }//end of 第3 层for 循环

           ...

       }//end of 第2 层for 循环

      

       //上面的for() 循环调用了cond_resched() 来调度CPU,那么,可能在睡眠的时候被产生信号触发返回,

       //所以,这里还需要调用signal_pending() 检测是否是被信号触发返回。所以,在select(); 中是可以被

       //信号中断返回,在pselect(); 函数中就可以屏蔽相应的信号。

       //如果retval >=1 表示有信号处理,所以,推出循环。

       if (retval || !__timeout || signal_pending(current))

           break;

       if(table.error) {

           retval = table.error;

           break;

       }

       //具有定时唤醒进程的调度CPU

       //而且,返回的时间是离__timeout 还剩下多少时间,如果已经超过__timeout,就返回0

       __timeout = schedule_timeout(__timeout);

    }//end of for(;;);

    .....

    *timeout = __timeout;    //更新参数timeout 为新的时间

    return retval;    //返回有多少个信号需要处理

}

可以看到,主要是通过“三重循环”来查询套接口是否有信息可以处理。

3.2.2 poll内核追踪

       本文分析的内核代码是参考2.6.12版本的内核。在 fs/select.c中有应用层poll(); 函数在内核的入口地址 sys_poll(); 函数。该函数的主要框架如下(具体的分析可以参考自己分析的2.6.12版本的内核源码):

asmlinkage long sys_poll(struct pollfd __user * ufds, unsigned int nfds, long timeout)

{

    ......

    //申请内存空间,存放用户层传递过来的参数

    while(i!=0) {

       struct poll_list *pp;

       pp = kmalloc(sizeof(struct poll_list)+

              sizeof(struct pollfd)*

              (i>POLLFD_PER_PAGE?POLLFD_PER_PAGE:i),

                  GFP_KERNEL);

       if(pp==NULL)

           goto out_fds;

       pp->next=NULL;

       pp->len = (i>POLLFD_PER_PAGE?POLLFD_PER_PAGE:i);

       if (head == NULL)

           head = pp;

       else

           walk->next = pp;

 

       walk = pp;

       if (copy_from_user(pp->entries, ufds + nfds-i,

              sizeof(struct pollfd)*pp->len)) {

           err = -EFAULT;

           goto out_fds;

       }

       i -= pp->len;

    }//end of while

 

    //进入轮询的主要操作

    fdcount = do_poll(nfds, head, &table, timeout);

    ......

}

主要是进入 do_poll(); 函数,该函数的主要框架如下:

static int do_poll(unsigned int nfds,  struct poll_list *list,

           struct poll_wqueues *wait, long timeout)

{

    .....

    //进入一个死循环,退出的条件有:

    //1 -- count != 0,就是说,监听的文件描述符有信号可以处理。

    //2 -- timeout == 0, 超时了。

    //3 -- signal_pending(); 检测到当前的进程接受到信号处理。

    //4 -- wait->error 记录的错误事件不为,表示有错误处理。

    for (;;) {

       ...

       set_current_state(TASK_INTERRUPTIBLE);

       while(walk != NULL) {

           do_pollfd( walk->len, walk->entries, &pt, &count);

           walk = walk->next;

       }

       .....

       if (count || !timeout || signal_pending(current))

           break;

       count = wait->error;

       if (count)

           break;

       timeout = schedule_timeout(timeout);

    }

    __set_current_state(TASK_RUNNING);

    return count;

}

该函数处理双重循环,然后,进入到 do_pollfd(); 函数中,该函数的主要框架如下:

static void do_pollfd(unsigned int num, struct pollfd * fdpage,

    poll_table ** pwait, int *count)

{

    ...

    //循环扫描文件描述符数组中的文件描述符

    for (i = 0; i < num; i++) {

       .....

           if (file != NULL) {

              //执行文件操作集中的poll 指针所指向的函数。

              //例如,自己在开发驱动程序的时候,此时的poll 指针,就执行我们定义注册

              //到file_operations 结构中的函数。

              //调用fd 文件描述符对应的驱动程序的poll 函数指针指向的函数。

              //如果fd 是一个网络套接,而且使用TCP 协议,那么,该函数指针就指向tcp_poll(); 函数。

              if (file->f_op && file->f_op->poll)

                  mask = file->f_op->poll(file, *pwait);

           ......

    }

}

在该函数中有一个循环处理,那么,总的来说,对于 sys_poll(); 函数也是需要 3 个循环来处理。它的处理模式与sys_select(); 差不多。

3.2.3 select poll 的比较

       通过上面的内核分析,都知道,select(); poll(); 函数实现差不多的功能,但是,还是有如下的区别:

1 select(); 要监听的信号集受到内核的限制,而poll(); 要监听的信号集是可以是自己任意设置的一个数组,不受内核限制。

2 select(); 函数把需要监听的信号存放在fd_set结构中,返回的信号也是存放同一个fd_set结构中,所以,监听信号集在每次调用select(); 之后被修改,当再次调用select(); 来监听的时候,需要重新设置监听的信号集。而poll(); 还需要监听的信号与返回的信号分开存放,需要监听的信号存放在events成员中,返回的信号存放在revents成员中。所以,当调用poll(); 之后,events 成员不会被改变,如果想再次调用poll(); 函数,可以不用修改events成员。同样,对于revents成员,但再次调用poll(); 函数的时候,可以不用给它清除,。因为,每次调用poll(); 函数的时候,内核都会认为revents输入的是0,当有信号产生的时候,才会设置revents成员,如果没有信号产生,就revents的值就是0

3 select(); 函数的最后一个参数是指针类型,其值会被修改,所以,当再次调用select(); 函数的时候,需要重新设置该参数。而poll(); 函数的最后一个参数是值类型,不会被修改。

4 ppoll

       poll(); 函数类似于select(); 函数一样,能够被信号中断,所以,select(); 就对应有pselect(); 函数来处理屏蔽信号,而poll(); 函数就对应有ppoll(); 函数来处理,屏蔽信号,该函数的定义如下:

int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout_ts, const sigset_t *sigmask);

       poll(); 函数相比,增加了 timeout_ts sigmask 参数,而这两个参数与pselect(); 中定义的一样,timeout_ts const 类型,不能够被修改,是定义等待超时的时间。sigmask就是屏蔽的信号。

测试的方法是:在参考“2.3 测试pselect();”函数的基础上,增加如下的成员函数。

//=================================================================

//处理ppoll(); 模式

//=================================================================

void client_sock::work_ppoll()

{

    int ret = 0, i = 0;

    char recv_buf[BUF_LEN];

    sigset_t sigset;

    struct timespec t;

 

    struct pollfd scan_fdset[MAX_SCAN_FDSET];

    int scan_fdset_num = 0;

 

    bzero(recv_buf, BUF_LEN);

    sigemptyset(&sigset);

    sigaddset(&sigset, SIGALRM);    //把SIGALRM 信号添加到sigset 信号集中

    sigaddset(&sigset, SIGUSR1);

 

    bzero(&t, sizeof(struct timespec));

 

    t.tv_sec = 10;

    t.tv_nsec = 0;

 

    bzero(scan_fdset, sizeof(struct pollfd)*MAX_SCAN_FDSET);

 

    scan_fdset[0].fd = fd;

    //注意,在《UNIX网络编程第一卷》P162中提到:POLLERR,POLLHUP,POLLNVAL 这些错误信号在events 中不能够设置,

    //当相应的条件触发的时候,它们会在revents 中返回。

    scan_fdset[0].events = POLLIN;  //设置需要监听的信号

 

    //数组中的其他元素设置为不可用

    for(i = 1; i < MAX_SCAN_FDSET; i++)

    {

       scan_fdset[i].fd = -1;

    }

 

    scan_fdset_num = 1;  //表示当前的scan_fdset[] 数组中只使用前面1 个元素存放需要监听的扫描符

 

    while(1)

    {

       ret = ppoll(scan_fdset, scan_fdset_num, &t, &sigset);

       deal_select_ret(ret);

 

       for(i = 0; i < MAX_SCAN_FDSET; i++)

       {

           if(scan_fdset[i].revents & POLLIN)

           {

              debug_printf("fd have signal!");

              ret = read(fd, recv_buf, BUF_LEN);

              if(-1 == ret)

              {

                  perror_printf("read err!");

                  continue;

              }

              show_data(recv_buf, BUF_LEN);

           }

       }//end of for

 

    }//end of while(1)

}

然后,在main(); 函数中启动该函数,测试的结果是:

[weikaifeng@weikaifeng client]$ ./client

father pid = 14202

connect to server success! ip = 127.0.0.1, port = 8861

child pid = 14203

client_sock.cpp(285)fd have signal!

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

main.cpp(52)in father process, send SIGUSR1!

main.cpp(52)in father process, send SIGUSR1!

main.cpp(52)in father process, send SIGUSR1!

main.cpp(52)in father process, send SIGUSR1!

可以看到父进程不停地向子进程发送SIGUSR1信号,但是,子进程的ppoll(); 函数并没有被中断返回。

转载地址:https://mylinux.blog.csdn.net/article/details/9001857 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:C++对象内存空间变化的问题!只是引入一个析构函数而已!
下一篇:Linux 多线程编程

发表评论

最新留言

哈哈,博客排版真的漂亮呢~
[***.90.31.176]2024年05月03日 17时53分52秒