Chinaunix首页 | 论坛 | 博客
  • 博客访问: 15111
  • 博文数量: 7
  • 博客积分: 235
  • 博客等级: 二等列兵
  • 技术积分: 70
  • 用 户 组: 普通用户
  • 注册时间: 2008-12-13 01:41
文章分类
文章存档

2011年(7)

我的朋友
最近访客

分类: 系统运维

2011-10-13 11:07:22

linux epoll模型  

转载自:http://www.cppblog.com/Khan/archive/2008/04/02/46013.html

    Linux I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。Linux 2.6内核中有提高网络I/O性能的新方法,即epoll 。

1、为什么select落后
    首先,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,在我用的2.6.15-25-386内核中,该值是1024,搜索内核源代码得到:
include/linux/posix_types.h:
#define __FD_SETSIZE         1024
    也就是说,如果想要同时检测1025个句柄的可读状态是不可能用select实现的。或者同时检测1025个句柄的可写状态也是不可能的。其次,内核中实现select是使用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数的执行时间与FD_SET中句柄的个数有一个比例关系,即select要检测的句柄数越多就会越费时。当然,在前文中我并没有提及poll方法,事实上用select的朋友一定也试过poll,我个人觉得select和poll大同小异,个人偏好于用select而已。

2、内核中提高I/O性能的新方法 epoll
    epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。要使用epoll只需要以下的三个系统函数调用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。

Linux2.6内核epoll介绍
    先介绍2本书《The Linux Networking Architecture--Design and Implementation of Network Protocols in the Linux Kernel》, 以2.4内核讲解Linux TCP/IP实现,相当不错。作为一个现实世界中的实现,很多时候你必须作很多权衡,这时候参考一个久经考验的系统更有实际意义。举个例子,linux内 核中sk_buff结构为了追求速度和安全,牺牲了部分内存,所以在发送TCP包的时候,无论应用层数据多大,sk_buff最小也有272的字节。其实 对于socket应用层程序来说,另外一本书《UNIX Network Programming Volume 1》意义更大一点。2003年的时候,这本书出了最新的第3版本,不过主要还是修订第2版本。其中第6章《I/O Multiplexing》是最重要的,Stevens给出了网络IO的基本模型。在 这里最重要的莫过于select模型和Asynchronous I/O模型。从理论上说,AIO似乎是最高效的,你的IO操作可以立即返回,然后等待os告诉你IO操作完成。但是一直以来,如何实现就没有一个完美的方 案。最著名的windows完成端口实现的AIO,实际上也只是内部用线程池实现的罢了,最后的结果是IO有个线程池,你的应用程序也需要一个线程 池...... 很多文档其实已经指出了这引发的线程context-switch所带来的代价。在linux 平台上,关于网络AIO一直是改动最多的地方,2.4的年代就有很多AIO内核patch,最著名的应该算是SGI。但是一直到2.6内核发布,网络模块 的AIO一直没有进入稳定内核版本(大部分都是使用用户线程模拟方法,在使用了NPTL的linux上面其实和windows的完成端口基本上差不多 了)。2.6内核所支持的AIO特指磁盘的AIO---支持io_submit(),io_getevents()以及对Direct IO的支持(即:就是绕过VFS系统buffer直接写硬盘,对于流服务器在内存平稳性上有相当的帮助)。
    所以,剩下的select模型基本上就成为我们在linux上面的唯一选择,其实,如果加上no-block socket的配置,可以完成一个"伪"AIO的实现,只不过推动力在于你而不是os而已。不过传统的select/poll函数有着一些无法忍受的缺 点,所以改进一直是2.4-2.5开发版本内核的任务,包括/dev/poll,realtime signal等等。最终,Davide Libenzi开发的epoll进入2.6内核成为正式的解决方案。

