高级IO

2018/12/18 服务器

高级IO

特殊的IO操作,包括文件锁、系统V的流、信号驱动的I/O、多路转I/O(select和pull函数)、readv和writev函数以及存贮映射I/O等概念和函数。

文件锁

文件锁是用户、多任务操作系统中非常重要的内容。文件锁机制为多个程序访问共享的文件提供重要的安全保护手段。

UNIX系统允许多个进程同时对一个文件进行读写操作,每一个read或write调用本身是原子的,但在两个系统调用之间并没有同步。

通过文件锁,进程在操作期间,可以锁定文件中敏感的部分以防止其他进程更改这部分中的数据。

文件锁根据其访问方式,分为读锁、写锁。

  • 读锁目的防止其他进程更改文件。在一个文件中设置读锁,其他进程仍可在相同部分设置读锁,但不允许设置写锁。故读锁也称为共享锁或共享读锁。
  • 写锁隔离文件使它所写的内容不受其他进程干扰。在一个文件上设置写锁,不允许其他进程在相同部分再设置任何类型的锁。故写锁称为互斥锁或互斥写锁。

fcntl文件锁操作

文件锁操作通过fcntl函数来完成。

image

该函数对文件描述字fields进行操作,具体操作取决于cmd参数。当fcntl函数用于文件锁时,cmd参数使用F_GETLK、F_SETLK和F_SETLKW3个专用命令。

为了防止一把锁,程序猿需要制定锁的类型、锁在文件中开始的字节偏移以及从哪一点字节开始的字节数(管辖区域)。故,fcntl必须提供第三个参数。

image

image

文件锁类型:共享读锁、互斥写锁。

共享锁和互斥锁的竞争规则:

image

fcntl 三种锁命令

  • F_GETLK 获取已打开文件描述字fileds给定文件的锁信息,它并不企图锁定该文件,只是查询在该文件的某个区域是否存在妨碍指定锁被创建的其他锁。锁区域由flock所指结构的l_whence、l_start和l_len成员给定,在这个区域可能会有多个锁影响心锁的创建,但fcntl函数只返回其一。 正常返回值是大于等于0的常数,失败返回-1,原因可能是参数非法。
  • F_SETLK 企图在已打开的文件描述字fields上设置有flock锁描述的锁。
  • F_SETLKW F_SETLK的阻塞版本,也同F_SETLK一样设置或清除一个锁,所不同的是它导致阻塞直至请求能够被完成为止。

锁的请求、释放和测试

在使用锁机制时,常用的操作有请求、释放及测试锁,必须首先形成适当的flock结构,然后调用fcntl函数来实际完成动作。

image

文件锁的作用是防止多个进程读写同一个文件时出现数据不一致的情形。应用一般在文件中有可能被多个进程修改的部分设置文件锁,每当读写这部分数据时通过显式的置锁和解锁来保证所读数据的完整性。

文件锁和进程与文件的关系

1) 锁是与进程相连的,这意味着当进程终止时,它的所有锁都被释放,并且锁决不会被fork创建的子进程所继承。 如果进程在获得一把锁之后调用fork,对于fork所继承的任何文件描述字而言,子进程都必须调用fcntl获得自己的锁。这样做的作用就是防止多个进程在同一时刻写同一文件,如果子进程由fork继承锁,则父子进程能同时写一个文件(违背锁的初衷)。

锁虽然不能被子进程继承,但可以被exec执行的新程序所继承,因为exec不派生进程。

2) 锁同时也与文件相连,这意味着当进程关闭一个文件描述字时,由此描述字所引用的文件上关于此进程的任何锁都被释放,即使这些锁是用另外的仍然打开的文件描述字所建立的。

内核不能区分(也不关心)父进程的锁是哪一个描述字获得的,故关闭一个文件描述字同时也释放同一文件其他描述字获得的锁的原因。

死锁

两个进程相互等待别另一个进程锁住的资源时,产生死锁。

当检测到死锁时,UNIX内核会选择让一个进程错误返回。内核并不能检测到所有死锁情形,一般只有在死锁等待处于系统内部时,内核才会检测到死锁并自动解开它。如果死锁发生在用户的程序代码中,内核则不一定检测到。

程序猿必须防止死锁的情形,避免死锁。

建议锁和强制锁

