63-ALTERNATIVE IO MODELS

本文介绍了Linux中处理I/O操作的多种方式,包括非阻塞I/O、I/O多路复用(select、poll、epoll)、信号驱动I/O以及边缘触发通知。重点讨论了epoll在处理大量文件描述符时的高性能,并比较了epoll与select、poll、信号驱动I/O的优缺点。epoll的level-triggered和edge-triggered两种通知模式也得到了探讨,强调了在使用边缘触发时防止文件描述符饥饿的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Please indicate the source: http://blog.youkuaiyun.com/gaoxiangnumber1

Welcome to my github: https://github.com/gaoxiangnumber1

63.1 Overview

  • Traditional blocking I/O model: A process performs I/O on one file descriptor at a time, and each I/O system call blocks until the data is transferred.
  • Disk files are a special case. The kernel employs the buffer cache to speed disk I/O requests.
    1. A write() to a disk returns as soon as the requested data has been transferred to the kernel buffer cache, rather than waiting until the data is written to disk(unless O_SYNC flag was specified when opening the file).
    2. A read() transfers data from the buffer cache to a user buffer, and if the required data is not in the buffer cache, then the kernel puts the process to sleep while a disk read is performed.
  • Some applications need to able to do one or both of the following:
    1. Check whether I/O is possible on a file descriptor without blocking if it is not possible.
    2. Monitor multiple file descriptors to see if I/O is possible on any of them.
  • Three techniques that partially address these needs: nonblocking I/O and the use of multiple processes or threads.
    1. If we place a file descriptor in nonblocking mode by enabling the O_NONBLOCK open file status flag, then an I/O system call that can’t be immediately completed returns an error instead of blocking. Nonblocking I/O can be employed with pipes, FIFOs, sockets, terminals, pseudo-terminals, and some other types of devices. Nonblocking I/O allows us to periodically check(“poll”) whether I/O is possible on a file descriptor.
    2. If we don’t want a process to block when performing I/O on a file descriptor, we can create a new process to perform the I/O. The parent process can then carry on to perform other tasks, while the child process blocks until the I/O is complete. If we need to handle I/O on multiple file descriptors, we can create one child for each descriptor. The problems are expense and complexity. Creating and maintaining processes places a load on the system, and the child processes will need to use some form of IPC to inform the parent about the status of I/O operations.
    3. Using multiple threads instead of processes is less demanding of resources, but the threads will probably still need to communicate information to one another about the status of I/O operations, and the programming can be complex, especially if we are using thread pools to minimize the number of threads used to handle large numbers of simultaneous clients.(One place where threads can be useful is if the application needs to call a third-party library that performs blocking I/O. An application can avoid blocking in this case by making the library call in a separate thread.)
  • Because of the limitations of both nonblocking I/O and the use of multiple threads or processes, one of the following alternatives is preferable:
    1. I/O multiplexing allows a process to simultaneously monitor multiple file descriptors to find out whether I/O is possible on any of them. select() and poll() perform I/O multiplexing.
    2. Signal-driven I/O is a technique whereby a process requests that the kernel send it a signal when input is available or data can be written on a specified file descriptor. The process can then carry on performing other activities, and is notified when I/O becomes possible via receipt of the signal. When monitoring large numbers of file descriptors, signal-driven I/O provides better performance than select() and poll().
    3. epoll is a Linux-specific feature.
      Like the I/O multiplexing APIs, epoll allows a process to monitor multiple file descriptors to see if I/O is possible on any of them.
      Like signal-driven I/O, epoll provides better performance when monitoring large numbers of file descriptors.
  • I/O multiplexing, signal-driven I/O, and epoll are all methods of achieving the same result: monitoring one or several file descriptors simultaneously to see if they are ready to perform I/O(to be precise, to see whether an I/O system call could be performed without blocking). The transition of a file descriptor into a ready state is triggered by some type of I/O event(the arrival of input, the completion of a socket connection and so on). None of these techniques performs I/O. They merely tell us that a file descriptor is ready.
  • One I/O model that we don’t describe in this chapter is POSIX asynchronous I/O(AIO). POSIX AIO allows a process to queue an I/O operation to a file and then later be notified when the operation is complete.
    Advantage: The initial I/O call returns immediately, so that the process is not tied up waiting for data to be transferred to the kernel or for the operation to complete. This allows the process to perform other tasks in parallel with the I/O(which may include queuing further I/O requests).