3、epoll的优点
<1> 支持一个进程打开大数目的socket描述符(FD)
    select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置, 默认值是2048。对于那些需要支持上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样 会带来网络效率的下降;二是可以选择多进程的解决方案(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加 上进程间数据同步远比不上线程间同步高效,所以这也不是一种完美的方案。不过epoll 没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于select 所支持的2048。举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

<2> IO效率不随FD数目增加而线性下降
    传统select/poll的另一个致命弱点就是当你拥有一个很大的socket集合,由于网络得延时,使得任一时间只有部分的socket是"活跃" 的,而select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进 行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。于是,只有"活跃"的socket才会主动去调用 callback函数,其他idle状态的socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll也不比select/poll低多少效率,但若 过多使用的调用epoll_ctl,效率稍微有些下降。然而一旦使用idle connections模拟WAN环境,那么epoll的效率就远在select/poll之上了。

<3> 使用mmap加速内核与用户空间的消息传递
    这点实际上涉及到epoll的具体实现。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就显 得很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你像我一样从2.5内核就开始关注epoll的话,一定不会忘记手 工mmap这一步的。

<4> 内核微调
    这一点其实不算epoll的优点,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比 如,内核TCP/IP协议栈使用内存池管理sk_buff结构,可以在运行期间动态地调整这个内存pool(skb_head_pool)的大小---通 过echo XXXX>/proc/sys/net/core/hot_list_length来完成。再比如listen函数的第2个参数(TCP完成3次握 手的数据包队列长度),也可以根据你平台内存大小来动态调整。甚至可以在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的 NAPI网卡驱动架构。

4、epoll的工作模式
    令人高兴的是,linux2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦的是epoll有2种工作方式:LT和ET
    LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表
    ET (edge-triggered) 是高速工作方式,只支持no-block socket。 在这种模式下,当描述符从未就绪变为就绪时,内核就通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的 就绪通知,直到你做了某些操作而导致那个文件描述符不再是就绪状态(比如 你在发送,接收或是接受请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核就不会发送更多的通知(only once)。不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。
    epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用,具体用法请参考,在也有一个完整的例子,大家一看就知道如何使用了。

5、 epoll的使用方法

epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数:
所用到的数据结构:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
结构体epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件,而epoll_data 联合体用来保存触发事件的某个文件描述符相关的数据。例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段,以便后面的读写操作在这个文件描述符上进行。epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件,可能的取值为:
EPOLLIN:表示对应的文件描述符可以读;
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读;
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:表示对应的文件描述符有事件发生;
所用到的函数:
1)、epoll_create函数
函数声明:int epoll_create(int size)
该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。
2)、epoll_ctl函数
函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数:
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除;
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功则返回0,不成功则返回-1。
3)、epoll_wait函数
函数声明:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
该函数用于轮询I/O事件的发生。
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值;
返回发生事件数。

    首先通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你的epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作都将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄

    之后在你的网络主循环里面调用epoll_wait(int epfd, epoll_event events, int max_events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait函数操作成功之后,events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout参 数指示 epoll_wait的超时条件,为0时表示马上返回;为-1时表示函数会一直等下去直到有事件返回;为任意正整数时表示等这么长的时间,如果一直没有事 件,则会返回。一般情况下如果网络主循环是单线程的话,可以用-1来等待,这样可以保证一些效率,如果是和主循环在同一个线程的话,则可以用0来保证主循 环的效率。epoll_wait返回之后,应该进入一个循环,以便遍历所有的事件。

    对epoll 的操作就这么简单,总共不过4个API:epoll_create, epoll_ctl, epoll_wait和close。以下是man中的一个例子。

struct epoll_event ev, *events;
for(;;) {
 nfds = epoll_wait(kdpfd, events, maxevents, -1); //等待I/O事件
 for(n = 0; n < nfds; ++n) {
  if(events[n].data.fd == listener) { //如果是主socket的事件,则表示有新连接进入,需要进行新连接的处理。
    client = accept(listener, (struct sockaddr *) &local,  &addrlen);
    if(client < 0){
      perror("accept error");
      continue;
    }
    setnonblocking(client); // 将新连接置于非阻塞模式
    ev.events = EPOLLIN | EPOLLET; 
                                   //注意这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,
                                   //如果有写操作的话,这个时候epoll是不会返回事件的,
                                   //如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET。
    ev.data.fd = client; // 并且将新连接也加入EPOLL的监听队列
    if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {  // 设置好event之后,将这个新的event通过epoll_ctl
                                                             //加入到epoll的监听队列里,这里用EPOLL_CTL_ADD
                                                             //来加一个新的 epoll事件。可以通过EPOLL_CTL_DEL来减少
                                                             //一个epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。
      fprintf(stderr, "epoll set insertion error: fd=%d"0, client);
      return -1;
    }
  }  else // 如果不是主socket的事件的话,则代表这是一个用户的socket的事件,
            // 则用来处理这个用户的socket的事情是,比如说read(fd,xxx)之类,或者一些其他的处理。
    do_use_fd(events[n].data.fd);
 }
}

6、Linux下epoll编程实例
    epoll 模型似乎只有一种格式,所以大家只要参考下面的代码,就能够对epoll有所了解了。

while (TRUE) {
  int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT); //等待EPOLL事件的发生
                                                                                    //至于相关的端口,则需要在初始化EPOLL的时候绑定。
  if (nfds <= 0)    continue;
  m_bOnTimeChecking = FALSE;
  g_CurTime = time(NULL);
  for (int i=0; i {
    try {
      if (m_events[i].data.fd == m_listen_http_fd) //如果新监测到一个HTTP用户连接到绑定的HTTP端口则建立新连接。
      {
        OnAcceptHttpEpoll ();
      } else if (m_events[i].data.fd == m_listen_sock_fd) //如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口则
                                                                                     //建立新的连接。
      {
        OnAcceptSockEpoll ();
      } else if (m_events[i].events & EPOLLIN) //如果是已经连接的用户,并且收到数据,那么进行读入操作。
      {
        OnReadEpoll (i);
      }
      OnWriteEpoll (i); //查看当前的活动连接是否有需要写出的数据。
    } catch (int) {
      PRINTF ("CATCH捕获错误\n");
      continue;
    }
  }
  m_bOnTimeChecking = TRUE;
  OnTimer (); //进行一些定时的操作,主要就是删除一些断线用户等。
}


 *****************************************************************************************************************************************




    Epoll模型主要负责对大量并发用户的请求进行及时处理,完成服务器与客户端的数据交互。其具体的实现步骤如下:
(a) 使用epoll_create()函数创建文件描述,设定可管理的最大socket描述符数目。
(b) 创建与epoll关联的接收线程,应用程序可以创建多个接收线程来处理epoll上的读通知事件,线程的数量依赖于程序的具体需要。
(c) 创建一个侦听socket的描述符ListenSock,并将该描述符设定为非阻塞模式,调用Listen()函数在该套接字上侦听有无新的连接请求,在epoll_event结构中设置要处理的事件类型EPOLLIN,工作方式为 epoll_ET,以提高工作效率,同时使用epoll_ctl()来注册事件,最后启动网络监视线程。
(d) 网络监视线程启动循环,epoll_wait()等待epoll事件发生。
(e) 如果epoll事件表明有新的连接请求,则调用accept()函数,将用户socket描述符添加到epoll_data联合体,同时设定该描述符为非阻塞,并在epoll_event结构中设置要处理的事件类型为读和写,工作方式为epoll_ET。
(f) 如果epoll事件表明socket描述符上有数据可读,则将该socket描述符加入可读队列,通知接收线程读入数据,并将接收到的数据放入到接收数据的链表中,经逻辑处理后,将反馈的数据包放入到发送数据链表中,等待由发送线程发送。

例子代码:

#include
#include
#include
#include
#include
#include
#include
#include

#define MAXLINE 10
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5555
#define INFTIM 1000

void setnonblocking(int sock)
{
  int opts;
  opts=fcntl(sock,F_GETFL);

  if(opts<0)
  {
    perror("fcntl(sock,GETFL)");
    exit(1);
  }

  opts = opts | O_NONBLOCK;

  if(fcntl(sock,F_SETFL,opts)<0)
  {
    perror("fcntl(sock,SETFL,opts)");
    exit(1);
  }
}


int main()
{
  int i, maxi, listenfd, connfd, sockfd, epfd, nfds;
  ssize_t n;
  char line[MAXLINE];
  socklen_t clilen;

  struct epoll_event ev,events[20]; //声明epoll_event结构体的变量, ev用于注册事件, events数组用于回传要处理的事件
  epfd=epoll_create(256); //生成用于处理accept的epoll专用的文件描述符, 指定生成描述符的最大范围为256 

  struct sockaddr_in clientaddr;
  struct sockaddr_in serveraddr;

  listenfd = socket(AF_INET, SOCK_STREAM, 0);

  setnonblocking(listenfd); //把用于监听的socket设置为非阻塞方式

  ev.data.fd=listenfd; //设置与要处理的事件相关的文件描述符
  ev.events=EPOLLIN | EPOLLET; //设置要处理的事件类型
  epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); //注册epoll事件

  bzero(&serveraddr, sizeof(serveraddr));
  serveraddr.sin_family = AF_INET;
  char *local_addr="200.200.200.204";
  inet_aton(local_addr,&(serveraddr.sin_addr));
  serveraddr.sin_port=htons(SERV_PORT);  //或者htons(SERV_PORT);

  bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));

  listen(listenfd, LISTENQ);  //同一时刻能监听的最大连接请求数为LISTENQ,始终受约束?

  maxi = 0;

  for( ; ; ) {
    nfds=epoll_wait(epfd,events,20,500); //等待epoll事件的发生

    for(i=0;i //处理所发生的所有事件
      { 
       if(events[i].data.fd==listenfd)    /**监听事件**/
        { 
           connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); 
           if(connfd<0){ 
             perror("connfd<0"); 
             exit(1); //还是continue;或break;语句???
           }

         setnonblocking(connfd); //把客户端的socket设置为非阻塞方式

         char *str = inet_ntoa(clientaddr.sin_addr);
         std::cout<<"connect from "<_u115 ? tr<

         ev.data.fd=connfd; //设置用于读操作的文件描述符
         ev.events=EPOLLIN | EPOLLET; //设置用于注测的读操作事件
         epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //注册ev事件 
       } 
      else if(events[i].events&EPOLLIN)     /**读事件**/
        {
           if ( (sockfd = events[i].data.fd) < 0) continue;
           if ( (n = read(sockfd, line, MAXLINE)) < 0) { 
              if (errno == ECONNRESET) {

                //epoll_ctl(epfd,EPOLL_CL_DEL,sockfd,&ev); //需要???
                close(sockfd);
                events[i].data.fd = -1; 

                //continue; //需要???
                } else
                  {
                    std::cout<<"readline error"<

                    //continue; //需要???
                  }
             } else if (n == 0) {

               //epoll_ctl(epfd,EPOLL_CL_DEL,sockfd,&ev); //需要???
                close(sockfd);
                events[i].data.fd = -1; 

                //continue; //需要???
              }

          ev.data.fd=sockfd; //设置用于写操作的文件描述符
          ev.events=EPOLLOUT | EPOLLET; //设置用于注测的写操作事件
          epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要处理的事件为EPOLLOUT 
       } 
      else if(events[i].events&EPOLLOUT)    /**写事件**/
        {
          sockfd = events[i].data.fd;
          write(sockfd, line, n);

          ev.data.fd=sockfd; //设置用于读操作的文件描述符
          ev.events=EPOLLIN | EPOLLET; //设置用于注册的读操作事件
          epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要处理的事件为EPOLIN
        }
     }
  }
}





