高级I/O

14 高级I/O

14.1 简介

高级I/O讨论的主要内容包括:非阻塞I/O、记录锁、I/O多路转接(select和poll函数)、异步I/O、readv和writev函数以及存储映射I/O(mmap)

14.2 非阻塞I/O

低速系统调用是可能会使进程永远阻塞的一类系统调用,其主要包括:

如果某些文件类型(如读管道、终端设备和网络设备)的数据并不存在,读操作可能会使调用者永远阻塞
如果数据不能被相同的文件类型立即接受(如管道中无空间,网络流控制),写操作可能会使调用者永远阻塞
在某种条件发生之前打开某些文件类型可能会发生阻塞(如果打开一个终端设备,需要先等待与之连接的调制解调器应答,又如若以只写模式打开FIFO,那么在没有其他进程已用读模式打开该FIFO时也要等待);
对已经加上强制性记录锁的文件进行读写;
某些ioctl操作;
某些进程间通信函数

非阻塞I/O是当发出open、read和write这样的I/O操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。

对于一个给定的描述符,有两种为其指定为非阻塞I/O的方法:

如果调用open获得描述符,则可指定O_NONBLOCK标志
对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志。

14.3 记录锁

记录锁(recording locking)的功能是:当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件。更合适的称呼为“字节范围锁(byte-range locking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。

共享读锁(L_RDLCK)和独占性写锁(L_WRLCK)的基本规则是:任意多个程序在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程有一把独占写锁。进一步,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性写锁,则不能再对它加任何读锁。

注意:上面的兼容适用于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求。如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加上一把锁,那么新锁将替换已有锁。

>当前区域:读锁:写锁
>无锁:允许:允许
>有一把或多把读锁:允许:拒绝
>有一把写锁:拒绝:拒绝

加读锁时,该描述符必须是读打开。加写锁时,该描述符必须是写打开。

死锁:如果两个进程相互等待对方持有并且不释放(锁定)的资源时,则这两个进程就处于死锁状态。如果一个进程已经控制了文件中的一个加锁区域,然后他又试图对另一个进程控制区域加锁,那么她就会休眠,这种情况下, 有发生死锁的可能性。

检测到死锁时,内核必须选择一个进程接收出错返回。从而释放锁,解开死锁。

锁的隐含继承和释放

关于记录锁的自动继承和释放主要有3条规则:

锁与进程和文件两者相关联。其含义是有两点:
当一个进程终止时,它所建立的锁全部释放
无论一个描述符何时关闭,该进程通过这一描述符引用的文件上的任何一把锁都会释放(这些锁都是该进程设置的)。
由fork产生的子进程不继承父进程所设置的锁。即若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程被视为另一个进程。对于fork从父进程处继承而来的描述符,子进程需要调用fcntl才能获得它自己的锁。这个约束是有道理的,因为锁的作用就是阻塞多个进程同时写同一个文件。如果子进程通过fork继承父进程的锁,则父进程和子进程就可以同时写同一个文件。
在执行exec后,新程序可以继承原执行程序的锁。但是注意,如果对一个文件描述符设置了执行时关闭标志,那么当作为exec的一部分关闭该文件描述符时,将释放相应文件的所有锁。

守护进程可用一把文件锁来保证只有该守护进程的唯一副本在运行。

在文件尾端加锁

在对相对于文件尾端的字节范围加锁或解锁时需要特别小心。

当对文件的一部分加锁时,内核将指定的偏移量变换成绝对文件偏移量。另外,除了指定一个绝对偏移量之外,fcntl还允许我们相对于文件中的某个点指定该偏移量,这个点是指当前偏移量或文件尾端。当前偏移量和文件尾端可能会不断变化,而这种变化又不应该影响现有锁的状态,所以内核必须独立于当前文件偏移量或文件尾端而记住锁。

建议性锁和强制性锁

考虑数据库访问例程库。如果该库中所有函数都以一致的方法处理记录锁,则称使用这些函数访问数据库的进程集合为合作进程。*如果这些函数是唯一地用来访问数据库的函数,那么它们使用建议性锁是可行的。*但是建议性锁并不能阻止对数据库文件有写权限的任何其它进程写这个数据库文件。不使用数据库访问例程库协同一致的方法来访问数据库的进程是非合作进程。

强制性锁会让内核检查每一个open、read和write,验证调用进程是否违背了正在访问的文件上的某一把锁。强制性锁有时也称为强迫方式锁。

如果一个进程试图读(read)或写(write)一个强制性锁起作用的文件,而欲读、写的部分又由其他进程加上了锁,此时会发生什么?对这一问题的回答取决于3方面的因素:操作类型(read或write)、其他进程持有的锁的类型(读锁或写锁)以及read或write的描述符是阻塞还是非阻塞的

其他进程在该区域上持有的现有锁的类型/阻塞描述符/非阻塞描述符

        Read   Wirte  Read       Write
读锁   允许 阻塞  允许     EAGAIN
写锁  阻塞  阻塞  EAGAIN    EAGAIN

14.4 I/O多路转接

函数select和pselect

在所有POSIX兼容的平台上,select函数使我们可以执行I/O多路转接。传给select的参数告诉内核:

我们所关心的描述符
对于每个描述符我们所关心的条件(是否想从一个给定的描述符读,是否想写一个给定的描述符,是否关心一个给定描述符的异常条件)
愿意等待多长时间(可以永远等待,等待一个固定的时间或者根本不等待)

从select返回时,内核告诉我们:

已准备好的描述符的总数量
对于读、写或异常这3个条件中的每一个,哪些描述符已准备好

使用这种返回信息,就可调用相应的I/O函数(一般是read或write),并且确知该函数不会阻塞。

Select返回值有三种情况:

返回值-1表示出错。这是可能发生的,如在所指定的描述符一个都没准备好时捕捉到一个信号。在此种情况下,一个描述符集都不修改。
返回值0表示没有描述符准备好。若指定的描述符一个都没有准备好,指定的时间就过了,那么就会发生这种情况。此时,所有描述符集都会置0。
一个正返回值说明了已经准备好的描述符数。该值时3个描述符集中已准备好的描述符之和,所以如果同一描述符已经准备好读和写,那么在返回中会对其计两次数。在这种情况下,3个描述符集中仍旧打开的位对应于已准备好的描述符。

对于“准备好”的含义具体包括以下情况:

若对读集(readfds)中的一个描述符进行的read操作不会阻塞,则认为此描述符是准备好的。
若对写集(writefds)中的一个描述符进行的write操作不会阻塞,则认为此描述符是准备好的
若对异常条件集(exceptfds)中的一个描述符有一个未觉异常条件,则认为此描述符是准备好的。现在,异常条件包括:在网络连接上到达带外的数据,或者在处于数据包模式的伪终端上发生了某些条件。
对于读、写和异常条件,普通文件的文件描述符总是返回准备好

一个描述符阻塞与否并不影响select是否阻塞。即,如果希望读一个非阻塞描述符,并且以超时值为5秒调用select,则select做多阻塞5s。如果指定一个无限的超时值,则在该描述符数据准备好,或捕获到一个信号之前,select会一直阻塞。

如果在一个描述符上碰到了文件尾端,则select会认为该描述符是可读的。然后调用read,它返回0,这是Unix系统指示到达文件尾端的方法。(不要错误的认为,当到达文件尾端时,select会指示一个异常条件。

pselect是select的一个变体,除以下几点外,pselect和select相同。

Select的超时值用timeval结构指定,但pselect使用timespec结构。Timespec结构以秒和纳秒表示超时值,而非秒和微妙。如果平台支持这样的时间精度,那么timespec就能提供更精准的超时时间。
Pselect超时值被声明为const,这保证了调用pselect不会改变此值。
Pselect可使用选信号屏蔽字。若sigmask为NULL,那么与写信号有关的方法,pselect的运行状况和select先沟通。否则,sigmask指向一信号屏蔽字,在调用pselect时,以原子操作的方式安装该信号屏蔽字。在返回时,恢复以前的信号屏蔽字。

函数poll

Poll函数类似于select,但是程序员接口有所不同。虽然poll函数是System V引入进来支持STREAMS子系统的,但是poll函数可用于任何类型的文件描述符。

与select不同,poll不是为每个条件(可读性,可写性,异常条件)构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感性却的条件

与select一样,一个描述符是否阻塞不会影响poll是否阻塞。

Select和poll都是可中断的。

14.5 异步I/O

在POSIX异步I/O接口,会带来以下麻烦:

每个异步操作有3处可能差生错误的地方:一处在操作系统提交的部分,一处是在操作本身的结果,还有一处在用于决定异步操作状态的函数中。
与POSIX异步I/O接口的传统方法相比,他们本身设计大量额外设置和处理规则。
从错误中恢复可能会比较困难。例如,提交了多个异步写操作,其中一个失败了,下一步如何做?如果这些写操作是相关的,则可能还需要撤销所有成功的写操作。

System V异步I/O

在System V中,异步I/O是STREAMS系统的一部分,它只对STREAMS设备和STREAMS管道起作用。System V的异步I/O信号是SIGPOLL。

BSD异步I/O

在BSD派生的系统中,异步I/O是信号SIGIO和SIGURG的组合。SIGIO是通过异步I/O信号,SIGURG则只用来通知进程网络连接上的带来数据已经到达。

POSIX异步I/O

POSIX异步I/O接口为对不同类型的文件进行异步I/O提供了一套一致的方法。这些异步I/O接口使用AIO控制块来描述I/O操作。

14.6 函数readv和writev

Readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散步读(scatter read)和聚集写(gather write)

应当尽量少的系统调用来完成任务。如果只写少量的数据,将会发现自己复制数据然后使用一次write会比用writev更合算。但也可能发现,管理自己的分段缓冲区会增加程序额外的复杂性成本,故从性能成本角度来看不合法。

14.7 函数readn和writen

管道、FIFO以及某些设备(特别是终端和网络)有下列两种性质:

一次read操作所返回的数据可能少于所要求的数据,即使还没达到文件尾端也可能是这样的。这不是一个错误,应当继续读该设备。
一次write操作的返回值也可能少于指定输出的字节数。这可能是由某个因素造成的,如内核输出缓冲区变满。这也不是一个错误,应该继续写余下的数据。(通常,只有非阻塞描述符,或捕获到一个信号时,才发生这种write的中途返回。)

在读、写磁盘文件时从未见到过这种情况,除非文件系统用完了空间,或者接近了配额限制,不能将要求写的数据全部写出。

通常,在读、写一个管道、网络设备或终端时,需要考虑这些特性。

14.8 存储映射I/O

存储映射I/O(memory-mapped I/O)能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中取数据时,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区时,相应字节就自动写入文件。这样,就可以在不使用read和write的情况下执行I/O。

Mmap和memcpy与read和write的区别?

二者的主要区别在于,与mmap和memcpy相比,read和write执行了更多的系统调用,并做了更多的复制。*Read和write将数据从内核缓冲区中复制到应用缓冲区(read),然后再把数据从应用缓冲区复制到内核缓冲区(write)。而mmap和memcpy则直接把数据从映射到地址空间的一个内核缓冲区复制到另一个内核缓冲区。*当引用尚不存在内存页时,这样的复制过程就会作为处理页错误的结果而出现(每次错页读发生一次错误,每次错页写发生一次错误)。如果系统调用和额外的复制操作的开销和页错误的开销不同,那么这两种方法中就会有一种比另一种表现更好。

在Linx 3.2.0中,相对于运行时间,两种版本的程序在时钟时间上显示出了巨大的差异:使用read和write的版本完成任务比使用mmap和memcpy的版本快了4倍。然而在Solaris 10中,使用mmap和memcpy的版本比使用read和write的版本要快。既然二者的CPU时间几乎是相同的,为何在时钟时间差异却如此之大呢?一种可能是,在一种版本中需要较长时间来等待I/O完成。这个等待时间并没有计算在CPU的处理时间中。另一种可能是,某些系统处理的时间可能并没有在程序中计算,比如系统守护进程把页的操作写到磁盘中的操作是随机的而非连续的,那么它们写入磁盘所需的时间会更长,因此在也可以被用来复用之前所需等待的时间也会更长

有的系统将一个普通文件复制到另一个普通文件中,存储映射I/O可能会比较快。但是有一些限制,如不能使用这种技术在某些设备之间(如网络设备或终端设备)进行复制,并且在对被复制的文件进行映射后,也要注意该文件的长度是否改变。尽管如此,某些应用程序仍然能得益于存储映射I/O,因为它处理的是存储空间而不是读、写一个文件,所以常常可以简化算法。从存储映射I/O中得益的一个例子是对帧缓冲设备的操作,该设备引用位图显示。

参考文献:Unix环境高级编程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值