在Linux环境下select函数的初体验

在linux中, 主要的 IO复用方式中, 有epoll, poll 和select, 这次先来学习下select。select 能够同时监视多个文件描述符的变法, 也支持超时返回。 2017-03-08 15:39:11 Linuxselect函数 Windows 7系统激活工具推荐,激活系统只要一分钟 相信很多用户对于安装Win7系统都巨细无遗,但很多用户又发现最常使用小马激活会因失败而告终,也许你们无法接受事实的真相,在这种情况下,我们完全可以在DOS下激活系统。请跟随笔者了解Win7激活工具怎么用。

linux中, 主要的 IO复用方式中, 有epoll, poll 和select, 这次先来学习下select。select 能够同时监视多个文件描述符的变法, 也支持超时返回。

[[184930]]

select介绍

在linux中, 主要的 IO复用方式中, 有epoll, poll 和select, 这次先来学习下select.

select 能够同时监视多个文件描述符的变法, 也支持超时返回.

先来看下select函数的定义

  1. /*/usr/include/sys/select.h*/
  2. externintselect(int__nfds,//最大文件描述符+1
  3. fd_set*__restrict__readfds,//读状态文件集
  4. fd_set*__restrict__writefds,//写状态文件集
  5. fd_set*__restrict__exceptfds,//异常状态文件集
  6. structtimeval*__restrict__timeout);//超时时间

如上图函数声明所示, 不管我们关注什么状态, 我们都应该把同一类状态的文件描述符存到同一个fd_set集合,以便select能够相应的位置打上标签, 以便后续我们来判断该文件描述符是否已经准备好

这些传递给select函数的参数, 将告诉内核:

  • 我们需要监听的文件描述符
  • 对于每个文件描述符, 我们所关心的状态 (读/写/异常)
  • 我们要等待多长时间 (无限长/超时返回)

而内核也会通过select的返回, 告知我们一些信息:

  • 已经准备好的文件描述符个数
  • 那三种状态分别是哪些文件描述符

我们可以通过以下方式将关注的文件描述符加入相应的文件集:

  1. intsocket_test;
  2. socket_test=socket(...);//创建socket文件描述符
  3. connent(socket_test,..);//连接服务端
  4. FD_SET(socket_test,&rdfds);//加入读状态文件集
  5. FD_SET(socket_test,&wdfds);//加入写状态文件集
  6. ....

select原理

select函数执行顺序是: SYSCALL_DEFINE5 (sys_select) -> core_sys_select -> do_select

我们都知道, select 支持监听三个文件集: 读文件集, 写文件集, 异常文件集;

在我们调用FD_SET(socket_test, &rdfds)时, 实际上执行的操作是: 在rdfds成员数组中, 将__FDELT (d)位置的值 设成 __FDMASK (d), 直接说会有点疑惑, 先看下相关的函数,宏定义是怎样定义的吧:

  1. /*取自:/usr/include/sys/select.h*/
  2. #defineFD_SET(fd,fdsetp)__FD_SET(fd,fdsetp)
  3. typedeflongint__fd_mask;
  4. /*取自:/usr/include/bits/select.h*/
  5. #define__NFDBITS(8*(int)sizeof(__fd_mask))
  6. #define__FDELT(d)((d)/__NFDBITS)
  7. #define__FDMASK(d)((__fd_mask)1<<((d)%__NFDBITS))
  8. #define__FD_SET(d,set)(__FDS_BITS(set)[__FDELT(d)]|=__FDMASK(d))
  9. typedefstruct
  10. {
  11. /*XPG4.2requiresthismembername.Otherwiseavoidthename
  12. fromtheglobalnamespace.*/
  13. #ifdef__USE_XOPEN
  14. __fd_maskfds_bits[__FD_SETSIZE/__NFDBITS];
  15. #define__FDS_BITS(set)((set)->fds_bits)
  16. #else
  17. __fd_mask__fds_bits[__FD_SETSIZE/__NFDBITS];
  18. #define__FDS_BITS(set)((set)->__fds_bits)
  19. #endif
  20. }fd_set;
  21. /*/usr/include/linux/posix_types.h*/
  22. #define__FD_SETSIZE1024

举个栗子, 假设 fd=3, 当我们执行FD_SET(fd, &rdfds)时:

  1. 算出 __FDELT(d) 和 __FDMASK(d)的值, 通过上面的宏定义, 可以分别得出结果: 3/(8*8), 1<<3%(8*8), 也就是0 和 二进制的 0000 0100
  2. 然后分别将值存入 rdfds.__fds_bits第0个位置, 值为十进制的8
  3. 我们可以将__fds_bit的每个索引看成是一个聚合的过程, 每个值8字节, 也就是有64位, 可以存64个fd, 在我的系统上, 算出数组的长度是__FD_SETSIZE / __NFDBITS = 1024/8=128个, 也就是大概能容纳 128*64=8192(如果理解错误请指出)