Windows I/O完成端口  

2009-09-17 18:17:42|  分类: NetProgram |字号 

WINDOWS完成端口编程
1、基本概念
2、WINDOWS完成端口的特点
3、完成端口(Completion Ports )相关数据结构和创建
4、完成端口线程的工作原理
5、Windows完成端口的实例代码

WINDOWS完成端口编程
   摘要:开发网络程序从来都不是一件容易的事情,尽管只需要遵守很少的一些规则:创建socket,发起连接,接受连接,发送和接收数据,等等。真正的困难 在于:让你的程序可以适应从单单一个连接到几千个连接乃至于上万个连接。利用Windows完成端口进行重叠I/O的技术,可以很方便地在Windows 平台上开发出支持大量连接的网络服务程序。本文介绍在Windows平台上使用完成端口模型开发的基本原理,同时给出实际的例子。本文主要关注C/S结构 的服务器端程序,因为一般来说,开发一个大容量、具有可扩展性的winsock程序就是指服务程序。

1、基本概念
   设备---指windows操作系统上允许通信的任何东西,比如文件、目录、串行口、并行口、邮件槽、命名管道、无名管道、套接字、控制台、逻辑磁盘、物理磁盘等。绝大多数与设备打交道的函数都是CreateFile/ReadFile/WriteFile等,所以我们不能看到**File函数就只想到文件设备

   与设备通信有两种方式,同步方式和异步方式:同步方式,当调用ReadFile这类函数时,函数会等待系统执行完所要求的工作,然后才返回;异步方式,ReadFile这类函数会直接返回,系统自己去完成对设备的操作,然后以某种方式通知完成操作。

   重叠I/O---- 顾名思义,就是当你调用了某个函数(比如ReadFile)就立刻返回接着做自己的其他动作的时候,系统同时也在对I/0设备进行你所请求的操作,在这段 时间内你的程序和系统的内部动作是重叠的,因此有更好的性能。所以,重叠I/O是在异步方式下使用I/O设备的。重叠I/O需要使用的一个非常重要的数据结构:OVERLAPPED

