当前对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的值为 -1,poll 就永远都不会超时。如果整数值为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可以被定义为POLLRDNORM和POLLRDBAND的逻辑或常值。POLLIN存在于SVR3的实现,它要早于SVR4中的优先级带,所以,此常值保持了向后兼容性。类似地,POLLOUT等效于POLLWRNORM,前者早于后者。
第一个参数 struct pollfd 结构的定义如下:
//该结构是应用层poll(); 函数监听文件描述符集合中用到。它存放了一个需要
//监听的文件描述符结构。
struct pollfd {
int fd; //需要监听的文件描述符
short events; //存放需要监听的事件
short revents; //存放产生的事件
};
fd是文件描述符值,events和revents是通过对代表各种事件的标识符进行逻辑或运算构建而成的,上面列举了这些标记,表示相应的事件。设置events来包含要监视的事件,poll用已经发生的事件来填写revents。
poll函数通过revents中设置标识符POLLHUP, POLLERR和POLLNVAL来反映相关条件的存在。不需要在events中对与这些标识符相关的比特位进行设置。
如果fd小于零,那么events字段被忽略。所以,如果我们要忽略该结构,就设置fd = -1,那么,events字段被忽略,而revents在调用poll(); 函数返回之后,就被设置为0。
标准中没有说明应该如何处理文件结束,文件结束可以通过revents的标识符POLLHUP或者返回0字节的常规读操作来传达。即使POLLIN或POLLRDNORM指出还有数据要读,POLLHUP也可能会被设置。因此,应该在错误检验之前处理正常的读操作。
注意:
select(); 调用对传递给它的文件描述符集进行了修改,所以,每次在调用select(); 函数的时候,都必须对其参数进行重新设置。poll(); 函数为输入和返回值使用了相互独立的变量,因此,不必在每次调用poll(); 之后重置被监视的描述符列表。poll(); 函数有很多优点,不需要在每次调用后重新设置掩码。
就是说,当调用poll(); 函数返回之后,如果有信号要处理,就设置了监听结构的revents 成员,那么,当我们再次调用poll(); 函数的时候,不需要给revents清零。因为,每次调用poll(); 函数的时候,内核都会认为revents输入的是0,当有信号产生的时候,才会设置revents成员,如果没有信号产生,就revents的值就是0。
与select(); 不同,poll(); 函数将错误当作引起poll(); 返回的事件来处理。尽管参数timeout的范围受限,但它更容易使用,此外,poll(); 函数不需要使用max_fd参数。
在上面“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
poll(); 函数如同select(); 函数一样,在阻塞等待的时候,能够被信号中断,具体的测试可以参考“2.1 select被信号中断”。
所以,相应select(); 函数能够有 pselect(); 屏蔽信号,那么poll(); 函数也是由ppoll(); 函数屏蔽信号处理。
本文分析的内核代码是参考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; //返回有多少个信号需要处理
}
可以看到,主要是通过“三重循环”来查询套接口是否有信息可以处理。
本文分析的内核代码是参考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(); 差不多。
通过上面的内核分析,都知道,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(); 函数的最后一个参数是值类型,不会被修改。
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(); 函数并没有被中断返回。