用户所根据保护机制分为建议锁和强制锁。

  • 建议锁 前面所述都是建议锁,建议锁简单由fcntl函数提供。建议锁是用户进程资源执行的机制,用户进程可以设置锁,但只有当协同工作的进程自愿的明确查看此锁时才对文件有保护作用,内核并不对建议锁作内部的强制保护或检查。

  • 强制锁 在强制锁机制下,用户进程可以设置锁,但内核会自动拒绝与锁有冲突的所有操作。如果对设置了吊证组ID位的文件使用fcntl锁机制,则称这种锁为强制锁。

从锁的实现机制来看,建议锁和强制锁二者唯一不同的是,对于强制锁,系统对该文件上的每一个I/O操作强制执行记录锁机制。也就是锁,若调用read或write,并且在文件上施加了强制锁,则这两个系统调用会首先根据在文件上设置的锁类型确定进程是否能够访问该文件。 因此,用强制锁,系统在执行I/O操作时根据锁类型控制着已锁记录的访问 ;用建议锁,访问是由协同进程在用户态下使用fcnlt操作来控制的。

流 目的主要是为实现网络服务和其他基于字符的输入输出提供一种莫模块化的、统一的机制。现在几乎所有现代UNIX系统都支持流。

流概貌

笼统说,流时系统调用、内核资源以及用于创建、使用和解除一个流的内核例程的集合,它也是书写设备驱动程序的一种基本结构,它为设备驱动程序的书写者指明了一组规程和准则,并提供了以模块方式开发这种驱动程序的机制和例程。

流的主要特征:

典型的流时用户进程和打开的内核设备或伪设备之间的一个全双工数据处理和传送路径。每一个流由一个流头,一个设备驱动程序,以及它们之间的o至多个模块组成。

流示意图:

image

流头提供内核地址看见与用户地址空间之间的接口,它允许应用程序通过系统调用访问流。设备驱动程序与设备本身通信。 设备驱动程序从流接受消息并转换他们为适合设备硬件需要的形式;它也按相反的方向从设备硬件获取消息并构造成消息向上发送给流头。位于流中间的模块对消息进行中间处理。

  • 消息

沿着流传送的所有数据都封装在流消息中。开始于流头的消息往设备驱动程序方向传送,成为往下流动;反方向传送的消息则成为向上流动。

  • 模块 位于流中间的模块在数据通过流传送时完成对数据的处理操作。

模块可以在运行时由用户进程动态的压入至流中或从流中弹出(先进先出)。

每一个模块(包含流头和驱动程序)有一个读队列和一个写队列,等待被传送的消息放置在模块的队列中。写队列从应用程序传送消息,即往下流动消息;读队列则往上流动消息,即从驱动程序流往应用。

  • 流控制

流的控制机制是可选的,只有当模块编写成关心其消息流量时他才工作。

  • 多路器

中间的模块一个位于另一个之下而组成一种竖直排列,但是这种线性排列不能适应所有的应用。

下图中,IP模块的消息可来自于UDP模块,也可来自于TCP模块。这种上面或下面有多个连接的模块称为多路器。x25模块根据消息包的目的地驱动多个设备,这是一个下多路器的列子。

image

流的优点: 每一个模块可以独立的、甚至由不同的人来编写,模块可以用不同的方法混合和匹配,有点像用UNIX shell的管道组合各种命令。因此流所提供的环境很容易写模块化的和可重复用的代码,并且代码简单,程序需要的许多支持函数都由流提供。

建立流连接首先要打开流设备驱动程序,列如,打开/dev/tcp。当打开一个基于流的设备时,便创建了一个包含连个模块的流:流头和驱动程序。然后程序必需按正确的顺序将新的模块依次压入到流中,流就像一个往下压入的栈一样。

一旦流已经设置,数据就能通过read、write、putmsg、putpmsg、getmsg、getpmsg等系统调用传送。当进程调用write、putmsg、putpmsg或ioctl时没消息从流往下传送,而read、getmsg则从流接收数据并传送给进程。要送往流下端设备的数据被打成包,然后往下流动。而来自设备的信号和数据则由设备驱动程序将它们组成消息并网上传送给流头。

流消息类型和优先级

流定义了一组消息类型,并且每一个消息都必须属于其中的某种类型。消息类型关系到消息的用途和排队的优先级,它允许模块识别特定的消息请求而不必关心消息的内容。

消息类型:

image

从用途上分,消息类型归为三类:

  • 包含实际输入输出的数据消息,如M_DATA(用户的I/O数据);
  • 包含流模块和底层实现指令的控制数据的消息,如M_PROTO(协议控制信息);
  • 包含文件描述字的其他消息,如M_PASSFP(高优先协议控制信息)。