2、WINDOWS完成端口的特点
   Win32重叠I/O(Overlapped I/O)机制允许发起一个操作,并在操作完成之后接收信息。对于那种需要很长时间才能完成的操作来说,重叠IO机制尤其有用,因为发起重叠操作的线程在重 叠请求发出后就可以自由地做别的事情了。在WinNT和Win2000上,提供的真正可扩展的I/O模型就是使用完成端口(Completion Port)的重叠I/O。完成端口---是一种WINDOWS内核对象完成端口用于异步方式的重叠I/0情况下,当然重叠I/O不一定非得使用完成端口不可,同样设备内核对象、事件对象、告警I/0等也可使用。但是完成端口内部提供了线程池的管理,可以避免反复创建线程的开销,同时可以根据CPU的个数灵活地决定线程个数,而且可以减少线程调度的次数从而提高性能。其实类似于WSAAsyncSelect和select函数的机制更容易兼容Unix,但是难以实现我们想要的“扩展性”。而且windows完成端口机制在操作系统的内部已经作了优化,从而具备了更高的效率。所以,我们选择完成端口开始我们的服务器程序开发。
   1)发起操作不一定完成:系统会在完成的时候通知你,通过用户在完成端口上的等待,处理操作的结果。所以要有检查完成端口和取操作结果的线程。在完成端口 上守候的线程系统有优化,除非在执行的线程发生阻塞,不会有新的线程被激活,以此来减少线程切换造成的性能代价。所以如果程序中没有太多的阻塞操作,就没 有必要启动太多的线程,使用CPU数量的两倍,一般这么多线程就够了
   2)操作与相关数据的绑定方式:在提交数据的时候用户对数据打上相应的标记,记录操作的类型,在用户处理操作结果的时候,通过检查自己打的标记和系统的操作结果进行相应的处理。
   3)操作返回的方式:一般操作完成后要通知程序进行后续处理。但写操作可以不通知用户,此时如果用户写操作不能马上完成,写操作的相关数据会被暂存到非交 换缓冲区中,在操作完成的时候,系统会自动释放缓冲区,此时发起完写操作,使用的内存就可以释放了。但如果占用非交换缓冲太多会使系统停止响应。