Which technique?

  • select() and poll() are standard interfaces that have been present on UNIX for many years.
    1. Advantage: portability.
    2. Disadvantage: they don’t scale well when monitoring large numbers(hundreds or thousands) of file descriptors.
  • Advantage of epoll: it allows an application to efficiently monitor large numbers of file descriptors.
    Disadvantage: it is only on Linux.
  • Signal-driven I/O allows an application to efficiently monitor large numbers of file descriptors. But epoll provides advantages over signal-driven I/O:
    1. Avoid the complexities of dealing with signals.
    2. Ability to specify the kind of monitoring that we want to perform(e.g., ready for reading/writing).
    3. Ability to select either level-triggered or edge-triggered notification(Section 63.1.1).
  • select() and poll() are more portable, while signal-driven I/O and epoll deliver better performance. For some applications, it is worthwhile writing an abstract software layer for monitoring file descriptor events. With such a layer, portable programs can employ epoll on Linux, and fall back to the use of select() or poll() on other systems.
  • libevent is a software layer that provides an abstraction for monitoring file descriptor events. It can employ any of the techniques: select(), poll(), signal-driven I/O, or epoll, as well as the Solaris specific /dev/poll interface or the BSD kqueue interface.

63.1.1 Level-Triggered and Edge-Triggered Notification

  • Level-triggered notification: A file descriptor is considered to be ready if it is possible to perform an I/O system call without blocking.
  • Edge-triggered notification: Notification is provided if there is I/O activity(e.g., new input) on a file descriptor since it was last monitored.
  • epoll can employ both level-triggered notification(the default) and edge-triggered notification.

How different notification model affects the way we design a program?

  • When we employ level-triggered notification, we can check the readiness of a file descriptor at any time. This means that when we determine that a file descriptor is ready(e.g., it has input available), we can perform I/O on the descriptor, and then repeat the monitoring operation to check if the descriptor is still ready(e.g., it still has more input available), in which case we can perform more I/O, and so on.
    Because the level-triggered model allows us to repeat the I/O monitoring operation at any time, it is not necessary to perform as much I/O as possible(e.g., read as many bytes as possible) on the file descriptor(or even perform any I/O at all) each time we are notified that a file descriptor is ready.
  • When we employ edge-triggered notification, we receive notification only when an I/O event occurs. We don’t receive any further notification until another I/O event occurs. Furthermore, when an I/O event is notified for a file descriptor, we usually don’t know how much I/O is possible(e.g., how many bytes are available for reading). Therefore, programs that employ edge-triggered notification are usually designed according to the following rules:
    1. After notification of an I/O event, the program should(at some point) perform as much I/O as possible(e.g., read as many bytes as possible) on the corresponding file descriptor. If the program fails to do this, then it might miss the opportunity to perform some I/O, because it would not be aware of the need to operate on the file descriptor until another I/O event occurred. This could lead to spurious data loss or blockages in a program.
      We said “at some point” because sometimes it may not be desirable to perform all of the I/O immediately after we determine that the file descriptor is ready. The problem is that we may starve other file descriptors of attention if we perform a large amount of I/O on one file descriptor(Section 63.4.6).
    2. If the program employs a loop to perform as much I/O as possible on the file descriptor, and the descriptor is marked as blocking, then eventually an I/O system call will block when no more I/O is possible. For this reason, each monitored file descriptor is normally placed in nonblocking mode, and after notification of an I/O event, I/O operations are performed repeatedly until the relevant system call(e.g., read() or write()) fails with the error EAGAIN or EWOULDBLOCK.

63.1.2 Employing Nonblocking I/O with Alternative I/O Models

  • Nonblocking I/O(the O_NONBLOCK flag) is often used in conjunction with the I/O models described in this chapter. Examples of why this can be useful are:
    1. As explained in the previous section, nonblocking I/O is usually employed in conjunction with I/O models that provide edge-triggered notification of I/O events.
    2. If multiple processes(or threads) are performing I/O on the same open file descriptions, then, from a particular process’s point of view, a descriptor’s readiness may change between the time the descriptor was notified as being ready and the time of the subsequent I/O call. Consequently, a blocking I/O call could block, thus preventing the process from monitoring other file descriptors.(This can occur for all of the I/O models that we describe in this chapter, regardless of whether they employ level-triggered or edge-triggered notification.)
    3. Even after a level-triggered API such as select() or poll() informs us that a file descriptor for a stream socket is ready for writing, if we write a large enough block of data in a single write() or send(), then the call will nevertheless block.
    4. In rare cases, level-triggered APIs such as select() and poll() can return spurious readiness notifications—they can falsely inform us that a file descriptor is ready. This could be caused by a kernel bug or be expected behavior in an uncommon scenario.
  • Section 16.6 of UNP describes one example of spurious readiness notifications on BSD systems for a listening socket. If a client connects to a server’s listening socket and then resets the connection, a select() performed by the server between these two events will indicate the listening socket as being readable, but a subsequent accept() that is performed after the client’s reset will block.

