IO系统.docx
《IO系统.docx》由会员分享,可在线阅读,更多相关《IO系统.docx(25页珍藏版)》请在冰点文库上搜索。
![IO系统.docx](https://file1.bingdoc.com/fileroot1/2023-5/1/9fa5bd70-3cdb-43cd-a904-f4511b730c8d/9fa5bd70-3cdb-43cd-a904-f4511b730c8d1.gif)
IO系统
I/O系统
第1节I/O的基本概念1
第2节磁盘结构1
2.1磁盘布局1
2.2影响磁盘读写速度的因素3
2.3磁盘的低级格式化3
第3节磁盘I/O4
3.1BIO结构4
3.2前置合并(frontmerge)和后置合并(backmerge)4
第4节调度算法4
4.1noop调度器5
4.2deadline调度器5
4.2.1设计和实现5
4.2.2调优参数6
4.3as6
4.4cfq7
4.4.1简要原理和工作流程7
4.4.2调优参数7
4.4.3ionice命令8
4.5I/O调度方法的查看与设置8
第5节I/O编程模型8
5.1同步和异步8
5.2阻塞和非阻塞8
5.3同步阻塞I/O9
5.4同步非阻塞I/O9
5.5异步阻塞I/O11
5.5.1select11
5.5.2epoll12
5.5.3edge-triggeredandlevel-triggered14
5.6异步非阻塞I/O14
5.7各种实现方法的优缺点15
第1节I/O的基本概念
与外设的通讯通常称之为输入输出,简称I/O。
第2节磁盘结构
2.1磁盘布局
每个磁道有几百个扇区,每个扇区是512字节
恒定角速度好处在于每块数据可以由磁道和扇区直接定位,缺点在于浪费了外圈的存储空间。
多区记录,将磁盘表面划分为多个同心的区域,一般为16个,每个外层区域比内层区域的扇区数大约多出4%(《现代操作系统》第三版)。
在一个区域内,每个磁道的扇区数是一样的。
因此这种方法的优点在于比较充分的利用了存储空间,代价是更复杂的旋转控制。
2.2影响磁盘读写速度的因素
寻道时间:
磁头移动到特定磁道所需要的时间
普通SATA硬盘在10ms左右。
服务器硬盘在2ms~4ms或更多
旋转延迟:
读取的扇区转到磁头下方所需要的时间
7200rpm,60000ms/7200=8.33ms,8.33ms/2=4.17ms
15000rpm,2ms
2.3磁盘的低级格式化
设定磁道、扇区。
柱面斜进
扇区交错
无交错单交错双交错
第3节磁盘I/O
3.1BIO结构
磁盘的读写并不是以数据块或磁盘扇区为单位的,而是以BIO结构为基本的读写单位。
一个BIO结构包含传输的起始扇区号、一个包含需要传输的内存页的数组(一般为属于同一个inode的多个页面),以及相关联的其它BIO实例。
对I/O读写是以请求队列来实现,一个请求结构可以包含多个BIO,多个请求连在一起形成队列。
为了减少磁盘寻道时间,减少磁头移动,并在一次旋转中尽可能多的进行读写,因此当一个新的BIO被提交过来,并不是放在队列的尾部,内核会分析这个BIO,并尝试将这个BIO合并进已有的请求中,如果无法合并,则将其放在尾部。
3.2前置合并(frontmerge)和后置合并(backmerge)
Linux的I/O调度器维护一个以每个请求最后一个扇区号为索引的哈希表,当有新的请求加入时,会检查这个新请求的起始扇区号是否与已有请求的最后扇区号相连,发现的后就执行合并,这就是后置合并。
若检查新请求的最后扇区号,看是否和已有请求的首扇区号相连,有的话就执行合并,这是前置合并。
在通常情况下,后置合并的可能性远远大于前置合并,因为读写通常都是前向的。
除非某用户进程倾向于从后向前读写数据,此时前置合并的几率才会高。
第4节调度算法
内核采用的各种用于调度和重排I/O的算法,称为I/O调度器。
I/O调度时,遵循电梯算法。
1)当向设备写入数据块或是从设备读出数据块时,请求都被安置在一个队列中等待完成.
2)每个块设备都有它自己的队列.
3)I/O调度程序负责维护这些队列的顺序,以更有效地利用介质.I/O调度程序将无序的I/O操作变为有序的I/O操作.
4)内核必须首先确定队列中一共有多少个请求,然后才开始进行调度.
4.1noop调度器
NOOP调度器维护一个先进先出(FIFO)的I/O队列,这个队列不能重排,但新提交的请求可以和现有请求执行后置合并。
noop调度器的设计思路是:
对I/O性能的优化已经被I/O路径上的某个层实现了,因此它的设计要尽可能简单,减少对CPU的消耗。
智能的SerialAttachedSCSI(SAS)RAID控制器和StorageAreaNetwork网络存储都具有I/O优化功能;固态磁盘没有机械装置,因此没有寻道和旋转延迟,它也不需要I/O调度优化;某些嵌入式系统I/O操作很少,也会采用NOOP调度器来减少CPU功耗。
NOOP调度器有利于写而不利于读。
因为写操作一般都是异步的,即写操作先写进写缓存中,然后定时将写缓存中的内容写进磁盘。
而读操作一般都是同步的,当数据不在缓存中,就要向磁盘发出读请求。
在这种情况下,到达调度器时,读操作是单个的请求,而写操作是多个,因此某些写操作更容易和前面的请求合并,而读操作一般只能乖乖排队。
4.2deadline调度器
4.2.1设计和实现
deadline调度器规定了一个期限,如果一个请求超过了这个期限,则它就会被调度。
这个期限是软限制,超过它,这个请求就会离开调度队列,进入到块设备的请求队列等待驱动去执行它,而不是这个请求立刻就实际发起。
deadline调度器考虑了批量执行、写饥饿、请求超时这三个因素。
deadline调度器下,一共有5个队列,一个是块设备的请求队列,调度器的任务是从其它四个队列中挑出一个请求放进块设备的请求队列。
读请求有两个队列,一个FIFO链表fifo_list,一个是按扇区编号排序的红黑树sort_list,写请求也是如此。
为了提高性能,定义了以静态常量fifo_batch,值为16,即一次传递给块设备的请求队列16个读或写请求(过去的实现是检查每一个请求是否和上一个请求的扇区号相连,不相连则中断传递),然后再做下一次选择。
请求的规则如下,
1首先判断是否已经传递过去16个请求,如果还没到16个,则从sort_list中按扇区号继续调出下一个请求。
注意,这16个请求必须是同方向的,即要么是读,要么是写。
2若这批请求已经到了16个,则要选择下一批传送的请求是读还是写。
deadline倾向于优先考虑读请求,因为发起应用程序发起读请求后会阻塞,而写一般都是异步写。
因此定义了全局静态常量writes_starved,值为2。
即调度两批读请求之后才会调度一批写请求。
因此这一步会检查writes_starved的值,若大于等于2,则决定下一批调度的是写请求。
3决定好是读还是写之后,就检查相应的fifo队列的队首:
3.1若这个请求已超时,则调度它(rq_a),下一个请求是sort_list队列中的扇区编号大于rq_a且与之相邻的请求。
3.2若这个请求未超时,则从上一批结束的位置(sort_list中的位置)继续向下调度。
3.3若这个请求未超时,但上一批请求已经到了sort_list的队尾,则调度fifo队首那个请求。
总结:
1deadline考虑了性能,体现在有一个按扇区号排序的sort_list队列,在其中一次提交16个连续的请求.
2deadline优先考虑了读请求,因为读请求会阻塞进程,而写一般不会。
3deadline通过设置writes_starved考虑了写饥饿
4deadline通过设置最后服务时间避免了请求被饿死。
读请求超时是500ms,写请求是5s。
不足(待验证):
在写请求与读请求调度切换时,有一次磁头的随机寻道。
在调度一批读请求之后,指针指向了读请求sort_list中下一个请求rq_r,然后调度一批写请求,写请求调度完成之后,另外一个指针指向了写请求的sort_list的下一个请求rq_w,再次调度读请求,若符合3.2中叙述的情况,则调度rq_r,而上批写请求中最后一个请求的扇区号与rq_r的扇区号没有任何关系,因此极有可能发生一次随机寻道。
从读请求切换至写请求也是如此。
解决办法是当符合3.2的情况时,考虑是否是发生了读写切换,如果是,则根据上一个请求的扇区号从新选择要调度的请求。
按提交时间排序
4.2.2调优参数
fifo_batch:
一批请求的最大数目,默认是16
front_merges:
是否执行扇区前置合并,默认为1,即执行
read_expire:
读请求的超时时间,默认为500ms
write_expire:
写请求的超时时间,默认为5s
writes_starved:
写请求队列的调度频率,默认是2。
即发送两批读请求之后,发送1批写请求。
4.3as
Anticipatoryscheduling是算法最复杂的I/O调度器,是2.6.0至2.6.18的默认调度器,在2.6.33中被删掉。
4.4cfq
从2.6.19内核开始,CFQ做为默认的I/O调度器。
CFQ试图将I/O带宽均匀分配给各个进程,但同时考虑了优先级、deadline和anticipatory。
CFQ适用于通用服务器、多媒体应用(video,audio)和桌面系统。
4.4.1简要原理和工作流程
1.进程的I/O优先级分为3类:
RT(real-time)、BE(best-effort)、Idle。
RT和BE各有8个等级,0的优先级最高、7的优先级最低。
具有RT优先级的请求优先被执行,没有RT之后才执行BE,没有BE之后才执行Idle。
同一优先级类的进程通过时间片长短来体现优先级的高低。
2.I/O请求被分为同步和异步两种。
3.异步队列是系统范围内的,即没有进程的区别,共有17个异步队列,分别对应3类17个优先级。
4.每一个进程的同步请求都单独成队列,每个进程的同步请求队列分为两列,分别是按扇区排序和fifo队列,当调度此进程I/O请求时,和deadline算法相同。
5.各个进程的同步请求队列被轮流调度,异步队列也在此轮转当中。
6.当一个进程的同步I/O请求被调度时,一次发送4个请求至驱动,这表明对磁盘的访问是独占的。
当4个请求已完成而时间片尚未用完,则继续调度此进程的请求。
若此进程没有更多的请求,则等待8ms(称为Idling),看此进程是否会产生新的请求。
若没有新请求产生则调度下一队列。
此算法借鉴了Anticipatoryscheduling调度器思想。
即基于局部性原理的考虑,同一个进程发出的I/O请求很有可能是按磁盘块号顺序的。
4个请求和8ms都是默认设置,可通过sys文件系统修改。
7.以下情况不做idling
a)队列已超时
b)idle优先级队列
c)异步队列
d)此队列和其它队列有closecooperator(请求合并)。
8.idle队列没有时间片,一次只发送一个请求至驱动。
9.当服务BE进程队列时,若出现具有RT优先级的进程,则被抢占。
即时间片未到,也不能发送下一批请求至驱动。
10.若进程启动时未指定I/O优先级,则I/O优先级会继承CPU优先级,即实时进程的I/O优先级也为RT,普通进程的I/O优先级为BE。
实时进程的CPU优先级为1~99,普通进程为100~139。
用户通过nice值设定实时和普通进程的优先级,-20~19经过转换变为1~139供内核使用。
nice值通过如下公式转换至I/O优先级:
(nice+20)/5。
4.4.2调优参数
back_seek_penalty:
反向寻道的代价。
即相对于正向寻道,反向寻道相同数目的扇区所需要花费的时间,是正向寻道所需时间的倍数。
此参数用于队列的扇区排序。
默认是2
back_seek_max:
反向寻道最大数值,默认是16M。
超过此值,则遵循电梯原则
fifo_expire_sync:
同deadline调度,fifo队列头的请求超时时间。
默认为125ms
fifo_expire_async:
同上。
默认为250ms
low_latency:
桌面模式,当打开时,会加速写操作的交互时间。
默认开启。
在服务器上可关闭它。
quantum:
一次从队列中发给驱动的请求数,默认为4
slice_sync:
同步请求队列优先级的基准时间片,即BE类优先级为4的队列的时间片,默认为100ms,每一级相差20ms
slice_async:
默认是40ms,每一级相差20ms
slice_idle:
队列已空,但时间片未用尽,等待下一个请求到达的时间。
默认为8ms。
此等待时间对于寻道时间长的块设备很有用,但对于raid来说,寻道时间并不重要,因此将此值设为0更有效率。
slice_async_rq:
基础值,用于确定一个发送给驱动的最大异步请求数目,当超过此数值,即使未用尽时间片,也认为它超时,开始调度下一个队列。
2*(base_rq+base_rq*(CFQ_PRIO_LISTS-1-cfqq->ioprio));
base_rq即slice_async_rq;CFQ_PRIO_LISTS为优先级个数,即为8;cfqq->ioprio为当前队列的优先级。
4.4.3ionice命令
ionice可以更改任务的类型和优先级,不过只有cfq调度程序可以用ionice.
有三个例子说明ionice的功能:
采用cfq的实时调度,优先级为7
ionice-c1-n7-ptimeddif=/dev/sda1f=/tmp/testbs=2Mcount=300&
采用缺省的磁盘I/O调度,优先级为3
ionice-c2-n3-ptimeddif=/dev/sda1f=/tmp/testbs=2Mcount=300&
采用空闲的磁盘调度,优先级为0
ionice-c3-n0-ptimeddif=/dev/sda1f=/tmp/testbs=2Mcount=300&
4.5I/O调度方法的查看与设置
1)查看当前系统的I/O调度方法:
cat/sys/block/sda/queue/scheduler
noopanticipatorydeadline[cfq]
2)临地更改I/O调度方法:
例如:
想更改到noop电梯调度算法:
echonoop>/sys/block/sda/queue/scheduler
3)想永久的更改I/O调度方法:
修改内核引导参数,加入elevator=调度程序名
vi/boot/grub/menu.lst
更改到如下内容:
kernel/boot/vmlinuz-2.6.18-8.el5roroot=LABEL=/elevator=deadlinerhgbquiet
4)I/O调度器是针对设备的,不同的磁盘可以设定不同的调度器。
5)调度器的参数设置:
/sys/block//queue/iosched
第5节I/O编程模型
5.1同步和异步
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,不做其它事。
异步的概念和同步相对。
当一个异步过程调用发出后,调用者并不等待结果,而是去做其它事。
实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
5.2阻塞和非阻塞
阻塞是指调用结果返回之前,当前进程或线程会被挂起。
非阻塞和阻塞的概念相对应,指在得到结果之前,该进程或线程不会被阻塞,而可以继续做其它事情。
5.3同步阻塞I/O
发出I/O读写请求,并等待结果。
由于I/O时间较长,内核会阻塞该进程,做上下文切换,调度其它进程来执行。
这是正常的、用的最多的I/O模型。
5.4同步非阻塞I/O
发出I/O请求,并要求立刻返回结果,如果结果没有返回(返回EAGAIN错误),则继续发请求,直至返回结果。
下面这个图是一个I/O的read请求,设置了非阻塞I/O后,read调用会立刻返回结果,刚开始肯定不会成功,反复的调用read,直至读出数据。
考虑一个telnet的例子,一个telnetd守护进程与一个客户端交互,通过read读取客户端的输入,同时还要等待新的客户端的连接。
这就需要多个I/O读取,它不能在任一个读取上阻塞,否则会影响另一个的读取。
因此它可以采用同步非阻塞的方法,设置非阻塞I/O,然后调用read,如果没有数据则立刻返回,过若干秒在去read一次。
这种形式的循环称作轮询(polling)
下面这段代码是write非阻塞I/O调用的一个实例,它从标准输入读500000字节,并试图将它们写到标准输出上。
该程序先将标准输出设置为非阻塞的,然后用for循环进行输出,每次写的结果都在标准错误输出上打印。
函数clr_fl类似于set_f1,但与set_f1的功能相反,它清除1个或多个标志位。
#include"apue.h"
#include
#include
charbuf[500000];
intmain(void)
{
intntowrite,nwrite;
char*ptr;
ntowrite=read(STDIN_FILENO,buf,sizeof(buf));
fprintf(stderr,"read%dbytes\n",ntowrite);
set_fl(STDOUT_FILENO,O_NONBLOCK);/*setnonblocking*/
ptr=buf;
while(ntowrite>0){
errno=0;
nwrite=write(STDOUT_FILENO,ptr,ntowrite);
fprintf(stderr,"nwrite=%d,errno=%d\n",nwrite,errno);
if(nwrite>0){
ptr+=nwrite;
ntowrite-=nwrite;
}
}
clr_fl(STDOUT_FILENO,O_NONBLOCK);/*clearnonblocking*/
exit(0);
}
程序说明:
若标准输出是普通文件,则wrIte只执行一次。
但是,若标准输出是终端,由于终端的限制,一次write调用不会写500000个字符,所以需要循环,写入成功时,每次write调用的返回值是的写成功字节数,有时则出错返回。
下面是在一个系统上运行程序的结果:
$a.outstderr.out
$catstderr.out
read100000bytes
nwrite=8192,errno=0
nwrite=8192,errno=0
nwrite=-1,errno=11这种错211次
⋯
nwrite=4096,errno=0
nwrite=-1,errno=11这种错658次
⋯
nwrite=4096,errno=0
nwrite=-1,errno=11这种错604次
⋯
⋯
nwrite=4096,errno=0
nwrite=-1,errno=11这种错1047次
⋯
nwrite=-1,errno=11这种错1046次
⋯
nwrite=4096,errno=0⋯等等
在该系统上,errno11是EAGAIN。
系统上的终端驱动程序总是一次接收4096或8192字节。
在另一个系统上,前三个wrIte返回2005,1822和1811,然后是96次出错返回,接着是返回1846等等。
每个wrIte能写多少字节依赖于系统。
这种I/O模型性能低下,在实际的的I/O编程中不会采用。
但在其它情况下有可能使用。
如进程A依赖进程B的结果,但进程B并不将结果返回给A,而将结果保存在其它位置上,那么A就反复读这个位置,直至取出数据。
应此,同步非阻塞这个概念并非只应用在I/O编程中,而是一种广义的编程模型。
5.5异步阻塞I/O
5.5.1select
在telnetd的例子中,先设置非阻塞I/O,然后等待客户端的连接。
客户端连接后,将连接文件描述符发送给select系统调用,此时进程阻塞在select系统调用上。
它与同步阻塞I/O的区别在于:
进程可以发起多个I/O请求,有读有写,select设置一个I/O集合(此集合的大小需要在调用select时设定好,以后不能增加,所以有局限性),进程依次将请求发给select的这个集合,当一个或多个I/O准备好了,select都会返回。
进程做一循环,检查是哪个I/O请求准备好了,然后调用相应的处理代码。
具体使用select编程的过程如下:
进程首先打开设备,取得文件描述符,然后通过FD_SET将这个描述符及I/O操作(读、写等)注册进一个fd_set结构(select中共有3个结构,分别是读、写、异常)。
当所有要做的I/O请求都已注册完毕,调用select,select会阻塞,当select返回时,进程循环检查是哪个I/O请求准备好了,并执行相应的代码。
在网络编程中,进程调用一个无限循环,循环起始调用select,在循环过程中,若有新的客户端,则注册进fd_set,下次循环select就会新老客户端一起检查。
若客户端超过事先设定的上限,则拒绝注册新的客户端。
异步阻塞I/O是对通知事件进行阻塞,而不是对I/O调用进行阻塞。
即当有通知到达时,一定有某个I/O是已经ready的。
若是单个I/O,则这种方式无意义,但当有多个I/O时,进程不会因为某一个I/O被阻塞而影响其它已经ready的I/O。
这种方式也称作I/O多路复用。
#include"csapp.h"
voidecho(intconnfd);
voidcommand(void);
intmain(intargc,char**argv)
{
intlistenfd,connfd,port;
socklen_tclientlen=sizeof(structsockaddr_in);
structsockaddr_inclientaddr;
fd_setread_set,ready_set;
if(argc!
=2){
fprintf(stderr,"usage:
%s\n",argv[0]);
exit(0);
}
port=atoi(argv[1]);
listenfd=Open_listenfd(port);//line:
conc:
select:
openlistenfd
FD_ZERO(&read_set);/*Clearreadset*///line:
conc:
select:
clearreadset
FD_SET(STDIN_FILENO,&read_set);/*Addstdintoreadset*///line:
conc:
select:
addstdin
FD_SET(listenfd,&read_set);/*Addlistenfdtoreadset*///line:
conc:
select:
a