3、完成端口(Completion Ports )相关数据结构和创建
    其实可以把完成端口看成系统维护的一个队列,操作系统把重叠IO操作完成的事件通知放到该队列里,由于是暴露 “操作完成”的事件通知,所以命名为“完成端口”(Completion Ports)。一个socket被创建后,就可以在任何时刻和一个完成端口联系起来。

OVERLAPPED数据结构
typedef struct _OVERLAPPED {
    ULONG_PTR Internal;  //被系统内部赋值,用来表示系统状态
    ULONG_PTR InternalHigh;  //被系统内部赋值,表示传输的字节数
    union {
        struct {
            DWORD Offset;  //与OffsetHigh合成一个64位的整数,用来表示从文件头部的多少字节开始操作 
            DWORD OffsetHigh;  //如果不是对文件I/O来操作,则Offset必须设定为0 
         }; 
       PVOID Pointer;
    }; 
   HANDLE hEvent;  //如果不使用,就务必设为0;否则请赋一个有效的Event句柄
} OVERLAPPED, *LPOVERLAPPED;

下面是异步方式使用ReadFile的一个例子
OVERLAPPED Overlapped;
Overlapped.Offset=345;
Overlapped.OffsetHigh=0;
Overlapped.hEvent=0;
//假定其他参数都已经被初始化
ReadFile(hFile,buffer,sizeof(buffer),&dwNumBytesRead,&Overlapped);
这样就完成了异步方式读文件的操作,然后ReadFile函数返回,由操作系统做自己的事情。

