全部博文(395)
分类: 嵌入式
2011-05-09 17:03:25
转自:
http://blogold.chinaunix.net/u2/73521/showart_1110536.html
(注:一下博文中的一些函数是在2.6低版本中才有的,如果你的内核是2.6.30版本的话,这些函数就没有了,他们是使用其他的方法来实现的,我用的2.6.18版本中就有这些函数)
轮询函数
轮询的概念和作用
使用非阻塞I/O的应用程序通常会使用select()和poll(),poll和select用于查询设备的状态,以便用户程序获知是
否能对设备进行非阻塞的访问,它们都需要设备驱动程序中的poll函数支持。
Select()和poll()系统调用最终会引发设备驱动设备中的poll()函数被执行。poll()函数为最终执行体)
Linux下select调用的过程:
1.用户层应用程序调用select(),底层调用poll())
2.核心层调用sys_select() --> do_select()
最终调用文件描述符fd对应的struct file类型变量的struct file_operations *f_op的poll函数。
当然2.25.45中还引入了epoll(),即拓展的poll();
select()和poll()系统调用的本质一样,前者在BSD UNIX中引入的,后者在System V
中引入的。
1、首先说什么是文件描述符,它有什么作用?
文件描述符是一个简单的整数,用以标明每一个被进程所打开的文件和socket。第一个打开的文件是0,第二个是
1,依此类推。Unix 操作系统通常给每个进程能打开的文件数量强加一个限制。更甚的是,unix 通常有一个系统
级的限制。
大多数情况下,1024 个文件描述符足够了。非常忙的cache可能需要4096或更多。当然,你可以自己配置文件描述
符限制,在配置文件描述符限制时,我推荐设置系统级限制的数量为每个进程限制的2 倍。
应用程序中的轮询编程
应用程序中最广泛用到的是BSD UNIX中引入的select()系统调用,其原型如下:
int select(int numfds,fd_set *readfme,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
select的第一个参数是文件描述符集中要被检测的数目,这个值必须至少比待检测的最大文件描述符大1;参数readfds
指定了被读监控的文件描述符集;参数writefds指定了被写监控的文件描述符集;而参数exceptfds指定了被例外
条件监控的文件描述符集。
Timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout时间后若没有文件描述符准备
好则返回,struct timeval数据结构的定义如下:
struct timeval
{
int tv_sec;//表示几秒
int tv_usec;//表示几微秒
};
timeout取不同的值,该调用就表现不同的性质:
1.timeout为0,调用立即返回;
2.timeout为NULL,select()调用就阻塞,直到知道有文件描述符就绪;(当有文件描述符就绪时,会向这个函数
发送信号,以唤醒此函数。)
3.timeout为正整数,就是一般的定时器。
select的返回值有如下情况:
1.正常情况下返回就绪的文件描述符个数;
2.经过了timeout时长后仍无设备准备好,返回值为0;
3.如果select被某个信号中断,它将返回-1并设置errno为EINTR。
4.如果出错,返回-1并设置相应的errno。
select()函数的 接口主要是建立在一种叫“fd_set”类型的基础上。这个类型是一组文件描述符(fd)的集合。
因为fd_set类型的长度在不同平台上不同,因此应该用一组标准的宏定义来处理这个类变量:
首先我们来了解fd_set这个结构的定义
#define FD_ZERO(fdsetp) __FD_ZERO(fdsetp)
(注意了,下面定义的常量是很有用的,因为在了解fd_set这个结构中会使用到,当然,这些值并不是规定死的,
在不同的程序中有不同的值,不过这些值只是取少数的几个规定值)
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)//计算得到的值为32
#define __FD_SETSIZE 1024
#define __NFDBITS (8 * sizeof(unsigned long)) //8*4=32
typedef struct {
37 unsigned long fds_bits [__FDSET_LONGS];
38} __kernel_fd_set;
typedef __kernel_fd_set fd_set;
以上就是对文件描述符集fd_set的定义,如果你能够有下图,那么我想,你更高兴地去学习文件描述符:
下面操作用来设置、清除、判断文件描述符集合。
FD_ZERO(fd_set *set);//将文件描述符集fd_set中的值置0,如此以来对应所有位都被设置为0;
下面是对此函数的具体操作:
static __inline__ void __FD_ZERO(__kernel_fd_set *p)
88{
89 unsigned long *tmp = p->fds_bits;
90 int i;
91
92 if (__builtin_constant_p(__FDSET_LONGS)) {
93 switch (__FDSET_LONGS) {//__FDSET_LONGS的取值由系统决定
94 case 16: (注:这个地方并没有使用循环管,直接赋值,这样虽然不方便,当时很快)
95 tmp[ 0] = 0; tmp[ 1] = 0; tmp[ 2] = 0; tmp[ 3] = 0;
96 tmp[ 4] = 0; tmp[ 5] = 0; tmp[ 6] = 0; tmp[ 7] = 0;
97 tmp[ 8] = 0; tmp[ 9] = 0; tmp[10] = 0; tmp[11] = 0;
98 tmp[12] = 0; tmp[13] = 0; tmp[14] = 0; tmp[15] = 0;
99 return;
100
101 case 8:
102 tmp[ 0] = 0; tmp[ 1] = 0; tmp[ 2] = 0; tmp[ 3] = 0;
103 tmp[ 4] = 0; tmp[ 5] = 0; tmp[ 6] = 0; tmp[ 7] = 0;
104 return;
105
106 case 4:
107 tmp[ 0] = 0; tmp[ 1] = 0; tmp[ 2] = 0; tmp[ 3] = 0;
108 return;
109 }
110 }
111 i = __FDSET_LONGS;
112 while (i) {
113 i--;
114 *tmp = 0;
115 tmp++;
116 }//此循环是为了使得上面的判断操作变得更安全。
117}
上面有内建函数 __builtin_constant_p ,这个函数用于判断一个值是否为编译时常数,假如参数__FDSET_LONGS的
值是常数,函数返回 1,否则返回 0。
FD_SET(int fd,fd_set *set);//将一个文件描述符加入文件描述集中 。
即将fd在文件描述符中所对应的位置1,*set为所操作的文件描述符集对象。
2static __inline__ void __FD_SET(unsigned long __fd, __kernel_fd_set *__fdsetp)
53{
54 unsigned long __tmp = __fd / __NFDBITS;
55 unsigned long __rem = __fd % __NFDBITS;
56 __fdsetp->fds_bits[__tmp] |= (1UL<<__rem);//将无符号长整型的1左移_rem位
57}
FD_CLR(int fd,fd_set *set)//将一个文件描述符从文件描述符集中清除。
static __inline__ void __FD_CLR(unsigned long __fd, __kernel_fd_set *__fdsetp)
61{
62 unsigned long __tmp = __fd / __NFDBITS;
63 unsigned long __rem = __fd % __NFDBITS;
64 __fdsetp->fds_bits[__tmp] &= ~(1UL<<__rem);
65}
FD_ISSET(int fd,fd_set *set)//判断文件描述符是否被置位。
9static __inline__ int __FD_ISSET(unsigned long __fd, const __kernel_fd_set *__p)
70{
71 unsigned long __tmp = __fd / __NFDBITS;
72 unsigned long __rem = __fd % __NFDBITS;
73 return (__p->fds_bits[__tmp] & (1UL<<__rem)) != 0;
74}(注:这个地方的return,你可以看看它的用法,实际上它返回的是一个0,或者1,很奇怪,呵呵)
设备驱动中的轮询操作:
unsigned int (*poll)(struct file *filp,struct poll_table *wait);
第一个参数为file结构体指针,第二个参数为轮询表指针。这个函数应该进行以下两项工作。
1、对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头添加到poll_table;
2、返回表示是否对设备进行无阻塞读、写访问的掩码。
关键的用于向poll_table注册等待队列的poll_wait(*)函数的原型如下:
void poll_wait(struct file *filp,wait_queue_heat_t *queue,poll_table *wait);
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
38{
39 if (p && wait_address)
40 p->qproc(filp, wait_address, p);//这个函数需要用户自己编写来实现。
41}
下面是对轮询表的定义:
33typedef struct poll_table_struct {
34 poll_queue_proc qproc;
35} poll_table;
31typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
由上面的代码可以知道poll_table 结构只是对一个函数的封装,更有趣的是,这个函数建立了实际的数据结构. 那个数据结构, 对于 poll和 select, 是一个内存页的链表, 其中包含 poll_table_entry 结构. 每个 poll_table_entry 持有被传递给 poll_wait 的 struct file 和 wait_queue_head_t 指针, 以及一个关联的等待队列入口.
下面是poll_table_entry结构体的源代码:
48struct poll_table_entry { 49 struct file * filp; 50 wait_queue_t wait; 51 wait_queue_head_t * wait_address; 52};
poll_wait()函数的名称非常容易让人产生误会,以为它和wait_event()等一样,会阻塞地等待某件事情的发生,
这个函数不会引起阻塞.poll_wait()函数所作的 工作是把当前进程添加到wait参数指定的等待列表(poll_table)中。
驱动程序poll()函数应该返回设备资源的可获取状态,即POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL等宏的位“或“结果。
每个宏的含义都表明设备的一种状态,如POLLIN意味着设备可以无阻塞地读,POLLOUT意味着设备可以无阻塞地写。
下面对poll()函数的可获得状态的取值进行解析:
POLLIN
如果设备可被不阻塞地读, 这个位必须设置.
POLLRDNORM
这个位必须设置, 如果"正常"数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM ).
POLLRDBAND
这个位指示带外数据可用来从设备中读取. 当前只用在 Linux 内核的一个地方( DECnet 代码 )并且通常对设备驱动不可用.
POLLPRI
高优先级数据(带外)可不阻塞地读取. 这个位使 select 报告在文件上遇到一个异常情况, 因为 selct 报告带外数据作为一个异常情况.
POLLHUP
当读这个设备的进程见到文件尾, 驱动必须设置 POLLUP(hang-up). 一个调用 select 的进程被告知设备是可读的, 如同 selcet 功能所规定的.
POLLERR
一个错误情况已在设备上发生. 当调用 poll, 设备被报告位可读可写, 因为读写都返回一个错误码而不阻塞.
POLLOUT
这个位在返回值中设置, 如果设备可被写入而不阻塞.
POLLWRNORM
这个位和 POLLOUT 有相同的含义, 并且有时它确实是相同的数. 一个可写的设备返回( POLLOUT|POLLWRNORM).
POLLWRBAND
如同 POLLRDBAND , 这个位意思是带有零优先级的数据可写入设备. 只有 poll 的数据报实现 使用这个位,
因为一个数据报看传送带外数据.应当重复一下 POLLRDBAND 和 POLLWRBAND 仅仅对关联到 socket 的文件描述符有意义:
通常设备驱动不使用这些标志.
发表于: 2008-08-06 ,修改于: 2008-08-07 16:11,已浏览1333次,有评论5条 推荐 投诉
网友评论
内容: 在static __inline__ int __FD_ISSET(unsigned long __fd, const __kernel_fd_set *__p)
函数中,__NFDBITS是8*sizeof(unsigned long),如果在32位系统下,就是32,__tmp是用于df_set类型中fds_bits数组成员的下标,之所以等于fd/__NFDBITS是因为要充分利用内存。例如__fd小于32的所有文件描述符都使用fds_bist[0]的相应位来标识其是否有变化,__fd等于0的使用fds_bits[0]的第零位,__fd等于1的使用fds_bits[0]的第一位,依次如此。这样fds_bits数组的每一个元素(一个unsigned long型)都可以检测32个文件描述符的变化情况,而不必要使用32个unsigned long型。
Niu Tao评论于:2008-08-06 12:06:49 (221.11.22.★)
内容: __tmp是用于df_set类型中fds_bits数组成员的下标,之所以等于fd/__NFDBITS是因为要充分利用内存。这样说法应该不怎么具有说服力吧!因为大多数情况下,1024个文件描述符已经足够了,当然,可能会大于这个值。但是想想看,1024位在整个内存中又算得了什么呢?所以,上面的说法说服力不是很强。那么,如果是为了更容易操作,那结果又会是怎样的呢?
chinahhucai 评论于:2008-08-06 21:29:20 (221.11.22.★)
内容: static __inline__ void __FD_ZERO(__kernel_fd_set *p)
在这个函数中,为什么不直接用循环来完成了,这样代码还简单些,因为循环效率低吗?
chenjifeng 评论于:2008-08-07 10:29:24 (221.11.22.★)
内容: 首先说说上面说的节省内存?在32位系统下,如果使用一位去监测一个文件描述符,按默认监测1024个文件描述符需要__FDSET_LONGS个long,也就是128字节,而如果使用一个long型去监测一个文件描述符,那就需要1024个long,也就是4096字节。也许对于现在的大内存而言没什么,但在像arm那样的板上会是如何?要知道每一个字节都很宝贵,都不能浪费。况且一个好的操作系统就是要很好的管理系统资源,用最小的代价提供最好的性能为应用程序服务,如果每个数据结构的设计都出于“浪费几个字节无所谓”的态度,那这样下来内核里的N多个浪费的几个字节加起来会是一个什么概念?恐怕要用MB去度量吧。当然对于你说的更容易操作,昨天的确是没考虑,第一感觉就是节省内存。今天你提起觉得的确是有这样的目的,比如__FD_SET()等都是用了位操作。
下来说说select具体是如何和我们写的poll联系起来的:
select(用户态调用select) (注:这个时候应用层调用的是select (),你也可以追踪内核源代码,看看应用层调用
-->sys_select poll时是如何一级一级用下去的,呵呵)
---->core_sys_select
------>do_select
-------->poll(驱动程序里的poll)
可以看到一层层调用下来,就到了我们写的poll了。其中select的第一个参数n最终是用在了do_select里控制循环了,后面三个在core_sys_select里被“加工”也是被传到do_select里了,而最后的参数timeout是被换算为HZ最中在do_select里被用做schedule_timeout()的参数了。
niutao.linux 评论于:2008-08-07 10:36:23 (221.11.22.★)
内容: 对于chenjifeng的问题我是这样理解的:
如果使用循环代码的确是会简单一些,但如果编译成汇编的话,循环必定涉及到指令的跳转,而简单的赋值操作却只是一个mov就搞定了,况且是顺序执行,效率显然比循环高多了。