63.2 I/O Multiplexing

  • I/O multiplexing allows us to simultaneously monitor multiple file descriptors to see if I/O is possible on any of them. We can perform I/O multiplexing using select()/ poll() to monitor file descriptors for regular files, terminals, pseudo-terminals, pipes, FIFOs, sockets, and some types of character devices.

63.2.1 The select() System Call

#include <sys/time.h>
#include <sys/select.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,                                                                      struct timeval *timeout);
Return: number of ready file descriptors, 0 on timeout, -1 on error
  • nfds, readfds, writefds, and exceptfds arguments specify the file descriptors that select() is to monitor.
  • timeout can be used to set an upper limit on the time for which select() will block.

File descriptor sets

  • readfds, writefds, and exceptfds are pointers to file descriptor sets that use the data type fd_set. These arguments are used as follows:
    1. readfds is the set of file descriptors to be tested to see if input is possible;
    2. writefds is the set of file descriptors to be tested to see if output is possible;
    3. exceptfds is the set of file descriptors to be tested to see if an exceptional condition has occurred. An exceptional condition occurs in just two circumstances on Linux:
      -1- A state change occurs on a pseudo-terminal slave connected to a master that is in packet mode(Section 64.5).
      -2- Out-of-band data is received on a stream socket(Section 61.13.1).
  • fd_set data type is implemented as a bit mask. All manipulation of file descriptor sets is done via four macros: FD_ZERO(), FD_SET(), FD_CLR(), and FD_ISSET().
#include <sys/time.h>
#include <sys/select.h>
#include <sys/types.h>
#include <unistd.h>
void FD_ZERO(fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);

int FD_ISSET(int fd, fd_set *fdset);
Return: true(1) if fd is in fdset, or false(0) otherwise
  • FD_ZERO() initializes the set pointed to by fdset to be empty.
    FD_SET() adds the file descriptor fd to the set pointed to by fdset.
    FD_CLR() removes the file descriptor fd from the set pointed to by fdset.
    FD_ISSET() returns true if the file descriptor fd is a member of the set pointed to by fdset.
  • A file descriptor set has a maximum size FD_SETSIZE, which is 1024 on Linux. If we want to change this limit, we must modify the definition in the glibc header files. If we need to monitor large numbers of descriptors, then using epoll is preferable to the use of select().
  • readfds, writefds, and exceptfds are all value-result. Before the call to select(), the fd_set structures pointed to by these arguments must be initialized(using FD_ZERO() and FD_SET()) to contain the set of file descriptors of interest. select() modifies each of these structures and on return, they contain the set of file descriptors that are ready. The structures can then be examined using FD_ISSET().
  • If we are not interested in a particular class of events, then the corresponding fd_set argument can be specified as NULL.
  • nfds is set one greater than the highest file descriptor number included in any of the three file descriptor sets. This argument allows select() to be efficient since the kernel knows not to check whether file descriptor numbers higher than this value are part of each file descriptor set.

The timeout argument

  • timeout can be specified as
    1. NULL: select() blocks indefinitely;
    2. A pointer to a timeval structure.
struct timeval
{
    time_t      tv_sec; /* Seconds */
    suseconds_t tv_usec;    /* Microseconds(long int) */
};
  • If both fields of timeout are 0, then select() doesn’t block; it polls the specified file descriptors to see which ones are ready and returns immediately. Otherwise, timeout specifies an upper limit on the time for which select() is to wait.
  • Although the timeval structure affords microsecond precision, the accuracy of the call is limited by the granularity of the software clock(Section 10.6).
  • When timeout is NULL, or points to a structure containing nonzero fields, select() blocks until one of the following occurs:
    1. at least one of the file descriptors specified in readfds, writefds, or exceptfds becomes ready;
    2. the call is interrupted by a signal handler;
    3. the amount of time specified by timeout has passed.
  • On Linux, if select() returns because one or more file descriptors became ready, and if timeout was non-NULL, then select() updates the structure to which timeout points to indicate how much time remained until the call would have timed out. Most other UNIX systems don’t modify this structure. Portable applications that employ select() within a loop should always ensure that the structure pointed to by timeout is initialized before each select(), and should ignore the information returned in the structure after the call.
  • On Linux, if select() is interrupted by a signal handler(so that it fails with the error EINTR), then the structure is