下面介绍几个与OVERLAPPED结构相关的函数。

等待重叠I/0操作完成的函数
BOOL GetOverlappedResult (
HANDLE hFile,
LPOVERLAPPED lpOverlapped, //接受返回的重叠I/0结构
LPDWORD lpcbTransfer, //成功传输了多少字节数
BOOL fWait  //TRUE只有当操作完成才返回,FALSE直接返回,如果操作没有完成,
                    //通过用GetLastError( )函数会返回ERROR_IO_INCOMPLETE
);

HasOverlappedIoCompleted可以帮助我们测试重叠I/0操作是否完成,该宏对OVERLAPPED结构的Internal成员进行了测试,查看是否等于STATUS_PENDING值。

   一般来说,一个应用程序可以创建多个工作线程来处理完成端口上的通知事件。工 作线程的数量依赖于程序的具体需要。但是在理想的情况下,应该对应一个CPU 创建一个线程。因为在完成端口理想模型中,每个线程都可以从系统获得一个“原子”性的时间片,轮番运行并检查完成端口,线程的切换是额外的开销。但在实际 开发的时候,还要考虑这些线程是否牵涉到其他堵塞操作的情况。如果某线程进行堵塞操作,系统则将其挂起,让别的线程获得运行时间。因此,如果有这样的情 况,可以多创建几个线程来尽量利用时间。

创建完成端口的函数
成端口是一个内核对象,使用时它总是要和至少一个有效的设备句柄相关联,完成端口是一个复杂的内核对象,创建它的函数是:
HANDLE CreateIoCompletionPort(
    IN HANDLE FileHandle,
    IN HANDLE ExistingCompletionPort,
    IN ULONG_PTR CompletionKey,
    IN DWORD NumberOfConcurrentThreads
    );

通常创建工作分两步:
第一步,创建一个新的完成端口内核对象,可以使用下面的函数:
       HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads)
       {    
          return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads);
       };      
第二步,将刚创建的完成端口和一个有效的设备句柄关联起来,可以使用下面的函数:
       bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey)
       {
          HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0);
          return h==hCompPort;
       };