根据流消息的类型,流将消息分为普通消息和高优先消息。高优先消息总是排在队列的前面并且先于普通消息被处理,它不受流的影响。

流的高优先消息规定的是与流消息系统本身相关的优先级,流系统能够识别它们。然而现实中还有一些消息对于流系统而言是普通消息,但对特定的协议而言则需要优先处理。如,很多网络协议支持外带数据的表示,它们由紧急的、协议专用的控制信息组成,这些消息必须优先于普通数据而被处理,并且不同于由消息类型所识别的高优先消息,故带外数据也称为畅通数据。

为支持协议有关的优先消息,流提供了一种称为优先波段的特征,它允许模块将消息分为若干波段并按波段值的大小优先处理它们。

故,按用户角度来看,消息分为3中:

  • 高优先消息;
  • 优先波段消息(即波段值为1~255的消息);
  • 普通消息(即波段值为0的消息)

它们在队列中的排队顺序:

image

访问流

进程使用流头提供的标准函数 open、close、read、write以及ioctl、pipe、putmsg、putpmsg、getmsg、getpmsg或poll访问基于流的文件。

访问流之前需要用open函数打开它。如何知道一个文件与流设备相连? 流文件通常驻存在目录/dev下,所有的流设备都是字符特别文件,但单纯从文件的名字上不能区别一个文件是否是流文件。

调用函数isastream:

image 调用函数isastream 测试一个打开的文件描述字fildes是否与基于流的文件相连。返回值1表示fildes是流文件,0不是。调用出错返回-1并设置errno。

一旦确定打开的文件是流文件后,进程就可以开始读写消息。

流消息由三部分组成:消息头、可选的控制消息、可选的数据。消息头是模块之间使用的消息,进程不能访问他们。

getmsg和putmsg 函数

getmsg从流中读取消息,putmsg往流中写消息。

image

geptmsg和putpmsg 函数

geptmsg和putpmsg函数具有getmsg和putmsg的功能,还可以读写消息的优先波段。

image

流操作控制函数ioctl

对于非流文件,ioctl执行的功能是设备特定的控制功能,它的请求和参数传送给指定的文件描述字并有设备驱动程序来解释。对于流文件,ioctl执行对流设备的各种控制操作。

# include<stropts.h>
int ioctl(int fileds, int request,../* args*/);

参数fileds是已打开的文件描述字,它引用一个流设备。参数request、arg被传送给fileds指定的流文件并由流头进行解释。这些参数的组合可以传给流的中间件或设备驱动程序。

ioctl命令:

image

信号驱动I/O

非阻塞I/O不阻塞进程,但为了知道描述字上面是否有可读的数据,进程必须采用轮询的方法不断调用read函数。采用信号驱动I/O的方法可以避免这种浪费时间的轮询。

采用信号驱动I/O方式,当在描述字上有数据到达时进程会收到一个信号,此时对该描述字进行输入输出操作将肯定不会被阻塞。这样进程可以在确定数据到达了的情况下才开始具体的I/O调用,而利用数据未达到的这段时间继续进行其他的工作。正因为如此,习惯上也称信号驱动的I/O方式为异步I/O。

SVR4的信号驱动I/0方式

SVR4中,信号驱动的I/O是流系统的一部分,它只适用于流设备,所使用的信号是SIGPOLL。

为了对流设备采用信号驱动的I/O形式,进程必须调用ioctl指明生成SIGPOLL信号的文件描述字以及生成信号的条件。

ioctl( fileds , L_SEISIG , arg );

image

除了调用ioctl指明生成SIGPOLL信号的条件之外,进程应当建立该信号的句柄。因为SIGPOLL信号的默认动作是终止进程,因此,应在调用ioctl函数之前调用sigaction建立信号句柄。

4.4BSD的信号驱动I/O方式

4.4BSD有两个信号:SIGIO 和 SIGURG,SIGURG信号只用于通知进程在网络连接上到达的带外数据。

在4.4BSD中,实现信号驱动I/O的步骤归纳为: 1)调用sigaction建立信号句柄; 2)调用fcntl函数用于F_SETOWN命令设置接受信号的进程或进程组; 3)如果要接受的信号是SIGIO,则必须调用fcntl函数用F_SETFL命令设置文件描述字的O_ASYNC标志使其生成SIGIO信号。

Search

    Table of Contents