经过上面的运算, 我们将需要关注的文件描述关联到 rdfds文件集了, 对于写文件集, 异常文件集都是同样的运算, 等这些步骤都进行完了, 接下来就是进入core_sys_select函数了:

  1. 执行到 core_sys_select 时, 定义一个fd_set_bits结构体: fds.
  2. 分别为fds的成员(in, out, ex, res_in, res_out, res_ex)申请内存
  3. 将我们传给select的 rdfds, wrfds, exfds分别赋值给 in, out, ex, 这样fds就能记录三个集合的结果了
  4. 初始化那个三个成员之后, 将执行do_select(n, &fds, end_time)
  5. 在do_select中, 函数将进入死循环,其中还有两个循环, 分别是针对 "最大文件描述符数" 和 fd_set_bits数组中单个值位数. 从上面我们已经知道, 在fd_set_bits每个值都代表所关注的文件描述符, 每个值是__NFDBITS(8 *8字节)大小,也就是64位, 所以在上面循环内, 还要再循环64次
  6. 看到这里其实大家都应该有个底了, 为什么要循环那么多次, 因为我们需要通过每个文件描述符对应的file_operations结构体的接口f_op->poll来得知是否已经准备好了

简单介绍 file_operations

我们都知道,当我们打开一些设备或者文件时, 总是返回一个文件描述符, 其实通过这个文件描述符, 我们通过fget_light 来获得对应的file结构体, 为什么还要反查这个file, 因为通过这个file结构体可以得到: file_operations结构体