说明如下:
1)CreateIoCompletionPort函数也可以一次性的既创建完成端口对象,又关联到一个有效的设备句柄。
2)CompletionKey是一个可以自己定义的参数,我们可以把一个结构的地址赋给它,然后在合适的时候取出来使用,最好要保证结构里面的内存不是分配在栈上,除非你有十分的把握内存会保留到你要使用的那一刻。
3)NumberOfConcurrentThreads用来指定要允许同时运行的的线程的最大个数,通常我们指定为0,这样系统会根据CPU的个数来自动确定。
4)创建和关联的动作完成后,系统会将完成端口关联的设备句柄、完成键作为一条纪录加入到这个完成端口的设备列表中。如果你有多个完成端口,就会有多个对应的设备列表。如果设备句柄被关闭,则表中该纪录会被自动删除。

4、完成端口线程的工作原理

1)完成端口管理线程池
   完成端口可以帮助我们管理线程池,但是线程池中的线程需要我们自己使用_beginthreadex来创建,凭什么通知完成端口管理我们的新线程呢?答案在函数GetQueuedCompletionStatus。该函数原型:
BOOL GetQueuedCompletionStatus(
    IN HANDLE CompletionPort,
    OUT LPDWORD lpNumberOfBytesTransferred,
    OUT PULONG_PTR lpCompletionKey,
    OUT LPOVERLAPPED *lpOverlapped,
    IN DWORD dwMilliseconds
); 

   这个函数试图从指定的完成端口的I/0完成队列中提取纪录。只有当重叠I/O动作完成的时候,完成队列中才有纪录。凡是调用这个函数的线程将会被放入到完成端口的等待线程队列中,因此完成端口就可以在自己的线程池中帮助我们维护这个线程。完成端口的I/0完成队列中存放了当重叠I/0完成的结果---- 一条纪录,该纪录拥有四个字段,前三项就对应GetQueuedCompletionStatus函数的2、3、4参数,最后一个字段是错误信息dwError。我们也可以通过调用PostQueudCompletionStatus模拟完成一个重叠I/0操作。 

   当I/0完成队列中出现了纪录,完成端口将会检查等待线程队列,该队列中的线程都是通过调用GetQueuedCompletionStatus函数使自己加入队列的。等待线程队列很简单,只是保存了这些线程的ID。完成端口按照后进先出的原则将一个线程队列的ID放入到释放线程列表中,同时该线程将从等待GetQueuedCompletionStatus函数返回的睡眠状态中变为可调度状态等待CPU的调度。所以我们的线程要想成为完成端口管理的线程,就必须要调用GetQueuedCompletionStatus函数。出于性能的优化,实际上完成端口还维护了一个暂停线程列表,具体细节可以参考《Windows高级编程指南》,我们现在知道的知识,已经足够了。

2)线程间数据传递
   完成端口线程间传递数据最常用的办法是在_beginthreadex函数中将参数传递给线程函数,或者使用全局变量。但完成端口也有自己的传递数据的方法,答案就在于CompletionKey和OVERLAPPED参数
CompletionKey 被保存在完成端口的设备表中,是和设备句柄一一对应的,我们可以将与设备句柄相关的数据保存到CompletionKey中,或者将 CompletionKey表示为结构指针,这样就可以传递更加丰富的内容。这些内容只能在一开始关联完成端口和设备句柄的时候做,因此不能在以后动态改 变。

   OVERLAPPED参数是在每次调用ReadFile这样的支持重叠I/0的函数时传递给完成端口的。 我们可以看到,如果我们不是对文件设备做操作,该结构的成员变量就对我们几乎毫无作用。我们需要附加信息,可以创建自己的结构,然后将 OVERLAPPED结构变量作为我们结构变量的第一个成员,然后传递第一个成员变量的地址给ReadFile这样的函数。因为类型匹配,当然可以通过编 译。当GetQueuedCompletionStatus函数返回时,我们可以获取到第一个成员变量的地址,然后一个简单的强制转换,我们就可以把它当 作完整的自定义结构的指针使用,这样就可以传递很多附加的数据了。太好了!只有一点要注意,如果跨线程传递,请注意将数据分配到堆上,并且接收端应该将数 据用完后释放。我们通常需要将ReadFile这样的异步函数的所需要的缓冲区放到我们自定义的结构中,这样当 GetQueuedCompletionStatus被返回时,我们的自定义结构的缓冲区变量中就存放了I/0操作的数据。CompletionKey和OVERLAPPED参数,都可以通过GetQueuedCompletionStatus函数获得

