I/0模型

     UNIX下有5种可用的I/0模型:
     •阻塞式I/0;
     •非阻塞式I/0;
     •I/0复用(select和poll);
     •信号驱动式I/0(SIGIO);
     •异步I/0(POSIX的aio_系列函数)。
     一个输入操作通常包括两个不同的阶段:
     (1)等待数据准备好;
     (2)从内核向进程复制数据。
     对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区(套接字接收缓冲区)。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
1 阻塞式I/0模型
     最流行的I/0模型是阻塞式I/0(blocking I/0)模型。默认情况下,所有套接字都是阻塞的。以数据报套接字作为例子,我们有如图1所示的情形。
                                        图1  阻塞式I/0模型
     我们使用UDP而不是TCP作为例子的原因在于就UDP而言,数据准备好读取的概念比较简单:要么整个数据报已经收到,要么还没有。然而对于TCP来说,诸如套接字低水位标记(low-water-mark)等额外变量起作用,导致这个概念变得复杂。
     在图1中,进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断。我们说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。recvfrom成功返回后,应用进程开始处理数据报。
2 非阻塞式I/0模型
     进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/0操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。图2概要展示了非阻塞式I/0(nonblocking I/0)模型。
                                        图2   非阻塞式I/0模型
     前三次调用recvfrom时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据包准备好,被复制到应用进程缓冲区,于是recvfrom成功返回。我们接着处理数据。
     当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量CPU时间,不过这种模型偶尔也会遇到,通常是在专门提供某一种功能的系统中才有。
3 I/0复用模型
     有了I/0复用(I/0 multiplexing),我们就可以调用select或poll,阻塞在这两个系统调用中的某个之上,而不是阻塞在真正的I/0系统调用上。图3概括展示了I/0复用模型。
                                        图3 I/0复用模型
     我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。
     比较图3和图1,I/0复用并不显得有什么优势,事实上由于使用select需要两个而不是单个系统调用,I/0复用还稍有劣势。不过,使用select的优势在于我们可以等待多个描述符就绪。
     与I/0复用密切相关的另一种I/0模型是在多线程中使用阻塞式I/0。这种模型与上述模型极为相似,但它没有使用select阻塞在多个文件描述符上,而是使用多线程(每个文件描述符一个线程),这样每个线程都可以自由地调用诸如recvfrom之类的阻塞式I/0系统调用了。
4 信号驱动式I/0模型
     我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动式I/0(signal-driven I/0),图4是它的概要展示。
                                        图4 信号驱动式I/0模型
     我们首先开启套接字的信号驱动式I/0功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。
     无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:即可以是数据已准备好被处理,也可以是数据报已准备好被读取。
5 异步I/0模型
     异步I/0(asynchronous I/0)由POSIX规范定义。演变成当前POSIX规范的各种早期标准所定义的实时函数中存在的差异已经取得一致。一般地说,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动式I/0是由内核通知我们何时可以启动一个I/0操作,而异步I/0模型是由内核通知我们I/0操作何时完成。图5给出了一个例子。
                                        图5 异步I/0模型
     我们调用aio_read函数,给内核传递描述符、缓冲区指针、缓冲区大小(与read相同的三个参数)和文件偏移(与lseek类似),并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待I/0完成期间,我们的进程不被阻塞。本例子中我们假设要求内核在操作完成时产生某个信号。该信号直到数据已复制到应用进程缓冲区才产生,这一点不同于信号驱动式I/0模型。
6 各种I/0模型的比较
     图6对比了上述5种不同的I/0模型。可以看出,前4种模型的主要区别在于第一阶段,因为它们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步I/0模型在这两个阶段都要处理,从而不同于其他4种模型。
                                        图6 5种I/0模型的比较
7 同步I/0和异步I/0对比
     POSIX把这两个术语定义如下:
     •同步I/0操作(synchronous I/0 operation)导致请求进程阻塞,直到I/0操作完成;
     •异步I/0操作(asynchronous I/0 operation)不导致请求进程阻塞。
     根据上述定义,我们的前4种模型--阻塞式I/0模型、非阻塞式I/0模型、I/0复用模型和信号驱动式I/0模型都是同步I/0模型,因为其中真正的I/0操作(recvfrom)将阻塞进程。只有异步I/0模型与POSIX定义的异步I/0相匹配。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于非阻塞I/O模型的服务器回射程序设计,可以使用以下步骤: 1. 创建 socket,并设置为非阻塞模式: ```c int listenfd = socket(AF_INET, SOCK_STREAM, 0); fcntl(listenfd, F_SETFL, O_NONBLOCK); ``` 2. 绑定 IP 地址和端口号: ```c struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(port); bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); ``` 3. 监听 socket: ```c listen(listenfd, SOMAXCONN); ``` 4. 创建一个事件循环,并注册监听 socket 的读事件: ```c int epfd = epoll_create(1); struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; ev.data.fd = listenfd; epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); struct epoll_event events[MAX_EVENTS]; ``` 5. 在事件循环中,处理监听 socket 的读事件和连接 socket 的读写事件: ```c while (1) { int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); for (int i = 0; i < nfds; i++) { if (events[i].data.fd == listenfd) { // 有新的连接 while (1) { struct sockaddr_in clientaddr; socklen_t clientaddrlen = sizeof(clientaddr); int connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientaddrlen); if (connfd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { break; } else { perror("accept"); break; } } fcntl(connfd, F_SETFL, O_NONBLOCK); ev.events = EPOLLIN | EPOLLET; ev.data.fd = connfd; epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); } } else { // 处理连接的读写事件 int connfd = events[i].data.fd; char buf[1024]; while (1) { int n = read(connfd, buf, sizeof(buf)); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { break; } else { perror("read"); epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, &ev); close(connfd); break; } } else if (n == 0) { epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, &ev); close(connfd); break; } else { write(connfd, buf, n); } } } } } ``` 在上面的代码中,使用 epoll 实现了非阻塞的事件监听,可以同时处理多个连接的读写事件。 总体而言,基于非阻塞 I/O 模型的服务器回射程序设计相对复杂一些,但可以实现高并发的连接处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值