file_operations结构体: 用来存储驱动内核模块提供的对 设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的 事务的函数的地址。

  1. /*linux-2.6.32/include/linux/fs.h*/
  2. structfile_operations{
  3. ...
  4. unsignedint(*poll)(structfile*,structpoll_table_struct*);//select通过这个来获取状态
  5. ...(其他接口忽略)

do_select 循环体源码:

  1. /*select.c/do_select()*/
  2. for(;;){
  3. unsignedlong*rinp,*routp,*rexp,*inp,*outp,*exp;
  4. inp=fds->in;outp=fds->out;exp=fds->ex;
  5. rinp=fds->res_in;routp=fds->res_out;rexp=fds->res_ex;
  6. for(i=0;i<n;++rinp,++routp,++rexp){
  7. unsignedlongin,out,ex,all_bits,bit=1,mask,j;
  8. unsignedlongres_in=0,res_out=0,res_ex=0;
  9. conststructfile_operations*f_op=NULL;
  10. structfile*file=NULL;
  11. in=*inp++;out=*outp++;ex=*exp++;
  12. all_bits=in|out|ex;
  13. if(all_bits==0){
  14. i+=__NFDBITS;
  15. continue;
  16. }
  17. for(j=0;j<__NFDBITS;++j,++i,bit<<=1){//遍历64位
  18. intfput_needed;
  19. if(i>=n)
  20. break;
  21. if(!(bit&all_bits))
  22. continue;
  23. //在当前进程的structfiles_struct中根据所谓的用户空间文件描述符fd来获取文件描述符
  24. file=fget_light(i,&fput_needed);
  25. if(file){
  26. f_op=file->f_op;//file_operations结构体
  27. mask=DEFAULT_POLLMASK;
  28. if(f_op&&f_op->poll){
  29. wait_key_set(wait,in,out,bit);
  30. mask=(*f_op->poll)(file,wait);
  31. }
  32. fput_light(file,fput_needed);
  33. if((mask&POLLIN_SET)&&(in&bit)){//判断读状态
  34. res_in|=bit;
  35. retval++;
  36. wait=NULL;
  37. }
  38. if((mask&POLLOUT_SET)&&(out&bit)){//判断写状态
  39. res_out|=bit;
  40. retval++;
  41. wait=NULL;
  42. }
  43. if((mask&POLLEX_SET)&&(ex&bit)){//判断异常状态
  44. res_ex|=bit;
  45. retval++;
  46. wait=NULL;
  47. }
  48. }
  49. }
  50. if(res_in)
  51. *rinp=res_in;
  52. if(res_out)
  53. *routp=res_out;
  54. if(res_ex)
  55. *rexp=res_ex;
  56. cond_resched();
  57. }
  58. wait=NULL;
  59. if(retval||timed_out||signal_pending(current))
  60. break;
  61. if(table.error){
  62. retval=table.error;
  63. break;
  64. }
  65. /*
  66. *Ifthisisthefirstloopandwehaveatimeout
  67. *given,thenweconverttoktime_tandsettheto
  68. *pointertotheexpiryvalue.
  69. */
  70. if(end_time&&!to){
  71. expire=timespec_to_ktime(*end_time);
  72. to=&expire;
  73. }
  74. if(!poll_schedule_timeout(&table,TASK_INTERRUPTIBLE,
  75. to,slack))
  76. timed_out=1;
  77. }

当select经历完上面的流程, 将会有以下结果:

  • >0: 准备好的文件描述符个数
  • 0: 超时
  • -1: 出错或者接收到信号

那我们接下来要做的就是,

  • 通过行FD_ISSET()判断之前绑定的文件fd, 如果为真, 则进行相应操作
  • 因为select返回后, 之前存好的rdfds, wdfds, exfds都会被清空, 所以需要用FD_SET()重新加入

select实战

上面已经学习到关于select的相关知识, 那么我们应该要来实战下:

这次我们需要实现的目标是:

一个程序, 同时连接3个socket_server, 并且将socket_server发送的消息打印出来(不需要响应, 也不需要交互)

程序代码:

  1. /*filename:test_select.c*/
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<sys/socket.h>
  5. #include<sys/select.h>
  6. #include<sys/types.h>
  7. #include<netinet/in.h>
  8. #include<unistd.h>
  9. #include<fcntl.h>
  10. voidmain()
  11. {
  12. //socket1
  13. intsocketd;
  14. charbuffer[1025];
  15. structsockaddr_inseraddr;
  16. socketd=socket(AF_INET,SOCK_STREAM,0);
  17. seraddr.sin_family=AF_INET;
  18. seraddr.sin_port=htons(9997);
  19. inet_pton(AF_INET,"127.0.0.1",&seraddr.sin_addr);
  20. if(connect(socketd,(structsockaddr*)&seraddr,sizeof(seraddr))<0)
  21. {
  22. printf("socketd1connectfailed\n");
  23. exit(3);
  24. }
  25. //socket2
  26. intsocketd2;
  27. charbuffer2[1025];
  28. structsockaddr_inseraddr2;
  29. socketd2=socket(AF_INET,SOCK_STREAM,0);
  30. seraddr2.sin_family=AF_INET;
  31. seraddr2.sin_port=htons(9998);
  32. inet_pton(AF_INET,"127.0.0.1",&seraddr2.sin_addr);
  33. if(connect(socketd2,(structsockaddr*)&seraddr2,sizeof(seraddr))<0)
  34. {
  35. printf("socketd2connectfailed\n");
  36. exit(3);
  37. }
  38. //scoket3
  39. intsocketd3;
  40. charbuffer3[1025];
  41. structsockaddr_inseraddr3;
  42. socketd3=socket(AF_INET,SOCK_STREAM,0);
  43. seraddr3.sin_family=AF_INET;
  44. seraddr3.sin_port=htons(9999);
  45. inet_pton(AF_INET,"127.0.0.1",&seraddr3.sin_addr);
  46. if(connect(socketd3,(structsockaddr*)&seraddr3,sizeof(seraddr))<0)
  47. {
  48. printf("socketd3connectfailed\n");
  49. exit(3);
  50. }
  51. intmaxfdp;
  52. fd_setfds;//select需要的文件描述符集合
  53. maxfdp=socketd3+1;//select第一个形参就是打开的最大文件描述符+1
  54. structtimevaltimeout={3,0};//超时设置
  55. while(1)
  56. {
  57. FD_ZERO(&fds);//初始化文件描述符集合
  58. FD_SET(socketd,&fds);//分别添加以上三个需要监听的文件描述符
  59. FD_SET(socketd2,&fds);
  60. FD_SET(socketd3,&fds);
  61. select(maxfdp,&fds,NULL,NULL,&timeout);
  62. //通过FD_ISSET来分别判断监听的文件描述符在fds有没有被设置成1
  63. if(FD_ISSET(socketd,&fds))
  64. {
  65. read(socketd,buffer,1024);
  66. printf("1%s\n",buffer);
  67. }
  68. if(FD_ISSET(socketd2,&fds))
  69. {
  70. read(socketd2,buffer2,1024);
  71. printf("2%s\n",buffer2);
  72. }
  73. if(FD_ISSET(socketd3,&fds))
  74. {
  75. read(socketd3,buffer3,1024);
  76. printf("3%s\n",buffer3);
  77. }
  78. }
  79. }

为了快速建立简单的测试服务端, 所以用python实现简单socket_server:

  1. #socket1.py
  2. importsocket
  3. s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  4. s.bind(('localhost',9997))
  5. s.listen(2)
  6. rint'Socket1isonready!'
  7. client,info=s.accept()
  8. printinfo
  9. while1:
  10. message=raw_input('input:')
  11. client.send(message)
  12. s.close()
  13. -------------------------------
  14. #socket2.py
  15. importsocket
  16. s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  17. s.bind(('localhost',9998))
  18. s.listen(2)
  19. rint'Socket2isonready!'
  20. client,info=s.accept()
  21. printinfo
  22. while1:
  23. message=raw_input('input:')
  24. client.send(message)
  25. s.close()
  26. -------------------------------
  27. #socket3.py
  28. importsocket
  29. s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  30. s.bind(('localhost',9999))
  31. s.listen(2)
  32. rint'Socket3isonready!'
  33. client,info=s.accept()
  34. printinfo
  35. while1:
  36. message=raw_input('input:')
  37. client.send(message)
  38. s.close()

分别运行 socket1.py, socket2.py, socket3.py将会看到如下结果:

  1. #运行socket1.py
  2. [root@iZ23pynfq19Z~]#pythonsocket1.py
  3. Socket1isonready!
  4. ----------------------
  5. #运行socket1.py
  6. [root@iZ23pynfq19Z~]#pythonsocket2.py
  7. Socket2isonready!
  8. ----------------------
  9. #运行socket1.py
  10. [root@iZ23pynfq19Z~]#pythonsocket3.py
  11. Socket3isonready!

当我们编译 test_select.c 并运行时, 将会看到三个服务端都出现了相应的响应:

  1. #运行socket1.py
  2. [root@iZ23pynfq19Z~]#pythonsocket1.py
  3. Socket1isonready!
  4. ('127.0.0.1',55951)#客户端连接的信息,端口不一定相同
  5. input:
  6. ----------------------
  7. #运行socket1.py
  8. [root@iZ23pynfq19Z~]#pythonsocket2.py
  9. Socket2isonready!
  10. ('127.0.0.1',55921)
  11. input:
  12. ----------------------
  13. #运行socket1.py
  14. [root@iZ23pynfq19Z~]#pythonsocket3.py
  15. Socket3isonready!
  16. ('127.0.0.1',55933)
  17. input:

那么我们来尝试三个服务端分别发送消息到select程序吧:

  1. #socket1.py
  2. [root@iZ23pynfq19Z~]#pythonsocket1.py
  3. Socket1isonready!
  4. ('127.0.0.1',55951)#客户端连接的信息,端口不一定相同
  5. input:asd
  6. input:qwe
  7. input:as
  8. input:
  9. ----------------------
  10. #socket1.py
  11. [root@iZ23pynfq19Z~]#pythonsocket2.py
  12. Socket2isonready!
  13. ('127.0.0.1',55921)
  14. input:asd
  15. input:asd
  16. input:asd
  17. input:as
  18. input:s
  19. input:
  20. ----------------------
  21. #socket1.py
  22. [root@iZ23pynfq19Z~]#pythonsocket3.py
  23. Socket3isonready!
  24. ('127.0.0.1',55933)
  25. input:asd
  26. input:qwe
  27. input:a
  28. input:

将看到select程序都能输出三个socket_server发出的消息:

在Linux环境下select函数的初体验

需要注意的是:

  • 前面的数字是socket_server的编号, 因为server发送消息的顺序是乱的, 所以输出的编号也是乱的
  • 这次只为验证select, 所以并没对程序的健壮性作较好的设计, 所以如果服务端/客户端刷屏了, 直接ctrl-c终止吧

经过上述的实验, 我们应该能够简单的了解select的用法和效果, 通过select实现IO多路复用, 可以让我们一定程度上避免多线程/多进程的繁琐, 在我们日常工作上, 有必要的话尝试这种方式也不失一种偷懒的方法.

©本文为清一色官方代发,观点仅代表作者本人,与清一色无关。清一色对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文不作为投资理财建议,请读者仅作参考,并请自行承担全部责任。文中部分文字/图片/视频/音频等来源于网络,如侵犯到著作权人的权利,请与我们联系(微信/QQ:1074760229)。转载请注明出处:清一色财经

(0)
打赏 微信扫码打赏 微信扫码打赏 支付宝扫码打赏 支付宝扫码打赏
清一色的头像清一色管理团队
上一篇 2023年5月6日 19:08
下一篇 2023年5月6日 19:08

相关推荐

发表评论

登录后才能评论

联系我们

在线咨询:1643011589-QQbutton

手机:13798586780

QQ/微信:1074760229

QQ群:551893940

工作时间:工作日9:00-18:00,节假日休息

关注微信