3)线程的安全退出
   很多线程为了不止一次地执行异步数据处理,需要使用如下语句
while (true)
{
       ......
       GetQueuedCompletionStatus(...); 
       ......
}
那么线程如何退出呢,答案就在于上面曾提到过的PostQueudCompletionStatus函数,我们可以向它发送一个自定义的包含了OVERLAPPED成员变量的结构地址,里面含一个状态变量,当状态变量为退出标志时,线程就执行清除动作然后退出。

5、Windows完成端口的实例代码
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
ULONG_PTR *PerHandleKey;
OVERLAPPED *Overlap;
OVERLAPPEDPLUS *OverlapPlus;
OVERLAPPEDPLUS *newolp;
DWORD dwBytesXfered;
while (1)
{
  ret = GetQueuedCompletionStatus(hIocp, &dwBytesXfered, (PULONG_PTR)&PerHandleKey, &Overlap, INFINITE);
  if (ret == 0)
   {
    // Operation failed
    continue;
   }
 OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol);
 switch (OverlapPlus->OpCode)
 {
 case OP_ACCEPT:
   // Client socket is contained in OverlapPlus.sclient
   // Add client to completion port
  CreateIoCompletionPort((HANDLE)OverlapPlus->sclient, hIocp, (ULONG_PTR)0, 0);
  // Need a new OVERLAPPEDPLUS structure
  // for the newly accepted socket. Perhaps
  // keep a look aside list of free structures.
  newolp = AllocateOverlappedPlus();
  if (!newolp)
   {
    // Error
   }
  newolp->s = OverlapPlus->sclient;
  newolp->OpCode = OP_READ;
  // This function divpares the data to be sent
  PrepareSendBuffer(&newolp->wbuf);
  ret = WSASend(newolp->s, &newolp->wbuf, 1, &newolp->dwBytes, 0, &newolp.ol, NULL);
  if (ret == SOCKET_ERROR)
   {
    if (WSAGetLastError() != WSA_IO_PENDING)
     {
       // Error
     }
   }
  // Put structure in look aside list for later use
  FreeOverlappedPlus(OverlapPlus);
  // Signal accept thread to issue another AcceptEx
  SetEvent(hAcceptThread);
  break;
case OP_READ:
  // Process the data read
  // Repost the read if necessary, reusing the same
  // receive buffer as before
  memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED));
  ret = WSARecv(OverlapPlus->s, &OverlapPlus->wbuf, 1, &OverlapPlus->dwBytes, &OverlapPlus->dwFlags, &OverlapPlus->ol, NULL);
  if (ret == SOCKET_ERROR)
  {
    if (WSAGetLastError() != WSA_IO_PENDING)
     {
      // Error
     }
  }
  break;
case OP_WRITE:
  // Process the data sent, etc.
  break;
} // switch
} // while
} // WorkerThread
 

查 看以上代码,注意如果Overlapped操作立刻失败(比如,返回SOCKET_ERROR或其他非WSA_IO_PENDING的错误),则没有任何 完成通知时间会被放到完成端口队列里。反之,则一定有相应的通知时间被放到完成端口队列。更完善的关于Winsock的完成端口机制,可以参考 MSDN的Microsoft PlatForm SDK,那里有完成端口的例子。

阅读(2558) | 评论(0) | 转发(0) |
0

上一篇:博客已升级,请注意变更地址

下一篇:没有了

给主人留下些什么吧!~~