I/O多路复用

  • 一、记录锁或(区域锁)[注1]
    我见过的对记录锁讲解最详细的书就是《Unix高级环境编程》,特别是关于进程、文件描述符和记录锁三者之间关系的讲解更是让人受益匪浅,有此书的朋友一定不要放过哟。这里将其中的三原则摘录到这:
    关于记录锁的自动继承和释放有三条规则:
    (1) 锁与进程、文件两方面有关。这有两重含意:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以存访的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。
    (2) 由fork产生的子程序不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程被视为另一个进程,对于从父进程处继承过来的任一描述符,子进程要调用fcntl以获得它自己的锁。这与锁的作用是相一致的。锁的作用是阻止多个进程同时写同一个文件(或同一文件区域)。如果子进程继承父进程的锁,则父、子进程就可以同时写同一个文件。
    (3) 在执行exec后,新程序可以继承原执行程序的锁。

    话归正题谈APR的记录锁,平心而论APR的提供的加索和解锁接口并没有什么独到的地方,APR之所以将之封装起来,无非是为了提供一个统一的跨平台接口,并且不破坏APR整体代码风格的一致性。APR记录锁源码位置在$(APR_HOME)/file_io/unix目录下flock.c,头文件仍然是apr_file_io.h。apr_file_lock和apr_file_unlock仅提供对整个文件的加锁和解锁,而并不支持对文件中任意范围数据的加锁和解锁。至于该锁是建议锁(advisory lock)还是强制锁(mandatory lock),需要看具体的平台的实现了。两个函数均利用fcntl实现记录锁功能(前提是所在平台支持fcntl,由于fcntl是POSIX标准,绝大多数平台都支持)。代码中有一处值得鉴赏:
    while ((rc = fcntl(thefile->filedes, fc, &l)) < 0 && errno == EINTR)
    continue;
    这里这么做的原因就是考虑到fcntl的调用可能被某信号中断,一旦中断我们去要重启fcntl函数。

    二、I/O多路复用[注2]
    在经典的《Unix网络编程第1卷》Chapter 6中作者详细介绍了五种I/O模型,分别为:
    - blocking I/O
    - nonblocking I/O
    - I/O multiplexing (select and poll)
    - signal driven I/O (SIGIO)
    - asynchronous I/O (the POSIX aio_functions)
    作者同时对这5种I/O模型作了很详细的对比分析,很值得一看。这里所说的I/O多路复用就是第三种模型,它既解决了Blocking I/O数据处理不及时,又解决了Non-Blocking I/O采用轮旬的CPU浪费问题,同时它与异步I/O不同的是它得到了各大平台的广泛支持。

    APR I/O多路复用源码主要在$(APR_HOME)/poll/unix目录下的poll.c和select.c中,头文件为apr_poll.h。APR提供统一的apr_poll接口,但是apr_pollset_t结构定义和apr_poll的实现则根据宏POLLSET_USES_SELECT、POLL_USES_POLL和POLLSET_USES_POLL的定义与否而不同。这里拿poll的实现(That is 使用poll来实现apr_poll及apr_pollset_xx相关,与之对应的是使用select来实现apr_poll及apr_pollset_xx相关)来分析:在poll的实现下,apr_pollset_t的定义如下:
    /* in poll.c */
    struct apr_pollset_t
    {
    apr_pool_t *pool;
    apr_uint32_t nelts;
    apr_uint32_t nalloc;
    struct pollfd *pollset;
    apr_pollfd_t *query_set;
    apr_pollfd_t *result_set;
    };

    统一的apr_pollfd_t定义如下:
    /* in apr_poll.h */
    struct apr_pollfd_t {
    apr_pool_t *p; /* associated pool */
    apr_datatype_e desc_type; /* descriptor type */
    apr_int16_t reqevents; /* requested events */
    apr_int16_t rtnevents; /* returned events */
    apr_descriptor desc; /* @see apr_descriptor */
    void *client_data; /* allows app to associate context */
    };
    把数据结构定义贴出来便于后面分析时参照理解。

    假设我们像这样apr_pollset_create(&mypollset, 10, p, 0)调用,那么在apr_pollset_create后,我们可以用图示来表示mypollset变量的状态:
    mypollset
    -------
    nalloc ----> 10 /* 该mypollset的“容量”,在create的时候由参数指定 */
    -------
    nelts ----> 0 /* 刚初始化,mypollset中并没有任何element,之后每add一次,nelts就+1 */
    -------
    ---------------------------------------------
    pollset ---------> pollset[0] | pollset[1] |...| pollset[nalloc-1]
    ---------------------------------------------
    -------
    -----------------------------------------------------
    query_set ---------> query_set[0] | query_set[1] |...| query_set[nalloc-1]
    -----------------------------------------------------
    -------
    ---------------------------------------------------------
    result_set ---------> result_set[0] | result_set[1] |...| result_set[nalloc-1]
    ---------------------------------------------------------
    -------

    pollset、query_set和result_set这几个集合的关系通过下图说明:
    apr_pollfd_t *descriptor ---> [pollset_add] --------> query_set ------ [pollset_poll] -----> result_set (输出)
    | /|\
    -------------------> pollset ------ [pollset_poll] --------------------
    apr_pollset_xx系列是改版后APR I/O复用新增的接口集,它以apr_pollset_t作为其管理的基本单位,其中apr_pollset_poll用于监视pollset中的所有descriptor(s)。而apr_poll则是旧版的APR I/O复用接口,它同样可以实现apr_pollset_poll的功能,只是它的基本管理单位是apr_pollfd_t,其相关函数还包括apr_poll_setup、apr_poll_socket_add等在apr-1.1.1版中已看不到的几个接口。新版本中建议使用apr_pollset_poll,起码APR的测试用例(testpoll.c)是这么做的。

    select实现的思路与poll实现的思路是一致的,只是apr_pollset_t的结构不同,原因不言自明。

    三、总结
    由于APR对高级I/O的封装很“薄”,所以基本上没有太多很精致的东西。

    四、参考资料
    1、《Unix高级环境编程》
    2、《Unix网络编程卷1、2》

    [注1]
    对于Unix,“记录”这个定语也是误用,因为Unix内核根本没有使用文件记录这种概念。一个更适合的术语可能是“区域锁”,因为它锁定的只是文件的一个区域(也可能是整个文件)-- 摘自《Unix高级环境编程》。

    [注2]
    在《Unix网络编程卷1》译者译为"多路复用",在《Unix高级环境编程》中译者译为"多路转接",我更倾向于前者。I/O多路复用其英文为"I/O Multiplexing"。


    IO多路复用

     

    与多线程和多进程相比,I/O多路复用的最大优势是系统开销小,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。

    主要应用:

    1)客户程序需要同时处理交互式的输入和服务器之间的网络连接

    2)客户端需要对多个网络连接作出反应

    3TCP服务器需要同时处理多个处于监听状态和多个连接状态的套接字

    4)服务器需要处理多个网络协议的套接字

    5)服务器需要同时处理不同的网络服务和协议

     

    3. IO多路复用模式

    IO多路复用

     

    4. select()函数

    #include <sys/time.h>

    int select(int nfds, fd_set *readfds, fd_set *wtitefds, fd_set *errnofds,

    struct timeval *timeout)

    注意:描述符不受限与套接字,任何描述符都行

    nfdsselect()函数监视的描述符数的最大值,一般取监视的描述符数的最大值+1

    其上限设置在sys/types.h中有定义

    #define FD_SETSIZE 256

    readfdsselect()函数监视的可读描述符集合

    wtitefdsselect()函数监视的可写描述符集合

    errnofdsselect()函数监视的异常描述符集合

    timeoutselect()函数监视超时结束时间,取NULL表示永久等待

    返回值:返回总的位数这些位对应已准备好的描述符,否则返回-1

    相关宏操作:

           FD_ZERO(fd_set *fdset):清空fdset与所有描述符的关系

           FD_SET(int fd, d_set * fdset):建立描述符fdfdset得关系

           FD_CLR(int fd, d_set * fdset):撤销描述符fdfdset得关系

           FD_ISSET(int fd, d_set * fdset):检查与fdset联系的描述符fd是否可以读写,返回非零表示可以读写

    5. select()函数实现IO多路复用的步骤

    1)清空描述符集合

    2)建立需要监视的描述符与描述符集合的关系

    3)调用select函数

    4)检查监视的描述符判断是否已经准备好

    5)对已经准备好的描述符进程IO操作

     

    同步阻塞IO在等待数据就绪上花去太多时间,而传统的同步非阻塞IO虽然不会阻塞进程,但是结合轮询来判断数据是否就绪仍然会耗费大量的CPU时间。

    多路IO复用提供了对大量文件描述符进行就绪检查的高性能方案。

    select

    select诞生于4.2BSD,在几乎所有平台上都支持,其良好的跨平台支持是它的主要的也是为数不多的优点之一。

    select的缺点(1)单个进程能够监视的文件描述符的数量存在最大限制(2)select需要复制大量的句柄数据结构,产生巨大的开销 (3)select返回的是含有整个句柄的列表,应用程序需要遍历整个列表才能发现哪些句柄发生了事件(4)select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。相对应方式的是边缘触发。

    poll

    poll 诞生于UNIX System V Release 3,那时AT&T已经停止了UNIX的源代码授权,所以显然也不会直接使用BSD的select,所以AT&T自己实现了一个和select没有多大差别的poll。

    poll和select是名字不同的孪生兄弟,除了没有监视文件数量的限制,select后面3条缺点同样适用于poll。

    面对select和poll的缺陷,不同的OS做出了不同的解决方案,可谓百花齐放。不过他们至少完成了下面两点超越,一是内核长期维护一个事件关注列表,我们只需要修改这个列表,而不需要将句柄数据结构复制到内核中;二是直接返回事件列表,而不是所有句柄列表。

    /dev/poll

    Sun在Solaris中提出了新的实现方案,它使用了虚拟的/dev/poll设备,开发者可以将要监视的文件描述符加入这个设备,然后通过ioctl()来等待事件通知。

    /dev/epoll

    名为/dev/epoll的设备以补丁的方式出现在Linux2.4中,它提供了类似/dev/poll的功能,并且在一定程度上使用mmap提高了性能。

    kqueue

    FreeBSD实现了kqueue,可以支持水平触发和边缘触发,性能和下面要提到的epoll非常接近。

    epoll

    epoll诞生于Linux 2.6内核,被公认为是Linux2.6下性能最好的多路IO复用方法。

    ?
    int epoll_create(int size)
     
    int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
     
    int epoll_wait(int epfd,struct epoll_event *events, int maxevents,int timeout)
    • epoll_create 创建 kernel 中的关注事件表,相当于创建 fd_set
    • epoll_ctl 修改这个表,相当于 FD_SET 等操作 
    • epoll_wait等待 I/O事件发生,相当于 select/poll 函数 

    epoll支持水平触发和边缘触发,理论上来说边缘触发性能更高,但是使用更加复杂,因为任何意外的丢失事件都会造成请求处理错误。Nginx就使用了epoll的边缘触发模型。

    这里提一下水平触发和边缘触发就绪通知的区别,这两个词来源于计算机硬件设计。它们的区别是只要句柄满足某种状态,水平触发就会发出通知;而只有当句柄状态改变时,边缘触发才会发出通知。例如一个socket经过长时间等待后接收到一段100k的数据,两种触发方式都会向程序发出就绪通知。假设程序从这个socket中读取了50k数据,并再次调用监听函数,水平触发依然会发出就绪通知,而边缘触发会因为socket“有数据可读”这个状态没有发生变化而不发出通知且陷入长时间的等待。

    因此在使用边缘触发的 api 时,要注意每次都要读到 socket返回 EWOULDBLOCK为止。


    6.单线程并发服务器设计实例

    功能:

    服务器等候客户连接请求,一旦连接成功显示客户地址,接收该客户的名字并显示,然后接收来自用户的信息,每收到一个字符串则显示,并将字符串反转,再将反转的字符串发回客户端。

     

    客户端首先与服务器相连,接着发送客户端名字,然后发送客户信息,接收到服务器信息并显示,之后等待用户输入Crtl+D,就关闭连接并退出。

     

     

    //server.c

        #include <stdio.h>          

        #include <string.h>          

        #include <unistd.h>         

        #include <sys/types.h>

        #include <sys/socket.h>

        #include <netinet/in.h>

        #include <arpa/inet.h>

        #include <sys/time.h>

        #include <stdlib.h>

     

        #define PORT 1234   

        #define BACKLOG 5   

        #define MAXDATASIZE 1000

        typedef struct CLIENT{

           int  fd;

           char*  name;

           struct sockaddr_in addr;

           char* data;                     

        }; 

        void process_cli(CLIENT *client, char* recvbuf, int len);

        void savedata(char* recvbuf, int len, char* data);

     

        main()

        {

        int i, maxi, maxfd,sockfd;

        int nready;

        ssize_t n;

        fd_set  rset, allset;

        int listenfd, connectfd;     

        struct sockaddr_in server;

        

        CLIENT client[FD_SETSIZE];

        char recvbuf[MAXDATASIZE];

        socklen_t sin_size;

     

        

        if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {

           

           perror("Creating socket failed.");

           exit(1);

           }

     

        int opt = SO_REUSEADDR;

        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

     

        bzero(&server,sizeof(server));

        server.sin_family=AF_INET;

        server.sin_port=htons(PORT);

        server.sin_addr.s_addr = htonl (INADDR_ANY);

        if (bind(listenfd, (struct sockaddr *)&server, sizeof(struct sockaddr)) == -1) {

           

           perror("Bind error.");

           exit(1);

           }   

     

        if(listen(listenfd,BACKLOG) == -1){  

           perror("listen() error\n");

           exit(1);

           }

     

        sin_size=sizeof(struct sockaddr_in);

        

        maxfd = listenfd;  

        maxi = -1;         

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

           client[i].fd = -1;  

           }

        FD_ZERO(&allset);

        FD_SET(listenfd, &allset);

     

        while(1)

        {

        struct sockaddr_in addr;

        rset = allset;     

        nready = select(maxfd+1, &rset, NULL, NULL, NULL);

     

        if (FD_ISSET(listenfd, &rset)) {    

           

           if ((connectfd = accept(listenfd,(struct sockaddr *)&addr,&sin_size))==-1) {

              perror("accept() error\n");

              continue;

              }

           

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

              if (client[i].fd < 0) {

                 client[i].fd = connectfd;   

                 client[i].name = new char[MAXDATASIZE];

                 client[i].addr = addr;

                 client[i].data = new char[MAXDATASIZE];

                 client[i].name[0] = '\0';

                 client[i].data[0] = '\0';

                 printf("You got a connection from %s.  ",inet_ntoa(client[i].addr.sin_addr) );

                 break;

                 }

              if (i == FD_SETSIZE)       printf("too many clients\n");

              FD_SET(connectfd, &allset);    

              if (connectfd > maxfd)  maxfd = connectfd;

              if (i > maxi)  maxi = i;      

              if (--nready <= 0) continue;   

              }

     

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

              if ( (sockfd = client[i].fd) < 0)  continue;

              if (FD_ISSET(sockfd, &rset)) {

                 if ( (n = recv(sockfd, recvbuf, MAXDATASIZE,0)) == 0) {

                    

                    close(sockfd);

                    printf("Client( %s ) closed connection. User's data: %s\n",client[i].name,client[i].data);

                    FD_CLR(sockfd, &allset);

                    client[i].fd = -1;

                    delete client[i].name;

                    delete client[i].data;

                    } else

                    process_cli(&client[i], recvbuf, n);

                 if (--nready <= 0)  break;  

                 }

           }

        }

        close(listenfd);           

        }

     

        void process_cli(CLIENT *client, char* recvbuf, int len)

        {

        char sendbuf[MAXDATASIZE];

     

        recvbuf[len-1] = '\0';

        if (strlen(client->name) == 0) {

           

           memcpy(client->name,recvbuf, len);

           printf("Client's name is %s.\n",client->name);

           return;

           }

     

        

        printf("Received client( %s ) message: %s\n",client->name, recvbuf);

        

        savedata(recvbuf,len, client->data);

        

        for (int i1 = 0; i1 < len - 1; i1++) {

           sendbuf[i1] = recvbuf[len - i1 -2];

        }

        sendbuf[len - 1] = '\0';

     

        send(client->fd,sendbuf,strlen(sendbuf),0);

        }

     

        void savedata(char* recvbuf, int len, char* data)

        {

        int start = strlen(data);

        for (int i = 0; i < len; i++) {

           data[start + i] = recvbuf[i];

        }        

        }

     

     

     

     

    // client.c

        #include <stdio.h>

        #include <unistd.h>

        #include <string.h>

        #include <sys/types.h>

        #include <sys/socket.h>

        #include <netinet/in.h>

        #include <netdb.h>        

        #include <stdlib.h>

     

        #define PORT 1234   

        #define MAXDATASIZE 100   

        void process(FILE *fp, int sockfd);

        char* getMessage(char* sendline,int len, FILE* fp);

     

        int main(int argc, char *argv[])

        {

        int fd;   

        struct hostent *he;         

        struct sockaddr_in server;  

     

        if (argc !=2) {      

           printf("Usage: %s <IP Address>\n",argv[0]);

           exit(1);

           }

     

        if ((he=gethostbyname(argv[1]))==NULL){

           printf("gethostbyname() error\n");

           exit(1);

           }

     

        if ((fd=socket(AF_INET, SOCK_STREAM, 0))==-1){  

           printf("socket() error\n");

           exit(1);

           }

     

        bzero(&server,sizeof(server));

        server.sin_family = AF_INET;

        server.sin_port = htons(PORT);

        server.sin_addr = *((struct in_addr *)he->h_addr); 

     

        if(connect(fd, (struct sockaddr *)&server,sizeof(struct sockaddr))==-1){

           printf("connect() error\n");

           exit(1);

           }

     

        process(stdin,fd);

     

        close(fd);    

        }

     

        void process(FILE *fp, int sockfd)

        {

        char    sendline[MAXDATASIZE], recvline[MAXDATASIZE];

        int numbytes;

     

        printf("Connected to server. \n");

        

        printf("Input name : ");

        if ( fgets(sendline, MAXDATASIZE, fp) == NULL) {

           printf("\nExit.\n");

           return;

           }

        send(sockfd, sendline, strlen(sendline),0);

     

         

        while (getMessage(sendline, MAXDATASIZE, fp) != NULL) {

           send(sockfd, sendline, strlen(sendline),0);

     

           if ((numbytes = recv(sockfd, recvline, MAXDATASIZE,0)) == 0) {

              printf("Server terminated.\n");

              return;

              }

     

           recvline[numbytes]='\0';

           printf("Server Message: %s\n",recvline);

     

           }

        printf("\nExit.\n");

        }

     

        char* getMessage(char* sendline,int len, FILE* fp)

        {

        printf("Input string to server:");

        return(fgets(sendline, MAXDATASIZE, fp));

        }


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值