SQL注入的原理 什么SQL注入 将SQL代码插入到应用程序的输入参数中,之后,SQL代码被传递到数据库执行。从而达到对应用程序的攻击目的。 注入原理 常见攻击方法 检测是否可以注入【检测注入点】 示例:http://192.168.0.1/temp.aspx?id=a or 1=1-- 如果上面语句可以执行说明可以实现注入,则可以 利用系统过程、系统表注入数据库 示例【给系统添加一个用户,黑客可以实现远程登录控制服务器】:http://192.168.0.1/temp.aspx?id=1;exec xp_cmdshell 'net user admin 123456 /add' 绕过程序的限制 示例:程序中往往限制单引号的数据,但是黑客传入单引号的ASCII码 跨站点注入 在Web页面挂某些恶意的HTML、JavaScript代码 防范SQL注入 限制错误信息的输出,避免错误信息的输出使得黑客知道网站的技术实现采用什么数据库,采用什么平台 示例:在Web.config文件中设置 限制访问数据库账号的权限 在开发应用系统的时候就应该限制,给程序最小访问数据库的权限 使用参数命令传递参数 不要使用拼接字符串的方式构造SQL语句而采用参数命令 使用存储过程 存储过程在数据库中 只能执行存储过程中固定的代码 限制输入长度 防止黑客输入超大字符串,导致服务器瘫痪 防止黑客输入较长的恶意脚本等 实现方法:文本框的MaxLength属性 URL重写技术 示例: http://testWeb/news.aspx?id=111 重写成:http://testWeb/10101111.html 传递参数尽量不用字符串 http://testWeb/news.aspx?id=111 and 1=1【黑色部分给了SQL注入的机会】 SQL优化 为什么要优化 开发是对性能考虑不多【技术差、项目工期紧等原因没有考虑性能问题】 系统运行中,数据量扩大,访问量增多,蹩脚的SQL危害开始显露 低效SQL的危害 系统响应变慢,软件开发中的8秒定律,当打开一个软件或网页超过8秒时间还没有显示,则响应太慢。 死锁,当不同用户都访问某些资源的时候SQL语句不当导致死锁 客户失去信心,软件失败 低效SQL低效在哪里?  性能低下的根源  硬件原因,数据库的配置不合理,数据库的数据文件和日志文件没有分磁盘存放,会极大影响IO性能  没有建立索引,或者SQL没有走索引。在千万级数据的表上建索引是很有必要的。  SQL过于复杂,过长的SQL语句满足程序需求但是影响性能。子查询嵌套过多对性能有影响,查询关联的表特别多也影响性能  频繁访问数据等等 SQL如何被SQLServer执行的 SQL执行原理  解释:首先解释SQL语句【语法是否正确】  解析:检验语句的出现的对象是否有效【进行一个解析,要检查对象所有权的权限】  优化:【检查SQL语句是否能够使用索引,SQL语句如何执行效率高,制定一个执行计划】  编译:  执行SQL语句:【把编译过的查询要求提交并进行处理】 如何优化SQL 完善开发的管理 完善代码审核、测试机制,软件开发是艺术! 检测SQL查询的效率 语法【对IO和Time对SQL执行进行统计】: SET STATISTICS IO ON SET STATISTICS TIME ON ------------SQL代码--------- SET STATISTICS IO OFF SET STATISTICS TIME OFF 注意:在检测之前要清理缓存,因为当我们执行SQL语句的时候查出的数据会在数据库中进行缓存,重新查询会返回缓存中的信息。 DBCC DROPCLEANBUFFERS DBCC FREEPROCCACHE 经验:使用子查询嵌套不要过多,尽量使用表连接查询代替子查询,因为表连接查询效率稍高一点。 SQL优化工具 使用报表服务 通过Reporting Service查找低效SQL 选择 性能 - 批处理执行统计信息和性能相关的几个报表服务: 性能 - 对象执行统计信息 性能 - 按平均CPU时间排在前面的查询 性能 - 按平均IO次数排在前面的查询 性能 - 按总CPU时间排在前面的查询 性能 -IO总次数排在前面的查询 操作示范: 1. 数据引擎上点击右键—报表—标准报表—关心的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值